mirror of
https://github.com/penpot/penpot.git
synced 2026-09-13 23:48:42 +00:00
🐛 Fix handler change to equal (#11612)
This commit is contained in:
parent
bda8459d89
commit
fc8c5a98de
@ -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]
|
||||
@ -946,7 +950,7 @@
|
||||
(ptk/data-event :layout/update {:ids [id]})))))
|
||||
|
||||
(defn- split-segments
|
||||
[_id {:keys [from-p to-p t]}]
|
||||
[id {:keys [from-p to-p t]}]
|
||||
(ptk/reify ::split-segments
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
@ -955,7 +959,9 @@
|
||||
(st/set-content (-> content
|
||||
(path/split-segments #{from-p to-p} t)
|
||||
(path/content)))
|
||||
(update-in (st/get-path-location state) path/update-geometry))))))
|
||||
(update-in (st/get-path-location state) path/update-geometry)
|
||||
;; The inserted command shifts the indices a handler id refers to.
|
||||
(update-in [:workspace-local :edit-path id] dissoc :edited-handler))))))
|
||||
|
||||
(defn create-node-at-position
|
||||
[params]
|
||||
|
||||
@ -180,6 +180,36 @@
|
||||
|
||||
: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 `edited-handler` when it is one of this node's two handlers, then its
|
||||
primary handle. `edited-handler` is the last handler edited anywhere in the
|
||||
path, so a node only gets this hint while it holds the latest edit."
|
||||
[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,117 @@
|
||||
(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])))))
|
||||
|
||||
(t/deftest making-handlers-equal-ignores-an-ambiguous-handler-selection
|
||||
(let [id (random-uuid)
|
||||
content (aligned-uneven-handlers-content)
|
||||
mk (fn [] (pth/selectable-path-state
|
||||
id content
|
||||
{:nodes #{} :segments #{} :handlers #{[1 :c2] [2 :c1]}}))
|
||||
equal (fn [state]
|
||||
(-> (ptk/update (path.tools/set-handler-type :mirror) state)
|
||||
(path.state/get-path :content)))]
|
||||
(t/testing "with both handlers selected the last edited one wins"
|
||||
(let [result (equal (-> (mk)
|
||||
(assoc-in [:workspace-local :edit-path id :edited-handler]
|
||||
[2 :c1])))]
|
||||
(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/testing "with both handlers selected and no hint the incoming one wins"
|
||||
(let [result (equal (mk))]
|
||||
(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 inserting-a-node-forgets-the-last-edited-handler
|
||||
(let [id (random-uuid)
|
||||
content (aligned-uneven-handlers-content)
|
||||
state (-> (pth/selectable-path-state
|
||||
id content {:nodes #{} :segments #{} :handlers #{}})
|
||||
(assoc-in [:workspace-local :edit-path id :edited-handler] [2 :c1]))
|
||||
events (let [out (atom [])]
|
||||
(->> (ptk/watch (path.edition/create-node-at-position
|
||||
{:from-p (gpt/point 0 0)
|
||||
:to-p (gpt/point 10 0)
|
||||
:t 0.5})
|
||||
state (rx/subject))
|
||||
(rx/subs! #(swap! out conj %)))
|
||||
@out)
|
||||
state' (ptk/update (first events) state)]
|
||||
;; The new command shifts every later index, so [2 :c1] is another node now.
|
||||
(t/is (= 4 (count (path.state/get-path state' :content))))
|
||||
(t/is (nil? (get-in state' [:workspace-local :edit-path id :edited-handler])))))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user