mirror of
https://github.com/penpot/penpot.git
synced 2026-10-28 21:40:34 +00:00
🎉 Expose per-side stroke width in the plugin API (#11994)
* ✨ Add stroke side-width helpers to common types Add set-width-to-all-sides, set-width-to-single-side and side-width to app.common.types.stroke, mirroring the border-radius helpers in app.common.types.shape.radius. They back the per-side stroke width API and are covered by unit tests. AI-assisted-by: deepseek-v4.1-flash * ✨ Round-trip per-side stroke widths through the plugin API Add strokeWidthTop/Right/Bottom/Left to format-stroke and parse-stroke so reading shape.strokes exposes the per-side widths and writing a stroke back preserves them instead of collapsing to the uniform width. parse-stroke prefers a live stroke proxy's hidden raw state when present, so a commit only rewrites the attributes the plugin touched. AI-assisted-by: deepseek-v4.1-flash * ✨ Add per-side stroke widths to the plugin API types Add the four per-side members to the Stroke interface and to the borderWidth and dimension token property lists, and refresh the generated API surface used by the plugin test suite. AI-assisted-by: deepseek-v4.1-flash * ✨ Expose per-side stroke width on the stroke proxy Add strokeWidthTop/Right/Bottom/Left to StrokeProxy. Reads fall back to the uniform strokeWidth; strokeWidth now writes all four sides, like borderRadius writes all four corners; each side setter writes only its own key and the top side also updates strokeWidth. Reject per-side writes on shapes other than boards and rectangles with a validation error naming the property, and expose the raw stroke map through a hidden proxy member so a commit preserves untouched attributes. AI-assisted-by: deepseek-v4.1-flash * ✨ Support stroke width tokens per side in the plugin API Token attribute expansion now accepts the uniform strokeWidth property and expands it to the four per-side attributes, mirroring the design tab. shape.tokens exposes the per-side keys and reports strokeWidth when all four sides share one token, and applyToken routes through the shared expander. AI-assisted-by: deepseek-v4.1-flash * 📚 Document per-side stroke width in the plugins changelog Note the new Stroke members, the restored strokeWidth token property and the shape-type validation in plugins/CHANGELOG.md. AI-assisted-by: deepseek-v4.1-flash * 🐛 Fix some minor issues --------- Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
This commit is contained in:
parent
9218448b29
commit
fad083314d
@ -25,11 +25,8 @@
|
||||
:stroke-width 1})
|
||||
|
||||
(defn materialize-stroke-side-widths
|
||||
"Given a stroke (or nil) and the set of edited side keys, returns the stroke
|
||||
with the four per-side width keys concretized: edited sides take `value`,
|
||||
the rest keep their current per-side width, falling back to `:stroke-width`
|
||||
(0 when `stroke` is nil). `:stroke-width` mirrors the top side so legacy
|
||||
consumers that only read it see the top value."
|
||||
"Sets edited widths and fills missing sides from `:stroke-width` (default 0).
|
||||
Keeps `:stroke-width` equal to the top width."
|
||||
[stroke edited-keys value]
|
||||
(let [base-width (or (:stroke-width stroke) 0)
|
||||
current (or stroke default-stroke)
|
||||
@ -42,3 +39,29 @@
|
||||
{}
|
||||
ctt/per-side-stroke-width-keys)]
|
||||
(merge current side-attrs {:stroke-width (get side-attrs :stroke-width-top)})))
|
||||
|
||||
(defn set-width-to-all-sides
|
||||
"Sets every side and `:stroke-width` to `value`."
|
||||
[stroke value]
|
||||
(materialize-stroke-side-widths stroke ctt/per-side-stroke-width-keys value))
|
||||
|
||||
(defn set-width-to-single-side
|
||||
"Sets one side and preserves the other widths.
|
||||
Keeps `:stroke-width` equal to the top width."
|
||||
[stroke attr value]
|
||||
(materialize-stroke-side-widths (or stroke default-stroke) #{attr} value))
|
||||
|
||||
(defn side-width
|
||||
"Returns the side width, falling back to `:stroke-width` when unset."
|
||||
[stroke attr]
|
||||
(d/nilv (get stroke attr) (:stroke-width stroke)))
|
||||
|
||||
(defn width-type
|
||||
"Returns simple when all effective side widths match, multiple otherwise."
|
||||
[stroke]
|
||||
(if (= (side-width stroke :stroke-width-top)
|
||||
(side-width stroke :stroke-width-right)
|
||||
(side-width stroke :stroke-width-bottom)
|
||||
(side-width stroke :stroke-width-left))
|
||||
:simple
|
||||
:multiple))
|
||||
|
||||
@ -72,3 +72,101 @@
|
||||
(t/is (= (:stroke-width-bottom stroke) 5))
|
||||
(t/is (= (:stroke-width-left stroke) 5))
|
||||
(t/is (= (:stroke-width stroke) 10)))))
|
||||
|
||||
;; --- set-width-to-all-sides
|
||||
|
||||
(t/deftest set-width-to-all-sides-uniform
|
||||
(t/testing "all four sides take the value and the alias mirrors the top"
|
||||
(let [stroke (cts/set-width-to-all-sides {:stroke-width 5 :stroke-color "#000000"} 8)]
|
||||
(t/is (= 8 (:stroke-width-top stroke)))
|
||||
(t/is (= 8 (:stroke-width-right stroke)))
|
||||
(t/is (= 8 (:stroke-width-bottom stroke)))
|
||||
(t/is (= 8 (:stroke-width-left stroke)))
|
||||
(t/is (= 8 (:stroke-width stroke)))
|
||||
(t/is (= "#000000" (:stroke-color stroke)))))
|
||||
|
||||
(t/testing "a nil stroke is materialized from the default stroke"
|
||||
(let [stroke (cts/set-width-to-all-sides nil 3)]
|
||||
(t/is (= 3 (:stroke-width stroke)))
|
||||
(t/is (= 3 (:stroke-width-top stroke)))
|
||||
(t/is (= 3 (:stroke-width-right stroke)))
|
||||
(t/is (= :solid (:stroke-style stroke))))))
|
||||
|
||||
;; --- set-width-to-single-side
|
||||
|
||||
(t/deftest set-width-to-single-side-non-top
|
||||
(t/testing "a non-top side only changes that side, keeping the alias on top"
|
||||
(let [stroke (cts/set-width-to-single-side
|
||||
{:stroke-width 5
|
||||
:stroke-width-top 5
|
||||
:stroke-width-right 5
|
||||
:stroke-width-bottom 6
|
||||
:stroke-width-left 7}
|
||||
:stroke-width-right
|
||||
9)]
|
||||
(t/is (= 5 (:stroke-width-top stroke)))
|
||||
(t/is (= 9 (:stroke-width-right stroke)))
|
||||
(t/is (= 6 (:stroke-width-bottom stroke)))
|
||||
(t/is (= 7 (:stroke-width-left stroke)))
|
||||
(t/is (= 5 (:stroke-width stroke))))))
|
||||
|
||||
(t/deftest set-width-to-single-side-top
|
||||
(t/testing "the top side also updates the :stroke-width alias"
|
||||
(let [stroke (cts/set-width-to-single-side {:stroke-width 5} :stroke-width-top 9)]
|
||||
(t/is (= 9 (:stroke-width-top stroke)))
|
||||
(t/is (= 9 (:stroke-width stroke)))))
|
||||
|
||||
(t/testing "a nil stroke is materialized from the default stroke"
|
||||
(let [stroke (cts/set-width-to-single-side nil :stroke-width-bottom 4)]
|
||||
(t/is (= 4 (:stroke-width-bottom stroke)))
|
||||
(t/is (= 1 (:stroke-width stroke)))
|
||||
(t/is (= :solid (:stroke-style stroke))))))
|
||||
|
||||
;; --- side-width
|
||||
|
||||
(t/deftest side-width-falls-back-to-global
|
||||
(t/testing "a side without its own value reads the uniform width"
|
||||
(t/is (= 5 (cts/side-width {:stroke-width 5} :stroke-width-right)))
|
||||
(t/is (= 5 (cts/side-width {:stroke-width 5 :stroke-width-left 4} :stroke-width-right))))
|
||||
|
||||
(t/testing "a side with its own value wins over the global width"
|
||||
(t/is (= 9 (cts/side-width {:stroke-width 5 :stroke-width-right 9} :stroke-width-right))))
|
||||
|
||||
(t/testing "a nil stroke reads nil"
|
||||
(t/is (nil? (cts/side-width nil :stroke-width-top)))))
|
||||
|
||||
(t/deftest width-setters-select-mode
|
||||
(let [uniform (cts/set-width-to-all-sides {:stroke-color "#000000"} 1)
|
||||
top (cts/set-width-to-single-side uniform :stroke-width-top 8)
|
||||
equal (-> top
|
||||
(cts/set-width-to-single-side :stroke-width-right 8)
|
||||
(cts/set-width-to-single-side :stroke-width-bottom 8)
|
||||
(cts/set-width-to-single-side :stroke-width-left 8))]
|
||||
(t/is (= :simple (cts/width-type uniform)))
|
||||
(t/is (= :multiple (cts/width-type top)))
|
||||
(t/is (= [8 1 1 1]
|
||||
(mapv #(cts/side-width top %)
|
||||
[:stroke-width-top :stroke-width-right :stroke-width-bottom :stroke-width-left])))
|
||||
(t/is (= :simple (cts/width-type equal)))
|
||||
(t/is (= 8 (:stroke-width equal)))
|
||||
(t/is (not (contains? equal :stroke-width-type)))
|
||||
(t/is (= :simple (cts/width-type (cts/set-width-to-all-sides equal 3))))))
|
||||
|
||||
(t/deftest top-width-preserves-unset-sides
|
||||
(let [stroke (cts/set-width-to-single-side {:stroke-width 5} :stroke-width-top 9)]
|
||||
(t/is (= [9 5 5 5]
|
||||
(mapv #(cts/side-width stroke %)
|
||||
[:stroke-width-top :stroke-width-right :stroke-width-bottom :stroke-width-left])))))
|
||||
|
||||
(t/deftest width-type-follows-effective-side-widths
|
||||
(let [stroke {:stroke-width 2 :stroke-width-top 2 :stroke-width-right 4
|
||||
:stroke-width-bottom 6 :stroke-width-left 8}
|
||||
simple (cts/set-width-to-all-sides stroke 2)]
|
||||
(t/is (= :multiple (cts/width-type stroke)))
|
||||
(t/is (= :simple (cts/width-type {:stroke-width 2})))
|
||||
(t/is (= :simple (cts/width-type {:stroke-width 2 :stroke-width-top 2})))
|
||||
(t/is (= :multiple (cts/width-type {:stroke-width 2 :stroke-width-right 4})))
|
||||
(t/is (= [2 2 2 2]
|
||||
(mapv simple [:stroke-width-top :stroke-width-right :stroke-width-bottom :stroke-width-left])))
|
||||
(t/is (= :simple (cts/width-type simple)))
|
||||
(t/is (not (contains? simple :stroke-width-type)))))
|
||||
|
||||
@ -467,13 +467,10 @@
|
||||
(assoc-in [:strokes index] stroke))))
|
||||
options))))))))
|
||||
|
||||
;; --- Per-side controls expansion (Design tab UI state)
|
||||
;; --- Per-side controls expansion
|
||||
|
||||
(defn toggle-stroke-per-side
|
||||
"Toggle the per-side width controls of the stroke at `index` for the shapes
|
||||
in `ids`. Purely ephemeral UI state: it lives in `:workspace-local` keyed by
|
||||
`[ids index]`, so it survives selecting another shape and coming back, but
|
||||
resets on reload."
|
||||
"Toggles per-side controls for stroke `index` on shapes `ids` in UI state."
|
||||
[ids index]
|
||||
(ptk/reify ::toggle-stroke-per-side
|
||||
ptk/UpdateEvent
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.types.stroke :as cts]
|
||||
[app.plugins.image-data :refer [create-image-data]]
|
||||
[app.util.object :as obj]))
|
||||
|
||||
@ -211,8 +212,9 @@
|
||||
(defn format-stroke
|
||||
[{:keys [stroke-color stroke-color-ref-file stroke-color-ref-id
|
||||
stroke-opacity stroke-style stroke-width stroke-alignment
|
||||
stroke-cap-start stroke-cap-end stroke-color-gradient
|
||||
stroke-image] :as stroke}]
|
||||
stroke-width-top stroke-width-right stroke-width-bottom
|
||||
stroke-width-left stroke-cap-start stroke-cap-end
|
||||
stroke-color-gradient stroke-image] :as stroke}]
|
||||
|
||||
(when (some? stroke)
|
||||
(obj/without-empty
|
||||
@ -222,6 +224,11 @@
|
||||
:strokeOpacity stroke-opacity
|
||||
:strokeStyle (format-key stroke-style)
|
||||
:strokeWidth stroke-width
|
||||
:strokeWidthType (format-key (cts/width-type stroke))
|
||||
:strokeWidthTop stroke-width-top
|
||||
:strokeWidthRight stroke-width-right
|
||||
:strokeWidthBottom stroke-width-bottom
|
||||
:strokeWidthLeft stroke-width-left
|
||||
:strokeAlignment (format-key stroke-alignment)
|
||||
:strokeCapStart (format-key stroke-cap-start)
|
||||
:strokeCapEnd (format-key stroke-cap-end)
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
[app.common.json :as json]
|
||||
[app.common.types.path :as path]
|
||||
[app.common.types.shape.interactions :as ctsi]
|
||||
[app.common.types.stroke :as cts]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.util.object :as obj]
|
||||
[cuerdas.core :as str]))
|
||||
@ -223,18 +224,34 @@
|
||||
(defn parse-stroke
|
||||
[^js stroke]
|
||||
(when (some? stroke)
|
||||
(d/without-nils
|
||||
{:stroke-color (-> (obj/get stroke "strokeColor") parse-hex)
|
||||
:stroke-color-ref-file (-> (obj/get stroke "strokeColorRefFile") parse-id)
|
||||
:stroke-color-ref-id (-> (obj/get stroke "strokeColorRefId") parse-id)
|
||||
:stroke-opacity (obj/get stroke "strokeOpacity")
|
||||
:stroke-style (-> (obj/get stroke "strokeStyle") parse-keyword)
|
||||
:stroke-width (obj/get stroke "strokeWidth")
|
||||
:stroke-alignment (-> (obj/get stroke "strokeAlignment") parse-keyword)
|
||||
:stroke-cap-start (-> (obj/get stroke "strokeCapStart") parse-keyword)
|
||||
:stroke-cap-end (-> (obj/get stroke "strokeCapEnd") parse-keyword)
|
||||
:stroke-color-gradient (-> (obj/get stroke "strokeColorGradient") parse-gradient)
|
||||
:stroke-image (-> (obj/get stroke "strokeImage") parse-image-data)})))
|
||||
(let [raw (obj/get stroke "$state")
|
||||
parsed (d/without-nils
|
||||
{:stroke-color (-> (obj/get stroke "strokeColor") parse-hex)
|
||||
:stroke-color-ref-file (-> (obj/get stroke "strokeColorRefFile") parse-id)
|
||||
:stroke-color-ref-id (-> (obj/get stroke "strokeColorRefId") parse-id)
|
||||
:stroke-opacity (obj/get stroke "strokeOpacity")
|
||||
:stroke-style (-> (obj/get stroke "strokeStyle") parse-keyword)
|
||||
:stroke-alignment (-> (obj/get stroke "strokeAlignment") parse-keyword)
|
||||
:stroke-cap-start (-> (obj/get stroke "strokeCapStart") parse-keyword)
|
||||
:stroke-cap-end (-> (obj/get stroke "strokeCapEnd") parse-keyword)
|
||||
:stroke-color-gradient (-> (obj/get stroke "strokeColorGradient") parse-gradient)
|
||||
:stroke-image (-> (obj/get stroke "strokeImage") parse-image-data)})]
|
||||
(if (map? raw)
|
||||
;; Reads the proxy's stored widths without replaying its public getters.
|
||||
(merge parsed (select-keys raw [:stroke-width
|
||||
:stroke-width-top :stroke-width-right
|
||||
:stroke-width-bottom :stroke-width-left]))
|
||||
(let [width (obj/get stroke "strokeWidth")
|
||||
top (obj/get stroke "strokeWidthTop")
|
||||
right (obj/get stroke "strokeWidthRight")
|
||||
bottom (obj/get stroke "strokeWidthBottom")
|
||||
left (obj/get stroke "strokeWidthLeft")]
|
||||
(cond-> parsed
|
||||
(some? width) (cts/set-width-to-all-sides width)
|
||||
(some? top) (cts/set-width-to-single-side :stroke-width-top top)
|
||||
(some? right) (cts/set-width-to-single-side :stroke-width-right right)
|
||||
(some? bottom) (cts/set-width-to-single-side :stroke-width-bottom bottom)
|
||||
(some? left) (cts/set-width-to-single-side :stroke-width-left left)))))))
|
||||
|
||||
(defn parse-strokes
|
||||
[^js strokes]
|
||||
|
||||
@ -31,7 +31,9 @@
|
||||
[app.common.types.shape.layout :as ctl]
|
||||
[app.common.types.shape.radius :as ctsr]
|
||||
[app.common.types.shape.shadow :as ctss]
|
||||
[app.common.types.stroke :as types.stroke]
|
||||
[app.common.types.text :as txt]
|
||||
[app.common.types.token :as ctt]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.data.exports.assets :as de]
|
||||
[app.main.data.exports.wasm :as wasm.exports]
|
||||
@ -64,7 +66,7 @@
|
||||
[app.plugins.strokes :as strokes]
|
||||
[app.plugins.system-events :as se]
|
||||
[app.plugins.text :as text]
|
||||
[app.plugins.tokens :refer [applied-tokens-plugin->applied-tokens token-attr-plugin->token-attr token-attr? valid-token-resolution?]]
|
||||
[app.plugins.tokens :refer [applied-tokens-plugin->applied-tokens expand-token-attrs token-attr-plugin->token-attr token-attr? valid-token-resolution?]]
|
||||
[app.plugins.utils :as u]
|
||||
[app.util.http :as http]
|
||||
[app.util.object :as obj]
|
||||
@ -273,21 +275,27 @@
|
||||
(st/emit! (dwsh/update-shapes [id] #(assoc % :fills value))))))
|
||||
|
||||
(defn commit-strokes!
|
||||
[plugin-id ^js self value]
|
||||
(let [id (obj/get self "$id")
|
||||
value (parser/parse-strokes value)]
|
||||
(cond
|
||||
(not (sm/validate [:vector cts/schema:stroke] value))
|
||||
(u/not-valid plugin-id :strokes value)
|
||||
([plugin-id self value]
|
||||
(commit-strokes! plugin-id self value nil))
|
||||
([plugin-id ^js self value options]
|
||||
(let [id (obj/get self "$id")
|
||||
value (parser/parse-strokes value)]
|
||||
(cond
|
||||
(not (sm/validate [:vector cts/schema:stroke] value))
|
||||
(u/not-valid plugin-id :strokes value)
|
||||
|
||||
(not (r/check-permission plugin-id "content:write"))
|
||||
(u/not-valid plugin-id :strokes "Plugin doesn't have 'content:write' permission")
|
||||
(not (r/check-permission plugin-id "content:write"))
|
||||
(u/not-valid plugin-id :strokes "Plugin doesn't have 'content:write' permission")
|
||||
|
||||
(not (u/page-active? (obj/get self "$page")))
|
||||
(u/not-valid plugin-id :strokes "Cannot modify a page that is not currently active")
|
||||
(not (u/page-active? (obj/get self "$page")))
|
||||
(u/not-valid plugin-id :strokes "Cannot modify a page that is not currently active")
|
||||
|
||||
:else
|
||||
(st/emit! (dwsh/update-shapes [id] #(assoc % :strokes value))))))
|
||||
(and (not (ctt/per-side-stroke-shape? (:type (u/proxy->shape self))))
|
||||
(some #(= :multiple (types.stroke/width-type %)) value))
|
||||
(u/not-valid plugin-id :strokes "Per-side stroke widths require a board or rectangle")
|
||||
|
||||
:else
|
||||
(st/emit! (dwsh/update-shapes [id] #(assoc % :strokes value) options))))))
|
||||
|
||||
(defn commit-shadows!
|
||||
[plugin-id ^js self value]
|
||||
@ -992,8 +1000,12 @@
|
||||
:strokes
|
||||
{:this true
|
||||
:get (fn [^js self]
|
||||
(strokes/format-strokes (-> self u/proxy->shape :strokes)
|
||||
#(commit-strokes! plugin-id self %)))
|
||||
(let [shape (u/proxy->shape self)]
|
||||
(strokes/format-strokes
|
||||
(:strokes shape)
|
||||
#(commit-strokes! plugin-id self %1 %2)
|
||||
{:per-side-allowed? (ctt/per-side-stroke-shape? (:type shape))
|
||||
:not-valid (fn [code value] (u/not-valid plugin-id code value))})))
|
||||
:set (fn [self value] (commit-strokes! plugin-id self value))}
|
||||
|
||||
:layoutChild
|
||||
@ -1782,7 +1794,7 @@
|
||||
(let [set-id (obj/get token "$set-id")
|
||||
token-id (obj/get token "$id")
|
||||
token (u/locate-token file-id set-id token-id)
|
||||
kw-attrs (into #{} (map token-attr-plugin->token-attr attrs))]
|
||||
kw-attrs (expand-token-attrs attrs)]
|
||||
(cond
|
||||
(not (r/check-permission plugin-id "content:write"))
|
||||
(u/not-valid plugin-id :applyToken "Plugin doesn't have 'content:write' permission")
|
||||
|
||||
@ -6,78 +6,121 @@
|
||||
|
||||
(ns app.plugins.strokes
|
||||
(:require
|
||||
[app.common.types.stroke :as cts]
|
||||
[app.plugins.format :as format]
|
||||
[app.plugins.gradients :as gradients]
|
||||
[app.plugins.parser :as parser]
|
||||
[app.util.object :as obj]))
|
||||
|
||||
(defn- set-side-width!
|
||||
"Sets one side width and reports the edited attribute to the commit callback.
|
||||
Rejects shape types that do not support per-side widths."
|
||||
[state attr value on-change! per-side-allowed? not-valid]
|
||||
(if (and (false? per-side-allowed?) (fn? not-valid))
|
||||
(not-valid attr value)
|
||||
(do
|
||||
(swap! state #(cts/set-width-to-single-side % attr value))
|
||||
(on-change! [attr]))))
|
||||
|
||||
(defn stroke-proxy
|
||||
[stroke-data on-change!]
|
||||
(let [state (atom stroke-data)]
|
||||
(obj/reify {:name "StrokeProxy"}
|
||||
:strokeColor
|
||||
{:get (fn [] (:stroke-color @state))
|
||||
:set (fn [v]
|
||||
(swap! state #(-> % (assoc :stroke-color v) (dissoc :stroke-color-gradient :stroke-image)))
|
||||
(on-change!))}
|
||||
([stroke-data on-change!]
|
||||
(stroke-proxy stroke-data on-change! nil))
|
||||
([stroke-data on-change! {:keys [per-side-allowed? not-valid]}]
|
||||
(let [state (atom stroke-data)]
|
||||
(obj/reify {:name "StrokeProxy"}
|
||||
;; The parser reads stored widths without invoking fallback getters.
|
||||
:$state {:enumerable false :get (fn [] @state)}
|
||||
|
||||
:strokeColorRefFile
|
||||
{:get (fn [] (format/format-id (:stroke-color-ref-file @state)))
|
||||
:set (fn [v] (swap! state assoc :stroke-color-ref-file (parser/parse-id v)) (on-change!))}
|
||||
:strokeColor
|
||||
{:get (fn [] (:stroke-color @state))
|
||||
:set (fn [v]
|
||||
(swap! state #(-> % (assoc :stroke-color v) (dissoc :stroke-color-gradient :stroke-image)))
|
||||
(on-change! [:stroke-color]))}
|
||||
|
||||
:strokeColorRefId
|
||||
{:get (fn [] (format/format-id (:stroke-color-ref-id @state)))
|
||||
:set (fn [v] (swap! state assoc :stroke-color-ref-id (parser/parse-id v)) (on-change!))}
|
||||
:strokeColorRefFile
|
||||
{:get (fn [] (format/format-id (:stroke-color-ref-file @state)))
|
||||
:set (fn [v] (swap! state assoc :stroke-color-ref-file (parser/parse-id v)) (on-change! [:stroke-color]))}
|
||||
|
||||
:strokeOpacity
|
||||
{:get (fn [] (:stroke-opacity @state))
|
||||
:set (fn [v] (swap! state assoc :stroke-opacity v) (on-change!))}
|
||||
:strokeColorRefId
|
||||
{:get (fn [] (format/format-id (:stroke-color-ref-id @state)))
|
||||
:set (fn [v] (swap! state assoc :stroke-color-ref-id (parser/parse-id v)) (on-change! [:stroke-color]))}
|
||||
|
||||
:strokeStyle
|
||||
{:get (fn [] (format/format-key (:stroke-style @state)))
|
||||
:set (fn [v] (swap! state assoc :stroke-style (parser/parse-keyword v)) (on-change!))}
|
||||
:strokeOpacity
|
||||
{:get (fn [] (:stroke-opacity @state))
|
||||
:set (fn [v] (swap! state assoc :stroke-opacity v) (on-change! [:stroke-color]))}
|
||||
|
||||
:strokeWidth
|
||||
{:get (fn [] (:stroke-width @state))
|
||||
:set (fn [v] (swap! state assoc :stroke-width v) (on-change!))}
|
||||
:strokeStyle
|
||||
{:get (fn [] (format/format-key (:stroke-style @state)))
|
||||
:set (fn [v] (swap! state assoc :stroke-style (parser/parse-keyword v)) (on-change! [:stroke-style]))}
|
||||
|
||||
:strokeAlignment
|
||||
{:get (fn [] (format/format-key (:stroke-alignment @state)))
|
||||
:set (fn [v] (swap! state assoc :stroke-alignment (parser/parse-keyword v)) (on-change!))}
|
||||
:strokeWidth
|
||||
{:get (fn [] (:stroke-width @state))
|
||||
:set (fn [v]
|
||||
(swap! state #(cts/set-width-to-all-sides % v))
|
||||
(on-change! [:stroke-width]))}
|
||||
|
||||
:strokeCapStart
|
||||
{:get (fn [] (format/format-key (:stroke-cap-start @state)))
|
||||
:set (fn [v] (swap! state assoc :stroke-cap-start (parser/parse-keyword v)) (on-change!))}
|
||||
:strokeWidthType
|
||||
{:get (fn [] (format/format-key (cts/width-type @state)))}
|
||||
|
||||
:strokeCapEnd
|
||||
{:get (fn [] (format/format-key (:stroke-cap-end @state)))
|
||||
:set (fn [v] (swap! state assoc :stroke-cap-end (parser/parse-keyword v)) (on-change!))}
|
||||
:strokeWidthTop
|
||||
{:get (fn [] (cts/side-width @state :stroke-width-top))
|
||||
:set (fn [v] (set-side-width! state :stroke-width-top v on-change! per-side-allowed? not-valid))}
|
||||
|
||||
:strokeColorGradient
|
||||
{:get (fn []
|
||||
(when-let [gradient (:stroke-color-gradient @state)]
|
||||
(let [gradient-state (atom gradient)
|
||||
gradient-change! (fn []
|
||||
(swap! state assoc :stroke-color-gradient @gradient-state)
|
||||
(on-change!))]
|
||||
(gradients/gradient-proxy gradient-state gradient-change!))))
|
||||
:set (fn [v]
|
||||
(swap! state #(-> % (assoc :stroke-color-gradient (parser/parse-gradient v)) (dissoc :stroke-color :stroke-image)))
|
||||
(on-change!))}
|
||||
:strokeWidthRight
|
||||
{:get (fn [] (cts/side-width @state :stroke-width-right))
|
||||
:set (fn [v] (set-side-width! state :stroke-width-right v on-change! per-side-allowed? not-valid))}
|
||||
|
||||
:strokeImage
|
||||
{:get (fn [] (format/format-image (:stroke-image @state)))
|
||||
:set (fn [v]
|
||||
(swap! state #(-> % (assoc :stroke-image (parser/parse-image-data v)) (dissoc :stroke-color :stroke-color-gradient)))
|
||||
(on-change!))})))
|
||||
:strokeWidthBottom
|
||||
{:get (fn [] (cts/side-width @state :stroke-width-bottom))
|
||||
:set (fn [v] (set-side-width! state :stroke-width-bottom v on-change! per-side-allowed? not-valid))}
|
||||
|
||||
:strokeWidthLeft
|
||||
{:get (fn [] (cts/side-width @state :stroke-width-left))
|
||||
:set (fn [v] (set-side-width! state :stroke-width-left v on-change! per-side-allowed? not-valid))}
|
||||
|
||||
:strokeAlignment
|
||||
{:get (fn [] (format/format-key (:stroke-alignment @state)))
|
||||
:set (fn [v] (swap! state assoc :stroke-alignment (parser/parse-keyword v)) (on-change! [:stroke-alignment]))}
|
||||
|
||||
:strokeCapStart
|
||||
{:get (fn [] (format/format-key (:stroke-cap-start @state)))
|
||||
:set (fn [v] (swap! state assoc :stroke-cap-start (parser/parse-keyword v)) (on-change! [:stroke-cap-start]))}
|
||||
|
||||
:strokeCapEnd
|
||||
{:get (fn [] (format/format-key (:stroke-cap-end @state)))
|
||||
:set (fn [v] (swap! state assoc :stroke-cap-end (parser/parse-keyword v)) (on-change! [:stroke-cap-end]))}
|
||||
|
||||
:strokeColorGradient
|
||||
{:get (fn []
|
||||
(when-let [gradient (:stroke-color-gradient @state)]
|
||||
(let [gradient-state (atom gradient)
|
||||
gradient-change! (fn []
|
||||
(swap! state assoc :stroke-color-gradient @gradient-state)
|
||||
(on-change! [:stroke-color]))]
|
||||
(gradients/gradient-proxy gradient-state gradient-change!))))
|
||||
:set (fn [v]
|
||||
(swap! state #(-> % (assoc :stroke-color-gradient (parser/parse-gradient v)) (dissoc :stroke-color :stroke-image)))
|
||||
(on-change! [:stroke-color]))}
|
||||
|
||||
:strokeImage
|
||||
{:get (fn [] (format/format-image (:stroke-image @state)))
|
||||
:set (fn [v]
|
||||
(swap! state #(-> % (assoc :stroke-image (parser/parse-image-data v)) (dissoc :stroke-color :stroke-color-gradient)))
|
||||
(on-change! [:stroke-color]))}))))
|
||||
|
||||
(defn format-strokes
|
||||
([strokes] (format-strokes strokes nil))
|
||||
([strokes commit-fn]
|
||||
([strokes] (format-strokes strokes nil nil))
|
||||
([strokes commit-fn] (format-strokes strokes commit-fn nil))
|
||||
([strokes commit-fn options]
|
||||
(if (and (some? strokes) (fn? commit-fn))
|
||||
(let [arr-ref (atom nil)
|
||||
on-change! (fn [] (commit-fn @arr-ref))
|
||||
arr (apply array (mapv #(stroke-proxy % on-change!) strokes))]
|
||||
on-change! (fn [index attrs]
|
||||
(commit-fn @arr-ref {:changed-sub-attr attrs :changed-item-index index}))
|
||||
arr (apply array
|
||||
(map-indexed
|
||||
(fn [index stroke]
|
||||
(stroke-proxy stroke (partial on-change! index) options))
|
||||
strokes))]
|
||||
(reset! arr-ref arr)
|
||||
arr)
|
||||
(format/format-array format/format-stroke strokes))))
|
||||
|
||||
@ -79,15 +79,38 @@
|
||||
(let [k (cond-> k (string? k) keyword)]
|
||||
(get map:token-attr-plugin->token-attr k k)))
|
||||
|
||||
(def ^:private stroke-width-alias
|
||||
"Uniform stroke width alias that expands to all four side token attributes."
|
||||
:stroke-width)
|
||||
|
||||
(defn expand-token-attrs
|
||||
"Resolves token attributes to internal keywords and expands `:stroke-width`
|
||||
to all four side attributes."
|
||||
[attrs]
|
||||
(let [attrs (into #{} (map token-attr-plugin->token-attr) attrs)]
|
||||
(if (contains? attrs stroke-width-alias)
|
||||
(into (disj attrs stroke-width-alias) cto/per-side-stroke-width-keys)
|
||||
attrs)))
|
||||
|
||||
(defn applied-tokens-plugin->applied-tokens
|
||||
[value]
|
||||
(into {}
|
||||
(map (fn [[k v]] [(token-attr->token-attr-plugin k) v]))
|
||||
value))
|
||||
(let [value (into {}
|
||||
(map (fn [[k v]] [(token-attr->token-attr-plugin k) v]))
|
||||
value)
|
||||
per-side (map #(get value %)
|
||||
[:stroke-width-top :stroke-width-right
|
||||
:stroke-width-bottom :stroke-width-left])]
|
||||
(cond-> value
|
||||
;; Exposes `strokeWidth` when all four sides share the same token.
|
||||
(and (every? some? per-side)
|
||||
(apply = per-side))
|
||||
(assoc :stroke-width (first per-side)))))
|
||||
|
||||
(defn token-attr?
|
||||
[attr]
|
||||
(cto/token-attr? (token-attr-plugin->token-attr attr)))
|
||||
(let [attr (token-attr-plugin->token-attr attr)]
|
||||
(or (= attr stroke-width-alias)
|
||||
(cto/token-attr? attr))))
|
||||
|
||||
(defn- token-name-schema
|
||||
[file-id set-id token]
|
||||
@ -180,7 +203,7 @@
|
||||
:else
|
||||
(st/emit!
|
||||
(-> (dwta/toggle-token {:token token
|
||||
:attrs (into #{} (map token-attr-plugin->token-attr) attrs)
|
||||
:attrs (expand-token-attrs attrs)
|
||||
:shape-ids shape-ids
|
||||
:expand-with-children false})
|
||||
(se/add-event plugin-id)))))))
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
[app.common.math :as m]
|
||||
[app.common.test-helpers.files :as cthf]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.data.workspace.colors :as dwc]
|
||||
[app.main.data.workspace.reflow :as wrf]
|
||||
[app.main.data.workspace.shapes :as dwsh]
|
||||
[app.main.data.workspace.texts :as dwtxt]
|
||||
@ -254,11 +255,49 @@
|
||||
|
||||
(t/testing " - strokes"
|
||||
(set! (.-strokes shape) #js [#js {:strokeColor "#fabada" :strokeOpacity 1 :strokeWidth 5}])
|
||||
(t/is (= (get-in @store (get-shape-path :strokes)) [{:stroke-color "#fabada" :stroke-opacity 1 :stroke-width 5}]))
|
||||
(t/is (= (get-in @store (get-shape-path :strokes)) [{:stroke-color "#fabada" :stroke-opacity 1 :stroke-width 5
|
||||
:stroke-width-top 5 :stroke-width-right 5 :stroke-width-bottom 5 :stroke-width-left 5}]))
|
||||
(t/is (= (-> (. ^js shape -strokes) (aget 0) (aget "strokeColor")) "#fabada"))
|
||||
(t/is (= (-> (. ^js shape -strokes) (aget 0) (aget "strokeOpacity")) 1))
|
||||
(t/is (= (-> (. ^js shape -strokes) (aget 0) (aget "strokeWidth")) 5)))
|
||||
|
||||
(t/testing " - strokes per-side widths"
|
||||
(set! (.-strokes shape) #js [#js {:strokeColor "#fabada" :strokeWidth 5}])
|
||||
(t/is (= 5 (-> (. ^js shape -strokes) (aget 0) (aget "strokeWidthTop")))
|
||||
"a uniform stroke reads the global width on every side")
|
||||
(obj/set! (aget (.-strokes shape) 0) "strokeWidthRight" 9)
|
||||
(t/is (= 9 (-> (. ^js shape -strokes) (aget 0) (aget "strokeWidthRight"))))
|
||||
(t/is (= 5 (-> (. ^js shape -strokes) (aget 0) (aget "strokeWidthTop"))))
|
||||
(t/is (= 9 (get-in @store (conj (get-shape-path :strokes) 0 :stroke-width-right)))
|
||||
"the edited side is stored")
|
||||
(t/is (= 5 (get-in @store (conj (get-shape-path :strokes) 0 :stroke-width-top)))
|
||||
"the untouched sides keep their uniform width"))
|
||||
|
||||
(t/testing " - UI toggles preserve equal side widths and their computed mode"
|
||||
(set! (.-strokes shape)
|
||||
#js [#js {:strokeColor "#fabada" :strokeWidth 1
|
||||
:strokeWidthTop 8 :strokeWidthRight 8
|
||||
:strokeWidthBottom 8 :strokeWidthLeft 8}])
|
||||
(t/is (not (contains? (get-in @store (conj (get-shape-path :strokes) 0)) :stroke-width-type)))
|
||||
(t/is (= 8 (get-in @store (conj (get-shape-path :strokes) 0 :stroke-width))))
|
||||
(ptk/emit! store (dwc/toggle-stroke-per-side [(aget shape "$id")] 0))
|
||||
(t/is (true? (get-in @store [:workspace-local :stroke-per-side [[(aget shape "$id")] 0]])))
|
||||
(t/is (= "simple" (aget (aget (.-strokes shape) 0) "strokeWidthType")))
|
||||
(ptk/emit! store (dwc/toggle-stroke-per-side [(aget shape "$id")] 0))
|
||||
(t/is (= "simple" (aget (aget (.-strokes shape) 0) "strokeWidthType")))
|
||||
(t/is (false? (get-in @store [:workspace-local :stroke-per-side [[(aget shape "$id")] 0]])))
|
||||
(t/is (= [8 8 8 8]
|
||||
(mapv (get-in @store (conj (get-shape-path :strokes) 0))
|
||||
[:stroke-width-top :stroke-width-right :stroke-width-bottom :stroke-width-left]))))
|
||||
|
||||
(t/testing " - per-side widths rejected on unsupported shapes"
|
||||
(let [^js ellipse (.createEllipse context)]
|
||||
(set! (.-strokes ellipse) #js [#js {:strokeColor "#fabada" :strokeWidth 1}])
|
||||
(t/is (thrown? js/Error
|
||||
(obj/set! (aget (.-strokes ellipse) 0) "strokeWidthTop" 4)))
|
||||
(t/is (thrown? js/Error
|
||||
(set! (.-strokes ellipse) #js [#js {:strokeColor "#fabada" :strokeWidthTop 4}])))))
|
||||
|
||||
(t/testing " - fills per-element property mutation (bug #8357)"
|
||||
(set! (.-fills shape) #js [#js {:fillColor "#fabada" :fillOpacity 1}])
|
||||
(obj/set! (aget (.-fills shape) 0) "fillColor" "#ff0000")
|
||||
@ -287,13 +326,15 @@
|
||||
(t/testing " - strokes per-element property mutation (bug #8357)"
|
||||
(set! (.-strokes shape) #js [#js {:strokeColor "#fabada" :strokeOpacity 1 :strokeWidth 5}])
|
||||
(obj/set! (aget (.-strokes shape) 0) "strokeColor" "#0000ff")
|
||||
(t/is (= (get-in @store (get-shape-path :strokes)) [{:stroke-color "#0000ff" :stroke-opacity 1 :stroke-width 5}])))
|
||||
(t/is (= (get-in @store (get-shape-path :strokes)) [{:stroke-color "#0000ff" :stroke-opacity 1 :stroke-width 5
|
||||
:stroke-width-top 5 :stroke-width-right 5 :stroke-width-bottom 5 :stroke-width-left 5}])))
|
||||
|
||||
(t/testing " - strokes gradient assignment replaces solid color (bug #8357)"
|
||||
(set! (.-strokes shape) #js [#js {:strokeColor "#fabada" :strokeOpacity 1 :strokeWidth 5}])
|
||||
(obj/set! (aget (.-strokes shape) 0) "strokeColorGradient" (gradient))
|
||||
(t/is (= (get-in @store (get-shape-path :strokes))
|
||||
[{:stroke-opacity 1 :stroke-width 5 :stroke-color-gradient parsed-gradient}])))
|
||||
[{:stroke-opacity 1 :stroke-width 5
|
||||
:stroke-width-top 5 :stroke-width-right 5 :stroke-width-bottom 5 :stroke-width-left 5 :stroke-color-gradient parsed-gradient}])))
|
||||
|
||||
(t/testing " - strokes nested gradient mutation (bug #8357)"
|
||||
(set! (.-strokes shape) #js [#js {:strokeColorGradient (gradient) :strokeOpacity 1 :strokeWidth 5}])
|
||||
@ -304,6 +345,7 @@
|
||||
(t/is (= (get-in @store (get-shape-path :strokes))
|
||||
[{:stroke-opacity 1
|
||||
:stroke-width 5
|
||||
:stroke-width-top 5 :stroke-width-right 5 :stroke-width-bottom 5 :stroke-width-left 5
|
||||
:stroke-color-gradient (-> parsed-gradient
|
||||
(assoc :end-y 0.75)
|
||||
(assoc-in [:stops 1 :opacity] 0.25))}])))))
|
||||
|
||||
@ -55,3 +55,16 @@
|
||||
|
||||
(t/deftest test-shape-type-reports-boolean
|
||||
(t/is (= "boolean" (format/shape-type :bool))))
|
||||
|
||||
(t/deftest test-format-stroke-exposes-per-side-widths
|
||||
(let [stroke (format/format-stroke {:stroke-color "#000000"
|
||||
:stroke-width 1
|
||||
:stroke-width-top 2
|
||||
:stroke-width-right 3
|
||||
:stroke-width-bottom 4
|
||||
:stroke-width-left 5})]
|
||||
(t/is (= 1 (aget stroke "strokeWidth")))
|
||||
(t/is (= 2 (aget stroke "strokeWidthTop")))
|
||||
(t/is (= 3 (aget stroke "strokeWidthRight")))
|
||||
(t/is (= 4 (aget stroke "strokeWidthBottom")))
|
||||
(t/is (= 5 (aget stroke "strokeWidthLeft")))))
|
||||
|
||||
@ -116,6 +116,10 @@
|
||||
["ShapeProxy.rotation" #(set! (.-rotation rect) 45)]
|
||||
["ShapeProxy.fills" #(set! (.-fills rect) #js [#js {:fillColor "#fabada" :fillOpacity 1}])]
|
||||
["ShapeProxy.strokes" #(set! (.-strokes rect) #js [#js {:strokeColor "#fabada" :strokeOpacity 1 :strokeWidth 2}])]
|
||||
["ShapeProxy.strokeWidthTop" #(do (set! (.-strokes rect) #js [#js {:strokeColor "#fabada" :strokeWidth 2}])
|
||||
(set! (.-strokeWidthTop (aget (.-strokes rect) 0)) 5))]
|
||||
["ShapeProxy.strokeWidthRight" #(do (set! (.-strokes rect) #js [#js {:strokeColor "#fabada" :strokeWidth 2}])
|
||||
(set! (.-strokeWidthRight (aget (.-strokes rect) 0)) 5))]
|
||||
;; relative geometry (shape inside a board)
|
||||
["ShapeProxy.boardX" #(set! (.-boardX child) 10)]
|
||||
["ShapeProxy.boardY" #(set! (.-boardY child) 10)]
|
||||
|
||||
@ -11,7 +11,9 @@
|
||||
[app.common.types.grid :as ctg]
|
||||
[app.common.types.shape.interactions :as ctsi]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.plugins.format :as format]
|
||||
[app.plugins.parser :as parser]
|
||||
[app.plugins.strokes :as strokes]
|
||||
[cljs.test :as t :include-macros true]))
|
||||
|
||||
(defn- overlay-action
|
||||
@ -193,3 +195,70 @@
|
||||
|
||||
(t/testing "a malformed id raises, so plugin bugs stay visible"
|
||||
(t/is (thrown? js/Error (parser/parse-id "not-a-uuid")))))
|
||||
|
||||
(t/deftest test-parse-stroke-maps-per-side-widths
|
||||
(let [stroke (parser/parse-stroke
|
||||
#js {:strokeColor "#000000"
|
||||
:strokeWidth 1
|
||||
:strokeWidthTop 2
|
||||
:strokeWidthRight 3
|
||||
:strokeWidthBottom 4
|
||||
:strokeWidthLeft 5})]
|
||||
(t/is (= 2 (:stroke-width-top stroke)))
|
||||
(t/is (= 3 (:stroke-width-right stroke)))
|
||||
(t/is (= 4 (:stroke-width-bottom stroke)))
|
||||
(t/is (= 5 (:stroke-width-left stroke))))
|
||||
|
||||
(t/testing "a uniform literal initializes all sides in simple mode"
|
||||
(let [stroke (parser/parse-stroke #js {:strokeColor "#000000" :strokeWidth 1})]
|
||||
(t/is (= "simple" (aget (format/format-stroke stroke) "strokeWidthType")))
|
||||
(t/is (= 1 (:stroke-width-top stroke)))
|
||||
(t/is (= 1 (:stroke-width-left stroke))))))
|
||||
|
||||
(t/deftest test-parse-stroke-applies-uniform-width-before-sides
|
||||
(let [stroke (parser/parse-stroke
|
||||
#js {:strokeColor "#000000"
|
||||
:strokeWidthLeft 8
|
||||
:strokeWidthBottom 8
|
||||
:strokeWidthRight 8
|
||||
:strokeWidthTop 8
|
||||
:strokeWidth 1})]
|
||||
(t/is (= "simple" (aget (format/format-stroke stroke) "strokeWidthType")))
|
||||
(t/is (= 8 (:stroke-width stroke)))
|
||||
(t/is (= [8 8 8 8]
|
||||
(mapv stroke [:stroke-width-top :stroke-width-right :stroke-width-bottom :stroke-width-left])))))
|
||||
|
||||
(t/deftest test-parse-stroke-top-preserves-other-widths
|
||||
(let [stroke (parser/parse-stroke #js {:strokeColor "#000000" :strokeWidth 5 :strokeWidthTop 9})]
|
||||
(t/is (= [9 5 5 5]
|
||||
(mapv stroke [:stroke-width-top :stroke-width-right :stroke-width-bottom :stroke-width-left])))))
|
||||
|
||||
(t/deftest test-stroke-proxy-mode-transitions
|
||||
(let [^js stroke (strokes/stroke-proxy {:stroke-color "#000000" :stroke-width 1} (fn [_] nil))]
|
||||
(t/is (= "simple" (.-strokeWidthType stroke)))
|
||||
(set! (.-strokeWidthTop stroke) 8)
|
||||
(t/is (= "multiple" (.-strokeWidthType stroke)))
|
||||
(t/is (= 1 (.-strokeWidthRight stroke)))
|
||||
(set! (.-strokeWidthRight stroke) 8)
|
||||
(set! (.-strokeWidthBottom stroke) 8)
|
||||
(set! (.-strokeWidthLeft stroke) 8)
|
||||
(t/is (= "simple" (.-strokeWidthType stroke)))
|
||||
(t/is (= 8 (.-strokeWidth stroke)))
|
||||
(set! (.-strokeColor stroke) "#ff0000")
|
||||
(t/is (not (contains? (parser/parse-stroke stroke) :stroke-width-type)))
|
||||
(set! (.-strokeWidth stroke) 3)
|
||||
(t/is (= "simple" (.-strokeWidthType stroke)))
|
||||
(t/is (= [3 3 3 3]
|
||||
(mapv #(aget stroke %) ["strokeWidthTop" "strokeWidthRight" "strokeWidthBottom" "strokeWidthLeft"])))
|
||||
(set! (.-strokeWidthRight stroke) 9)
|
||||
(t/is (= "multiple" (.-strokeWidthType stroke)))
|
||||
(set! (.-strokeWidth stroke) 3)
|
||||
(t/is (= [3 3 3 3]
|
||||
(mapv #(aget stroke %) ["strokeWidthTop" "strokeWidthRight" "strokeWidthBottom" "strokeWidthLeft"])))
|
||||
(t/is (= "simple" (.-strokeWidthType stroke)))))
|
||||
|
||||
(t/deftest test-stroke-mode-round-trips-through-format
|
||||
(doseq [right [8 12]]
|
||||
(let [stroke (parser/parse-stroke
|
||||
#js {:strokeColor "#000000" :strokeWidth 8 :strokeWidthRight right})]
|
||||
(t/is (= stroke (parser/parse-stroke (format/format-stroke stroke)))))))
|
||||
|
||||
@ -11,17 +11,21 @@
|
||||
[app.common.test-helpers.files :as cthf]
|
||||
[app.common.test-helpers.ids-map :as cthi]
|
||||
[app.common.test-helpers.tokens :as ctht]
|
||||
[app.common.types.token :as ctt]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.common.types.tokens-status :as ctos]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.data.tokenscript :as ts]
|
||||
[app.main.data.workspace.shapes :as dwsh]
|
||||
[app.main.data.workspace.tokens.library-edit :as dwtl]
|
||||
[app.main.store :as st]
|
||||
[app.plugins.api :as api]
|
||||
[app.plugins.register :as r]
|
||||
[app.plugins.tokens :as ptok]
|
||||
[app.plugins.utils :as u]
|
||||
[app.util.object :as obj]
|
||||
[cljs.test :as t :include-macros true]
|
||||
[frontend-tests.helpers.async :as tha]
|
||||
[frontend-tests.helpers.mock :as mock]
|
||||
[frontend-tests.helpers.state :as ths]
|
||||
[frontend-tests.helpers.wasm :as thw]
|
||||
@ -35,6 +39,73 @@
|
||||
|
||||
(def ^:private get-resolved-value @#'ptok/get-resolved-value)
|
||||
|
||||
(t/deftest ^:async stroke-property-edits-only-remove-related-token-bindings
|
||||
(let [tokens {:stroke-color "border.color"
|
||||
:stroke-width-top "border.top"
|
||||
:stroke-width-right "border.right"
|
||||
:stroke-width-bottom "border.bottom"
|
||||
:stroke-width-left "border.left"}
|
||||
cases [[0 "strokeWidthTop" 8 #{:stroke-width-top}]
|
||||
[0 "strokeWidthRight" 8 #{:stroke-width-right}]
|
||||
[0 "strokeWidthBottom" 8 #{:stroke-width-bottom}]
|
||||
[0 "strokeWidthLeft" 8 #{:stroke-width-left}]
|
||||
[0 "strokeWidth" 8 ctt/per-side-stroke-width-keys]
|
||||
[0 "strokeColor" "#ff0000" #{:stroke-color}]
|
||||
[0 "strokeOpacity" 0.5 #{:stroke-color}]
|
||||
[0 "strokeStyle" "dashed" #{}]
|
||||
[0 "strokeAlignment" "center" #{}]
|
||||
[1 "strokeWidthTop" 8 #{}]
|
||||
[1 "strokeWidth" 8 #{}]
|
||||
[1 "strokeColor" "#ff0000" #{}]]]
|
||||
(doseq [[index property value removed] cases]
|
||||
(t/testing (str "stroke " index " property " property)
|
||||
(let [store (ths/setup-store (cthf/sample-file :file1 :page-label :page1))
|
||||
_ (set! st/state store)
|
||||
^js ctx (api/create-context "00000000-0000-0000-0000-000000000000")
|
||||
^js rect (.createRectangle ctx)
|
||||
id (obj/get rect "$id")
|
||||
path [:files (:current-file-id @store) :data :pages-index
|
||||
(:current-page-id @store) :objects id :applied-tokens]]
|
||||
(set! (.-strokes rect)
|
||||
#js [#js {:strokeColor "#000000" :strokeWidth 1 :strokeWidthRight 2}
|
||||
#js {:strokeColor "#000000" :strokeWidth 1}])
|
||||
(ptk/emit! store (dwsh/update-shapes [id] #(assoc % :applied-tokens tokens)))
|
||||
(obj/set! (aget (.-strokes rect) index) property value)
|
||||
(await (tha/settle))
|
||||
(t/is (= (apply dissoc tokens removed) (get-in @store path)))
|
||||
(t/is (= value (obj/get (aget (.-strokes rect) index) property))))))))
|
||||
|
||||
(t/deftest ^:async stroke-gradient-edits-preserve-width-token-bindings
|
||||
(let [store (ths/setup-store (cthf/sample-file :file1 :page-label :page1))
|
||||
_ (set! st/state store)
|
||||
^js ctx (api/create-context "00000000-0000-0000-0000-000000000000")
|
||||
^js rect (.createRectangle ctx)
|
||||
id (obj/get rect "$id")
|
||||
path [:files (:current-file-id @store) :data :pages-index
|
||||
(:current-page-id @store) :objects id :applied-tokens]
|
||||
tokens {:stroke-color "border.color"
|
||||
:stroke-width-top "border.width"
|
||||
:stroke-width-right "border.width"
|
||||
:stroke-width-bottom "border.width"
|
||||
:stroke-width-left "border.width"}]
|
||||
(set! (.-strokes rect)
|
||||
#js [#js {:strokeWidth 1
|
||||
:strokeColorGradient
|
||||
#js {:type "linear" :startX 0 :startY 0 :endX 1 :endY 1 :width 1
|
||||
:stops #js [#js {:color "#000000" :opacity 1 :offset 0}
|
||||
#js {:color "#ffffff" :opacity 1 :offset 1}]}}])
|
||||
(ptk/emit! store (dwsh/update-shapes [id] #(assoc % :applied-tokens tokens)))
|
||||
(let [^js gradient (obj/get (aget (.-strokes rect) 0) "strokeColorGradient")]
|
||||
(set! (.-startX gradient) 0.5)
|
||||
(await (tha/settle))
|
||||
(t/is (= (dissoc tokens :stroke-color) (get-in @store path)))
|
||||
(t/is (= 0.5 (obj/get (obj/get (aget (.-strokes rect) 0) "strokeColorGradient") "startX")))
|
||||
(ptk/emit! store (dwsh/update-shapes [id] #(assoc % :applied-tokens tokens)))
|
||||
(obj/set! (aget (.-stops gradient) 0) "color" "#ff0000")
|
||||
(await (tha/settle))
|
||||
(t/is (= (dissoc tokens :stroke-color) (get-in @store path)))
|
||||
(t/is (= "#ff0000" (obj/get (aget (obj/get (obj/get (aget (.-strokes rect) 0) "strokeColorGradient") "stops") 0) "color"))))))
|
||||
|
||||
;; Regression coverage for issue #9162.
|
||||
;;
|
||||
;; Plugin code calling `shape.applyToken(token, ["fill"])` or
|
||||
@ -122,6 +193,25 @@
|
||||
(t/is (true? (boolean (ptok/token-attr? "r1"))))
|
||||
(t/is (true? (boolean (ptok/token-attr? "m3")))))
|
||||
|
||||
(t/deftest token-attr?-accepts-per-side-stroke-width
|
||||
;; Accepts the uniform width alias and side attributes as keywords or strings.
|
||||
(t/is (true? (boolean (ptok/token-attr? :stroke-width))))
|
||||
(t/is (true? (boolean (ptok/token-attr? "stroke-width"))))
|
||||
(t/is (true? (boolean (ptok/token-attr? :stroke-width-top))))
|
||||
(t/is (true? (boolean (ptok/token-attr? "stroke-width-left")))))
|
||||
|
||||
(t/deftest expand-token-attrs-expands-uniform-stroke-width
|
||||
(t/testing "the uniform alias becomes the four per-side attributes"
|
||||
(t/is (= ctt/per-side-stroke-width-keys
|
||||
(ptok/expand-token-attrs #{:stroke-width})))
|
||||
(t/is (= ctt/per-side-stroke-width-keys
|
||||
(ptok/expand-token-attrs #{"stroke-width"}))))
|
||||
|
||||
(t/testing "per-side and unrelated attributes pass through unchanged"
|
||||
(t/is (= #{:stroke-width-top} (ptok/expand-token-attrs #{:stroke-width-top})))
|
||||
(t/is (= #{:fill} (ptok/expand-token-attrs #{:fill})))
|
||||
(t/is (= #{} (ptok/expand-token-attrs nil)))))
|
||||
|
||||
(t/deftest shape-apply-token-accepts-padding-top
|
||||
(t/async
|
||||
done
|
||||
@ -807,4 +897,3 @@
|
||||
(.addSet catalog #js {"name" "NewSet"})
|
||||
(t/is (= 1 (count @errors)))
|
||||
(t/is (= [plugin-id :addSet "Plugin doesn't have 'content:write' permission"] (first @errors)))))))
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
- **plugin-types:** Added `paddingType` (`'simple' | 'multiple'`) to flex and grid layouts and `marginType` (`'simple' | 'multiple'`) to layout children, exposing whether the four padding/margin sides are mirrored or honoured independently.
|
||||
- **plugin-types**: Added `waitForLayoutUpdate` to wait until pending layout updates have finished. It rejects when the optional timeout elapses, defaulting to 30 seconds so a wait never hangs.
|
||||
- **plugin-types**: Added `waitForLayoutUpdate` to the `Shape` interface to wait until the pending layout updates of a shape and its children have finished
|
||||
- **plugin-types:** `Stroke` exposes per-side widths for boards and rectangles, also supported by `borderWidth` and `dimension` tokens. Read-only `strokeWidthType` reports `simple` when all four effective widths match, otherwise `multiple`.
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
|
||||
@ -472,7 +472,12 @@
|
||||
"strokeImage",
|
||||
"strokeOpacity",
|
||||
"strokeStyle",
|
||||
"strokeWidth"
|
||||
"strokeWidth",
|
||||
"strokeWidthBottom",
|
||||
"strokeWidthLeft",
|
||||
"strokeWidthRight",
|
||||
"strokeWidthTop",
|
||||
"strokeWidthType"
|
||||
],
|
||||
"SvgRaw": ["type"],
|
||||
"Text": [
|
||||
@ -7831,6 +7836,36 @@
|
||||
"type": null,
|
||||
"array": false
|
||||
},
|
||||
"strokeWidthType": {
|
||||
"decl": "Stroke",
|
||||
"kind": "get",
|
||||
"type": null,
|
||||
"array": false
|
||||
},
|
||||
"strokeWidthTop": {
|
||||
"decl": "Stroke",
|
||||
"kind": "getset",
|
||||
"type": null,
|
||||
"array": false
|
||||
},
|
||||
"strokeWidthRight": {
|
||||
"decl": "Stroke",
|
||||
"kind": "getset",
|
||||
"type": null,
|
||||
"array": false
|
||||
},
|
||||
"strokeWidthBottom": {
|
||||
"decl": "Stroke",
|
||||
"kind": "getset",
|
||||
"type": null,
|
||||
"array": false
|
||||
},
|
||||
"strokeWidthLeft": {
|
||||
"decl": "Stroke",
|
||||
"kind": "getset",
|
||||
"type": null,
|
||||
"array": false
|
||||
},
|
||||
"strokeAlignment": {
|
||||
"decl": "Stroke",
|
||||
"kind": "getset",
|
||||
|
||||
@ -226,6 +226,52 @@ describe('Fills & strokes', () => {
|
||||
expect(stroke.strokeCapEnd).toBe('triangle-arrow');
|
||||
});
|
||||
|
||||
test('per-side widths survive an unrelated write', (ctx) => {
|
||||
const r = rect(ctx);
|
||||
r.strokes = [
|
||||
{
|
||||
strokeColor: '#000000',
|
||||
strokeWidth: 1,
|
||||
strokeWidthTop: 2,
|
||||
strokeWidthRight: 3,
|
||||
strokeWidthBottom: 4,
|
||||
strokeWidthLeft: 5,
|
||||
},
|
||||
];
|
||||
const stroke = r.strokes[0];
|
||||
stroke.strokeColor = '#ff0000';
|
||||
|
||||
const updated = r.strokes[0];
|
||||
expect(updated.strokeColor).toBe('#ff0000');
|
||||
expect(updated.strokeWidthTop).toBeCloseTo(2, 0);
|
||||
expect(updated.strokeWidthRight).toBeCloseTo(3, 0);
|
||||
expect(updated.strokeWidthBottom).toBeCloseTo(4, 0);
|
||||
expect(updated.strokeWidthLeft).toBeCloseTo(5, 0);
|
||||
});
|
||||
|
||||
test('a literal without per-side keys reads uniform', (ctx) => {
|
||||
const r = rect(ctx);
|
||||
r.strokes = [{ strokeColor: '#000000', strokeWidth: 6 }];
|
||||
expect(r.strokes[0].strokeWidth).toBeCloseTo(6, 0);
|
||||
expect(r.strokes[0].strokeWidthTop).toBeCloseTo(6, 0);
|
||||
expect(r.strokes[0].strokeWidthLeft).toBeCloseTo(6, 0);
|
||||
});
|
||||
|
||||
test('a literal side width preserves the other uniform widths', (ctx) => {
|
||||
const r = rect(ctx);
|
||||
r.strokes = [
|
||||
{ strokeColor: '#000000', strokeWidthTop: 9, strokeWidth: 5 },
|
||||
];
|
||||
|
||||
const stroke = r.strokes[0];
|
||||
expect(stroke.strokeWidthType).toBe('multiple');
|
||||
expect(stroke.strokeWidth).toBe(9);
|
||||
expect(stroke.strokeWidthTop).toBe(9);
|
||||
expect(stroke.strokeWidthRight).toBe(5);
|
||||
expect(stroke.strokeWidthBottom).toBe(5);
|
||||
expect(stroke.strokeWidthLeft).toBe(5);
|
||||
});
|
||||
|
||||
test('dashed stroke style is preserved', (ctx) => {
|
||||
const r = rect(ctx);
|
||||
r.strokes = [
|
||||
|
||||
@ -273,6 +273,28 @@ describe('Shapes', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Per-side stroke width', () => {
|
||||
test('per-side widths are readable and writable on a rectangle', (ctx) => {
|
||||
const r = rect(ctx);
|
||||
r.strokes = [{ strokeColor: '#000000', strokeWidth: 1 }];
|
||||
const stroke = r.strokes[0];
|
||||
stroke.strokeWidthTop = 2;
|
||||
stroke.strokeWidthRight = 3;
|
||||
expect(stroke.strokeWidthTop).toBeCloseTo(2, 0);
|
||||
expect(stroke.strokeWidthRight).toBeCloseTo(3, 0);
|
||||
});
|
||||
|
||||
test('per-side widths are rejected on shapes that do not support them', (ctx) => {
|
||||
const path = ctx.penpot.createPath();
|
||||
ctx.board.appendChild(path);
|
||||
path.d = 'M0 0 L40 0';
|
||||
path.strokes = [{ strokeColor: '#000000', strokeWidth: 1 }];
|
||||
expect(() => {
|
||||
path.strokes[0].strokeWidthTop = 4;
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Ordering', () => {
|
||||
test('setParentIndex moves the shape to the given index', (ctx) => {
|
||||
const a = rect(ctx);
|
||||
|
||||
@ -33,6 +33,39 @@ function unique(prefix: string): string {
|
||||
return `${prefix}-${runTag}-${counter}`;
|
||||
}
|
||||
|
||||
async function rectangleWithStrokeTokens(ctx: TestContext) {
|
||||
const set = activeSet(ctx, unique('stroke-set'));
|
||||
const width = set.addToken({
|
||||
type: 'borderWidth',
|
||||
name: unique('border.width'),
|
||||
value: '4',
|
||||
});
|
||||
const color = set.addToken({
|
||||
type: 'color',
|
||||
name: unique('border.color'),
|
||||
value: '#112233',
|
||||
});
|
||||
const rect = ctx.penpot.createRectangle();
|
||||
ctx.board.appendChild(rect);
|
||||
rect.strokes = [
|
||||
{ strokeColor: '#000000', strokeWidth: 1 },
|
||||
{ strokeColor: '#ffffff', strokeWidth: 2 },
|
||||
];
|
||||
rect.applyToken(width, ['strokeWidth']);
|
||||
await waitFor(() => rect.tokens.strokeWidth === width.name);
|
||||
rect.applyToken(color, ['strokeColor']);
|
||||
await waitFor(() => rect.tokens.strokeColor === color.name);
|
||||
expect(rect.tokens).toEqual({
|
||||
strokeColor: color.name,
|
||||
strokeWidth: width.name,
|
||||
strokeWidthTop: width.name,
|
||||
strokeWidthRight: width.name,
|
||||
strokeWidthBottom: width.name,
|
||||
strokeWidthLeft: width.name,
|
||||
});
|
||||
return { rect, width, color };
|
||||
}
|
||||
|
||||
/** Token application and theme/set wiring update the store asynchronously. */
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
@ -385,6 +418,102 @@ describe('Tokens', () => {
|
||||
expect(Object.keys(b.tokens)).toContain('paddingLeft');
|
||||
});
|
||||
|
||||
test('applyToken accepts the uniform strokeWidth property', async (ctx) => {
|
||||
const set = activeSet(ctx, unique('set'));
|
||||
const token = set.addToken({
|
||||
type: 'borderWidth',
|
||||
name: unique('width.'),
|
||||
value: '4',
|
||||
});
|
||||
const rect = ctx.penpot.createRectangle();
|
||||
ctx.board.appendChild(rect);
|
||||
|
||||
rect.applyToken(token, ['strokeWidth']);
|
||||
|
||||
await waitFor(() => Object.keys(rect.tokens).length > 0);
|
||||
const keys = Object.keys(rect.tokens);
|
||||
expect(keys).toContain('strokeWidthTop');
|
||||
expect(keys).toContain('strokeWidthRight');
|
||||
expect(keys).toContain('strokeWidthBottom');
|
||||
expect(keys).toContain('strokeWidthLeft');
|
||||
});
|
||||
|
||||
test('applyToken binds a single per-side stroke width property', async (ctx) => {
|
||||
const set = activeSet(ctx, unique('set'));
|
||||
const token = set.addToken({
|
||||
type: 'borderWidth',
|
||||
name: unique('width.'),
|
||||
value: '4',
|
||||
});
|
||||
const rect = ctx.penpot.createRectangle();
|
||||
ctx.board.appendChild(rect);
|
||||
|
||||
rect.applyToken(token, ['strokeWidthTop']);
|
||||
|
||||
await waitFor(() => Object.keys(rect.tokens).includes('strokeWidthTop'));
|
||||
const keys = Object.keys(rect.tokens);
|
||||
expect(keys).toContain('strokeWidthTop');
|
||||
expect(keys).not.toContain('strokeWidthRight');
|
||||
});
|
||||
|
||||
for (const side of [
|
||||
'strokeWidthTop',
|
||||
'strokeWidthRight',
|
||||
'strokeWidthBottom',
|
||||
'strokeWidthLeft',
|
||||
] as const) {
|
||||
test(`${side} edits preserve unrelated stroke tokens`, async (ctx) => {
|
||||
const { rect } = await rectangleWithStrokeTokens(ctx);
|
||||
const expected: Partial<typeof rect.tokens> = { ...rect.tokens };
|
||||
delete expected[side];
|
||||
delete expected.strokeWidth;
|
||||
|
||||
rect.strokes[0][side] = 8;
|
||||
await ctx.penpot.waitForLayoutUpdate();
|
||||
|
||||
expect(rect.strokes[0][side]).toBe(8);
|
||||
expect(rect.tokens).toEqual(expected);
|
||||
});
|
||||
}
|
||||
|
||||
test('uniform stroke width edits preserve the color token', async (ctx) => {
|
||||
const { rect, color } = await rectangleWithStrokeTokens(ctx);
|
||||
rect.strokes[0].strokeWidth = 8;
|
||||
await ctx.penpot.waitForLayoutUpdate();
|
||||
|
||||
expect(rect.tokens).toEqual({ strokeColor: color.name });
|
||||
expect(rect.strokes[0].strokeWidthType).toBe('simple');
|
||||
expect(rect.strokes[0].strokeWidthTop).toBe(8);
|
||||
expect(rect.strokes[0].strokeWidthRight).toBe(8);
|
||||
expect(rect.strokes[0].strokeWidthBottom).toBe(8);
|
||||
expect(rect.strokes[0].strokeWidthLeft).toBe(8);
|
||||
});
|
||||
|
||||
test('stroke color edits preserve the width tokens', async (ctx) => {
|
||||
const { rect } = await rectangleWithStrokeTokens(ctx);
|
||||
const expected: Partial<typeof rect.tokens> = { ...rect.tokens };
|
||||
delete expected.strokeColor;
|
||||
rect.strokes[0].strokeColor = '#ff0000';
|
||||
await ctx.penpot.waitForLayoutUpdate();
|
||||
|
||||
expect(rect.strokes[0].strokeColor).toBe('#ff0000');
|
||||
expect(rect.tokens).toEqual(expected);
|
||||
});
|
||||
|
||||
test('later stroke edits preserve the first stroke tokens', async (ctx) => {
|
||||
const { rect } = await rectangleWithStrokeTokens(ctx);
|
||||
const expected = { ...rect.tokens };
|
||||
rect.strokes[1].strokeWidthTop = 8;
|
||||
rect.strokes[1].strokeColor = '#ff0000';
|
||||
await ctx.penpot.waitForLayoutUpdate();
|
||||
|
||||
expect(rect.strokes[1].strokeWidthTop).toBe(8);
|
||||
expect(rect.strokes[1].strokeColor).toBe('#ff0000');
|
||||
expect(rect.strokes[0].strokeWidthTop).toBe(4);
|
||||
expect(rect.strokes[0].strokeColor).toBe('#112233');
|
||||
expect(rect.tokens).toEqual(expected);
|
||||
});
|
||||
|
||||
test('duplicate and remove a token', (ctx) => {
|
||||
const set = activeSet(ctx, unique('set'));
|
||||
const token = set.addToken({
|
||||
|
||||
@ -129,6 +129,114 @@ describe('Value objects', () => {
|
||||
expect(stroke.strokeCapEnd).toBe('square');
|
||||
});
|
||||
|
||||
test('per-side width members round-trip', (ctx) => {
|
||||
const r = rect(ctx);
|
||||
r.strokes = [{ strokeColor: '#000000', strokeWidth: 1 }];
|
||||
const stroke = r.strokes[0];
|
||||
stroke.strokeWidthTop = 2;
|
||||
stroke.strokeWidthRight = 3;
|
||||
stroke.strokeWidthBottom = 4;
|
||||
stroke.strokeWidthLeft = 5;
|
||||
expect(stroke.strokeWidthTop).toBeCloseTo(2, 0);
|
||||
expect(stroke.strokeWidthRight).toBeCloseTo(3, 0);
|
||||
expect(stroke.strokeWidthBottom).toBeCloseTo(4, 0);
|
||||
expect(stroke.strokeWidthLeft).toBeCloseTo(5, 0);
|
||||
});
|
||||
|
||||
test('stroke literals apply uniform width before side widths', (ctx) => {
|
||||
const r = rect(ctx);
|
||||
r.strokes = [
|
||||
{
|
||||
strokeColor: '#000000',
|
||||
strokeWidthLeft: 8,
|
||||
strokeWidthBottom: 8,
|
||||
strokeWidthRight: 8,
|
||||
strokeWidthTop: 8,
|
||||
strokeWidth: 1,
|
||||
},
|
||||
];
|
||||
const stroke = r.strokes[0];
|
||||
expect(stroke.strokeWidthType).toBe('simple');
|
||||
expect(stroke.strokeWidth).toBe(8);
|
||||
expect(stroke.strokeWidthTop).toBe(8);
|
||||
expect(stroke.strokeWidthRight).toBe(8);
|
||||
expect(stroke.strokeWidthBottom).toBe(8);
|
||||
expect(stroke.strokeWidthLeft).toBe(8);
|
||||
|
||||
stroke.strokeWidthRight = 12;
|
||||
expect(stroke.strokeWidthType).toBe('multiple');
|
||||
expect(stroke.strokeWidthTop).toBe(8);
|
||||
stroke.strokeWidth = 3;
|
||||
expect(stroke.strokeWidthType).toBe('simple');
|
||||
expect(stroke.strokeWidthRight).toBe(3);
|
||||
});
|
||||
|
||||
test('a top width change preserves the other uniform sides', (ctx) => {
|
||||
const r = rect(ctx);
|
||||
r.strokes = [{ strokeColor: '#000000', strokeWidth: 1 }];
|
||||
r.strokes[0].strokeWidthTop = 8;
|
||||
|
||||
const stroke = r.strokes[0];
|
||||
expect(stroke.strokeWidthType).toBe('multiple');
|
||||
expect(stroke.strokeWidth).toBe(8);
|
||||
expect(stroke.strokeWidthTop).toBe(8);
|
||||
expect(stroke.strokeWidthRight).toBe(1);
|
||||
expect(stroke.strokeWidthBottom).toBe(1);
|
||||
expect(stroke.strokeWidthLeft).toBe(1);
|
||||
});
|
||||
|
||||
test('side setters return to simple mode when all widths match', (ctx) => {
|
||||
const r = rect(ctx);
|
||||
r.strokes = [{ strokeColor: '#000000', strokeWidth: 1 }];
|
||||
|
||||
r.strokes[0].strokeWidthTop = 8;
|
||||
expect(r.strokes[0].strokeWidthType).toBe('multiple');
|
||||
r.strokes[0].strokeWidthRight = 8;
|
||||
expect(r.strokes[0].strokeWidthType).toBe('multiple');
|
||||
r.strokes[0].strokeWidthBottom = 8;
|
||||
expect(r.strokes[0].strokeWidthType).toBe('multiple');
|
||||
r.strokes[0].strokeWidthLeft = 8;
|
||||
|
||||
const stroke = r.strokes[0];
|
||||
expect(stroke.strokeWidthType).toBe('simple');
|
||||
expect(stroke.strokeWidth).toBe(8);
|
||||
expect(stroke.strokeWidthTop).toBe(8);
|
||||
expect(stroke.strokeWidthRight).toBe(8);
|
||||
expect(stroke.strokeWidthBottom).toBe(8);
|
||||
expect(stroke.strokeWidthLeft).toBe(8);
|
||||
});
|
||||
|
||||
test('a uniform stroke reads the same width on every side', (ctx) => {
|
||||
const r = rect(ctx);
|
||||
r.strokes = [{ strokeColor: '#000000', strokeWidth: 6 }];
|
||||
const stroke = r.strokes[0];
|
||||
expect(stroke.strokeWidthTop).toBeCloseTo(6, 0);
|
||||
expect(stroke.strokeWidthRight).toBeCloseTo(6, 0);
|
||||
expect(stroke.strokeWidthBottom).toBeCloseTo(6, 0);
|
||||
expect(stroke.strokeWidthLeft).toBeCloseTo(6, 0);
|
||||
});
|
||||
|
||||
test('setting strokeWidth flattens every side', (ctx) => {
|
||||
const r = rect(ctx);
|
||||
r.strokes = [
|
||||
{
|
||||
strokeColor: '#000000',
|
||||
strokeWidth: 1,
|
||||
strokeWidthTop: 2,
|
||||
strokeWidthRight: 3,
|
||||
strokeWidthBottom: 4,
|
||||
strokeWidthLeft: 5,
|
||||
},
|
||||
];
|
||||
const stroke = r.strokes[0];
|
||||
stroke.strokeWidth = 7;
|
||||
expect(stroke.strokeWidth).toBeCloseTo(7, 0);
|
||||
expect(stroke.strokeWidthTop).toBeCloseTo(7, 0);
|
||||
expect(stroke.strokeWidthRight).toBeCloseTo(7, 0);
|
||||
expect(stroke.strokeWidthBottom).toBeCloseTo(7, 0);
|
||||
expect(stroke.strokeWidthLeft).toBeCloseTo(7, 0);
|
||||
});
|
||||
|
||||
test('stroke reference and gradient members round-trip', (ctx) => {
|
||||
const r = rect(ctx);
|
||||
r.strokes = [{ strokeColor: '#000000', strokeWidth: 1 }];
|
||||
|
||||
43
plugins/libs/plugin-types/index.d.ts
vendored
43
plugins/libs/plugin-types/index.d.ts
vendored
@ -4164,8 +4164,8 @@ export interface Slide {
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents stroke properties in Penpot. You can add a stroke to any shape except for groups.
|
||||
* This interface includes properties for defining the color, style, width, alignment, and caps of a stroke.
|
||||
* Stroke properties for any shape except groups.
|
||||
* Literals apply strokeWidth before top/right/bottom/left widths.
|
||||
*/
|
||||
export interface Stroke {
|
||||
/**
|
||||
@ -4190,9 +4190,33 @@ export interface Stroke {
|
||||
*/
|
||||
strokeStyle?: 'solid' | 'dotted' | 'dashed' | 'mixed' | 'none' | 'svg';
|
||||
/**
|
||||
* The optional width of the stroke.
|
||||
* Reads the top width. Setting it selects simple mode and sets all four sides.
|
||||
*/
|
||||
strokeWidth?: number;
|
||||
/**
|
||||
* Computed width mode: simple when all four effective widths match, multiple otherwise.
|
||||
*/
|
||||
readonly strokeWidthType?: 'simple' | 'multiple';
|
||||
/**
|
||||
* Top side width; setting it preserves the other side widths.
|
||||
* Boards and rectangles only; other shape types raise a validation error.
|
||||
*/
|
||||
strokeWidthTop?: number;
|
||||
/**
|
||||
* Right side width; setting it preserves the other side widths.
|
||||
* Boards and rectangles only; other shape types raise a validation error.
|
||||
*/
|
||||
strokeWidthRight?: number;
|
||||
/**
|
||||
* Bottom side width; setting it preserves the other side widths.
|
||||
* Boards and rectangles only; other shape types raise a validation error.
|
||||
*/
|
||||
strokeWidthBottom?: number;
|
||||
/**
|
||||
* Left side width; setting it preserves the other side widths.
|
||||
* Boards and rectangles only; other shape types raise a validation error.
|
||||
*/
|
||||
strokeWidthLeft?: number;
|
||||
/**
|
||||
* The optional alignment of the stroke relative to the shape's boundary.
|
||||
*/
|
||||
@ -5465,7 +5489,11 @@ type TokenDimensionProps =
|
||||
| 'y'
|
||||
|
||||
// Stroke width
|
||||
| 'strokeWidth';
|
||||
| 'strokeWidth'
|
||||
| 'strokeWidthTop'
|
||||
| 'strokeWidthRight'
|
||||
| 'strokeWidthBottom'
|
||||
| 'strokeWidthLeft';
|
||||
|
||||
/**
|
||||
* The properties that a FontFamilies token can be applied to.
|
||||
@ -5534,7 +5562,12 @@ type TokenSpacingProps =
|
||||
/**
|
||||
* The properties that a BorderWidth token can be applied to.
|
||||
*/
|
||||
type TokenBorderWidthProps = 'strokeWidth';
|
||||
type TokenBorderWidthProps =
|
||||
| 'strokeWidth'
|
||||
| 'strokeWidthTop'
|
||||
| 'strokeWidthRight'
|
||||
| 'strokeWidthBottom'
|
||||
| 'strokeWidthLeft';
|
||||
|
||||
/**
|
||||
* The properties that a TextCase token can be applied to.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user