mirror of
https://github.com/penpot/penpot.git
synced 2026-10-29 22:10:34 +00:00
✨ Turn a swapped copy head with the child it replaces
A swapped copy head followed the position, constraints and layout item attributes of the main child it replaces, but not its rotation or flips. When the main rotated or flipped that child, the copies that kept it turned and the swapped ones did not. Sync, and a variant switch that carries a swap, now give the head and its subtree the flips and then the rotation of that child, around the head's own center, unless the user changed the head's geometry. The swap itself uses the same step. Benchmark (lower quantile, ms; baseline / now / ratio): deepest-edit-through-three-levels 35.2 / 34.1 / 0.97 edit-through-a-chain-of-dependents 68.9 / 75.5 / 1.10 library-update-into-copies 83.1 / 81.3 / 0.98 main-edit-into-10-copies 1.8 / 1.6 / 0.88 main-edit-into-100-copies 15.4 / 15.6 / 1.01 main-edit-into-1000-copies 153.1 / 153.2 / 1.00 variant-switch-into-copies 32.1 / 31.4 / 0.98 AI-assisted-by: claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6b7101bfd1
commit
4735c0a535
@ -22,7 +22,7 @@ Intended semantics of copies, `:touched` and component sync. This is the spec: w
|
||||
- Edit on a copy shape (authored): sets the attr's group (text content: `:content-group` + diff subgroups), clears `:remote-synced`. Descendants moved/resized only because of it stay untouched.
|
||||
- Rename: an explicit rename touches `:name-group` on root or child. Automatic renames (text name following its content) are derived. Untouched names sync from the near main; a touched root name on update-main renames the component.
|
||||
- Edit on a main: no touched change on the main; triggers sync.
|
||||
- Sync main→copy: every untouched group gets the near main's value (geometry: expected geometry); touched groups keep their values; never sets or clears `:touched`. Main child added → added clean to copies. Main child deleted → copy child removed, overrides or not. Main reorder → copies follow. A swapped sub-head pairs with the main child it replaces (its `:shape-ref`) and takes only that child's placement (position, constraints, `layout-item-*`); everything else syncs from its own component's main. A main child deleted → its swapped copies go with it. A swap made in the main reaches copies without `:swap-group` and spares those with it.
|
||||
- Sync main→copy: every untouched group gets the near main's value (geometry: expected geometry); touched groups keep their values; never sets or clears `:touched`. Main child added → added clean to copies. Main child deleted → copy child removed, overrides or not. Main reorder → copies follow. A swapped sub-head pairs with the main child it replaces (its `:shape-ref`) and takes only that child's placement (position, rotation, flips, constraints, `layout-item-*`); everything else syncs from its own component's main. A main child deleted → its swapped copies go with it. A swap made in the main reaches copies without `:swap-group` and spares those with it.
|
||||
- Sync cascade is eager: one sync runs to a fixpoint in memory (copies of A, including the A copy in B's main → copies of B → …) and commits once in the triggering op's undo group. The file is never left partly synced. Syncing only the components reported as changed equals a full-file sync.
|
||||
- Reset: the selected head's subtree becomes equal to its near main (expected geometry); all its touched groups cleared, `:swap-group` included: a swapped head turns back into the near main's component in place, keeping its id. Nothing outside that subtree changes.
|
||||
- Update main (from a top copy head): the copy's differing values go to the main; the copy's touched cleared; other copies then sync and keep their own overrides. A swapped child replaces the main child it stands for, and neither keeps `:swap-group`.
|
||||
|
||||
@ -115,6 +115,7 @@
|
||||
(declare update-flex-child-main-attrs)
|
||||
(declare update-flex-child-copy-attrs)
|
||||
(declare update-swapped-placement)
|
||||
(declare orient-like)
|
||||
(declare reposition-shape)
|
||||
(declare make-change)
|
||||
|
||||
@ -2420,9 +2421,10 @@
|
||||
(defn generate-take-placement
|
||||
"Give the swapped copy head `shape-inst` the placement of `placed`, the
|
||||
shape whose position it takes, expressed in the same coordinates: the
|
||||
attributes that place it in its parent (position, constraints and layout
|
||||
item attributes) that the user did not change. Its content comes from its
|
||||
own component. With `layout-placed?`, the position is left to the layout.
|
||||
attributes that place it in its parent (position, rotation, flips,
|
||||
constraints and layout item attributes) that the user did not change. Its
|
||||
content comes from its own component. With `layout-placed?`, the position
|
||||
is left to the layout.
|
||||
`objects` holds the subtree of `shape-inst`, which moves with it."
|
||||
[changes shape-inst placed objects layout-placed?]
|
||||
(let [touched (get shape-inst :touched #{})
|
||||
@ -2444,7 +2446,9 @@
|
||||
delta (gpt/subtract (position placed) (position shape-inst))
|
||||
move? (and (untouched? :x)
|
||||
(not layout-placed?)
|
||||
(not (gpt/almost-zero? delta)))]
|
||||
(not (gpt/almost-zero? delta)))
|
||||
orient (when (untouched? :rotation)
|
||||
(orient-like shape-inst placed (gsh/shape->center shape-inst)))]
|
||||
(cond-> changes
|
||||
(seq placement)
|
||||
(pcb/update-shapes [(:id shape-inst)]
|
||||
@ -2457,9 +2461,11 @@
|
||||
placement))
|
||||
{:ignore-touched true})
|
||||
|
||||
move?
|
||||
(or move? (some? orient))
|
||||
(pcb/update-shapes (cfh/get-children-ids-with-self objects (:id shape-inst))
|
||||
#(gsh/move % delta)
|
||||
#(cond-> %
|
||||
(some? orient) (orient)
|
||||
move? (gsh/move delta))
|
||||
{:ignore-touched true}))))
|
||||
|
||||
(defn- update-swapped-placement
|
||||
@ -2768,6 +2774,28 @@
|
||||
(pcb/with-objects (:objects container))
|
||||
(generate-detach-instance container libraries id))))
|
||||
|
||||
(defn- orient-like
|
||||
"A function that turns each shape of the subtree of `head` around `center`,
|
||||
so that `head` takes the flips and then the rotation of `target`; nil when
|
||||
they already agree."
|
||||
[head target center]
|
||||
(let [flip-x? (not= (true? (:flip-x head)) (true? (:flip-x target)))
|
||||
flip-y? (not= (true? (:flip-y head)) (true? (:flip-y target)))
|
||||
flip (when (or flip-x? flip-y?)
|
||||
(ctm/resize-modifiers (gpt/point (if flip-x? -1 1) (if flip-y? -1 1)) center))
|
||||
head' (cond-> head (some? flip) (gsh/transform-shape flip))
|
||||
angle (- (d/nilv (:rotation target) 0) (d/nilv (:rotation head') 0))
|
||||
rotate? (not (mth/close? (mod angle 360) 0 0.01))
|
||||
angle (if (> angle 180) (- angle 360) angle)]
|
||||
(when (or (some? flip) rotate?)
|
||||
(fn [shape]
|
||||
(cond-> shape
|
||||
(some? flip)
|
||||
(gsh/transform-shape flip)
|
||||
|
||||
rotate?
|
||||
(as-> shape (gsh/transform-shape shape (ctm/rotation-modifiers shape center angle))))))))
|
||||
|
||||
(defn- added-subtree
|
||||
"The shapes the changes add under the shape `id`, itself included."
|
||||
[changes id]
|
||||
@ -2784,25 +2812,12 @@
|
||||
of the shape it replaces, around the head's own center. They come from
|
||||
its parent, as when the parent is rotated, and are not overrides."
|
||||
[changes new-shape shape]
|
||||
(let [angle (d/nilv (:rotation shape) 0)
|
||||
flip-x? (true? (:flip-x shape))
|
||||
flip-y? (true? (:flip-y shape))]
|
||||
(if (and (mth/almost-zero? angle) (not flip-x?) (not flip-y?))
|
||||
changes
|
||||
(let [center (gsh/shape->center new-shape)
|
||||
flip (when (or flip-x? flip-y?)
|
||||
(ctm/resize-modifiers (gpt/point (if flip-x? -1 1) (if flip-y? -1 1)) center))
|
||||
orient (fn [shape]
|
||||
(cond-> shape
|
||||
(some? flip)
|
||||
(gsh/transform-shape flip)
|
||||
|
||||
(not (mth/almost-zero? angle))
|
||||
(as-> shape (gsh/transform-shape shape (ctm/rotation-modifiers shape center angle)))))]
|
||||
(pcb/update-shapes changes
|
||||
(map :id (added-subtree changes (:id new-shape)))
|
||||
orient
|
||||
{:ignore-touched true})))))
|
||||
(if-let [orient (orient-like new-shape shape (gsh/shape->center new-shape))]
|
||||
(pcb/update-shapes changes
|
||||
(map :id (added-subtree changes (:id new-shape)))
|
||||
orient
|
||||
{:ignore-touched true})
|
||||
changes))
|
||||
|
||||
(defn- main-ref-ids
|
||||
"Map the shapes of a main subtree to the shapes of a copy of it: for each
|
||||
|
||||
@ -583,3 +583,39 @@
|
||||
child (ths/get-shape-by-id file' (first (:shapes copy)))]
|
||||
(t/is (== 30 (mth/precision (:rotation copy) 2)))
|
||||
(t/is (== 30 (mth/precision (:rotation child) 2)))))
|
||||
|
||||
(t/deftest swapped-head-follows-the-rotation-of-the-replaced-child
|
||||
(let [file (-> (setup-sized)
|
||||
(tho/swap-component-in-shape :copy-star :heart))
|
||||
file' (-> file
|
||||
(transform-subtree :card-star #(ctm/rotation-modifiers %1 %2 30))
|
||||
(ths/update-shape :card-main :opacity 0.9)
|
||||
(propagate :card)
|
||||
(thf/validate-file!))
|
||||
head (ths/get-shape file' :copy-star)]
|
||||
(t/is (== 30 (:rotation (orientation file' :card-star))))
|
||||
(t/is (== 30 (:rotation (orientation file' :copy-star))))
|
||||
(t/is (== 30 (mth/precision (:rotation (ths/get-shape-by-id file' (first (:shapes head)))) 2)))
|
||||
(t/is (ctk/swapped? head))
|
||||
(t/is (not (ctk/touched-group? head :geometry-group)))))
|
||||
|
||||
(t/deftest swapped-head-follows-the-flip-of-the-replaced-child
|
||||
(let [file' (-> (setup-sized)
|
||||
(tho/swap-component-in-shape :copy-star :heart)
|
||||
(transform-subtree :card-star (fn [_ center] (ctm/resize-modifiers (gpt/point -1 1) center)))
|
||||
(ths/update-shape :card-main :opacity 0.9)
|
||||
(propagate :card)
|
||||
(thf/validate-file!))]
|
||||
(t/is (:flip-x (orientation file' :card-star)))
|
||||
(t/is (:flip-x (orientation file' :copy-star)))))
|
||||
|
||||
(t/deftest rotated-swapped-head-keeps-its-rotation
|
||||
;; the user rotated the swapped head: the main's rotation does not reach it
|
||||
(let [file' (-> (setup-sized)
|
||||
(tho/swap-component-in-shape :copy-star :heart)
|
||||
(transform-subtree :copy-star #(ctm/rotation-modifiers %1 %2 10))
|
||||
(transform-subtree :card-star #(ctm/rotation-modifiers %1 %2 30))
|
||||
(ths/update-shape :card-main :opacity 0.9)
|
||||
(propagate :card)
|
||||
(thf/validate-file!))]
|
||||
(t/is (== 10 (:rotation (orientation file' :copy-star))))))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user