0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-04-10 14:01:29 -05:00

Ability to add multiple strokes to a shape

This commit is contained in:
Alejandro Alonso 2022-02-17 15:19:36 +01:00 committed by Alonso Torres
parent 719aacd6f8
commit a73a393e26
30 changed files with 680 additions and 353 deletions

View file

@ -5,6 +5,7 @@
### :boom: Breaking changes
### :sparkles: New features
- Ability to add multiple strokes to a shape [Taiga #2778](https://tree.taiga.io/project/penpot/us/2778)
- Scroll to selected size in font size selector [Taiga #2825](https://tree.taiga.io/project/penpot/us/2825)
- Duplicate artboards create new flows if needed [Taiga #2221](https://tree.taiga.io/project/penpot/issue/2221)
- Add new invitations section [Taiga #2797](https://tree.taiga.io/project/penpot/us/2797)

View file

@ -9,7 +9,7 @@
[app.common.colors :as clr]
[app.common.uuid :as uuid]))
(def file-version 14)
(def file-version 15)
(def default-color clr/gray-20)
(def root uuid/zero)
@ -32,6 +32,7 @@
:letter-spacing :text-display-group
:line-height :text-display-group
:text-align :text-display-group
:strokes :stroke-group
:stroke-color :stroke-group
:stroke-color-gradient :stroke-group
:stroke-color-ref-file :stroke-group
@ -84,7 +85,19 @@
:fill-color-ref-id
:fill-color-ref-file
:fill-color-gradient
:hide-fill-on-export}
:hide-fill-on-export
:strokes
:stroke-style
:stroke-alignment
:stroke-width
:stroke-color
:stroke-color-ref-id
:stroke-color-ref-file
:stroke-opacity
:stroke-color-gradient
:stroke-cap-start
:stroke-cap-end}
:group #{:proportion-lock
:width :height
@ -131,7 +144,8 @@
:fill-color-ref-id
:fill-color-ref-file
:fill-color-gradient
:strokes
:stroke-style
:stroke-alignment
:stroke-width
@ -171,6 +185,7 @@
:fill-color-ref-file
:fill-color-gradient
:strokes
:stroke-style
:stroke-alignment
:stroke-width
@ -210,6 +225,7 @@
:fill-color-ref-file
:fill-color-gradient
:strokes
:stroke-style
:stroke-alignment
:stroke-width
@ -339,6 +355,7 @@
:fill-color-ref-file
:fill-color-gradient
:strokes
:stroke-style
:stroke-alignment
:stroke-width

View file

@ -35,6 +35,7 @@
{:frame-id uuid/zero
:fills [{:fill-color clr/white
:fill-opacity 1}]
:strokes []
:shapes []
:hide-fill-on-export false})
@ -43,47 +44,32 @@
:name "Rect-1"
:fills [{:fill-color default-color
:fill-opacity 1}]
:stroke-style :none
:stroke-alignment :center
:stroke-width 0
:stroke-color clr/black
:stroke-opacity 0
:strokes []
:rx 0
:ry 0}
{:type :image
:rx 0
:ry 0
:fills []}
:fills []
:strokes []}
{:type :circle
:name "Circle-1"
:fills [{:fill-color default-color
:fill-opacity 1}]
:stroke-style :none
:stroke-alignment :center
:stroke-width 0
:stroke-color clr/black
:stroke-opacity 0}
:strokes []}
{:type :path
:name "Path-1"
:fills []
:stroke-style :solid
:stroke-alignment :center
:stroke-width 2
:stroke-color clr/black
:stroke-opacity 1}
:strokes []}
{:type :frame
:name "Artboard-1"
:fills [{:fill-color clr/white
:fill-opacity 1}]
:stroke-style :none
:stroke-alignment :center
:stroke-width 0
:stroke-color clr/black
:stroke-opacity 0}
:strokes []}
{:type :text
:name "Text-1"

View file

@ -310,13 +310,9 @@
:fill-opacity (:fill-opacity shape)}
clean-attrs (d/without-nils attrs)]
(-> shape
(assoc :fills [clean-attrs])
(dissoc :fill-color)
(dissoc :fill-color-gradient)
(dissoc :fill-color-ref-file)
(dissoc :fill-color-ref-id)
(dissoc :fill-opacity))))
(cond-> shape
(not (empty? clean-attrs))
(assoc :fills [clean-attrs]))))
;; Add fills to shapes
(defmethod migrate 14
@ -328,5 +324,34 @@
(update-page [_ page]
(update page :objects #(d/mapm update-object %)))]
(update data :pages-index #(d/mapm update-page %))))
(defn set-strokes
[shape]
(let [attrs {:stroke-style (:stroke-style shape)
:stroke-alignment (:stroke-alignment shape)
:stroke-width (:stroke-width shape)
:stroke-color (:stroke-color shape)
:stroke-color-ref-id (:stroke-color-ref-id shape)
:stroke-color-ref-file (:stroke-color-ref-file shape)
:stroke-opacity (:stroke-opacity shape)
:stroke-color-gradient (:stroke-color-gradient shape)
:stroke-cap-start (:stroke-cap-start shape)
:stroke-cap-end (:stroke-cap-end shape)}
clean-attrs (d/without-nils attrs)]
(cond-> shape
(not (empty? clean-attrs))
(assoc :strokes [clean-attrs]))))
;; Add strokes to shapes
(defmethod migrate 15
[data]
(letfn [(update-object [_ object]
(cond-> object
(and (not (= :text (:type object))) (nil? (:strokes object)))
(set-strokes)))
(update-page [_ page]
(update page :objects #(d/mapm update-object %)))]
(update data :pages-index #(d/mapm update-page %))))

View file

@ -463,6 +463,12 @@
}
}
.element-set-content .border-data {
&[draggable="true"] {
cursor: pointer;
}
}
.element-set-content .grid-option-main {
.editable-select {
height: 2rem;

View file

@ -135,12 +135,12 @@
(rx/from (map #(dwt/update-text-with-function % transform-attrs) text-ids))
(rx/of (dch/update-shapes shape-ids transform-attrs)))))
(defn swap-fills [shape index new-index]
(let [first (get-in shape [:fills index])
second (get-in shape [:fills new-index])]
(defn swap-attrs [shape attr index new-index]
(let [first (get-in shape [attr index])
second (get-in shape [attr new-index])]
(-> shape
(assoc-in [:fills index] second)
(assoc-in [:fills new-index] first))))
(assoc-in [attr index] second)
(assoc-in [attr new-index] first))))
(defn reorder-fills
[ids index new-index]
@ -152,7 +152,7 @@
is-text? #(= :text (:type (get objects %)))
text-ids (filter is-text? ids)
shape-ids (remove is-text? ids)
transform-attrs #(swap-fills % index new-index)]
transform-attrs #(swap-attrs % :fills index new-index)]
(rx/concat
(rx/from (map #(dwt/update-text-with-function % transform-attrs) text-ids))
@ -225,37 +225,72 @@
shape))))))))
(defn change-stroke
[ids color]
[ids attrs index]
(ptk/reify ::change-stroke
ptk/WatchEvent
(watch [_ _ _]
(let [attrs (cond-> {:stroke-color nil
:stroke-color-ref-id nil
:stroke-color-ref-file nil
:stroke-color-gradient nil
:stroke-opacity nil}
(contains? color :color)
(assoc :stroke-color (:color color))
(let [color-attrs (cond-> {}
(contains? attrs :color)
(assoc :stroke-color (:color attrs))
(contains? color :id)
(assoc :stroke-color-ref-id (:id color))
(contains? attrs :id)
(assoc :stroke-color-ref-id (:id attrs))
(contains? color :file-id)
(assoc :stroke-color-ref-file (:file-id color))
(contains? attrs :file-id)
(assoc :stroke-color-ref-file (:file-id attrs))
(contains? color :gradient)
(assoc :stroke-color-gradient (:gradient color))
(contains? attrs :gradient)
(assoc :stroke-color-gradient (:gradient attrs))
(contains? color :opacity)
(assoc :stroke-opacity (:opacity color)))]
(contains? attrs :opacity)
(assoc :stroke-opacity (:opacity attrs)))
attrs (merge attrs color-attrs)]
(rx/of (dch/update-shapes ids (fn [shape]
(cond-> (d/merge shape attrs)
(= (:stroke-style shape) :none)
(assoc :stroke-style :solid
:stroke-width 1
:stroke-opacity 1)))))))))
(assoc-in shape [:strokes index] (merge (get-in shape [:strokes index]) attrs)))))))))
(defn add-stroke
[ids stroke]
(ptk/reify ::add-stroke
ptk/WatchEvent
(watch [_ _ _]
(let [add (fn [shape attrs] (assoc shape :strokes (into [attrs] (:strokes shape))))]
(rx/of (dch/update-shapes
ids
#(add % stroke)))))))
(defn remove-stroke
[ids position]
(ptk/reify ::remove-stroke
ptk/WatchEvent
(watch [_ _ _]
(let [remove-fill-by-index (fn [values index] (->> (d/enumerate values)
(filterv (fn [[idx _]] (not= idx index)))
(mapv second)))
remove (fn [shape] (update shape :strokes remove-fill-by-index position))]
(rx/of (dch/update-shapes
ids
#(remove %)))))))
(defn remove-all-strokes
[ids]
(ptk/reify ::remove-all-strokes
ptk/WatchEvent
(watch [_ _ _]
(let [remove-all (fn [shape] (assoc shape :strokes []))]
(rx/of (dch/update-shapes
ids
#(remove-all %)))))))
(defn reorder-strokes
[ids index new-index]
(ptk/reify ::reorder-strokes
ptk/WatchEvent
(watch [_ _ _]
(rx/of (dch/update-shapes
ids
#(swap-attrs % :strokes index new-index))))))
(defn picker-for-selected-shape
[]

View file

@ -101,25 +101,26 @@
(get-in shape [:svg-attrs :style :stroke-linecap]))
((d/nilf str/trim))
((d/nilf keyword)))
shape
(cond-> shape
(uc/color? (get-in shape [:svg-attrs :stroke]))
(uc/color? (str/trim (get-in shape [:svg-attrs :stroke])))
(-> (update :svg-attrs dissoc :stroke)
(assoc :stroke-color (get-in shape [:svg-attrs :stroke])))
(assoc-in [:strokes 0 :stroke-color] (get-in shape [:svg-attrs :stroke])))
(uc/color? (get-in shape [:svg-attrs :style :stroke]))
(uc/color? (str/trim (get-in shape [:svg-attrs :style :stroke])))
(-> (update-in [:svg-attrs :style] dissoc :stroke)
(assoc :stroke-color (get-in shape [:svg-attrs :style :stroke])))
(assoc-in [:strokes 0 :stroke-color] (get-in shape [:svg-attrs :style :stroke])))
(get-in shape [:svg-attrs :stroke-width])
(-> (update :svg-attrs dissoc :stroke-width)
(assoc :stroke-width (-> (get-in shape [:svg-attrs :stroke-width])
(d/parse-double))))
(assoc-in [:strokes 0 :stroke-width] (-> (get-in shape [:svg-attrs :stroke-width])
(d/parse-double))))
(get-in shape [:svg-attrs :style :stroke-width])
(-> (update-in [:svg-attrs :style] dissoc :stroke-width)
(assoc :stroke-width (-> (get-in shape [:svg-attrs :style :stroke-width])
(d/parse-double))))
(assoc-in [:strokes 0 :stroke-width] (-> (get-in shape [:svg-attrs :style :stroke-width])
(d/parse-double))))
(and stroke-linecap (= (:type shape) :path))
(-> (update-in [:svg-attrs :style] dissoc :stroke-linecap)
@ -128,8 +129,8 @@
(assoc :stroke-cap-start stroke-linecap
:stroke-cap-end stroke-linecap))))]
(if (d/any-key? shape :stroke-color :stroke-opacity :stroke-width :stroke-cap-start :stroke-cap-end)
(merge {:stroke-style :svg} shape)
(if (d/any-key? (get-in [:strokes 0] shape) :stroke-color :stroke-opacity :stroke-width :stroke-cap-start :stroke-cap-end)
(assoc-in shape [:strokes 0 :stroke-style] :svg)
shape)))
(defn setup-opacity [shape]
@ -383,6 +384,7 @@
#_other (create-raw-svg name frame-id svg-data element-data)))
shape (assoc shape :fills [])
shape (assoc shape :strokes [])
shape (when (some? shape)
(-> shape

View file

@ -113,9 +113,9 @@
(obj/merge! attrs (clj->js fill-attrs)))))
(defn add-stroke [attrs shape render-id]
(defn add-stroke [attrs shape render-id index]
(let [stroke-style (:stroke-style shape :none)
stroke-color-gradient-id (str "stroke-color-gradient_" render-id)
stroke-color-gradient-id (str "stroke-color-gradient_" render-id "_" index)
stroke-width (:stroke-width shape 1)]
(if (not= stroke-style :none)
(let [stroke-attrs
@ -198,14 +198,13 @@
styles (-> (obj/get props "style" (obj/new))
(obj/merge! svg-styles)
(add-stroke shape render-id)
(add-layer-props shape))
styles (cond (or (some? (:fill-image shape))
(= :image (:type shape))
(> (count (:fills shape)) 1)
(some #(some? (:fill-color-gradient %)) (:fills shape)))
(obj/set! styles "fill" (str "url(#fill-0-" render-id ")"))
(obj/set! styles "fill" (str "url(#fill-0-" render-id ")"))
;; imported svgs can have fill and fill-opacity attributes
(obj/contains? svg-styles "fill")
@ -233,7 +232,15 @@
(-> (obj/new)
(obj/set! "style" fill-styles))))
(defn extract-stroke-attrs
[shape index]
(let [render-id (mf/use-ctx muc/render-ctx)
stroke-styles (-> (obj/get shape "style" (obj/new))
(add-stroke shape render-id index))]
(-> (obj/new)
(obj/set! "style" stroke-styles))))
(defn extract-border-radius-attrs
[shape]
(-> (obj/new)
(add-border-radius shape)))
(-> (obj/new)
(add-border-radius shape)))

View file

@ -8,7 +8,7 @@
(:require
[app.common.geom.shapes :as geom]
[app.main.ui.shapes.attrs :as attrs]
[app.main.ui.shapes.custom-stroke :refer [shape-custom-stroke]]
[app.main.ui.shapes.custom-stroke :refer [shape-custom-strokes]]
[app.util.object :as obj]
[rumext.alpha :as mf]))
@ -32,5 +32,5 @@
:ry ry
:transform transform}))]
[:& shape-custom-stroke {:shape shape}
[:& shape-custom-strokes {:shape shape}
[:> :ellipse props]]))

View file

@ -6,8 +6,11 @@
(ns app.main.ui.shapes.custom-stroke
(:require
[app.common.data :as d]
[app.common.geom.shapes :as gsh]
[app.main.ui.context :as muc]
[app.main.ui.shapes.attrs :as attrs]
[app.main.ui.shapes.gradients :as grad]
[app.util.object :as obj]
[cuerdas.core :as str]
[rumext.alpha :as mf]))
@ -39,8 +42,20 @@
stroke-width (case (:stroke-alignment shape :center)
:center (/ (:stroke-width shape 0) 2)
:outer (:stroke-width shape 0)
0)]
[:mask {:id stroke-mask-id}
0)
margin (gsh/shape-stroke-margin shape stroke-width)
bounding-box (-> (gsh/points->selrect (:points shape))
(update :x - (+ stroke-width margin))
(update :y - (+ stroke-width margin))
(update :width + (* 2 (+ stroke-width margin)))
(update :height + (* 2 (+ stroke-width margin))))]
[:mask {:id stroke-mask-id
:x (:x bounding-box)
:y (:y bounding-box)
:width (:width bounding-box)
:height (:height bounding-box)
:maskUnits "userSpaceOnUse"}
[:use {:xlinkHref (str "#" shape-id)
:style #js {:fill "none" :stroke "white" :strokeWidth (* stroke-width 2)}}]
@ -49,13 +64,13 @@
:stroke "none"}}]]))
(mf/defc cap-markers
[{:keys [shape render-id]}]
[{:keys [shape render-id index]}]
(let [marker-id-prefix (str "marker-" render-id)
cap-start (:stroke-cap-start shape)
cap-end (:stroke-cap-end shape)
stroke-color (if (:stroke-color-gradient shape)
(str/format "url(#%s)" (str "stroke-color-gradient_" render-id))
(str/format "url(#%s)" (str "stroke-color-gradient_" render-id "_" index))
(:stroke-color shape))
stroke-opacity (when-not (:stroke-color-gradient shape)
@ -154,26 +169,35 @@
[{:keys [shape render-id index]}]
(let [open-path? (and (= :path (:type shape)) (gsh/open-path? shape))]
(cond
(and (not open-path?)
(= :inner (:stroke-alignment shape :center))
(> (:stroke-width shape 0) 0))
[:& inner-stroke-clip-path {:shape shape
:render-id render-id
:index index}]
[:*
(cond (some? (:stroke-color-gradient shape))
(case (:type (:stroke-color-gradient shape))
:linear [:> grad/linear-gradient #js {:id (str (name :stroke-color-gradient) "_" render-id "_" index)
:gradient (:stroke-color-gradient shape)
:shape shape}]
:radial [:> grad/radial-gradient #js {:id (str (name :stroke-color-gradient) "_" render-id "_" index)
:gradient (:stroke-color-gradient shape)
:shape shape}]))
(cond
(and (not open-path?)
(= :inner (:stroke-alignment shape :center))
(> (:stroke-width shape 0) 0))
[:& inner-stroke-clip-path {:shape shape
:render-id render-id
:index index}]
(and (not open-path?)
(= :outer (:stroke-alignment shape :center))
(> (:stroke-width shape 0) 0))
[:& outer-stroke-mask {:shape shape
:render-id render-id
:index index}]
(and (not open-path?)
(= :outer (:stroke-alignment shape :center))
(> (:stroke-width shape 0) 0))
[:& outer-stroke-mask {:shape shape
:render-id render-id
:index index}]
(or (some? (:stroke-cap-start shape))
(some? (:stroke-cap-end shape)))
[:& cap-markers {:shape shape
:render-id render-id
:index index}])))
(or (some? (:stroke-cap-start shape))
(some? (:stroke-cap-end shape)))
[:& cap-markers {:shape shape
:render-id render-id
:index index}])]))
;; Outer alignment: display the shape in two layers. One
;; without stroke (only fill), and another one only with stroke
@ -265,6 +289,7 @@
(let [child (obj/get props "children")
shape (obj/get props "shape")
render-id (mf/use-ctx muc/render-ctx)
index (obj/get props "index")
stroke-width (:stroke-width shape 0)
stroke-style (:stroke-style shape :none)
@ -286,5 +311,62 @@
child]
:else
child)))
[:g.stroke-shape
[:defs
[:& stroke-defs {:shape shape :render-id render-id :index index}]]
child])))
(defn build-stroke-props [position shape child value]
(let [render-id (mf/use-ctx muc/render-ctx)
url-fill? (or (some? (:fill-image shape))
(= :image (:type shape))
(> (count (:fills shape)) 1)
(some :fill-color-gradient (:fills shape)))
one-fill? (= (count (:fills shape)) 1)
no-fills? (= (count (:fills shape)) 0)
last-stroke? (= position (- (count (:strokes shape)) 1))
props (-> (obj/get child "props")
(obj/clone))
props (cond
(and last-stroke? url-fill?)
;; TODO: check this zero
(obj/set! props "fill" (str "url(#fill-0-" render-id ")"))
(and last-stroke? one-fill?)
(obj/merge!
props
(attrs/extract-fill-attrs (get-in shape [:fills 0]) render-id 0))
:else
(-> props
(obj/without ["fill" "fillOpacity"])
(obj/set!
"style"
(-> (obj/get props "style")
(obj/set! "fill" "none")
(obj/set! "fillOpacity" "none")))))
props (-> props
(add-style
(obj/get (attrs/extract-stroke-attrs value position) "style")))]
props))
(mf/defc shape-custom-strokes
{::mf/wrap-props false}
[props]
(let [child (obj/get props "children")
shape (obj/get props "shape")
elem-name (obj/get child "type")]
(cond
(seq (:strokes shape))
[:*
(for [[index value] (-> (d/enumerate (:strokes shape)) reverse)]
[:& shape-custom-stroke {:shape (assoc value :points (:points shape)) :index index}
[:> elem-name (build-stroke-props index shape child value)]])]
:else
[:& shape-custom-stroke {:shape shape :index 0}
child])))

View file

@ -11,6 +11,7 @@
(:require
[app.common.data :as d]
[app.common.geom.shapes :as gsh]
[app.main.ui.context :as muc]
[app.util.json :as json]
[app.util.object :as obj]
[app.util.svg :as usvg]
@ -272,6 +273,39 @@
(for [leaf (->> shape :content :content (filter string?))]
[:> "penpot:svg-child" {} leaf])]))]))
(defn- export-fills-data [{:keys [fills]}]
(when-let [fills (seq fills)]
(mf/html
[:> "penpot:fills" #js {}
(for [[index fill] (d/enumerate fills)]
[:> "penpot:fill"
#js {:penpot:fill-color (if (some? (:fill-color-gradient fill))
(str/format "url(#%s)" (str "fill-color-gradient_" (mf/use-ctx muc/render-ctx) "_" index))
(d/name (:fill-color fill)))
:penpot:fill-color-ref-file (d/name (:fill-color-ref-file fill))
:penpot:fill-color-ref-id (d/name (:fill-color-ref-id fill))
:penpot:fill-opacity (d/name (:fill-opacity fill))}])])))
(defn- export-strokes-data [{:keys [strokes]}]
(when-let [strokes (seq strokes)]
(mf/html
[:> "penpot:strokes" #js {}
(for [[index stroke] (d/enumerate strokes)]
[:> "penpot:stroke"
#js {:penpot:stroke-color (if (some? (:stroke-color-gradient stroke))
(str/format "url(#%s)" (str "stroke-color-gradient_" (mf/use-ctx muc/render-ctx) "_" index))
(d/name (:stroke-color stroke)))
:penpot:stroke-color-ref-file (d/name (:stroke-color-ref-file stroke))
:penpot:stroke-color-ref-id (d/name (:stroke-color-ref-id stroke))
:penpot:stroke-opacity (d/name (:stroke-opacity stroke))
:penpot:stroke-style (d/name (:stroke-style stroke))
:penpot:stroke-width (d/name (:stroke-width stroke))
:penpot:stroke-alignment (d/name (:stroke-alignment stroke))
:penpot:stroke-cap-start (d/name (:stroke-cap-start stroke))
:penpot:stroke-cap-end (d/name (:stroke-cap-end stroke))}])])))
(defn- export-interactions-data [{:keys [interactions]}]
(when-let [interactions (seq interactions)]
(mf/html
@ -300,5 +334,7 @@
(export-exports-data shape)
(export-svg-data shape)
(export-interactions-data shape)
(export-fills-data shape)
(export-strokes-data shape)
(export-grid-data shape)]))

View file

@ -29,7 +29,7 @@
(let [{:keys [x y width height]} (:selrect shape)
{:keys [metadata]} shape
has-image (or metadata (:fill-image shape))
uri (if metadata
(cfg/resolve-file-media metadata)

View file

@ -201,11 +201,11 @@
:height (- y2 y1)})))))
(defn calculate-padding [shape]
(let [stroke-width (case (:stroke-alignment shape :center)
:center (/ (:stroke-width shape 0) 2)
:outer (:stroke-width shape 0)
0)
margin (gsh/shape-stroke-margin shape stroke-width)]
(let [stroke-width (apply max 0 (map #(case (:stroke-alignment % :center)
:center (/ (:stroke-width % 0) 2)
:outer (:stroke-width % 0)
0) (:strokes shape)))
margin (apply max 0 (map #(gsh/shape-stroke-margin % stroke-width) (:strokes shape)))]
(+ stroke-width margin)))
(defn change-filter-in

View file

@ -8,6 +8,8 @@
(:require
[app.common.data.macros :as dm]
[app.main.ui.shapes.attrs :as attrs]
[app.main.ui.shapes.custom-stroke :refer [shape-custom-strokes]]
[app.main.ui.shapes.filters :as filters]
[app.util.object :as obj]
[debug :refer [debug?]]
[rumext.alpha :as mf]))
@ -24,9 +26,10 @@
(mf/defc frame-clip-def
[{:keys [shape render-id]}]
(when (= :frame (:type shape))
(let [{:keys [x y width height]} shape]
(let [{:keys [x y width height]} shape
padding (filters/calculate-padding shape)]
[:clipPath {:id (frame-clip-id shape render-id) :class "frame-clip"}
[:rect {:x x :y y :width width :height height}]])))
[:rect {:x (- x padding) :y (- y padding) :width (+ width (* 2 padding)) :height (+ height (* 2 padding))}]])))
(mf/defc frame-thumbnail
{::mf/wrap-props false}
@ -59,8 +62,10 @@
:width width
:height height
:className "frame-background"}))]
[:*
[:> :rect props]
[:& shape-custom-strokes {:shape shape}
[:> :rect props]]
(for [item childs]
[:& shape-wrapper {:shape item

View file

@ -8,7 +8,7 @@
(:require
[app.common.geom.shapes :as gsh]
[app.main.ui.shapes.attrs :as attrs]
[app.main.ui.shapes.custom-stroke :refer [shape-custom-stroke]]
[app.main.ui.shapes.custom-stroke :refer [shape-custom-strokes]]
[app.util.object :as obj]
[rumext.alpha :as mf]))
@ -29,8 +29,7 @@
:height height}))
path? (some? (.-d props))]
[:g
[:& shape-custom-stroke {:shape shape}
(if path?
[:> :path props]
[:> :rect props])]]))
[:& shape-custom-strokes {:shape shape}
(if path?
[:> :path props]
[:> :rect props])]))

View file

@ -8,7 +8,7 @@
(:require
[app.common.logging :as log]
[app.main.ui.shapes.attrs :as attrs]
[app.main.ui.shapes.custom-stroke :refer [shape-custom-stroke]]
[app.main.ui.shapes.custom-stroke :refer [shape-custom-strokes]]
[app.util.object :as obj]
[app.util.path.format :as upf]
[rumext.alpha :as mf]))
@ -31,5 +31,5 @@
props (-> (attrs/extract-style-attrs shape)
(obj/set! "d" pdata))]
[:& shape-custom-stroke {:shape shape}
[:& shape-custom-strokes {:shape shape}
[:> :path props]]))

View file

@ -8,7 +8,7 @@
(:require
[app.common.geom.shapes :as gsh]
[app.main.ui.shapes.attrs :as attrs]
[app.main.ui.shapes.custom-stroke :refer [shape-custom-stroke]]
[app.main.ui.shapes.custom-stroke :refer [shape-custom-strokes]]
[app.util.object :as obj]
[rumext.alpha :as mf]))
@ -29,7 +29,7 @@
path? (some? (.-d props))]
[:& shape-custom-stroke {:shape shape}
[:& shape-custom-strokes {:shape shape}
(if path?
[:> :path props]
[:> :rect props])]))

View file

@ -14,7 +14,6 @@
[app.main.ui.shapes.fills :as fills]
[app.main.ui.shapes.filters :as filters]
[app.main.ui.shapes.frame :as frame]
[app.main.ui.shapes.gradients :as grad]
[app.main.ui.shapes.svg-defs :as defs]
[app.util.object :as obj]
[rumext.alpha :as mf]))
@ -62,7 +61,6 @@
[:defs
[:& defs/svg-defs {:shape shape :render-id render-id}]
[:& filters/filters {:shape shape :filter-id filter-id}]
[:& grad/gradient {:shape shape :attr :stroke-color-gradient}]
[:& fills/fills {:shape shape :render-id render-id}]
[:& frame/frame-clip-def {:shape shape :render-id render-id}]]
children]]))

View file

@ -10,7 +10,7 @@
[app.common.geom.shapes :as gsh]
[app.main.ui.context :as muc]
[app.main.ui.shapes.attrs :as attrs]
[app.main.ui.shapes.custom-stroke :refer [shape-custom-stroke]]
[app.main.ui.shapes.custom-stroke :refer [shape-custom-strokes]]
[app.main.ui.shapes.gradients :as grad]
[app.util.object :as obj]
[rumext.alpha :as mf]))
@ -21,7 +21,7 @@
{::mf/wrap-props false
::mf/wrap [mf/memo]}
[props]
(let [render-id (mf/use-ctx muc/render-ctx)
{:keys [x y width height position-data] :as shape} (obj/get props "shape")
transform (str (gsh/transform-matrix shape))
@ -60,7 +60,7 @@
:direction (if (:rtl data) "rtl" "ltr")
:whiteSpace "pre"}
(obj/set! "fill" (str "url(#fill-" index "-" render-id ")")))})]
[:& shape-custom-stroke {:shape shape :index index}
[:& shape-custom-strokes {:shape shape}
[:> :text props (:text data)]]))]]))

View file

@ -21,7 +21,7 @@
(def type->options
{:multiple [:fill :stroke :image :text :shadow :blur]
:frame [:layout :fill]
:frame [:layout :fill :stroke]
:group [:layout :svg]
:rect [:layout :fill :stroke :shadow :blur :svg]
:circle [:layout :fill :stroke :shadow :blur :svg]

View file

@ -30,10 +30,13 @@
color (-> shape shape->color uc/color->background)]
(str/format "%spx %s %s" width style color)))
(defn has-stroke? [{:keys [stroke-style]}]
(and stroke-style
(and (not= stroke-style :none)
(not= stroke-style :svg))))
(defn has-stroke? [shape]
(let [stroke-style (:stroke-style shape)]
(or
(and stroke-style
(and (not= stroke-style :none)
(not= stroke-style :svg)))
(seq (:strokes shape)))))
(defn copy-stroke-data [shape]
(cg/generate-css-props
@ -80,6 +83,11 @@
[:& copy-button {:data (copy-stroke-data (first shapes))}])]
(for [shape shapes]
[:& stroke-block {:key (str "stroke-color-" (:id shape))
:shape shape
:locale locale}])])))
(if (seq (:strokes shape))
(for [value (:strokes shape [])]
[:& stroke-block {:key (str "stroke-color-" (:id shape))
:shape value
:locale locale}])
[:& stroke-block {:key (str "stroke-color-" (:id shape))
:shape shape
:locale locale}]))])))

View file

@ -45,7 +45,7 @@
select-color
(fn [event]
(if (kbd/alt? event)
(st/emit! (mdc/change-stroke ids-with-children (merge uc/empty-color color)))
(st/emit! (mdc/change-stroke ids-with-children (merge uc/empty-color color) 0))
(st/emit! (mdc/change-fill ids-with-children (merge uc/empty-color color) 0))))]
[:div.color-cell {:on-click select-color}

View file

@ -803,7 +803,7 @@
(fn [_ event]
(let [ids (wsh/lookup-selected @st/state)]
(if (kbd/alt? event)
(st/emit! (dc/change-stroke ids color))
(st/emit! (dc/change-stroke ids color 0))
(st/emit! (dc/change-fill ids color 0)))))
rename-color

View file

@ -78,7 +78,6 @@
[:div.element-options
[:& interactions-menu {:shape (first shapes)}]]]]]])
;; TODO: this need optimizations, selected-objects and
;; selected-objects-with-children are derefed always but they only
;; need on multiple selection in majority of cases
@ -93,6 +92,8 @@
file-id (mf/use-ctx ctx/current-file-id)
shapes (mf/deref refs/selected-objects)
shapes-with-children (mf/deref refs/selected-shapes-with-children)]
;; TODO: review performance]
[:& options-content {:shapes shapes
:selected selected
:shapes-with-children shapes-with-children

View file

@ -98,7 +98,17 @@
(mf/deps ids)
(fn [event]
(let [value (-> event dom/get-target dom/checked?)]
(st/emit! (dc/change-hide-fill-on-export ids (not value))))))]
(st/emit! (dc/change-hide-fill-on-export ids (not value))))))
disable-drag (mf/use-state false)
select-all (fn [event]
(when (not @disable-drag)
(dom/select-text! (dom/get-target event)))
(reset! disable-drag true))
on-blur (fn [_]
(reset! disable-drag false))]
(mf/use-layout-effect
(mf/deps hide-fill-on-export?)
@ -139,7 +149,10 @@
:on-change (on-change index)
:on-reorder (on-reorder index)
:on-detach (on-detach index)
:on-remove (on-remove index)}])])
:on-remove (on-remove index)
:disable-drag disable-drag
:select-all select-all
:on-blur on-blur}])])
(when (or (= type :frame)
(and (= type :multiple) (some? hide-fill-on-export?)))

View file

@ -8,20 +8,19 @@
(:require
[app.common.colors :as clr]
[app.common.data :as d]
[app.main.data.workspace.changes :as dch]
[app.main.data.workspace.colors :as dc]
[app.main.store :as st]
[app.main.ui.components.dropdown :refer [dropdown]]
[app.main.ui.components.numeric-input :refer [numeric-input]]
[app.main.ui.hooks :as h]
[app.main.ui.icons :as i]
[app.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row]]
[app.main.ui.workspace.sidebar.options.rows.stroke-row :refer [stroke-row]]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[cuerdas.core :as str]
[rumext.alpha :as mf]))
(def stroke-attrs
[:stroke-style
[:strokes
:stroke-style
:stroke-alignment
:stroke-width
:stroke-color
@ -32,36 +31,6 @@
:stroke-cap-start
:stroke-cap-end])
(defn- width->string [width]
(if (= width :multiple)
""
(str (or width 1))))
(defn- enum->string [value]
(if (= value :multiple)
""
(pr-str value)))
(defn- stroke-cap-names []
[[nil (tr "workspace.options.stroke-cap.none") false]
[:line-arrow (tr "workspace.options.stroke-cap.line-arrow") true]
[:triangle-arrow (tr "workspace.options.stroke-cap.triangle-arrow") false]
[:square-marker (tr "workspace.options.stroke-cap.square-marker") false]
[:circle-marker (tr "workspace.options.stroke-cap.circle-marker") false]
[:diamond-marker (tr "workspace.options.stroke-cap.diamond-marker") false]
[:round (tr "workspace.options.stroke-cap.round") true]
[:square (tr "workspace.options.stroke-cap.square") false]])
(defn- value->name [value]
(if (= value :multiple)
"--"
(-> (d/seek #(= (first %) value) (stroke-cap-names))
(second))))
(defn- value->img [value]
(when (and value (not= value :multiple))
(str "images/cap-" (name value) ".svg")))
(mf/defc stroke-menu
{::mf/wrap [#(mf/memo' % (mf/check-props ["ids" "values" "type" "show-caps"]))]}
[{:keys [ids type values show-caps] :as props}]
@ -70,65 +39,62 @@
:group (tr "workspace.options.group-stroke")
(tr "workspace.options.stroke"))
show-options (not= (or (:stroke-style values) :none) :none)
show-caps (and show-caps
(not (#{:inner :outer} (:stroke-alignment values))))
start-caps-state (mf/use-state {:open? false
:top 0
:left 0})
end-caps-state (mf/use-state {:open? false
:top 0
:left 0})
current-stroke-color {:color (:stroke-color values)
:opacity (:stroke-opacity values)
:id (:stroke-color-ref-id values)
:file-id (:stroke-color-ref-file values)
:gradient (:stroke-color-gradient values)}
handle-change-stroke-color
(mf/use-callback
(mf/deps ids)
(fn [color]
(let [remove-multiple (fn [[_ value]] (not= value :multiple))
color (into {} (filter remove-multiple) color)]
(st/emit! (dc/change-stroke ids color)))))
(mf/deps ids)
(fn [index]
(fn [color]
(st/emit! (dc/change-stroke ids color index)))))
handle-remove
(mf/use-callback
(mf/deps ids)
(fn [index]
(fn []
(st/emit! (dc/remove-stroke ids index)))))
handle-remove-remove-all
(fn [_]
(st/emit! (dc/remove-all-strokes ids)))
handle-detach
(mf/use-callback
(mf/deps ids)
(fn []
(let [remove-multiple (fn [[_ value]] (not= value :multiple))
current-stroke-color (-> (into {} (filter remove-multiple) current-stroke-color)
(assoc :id nil :file-id nil))]
(st/emit! (dc/change-stroke ids current-stroke-color)))))
(mf/deps ids)
(fn [index]
(fn [color]
(let [color (-> color
(assoc :id nil :file-id nil))]
(st/emit! (dc/change-stroke ids color index))))))
handle-reorder
(mf/use-callback
(mf/deps ids)
(fn [new-index]
(fn [index]
(st/emit! (dc/reorder-strokes ids index new-index)))))
on-stroke-style-change
(fn [event]
(let [value (-> (dom/get-target event)
(dom/get-value)
(d/read-string))]
(st/emit! (dch/update-shapes ids #(assoc % :stroke-style value)))))
(fn [index]
(fn [event]
(let [value (-> (dom/get-target event)
(dom/get-value)
(d/read-string))]
(st/emit! (dc/change-stroke ids {:stroke-style value} index)))))
on-stroke-alignment-change
(fn [event]
(let [value (-> (dom/get-target event)
(dom/get-value)
(d/read-string))]
(when-not (str/empty? value)
(st/emit! (dch/update-shapes ids #(assoc % :stroke-alignment value))))))
(fn [index]
(fn [event]
(let [value (-> (dom/get-target event)
(dom/get-value)
(d/read-string))]
(when-not (str/empty? value)
(st/emit! (dc/change-stroke ids {:stroke-alignment value} index))))))
on-stroke-width-change
(fn [value]
(when-not (str/empty? value)
(st/emit! (dch/update-shapes ids #(assoc % :stroke-width value)))))
update-cap-attr
(fn [& kvs]
#(if (= :path (:type %))
(apply (partial assoc %) kvs)
%))
(fn [index]
(fn [value]
(when-not (str/empty? value)
(st/emit! (dc/change-stroke ids {:stroke-width value} index)))))
open-caps-select
(fn [caps-state]
@ -146,8 +112,8 @@
(:left rect)
(- (:width window-size) 205))]
(swap! caps-state assoc :open? true
:left left
:top top))))
:left left
:top top))))
close-caps-select
(fn [caps-state]
@ -155,119 +121,72 @@
(swap! caps-state assoc :open? false)))
on-stroke-cap-start-change
(fn [value]
(st/emit! (dch/update-shapes ids (update-cap-attr :stroke-cap-start value))))
(fn [index value]
(st/emit! (dc/change-stroke ids {:stroke-cap-start value} index)))
on-stroke-cap-end-change
(fn [value]
(st/emit! (dch/update-shapes ids (update-cap-attr :stroke-cap-end value))))
(fn [index value]
(st/emit! (dc/change-stroke ids {:stroke-cap-end value} index)))
on-stroke-cap-switch
(fn [_]
(let [stroke-cap-start (:stroke-cap-start values)
stroke-cap-end (:stroke-cap-end values)]
(fn [index]
(let [stroke-cap-start (get-in values [:strokes index :stroke-cap-start])
stroke-cap-end (get-in values [:strokes index :stroke-cap-end])]
(when (and (not= stroke-cap-start :multiple)
(not= stroke-cap-end :multiple))
(st/emit! (dch/update-shapes ids (update-cap-attr
:stroke-cap-start stroke-cap-end
:stroke-cap-end stroke-cap-start))))))
(st/emit! (dc/change-stroke ids {:stroke-cap-start stroke-cap-end
:stroke-cap-end stroke-cap-start} index)))))
on-add-stroke
(fn [_]
(st/emit! (dch/update-shapes ids #(assoc %
:stroke-style :solid
:stroke-color clr/black
:stroke-opacity 1
:stroke-width 1))))
(st/emit! (dc/add-stroke ids {:stroke-style :solid
:stroke-color clr/black
:stroke-opacity 1
:stroke-width 1})))
on-del-stroke
(fn [_]
(st/emit! (dch/update-shapes ids #(assoc % :stroke-style :none))))]
disable-drag (mf/use-state false)
(if show-options
[:div.element-set
[:div.element-set-title
[:span label]
[:div.add-page {:on-click on-del-stroke} i/minus]]
select-all (fn [event]
(when (not @disable-drag)
(dom/select-text! (dom/get-target event)))
(reset! disable-drag true))
[:div.element-set-content
;; Stroke Color
[:& color-row {:color current-stroke-color
:title (tr "workspace.options.stroke-color")
:on-change handle-change-stroke-color
:on-detach handle-detach}]
on-blur (fn [_]
(reset! disable-drag false))]
;; Stroke Width, Alignment & Style
[:div.row-flex
[:div.input-element
{:class (dom/classnames :pixels (not= (:stroke-width values) :multiple))
:title (tr "workspace.options.stroke-width")}
[:div.element-set
[:div.element-set-title
[:span label]
[:div.add-page {:on-click on-add-stroke} i/close]]
[:> numeric-input
{:min 0
:value (-> (:stroke-width values) width->string)
:precision 2
:placeholder (tr "settings.multiple")
:on-change on-stroke-width-change}]]
[:div.element-set-content
(cond
(= :multiple (:strokes values))
[:div.element-set-options-group
[:div.element-set-label (tr "settings.multiple")]
[:div.element-set-actions
[:div.element-set-actions-button {:on-click handle-remove-remove-all}
i/minus]]]
[:select#style.input-select {:value (enum->string (:stroke-alignment values))
:on-change on-stroke-alignment-change}
(when (= (:stroke-alignment values) :multiple)
[:option {:value ""} "--"])
[:option {:value ":center"} (tr "workspace.options.stroke.center")]
[:option {:value ":inner"} (tr "workspace.options.stroke.inner")]
[:option {:value ":outer"} (tr "workspace.options.stroke.outer")]]
[:select#style.input-select {:value (enum->string (:stroke-style values))
:on-change on-stroke-style-change}
(when (= (:stroke-style values) :multiple)
[:option {:value ""} "--"])
[:option {:value ":solid"} (tr "workspace.options.stroke.solid")]
[:option {:value ":dotted"} (tr "workspace.options.stroke.dotted")]
[:option {:value ":dashed"} (tr "workspace.options.stroke.dashed")]
[:option {:value ":mixed"} (tr "workspace.options.stroke.mixed")]]]
;; Stroke Caps
(when show-caps
[:div.row-flex
[:div.cap-select {:tab-index 0 ;; tab-index to make the element focusable
:on-click (open-caps-select start-caps-state)}
(value->name (:stroke-cap-start values))
[:span.cap-select-button
i/arrow-down]]
[:& dropdown {:show (:open? @start-caps-state)
:on-close (close-caps-select start-caps-state)}
[:ul.dropdown.cap-select-dropdown {:style {:top (:top @start-caps-state)
:left (:left @start-caps-state)}}
(for [[value label separator] (stroke-cap-names)]
(let [img (value->img value)]
[:li {:class (dom/classnames :separator separator)
:on-click #(on-stroke-cap-start-change value)}
(when img [:img {:src (value->img value)}])
label]))]]
[:div.element-set-actions-button {:on-click on-stroke-cap-switch}
i/switch]
[:div.cap-select {:tab-index 0
:on-click (open-caps-select end-caps-state)}
(value->name (:stroke-cap-end values))
[:span.cap-select-button
i/arrow-down]]
[:& dropdown {:show (:open? @end-caps-state)
:on-close (close-caps-select end-caps-state)}
[:ul.dropdown.cap-select-dropdown {:style {:top (:top @end-caps-state)
:left (:left @end-caps-state)}}
(for [[value label separator] (stroke-cap-names)]
(let [img (value->img value)]
[:li {:class (dom/classnames :separator separator)
:on-click #(on-stroke-cap-end-change value)}
(when img [:img {:src (value->img value)}])
label]))]]])]]
;; NO STROKE
[:div.element-set
[:div.element-set-title
[:span label]
[:div.add-page {:on-click on-add-stroke} i/close]]])))
(seq (:strokes values))
[:& h/sortable-container {}
(for [[index value] (d/enumerate (:strokes values []))]
[:& stroke-row {:stroke value
:title (tr "workspace.options.stroke-color")
:index index
:show-caps show-caps
:on-color-change handle-change-stroke-color
:on-color-detach handle-detach
:on-stroke-width-change on-stroke-width-change
:on-stroke-style-change on-stroke-style-change
:on-stroke-alignment-change on-stroke-alignment-change
:open-caps-select open-caps-select
:close-caps-select close-caps-select
:on-stroke-cap-start-change on-stroke-cap-start-change
:on-stroke-cap-end-change on-stroke-cap-end-change
:on-stroke-cap-switch on-stroke-cap-switch
:on-remove handle-remove
:on-reorder (handle-reorder index)
:disable-drag disable-drag
:select-all select-all
:on-blur on-blur}])])]]))

View file

@ -61,12 +61,11 @@
(if (= v :multiple) nil v))
(mf/defc color-row
[{:keys [index color disable-gradient disable-opacity on-change on-reorder on-detach on-open on-close title on-remove]}]
[{:keys [index color disable-gradient disable-opacity on-change on-reorder on-detach on-open on-close title on-remove disable-drag select-all on-blur]}]
(let [current-file-id (mf/use-ctx ctx/current-file-id)
file-colors (mf/deref refs/workspace-file-colors)
shared-libs (mf/deref refs/workspace-libraries)
hover-detach (mf/use-state false)
disable-drag (mf/use-state false)
get-color-name (fn [{:keys [id file-id]}]
(let [src-colors (if (= file-id current-file-id)
@ -107,14 +106,6 @@
handle-opacity-change (fn [value]
(change-opacity (/ value 100)))
select-all (fn [event]
(when (not @disable-drag)
(dom/select-text! (dom/get-target event)))
(reset! disable-drag true))
on-blur (fn [_]
(reset! disable-drag false))
handle-click-color (mf/use-callback
(mf/deps color)
(color-picker-callback color
@ -194,6 +185,7 @@
[:> numeric-input {:value (-> color :opacity opacity->string)
:placeholder (tr "settings.multiple")
:on-click select-all
:on-blur on-blur
:on-change handle-opacity-change
:min 0
:max 100}]])])

View file

@ -0,0 +1,159 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) UXBOX Labs SL
(ns app.main.ui.workspace.sidebar.options.rows.stroke-row
(:require
[app.common.data :as d]
[app.main.ui.components.dropdown :refer [dropdown]]
[app.main.ui.components.numeric-input :refer [numeric-input]]
[app.main.ui.hooks :as h]
[app.main.ui.icons :as i]
[app.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row]]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[rumext.alpha :as mf]))
(defn- width->string [width]
(if (= width :multiple)
""
(str (or width 1))))
(defn- enum->string [value]
(if (= value :multiple)
""
(pr-str value)))
(defn- stroke-cap-names []
[[nil (tr "workspace.options.stroke-cap.none") false]
[:line-arrow (tr "workspace.options.stroke-cap.line-arrow") true]
[:triangle-arrow (tr "workspace.options.stroke-cap.triangle-arrow") false]
[:square-marker (tr "workspace.options.stroke-cap.square-marker") false]
[:circle-marker (tr "workspace.options.stroke-cap.circle-marker") false]
[:diamond-marker (tr "workspace.options.stroke-cap.diamond-marker") false]
[:round (tr "workspace.options.stroke-cap.round") true]
[:square (tr "workspace.options.stroke-cap.square") false]])
(defn- value->img [value]
(when (and value (not= value :multiple))
(str "images/cap-" (name value) ".svg")))
(defn- value->name [value]
(if (= value :multiple)
"--"
(-> (d/seek #(= (first %) value) (stroke-cap-names))
(second))))
(mf/defc stroke-row
[{:keys [index stroke title show-caps on-color-change on-reorder on-color-detach on-remove on-stroke-width-change on-stroke-style-change on-stroke-alignment-change open-caps-select close-caps-select on-stroke-cap-start-change on-stroke-cap-end-change on-stroke-cap-switch disable-drag select-all on-blur]}]
(let [start-caps-state (mf/use-state {:open? false
:top 0
:left 0})
end-caps-state (mf/use-state {:open? false
:top 0
:left 0})
on-drop
(fn [_ data]
(on-reorder (:index data)))
[dprops dref] (if (some? on-reorder)
(h/use-sortable
:data-type "penpot/stroke-row"
:on-drop on-drop
:disabled @disable-drag
:detect-center? false
:data {:id (str "stroke-row-" index)
:index index
:name (str "Border row" index)})
[nil nil])]
[:div.border-data {:class (dom/classnames
:dnd-over-top (= (:over dprops) :top)
:dnd-over-bot (= (:over dprops) :bot))
:ref dref}
;; Stroke Color
[:& color-row {:color {:color (:stroke-color stroke)
:opacity (:stroke-opacity stroke)
:id (:stroke-color-ref-id stroke)
:file-id (:stroke-color-ref-file stroke)
:gradient (:stroke-color-gradient stroke)}
:index index
:title title
:on-change (on-color-change index)
:on-detach (on-color-detach index)
:on-remove (on-remove index)
:disable-drag disable-drag
:select-all select-all
:on-blur on-blur}]
;; Stroke Width, Alignment & Style
[:div.row-flex
[:div.input-element
{:class (dom/classnames :pixels (not= (:stroke-width stroke) :multiple))
:title (tr "workspace.options.stroke-width")}
[:> numeric-input
{:min 0
:value (-> (:stroke-width stroke) width->string)
:precision 2
:placeholder (tr "settings.multiple")
:on-change (on-stroke-width-change index)
:on-click select-all
:on-blur on-blur}]]
[:select#style.input-select {:value (enum->string (:stroke-alignment stroke))
:on-change (on-stroke-alignment-change index)}
(when (= (:stroke-alignment stroke) :multiple)
[:option {:value ""} "--"])
[:option {:value ":center"} (tr "workspace.options.stroke.center")]
[:option {:value ":inner"} (tr "workspace.options.stroke.inner")]
[:option {:value ":outer"} (tr "workspace.options.stroke.outer")]]
[:select#style.input-select {:value (enum->string (:stroke-style stroke))
:on-change (on-stroke-style-change index)}
(when (= (:stroke-style stroke) :multiple)
[:option {:value ""} "--"])
[:option {:value ":solid"} (tr "workspace.options.stroke.solid")]
[:option {:value ":dotted"} (tr "workspace.options.stroke.dotted")]
[:option {:value ":dashed"} (tr "workspace.options.stroke.dashed")]
[:option {:value ":mixed"} (tr "workspace.options.stroke.mixed")]]]
;; Stroke Caps
(when show-caps
[:div.row-flex
[:div.cap-select {:tab-index 0 ;; tab-index to make the element focusable
:on-click (open-caps-select start-caps-state)}
(value->name (:stroke-cap-start stroke))
[:span.cap-select-button
i/arrow-down]]
[:& dropdown {:show (:open? @start-caps-state)
:on-close (close-caps-select start-caps-state)}
[:ul.dropdown.cap-select-dropdown {:style {:top (:top @start-caps-state)
:left (:left @start-caps-state)}}
(for [[value label separator] (stroke-cap-names)]
(let [img (value->img value)]
[:li {:class (dom/classnames :separator separator)
:on-click #(on-stroke-cap-start-change index value)}
(when img [:img {:src (value->img value)}])
label]))]]
[:div.element-set-actions-button {:on-click #(on-stroke-cap-switch index)}
i/switch]
[:div.cap-select {:tab-index 0
:on-click (open-caps-select end-caps-state)}
(value->name (:stroke-cap-end stroke))
[:span.cap-select-button
i/arrow-down]]
[:& dropdown {:show (:open? @end-caps-state)
:on-close (close-caps-select end-caps-state)}
[:ul.dropdown.cap-select-dropdown {:style {:top (:top @end-caps-state)
:left (:left @end-caps-state)}}
(for [[value label separator] (stroke-cap-names)]
(let [img (value->img value)]
[:li {:class (dom/classnames :separator separator)
:on-click #(on-stroke-cap-end-change index value)}
(when img [:img {:src (value->img value)}])
label]))]]])]))

View file

@ -8,6 +8,7 @@
(:require
[app.common.attrs :as attrs]
[app.common.data :as d]
[app.common.geom.shapes :as gsh]
[app.common.pages.common :as cpc]
[app.common.text :as txt]
[app.main.ui.hooks :as hooks]
@ -34,7 +35,7 @@
:fill :shape
:shadow :children
:blur :children
:stroke :children
:stroke :shape
:text :children}
:group
@ -205,6 +206,7 @@
(let [shapes (unchecked-get props "shapes")
shapes-with-children (unchecked-get props "shapes-with-children")
objects (->> shapes-with-children (group-by :id) (d/mapm (fn [_ v] (first v))))
show-caps (some #(and (= :path (:type %)) (gsh/open-path? %)) shapes)
;; Selrect/points only used for measures and it's the one that changes the most. We separate it
;; so we can memoize it
@ -249,14 +251,14 @@
(when-not (empty? fill-ids)
[:& fill-menu {:type type :ids fill-ids :values fill-values}])
(when-not (empty? stroke-ids)
[:& stroke-menu {:type type :ids stroke-ids :show-caps show-caps :values stroke-values}])
(when-not (empty? shadow-ids)
[:& shadow-menu {:type type :ids shadow-ids :values shadow-values}])
(when-not (empty? blur-ids)
[:& blur-menu {:type type :ids blur-ids :values blur-values}])
(when-not (empty? stroke-ids)
[:& stroke-menu {:type type :ids stroke-ids :show-caps true :values stroke-values}])
(when-not (empty? text-ids)
[:& ot/text-menu {:type type :ids text-ids :values text-values}])]))

View file

@ -208,7 +208,11 @@
(reduce add-attrs node-attrs))
(= type :frame)
(let [svg-node (->> node :content (d/seek #(= "frame-background" (get-in % [:attrs :class]))))]
(let [;; The nodes with the "frame-background" class can have some anidation depending on the strokes they have
g-nodes (find-all-nodes node :g)
defs-nodes (flatten (map #(find-all-nodes % :defs) g-nodes))
rect-nodes (flatten [(map #(find-all-nodes % :rect) defs-nodes) (map #(find-all-nodes % :rect) g-nodes)])
svg-node (d/seek #(= "frame-background" (get-in % [:attrs :class])) rect-nodes)]
(merge (add-attrs {} (:attrs svg-node)) node-attrs))
(= type :svg-raw)
@ -685,20 +689,48 @@
props)))
(defn add-fills
[props node svg-data]
(let [fills (-> node
(find-node :defs)
(find-node :pattern)
(find-node :g)
(find-all-nodes :rect)
(reverse))
fills (if (= 0 (count fills))
[(add-fill {} node svg-data)]
(map #(add-fill {} node (get-svg-data :rect %)) fills))]
(-> props
(assoc :fills fills))))
(defn parse-fills
[node svg-data]
(let [fills-node (get-data node :penpot:fills)
fills (->> (find-all-nodes fills-node :penpot:fill)
(mapv (fn [fill-node]
{:fill-color (when (not (str/starts-with? (get-meta fill-node :fill-color) "url"))
(get-meta fill-node :fill-color))
:fill-color-gradient (when (str/starts-with? (get-meta fill-node :fill-color) "url")
(parse-gradient node (get-meta fill-node :fill-color)))
:fill-color-ref-file (get-meta fill-node :fill-color-ref-file uuid/uuid)
:fill-color-ref-id (get-meta fill-node :fill-color-ref-id uuid/uuid)
:fill-opacity (get-meta fill-node :fill-opacity d/parse-double)}))
(mapv d/without-nils))]
(if (seq fills)
fills
(->> [(-> (add-fill {} node svg-data)
(d/without-nils))]
(filterv not-empty)))))
(defn parse-strokes
[node svg-data]
(let [strokes-node (get-data node :penpot:strokes)
strokes (->> (find-all-nodes strokes-node :penpot:stroke)
(mapv (fn [stroke-node]
{:stroke-color (when (not (str/starts-with? (get-meta stroke-node :stroke-color) "url"))
(get-meta stroke-node :stroke-color))
:stroke-color-gradient (when (str/starts-with? (get-meta stroke-node :stroke-color) "url")
(parse-gradient node (get-meta stroke-node :stroke-color)))
:stroke-color-ref-file (get-meta stroke-node :stroke-color-ref-file uuid/uuid)
:stroke-color-ref-id (get-meta stroke-node :stroke-color-ref-id uuid/uuid)
:stroke-opacity (get-meta stroke-node :stroke-opacity d/parse-double)
:stroke-style (get-meta stroke-node :stroke-style keyword)
:stroke-width (get-meta stroke-node :stroke-width d/parse-double)
:stroke-alignment (get-meta stroke-node :stroke-alignment keyword)
:stroke-cap-start (get-meta stroke-node :stroke-cap-start keyword)
:stroke-cap-end (get-meta stroke-node :stroke-cap-end keyword)}))
(mapv d/without-nils))]
(if (seq strokes)
strokes
(->> [(-> (add-stroke {} node svg-data)
(d/without-nils))]
(filterv #(and (not-empty %) (not= (:stroke-style %) :none)))))))
(defn add-svg-content
[props node]
@ -765,14 +797,16 @@
(-> {}
(add-common-data node)
(add-position type node svg-data)
(add-stroke node svg-data)
(add-layer-options svg-data)
(add-shadows node)
(add-blur node)
(add-exports node)
(add-svg-attrs node svg-data)
(add-library-refs node)
(add-fills node svg-data)
(assoc :fills (parse-fills node svg-data))
(assoc :strokes (parse-strokes node svg-data))
(cond-> (= :svg-raw type)
(add-svg-content node))