mirror of
https://github.com/penpot/penpot.git
synced 2026-11-01 07:20:32 +00:00
🐛 Keep inherited placement untouched during a plain swap
Compare retained orientation with the position source in copy-parent coordinates, including cached deleted mains. A rotation inherited from that source does not author a geometry override (F154). The focused regression fails before the change. Swap and variant suites pass 100 tests and 510 assertions; common lint and format pass. Benchmark ratios: mains 0.47/0.53/0.55; levels 0.51; chain 0.64; library 0.64; many shapes 0.96; switch 0.68, with no regression flags. AI-assisted-by: GPT-6
This commit is contained in:
parent
84a0e6a94c
commit
6e1ee1fe9a
@ -29,7 +29,8 @@ Intended semantics of copies, `:touched` and component sync. This is the spec: w
|
||||
- Detach: the subtree loses component links, `:touched`, `:remote-synced`. First-level nested heads stay copies, move one nesting level up and inherit the near main's touched groups, so values their old near main overrode become overrides. A swapped head left with no enclosing copy becomes a plain copy of its own component (`:shape-ref` = its main root, no `:swap-group`).
|
||||
- Instantiate / nest a copy: the new subtree has no touched groups.
|
||||
- Duplicate a copy: the new subtree keeps the source's values and touched; if its nesting level changes, touched is inherited as in detach. Paste and move out of a copy follow the same rule.
|
||||
- Swap without keep-touched: the new subtree keeps `swap-keep-attrs` values; the new head keeps the old shape's id. A retained rotation or flip that differs from the new component's expected placement becomes a `:geometry-group` override so later sync preserves it, including orientation inherited from the old component. The swap authors that choice. Size-only overrides clear, and parent-derived orientation stays untouched. Other content overrides clear. Inside a copy it keeps the old `:shape-ref` and gets `:swap-group` (none when the new component is the near main's); inside a main it is a plain nested copy.
|
||||
- Swap without keep-touched: the new subtree keeps `swap-keep-attrs` values; the new head keeps the old shape's id. A retained rotation or flip that differs from the new component's expected placement becomes a `:geometry-group` override so later sync preserves it, including orientation inherited from the old component. The swap authors that choice. Size-only overrides clear. Orientation inherited from the position source or
|
||||
the parent stays untouched, including references into deleted mains (F154). Other content overrides clear. Inside a copy it keeps the old `:shape-ref` and gets `:swap-group` (none when the new component is the near main's); inside a main it is a plain nested copy.
|
||||
- Variant switch with keep-touched: an attr is carried from the old copy only when its group is touched and the old and new mains hold the same value for it. Resulting touched = groups of carried attrs (+ `:swap-group` when the head is inside a copy). The root also keeps its authored rotation or flip as a geometry override when swap placement retains it (F146). A value equal to the new main is not carried and not touched. Texts: the same per text group (letters carried only when both mains have the same letters; a style group when both mains agree on it once placed on each other's letters).
|
||||
- Delete component: copies keep their data. The deleted main cannot be edited, so it starts no sync, but changes from components nested in it still pass through: sync updates its stored shapes (`:objects` of the deleted component), its copies, and the components that nest it. Restore brings back an up-to-date main.
|
||||
- Undo/redo: restores the exact prior state, `:touched` and `:remote-synced` included, of the op and the sync it caused together (same undo group, one step). Undo never needs a re-sync.
|
||||
|
||||
@ -324,16 +324,20 @@
|
||||
(defn- keep-orientation-override
|
||||
"Retained orientation that differs from the new component's expected
|
||||
placement needs a geometry override to survive later sync."
|
||||
[changes shape baseline page ldata component-id]
|
||||
[changes shape baseline page ldata component-id position]
|
||||
(let [parent (get-in page [:objects (:parent-id shape)])
|
||||
main (ctf/get-component-root ldata (ctkl/get-component ldata component-id))
|
||||
main-parent (get-in (ctf/get-component-container ldata (ctkl/get-component ldata component-id))
|
||||
[:objects (:parent-id main)])
|
||||
parent-delta (if (:shape-ref parent) 0
|
||||
baseline (or position baseline)
|
||||
main-parent (if position
|
||||
(get-in (meta position) [:container :objects (:parent-id position)])
|
||||
main-parent)
|
||||
parent-delta (if (and (:shape-ref parent) (nil? position)) 0
|
||||
(- (or (:rotation parent) 0) (or (:rotation main-parent) 0)))
|
||||
rotation (mod (+ (or (:rotation baseline) 0) parent-delta) 360)
|
||||
flip (fn [key]
|
||||
(if (:shape-ref parent)
|
||||
(if (and (:shape-ref parent) (nil? position))
|
||||
(boolean (get baseline key))
|
||||
(odd? (count (filter identity [(get baseline key) (get parent key)
|
||||
(get main-parent key)])))))
|
||||
@ -426,7 +430,7 @@
|
||||
(ctk/touched-group? % :geometry-group)) orig-touched)
|
||||
(keep-orientation-override original-shape new-shape page
|
||||
(or (get-in libraries [(:component-file new-shape) :data]) ldata)
|
||||
(:component-id new-shape)))]
|
||||
(:component-id new-shape) nil))]
|
||||
[changes (mapv (comp :parent-id :new) pairs)]))
|
||||
|
||||
(defn- shape-index
|
||||
@ -460,7 +464,10 @@
|
||||
[changes parents-of-swapped]
|
||||
(if keep-touched?
|
||||
(generate-keep-touched changes new-shape shape orig-shapes page libraries ldata)
|
||||
[(keep-orientation-override changes shape new-shape page ldata component-id) []])
|
||||
[(keep-orientation-override changes shape new-shape page ldata component-id
|
||||
(when (:shape-ref parent)
|
||||
(ctf/find-ref-shape nil page libraries shape
|
||||
:include-deleted? true :with-context? true))) []])
|
||||
|
||||
;; Replacement keeps the head id and its cell. Cleanup during the
|
||||
;; temporary deletion must not change the parent's placement data.
|
||||
|
||||
@ -797,3 +797,11 @@
|
||||
(t/is (= (:shapes main) (mapv #(get-in objects [% :shape-ref]) (:shapes head))))
|
||||
(t/is (= (thn/normalize-file (tm/file initial))
|
||||
(thn/normalize-file (tm/file (r/step detached (n/undo))))))))
|
||||
|
||||
(t/deftest plain-swap-does-not-author-the-positions-inherited-orientation
|
||||
(let [rotated (-> (tm/make-situation (setup-sized))
|
||||
(r/step (n/rotate :card-star 45)) r/settle)
|
||||
swapped (-> rotated (r/step (n/swap-shape :copy-star :heart)) r/settle)
|
||||
head (tm/resolve-shape swapped :copy-star)]
|
||||
(t/is (mth/close? 45 (:rotation head) 0.001))
|
||||
(t/is (= #{:swap-group} (:touched head)))))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user