Import paths as native shapes

This commit is contained in:
alonso.torres 2021-02-22 21:40:15 +01:00 committed by Andrey Antukh
parent 741d67c30b
commit 19febde547
28 changed files with 921 additions and 209 deletions

File diff suppressed because one or more lines are too long

View file

@ -302,6 +302,14 @@
default default
v)))) v))))
(defn num-string? [v]
;; https://stackoverflow.com/questions/175739/built-in-way-in-javascript-to-check-if-a-string-is-a-valid-number
#?(:cljs (and (string? v)
(not (js/isNaN v))
(not (js/isNaN (parse-double v))))
:clj (not= (parse-double v :nan) :nan)))
(defn read-string (defn read-string
[v] [v]
(r/read-string v)) (r/read-string v))

View file

@ -11,6 +11,8 @@
(:require (:require
#?(:cljs [cljs.pprint :as pp] #?(:cljs [cljs.pprint :as pp]
:clj [clojure.pprint :as pp]) :clj [clojure.pprint :as pp])
[cuerdas.core :as str]
[app.common.data :as d]
[app.common.math :as mth] [app.common.math :as mth]
[app.common.geom.point :as gpt])) [app.common.geom.point :as gpt]))
@ -21,6 +23,22 @@
(toString [_] (toString [_]
(str "matrix(" a "," b "," c "," d "," e "," f ")"))) (str "matrix(" a "," b "," c "," d "," e "," f ")")))
(defonce matrix-regex #"matrix\((.*),(.*),(.*),(.*),(.*),(.*)\)")
(defn matrix
"Create a new matrix instance."
([]
(Matrix. 1 0 0 1 0 0))
([a b c d e f]
(Matrix. a b c d e f)))
(defn parse-matrix [mtx]
(let [[_ a b c d e f] (re-matches matrix-regex mtx)]
(->> [a b c d e f]
(map str/trim)
(map d/parse-double)
(apply matrix))))
(defn multiply (defn multiply
([{m1a :a m1b :b m1c :c m1d :d m1e :e m1f :f} ([{m1a :a m1b :b m1c :c m1d :d m1e :e m1f :f}
{m2a :a m2b :b m2c :c m2d :d m2e :e m2f :f}] {m2a :a m2b :b m2c :c m2d :d m2e :e m2f :f}]
@ -46,12 +64,7 @@
[v] [v]
(instance? Matrix v)) (instance? Matrix v))
(defn matrix
"Create a new matrix instance."
([]
(Matrix. 1 0 0 1 0 0))
([a b c d e f]
(Matrix. a b c d e f)))
(def base (matrix)) (def base (matrix))

View file

@ -148,10 +148,10 @@
(update-in [:selrect :x2] - x) (update-in [:selrect :x2] - x)
(update-in [:selrect :y2] - y) (update-in [:selrect :y2] - y)
(d/update-when :points #(map move-point %)) (d/update-when :points #(mapv move-point %))
(cond-> (= :path type) (cond-> (= :path type)
(d/update-when :content #(map move-segment %)))))) (d/update-when :content #(mapv move-segment %))))))
;; --- Helpers ;; --- Helpers

View file

@ -229,7 +229,7 @@
(s/def :internal.shape/stroke-color-ref-file (s/nilable uuid?)) (s/def :internal.shape/stroke-color-ref-file (s/nilable uuid?))
(s/def :internal.shape/stroke-color-ref-id (s/nilable uuid?)) (s/def :internal.shape/stroke-color-ref-id (s/nilable uuid?))
(s/def :internal.shape/stroke-opacity ::safe-number) (s/def :internal.shape/stroke-opacity ::safe-number)
(s/def :internal.shape/stroke-style #{:solid :dotted :dashed :mixed :none}) (s/def :internal.shape/stroke-style #{:solid :dotted :dashed :mixed :none :svg})
(s/def :internal.shape/stroke-width ::safe-number) (s/def :internal.shape/stroke-width ::safe-number)
(s/def :internal.shape/stroke-alignment #{:center :inner :outer}) (s/def :internal.shape/stroke-alignment #{:center :inner :outer})
(s/def :internal.shape/text-align #{"left" "right" "center" "justify"}) (s/def :internal.shape/text-align #{"left" "right" "center" "justify"})

File diff suppressed because one or more lines are too long

View file

@ -5505,5 +5505,12 @@
"zh_cn" : "单击以闭合路径" "zh_cn" : "单击以闭合路径"
}, },
"unused" : true "unused" : true
},
"workspace.sidebar.options.svg-attrs.title": {
"translations": {
"en": "Imported SVG Attributes",
"es": "Atributos del SVG Importado"
}
} }
} }

View file

@ -449,6 +449,11 @@ ul.slider-dots {
content: "Y"; content: "Y";
} }
} }
&.large {
width: auto;
min-width: 9rem;
}
} }
input, input,

View file

@ -709,6 +709,8 @@
.element-set-content .input-row { .element-set-content .input-row {
& .element-set-subtitle { & .element-set-subtitle {
width: 5.5rem; width: 5.5rem;
overflow: hidden;
text-overflow: ellipsis;
} }
} }
@ -771,7 +773,6 @@
min-width: 56px; min-width: 56px;
max-height: 10rem; max-height: 10rem;
} }
} }
} }

View file

@ -219,15 +219,19 @@
(defn generate-unique-name (defn generate-unique-name
"A unique name generator" "A unique name generator"
[used basename] ([used basename]
(s/assert ::set-of-string used) (generate-unique-name used basename false))
(s/assert ::us/string basename) ([used basename prefix-first?]
(let [[prefix initial] (extract-numeric-suffix basename)] (s/assert ::set-of-string used)
(loop [counter initial] (s/assert ::us/string basename)
(let [candidate (str prefix "-" counter)] (let [[prefix initial] (extract-numeric-suffix basename)]
(if (contains? used candidate) (loop [counter initial]
(recur (inc counter)) (let [candidate (if (and (= 1 counter) prefix-first?)
candidate))))) (str prefix)
(str prefix "-" counter))]
(if (contains? used candidate)
(recur (inc counter))
candidate))))))
;; --- Shape attrs (Layers Sidebar) ;; --- Shape attrs (Layers Sidebar)

View file

@ -10,16 +10,19 @@
(ns app.main.data.workspace.svg-upload (ns app.main.data.workspace.svg-upload
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.util.data :as ud] [app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh] [app.common.geom.shapes :as gsh]
[app.common.pages :as cp] [app.common.pages :as cp]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.main.data.workspace.common :as dwc] [app.main.data.workspace.common :as dwc]
[app.util.color :as uc]
[app.util.data :as ud]
[app.util.geom.path :as ugp]
[app.util.svg :as usvg]
[beicon.core :as rx] [beicon.core :as rx]
[cuerdas.core :as str] [cuerdas.core :as str]
[potok.core :as ptk] [potok.core :as ptk]))
[app.util.svg :as usvg]
[app.util.geom.path :as ugp]))
(defn- svg-dimensions [data] (defn- svg-dimensions [data]
(let [width (get-in data [:attrs :width] 100) (let [width (get-in data [:attrs :width] 100)
@ -30,40 +33,75 @@
height (d/parse-integer height-str)] height (d/parse-integer height-str)]
[width height])) [width height]))
(defn tag-name [tag] (defn tag->name
(cond (string? tag) tag "Given a tag returns its layer name"
(keyword? tag) (name tag) [tag]
(nil? tag) "node" (str "svg-" (cond (string? tag) tag
:else (str tag))) (keyword? tag) (name tag)
(nil? tag) "node"
:else (str tag))))
(defn setup-fill [shape attrs] (defn fix-dot-number
(let [fill-color (or (get-in attrs [:fill]) "Fixes decimal numbers starting in dot but without leading 0"
(get-in attrs [:style :fill]) [num-str]
"#000000") (if (str/starts-with? num-str ".")
fill-opacity (ud/parse-float (or (get-in attrs [:fill-opacity]) (str "0" num-str)
(get-in attrs [:style :fill-opacity]) num-str))
"1"))]
(-> shape
(assoc :fill-color fill-color)
(assoc :fill-opacity fill-opacity))))
(defn setup-stroke [shape attrs] (defn setup-fill [shape]
(-> shape
(assoc :stroke-color (:stroke attrs "#000000"))
(assoc :stroke-opacity (ud/parse-float (:stroke-opacity attrs 1)))
(assoc :stroke-style :solid)
(assoc :stroke-width (ud/parse-float (:stroke-width attrs "1")))
(assoc :stroke-alignment :center)))
(defn add-style-attributes [shape {:keys [attrs]}]
(cond-> shape (cond-> shape
(d/any-key? attrs :fill :fill-opacity) ;; Color present as attribute
(setup-fill attrs) (uc/color? (get-in shape [:svg-attrs :fill]))
(-> (update :svg-attrs dissoc :fill)
(assoc :fill-color (-> (get-in shape [:svg-attrs :fill])
(uc/parse-color))))
(d/any-key? attrs :stroke :stroke-width :stroke-opacity) ;; Color present as style
(setup-stroke attrs))) (uc/color? (get-in shape [:svg-attrs :style :fill]))
(-> (update-in [:svg-attrs :style] dissoc :fill)
(assoc :fill-color (-> (get-in shape [:svg-attrs :style :fill])
(uc/parse-color))))
(defn create-raw-svg [name frame-id svg-data element-data] (get-in shape [:svg-attrs :fill-opacity])
(-> (update :svg-attrs dissoc :fill-opacity)
(assoc :fill-opacity (-> (get-in shape [:svg-attrs :fill-opacity])
(ud/parse-float))))
(get-in shape [:svg-attrs :style :fill-opacity])
(-> (update :svg-attrs dissoc :fill-opacity)
(assoc :fill-opacity (-> (get-in shape [:svg-attrs :style :fill-opacity])
(ud/parse-float))))))
(defonce default-stroke {:stroke-color "#000000"
:stroke-opacity 1
:stroke-alignment :center
:stroke-style :svg})
(defn setup-stroke [shape]
(let [shape
(cond-> shape
(uc/color? (get-in shape [:svg-attrs :stroke]))
(-> (update :svg-attrs dissoc :stroke)
(assoc :stroke-color (get-in shape [:svg-attrs :stroke])))
(uc/color? (get-in shape [:svg-attrs :style :stroke]))
(-> (update-in [:svg-attrs :style] dissoc :stroke)
(assoc :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])
(ud/parse-float))))
(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])
(ud/parse-float)))))]
(if (d/any-key? shape :stroke-color :stroke-opacity :stroke-width)
(merge default-stroke shape)
shape)))
(defn create-raw-svg [name frame-id svg-data {:keys [attrs] :as data}]
(let [{:keys [x y width height]} svg-data] (let [{:keys [x y width height]} svg-data]
(-> {:id (uuid/next) (-> {:id (uuid/next)
:type :svg-raw :type :svg-raw
@ -73,9 +111,10 @@
:height height :height height
:x x :x x
:y y :y y
:root-attrs (select-keys svg-data [:width :height]) :content (cond-> data
:content (cond-> element-data (map? data) (update :attrs usvg/clean-attrs))}
(map? element-data) (update :attrs usvg/clean-attrs))} (assoc :svg-attrs attrs)
(assoc :svg-viewbox (select-keys svg-data [0 0 :width :height]))
(gsh/setup-selrect)))) (gsh/setup-selrect))))
(defn create-svg-root [frame-id svg-data] (defn create-svg-root [frame-id svg-data]
@ -87,14 +126,20 @@
:width width :width width
:height height :height height
:x x :x x
:y y :y y}
:attrs (-> (get svg-data :attrs) usvg/clean-attrs) (gsh/setup-selrect)
;;:content (if (map? data) (update data :attrs usvg/clean-attrs) data) (assoc :svg-attrs (-> (:attrs svg-data)
} (dissoc :viewBox :xmlns))))))
(gsh/setup-selrect))))
(defn apply-svg-transform [content transform-str]
(let [transform (gmt/parse-matrix transform-str)]
(gsh/transform-content content transform)))
(defn create-path-shape [name frame-id svg-data {:keys [attrs] :as data}]
(let [content (cond-> (ugp/path->content (:d attrs))
(contains? attrs :transform)
(apply-svg-transform (:transform attrs)))
(defn parse-path [name frame-id {:keys [attrs] :as data}]
(let [content (ugp/path->content (:d attrs))
selrect (gsh/content->selrect content) selrect (gsh/content->selrect content)
points (gsh/rect->points selrect)] points (gsh/rect->points selrect)]
(-> {:id (uuid/next) (-> {:id (uuid/next)
@ -104,21 +149,42 @@
:content content :content content
:selrect selrect :selrect selrect
:points points} :points points}
(assoc :svg-viewbox (select-keys selrect [:x :y :width :height]))
(assoc :svg-attrs (dissoc attrs :d :transform))
(gsh/translate-to-frame svg-data))))
(add-style-attributes data)))) (defn create-group [name frame-id svg-data {:keys [attrs]}]
(let [{:keys [x y width height]} svg-data]
(-> {:id (uuid/next)
:type :group
:name name
:frame-id frame-id
:x x
:y y
:width width
:height height}
(assoc :svg-attrs attrs)
(assoc :svg-viewbox (select-keys svg-data [0 0 :width :height]))
(gsh/setup-selrect))))
(defn parse-svg-element [frame-id svg-data element-data unames] (defn parse-svg-element [frame-id svg-data element-data unames]
(let [{:keys [tag]} element-data (let [{:keys [tag attrs]} element-data
name (dwc/generate-unique-name unames (str "svg-" (tag-name tag)))] name (dwc/generate-unique-name unames (or (:id attrs) (tag->name tag)) true)
att-refs (usvg/find-attr-references attrs)
references (usvg/find-def-references (:defs svg-data) att-refs)]
(case tag (-> (case tag
;; :rect (parse-rect data) :g (create-group name frame-id svg-data element-data)
;; :path (parse-path name frame-id data) ;; :rect (parse-rect data)
(create-raw-svg name frame-id svg-data element-data)))) :path (create-path-shape name frame-id (gpt/negate (gpt/point svg-data)) element-data)
(create-raw-svg name frame-id svg-data element-data))
(assoc :svg-defs (select-keys (:defs svg-data) references))
(setup-fill)
(setup-stroke))))
(defn add-svg-child-changes [page-id objects selected frame-id parent-id svg-data ids-mappings result [index data]] (defn add-svg-child-changes [page-id objects selected frame-id parent-id svg-data ids-mappings result [index data]]
(let [[unames [rchs uchs]] result (let [[unames [rchs uchs]] result
data (update data :attrs usvg/replace-attrs-ids ids-mappings)
shape (parse-svg-element frame-id svg-data data unames) shape (parse-svg-element frame-id svg-data data unames)
shape-id (:id shape) shape-id (:id shape)
[rch1 uch1] (dwc/add-shape-changes page-id objects selected shape) [rch1 uch1] (dwc/add-shape-changes page-id objects selected shape)
@ -164,6 +230,9 @@
:height height :height height
:name svg-name)) :name svg-name))
[def-nodes svg-data] (usvg/extract-defs svg-data)
svg-data (assoc svg-data :defs def-nodes)
root-shape (create-svg-root frame-id svg-data) root-shape (create-svg-root frame-id svg-data)
root-id (:id root-shape) root-id (:id root-shape)

View file

@ -12,7 +12,8 @@
[rumext.alpha :as mf] [rumext.alpha :as mf]
[cuerdas.core :as str] [cuerdas.core :as str]
[app.util.object :as obj] [app.util.object :as obj]
[app.main.ui.context :as muc])) [app.main.ui.context :as muc]
[app.util.svg :as usvg]))
(defn- stroke-type->dasharray (defn- stroke-type->dasharray
[style] [style]
@ -74,45 +75,77 @@
attrs))) attrs)))
(defn add-fill [attrs shape render-id] (defn add-fill [attrs shape render-id]
(let [fill-color-gradient-id (str "fill-color-gradient_" render-id)] (let [fill-attrs (cond
(cond (contains? shape :fill-color-gradient)
(:fill-color-gradient shape) (let [fill-color-gradient-id (str "fill-color-gradient_" render-id)]
(obj/merge! attrs #js {:fill (str/format "url(#%s)" fill-color-gradient-id)}) {:fill (str/format "url(#%s)" fill-color-gradient-id)})
(and (not= :svg-raw (:type shape)) (contains? shape :fill-color)
(not (:fill-color-gradient shape))) {:fill (:fill-color shape)}
(obj/merge! attrs #js {:fill (or (:fill-color shape) "transparent")
:fillOpacity (:fill-opacity shape nil)})
(and (= :svg-raw (:type shape)) ;; If contains svg-attrs the origin is svg. If it's not svg origin
(or (:fill-opacity shape) (:fill-color shape))) ;; we setup the default fill as transparent (instead of black)
(obj/merge! attrs #js {:fill (:fill-color shape) (not (contains? shape :svg-attrs))
:fillOpacity (:fill-opacity shape nil)}) {:fill "transparent"}
:else attrs))) :else
{})
fill-attrs (cond-> fill-attrs
(contains? shape :fill-opacity)
(assoc :fillOpacity (:fill-opacity shape)))]
(obj/merge! attrs (clj->js fill-attrs))))
(defn add-stroke [attrs shape render-id] (defn add-stroke [attrs shape render-id]
(let [stroke-style (:stroke-style shape :none) (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)]
(if (not= stroke-style :none) (if (not= stroke-style :none)
(if (:stroke-color-gradient shape) (let [stroke-attrs
(obj/merge! attrs (cond-> {:strokeWidth (:stroke-width shape 1)}
#js {:stroke (str/format "url(#%s)" stroke-color-gradient-id) (:stroke-color-gradient shape)
:strokeWidth (:stroke-width shape 1) (assoc :stroke (str/format "url(#%s)" stroke-color-gradient-id))
:strokeDasharray (stroke-type->dasharray stroke-style)})
(obj/merge! attrs (not (:stroke-color-gradient shape))
#js {:stroke (:stroke-color shape nil) (assoc :stroke (:stroke-color shape nil)
:strokeWidth (:stroke-width shape 1) :strokeOpacity (:stroke-opacity shape nil))
:strokeOpacity (:stroke-opacity shape nil)
:strokeDasharray (stroke-type->dasharray stroke-style)})))) (not= stroke-style :svg)
attrs) (assoc :strokeDasharray (stroke-type->dasharray stroke-style)))]
(obj/merge! attrs (clj->js stroke-attrs)))
attrs)))
(defn extract-svg-attrs
[render-id svg-defs svg-attrs]
(let [replace-id (fn [id]
(if (contains? svg-defs id)
(str render-id "-" id)
id))
svg-attrs (-> svg-attrs
(usvg/update-attr-ids replace-id)
(usvg/clean-attrs))
attrs (-> svg-attrs (dissoc :style) (clj->js))
styles (-> svg-attrs (:style {}) (clj->js))]
[attrs styles]))
(defn extract-style-attrs (defn extract-style-attrs
([shape] ([shape]
(let [render-id (mf/use-ctx muc/render-ctx) (let [render-id (mf/use-ctx muc/render-ctx)
svg-defs (:svg-defs shape {})
svg-attrs (:svg-attrs shape {})
[svg-attrs svg-styles] (mf/use-memo
(mf/deps render-id svg-defs svg-attrs)
#(extract-svg-attrs render-id svg-defs svg-attrs))
styles (-> (obj/new) styles (-> (obj/new)
(obj/merge! svg-styles)
(add-fill shape render-id) (add-fill shape render-id)
(add-stroke shape render-id))] (add-stroke shape render-id))]
(-> (obj/new) (-> (obj/new)
(obj/merge! svg-attrs)
(add-border-radius shape) (add-border-radius shape)
(obj/set! "style" styles))))) (obj/set! "style" styles)))))

View file

@ -9,7 +9,9 @@
(ns app.main.ui.shapes.group (ns app.main.ui.shapes.group
(:require (:require
[app.util.object :as obj]
[rumext.alpha :as mf] [rumext.alpha :as mf]
[app.main.ui.shapes.attrs :as attrs]
[app.main.ui.shapes.mask :refer [mask-str mask-factory]])) [app.main.ui.shapes.mask :refer [mask-str mask-factory]]))
(defn group-shape (defn group-shape
@ -28,12 +30,15 @@
show-mask? (and (:masked-group? shape) (not expand-mask)) show-mask? (and (:masked-group? shape) (not expand-mask))
mask (when show-mask? (first childs)) mask (when show-mask? (first childs))
childs (if show-mask? (rest childs) childs)] childs (if show-mask? (rest childs) childs)
[:g.group props (-> (attrs/extract-style-attrs shape)
{:pointer-events pointer-events (obj/merge!
:mask (when (and mask (not expand-mask)) (mask-str mask))} #js {:className "group"
:pointerEvents pointer-events
:mask (when (and mask (not expand-mask)) (mask-str mask))}))]
[:> :g props
(when mask (when mask
[:> render-mask #js {:frame frame :mask mask}]) [:> render-mask #js {:frame frame :mask mask}])

View file

@ -9,13 +9,12 @@
(ns app.main.ui.shapes.shape (ns app.main.ui.shapes.shape
(:require (:require
[app.common.geom.shapes :as geom]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.main.ui.context :as muc] [app.main.ui.context :as muc]
[app.main.ui.shapes.filters :as filters] [app.main.ui.shapes.filters :as filters]
[app.main.ui.shapes.gradients :as grad] [app.main.ui.shapes.gradients :as grad]
[app.main.ui.shapes.svg-defs :as defs]
[app.util.object :as obj] [app.util.object :as obj]
[cuerdas.core :as str]
[rumext.alpha :as mf])) [rumext.alpha :as mf]))
(mf/defc shape-container (mf/defc shape-container
@ -48,6 +47,7 @@
[:& (mf/provider muc/render-ctx) {:value render-id} [:& (mf/provider muc/render-ctx) {:value render-id}
[:> wrapper-tag group-props [:> wrapper-tag group-props
[:defs [:defs
[:& defs/svg-defs {:shape shape :render-id render-id}]
[:& filters/filters {:shape shape :filter-id filter-id}] [:& filters/filters {:shape shape :filter-id filter-id}]
[:& grad/gradient {:shape shape :attr :fill-color-gradient}] [:& grad/gradient {:shape shape :attr :fill-color-gradient}]
[:& grad/gradient {:shape shape :attr :stroke-color-gradient}]] [:& grad/gradient {:shape shape :attr :stroke-color-gradient}]]

View file

@ -0,0 +1,103 @@
;; 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/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020-2021 UXBOX Labs SL
(ns app.main.ui.shapes.svg-defs
(:require
[app.common.data :as d]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.util.object :as obj]
[app.util.svg :as usvg]
[rumext.alpha :as mf]))
(defn add-matrix [attrs transform-key transform-matrix]
(update attrs
transform-key
(fn [val]
(if val
(str transform-matrix " " val)
(str transform-matrix)))))
(defn transform-region [attrs transform]
(let [{x-str :x y-str :y width-str :width height-str :height} attrs
data (map d/parse-double [x-str y-str width-str height-str])]
(if (every? (comp not nil?) data)
(let [[x y width height] data
p1 (-> (gpt/point x y)
(gpt/transform transform))
p2 (-> (gpt/point (+ x width) (+ y height))
(gpt/transform transform))]
(assoc attrs
:x (:x p1)
:y (:y p1)
:width (- (:x p2) (:x p1))
:height (- (:y p2) (:y p1))))
attrs)))
(mf/defc svg-node [{:keys [node prefix-id transform]}]
(cond
(string? node) node
:else
(let [{:keys [tag attrs content]} node
transform-gradient? (and (#{:linearGradient :radialGradient} tag)
(= "userSpaceOnUse" (get attrs :gradientUnits "userSpaceOnUse")))
transform-pattern? (and (= :pattern tag)
(every? d/num-string? [(:x attrs "0") (:y attrs "0") (:width attrs "0") (:height attrs "0")])
(= "userSpaceOnUse" (get attrs :patternUnits "userSpaceOnUse")))
transform-filter? (and (= #{:filter
;; Filter primitives. We need to remap subregions
:feBlend :feColorMatrix :feComponentTransfer :feComposite :feConvolveMatrix
:feDiffuseLighting :feDisplacementMap :feFlood :feGaussianBlur
:feImage :feMerge :feMorphology :feOffset
:feSpecularLighting :feTile :feTurbulence} tag)
(= "userSpaceOnUse" (get attrs :filterUnits "userSpaceOnUse")))
attrs (-> attrs
(usvg/update-attr-ids prefix-id)
(usvg/clean-attrs)
(cond->
transform-gradient? (add-matrix :gradientTransform transform)
transform-pattern? (add-matrix :patternTransform transform)
transform-filter? (transform-region transform)))
[wrapper wrapper-props] (if (= tag :mask)
["g" #js {:transform (str transform)}]
[mf/Fragment (obj/new)])]
[:> (name tag) (clj->js attrs)
[:> wrapper wrapper-props
(for [node content] [:& svg-node {:node node
:prefix-id prefix-id
:transform transform}])]])))
(mf/defc svg-defs [{:keys [shape render-id]}]
(let [svg-defs (:svg-defs shape)
transform (mf/use-memo
(mf/deps shape)
#(if (= :svg-raw (:type shape))
(gmt/matrix)
(usvg/svg-transform-matrix shape)))
prefix-id
(fn [id]
(cond->> id
(contains? svg-defs id) (str render-id "-")))]
(when (and svg-defs (not (empty? svg-defs)))
(for [svg-def (vals svg-defs)]
[:& svg-node {:node svg-def
:prefix-id prefix-id
:transform transform}]))))

View file

@ -21,7 +21,7 @@
[rumext.alpha :as mf])) [rumext.alpha :as mf]))
;; Graphic tags ;; Graphic tags
(defonce graphic-element? #{ :circle :ellipse :image :line :path :polygon :polyline :rect :text #_"use"}) (defonce graphic-element? #{:circle :ellipse :image :line :path :polygon :polyline :rect :text :use})
;; Context to store a re-mapping of the ids ;; Context to store a re-mapping of the ids
(def svg-ids-ctx (mf/create-context nil)) (def svg-ids-ctx (mf/create-context nil))
@ -37,17 +37,11 @@
(obj/set! "style" style)))) (obj/set! "style" style))))
(defn translate-shape [attrs shape] (defn translate-shape [attrs shape]
(let [{svg-width :width svg-height :height :as root-shape} (:root-attrs shape) (let [transform (str (usvg/svg-transform-matrix shape)
{:keys [x y width height]} (:selrect shape) " "
transform (->> (:transform attrs "") (:transform attrs ""))]
(str (gmt/multiply
(gmt/matrix)
(gsh/transform-matrix shape)
(gmt/translate-matrix (gpt/point x y))
(gmt/scale-matrix (gpt/point (/ width svg-width) (/ height svg-height))))
" "))]
(cond-> attrs (cond-> attrs
(and root-shape (graphic-element? (-> shape :content :tag))) (and (:svg-viewbox shape) (graphic-element? (-> shape :content :tag)))
(assoc :transform transform)))) (assoc :transform transform))))
(mf/defc svg-root (mf/defc svg-root

View file

@ -33,7 +33,6 @@
{::mf/wrap-props false} {::mf/wrap-props false}
[props] [props]
(let [shape (unchecked-get props "shape") (let [shape (unchecked-get props "shape")
hover? (or (mf/deref refs/current-hover) #{})
content-modifiers-ref (pc/make-content-modifiers-ref (:id shape)) content-modifiers-ref (pc/make-content-modifiers-ref (:id shape))
content-modifiers (mf/deref content-modifiers-ref) content-modifiers (mf/deref content-modifiers-ref)
editing-id (mf/deref refs/selected-edition) editing-id (mf/deref refs/selected-edition)

View file

@ -64,6 +64,7 @@
:width width :width width
:height height :height height
:fill "transparent" :fill "transparent"
:stroke "none"
:on-mouse-down handle-mouse-down :on-mouse-down handle-mouse-down
:on-double-click handle-double-click :on-double-click handle-double-click
:on-context-menu handle-context-menu :on-context-menu handle-context-menu

View file

@ -19,7 +19,8 @@
[app.main.ui.workspace.sidebar.options.blur :refer [blur-menu]] [app.main.ui.workspace.sidebar.options.blur :refer [blur-menu]]
[app.main.ui.workspace.sidebar.options.shadow :refer [shadow-menu]] [app.main.ui.workspace.sidebar.options.shadow :refer [shadow-menu]]
[app.main.ui.workspace.sidebar.options.stroke :refer [stroke-menu]] [app.main.ui.workspace.sidebar.options.stroke :refer [stroke-menu]]
[app.main.ui.workspace.sidebar.options.text :as ot])) [app.main.ui.workspace.sidebar.options.text :as ot]
[app.main.ui.workspace.sidebar.options.svg-attrs :refer [svg-attrs-menu]]))
(mf/defc options (mf/defc options
{::mf/wrap [mf/memo] {::mf/wrap [mf/memo]
@ -36,6 +37,7 @@
[blur-ids blur-values] (get-attrs [shape] objects :blur) [blur-ids blur-values] (get-attrs [shape] objects :blur)
[stroke-ids stroke-values] (get-attrs [shape] objects :stroke) [stroke-ids stroke-values] (get-attrs [shape] objects :stroke)
[text-ids text-values] (get-attrs [shape] objects :text) [text-ids text-values] (get-attrs [shape] objects :text)
[svg-ids svg-values] [[(:id shape)] (select-keys shape [:svg-attrs])]
[comp-ids comp-values] [[(:id shape)] (select-keys shape component-attrs)]] [comp-ids comp-values] [[(:id shape)] (select-keys shape component-attrs)]]
[:div.options [:div.options
@ -55,6 +57,10 @@
[:& stroke-menu {:type type :ids stroke-ids :values stroke-values}]) [:& stroke-menu {:type type :ids stroke-ids :values stroke-values}])
(when-not (empty? text-ids) (when-not (empty? text-ids)
[:& ot/text-menu {:type type :ids text-ids :values text-values}])])) [:& ot/text-menu {:type type :ids text-ids :values text-values}])
(when-not (empty? svg-values)
[:& svg-attrs-menu {:ids svg-ids
:values svg-values}])]))

View file

@ -15,7 +15,8 @@
[app.main.ui.workspace.sidebar.options.fill :refer [fill-attrs fill-menu]] [app.main.ui.workspace.sidebar.options.fill :refer [fill-attrs fill-menu]]
[app.main.ui.workspace.sidebar.options.stroke :refer [stroke-attrs stroke-menu]] [app.main.ui.workspace.sidebar.options.stroke :refer [stroke-attrs stroke-menu]]
[app.main.ui.workspace.sidebar.options.shadow :refer [shadow-menu]] [app.main.ui.workspace.sidebar.options.shadow :refer [shadow-menu]]
[app.main.ui.workspace.sidebar.options.blur :refer [blur-menu]])) [app.main.ui.workspace.sidebar.options.blur :refer [blur-menu]]
[app.main.ui.workspace.sidebar.options.svg-attrs :refer [svg-attrs-menu]]))
(mf/defc options (mf/defc options
[{:keys [shape] :as props}] [{:keys [shape] :as props}]
@ -36,4 +37,7 @@
[:& shadow-menu {:ids ids [:& shadow-menu {:ids ids
:values (select-keys shape [:shadow])}] :values (select-keys shape [:shadow])}]
[:& blur-menu {:ids ids [:& blur-menu {:ids ids
:values (select-keys shape [:blur])}]])) :values (select-keys shape [:blur])}]
[:& svg-attrs-menu {:ids ids
:values (select-keys shape [:svg-attrs])}]]))

View file

@ -36,6 +36,11 @@
:placeholder placeholder :placeholder placeholder
:on-change on-change}] :on-change on-change}]
:text
[:input {:value value
:class "input-text"
:on-change on-change} ]
[:> numeric-input {:placeholder placeholder [:> numeric-input {:placeholder placeholder
:min min :min min
:max max :max max

View file

@ -0,0 +1,53 @@
;; 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/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.ui.workspace.sidebar.options.svg-attrs
(:require
[app.common.data :as d]
[app.main.data.workspace.common :as dwc]
[app.main.store :as st]
[app.main.ui.workspace.sidebar.options.rows.input-row :refer [input-row]]
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
[rumext.alpha :as mf]))
(mf/defc attribute-value [{:keys [attr value on-change] :as props}]
(let [handle-change
(mf/use-callback
(mf/deps attr on-change)
(fn [event]
(on-change attr (dom/get-target-val event))))]
[:div.element-set-content
[:& input-row {:label (name attr)
:type :text
:class "large"
:value (str value)
:on-change handle-change}]]))
(mf/defc svg-attrs-menu [{:keys [ids type values]}]
(let [handle-change
(mf/use-callback
(mf/deps ids)
(fn [attr value]
(let [update-fn
(fn [shape] (assoc-in shape [:svg-attrs attr] value))]
(st/emit! (dwc/update-shapes ids update-fn)))))]
(when-not (empty? (:svg-attrs values))
[:div.element-set
[:div.element-set-title
[:span (tr "workspace.sidebar.options.svg-attrs.title")]]
(for [[index [attr-key attr-value]] (d/enumerate (:svg-attrs values))]
[:& attribute-value {:key attr-key
:ids ids
:attr attr-key
:value attr-value
:on-change handle-change}])])))

View file

@ -17,11 +17,12 @@
[app.main.ui.workspace.sidebar.options.fill :refer [fill-attrs fill-menu]] [app.main.ui.workspace.sidebar.options.fill :refer [fill-attrs fill-menu]]
[app.main.ui.workspace.sidebar.options.stroke :refer [stroke-attrs stroke-menu]] [app.main.ui.workspace.sidebar.options.stroke :refer [stroke-attrs stroke-menu]]
[app.main.ui.workspace.sidebar.options.shadow :refer [shadow-menu]] [app.main.ui.workspace.sidebar.options.shadow :refer [shadow-menu]]
[app.main.ui.workspace.sidebar.options.blur :refer [blur-menu]])) [app.main.ui.workspace.sidebar.options.blur :refer [blur-menu]]
[app.main.ui.workspace.sidebar.options.svg-attrs :refer [svg-attrs-menu]]))
;; This is a list of svg tags that can be grouped in shape-container ;; This is a list of svg tags that can be grouped in shape-container
;; this allows them to have gradients, shadows and masks ;; this allows them to have gradients, shadows and masks
(def svg-elements #{:svg :circle :ellipse :image :line :path :polygon :polyline :rect :symbol :text :textPath}) (def svg-elements #{:svg :g :circle :ellipse :image :line :path :polygon :polyline :rect :symbol :text :textPath})
(defn hex->number [hex] 1) (defn hex->number [hex] 1)
(defn shorthex->longhex [hex] (defn shorthex->longhex [hex]
@ -113,4 +114,7 @@
:values (select-keys shape [:shadow])}] :values (select-keys shape [:shadow])}]
[:& blur-menu {:ids ids [:& blur-menu {:ids ids
:values (select-keys shape [:blur])}]]))) :values (select-keys shape [:blur])}]
[:& svg-attrs-menu {:ids ids
:values (select-keys shape [:svg-attrs])}]])))

View file

@ -0,0 +1,203 @@
/**
* Arc to Bezier curves transformer
*
* Is a modified and google closure complatible version of the a2c
* functions by https://github.com/fontello/svgpath
*
* @author UXBOX Labs SL
* @license MIT License <https://opensource.org/licenses/MIT>
*/
"use strict";
goog.provide("app.util.a2c");
// https://raw.githubusercontent.com/fontello/svgpath/master/lib/a2c.js
goog.scope(function() {
const self = app.util.a2c;
var TAU = Math.PI * 2;
/* eslint-disable space-infix-ops */
// Calculate an angle between two unit vectors
//
// Since we measure angle between radii of circular arcs,
// we can use simplified math (without length normalization)
//
function unit_vector_angle(ux, uy, vx, vy) {
var sign = (ux * vy - uy * vx < 0) ? -1 : 1;
var dot = ux * vx + uy * vy;
// Add this to work with arbitrary vectors:
// dot /= Math.sqrt(ux * ux + uy * uy) * Math.sqrt(vx * vx + vy * vy);
// rounding errors, e.g. -1.0000000000000002 can screw up this
if (dot > 1.0) { dot = 1.0; }
if (dot < -1.0) { dot = -1.0; }
return sign * Math.acos(dot);
}
// Convert from endpoint to center parameterization,
// see http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes
//
// Return [cx, cy, theta1, delta_theta]
//
function get_arc_center(x1, y1, x2, y2, fa, fs, rx, ry, sin_phi, cos_phi) {
// Step 1.
//
// Moving an ellipse so origin will be the middlepoint between our two
// points. After that, rotate it to line up ellipse axes with coordinate
// axes.
//
var x1p = cos_phi*(x1-x2)/2 + sin_phi*(y1-y2)/2;
var y1p = -sin_phi*(x1-x2)/2 + cos_phi*(y1-y2)/2;
var rx_sq = rx * rx;
var ry_sq = ry * ry;
var x1p_sq = x1p * x1p;
var y1p_sq = y1p * y1p;
// Step 2.
//
// Compute coordinates of the centre of this ellipse (cx', cy')
// in the new coordinate system.
//
var radicant = (rx_sq * ry_sq) - (rx_sq * y1p_sq) - (ry_sq * x1p_sq);
if (radicant < 0) {
// due to rounding errors it might be e.g. -1.3877787807814457e-17
radicant = 0;
}
radicant /= (rx_sq * y1p_sq) + (ry_sq * x1p_sq);
radicant = Math.sqrt(radicant) * (fa === fs ? -1 : 1);
var cxp = radicant * rx/ry * y1p;
var cyp = radicant * -ry/rx * x1p;
// Step 3.
//
// Transform back to get centre coordinates (cx, cy) in the original
// coordinate system.
//
var cx = cos_phi*cxp - sin_phi*cyp + (x1+x2)/2;
var cy = sin_phi*cxp + cos_phi*cyp + (y1+y2)/2;
// Step 4.
//
// Compute angles (theta1, delta_theta).
//
var v1x = (x1p - cxp) / rx;
var v1y = (y1p - cyp) / ry;
var v2x = (-x1p - cxp) / rx;
var v2y = (-y1p - cyp) / ry;
var theta1 = unit_vector_angle(1, 0, v1x, v1y);
var delta_theta = unit_vector_angle(v1x, v1y, v2x, v2y);
if (fs === 0 && delta_theta > 0) {
delta_theta -= TAU;
}
if (fs === 1 && delta_theta < 0) {
delta_theta += TAU;
}
return [ cx, cy, theta1, delta_theta ];
}
//
// Approximate one unit arc segment with bézier curves,
// see http://math.stackexchange.com/questions/873224
//
function approximate_unit_arc(theta1, delta_theta) {
var alpha = 4/3 * Math.tan(delta_theta/4);
var x1 = Math.cos(theta1);
var y1 = Math.sin(theta1);
var x2 = Math.cos(theta1 + delta_theta);
var y2 = Math.sin(theta1 + delta_theta);
return [ x1, y1, x1 - y1*alpha, y1 + x1*alpha, x2 + y2*alpha, y2 - x2*alpha, x2, y2 ];
}
function a2c(x1, y1, x2, y2, fa, fs, rx, ry, phi) {
var sin_phi = Math.sin(phi * TAU / 360);
var cos_phi = Math.cos(phi * TAU / 360);
// Make sure radii are valid
//
var x1p = cos_phi*(x1-x2)/2 + sin_phi*(y1-y2)/2;
var y1p = -sin_phi*(x1-x2)/2 + cos_phi*(y1-y2)/2;
if (x1p === 0 && y1p === 0) {
// we're asked to draw line to itself
return [];
}
if (rx === 0 || ry === 0) {
// one of the radii is zero
return [];
}
// Compensate out-of-range radii
//
rx = Math.abs(rx);
ry = Math.abs(ry);
var lambda = (x1p * x1p) / (rx * rx) + (y1p * y1p) / (ry * ry);
if (lambda > 1) {
rx *= Math.sqrt(lambda);
ry *= Math.sqrt(lambda);
}
// Get center parameters (cx, cy, theta1, delta_theta)
//
var cc = get_arc_center(x1, y1, x2, y2, fa, fs, rx, ry, sin_phi, cos_phi);
var result = [];
var theta1 = cc[2];
var delta_theta = cc[3];
// Split an arc to multiple segments, so each segment
// will be less than τ/4 (= 90°)
//
var segments = Math.max(Math.ceil(Math.abs(delta_theta) / (TAU / 4)), 1);
delta_theta /= segments;
for (var i = 0; i < segments; i++) {
result.push(approximate_unit_arc(theta1, delta_theta));
theta1 += delta_theta;
}
// We have a bezier approximation of a unit circle,
// now need to transform back to the original ellipse
//
return result.map(function (curve) {
for (var i = 0; i < curve.length; i += 2) {
var x = curve[i + 0];
var y = curve[i + 1];
// scale
x *= rx;
y *= ry;
// rotate
var xp = cos_phi*x - sin_phi*y;
var yp = sin_phi*x + cos_phi*y;
// translate
curve[i + 0] = xp + cc[0];
curve[i + 1] = yp + cc[1];
}
return curve;
});
}
self.a2c = a2c;
});

View file

@ -116,6 +116,11 @@
(= id :multiple) (= id :multiple)
(= file-id :multiple))) (= file-id :multiple)))
(defn color? [^string color-str]
(and (not (nil? color-str))
(not (empty? color-str))
(gcolor/isValidColor color-str)))
(defn parse-color [^string color-str] (defn parse-color [^string color-str]
(let [result (gcolor/parse color-str)] (let [result (gcolor/parse color-str)]
(str (.-hex ^js result)))) (str (.-hex ^js result))))

View file

@ -9,12 +9,13 @@
(ns app.util.geom.path (ns app.util.geom.path
(:require (:require
[cuerdas.core :as str]
[app.common.data :as cd] [app.common.data :as cd]
[app.util.data :as d]
[app.common.data :as cd] [app.common.data :as cd]
[app.common.geom.point :as gpt] [app.common.geom.point :as gpt]
[app.util.geom.path-impl-simplify :as impl-simplify])) [app.util.a2c :refer [a2c]]
[app.util.data :as d]
[app.util.geom.path-impl-simplify :as impl-simplify]
[cuerdas.core :as str]))
(defn simplify (defn simplify
([points] ([points]
@ -28,23 +29,42 @@
;; Matches numbers for path values allows values like... -.01, 10, +12.22 ;; Matches numbers for path values allows values like... -.01, 10, +12.22
;; 0 and 1 are special because can refer to flags ;; 0 and 1 are special because can refer to flags
(def num-regex #"([+-]?(([1-9]\d*(\.\d+)?)|(\.\d+)|0|1))") (def num-regex #"[+-]?(\d+(\.\d+)?|\.\d+)")
(def flag-regex #"[01]")
(defn coord-n [size] (defn fix-dot-number [val]
(re-pattern (str "(?i)[a-z]\\s*" (if (str/starts-with? val ".")
(->> (range size) (str "0" val)
(map #(identity num-regex)) val))
(str/join "\\s+")))))
(defn extract-params [cmd-str extract-commands]
(loop [result []
extract-idx 0
current {}
remain (-> cmd-str (subs 1) (str/trim))]
(defn parse-params [cmd-str num-params] (let [[param type] (nth extract-commands extract-idx)
(let [fix-starting-dot (fn [arg] (str/replace arg #"([^\d]|^)\." "$10."))] regex (case type
(->> (re-seq num-regex cmd-str) :flag flag-regex
(map first) #_:number num-regex)
(map fix-starting-dot) match (re-find regex remain)]
(map d/read-string)
(partition num-params)))) (if match
(let [value (-> match first fix-dot-number d/read-string)
remain (str/replace-first remain regex "")
current (assoc current param value)
extract-idx (inc extract-idx)
[result current extract-idx]
(if (>= extract-idx (count extract-commands))
[(conj result current) {} 0]
[result current extract-idx])]
(recur result
extract-idx
current
remain))
(cond-> result
(not (empty? current)) (conj current))))))
(defn command->param-list [{:keys [command params]}] (defn command->param-list [{:keys [command params]}]
(case command (case command
@ -73,96 +93,99 @@
(defmethod parse-command "M" [cmd] (defmethod parse-command "M" [cmd]
(let [relative (str/starts-with? cmd "m") (let [relative (str/starts-with? cmd "m")
params (parse-params cmd 2)] param-list (extract-params cmd [[:x :number]
(for [[x y] params] [:y :number]])]
(for [params param-list]
{:command :move-to {:command :move-to
:relative relative :relative relative
:params {:x x :y y}}))) :params params})))
(defmethod parse-command "Z" [cmd] (defmethod parse-command "Z" [cmd]
[{:command :close-path}]) [{:command :close-path}])
(defmethod parse-command "L" [cmd] (defmethod parse-command "L" [cmd]
(let [relative (str/starts-with? cmd "l") (let [relative (str/starts-with? cmd "l")
params (parse-params cmd 2)] param-list (extract-params cmd [[:x :number]
(for [[x y] params] [:y :number]])]
(for [params param-list]
{:command :line-to {:command :line-to
:relative relative :relative relative
:params {:x x :y y}}))) :params params})))
(defmethod parse-command "H" [cmd] (defmethod parse-command "H" [cmd]
(let [relative (str/starts-with? cmd "h") (let [relative (str/starts-with? cmd "h")
params (parse-params cmd 1)] param-list (extract-params cmd [[:value :number]])]
(for [[value] params] (for [params param-list]
{:command :line-to-horizontal {:command :line-to-horizontal
:relative relative :relative relative
:params {:value value}}))) :params params})))
(defmethod parse-command "V" [cmd] (defmethod parse-command "V" [cmd]
(let [relative (str/starts-with? cmd "v") (let [relative (str/starts-with? cmd "v")
params (parse-params cmd 1)] param-list (extract-params cmd [[:value :number]])]
(for [[value] params] (for [params param-list]
{:command :line-to-vertical {:command :line-to-vertical
:relative relative :relative relative
:params {:value value}}))) :params params})))
(defmethod parse-command "C" [cmd] (defmethod parse-command "C" [cmd]
(let [relative (str/starts-with? cmd "c") (let [relative (str/starts-with? cmd "c")
params (parse-params cmd 6)] param-list (extract-params cmd [[:c1x :number]
(for [[c1x c1y c2x c2y x y] params] [:c1y :number]
[:c2x :number]
[:c2y :number]
[:x :number]
[:y :number]])
]
(for [params param-list]
{:command :curve-to {:command :curve-to
:relative relative :relative relative
:params {:c1x c1x :params params})))
:c1y c1y
:c2x c2x
:c2y c2y
:x x
:y y}})))
(defmethod parse-command "S" [cmd] (defmethod parse-command "S" [cmd]
(let [relative (str/starts-with? cmd "s") (let [relative (str/starts-with? cmd "s")
params (parse-params cmd 4)] param-list (extract-params cmd [[:c1x :number]
(for [[cx cy x y] params] [:c2y :number]
[:x :number]
[:y :number]])]
(for [params param-list]
{:command :smooth-curve-to {:command :smooth-curve-to
:relative relative :relative relative
:params {:cx cx :params params})))
:cy cy
:x x
:y y}})))
(defmethod parse-command "Q" [cmd] (defmethod parse-command "Q" [cmd]
(let [relative (str/starts-with? cmd "s") (let [relative (str/starts-with? cmd "s")
params (parse-params cmd 4)] param-list (extract-params cmd [[:c1x :number]
(for [[cx cy x y] params] [:c1y :number]
[:x :number]
[:y :number]])]
(for [params param-list]
{:command :quadratic-bezier-curve-to {:command :quadratic-bezier-curve-to
:relative relative :relative relative
:params {:cx cx :params params})))
:cy cy
:x x
:y y}})))
(defmethod parse-command "T" [cmd] (defmethod parse-command "T" [cmd]
(let [relative (str/starts-with? cmd "t") (let [relative (str/starts-with? cmd "t")
params (parse-params cmd (coord-n 2))] param-list (extract-params cmd [[:x :number]
(for [[cx cy x y] params] [:y :number]])]
(for [params param-list]
{:command :smooth-quadratic-bezier-curve-to {:command :smooth-quadratic-bezier-curve-to
:relative relative :relative relative
:params {:x x :params params})))
:y y}})))
(defmethod parse-command "A" [cmd] (defmethod parse-command "A" [cmd]
(let [relative (str/starts-with? cmd "a") (let [relative (str/starts-with? cmd "a")
params (parse-params cmd 7)] param-list (extract-params cmd [[:rx :number]
(for [[rx ry x-axis-rotation large-arc-flag sweep-flag x y] params] [:ry :number]
[:x-axis-rotation :number]
[:large-arc-flag :flag]
[:sweep-flag :flag]
[:x :number]
[:y :number]])]
(for [params param-list]
{:command :elliptical-arc {:command :elliptical-arc
:relative relative :relative relative
:params {:rx rx :params params})))
:ry ry
:x-axis-rotation x-axis-rotation
:large-arc-flag large-arc-flag
:sweep-flag sweep-flag
:x x
:y y}})))
(defn command->string [{:keys [command relative params] :as entry}] (defn command->string [{:keys [command relative params] :as entry}]
(let [command-str (case command (let [command-str (case command
@ -185,6 +208,21 @@
(contains? params :y)) (contains? params :y))
(gpt/point params))) (gpt/point params)))
(defn arc->beziers [prev command]
(let [to-command
(fn [[_ _ c1x c1y c2x c2y x y]]
{:command :curve-to
:relative (:relative command)
:params {:c1x c1x :c1y c1y
:c2x c2x :c2y c2y
:x x :y y}})
{from-x :x from-y :y} (:params prev)
{:keys [rx ry x-axis-rotation large-arc-flag sweep-flag x y]} (:params command)
result (a2c from-x from-y x y large-arc-flag sweep-flag rx ry x-axis-rotation)]
(mapv to-command result)))
(defn simplify-commands (defn simplify-commands
"Removes some commands and convert relative to absolute coordinates" "Removes some commands and convert relative to absolute coordinates"
[commands] [commands]
@ -208,11 +246,13 @@
(:relative command) (:relative command)
(-> (assoc :relative false) (-> (assoc :relative false)
(cd/update-in-when [:params :x] + (:x pos)) (cd/update-in-when [:params :x] + (:x pos))
(cd/update-in-when [:params :y] + (:y pos))) (cd/update-in-when [:params :y] + (:y pos))))
result #_(conj result command)
)] (if (= :elliptical-arc (:command command))
[(cmd-pos command) (conj result command)])) (cd/concat result (arc->beziers prev command))
(conj result command))]
[(cmd-pos command) result]))
start (first commands) start (first commands)
start-pos (cmd-pos start)] start-pos (cmd-pos start)]

View file

@ -11,9 +11,16 @@
(:require (:require
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.common.data :as cd] [app.common.data :as cd]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[cuerdas.core :as str])) [cuerdas.core :as str]))
(defonce replace-regex #"[^#]*#([^)\s]+).*") (defonce replace-regex #"#([^\W]+)")
(defn extract-ids [val]
(->> (re-seq replace-regex val)
(mapv second)))
(defn clean-attrs (defn clean-attrs
"Transforms attributes to their react equivalent" "Transforms attributes to their react equivalent"
@ -36,34 +43,39 @@
(into {}))) (into {})))
(map-fn [[key val]] (map-fn [[key val]]
(cond (let [key (keyword key)]
(= key :class) [:className val] (cond
(and (= key :style) (string? val)) [key (format-styles val)] (= key :class) [:className val]
:else (vector (transform-key key) val)))] (and (= key :style) (string? val)) [key (format-styles val)]
:else (vector (transform-key key) val))))]
(->> attrs (->> attrs
(map map-fn) (map map-fn)
(into {})))) (into {}))))
(defn update-attr-ids
"Replaces the ids inside a property"
[attrs replace-fn]
(letfn [(update-ids [key val]
(cond
(map? val)
(cd/mapm update-ids val)
(= key :id)
(replace-fn val)
:else
(let [replace-id
(fn [result it]
(str/replace result it (replace-fn it)))]
(reduce replace-id val (extract-ids val)))))]
(cd/mapm update-ids attrs)))
(defn replace-attrs-ids (defn replace-attrs-ids
"Replaces the ids inside a property" "Replaces the ids inside a property"
[attrs ids-mapping] [attrs ids-mapping]
(if (and ids-mapping (not (empty? ids-mapping))) (if (and ids-mapping (not (empty? ids-mapping)))
(letfn [(replace-ids [key val] (update-attr-ids attrs (fn [id] (get ids-mapping id id)))
(cond
(map? val)
(cd/mapm replace-ids val)
(and (= key :id) (contains? ids-mapping val))
(get ids-mapping val)
:else
(let [[_ from-id] (re-matches replace-regex val)]
(if (and from-id (contains? ids-mapping from-id))
(str/replace val from-id (get ids-mapping from-id))
val))))]
(cd/mapm replace-ids attrs))
;; Ids-mapping is null ;; Ids-mapping is null
attrs)) attrs))
@ -74,3 +86,83 @@
element-id (assoc element-id (str (uuid/next))))] element-id (assoc element-id (str (uuid/next))))]
(reduce visit-node result (:content node))))] (reduce visit-node result (:content node))))]
(visit-node {} content))) (visit-node {} content)))
(defn extract-defs [{:keys [tag content] :as node}]
(if-not (map? node)
[{} node]
(letfn [(def-tag? [{:keys [tag]}] (= tag :defs))
(assoc-node [result node]
(assoc result (-> node :attrs :id) node))
(node-data [node]
(->> (:content node) (reduce assoc-node {})))]
(let [current-def (->> content
(filterv def-tag?)
(map node-data)
(reduce merge))
result (->> content
(filter (comp not def-tag?))
(map extract-defs))
current-def (->> result (map first) (reduce merge current-def))
content (->> result (mapv second))]
[current-def (assoc node :content content)]))))
(defn find-attr-references [attrs]
(->> attrs
(mapcat (fn [[_ attr-value]] (extract-ids attr-value)))))
(defn find-node-references [node]
(let [current (->> (find-attr-references (:attrs node)) (into #{}))
children (->> (:content node) (map find-node-references) (flatten) (into #{}))]
(-> (cd/concat current children)
(vec))))
(defn find-def-references [defs references]
(loop [result (into #{} references)
checked? #{}
to-check (first references)
pending (rest references)]
(cond
(nil? to-check)
result
(checked? to-check)
(recur result
checked?
(first pending)
(rest pending))
:else
(let [node (get defs to-check)
new-refs (find-node-references node)]
(recur (cd/concat result new-refs)
(conj checked? to-check)
(first pending)
(rest pending))))))
(defn svg-transform-matrix [shape]
(if (:svg-viewbox shape)
(let [{svg-x :x
svg-y :y
svg-width :width
svg-height :height} (:svg-viewbox shape)
{:keys [x y width height]} (:selrect shape)
scale-x (/ width svg-width)
scale-y (/ height svg-height)]
(gmt/multiply
(gmt/matrix)
(gsh/transform-matrix shape)
(gmt/translate-matrix (gpt/point (- x (* scale-x svg-x)) (- y (* scale-y svg-y))))
(gmt/scale-matrix (gpt/point scale-x scale-y))))
;; :else
(gmt/matrix)))

View file

@ -1,3 +1,4 @@
/*
const plugins = [ const plugins = [
{removeDimensions: true}, {removeDimensions: true},
{removeScriptElement: true}, {removeScriptElement: true},
@ -11,6 +12,63 @@ const plugins = [
forceAbsolutePath: true, forceAbsolutePath: true,
}} }}
]; ];
*/
const plugins = [
'removeDoctype',
'removeXMLProcInst',
'removeComments',
'removeMetadata',
// 'removeXMLNS',
'removeEditorsNSData',
'cleanupAttrs',
'inlineStyles',
'minifyStyles',
// 'convertStyleToAttrs'
'cleanupIDs',
// 'prefixIds',
// 'removeRasterImages',
// 'removeUselessDefs',
'cleanupNumericValues',
// 'cleanupListOfValues',
'convertColors',
'removeUnknownsAndDefaults',
'removeNonInheritableGroupAttrs',
'removeUselessStrokeAndFill',
// 'removeViewBox',
'cleanupEnableBackground',
'removeHiddenElems',
'removeEmptyText',
'convertShapeToPath',
'convertEllipseToCircle',
// 'moveElemsAttrsToGroup',
'moveGroupAttrsToElems',
'collapseGroups',
{'convertPathData': {
'lineShorthands': false,
'curveSmoothShorthands': false,
'forceAbsolutePath': true,
}},
'convertTransform',
'removeEmptyAttrs',
'removeEmptyContainers',
'mergePaths',
'removeUnusedNS',
// 'sortAttrs',
'sortDefsChildren',
'removeTitle',
'removeDesc',
'removeDimensions',
'removeAttrs',
// 'removeAttributesBySelector',
// 'removeElementsByAttr',
// 'addClassesToSVGElement',
'removeStyleElement',
'removeScriptElement',
// 'addAttributesToSVGElement',
// 'removeOffCanvasPaths',
// 'reusePaths',
];
const svgc = require("./src/svgclean.js"); const svgc = require("./src/svgclean.js");
const inst = svgc.configure({plugins}); const inst = svgc.configure({plugins});