From 2b8344d3a87d49dacea097e223877e064ee8af5a Mon Sep 17 00:00:00 2001 From: Alonso Torres Date: Mon, 28 Sep 2026 09:06:47 +0200 Subject: [PATCH] :bug: Fix delete on curve handlers (#11896) --- .../app/main/data/workspace/path/tools.cljs | 42 ++++++++++++++----- .../frontend_tests/logic/path_tools_test.cljs | 29 +++++++++++++ 2 files changed, 60 insertions(+), 11 deletions(-) diff --git a/frontend/src/app/main/data/workspace/path/tools.cljs b/frontend/src/app/main/data/workspace/path/tools.cljs index 1ab572d7bf..fe25a9e13e 100644 --- a/frontend/src/app/main/data/workspace/path/tools.cljs +++ b/frontend/src/app/main/data/workspace/path/tools.cljs @@ -229,20 +229,35 @@ (update-path-content state (path/toggle-segment-curve content index)) state))))) -(defn remove-handler - "Collapses one handler onto its node, which becomes independent." - [index prefix] - (ptk/reify ::remove-handler +(defn remove-handlers + "Collapses handlers `[index prefix]` onto their nodes, which become + independent." + [handlers] + (ptk/reify ::remove-handlers ptk/UpdateEvent (update [_ state] (let [id (st/get-path-id state) content (st/get-path state :content)] - (if (some? content) - (-> (update-path-content state (path/collapse-handler content index prefix)) - (update-in [:workspace-local :edit-path id :handler-types] - dissoc (helpers/handler-node-index index prefix))) + (if (and (some? content) (seq handlers)) + (let [new-content (reduce (fn [content [index prefix]] + (path/collapse-handler content index prefix)) + content + handlers) + nodes (map (fn [[index prefix]] + (helpers/handler-node-index index prefix)) + handlers)] + (-> (update-path-content state new-content) + (update-in [:workspace-local :edit-path id :handler-types] + #(apply dissoc % nodes)) + (update-in [:workspace-local :edit-path id :selection :handlers] + #(reduce disj (or % #{}) handlers)))) state))))) +(defn remove-handler + "Collapses one handler onto its node, which becomes independent." + [index prefix] + (remove-handlers #{[index prefix]})) + (defn merge-nodes [] (process-path-tool path/merge-nodes)) @@ -287,7 +302,8 @@ (path/separate-nodes content points offset))))) (defn delete-selected - "Heals selected nodes or opens selected segments." + "Heals selected nodes, opens selected segments or collapses selected + handlers." [] (ptk/reify ::delete-selected ptk/WatchEvent @@ -296,17 +312,21 @@ content (st/get-path state :content) selection (st/get-selection state id) nodes (get selection :nodes #{}) - segments (get selection :segments #{})] + segments (get selection :segments #{}) + handlers (get selection :handlers #{})] (rx/of (cond ;; Node selection takes priority in mixed selections. (seq nodes) (process-path-tool (helpers/node-positions content nodes) path/remove-nodes) - ;; Segment-only selection opens the path. + ;; Segment selection opens the path. (seq segments) (separate-nodes) + (seq handlers) + (remove-handlers handlers) + :else (remove-node))))))) diff --git a/frontend/test/frontend_tests/logic/path_tools_test.cljs b/frontend/test/frontend_tests/logic/path_tools_test.cljs index fc5aa33785..0509c991cb 100644 --- a/frontend/test/frontend_tests/logic/path_tools_test.cljs +++ b/frontend/test/frontend_tests/logic/path_tools_test.cljs @@ -517,6 +517,35 @@ (t/is (< (nodes mixed) 4)) (t/is (= 1 (move-tos mixed)))))) +(t/deftest delete-selected-collapses-selected-handlers + (let [id (random-uuid) + content (pth/selectable-path-content) + run (fn [handlers] + (let [state (pth/selectable-path-state + id content {:nodes #{} :segments #{} :handlers handlers}) + events (atom [])] + (->> (ptk/watch (path.tools/delete-selected) state nil) + (rx/subs! #(swap! events conj %))) + (ptk/update (first @events) state))) + content-of (fn [st] (vec (get-in st [:workspace-drawing :object :content]))) + selection-of (fn [st] (get-in st [:workspace-local :edit-path id :selection]))] + + (t/testing "single handler collapses onto its node" + (let [state' (run #{[2 :c1]}) + content' (content-of state')] + (t/is (= (gpt/point 10 0) (path/get-handler-point content' 2 :c1))) + (t/is (= (gpt/point 18 0) (path/get-handler-point content' 2 :c2))) + (t/is (= (gpt/point 8 0) (path/get-handler-point content' 1 :c2))) + (t/is (empty? (:handlers (selection-of state')))))) + + (t/testing "multiple handlers collapse and flat curves become lines" + (let [state' (run #{[1 :c1] [1 :c2] [2 :c1]}) + content' (content-of state')] + (t/is (= [:move-to :line-to :curve-to] (mapv :command content'))) + (t/is (= (gpt/point 10 0) (path/get-handler-point content' 2 :c1))) + (t/is (= (gpt/point 18 0) (path/get-handler-point content' 2 :c2))) + (t/is (empty? (:handlers (selection-of state')))))))) + (t/deftest deleting-a-closed-seam-node-heals-its-adjacent-segments (let [id (random-uuid) content (path/content