From 8d3516d06dd121150016f9d03cbf1d53ddc27c1e Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Fri, 29 May 2026 09:32:07 +0200 Subject: [PATCH] :bug: Fix path export crop when stroke has arrow/marker caps --- common/src/app/common/geom/shapes/bounds.cljc | 25 +++++++- common/src/app/common/types/shape.cljc | 5 +- common/src/app/common/types/stroke.cljc | 3 + .../geom_shapes_strokes_test.cljc | 62 +++++++++++++++++++ 4 files changed, 90 insertions(+), 5 deletions(-) diff --git a/common/src/app/common/geom/shapes/bounds.cljc b/common/src/app/common/geom/shapes/bounds.cljc index 96e40651fb..3da3a1eef8 100644 --- a/common/src/app/common/geom/shapes/bounds.cljc +++ b/common/src/app/common/geom/shapes/bounds.cljc @@ -11,7 +11,8 @@ [app.common.files.helpers :as cfh] [app.common.geom.rect :as grc] [app.common.math :as mth] - [app.common.types.path :as path])) + [app.common.types.path :as path] + [app.common.types.stroke :as cts])) (defn shape-stroke-margin [shape stroke-width] @@ -105,6 +106,19 @@ (grc/points->rect))] (get-rect-filter-bounds srect filters blur-value ignore-shadow-margin?))))) +(def ^:private stroke-margin-multiplier 4.25) + +(defn- stroke-cap-marker-margin + [strokes open-path?] + (if open-path? + (->> strokes + (filter (fn [s] + (or (cts/stroke-caps-marker (:stroke-cap-start s)) + (cts/stroke-caps-marker (:stroke-cap-end s))))) + (map #(* stroke-margin-multiplier (:stroke-width % 0))) + (reduce d/max 0)) + 0)) + (defn calculate-padding ([shape] (calculate-padding shape false false)) @@ -127,6 +141,11 @@ 0 (shape-stroke-margin shape stroke-width)) + stroke-cap-margin + (if ignore-margin? + 0 + (stroke-cap-marker-margin strokes open-path?)) + shadow-width (->> (:shadow shape) (remove :hidden) @@ -149,8 +168,8 @@ shadow-width (if ignore-shadow-margin? 0 shadow-width)] - {:horizontal (mth/ceil (+ stroke-margin shadow-width)) - :vertical (mth/ceil (+ stroke-margin shadow-height))}))) + {:horizontal (mth/ceil (+ stroke-margin stroke-cap-margin shadow-width)) + :vertical (mth/ceil (+ stroke-margin stroke-cap-margin shadow-height))}))) (defn- add-padding [bounds padding] diff --git a/common/src/app/common/types/shape.cljc b/common/src/app/common/types/shape.cljc index a631644389..c3275b421f 100644 --- a/common/src/app/common/types/shape.cljc +++ b/common/src/app/common/types/shape.cljc @@ -30,6 +30,7 @@ [app.common.types.shape.layout :as ctsl] [app.common.types.shape.shadow :as ctss] [app.common.types.shape.text :as ctsx] + [app.common.types.stroke :as stroke] [app.common.types.text :as txt] [app.common.types.token :as cto] [app.common.types.variant :as ctv] @@ -61,8 +62,8 @@ (map->Shape attrs)) :clj (map->Shape attrs))) -(def stroke-caps-line #{:round :square}) -(def stroke-caps-marker #{:line-arrow :triangle-arrow :square-marker :circle-marker :diamond-marker}) +(def stroke-caps-line stroke/stroke-caps-line) +(def stroke-caps-marker stroke/stroke-caps-marker) (def stroke-caps (conj (set/union stroke-caps-line stroke-caps-marker) nil)) (def shape-types diff --git a/common/src/app/common/types/stroke.cljc b/common/src/app/common/types/stroke.cljc index 5bd2719491..a3a84f6921 100644 --- a/common/src/app/common/types/stroke.cljc +++ b/common/src/app/common/types/stroke.cljc @@ -12,6 +12,9 @@ ;; SCHEMAS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(def stroke-caps-line #{:round :square}) +(def stroke-caps-marker #{:line-arrow :triangle-arrow :square-marker :circle-marker :diamond-marker}) + (def default-stroke {:stroke-alignment :inner :stroke-style :solid diff --git a/common/test/common_tests/geom_shapes_strokes_test.cljc b/common/test/common_tests/geom_shapes_strokes_test.cljc index 140691aafc..1993add116 100644 --- a/common/test/common_tests/geom_shapes_strokes_test.cljc +++ b/common/test/common_tests/geom_shapes_strokes_test.cljc @@ -6,9 +6,71 @@ (ns common-tests.geom-shapes-strokes-test (:require + [app.common.geom.shapes.bounds :as gsb] [app.common.geom.shapes.strokes :as gss] + [app.common.types.path :as path] + [app.common.types.shape :as cts] [clojure.test :as t])) +(defn- make-open-path-with-stroke-cap + [cap-type stroke-width] + (cts/setup-shape + {:type :path + :content (path/content [{:command :move-to :params {:x 0 :y 100}} + {:command :curve-to :params {:x 200 :y 100 :c1x 0 :c1y -50 :c2x 200 :c2y -50}}]) + :strokes [{:stroke-color "#FF0000" + :stroke-width stroke-width + :stroke-alignment :center + :stroke-cap-start cap-type + :stroke-cap-end cap-type}]})) + +(t/deftest stroke-cap-marker-padding-test + (t/testing "Path with triangle-arrow caps should have enough padding to contain markers" + ;; For triangle-arrow with strokeWidth=3: + ;; markerHeight=8.5, viewBox 0 0 3 6, scale=min(8.5/3,8.5/6)*3=4.25 + ;; lateral extent = 3 * 4.25 = 12.75 user units per side + ;; So padding should be > 12 user units + (let [shape (make-open-path-with-stroke-cap :triangle-arrow 3) + padding (gsb/calculate-padding shape)] + (t/is (> (:horizontal padding) 12) + "Horizontal padding should accommodate triangle-arrow marker lateral extent") + (t/is (> (:vertical padding) 12) + "Vertical padding should accommodate triangle-arrow marker lateral extent"))) + + (t/testing "Path with line-arrow caps should have enough padding to contain markers" + (let [shape (make-open-path-with-stroke-cap :line-arrow 3) + padding (gsb/calculate-padding shape)] + (t/is (> (:horizontal padding) 12) + "Horizontal padding should accommodate line-arrow marker lateral extent") + (t/is (> (:vertical padding) 12) + "Vertical padding should accommodate line-arrow marker lateral extent"))) + + (t/testing "Path with diamond-marker caps should have enough padding" + ;; diamond-marker: markerWidth=6, viewBox 0 0 6 6, scale=min(6/6,6/6)*3=3 + ;; lateral extent = 3 * 3 = 9 user units per side + (let [shape (make-open-path-with-stroke-cap :diamond-marker 3) + padding (gsb/calculate-padding shape)] + (t/is (> (:horizontal padding) 8) + "Horizontal padding should accommodate diamond-marker lateral extent") + (t/is (> (:vertical padding) 8) + "Vertical padding should accommodate diamond-marker lateral extent"))) + + (t/testing "Closed path with marker caps should not have extra marker padding" + (let [shape (cts/setup-shape + {:type :path + :content (path/content [{:command :move-to :params {:x 0 :y 0}} + {:command :line-to :params {:x 100 :y 0}} + {:command :close-path}]) + :strokes [{:stroke-color "#FF0000" + :stroke-width 3 + :stroke-alignment :center + :stroke-cap-start :triangle-arrow + :stroke-cap-end :triangle-arrow}]}) + padding (gsb/calculate-padding shape)] + ;; Closed path: marker caps don't apply, so padding stays small + (t/is (<= (:horizontal padding) 5) + "Closed path should not have extra marker padding")))) + (t/deftest update-stroke-width-test (t/testing "Scale a stroke by 2" (let [stroke {:stroke-width 4 :stroke-color "#000"}