Compare commits

...
1 Commits
Author SHA1 Message Date
alonso.torres 7861631058 🐛 Fix handler change to equal 2026-09-10 12:31:29 +02:00
4 changed files with 121 additions and 7 deletions

No files matched your search

@@ -163,6 +163,7 @@
(-> state
(assoc-in [:workspace-local :edit-path id :content-modifiers] modifiers)
(assoc-in [:workspace-local :edit-path id :moving-handler] moving-handler)
(assoc-in [:workspace-local :edit-path id :edited-handler] primary)
(cond-> (some? new-prev-handler)
(assoc-in [:workspace-local :edit-path id :prev-handler] new-prev-handler)))))))
@@ -286,7 +287,10 @@
content-modifiers))]
(-> state
(assoc-in [:workspace-local :edit-path id :content-modifiers] content-modifiers))))))
(assoc-in [:workspace-local :edit-path id :content-modifiers] content-modifiers)
(cond-> (= 1 (count handler-ids))
(assoc-in [:workspace-local :edit-path id :edited-handler]
(first handler-ids))))))))
(defn- move-node-indices
[state node-indices from-point to-point]
@@ -180,6 +180,34 @@
:else nil)))
(defn node-handler-ids
"Returns a node's curve handlers, its primary handle first."
[content node-index]
(if-let [[index prefix :as primary] (node-primary-handler content node-index)]
(let [[op-idx op-prefix] (path/opposite-index content index prefix)]
(if (some? op-idx)
[primary [op-idx op-prefix]]
[primary]))
[]))
(defn handler-type-reference
"Returns the handler that keeps its geometry when a node's handler type changes.
The other handler adapts to it. Priority: the node's only selected handler,
then its most recently edited handler, then its primary handle."
[content selection edited-handler node-index]
(let [handler-ids (node-handler-ids content node-index)
selected (filterv (get selection :handlers #{}) handler-ids)]
(cond
(= 1 (count selected))
(first selected)
(some #{edited-handler} handler-ids)
edited-handler
:else
(first handler-ids))))
(defn handlers-equal-length?
"True when a node's two handlers are the same distance from the node."
[content index prefix]
@@ -49,7 +49,8 @@
(update-in [:workspace-local :edit-path id :selection]
#(helpers/remap-selection % old-content new-content))
(update-in [:workspace-local :edit-path id :handler-types]
#(helpers/remap-handler-types % old-content new-content))))
#(helpers/remap-handler-types % old-content new-content))
(update-in [:workspace-local :edit-path id] dissoc :edited-handler)))
state)))
ptk/WatchEvent
@@ -80,9 +81,11 @@
(reduce path/make-curve-point content))))))
(defn- apply-handler-type-modifiers
"Returns modifiers that reshape a node's handlers to `type`."
[content node-index type]
(if-let [[idx prefix] (helpers/node-primary-handler content node-index)]
"Returns modifiers that reshape a node's handlers to `type`.
`reference` keeps its geometry; the opposite handler adapts to it."
[content reference type]
(if-let [[idx prefix] reference]
(case type
:mirror (helpers/move-handler-modifiers content idx prefix true true true 0 0)
:aligned (helpers/align-handler-modifiers content idx prefix 0 0)
@@ -98,10 +101,13 @@
(let [id (st/get-path-id state)
content (st/get-path state :content)
selection (st/get-selection state id)
edited (dm/get-in state [:workspace-local :edit-path id :edited-handler])
nodes (helpers/handler-target-nodes content selection)]
(if (and (some? content) (seq nodes))
(let [modifiers (reduce (fn [acc node-index]
(d/deep-merge acc (apply-handler-type-modifiers content node-index type)))
(let [reference (helpers/handler-type-reference
content selection edited node-index)]
(d/deep-merge acc (apply-handler-type-modifiers content reference type))))
{} nodes)
new-content (path/apply-content-modifiers content modifiers)]
(-> (st/set-content state new-content)
@@ -146,7 +152,8 @@
(update-in [:workspace-local :edit-path id :selection]
#(helpers/remap-selection % old-content new-content))
(update-in [:workspace-local :edit-path id :handler-types]
#(helpers/remap-handler-types % old-content new-content)))))
#(helpers/remap-handler-types % old-content new-content))
(update-in [:workspace-local :edit-path id] dissoc :edited-handler))))
(defn remove-segments
"Removes segments and opens the path at their endpoints."
@@ -802,3 +802,78 @@
(t/is (empty? (emit-of (mk {:nodes #{3} :segments #{} :handlers #{}})))))))
;; Path-local undo and redo events use a seeded local stack.
;; --- Handler type changes pick which handler keeps its geometry
(defn- aligned-uneven-handlers-content
"Returns content whose node (10,0) has aligned handlers at (8,0) and (16,0)."
[]
(path/content
[{:command :move-to :params {:x 0 :y 0}}
{:command :curve-to
:params {:c1x 2 :c1y 0 :c2x 8 :c2y 0 :x 10 :y 0}}
{:command :curve-to
:params {:c1x 16 :c1y 0 :c2x 28 :c2y 0 :x 30 :y 0}}]))
(t/deftest making-handlers-equal-keeps-the-selected-handler-length
(let [id (random-uuid)
content (aligned-uneven-handlers-content)
state (pth/selectable-path-state
id content {:nodes #{} :segments #{} :handlers #{[2 :c1]}})
result (-> (ptk/update (path.tools/set-handler-type :mirror) state)
(path.state/get-path :content))]
;; The node is aligned with handlers of unequal length.
(t/is (= :aligned (path.helpers/derive-handler-type content 1)))
;; The selected handler keeps its length and the opposite one adapts.
(t/is (= (gpt/point 16 0) (path/get-handler-point result 2 :c1)))
(t/is (= (gpt/point 4 0) (path/get-handler-point result 1 :c2)))))
(t/deftest making-handlers-equal-falls-back-to-the-last-edited-handler
(let [id (random-uuid)
content (aligned-uneven-handlers-content)
state (-> (pth/selectable-path-state
id content {:nodes #{1} :segments #{} :handlers #{}})
(assoc-in [:workspace-local :edit-path id :edited-handler]
[2 :c1]))
result (-> (ptk/update (path.tools/set-handler-type :mirror) state)
(path.state/get-path :content))]
(t/is (= (gpt/point 16 0) (path/get-handler-point result 2 :c1)))
(t/is (= (gpt/point 4 0) (path/get-handler-point result 1 :c2)))))
(t/deftest making-handlers-equal-uses-the-incoming-handler-with-no-hint
(let [id (random-uuid)
content (aligned-uneven-handlers-content)
state (pth/selectable-path-state
id content {:nodes #{1} :segments #{} :handlers #{}})
result (-> (ptk/update (path.tools/set-handler-type :mirror) state)
(path.state/get-path :content))]
(t/is (= (gpt/point 8 0) (path/get-handler-point result 1 :c2)))
(t/is (= (gpt/point 12 0) (path/get-handler-point result 2 :c1)))))
(t/deftest aligning-handlers-takes-the-angle-of-the-selected-handler
(let [id (random-uuid)
content (path/content
[{:command :move-to :params {:x 0 :y 0}}
{:command :curve-to
:params {:c1x 2 :c1y 0 :c2x 10 :c2y -3 :x 10 :y 0}}
{:command :curve-to
:params {:c1x 16 :c1y 0 :c2x 28 :c2y 0 :x 30 :y 0}}])
state (pth/selectable-path-state
id content {:nodes #{} :segments #{} :handlers #{[2 :c1]}})
result (-> (ptk/update (path.tools/set-handler-type :aligned) state)
(path.state/get-path :content))]
;; The selected handler stays put; the opposite one rotates onto its axis
;; keeping its own length.
(t/is (= (gpt/point 16 0) (path/get-handler-point result 2 :c1)))
(t/is (= (gpt/point 7 0) (path/get-handler-point result 1 :c2)))))
(t/deftest dragging-a-handler-records-it-as-the-last-edited-one
(let [id (random-uuid)
content (aligned-uneven-handlers-content)
state (pth/selectable-path-state
id content {:nodes #{} :segments #{} :handlers #{[2 :c1]}})
state' (ptk/update
(path.edition/modify-selected-handlers id [2 :c1] {} 3 0 :independent false)
state)]
(t/is (= [2 :c1]
(get-in state' [:workspace-local :edit-path id :edited-handler])))))