🐛 Fix path export crop when stroke has arrow/marker caps

This commit is contained in:
alonso.torres 2026-05-29 09:32:07 +02:00 committed by Alonso Torres
parent 9911ff7959
commit 8d3516d06d
4 changed files with 90 additions and 5 deletions

View File

@ -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]

View File

@ -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

View File

@ -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

View File

@ -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"}