🐛 Keep text names and measured sizes derived

The name a text takes from its content and the size it is measured to are
derived: they never touch a copy (decisions (d) and (g), Phase 3c).

- Let a copy text's automatic name follow its own letters.

  The editors name a text after its letters on every edit. In a copy that
  rename went through the same write as the content, so it touched
  :name-group (F27), against decision (d) of the component contract: only an
  explicit rename is an override.

  - update-shapes takes :ignore-touched as a set of attributes too, and the
    editors' commits (v1 and v3, finalize and keystrokes) and the replace of
    text pass #{:name}: the name follows the content, so its write is
    derived. Token unapply still runs for such an update.
  - Sync gives an untouched copy text the name generated from the letters it
    shows after the sync when its main's name is the one generated from the
    main's letters (decision (d), refined), and the main's name otherwise.
    So a copy that retyped its text keeps that name, and one that did not
    follows the main's.

  The pure runner's text edit makes the same commit. Case Y no longer pins
  F27 and checks the copy's name follows its letters; both golden masters
  record the copy without :name-group.

  text-sync-test: an untouched copy takes the main's letters and name, a
  copy that typed its letters keeps the name they give, and a main named by
  hand passes its name on.

- Name a text with several paragraphs without line breaks.

  The editors name a text after its letters, with the line breaks of its
  paragraphs included, so a text "Hello world" + Enter + "Ends Sunday" was
  named "Hello world\nEnds Sunday"; the layers panel showed a space there
  (F82). generate-shape-name now turns each line break, with the spaces
  around it, into one space.

  text-test: the name of a two-paragraph text joins them with a space.

- Keep the size a WASM text grows to from touching the copy.

  A text that grows with its content (auto width, auto height) gets its size
  from the WASM renderer after a font loads, a style changes, a paste or an
  edit ends. That size is derived from the content, yet the resize applied
  it as a user change, so a copy's text took :geometry-group each time and
  kept that size against its main (F1). The resize and the debounced resize,
  the size an edit ends with and its geometry undo now pass ignore-touched.

  These paths measure the text in WASM, so no headless test reaches them.
  Checked in the browser with the WASM renderer: an edit of a copy's text
  touches :content-group only, and a resize after a font size change of 24
  grows the text with no :geometry-group.

- Keep the finalize resize of a text out of the undo stack.

  When an edit of an existing text ends, the WASM renderer gives the size it
  grows to, and the finalize commit stores that size along with its undo.
  The resize that shows it on the canvas recorded an undo of its own with no
  undo group: a release build kept a second undo entry for one edit, and a
  dev build failed the undo entry check (F36). It now records none; a new
  text still joins the undo group of its creation.

  Checked in the browser with the WASM renderer: an edit of an existing text
  adds one undo entry.

- Keep the size a text edit ends with from touching the copy.

  When an edit of a text ends, the editor stores its content together with
  the size measured for it (the SVG renderer's text modifier, or the WASM
  measure in the v1 editor). Only the name was written as derived, so an
  edit of a copy's text touched :geometry-group besides :content-group, and
  the text kept its size against its main (F1, in the SVG renderer). These
  writes now take the size, the position data and the name as derived; the
  content still touches.

  workspace-text-modifier-test: an edit that ends with a measured width
  touches the copy text's content only. Checked in the browser with the SVG
  renderer: an edit of a copy's text touches :content-group and
  :text-content-text only.

- Measure again on WASM the texts a sync writes.

  The size of a text that grows with its content is derived (decision (g) of
  the component contract): sync writes the main's, and the renderer measures
  the text again. The SVG renderer does so when it draws the text; the WASM
  renderer did not, so a copy text with content of its own kept its main's
  width and height after a sync (F35). Now, with the WASM renderer, a sync
  of the current file measures again the auto-grow texts of the current page
  it writes, without a component sync of its own, in the undo group of the
  sync; a sync with no undo group gets one for both.

  Texts on other pages keep the size sync wrote until they are measured
  again.

  workspace-sync-wasm-text-test: a sync that writes a copy text with content
  of its own measures it again, in the undo group of the sync. Checked in
  the browser with the WASM renderer: after the main text grows or shrinks,
  two copy texts with content of their own get their own widths and no
  :geometry-group.

Benchmark (lower quantile, ms; baseline / now / ratio):
  deepest-edit-through-three-levels    35.2 /  18.1 / 0.51
  edit-through-a-chain-of-dependents   68.9 /  34.0 / 0.49
  library-update-into-copies           83.1 /  40.2 / 0.48
  main-edit-among-many-shapes           4.8 /   2.5 / 0.51
  main-edit-into-10-copies              1.8 /   0.8 / 0.44
  main-edit-into-100-copies            15.4 /   8.5 / 0.55
  main-edit-into-1000-copies          153.1 /  83.5 / 0.55
  variant-switch-into-copies           32.1 /  18.1 / 0.56

AI-assisted-by: claude-opus-5-5
This commit is contained in:
alonso.torres 2026-10-15 15:11:34 +02:00
parent b42a84d28e
commit 290df3930d
16 changed files with 292 additions and 31 deletions

View File

@ -585,7 +585,8 @@
(defn update-shapes
"Calculate the changes and undos to be done when a function is applied to a
single object"
single object. `ignore-touched` is a boolean, or the set of attributes whose
change on a copy is derived and touches nothing."
([changes ids update-fn]
(update-shapes changes ids update-fn nil))
@ -612,7 +613,9 @@
changed?
(conj {:type :set :attr attr :val new-val
:ignore-geometry ignore-geometry?
:ignore-touched ignore-touched}))
:ignore-touched (if (set? ignore-touched)
(contains? ignore-touched attr)
ignore-touched)}))
uops
(cond-> uops

View File

@ -971,6 +971,29 @@
[[] '()]
updatable-attrs))
(defn- automatic-name
"The name the editors give a text after its `content`, or nil when it has
no letters."
[content]
(let [name (txt/generate-shape-name (txt/content->text content))]
(when (not= name "") name)))
(defn- synced-text-name
"The name an untouched copy text takes from `origin-shape`, its main: the
main's own name, or, when that one follows the main's letters, the name
that follows the letters the copy shows after the sync."
[dest-shape origin-shape touched omit-touched?]
(let [origin-name (:name origin-shape)]
(if (and (some? origin-name)
(= origin-name (automatic-name (:content origin-shape))))
(let [content (if (and omit-touched? (touched :content-group))
(txt/merge-overrides (:content dest-shape)
(:content origin-shape)
(txt/text-overrides touched))
(:content origin-shape))]
(or (automatic-name content) origin-name))
origin-name)))
(defn- sync-attr-operations
"The `[attr value]` pairs that sync `attr` from `origin-shape`, a main
shape placed like `dest-shape`, into `dest-shape`, its copy. With
@ -1003,8 +1026,15 @@
(= :content attr)
(touched sync-group))
;; the name of a text may follow its letters
text-name?
(and (cfh/text-shape? origin-shape)
(cfh/text-shape? dest-shape)
(= :name attr))
skip?
(or (= (get origin-shape attr) (get dest-shape attr))
(or (and (not text-name?)
(= (get origin-shape attr) (get dest-shape attr)))
(and (touched sync-group)
omit-touched?
;; When it is a text-partial-change, we should generate operations
@ -1025,6 +1055,9 @@
(:content origin-shape)
(txt/text-overrides touched))
text-name?
(synced-text-name dest-shape origin-shape touched omit-touched?)
:else
(get origin-shape attr)))]

View File

@ -106,7 +106,7 @@
(-> (pcb/update-shapes grid-ids ctl/assign-cell-positions {:with-objects? true})
(pcb/reorder-grid-children ids))
(not ignore-touched)
(not (true? ignore-touched))
(generate-unapply-tokens objects changed-sub-attr changed-item-index))]
changes))

View File

@ -37,7 +37,7 @@
W — one undo node undoes several steps over nested cascades.
X — an undo committed before the edit's sync leaves copies unchanged
(frontend only).
AJ — one undo after a nested sync reverts only the reflow (F43, frontend
AJ — one undo after a nested sync reverts the change and its reflow (frontend
only).
Y — a text override merges with a main text change per subgroup.
Z — a move of a main child reaches the copies (frontend only).
@ -85,6 +85,7 @@
[app.common.types.components-list :as ctkl]
[app.common.types.shape-tree :as ctst]
[app.common.types.shape.layout :as ctl]
[app.common.types.text :as txt]
[app.common.uuid :as uuid]
[clojure.test :as t]
[clojure.walk :as walk]))
@ -653,7 +654,8 @@
the same for the font size. An edited copy has `:content-group` and the
group of its edit touched; an unedited one has nothing touched. The text starts with
the name generated from its content, so an edit of its characters renames
it, an automatic rename that must not touch `:name-group` (decision (d))."
it, an automatic rename that must not touch `:name-group` (decision (d)),
and the copy's name keeps following the letters it shows."
[]
(let [copy-edit (tm/one-of [(n/edit-text :copy-instance :text "copy text")
(n/edit-text :copy-instance :font-size "32")])
@ -667,6 +669,8 @@
main (setup/main-instance s)
edited (tm/get-choice s copy-edit)]
(t/is (n/has-property-of (tm/get-choice s main-edit) main))
(t/is (= (txt/generate-shape-name (txt/content->text (:content copy)))
(:name copy)))
(doseq [edit [:text :font-size]]
(t/is (= (if (= edit (:edit edited))
(:value edited)
@ -677,13 +681,6 @@
(nil? edited)
(t/is (empty? (:touched copy)))
;; the automatic rename touches the name
(= :text (:edit edited))
(tm/known-failure "F27"
#{:content-group :text-content-text}
#{:content-group :text-content-text :name-group}
(:touched copy))
:else
(t/is (= #{:content-group (subgroup (:edit edited))}
(:touched copy))))))}))

View File

@ -254,11 +254,13 @@
(let [the-file (tm/file situation)
shape-id (tm/target-shape-id situation target)
page (thf/current-page the-file)
;; as the editor's commit, the name that follows the content is a
;; derived write
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{shape-id}
#(edit-text-content % edit value)
(:objects page)
{})
{:ignore-touched #{:name}})
file' (thf/apply-changes the-file changes)]
(-> situation
(tm/with-file file')

View File

@ -242,8 +242,11 @@
(update :content transform))))
(defn generate-shape-name
"The name of a text whose letters are `text`: its first 280 characters,
each line break a space."
[text]
(subs text 0 (min 280 (count text))))
(let [text (str/replace text #"\s*\n\s*" " ")]
(subs text 0 (min 280 (count text)))))
(defn- compare-text-attr
"Compare two attribute values and return true if they are different.

View File

@ -185,8 +185,8 @@
{:transcript "create-component {:component \"main\", :id sync-main-component}\n make-nested-component {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n instantiate-copy {:component \"main\", :copy sync-main-copyhead-0}\n change-property {:target #fn, :property :fills, :value \"#0000ff\"}\n change-property {:target #fn, :property :fills, :value \"#00ff00\"}\n change-property {:target #fn, :property :fills, :value \"#aaaaaa\"}\n undo {:undone 6}", :states ["1de51816cdc0ad11" "ed2117b3a6ae1dd6" "c90876e1b44d00fb" "a5b1e4b00086c891" "f09bed0fa67d711c"]}
{:transcript "create-component {:component \"main\", :id sync-main-component}\n make-nested-component {:component \"main\", :level 1, :outer sync-main-outercomp-1}\n make-nested-component {:component \"main\", :level 1, :outer sync-main-outercomp-1}\n instantiate-copy {:component \"main\", :copy sync-main-copyhead-0}\n change-property {:target #fn, :property :fills, :value \"#0000ff\"}\n change-property {:target #fn, :property :fills, :value \"#00ff00\"}\n change-property {:target #fn, :property :fills, :value \"#aaaaaa\"}\n undo {:undone 7}", :states ["262f632dd4a0ac41" "af99191229bac7b4" "8ba7cefbe6e101d8" "ab0a68e1af334a44" "987979e4d37001b6"]}]
:y
[{:transcript "one-of -> recorded-choice\n edit-text {:target :copy-instance, :edit :text, :value \"copy text\"}\n edit-text {:target :main-instance, :edit :text, :value \"main text\"}", :states ["8cd35014bebff5a0" "260d9897b862470e" "01f96b240a9f7786"]}
{:transcript "one-of -> recorded-choice\n edit-text {:target :copy-instance, :edit :text, :value \"copy text\"}\n edit-text {:target :main-instance, :edit :font-size, :value \"24\"}", :states ["8cd35014bebff5a0" "9a2bfd72cc5a6898" "8df2236d989a127c"]}
[{:transcript "one-of -> recorded-choice\n edit-text {:target :copy-instance, :edit :text, :value \"copy text\"}\n edit-text {:target :main-instance, :edit :text, :value \"main text\"}", :states ["73a1f9665fb9d817" "c6b6d7c691dfcaa8" "728400277dd671c4"]}
{:transcript "one-of -> recorded-choice\n edit-text {:target :copy-instance, :edit :text, :value \"copy text\"}\n edit-text {:target :main-instance, :edit :font-size, :value \"24\"}", :states ["73a1f9665fb9d817" "85eac5dd891c03b4" "3eaf765ba42c63d1"]}
{:transcript "one-of -> recorded-choice\n edit-text {:target :copy-instance, :edit :font-size, :value \"32\"}\n edit-text {:target :main-instance, :edit :text, :value \"main text\"}", :states ["c0b1a5e708231296" "75e32d376c1957f9" "b1213922be7ac4cb"]}
{:transcript "one-of -> recorded-choice\n edit-text {:target :copy-instance, :edit :font-size, :value \"32\"}\n edit-text {:target :main-instance, :edit :font-size, :value \"24\"}", :states ["c0b1a5e708231296" "8eaab011168c2a4b" "14063d206179c049"]}
{:transcript "one-of -> skip\n edit-text {:target :main-instance, :edit :text, :value \"main text\"}", :states ["6c385dbccb923ae0" "f357affeab5963f4"]}

View File

@ -15,6 +15,7 @@
[app.common.test-helpers.files :as thf]
[app.common.test-helpers.ids-map :as thi]
[app.common.test-helpers.shapes :as ths]
[app.common.test-helpers.texts :as thtx]
[app.common.types.text :as txt]
[clojure.test :as t]))
@ -933,3 +934,47 @@
(t/is (= "copy text" (:text line)))
(t/is (= "32" (:font-size line)))
(t/is (= #{:content-group :text-content-text} (:touched copy-child')))))
(defn- type-letters
"The editor's commit of `text` (in the notation of `thtx`) into the text
`label`: the content, and the name that follows it, a derived write."
[file label text]
(let [page (thf/current-page file)
content (thtx/content text)
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(thi/id label)}
#(assoc % :content content
:name (txt/generate-shape-name (txt/content->text content)))
(:objects page)
{:ignore-touched #{:name}})]
(thf/apply-changes file changes)))
(defn- sync-component1
[file]
(thf/apply-changes file (cll/generate-sync-file-changes (pcb/empty-changes) nil :components
(:id file) (thi/id :component1) (:id file)
{(:id file) file} (:id file))))
(t/deftest the-automatic-name-of-a-copy-text-follows-its-letters
(let [file (-> (thf/sample-file :file1)
(thtx/add-text-component-with-copy [[["hello world"]]])
(type-letters :main-child [[["hello world"]]])
(sync-component1))]
(t/testing "an untouched copy takes the main's letters and name"
(let [file' (-> file (type-letters :main-child [[["main text"]]]) (sync-component1))]
(t/is (= "main text" (:name (ths/get-shape file' :copy-child))))
(t/is (nil? (:touched (ths/get-shape file' :copy-child))))))
(t/testing "a copy that typed its letters keeps the name they give"
(let [file' (-> file
(type-letters :copy-child [[["copy text"]]])
(type-letters :main-child [[["main text"]]])
(sync-component1))
copy (ths/get-shape file' :copy-child)]
(t/is (= "copy text" (:name copy)))
(t/is (= #{:content-group :text-content-text} (:touched copy)))))
(t/testing "a main named by hand passes its name on"
(let [file' (-> file
(ths/update-shape :main-child :name "Title")
(sync-component1))]
(t/is (= "Title" (:name (ths/get-shape file' :copy-child))))))))

View File

@ -368,3 +368,8 @@
(t/is (every? #(= % "14") original-font-sizes))
(t/is (every? #(= % "32") updated-font-sizes))))
(t/deftest the-name-of-a-text-with-several-paragraphs
(t/is (= "Hello world Ends Sunday" (cttx/generate-shape-name "Hello world\nEnds Sunday")))
(t/is (= "One Two" (cttx/generate-shape-name "One \n\n Two")))
(t/is (= 280 (count (cttx/generate-shape-name (apply str (repeat 300 "a")))))))

View File

@ -1070,13 +1070,33 @@
(assoc state :updating-library true)
(dissoc state :updating-library)))))
(defn- synced-text-ids
"The texts of the current page that grow with their content and that
`changes` write: their size comes from their main, so the WASM renderer
measures them again (the SVG renderer does when it draws them)."
[state changes]
(when (features/active-feature? state "render-wasm/v1")
(let [page-id (:current-page-id state)
objects (dsh/lookup-page-objects state page-id)]
(into []
(comp (filter #(and (= :mod-obj (:type %)) (= page-id (:page-id %))))
(map :id)
(distinct)
(filter #(let [shape (get objects %)]
(and (cfh/text-shape? shape)
(not= :fixed (:grow-type shape))))))
(:redo-changes changes)))))
(defn- sync-file-frontend-events
[file-id changes updated-frames undo-group]
[file-id changes updated-frames text-ids undo-group]
(rx/concat
(rx/of (set-updating-library false)
(ntf/hide {:tag :sync-dialog}))
(when (seq (:redo-changes changes))
(rx/of (dch/commit-changes changes)))
(when (seq text-ids)
(rx/of (dwwt/resize-wasm-text-all text-ids {:skip-component-sync? true
:undo-group undo-group})))
(when-not (empty? updated-frames)
(let [frames-by-page (group-by :page-id updated-frames)]
(rx/merge
@ -1159,9 +1179,17 @@
pending-ids (sync-file-pending-ids file-id changes)
text-ids (when (= file-id current-file-id)
(synced-text-ids state changes))
;; the texts measured again join the undo of the sync
undo-group (or undo-group
(when (seq text-ids) (uuid/next)))
changes (pcb/set-undo-group changes undo-group)
frontend-sync
(sync-file-frontend-events
file-id changes updated-frames undo-group)]
file-id changes updated-frames text-ids undo-group)]
(log/debug :msg "SYNC-FILE finished" :js/rchanges (log-changes
(:redo-changes changes)

View File

@ -297,6 +297,11 @@
(update :workspace-editor-state dissoc id)
(update :workspace-new-text-shapes disj id))))))
(def ^:private derived-attrs
"The attributes an edit of a text writes that follow its content: its name
and the size it grows to."
#{:name :selrect :points :x :y :width :height :transform :transform-inverse :position-data})
(defn finalize-editor-state
[id update-name?]
(ptk/reify ::finalize-editor-state
@ -332,7 +337,8 @@
(cond-> (some? new-size)
(gsh/transform-shape
(ctm/change-size shape (:width new-size) (:height new-size))))))
{:undo-group (when new-shape? id)})))))
{:undo-group (when new-shape? id)
:ignore-touched derived-attrs})))))
(let [content (d/merge (ted/export-content content)
(dissoc (:content shape) :children))
@ -355,7 +361,8 @@
(assoc :name name))
(cond-> (or (some? width) (some? height))
(gsh/transform-shape (ctm/change-size shape width height))))))
{:undo-group (when new-shape? id)}))))))
{:undo-group (when new-shape? id)
:ignore-touched derived-attrs}))))))
(when (some? id)
(rx/of (dws/deselect-shape id)
@ -1270,7 +1277,8 @@
final-shape-fn (fn [shape] (merge shape final-geom))]
(-> base
(pcb/with-objects objects-with-old)
(pcb/update-shapes [id] final-shape-fn {:attrs attrs}))))
;; the size a text grows to is derived: it touches nothing
(pcb/update-shapes [id] final-shape-fn {:attrs attrs :ignore-touched true}))))
(defn- build-finalize-commit-changes
"Builds the commit changes for text finalization (content + geometry undo).
@ -1286,7 +1294,8 @@
(pcb/with-objects objects)
(pcb/set-text-content id content original-content)
(cond-> (and update-name? (some? name) (not= (:name shape*) name))
(pcb/update-shapes [id] (fn [s] (assoc s :name name)) {:attrs [:name]}))
;; the name follows the content: a derived write
(pcb/update-shapes [id] (fn [s] (assoc s :name name)) {:attrs [:name] :ignore-touched true}))
(cond-> new-shape?
(-> (pcb/set-undo-group id)
(pcb/set-stack-undo? true))))
@ -1396,12 +1405,19 @@
(assoc :name name))))
{:save-undo? finalize-save-undo-first?
:stack-undo? effective-stack-undo?
:undo-group (when new-shape? id)})
:undo-group (when new-shape? id)
;; the name follows the content: a derived write
:ignore-touched #{:name}})
;; Push the auto-grow geometry to WASM/app state; the commit persists it via `resize-geom`.
;; Skipped for a no-op finalize: applying it would record an undo transaction.
;; An existing text's finalize commit holds the undo of that geometry, so this
;; one records none. The size a text grows to is derived: it touches nothing.
(when (and (some? resize-modifiers) (not finalize-no-op?))
(dwm/apply-wasm-modifiers resize-modifiers {:undo-group (when new-shape? id)})))
(dwm/apply-wasm-modifiers resize-modifiers
(if new-shape?
{:undo-group id :ignore-touched true}
{:save-undo? false :ignore-touched true}))))
(when finalize?
(rx/concat
@ -1457,7 +1473,8 @@
(assoc :name name))
(cond-> (or (some? width) (some? height))
(gsh/transform-shape (ctm/change-size shape width height))))))
{:undo-group (when new-shape? id)}))))))))
{:undo-group (when new-shape? id)
:ignore-touched derived-attrs}))))))))
(defn- editor-content-update
"Event saving `content` from the WASM editor, renaming the shape after its text."
@ -1862,7 +1879,8 @@
new-name (txt/generate-shape-name (txt/content->text new-content))]
(-> shape (assoc :content new-content) (assoc :name new-name)))
shape))
{:attrs #{:content :name} :undo-group undo-group})]
;; the name follows the content: a derived write
{:attrs #{:content :name} :undo-group undo-group :ignore-touched #{:name}})]
(rx/concat
(rx/of update-event)
(if (features/active-feature? state "render-wasm/v1")

View File

@ -92,7 +92,8 @@
(watch [_ state _]
(let [objects (dsh/lookup-page-objects state)
shape (get objects id)
apply-opts (or opts {})
;; the size a text grows to is derived: it touches nothing
apply-opts (assoc opts :ignore-touched true)
resize-stream
(if (and (some? shape)
(cfh/text-shape? shape)
@ -132,7 +133,8 @@
;; When undo-id is present, extend the current undo transaction instead of
;; creating a new one, and commit it after the resize (single undo action).
extend-tx? (some? undo-id)
apply-opts (cond-> {}
;; the size a text grows to is derived: it touches nothing
apply-opts (cond-> {:ignore-touched true}
(some? undo-group) (assoc :undo-group undo-group)
extend-tx? (assoc :undo-transation? false)
skip-component-sync? (assoc :skip-component-sync? true))]

View File

@ -207,8 +207,8 @@
:case-x-undo-before-sync-leaves-copies-unchanged
[{:transcript " {:target :main-instance, :property :fills, :value \"#ff0000\"}", :states ["734ec524d88eb6bb" "ff1fa863b88027e5"]}]
:case-y-text-override-merges-with-main-text-change
[{:transcript "one-of -> \n {:target :copy-instance, :edit :text, :value \"copy text\"}\n {:target :main-instance, :edit :text, :value \"main text\"}", :states ["1fdda10ff2de93bb" "9caff776f882850b" "cc39679f8f901c8e"]}
{:transcript "one-of -> \n {:target :copy-instance, :edit :text, :value \"copy text\"}\n {:target :main-instance, :edit :font-size, :value \"24\"}", :states ["1fdda10ff2de93bb" "b44573ca4817a1c2" "805af3e9523db814"]}
[{:transcript "one-of -> \n {:target :copy-instance, :edit :text, :value \"copy text\"}\n {:target :main-instance, :edit :text, :value \"main text\"}", :states ["7ea87003598e5626" "f823f8147e941701" "3a6ecb19a9315cb8"]}
{:transcript "one-of -> \n {:target :copy-instance, :edit :text, :value \"copy text\"}\n {:target :main-instance, :edit :font-size, :value \"24\"}", :states ["7ea87003598e5626" "bc06b54fa303bde9" "230d1c77384fc557"]}
{:transcript "one-of -> \n {:target :copy-instance, :edit :font-size, :value \"32\"}\n {:target :main-instance, :edit :text, :value \"main text\"}", :states ["c5ad2b1e98811951" "ed2ffe7c84c65ffa" "50166a4e5025f6cb"]}
{:transcript "one-of -> \n {:target :copy-instance, :edit :font-size, :value \"32\"}\n {:target :main-instance, :edit :font-size, :value \"24\"}", :states ["c5ad2b1e98811951" "c75ab24b318795d7" "8896671645bb0a65"]}
{:transcript "one-of -> \n {:target :main-instance, :edit :text, :value \"main text\"}", :states ["dd9f23b4112dddae" "3bd3720125090159"]}

View File

@ -0,0 +1,84 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns frontend-tests.data.workspace-sync-wasm-text-test
"With the WASM renderer, the size of a text that grows with its content
is measured again after a sync writes it, as the SVG renderer does when
it draws the text: a copy text with content of its own keeps its own
size."
(:require
[app.common.files.changes-builder :as pcb]
[app.common.geom.shapes :as gsh]
[app.common.logic.shapes :as cls]
[app.common.test-helpers.components :as cthc]
[app.common.test-helpers.compositions :as ctho]
[app.common.test-helpers.files :as cthf]
[app.common.test-helpers.ids-map :as cthi]
[app.common.test-helpers.texts :as ctht]
[app.common.types.modifiers :as ctm]
[app.main.data.workspace.libraries :as dwl]
[app.main.data.workspace.wasm-text :as dwwt]
[cljs.test :as t :include-macros true]
[frontend-tests.helpers.state :as ths]))
(def ^:private orig-resize-wasm-text-all dwwt/resize-wasm-text-all)
(def ^:private resize-calls (atom []))
(defn- mock-resize-wasm-text-all
([ids] (mock-resize-wasm-text-all ids nil))
([ids opts]
(swap! resize-calls conj {:ids (set ids) :opts opts})
nil))
(t/use-fixtures :each
{:before (fn []
(cthi/reset-idmap!)
(reset! resize-calls [])
(set! dwwt/resize-wasm-text-all mock-resize-wasm-text-all))
:after (fn []
(set! dwwt/resize-wasm-text-all orig-resize-wasm-text-all))})
(defn- resize
"Resize the shape `label` of `file` to `width`, as a measure of its text
would."
[file label width]
(let [page (cthf/current-page file)
changes (cls/generate-update-shapes
(pcb/empty-changes nil (:id page))
#{(cthi/id label)}
#(gsh/transform-shape % (ctm/change-size % width (:height %)))
(:objects page)
{:ignore-touched true})]
(cthf/apply-changes file changes)))
(defn- setup-file
"A component with a text and a copy whose text has content of its own;
the main text then grows."
[]
(-> (cthf/sample-file :file1)
(ctho/add-frame-with-text :main-root :main-text "hello")
(cthc/make-component :component1 :main-root)
(cthc/instantiate-component :component1 :copy-root
:children-labels [:copy-text])
(ctht/set-text :copy-text [[["a longer text of its own"]]])
(resize :main-text 200)))
(t/deftest a-sync-measures-again-the-copy-texts-it-writes
(t/async
done
(let [file (setup-file)
store (ths/setup-store file)
undo-group (random-uuid)]
(ths/run-store
store done
[(dwl/sync-file (:id file) (:id file) :components (cthi/id :component1) undo-group)]
(fn [_]
(t/is (= 1 (count @resize-calls)))
(let [{:keys [ids opts]} (first @resize-calls)]
(t/is (contains? ids (cthi/id :copy-text)))
(t/is (:skip-component-sync? opts))
(t/is (= undo-group (:undo-group opts)))))))))

View File

@ -0,0 +1,37 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns frontend-tests.data.workspace-text-modifier-test
"The size the SVG renderer measures for a text while it is edited, which
the edit stores when it ends: it follows the content, so it touches
nothing."
(:require
[app.common.test-helpers.composable.comp.setups :as setup]
[app.common.test-helpers.ids-map :as thi]
[app.common.test-helpers.shapes :as ths]
[app.common.test-helpers.texts :as tht]
[app.main.data.workspace.texts :as dwt]
[cljs.test :as t :include-macros true]
[frontend-tests.composable-tests.interpreter :as ftm]))
(t/use-fixtures :each ftm/fixture)
(defn- measure
"Stands in for the DOM measurement of the text `id`."
[id size]
(fn [state]
(assoc-in state [:workspace-text-modifier id] size)))
(t/deftest ^:async an-edit-of-a-copy-text-touches-its-content-only
(ftm/install! (setup/simple-text-component-with-copy))
(let [id (thi/id :copy-text)]
(await (ftm/await-step [(measure id {:width 140})
(dwt/v2-update-text-shape-content id (tht/content [[["hello there"]]]))]))
(let [text (ths/get-shape (ftm/current-file) :copy-text)]
(t/is (= [[["hello there"]]] (tht/text-of (ftm/current-file) :copy-text)))
(t/is (= 140 (:width text)))
(t/is (contains? (:touched text) :content-group))
(t/is (not (contains? (:touched text) :geometry-group))))))

View File

@ -46,6 +46,8 @@
[frontend-tests.data.workspace-reflow-test]
[frontend-tests.data.workspace-shortcuts-test]
[frontend-tests.data.workspace-sync-barrier-test]
[frontend-tests.data.workspace-sync-wasm-text-test]
[frontend-tests.data.workspace-text-modifier-test]
[frontend-tests.data.workspace-texts-test]
[frontend-tests.data.workspace-thumbnails-test]
[frontend-tests.data.workspace-undo-test]
@ -216,6 +218,7 @@
'frontend-tests.data.workspace-interactions-test
'frontend-tests.data.workspace-font-variants-test
'frontend-tests.data.workspace-frame-guides-undo-test
'frontend-tests.data.workspace-text-modifier-test
'frontend-tests.data.workspace-libraries-test
'frontend-tests.data.workspace-library-links-test
'frontend-tests.data.workspace-library-updates-test
@ -226,6 +229,7 @@
'frontend-tests.data.workspace-reflow-test
'frontend-tests.data.workspace-shortcuts-test
'frontend-tests.data.workspace-sync-barrier-test
'frontend-tests.data.workspace-sync-wasm-text-test
'frontend-tests.data.workspace-texts-test
'frontend-tests.data.workspace-thumbnails-test
'frontend-tests.data.workspace-undo-test