mirror of
https://github.com/penpot/penpot.git
synced 2026-10-29 05:50:35 +00:00
🐛 Keep authored root orientation as a switch override
A switch places the new root in the old orientation before carrying attributes. When rotation is already equal, the carry step emits no write and drops its geometry group. Preserve that group for retained authored root orientation so later sync keeps the visible result. Extend the rotation regression to check touched and later sync: two assertions fail before the fix. Focused checks pass 43 tests and 166 assertions, including the two previously shrunk generated regressions. Benchmark vs Phase 1e: main 10/100/1000 0.64/0.65/0.97; levels 0.49; chain 0.59; library 0.58; many shapes 0.89; switch 0.60. No flags. AI-assisted-by: GPT-6
This commit is contained in:
parent
fbb56e2916
commit
9f10e5e7c3
@ -30,7 +30,7 @@ Intended semantics of copies, `:touched` and component sync. This is the spec: w
|
||||
- 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.
|
||||
- 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). 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).
|
||||
- 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.
|
||||
|
||||
|
||||
@ -321,6 +321,31 @@
|
||||
(let [new-touched (ctf/get-touched-from-ref-chain-until-target-ref container libraries shape nil)]
|
||||
(assoc shape :touched (disj new-touched :swap-group)))))
|
||||
|
||||
(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]
|
||||
(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
|
||||
(- (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)
|
||||
(boolean (get baseline key))
|
||||
(odd? (count (filter identity [(get baseline key) (get parent key)
|
||||
(get main-parent key)])))))
|
||||
changed? (or (not (mth/close? (mod (or (:rotation shape) 0) 360) rotation 0.01))
|
||||
(not= (boolean (:flip-x shape)) (flip :flip-x))
|
||||
(not= (boolean (:flip-y shape)) (flip :flip-y)))]
|
||||
(cond-> changes
|
||||
changed?
|
||||
(pcb/update-shapes [(:id shape)]
|
||||
#(update % :touched (fnil conj #{}) :geometry-group)
|
||||
{:ignore-touched true}))))
|
||||
|
||||
(defn generate-keep-touched
|
||||
"This is used as part of the switch process, when you switch from
|
||||
an original-shape to a new-shape. It generate changes to
|
||||
@ -395,7 +420,12 @@
|
||||
(keep-swapped-item changes new old ldata page libraries)
|
||||
(clsw/update-attrs-on-switch changes new old new-shape original-shape old-ref container)))
|
||||
changes
|
||||
pairs)]
|
||||
pairs)
|
||||
changes (cond-> changes
|
||||
(ctk/touched-group? original-shape :geometry-group)
|
||||
(keep-orientation-override original-shape new-shape page
|
||||
(or (get-in libraries [(:component-file new-shape) :data]) ldata)
|
||||
(:component-id new-shape)))]
|
||||
[changes (mapv (comp :parent-id :new) pairs)]))
|
||||
|
||||
(defn- shape-index
|
||||
@ -405,31 +435,6 @@
|
||||
(when-let [parent (get objects parent-id)]
|
||||
(or (d/index-of (:shapes parent) shape-id) 0)))
|
||||
|
||||
(defn- keep-orientation-override
|
||||
"A plain swap keeps its old orientation. When that differs from the new
|
||||
component's expected placement, the swap authors a geometry override."
|
||||
[changes shape baseline page ldata component-id]
|
||||
(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
|
||||
(- (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)
|
||||
(boolean (get baseline key))
|
||||
(odd? (count (filter identity [(get baseline key) (get parent key)
|
||||
(get main-parent key)])))))
|
||||
changed? (or (not (mth/close? (mod (or (:rotation shape) 0) 360) rotation 0.01))
|
||||
(not= (boolean (:flip-x shape)) (flip :flip-x))
|
||||
(not= (boolean (:flip-y shape)) (flip :flip-y)))]
|
||||
(cond-> changes
|
||||
changed?
|
||||
(pcb/update-shapes [(:id shape)]
|
||||
#(update % :touched (fnil conj #{}) :geometry-group)
|
||||
{:ignore-touched true}))))
|
||||
|
||||
(defn generate-component-swap-in-place
|
||||
"Swap the copy head `shape` of `page` for a copy of the component
|
||||
`component-id` of library data `ldata`. The new head takes the old one's
|
||||
|
||||
@ -642,7 +642,10 @@
|
||||
copy (ths/get-shape file' :copy01)
|
||||
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/is (== 30 (mth/precision (:rotation child) 2)))
|
||||
(t/is (ctk/touched-group? copy :geometry-group))
|
||||
(t/is (= (orientation file' :copy01)
|
||||
(orientation (propagate file' :c02) :copy01)))))
|
||||
|
||||
(t/deftest switch-turns-the-swap-with-the-child-of-the-new-variant
|
||||
(let [file (-> (thf/sample-file :file1)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user