0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-03-13 16:21:57 -05:00

Merge pull request #733 from penpot/feat/svg-native

Advanced SVG Import
This commit is contained in:
Andrey Antukh 2021-03-08 13:26:50 +01:00 committed by GitHub
commit 99d9d77c63
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 2148 additions and 510 deletions

View file

@ -6,9 +6,9 @@
- Add more chinese translations [#726](https://github.com/penpot/penpot/pull/726)
- Duplicate and move files and projects [Taiga #267](https://tree.taiga.io/project/penpot/us/267)
- Import SVG will create Penpot's shapes
- Improve french translations [#731](https://github.com/penpot/penpot/pull/731)
### :bug: Bugs fixed
- Fix broken profile and profile options form.
@ -16,6 +16,7 @@
### :heart: Community contributions by (Thank you!)
- iblueer [#731](https://github.com/penpot/penpot/pull/731)
## 1.3.0-alpha

File diff suppressed because one or more lines are too long

View file

@ -14,7 +14,9 @@
[promesa.exec :as px]))
(def default-client
(delay (http/build-client {:executor @px/default-executor})))
(delay (http/build-client {:executor @px/default-executor
:connect-timeout 10000 ;; 10s
:follow-redirects :always})))
(defn get!
[url opts]

View file

@ -21,4 +21,4 @@
data (slurp (io/resource "app/tests/_files/sample1.svg"))
res (svgc data)]
(t/is (string? res))
(t/is (= 2533 (count res)))))
(t/is (= 2609 (count res)))))

View file

@ -6,7 +6,7 @@
(ns app.common.data
"Data manipulation and query helper functions."
(:refer-clojure :exclude [concat read-string hash-map merge])
(:refer-clojure :exclude [concat read-string hash-map merge name])
#?(:cljs
(:require-macros [app.common.data]))
(:require
@ -132,8 +132,9 @@
"Return a map without the keys provided
in the `keys` parameter."
[data keys]
(persistent!
(reduce #(dissoc! %1 %2) (transient data) keys)))
(when data
(persistent!
(reduce #(dissoc! %1 %2) (transient data) keys))))
(defn remove-at-index
[v index]
@ -302,6 +303,14 @@
default
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
[v]
(r/read-string v))
@ -350,7 +359,7 @@
;; Code for ClojureScript
(let [mdata (aapi/resolve &env v)
arglists (second (get-in mdata [:meta :arglists]))
sym (symbol (name v))
sym (symbol (core/name v))
andsym (symbol "&")
procarg #(if (= % andsym) % (gensym "param"))]
(if (pos? (count arglists))
@ -382,3 +391,16 @@
(defn any-key? [element & rest]
(some #(contains? element %) rest))
(defn name
"Improved version of name that won't fail if the input is not a keyword"
[maybe-keyword]
(cond
(keyword? maybe-keyword)
(core/name maybe-keyword)
(nil? maybe-keyword) nil
:else
(str maybe-keyword)))

View file

@ -11,6 +11,8 @@
(:require
#?(:cljs [cljs.pprint :as pp]
:clj [clojure.pprint :as pp])
[cuerdas.core :as str]
[app.common.data :as d]
[app.common.math :as mth]
[app.common.geom.point :as gpt]))
@ -21,6 +23,13 @@
(toString [_]
(str "matrix(" a "," b "," c "," d "," e "," f ")")))
(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 multiply
([{m1a :a m1b :b m1c :c m1d :d m1e :e m1f :f}
{m2a :a m2b :b m2c :c m2d :d m2e :e m2f :f}]
@ -46,13 +55,6 @@
[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))
(defn base?

View file

@ -204,6 +204,11 @@
(defn to-vec [p1 p2]
(subtract p2 p1))
(defn scale [v scalar]
(-> v
(update :x * scalar)
(update :y * scalar)))
(defn dot [{x1 :x y1 :y} {x2 :x y2 :y}]
(+ (* x1 x2) (* y1 y2)))

View file

@ -148,10 +148,10 @@
(update-in [:selrect :x2] - x)
(update-in [:selrect :y2] - y)
(d/update-when :points #(map move-point %))
(d/update-when :points #(mapv move-point %))
(cond-> (= :path type)
(d/update-when :content #(map move-segment %))))))
(d/update-when :content #(mapv move-segment %))))))
;; --- Helpers
@ -281,6 +281,7 @@
(d/export gtr/transform-rect)
(d/export gtr/update-group-selrect)
(d/export gtr/transform-points)
(d/export gtr/calculate-adjust-matrix)
;; PATHS
(d/export gsp/content->points)

View file

@ -139,11 +139,15 @@
(defn- calculate-height
"Calculates the height of a paralelogram given by the points"
[[p1 _ p3 p4]]
(let [v1 (gpt/to-vec p3 p4)
v2 (gpt/to-vec p4 p1)
angle (gpt/angle-with-other v1 v2)]
(* (gpt/length v2) (mth/sin (mth/radians angle)))))
[[p1 _ _ p4]]
(-> (gpt/to-vec p4 p1)
(gpt/length)))
(defn- calculate-width
"Calculates the width of a paralelogram given by the points"
[[p1 p2 _ _]]
(-> (gpt/to-vec p1 p2)
(gpt/length)))
(defn- calculate-rotation
"Calculates the rotation between two shapes given the resize vector direction"
@ -173,44 +177,49 @@
"Calculates a matrix that is a series of transformations we have to do to the transformed rectangle so that
after applying them the end result is the `shape-pathn-temp`.
This is compose of three transformations: skew, resize and rotation"
[points-temp points-rec flip-x flip-y]
(let [center (gco/center-points points-temp)
([points-temp points-rec] (calculate-adjust-matrix points-temp points-rec false false))
([points-temp points-rec flip-x flip-y]
(let [center (gco/center-points points-temp)
stretch-matrix (gmt/matrix)
stretch-matrix (gmt/matrix)
skew-angle (calculate-skew-angle points-temp)
skew-angle (calculate-skew-angle points-temp)
;; When one of the axis is flipped we have to reverse the skew
;; skew-angle (if (neg? (* (:x resize-vector) (:y resize-vector))) (- skew-angle) skew-angle )
skew-angle (if (and (or flip-x flip-y)
(not (and flip-x flip-y))) (- skew-angle) skew-angle )
skew-angle (if (mth/nan? skew-angle) 0 skew-angle)
;; When one of the axis is flipped we have to reverse the skew
;; skew-angle (if (neg? (* (:x resize-vector) (:y resize-vector))) (- skew-angle) skew-angle )
skew-angle (if (and (or flip-x flip-y)
(not (and flip-x flip-y))) (- skew-angle) skew-angle )
skew-angle (if (mth/nan? skew-angle) 0 skew-angle)
stretch-matrix (gmt/multiply stretch-matrix (gmt/skew-matrix skew-angle 0))
stretch-matrix (gmt/multiply stretch-matrix (gmt/skew-matrix skew-angle 0))
h1 (max 1 (calculate-height points-temp))
h2 (max 1 (calculate-height (transform-points points-rec center stretch-matrix)))
h3 (if-not (mth/almost-zero? h2) (/ h1 h2) 1)
h3 (if (mth/nan? h3) 1 h3)
h1 (max 1 (calculate-height points-temp))
h2 (max 1 (calculate-height (transform-points points-rec center stretch-matrix)))
h3 (if-not (mth/almost-zero? h2) (/ h1 h2) 1)
h3 (if (mth/nan? h3) 1 h3)
stretch-matrix (gmt/multiply stretch-matrix (gmt/scale-matrix (gpt/point 1 h3)))
w1 (max 1 (calculate-width points-temp))
w2 (max 1 (calculate-width (transform-points points-rec center stretch-matrix)))
w3 (if-not (mth/almost-zero? w2) (/ w1 w2) 1)
w3 (if (mth/nan? w3) 1 w3)
rotation-angle (calculate-rotation
center
(transform-points points-rec (gco/center-points points-rec) stretch-matrix)
points-temp
flip-x
flip-y)
stretch-matrix (gmt/multiply stretch-matrix (gmt/scale-matrix (gpt/point w3 h3)))
stretch-matrix (gmt/multiply (gmt/rotate-matrix rotation-angle) stretch-matrix)
rotation-angle (calculate-rotation
center
(transform-points points-rec (gco/center-points points-rec) stretch-matrix)
points-temp
flip-x
flip-y)
stretch-matrix (gmt/multiply (gmt/rotate-matrix rotation-angle) stretch-matrix)
;; This is the inverse to be able to remove the transformation
stretch-matrix-inverse (-> (gmt/matrix)
(gmt/scale (gpt/point 1 (/ 1 h3)))
(gmt/skew (- skew-angle) 0)
(gmt/rotate (- rotation-angle)))]
[stretch-matrix stretch-matrix-inverse]))
;; This is the inverse to be able to remove the transformation
stretch-matrix-inverse (-> (gmt/matrix)
(gmt/scale (gpt/point (/ 1 w3) (/ 1 h3)))
(gmt/skew (- skew-angle) 0)
(gmt/rotate (- rotation-angle)))]
[stretch-matrix stretch-matrix-inverse rotation-angle])))
(defn apply-transform
"Given a new set of points transformed, set up the rectangle so it keeps
@ -276,6 +285,24 @@
(dissoc :modifiers)))
shape)))
(defn update-group-viewbox
"Updates the viewbox for groups imported from SVG's"
[{:keys [selrect svg-viewbox] :as group} new-selrect]
(let [;; Gets deltas for the selrect to update the svg-viewbox (for svg-imports)
deltas {:x (- (:x new-selrect) (:x selrect))
:y (- (:y new-selrect) (:y selrect))
:width (- (:width new-selrect) (:width selrect))
:height (- (:height new-selrect) (:height selrect))}]
(cond-> group
svg-viewbox
(update :svg-viewbox
#(-> %
(update :x + (:x deltas))
(update :y + (:y deltas))
(update :width + (:width deltas))
(update :height + (:height deltas)))))))
(defn update-group-selrect [group children]
(let [shape-center (gco/center-shape group)
transform (:transform group (gmt/matrix))
@ -297,6 +324,7 @@
;; Updates the shape and the applytransform-rect will update the other properties
(-> group
(update-group-viewbox new-selrect)
(assoc :selrect new-selrect)
(assoc :points new-points)

View file

@ -229,7 +229,7 @@
(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-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-alignment #{:center :inner :outer})
(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,45 @@
"zh_cn" : "单击以闭合路径"
},
"unused" : true
},
"workspace.sidebar.options.svg-attrs.title": {
"translations": {
"en": "Imported SVG Attributes",
"es": "Atributos del SVG Importado"
}
},
"handoff.attributes.stroke.alignment.inner" : {
"translations" : {
"en" : "Inside",
"es" : "Interior",
"fr" : "Intérieur",
"ru" : "Внутрь",
"zh_cn" : "内部"
},
"permanent": true
},
"handoff.attributes.stroke.alignment.outer" : {
"translations" : {
"en" : "Outside",
"es" : "Exterior",
"fr" : "Extérieur",
"ru" : "Наружу",
"zh_cn" : "外部"
},
"permanent": true
},
"handoff.attributes.stroke.alignment.center" : {
"translations" : {
"en" : "Center",
"es" : "Centro",
"fr" : "Centre",
"ru" : "Центр",
"zh_cn" : "居中"
},
"permanent": true
}
}

View file

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

View file

@ -94,6 +94,10 @@
.attributes-label,
.attributes-value {
margin-right: 0.5rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 50%;
}
.copy-button {

View file

@ -709,6 +709,9 @@
.element-set-content .input-row {
& .element-set-subtitle {
width: 5.5rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
@ -771,7 +774,6 @@
min-width: 56px;
max-height: 10rem;
}
}
}
@ -911,6 +913,7 @@
}
.row-flex-removable:hover .element-set-actions,
.element-set-options-group:hover .element-set-actions {
visibility: visible;
}

View file

@ -219,15 +219,19 @@
(defn generate-unique-name
"A unique name generator"
[used basename]
(s/assert ::set-of-string used)
(s/assert ::us/string basename)
(let [[prefix initial] (extract-numeric-suffix basename)]
(loop [counter initial]
(let [candidate (str prefix "-" counter)]
(if (contains? used candidate)
(recur (inc counter))
candidate)))))
([used basename]
(generate-unique-name used basename false))
([used basename prefix-first?]
(s/assert ::set-of-string used)
(s/assert ::us/string basename)
(let [[prefix initial] (extract-numeric-suffix basename)]
(loop [counter initial]
(let [candidate (if (and (= 1 counter) prefix-first?)
(str prefix)
(str prefix "-" counter))]
(if (contains? used candidate)
(recur (inc counter))
candidate))))))
;; --- Shape attrs (Layers Sidebar)
@ -574,36 +578,39 @@
[frame-id parent-id (inc index)])))))
(defn add-shape-changes
[page-id objects selected attrs]
(let [id (:id attrs)
shape (gpr/setup-proportions attrs)
([page-id objects selected attrs]
(add-shape-changes page-id objects selected attrs true))
([page-id objects selected attrs reg-object?]
(let [id (:id attrs)
shape (gpr/setup-proportions attrs)
default-attrs (if (= :frame (:type shape))
cp/default-frame-attrs
cp/default-shape-attrs)
default-attrs (if (= :frame (:type shape))
cp/default-frame-attrs
cp/default-shape-attrs)
shape (merge default-attrs shape)
shape (merge default-attrs shape)
not-frame? #(not (= :frame (get-in objects [% :type])))
selected (into #{} (filter not-frame?) selected)
not-frame? #(not (= :frame (get-in objects [% :type])))
selected (into #{} (filter not-frame?) selected)
[frame-id parent-id index] (get-shape-layer-position objects selected attrs)
[frame-id parent-id index] (get-shape-layer-position objects selected attrs)
redo-changes [{:type :add-obj
:id id
:page-id page-id
:frame-id frame-id
:parent-id parent-id
:index index
:obj shape}
{:type :reg-objects
:page-id page-id
:shapes [id]}]
undo-changes [{:type :del-obj
:page-id page-id
:id id}]]
redo-changes (cond-> [{:type :add-obj
:id id
:page-id page-id
:frame-id frame-id
:parent-id parent-id
:index index
:obj shape}]
reg-object?
(conj {:type :reg-objects
:page-id page-id
:shapes [id]}))
undo-changes [{:type :del-obj
:page-id page-id
:id id}]]
[redo-changes undo-changes]))
[redo-changes undo-changes])))
(defn add-shape
[attrs]

View file

@ -32,10 +32,12 @@
[app.util.router :as rt]
[app.util.time :as dt]
[app.util.transit :as t]
[app.util.uri :as uu]
[beicon.core :as rx]
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[potok.core :as ptk]))
[potok.core :as ptk]
[promesa.core :as p]))
(declare persist-changes)
(declare persist-sychronous-changes)
@ -392,7 +394,7 @@
(or (contains? props :data)
(contains? props :uris)))))
(defn parse-svg [text]
(defn parse-svg [[name text]]
(->> (http/send! {:method :post
:uri "/api/svg"
:headers {"content-type" "image/svg+xml"}
@ -400,19 +402,14 @@
(rx/map (fn [{:keys [status body]}]
(let [result (t/decode body)]
(if (= status 200)
result
(assoc result :name name)
(throw result)))))))
(defn fetch-svg [uri]
(defn fetch-svg [name uri]
(->> (http/send! {:method :get :uri uri})
(rx/map :body)))
(defn url-name [url]
(let [query-idx (str/last-index-of url "?")
url (if (> query-idx 0) (subs url 0 query-idx) url)
filename (->> (str/split url "/") (last))
ext-idx (str/last-index-of filename ".")]
(if (> ext-idx 0) (subs filename 0 ext-idx) filename)))
(rx/map #(vector
(or name (uu/uri-name uri))
(:body %)))))
(defn- handle-upload-error [on-error stream]
(->> stream
@ -456,7 +453,7 @@
(prepare-uri [uri]
{:file-id file-id
:is-local local?
:name (or name (url-name uri))
:name (or name (uu/uri-name uri))
:url uri})]
(rx/merge
(->> (rx/from uris)
@ -467,10 +464,8 @@
(->> (rx/from uris)
(rx/filter svg-url?)
(rx/merge-map fetch-svg)
(rx/merge-map (partial fetch-svg name))
(rx/merge-map parse-svg)
(rx/with-latest vector uris)
(rx/map #(assoc (first %) :name (or name (url-name (second %)))))
(rx/do on-svg)))))
(defn- upload-data [file-id local? name data force-media on-image on-svg]
@ -485,6 +480,12 @@
:is-local local?
:content blob}))
extract-content
(fn [blob]
(let [name (or name (.-name blob))]
(-> (.text blob)
(p/then #(vector name %)))))
file-stream (->> (rx/from data)
(rx/map di/validate-file))]
(rx/merge
@ -496,10 +497,8 @@
(->> file-stream
(rx/filter svg-blob?)
(rx/merge-map #(.text %))
(rx/merge-map extract-content)
(rx/merge-map parse-svg)
(rx/with-latest vector file-stream)
(rx/map #(assoc (first %) :name (.-name (second %))))
(rx/do on-svg)))))
(defn- upload-media-objects
@ -538,7 +537,7 @@
[params position]
(let [{:keys [x y]} position
mdata {:on-image #(st/emit! (dwc/image-uploaded % x y))
:on-svg #(st/emit! (svg/svg-uploaded % x y))}
:on-svg #(st/emit! (svg/svg-uploaded % (:file-id params) x y))}
params (-> (assoc params :local? true)
(with-meta mdata))]

View file

@ -10,75 +10,151 @@
(ns app.main.data.workspace.svg-upload
(:require
[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.proportions :as gpr]
[app.common.pages :as cp]
[app.common.uuid :as uuid]
[app.main.data.workspace.common :as dwc]
[app.main.repo :as rp]
[app.util.color :as uc]
[app.util.geom.path :as ugp]
[app.util.object :as obj]
[app.util.svg :as usvg]
[app.util.uri :as uu]
[beicon.core :as rx]
[cuerdas.core :as str]
[potok.core :as ptk]
[app.util.svg :as usvg]
[app.util.geom.path :as ugp]))
[promesa.core :as p]))
(defonce default-rect {:x 0 :y 0 :width 1 :height 1 :rx 0 :ry 0})
(defonce default-circle {:r 0 :cx 0 :cy 0})
(defonce default-image {:x 0 :y 0 :width 1 :height 1})
(defn- svg-dimensions [data]
(let [width (get-in data [:attrs :width] 100)
height (get-in data [:attrs :height] 100)
viewbox (get-in data [:attrs :viewBox] (str "0 0 " width " " height))
[_ _ width-str height-str] (str/split viewbox " ")
width (d/parse-integer width-str)
height (d/parse-integer height-str)]
[width height]))
[x y width height] (->> (str/split viewbox " ")
(map d/parse-double))]
[x y width height]))
(defn tag-name [{:keys [tag]}]
(cond (string? tag) tag
(keyword? tag) (name tag)
(nil? tag) "node"
:else (str tag)))
(defn tag->name
"Given a tag returns its layer name"
[tag]
(str "svg-" (cond (string? tag) tag
(keyword? tag) (d/name tag)
(nil? tag) "node"
:else (str tag))))
(defn setup-fill [shape attrs]
(let [fill-color (or (get-in attrs [:fill])
(get-in attrs [:style :fill])
"#000000")
fill-opacity (ud/parse-float (or (get-in attrs [:fill-opacity])
(get-in attrs [:style :fill-opacity])
"1"))]
(-> shape
(assoc :fill-color fill-color)
(assoc :fill-opacity fill-opacity))))
(defn setup-stroke [shape attrs]
(-> 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]}]
(defn setup-fill [shape]
(cond-> shape
(d/any-key? attrs :fill :fill-opacity)
(setup-fill attrs)
(d/any-key? attrs :stroke :stroke-width :stroke-opacity)
(setup-stroke attrs)))
;; Color present as attribute
(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))))
(defn create-raw-svg [name frame-id x y width height data]
(-> {:id (uuid/next)
:type :svg-raw
:name name
:frame-id frame-id
:width width
:height height
:x x
:y y
:content (if (map? data) (update data :attrs usvg/clean-attrs) data)}
(gsh/setup-selrect)))
;; Color present as style
(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))))
(get-in shape [:svg-attrs :fill-opacity])
(-> (update :svg-attrs dissoc :fill-opacity)
(assoc :fill-opacity (-> (get-in shape [:svg-attrs :fill-opacity])
(d/parse-double))))
(get-in shape [:svg-attrs :style :fill-opacity])
(-> (update-in [:svg-attrs :style] dissoc :fill-opacity)
(assoc :fill-opacity (-> (get-in shape [:svg-attrs :style :fill-opacity])
(d/parse-double))))))
(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])
(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)))))]
(if (d/any-key? shape :stroke-color :stroke-opacity :stroke-width)
(merge {:stroke-style :svg} shape)
shape)))
(defn create-raw-svg [name frame-id svg-data {:keys [attrs] :as data}]
(let [{:keys [x y width height offset-x offset-y]} svg-data]
(-> {:id (uuid/next)
:type :svg-raw
:name name
:frame-id frame-id
:width width
:height height
:x x
:y y
:content (cond-> data
(map? data) (update :attrs usvg/clean-attrs))}
(assoc :svg-attrs attrs)
(assoc :svg-viewbox (-> (select-keys svg-data [:width :height])
(assoc :x offset-x :y offset-y)))
(gsh/setup-selrect))))
(defn create-svg-root [frame-id svg-data]
(let [{:keys [name x y width height offset-x offset-y]} svg-data]
(-> {:id (uuid/next)
:type :group
:name name
:frame-id frame-id
:width width
:height height
:x (+ x offset-x)
:y (+ y offset-y)}
(gsh/setup-selrect)
(assoc :svg-attrs (-> (:attrs svg-data)
(dissoc :viewBox :xmlns))))))
(defn create-group [name frame-id svg-data {:keys [attrs]}]
(let [svg-transform (usvg/parse-transform (:transform attrs))
{:keys [x y width height offset-x offset-y]} svg-data]
(-> {:id (uuid/next)
:type :group
:name name
:frame-id frame-id
:x (+ x offset-x)
:y (+ y offset-y)
:width width
:height height}
(assoc :svg-transform svg-transform)
(assoc :svg-attrs (d/without-keys attrs usvg/inheritable-props))
(assoc :svg-viewbox (-> (select-keys svg-data [:width :height])
(assoc :x offset-x :y offset-y)))
(gsh/setup-selrect))))
(defn create-path-shape [name frame-id svg-data {:keys [attrs] :as data}]
(let [svg-transform (usvg/parse-transform (:transform attrs))
path-content (ugp/path->content (:d attrs))
content (cond-> path-content
svg-transform
(gsh/transform-content svg-transform))
(defn parse-path [name frame-id {:keys [attrs] :as data}]
(let [content (ugp/path->content (:d attrs))
selrect (gsh/content->selrect content)
points (gsh/rect->points selrect)]
points (gsh/rect->points selrect)
origin (gpt/negate (gpt/point svg-data))]
(-> {:id (uuid/next)
:type :path
:name name
@ -86,68 +162,283 @@
:content content
:selrect selrect
:points points}
(assoc :svg-viewbox (select-keys selrect [:x :y :width :height]))
(assoc :svg-attrs (dissoc attrs :d :transform))
(assoc :svg-transform svg-transform)
(gsh/translate-to-frame origin))))
(add-style-attributes data))))
(defn calculate-rect-metadata [rect-data transform]
(let [points (-> (gsh/rect->points rect-data)
(gsh/transform-points transform))
(defn parse-svg-element [root-shape data unames]
(let [root-id (:id root-shape)
frame-id (:frame-id root-shape)
{:keys [x y width height]} (:selrect root-shape)
{:keys [tag]} data
name (dwc/generate-unique-name unames (str "svg-" (tag-name data)))
shape
(case tag
;; :rect (parse-rect data)
;; :path (parse-path name frame-id data)
(create-raw-svg name frame-id x y width height data))]
(-> shape
(assoc :svg-id root-id))))
center (gsh/center-points points)
(defn svg-uploaded [data x y]
rect-shape (-> (gsh/make-centered-rect center (:width rect-data) (:height rect-data))
(update :width max 1)
(update :height max 1))
selrect (gsh/rect->selrect rect-shape)
rect-points (gsh/rect->points rect-shape)
[shape-transform shape-transform-inv rotation]
(gsh/calculate-adjust-matrix points rect-points (neg? (:a transform)) (neg? (:d transform)))]
(merge rect-shape
{:selrect selrect
:points points
:rotation rotation
:transform shape-transform
:transform-inverse shape-transform-inv})))
(defn create-rect-shape [name frame-id svg-data {:keys [attrs] :as data}]
(let [svg-transform (usvg/parse-transform (:transform attrs))
transform (->> svg-transform
(gmt/transform-in (gpt/point svg-data)))
rect (->> (select-keys attrs [:x :y :width :height])
(d/mapm #(d/parse-double %2)))
origin (gpt/negate (gpt/point svg-data))
rect-data (-> (merge default-rect rect)
(update :x - (:x origin))
(update :y - (:y origin)))
metadata (calculate-rect-metadata rect-data transform)]
(-> {:id (uuid/next)
:type :rect
:name name
:frame-id frame-id}
(cond->
(contains? attrs :rx) (assoc :rx (d/parse-double (:rx attrs)))
(contains? attrs :ry) (assoc :ry (d/parse-double (:ry attrs))))
(merge metadata)
(assoc :svg-viewbox (select-keys rect [:x :y :width :height]))
(assoc :svg-attrs (dissoc attrs :x :y :width :height :rx :ry :transform)))))
(defn create-circle-shape [name frame-id svg-data {:keys [attrs] :as data}]
(let [svg-transform (usvg/parse-transform (:transform attrs))
transform (->> svg-transform
(gmt/transform-in (gpt/point svg-data)))
circle (->> (select-keys attrs [:r :ry :rx :cx :cy])
(d/mapm #(d/parse-double %2)))
{:keys [cx cy]} circle
rx (or (:r circle) (:rx circle))
ry (or (:r circle) (:ry circle))
rect {:x (- cx rx)
:y (- cy ry)
:width (* 2 rx)
:height (* 2 ry)}
origin (gpt/negate (gpt/point svg-data))
rect-data (-> rect
(update :x - (:x origin))
(update :y - (:y origin)))
metadata (calculate-rect-metadata rect-data transform)]
(-> {:id (uuid/next)
:type :circle
:name name
:frame-id frame-id}
(merge metadata)
(assoc :svg-viewbox (select-keys rect [:x :y :width :height]))
(assoc :svg-attrs (dissoc attrs :cx :cy :r :rx :ry :transform)))))
(defn create-image-shape [name frame-id svg-data {:keys [attrs] :as data}]
(let [svg-transform (usvg/parse-transform (:transform attrs))
transform (->> svg-transform
(gmt/transform-in (gpt/point svg-data)))
image-url (:xlink:href attrs)
image-data (get-in svg-data [:image-data image-url])
rect (->> (select-keys attrs [:x :y :width :height])
(d/mapm #(d/parse-double %2)))
origin (gpt/negate (gpt/point svg-data))
rect-data (-> (merge default-image rect)
(update :x - (:x origin))
(update :y - (:y origin)))
rect-metadata (calculate-rect-metadata rect-data transform)]
(-> {:id (uuid/next)
:type :image
:name name
:frame-id frame-id
:metadata {:width (:width image-data)
:height (:height image-data)
:mtype (:mtype image-data)
:id (:id image-data)}}
(merge rect-metadata)
(assoc :svg-viewbox (select-keys rect [:x :y :width :height]))
(assoc :svg-attrs (dissoc attrs :x :y :width :height :xlink:href)))))
(defn parse-svg-element [frame-id svg-data element-data unames]
(let [{:keys [tag attrs]} element-data
attrs (usvg/format-styles attrs)
element-data (cond-> element-data (map? element-data) (assoc :attrs attrs))
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)
href-id (-> (or (:href attrs) (:xlink:href attrs) "")
(subs 1))
defs (:defs svg-data)
use-tag? (and (= :use tag) (contains? defs href-id))]
(if use-tag?
(let [use-data (get defs href-id)
displacement (gpt/point (d/parse-double (:x attrs "0")) (d/parse-double (:y attrs "0")))
disp-matrix (str (gmt/translate-matrix displacement))
element-data (-> element-data
(assoc :tag :g)
(update :attrs dissoc :x :y :width :height :href :xlink:href)
(update :attrs usvg/add-transform disp-matrix)
(assoc :content [use-data]))]
(parse-svg-element frame-id svg-data element-data unames))
;; SVG graphic elements
;; :circle :ellipse :image :line :path :polygon :polyline :rect :text :use
(let [shape (-> (case tag
(:g :a) (create-group name frame-id svg-data element-data)
:rect (create-rect-shape name frame-id svg-data element-data)
(:circle
:ellipse) (create-circle-shape name frame-id svg-data element-data)
:path (create-path-shape name frame-id svg-data element-data)
:polyline (create-path-shape name frame-id svg-data (-> element-data usvg/polyline->path))
:polygon (create-path-shape name frame-id svg-data (-> element-data usvg/polygon->path))
:line (create-path-shape name frame-id svg-data (-> element-data usvg/line->path))
:image (create-image-shape name frame-id svg-data element-data)
#_other (create-raw-svg name frame-id svg-data element-data))
(assoc :svg-defs (select-keys (:defs svg-data) references))
(setup-fill)
(setup-stroke))
children (cond->> (:content element-data)
(= tag :g)
(mapv #(usvg/inherit-attributes attrs %)))]
[shape children]))))
(defn add-svg-child-changes [page-id objects selected frame-id parent-id svg-data [unames [rchs uchs]] [index data]]
(let [[shape children] (parse-svg-element frame-id svg-data data unames)
shape-id (:id shape)
[rch1 uch1] (dwc/add-shape-changes page-id objects selected shape false)
;; Mov-objects won't have undo because we "delete" the object in the undo of the
;; previous operation
rch2 [{:type :mov-objects
:parent-id parent-id
:frame-id frame-id
:page-id page-id
:index index
:shapes [shape-id]}]
;; Careful! the undo changes are concatenated reversed (we undo in reverse order
changes [(d/concat rchs rch1 rch2) (d/concat uch1 uchs)]
unames (conj unames (:name shape))
reducer-fn (partial add-svg-child-changes page-id objects selected frame-id shape-id svg-data)]
(reduce reducer-fn [unames changes] (d/enumerate children))))
(declare create-svg-shapes)
(defn svg-uploaded [svg-data file-id x y]
(ptk/reify ::svg-uploaded
ptk/WatchEvent
(watch [_ state stream]
(let [page-id (:current-page-id state)
objects (dwc/lookup-page-objects state page-id)
frame-id (cp/frame-id-by-position objects {:x x :y y})
selected (get-in state [:workspace-local :selected])
(let [images-to-upload (-> svg-data (usvg/collect-images))
[width height] (svg-dimensions data)
x (- x (/ width 2))
y (- y (/ height 2))
prepare-uri
(fn [uri]
(merge
{:file-id file-id
:is-local true
:url uri}
add-svg-child
(fn add-svg-child [parent-id root-shape [unames [rchs uchs]] [index {:keys [content] :as data}]]
(let [shape (parse-svg-element root-shape data unames)
shape-id (:id shape)
[rch1 uch1] (dwc/add-shape-changes page-id objects selected shape)
(if (str/starts-with? uri "data:")
{:name "image"
:content (uu/data-uri->blob uri)}
{:name (uu/uri-name uri)})))]
;; Mov-objects won't have undo because we "delete" the object in the undo of the
;; previous operation
rch2 [{:type :mov-objects
:parent-id parent-id
:frame-id frame-id
:page-id page-id
:index index
:shapes [shape-id]}]
(->> (rx/from images-to-upload)
(rx/map prepare-uri)
(rx/mapcat (fn [uri-data]
(->> (rp/mutation! (if (contains? uri-data :content)
:upload-file-media-object
:create-file-media-object-from-url) uri-data)
(rx/map #(vector (:url uri-data) %)))))
(rx/reduce (fn [acc [url image]] (assoc acc url image)) {})
(rx/map #(create-svg-shapes (assoc svg-data :image-data %) x y)))))))
;; Careful! the undo changes are concatenated reversed (we undo in reverse order
changes [(d/concat rchs rch1 rch2) (d/concat uch1 uchs)]
unames (conj unames (:name shape))]
(reduce (partial add-svg-child shape-id root-shape) [unames changes] (d/enumerate content))))
(defn create-svg-shapes [svg-data x y]
(ptk/reify ::create-svg-shapes
ptk/WatchEvent
(watch [_ state stream]
(try
(let [page-id (:current-page-id state)
objects (dwc/lookup-page-objects state page-id)
frame-id (cp/frame-id-by-position objects {:x x :y y})
selected (get-in state [:workspace-local :selected])
unames (dwc/retrieve-used-names objects)
[vb-x vb-y vb-width vb-height] (svg-dimensions svg-data)
x (- x vb-x (/ vb-width 2))
y (- y vb-y (/ vb-height 2))
svg-name (->> (str/replace (:name data) ".svg" "")
(dwc/generate-unique-name unames))
unames (dwc/retrieve-used-names objects)
root-shape (create-raw-svg svg-name frame-id x y width height data)
root-id (:id root-shape)
svg-name (->> (str/replace (:name svg-data) ".svg" "")
(dwc/generate-unique-name unames))
changes (dwc/add-shape-changes page-id objects selected root-shape)
svg-data (-> svg-data
(assoc :x x
:y y
:offset-x vb-x
:offset-y vb-y
:width vb-width
:height vb-height
:name svg-name))
[_ [rchanges uchanges]] (reduce (partial add-svg-child root-id root-shape) [unames changes] (d/enumerate (:content data)))]
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})
(dwc/select-shapes (d/ordered-set root-id)))))))
[def-nodes svg-data] (-> svg-data
(usvg/fix-default-values)
(usvg/fix-percents)
(usvg/extract-defs))
svg-data (assoc svg-data :defs def-nodes)
root-shape (create-svg-root frame-id svg-data)
root-id (:id root-shape)
;; Creates the root shape
changes (dwc/add-shape-changes page-id objects selected root-shape false)
;; Reduces the children to create the changes to add the children shapes
[_ [rchanges uchanges]]
(reduce (partial add-svg-child-changes page-id objects selected frame-id root-id svg-data)
[unames changes]
(d/enumerate (:content svg-data)))
reg-objects-action {:type :reg-objects
:page-id page-id
:shapes (->> rchanges (filter #(= :add-obj (:type %))) (map :id) reverse vec)}
rchanges (conj rchanges reg-objects-action)]
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})
(dwc/select-shapes (d/ordered-set root-id))))
(catch :default e
(.error js/console "Error upload" e))))))

View file

@ -78,7 +78,8 @@
(defn ^:export dump-object [name]
(let [page-id (get @state :current-page-id)]
(let [objects (get-in @state [:workspace-data :pages-index page-id :objects])
target (d/seek (fn [[id shape]] (= name (:name shape))) objects)]
target (or (d/seek (fn [[id shape]] (= name (:name shape))) objects)
(get objects (uuid name)))]
(->> target
(logjs "state")))))

View file

@ -19,16 +19,17 @@
[app.main.ui.handoff.attributes.shadow :refer [shadow-panel]]
[app.main.ui.handoff.attributes.blur :refer [blur-panel]]
[app.main.ui.handoff.attributes.image :refer [image-panel]]
[app.main.ui.handoff.attributes.text :refer [text-panel]]))
[app.main.ui.handoff.attributes.text :refer [text-panel]]
[app.main.ui.handoff.attributes.svg :refer [svg-panel]]))
(def type->options
{:multiple [:fill :stroke :image :text :shadow :blur]
:frame [:layout :fill]
:group [:layout]
:rect [:layout :fill :stroke :shadow :blur]
:circle [:layout :fill :stroke :shadow :blur]
:path [:layout :fill :stroke :shadow :blur]
:image [:image :layout :shadow :blur]
:group [:layout :svg]
:rect [:layout :fill :stroke :shadow :blur :svg]
:circle [:layout :fill :stroke :shadow :blur :svg]
:path [:layout :fill :stroke :shadow :blur :svg]
:image [:image :layout :shadow :blur :svg]
:text [:layout :text :shadow :blur]})
(mf/defc attributes
@ -46,7 +47,8 @@
:shadow shadow-panel
:blur blur-panel
:image image-panel
:text text-panel)
:text text-panel
:svg svg-panel)
{:shapes shapes
:frame frame
:locale locale}])

View file

@ -37,53 +37,55 @@
(mf/defc layout-block
[{:keys [shape locale]}]
[:*
[:div.attributes-unit-row
[:div.attributes-label (t locale "handoff.attributes.layout.width")]
[:div.attributes-value (mth/precision (:width shape) 2) "px"]
[:& copy-button {:data (copy-data shape :width)}]]
[:div.attributes-unit-row
[:div.attributes-label (t locale "handoff.attributes.layout.height")]
[:div.attributes-value (mth/precision (:height shape) 2) "px"]
[:& copy-button {:data (copy-data shape :height)}]]
(when (not= (:x shape) 0)
(let [selrect (:selrect shape)
{:keys [width height x y]} selrect]
[:*
[:div.attributes-unit-row
[:div.attributes-label (t locale "handoff.attributes.layout.left")]
[:div.attributes-value (mth/precision (:x shape) 2) "px"]
[:& copy-button {:data (copy-data shape :x)}]])
[:div.attributes-label (t locale "handoff.attributes.layout.width")]
[:div.attributes-value (mth/precision width 2) "px"]
[:& copy-button {:data (copy-data selrect :width)}]]
(when (not= (:y shape) 0)
[:div.attributes-unit-row
[:div.attributes-label (t locale "handoff.attributes.layout.top")]
[:div.attributes-value (mth/precision (:y shape) 2) "px"]
[:& copy-button {:data (copy-data shape :y)}]])
[:div.attributes-label (t locale "handoff.attributes.layout.height")]
[:div.attributes-value (mth/precision height 2) "px"]
[:& copy-button {:data (copy-data selrect :height)}]]
(when (and (:rx shape) (not= (:rx shape) 0))
[:div.attributes-unit-row
[:div.attributes-label (t locale "handoff.attributes.layout.radius")]
[:div.attributes-value (mth/precision (:rx shape) 2) "px"]
[:& copy-button {:data (copy-data shape :rx)}]])
(when (not= (:x shape) 0)
[:div.attributes-unit-row
[:div.attributes-label (t locale "handoff.attributes.layout.left")]
[:div.attributes-value (mth/precision x 2) "px"]
[:& copy-button {:data (copy-data selrect :x)}]])
(when (and (:r1 shape)
(or (not= (:r1 shape) 0)
(not= (:r2 shape) 0)
(not= (:r3 shape) 0)
(not= (:r4 shape) 0)))
[:div.attributes-unit-row
[:div.attributes-label (t locale "handoff.attributes.layout.radius")]
[:div.attributes-value (mth/precision (:r1 shape) 2) ", "
(mth/precision (:r2 shape) 2) ", "
(mth/precision (:r3 shape) 2) ", "
(mth/precision (:r4 shape) 2) "px"]
[:& copy-button {:data (copy-data shape :r1)}]])
(when (not= (:y shape) 0)
[:div.attributes-unit-row
[:div.attributes-label (t locale "handoff.attributes.layout.top")]
[:div.attributes-value (mth/precision y 2) "px"]
[:& copy-button {:data (copy-data selrect :y)}]])
(when (not= (:rotation shape 0) 0)
[:div.attributes-unit-row
[:div.attributes-label (t locale "handoff.attributes.layout.rotation")]
[:div.attributes-value (mth/precision (:rotation shape) 2) "deg"]
[:& copy-button {:data (copy-data shape :rotation)}]])])
(when (and (:rx shape) (not= (:rx shape) 0))
[:div.attributes-unit-row
[:div.attributes-label (t locale "handoff.attributes.layout.radius")]
[:div.attributes-value (mth/precision (:rx shape) 2) "px"]
[:& copy-button {:data (copy-data shape :rx)}]])
(when (and (:r1 shape)
(or (not= (:r1 shape) 0)
(not= (:r2 shape) 0)
(not= (:r3 shape) 0)
(not= (:r4 shape) 0)))
[:div.attributes-unit-row
[:div.attributes-label (t locale "handoff.attributes.layout.radius")]
[:div.attributes-value (mth/precision (:r1 shape) 2) ", "
(mth/precision (:r2 shape) 2) ", "
(mth/precision (:r3 shape) 2) ", "
(mth/precision (:r4 shape) 2) "px"]
[:& copy-button {:data (copy-data shape :r1)}]])
(when (not= (:rotation shape 0) 0)
[:div.attributes-unit-row
[:div.attributes-label (t locale "handoff.attributes.layout.rotation")]
[:div.attributes-value (mth/precision (:rotation shape) 2) "deg"]
[:& copy-button {:data (copy-data shape :rotation)}]])]))
(mf/defc layout-panel

View file

@ -11,6 +11,7 @@
(:require
[rumext.alpha :as mf]
[cuerdas.core :as str]
[app.common.data :as d]
[app.util.i18n :refer [t]]
[app.util.code-gen :as cg]
[app.main.ui.icons :as i]
@ -40,7 +41,7 @@
copy-data (shadow-copy-data shadow)]
[:div.attributes-shadow-block
[:div.attributes-shadow-row
[:div.attributes-label (->> shadow :style name (str "handoff.attributes.shadow.style.") (t locale))]
[:div.attributes-label (->> shadow :style d/name (str "handoff.attributes.shadow.style.") (t locale))]
[:div.attributes-shadow
[:div.attributes-label (t locale "handoff.attributes.shadow.shorthand.offset-x")]
[:div.attributes-value (str (:offset-x shadow))]]

View file

@ -11,6 +11,8 @@
(:require
[rumext.alpha :as mf]
[cuerdas.core :as str]
[app.common.data :as d]
[app.common.math :as mth]
[app.util.i18n :refer [t]]
[app.util.color :as uc]
[app.main.ui.icons :as i]
@ -27,13 +29,15 @@
(defn format-stroke [shape]
(let [width (:stroke-width shape)
style (name (:stroke-style shape))
style (d/name (:stroke-style shape))
style (if (= style "svg") "solid" style)
color (-> shape shape->color uc/color->background)]
(str/format "%spx %s %s" width style color)))
(defn has-stroke? [shape]
(and (:stroke-style shape)
(not= (:stroke-style shape) :none)))
(defn has-stroke? [{:keys [stroke-style]}]
(and stroke-style
(and (not= stroke-style :none)
(not= stroke-style :svg))))
(defn copy-stroke-data [shape]
(cg/generate-css-props
@ -59,12 +63,15 @@
:copy-data (copy-color-data shape)
:on-change-format #(reset! color-format %)}]
[:div.attributes-stroke-row
[:div.attributes-label (t locale "handoff.attributes.stroke.width")]
[:div.attributes-value (:stroke-width shape) "px"]
[:div.attributes-value (->> shape :stroke-style name (str "handoff.attributes.stroke.style.") (t locale))]
[:div.attributes-label (->> shape :stroke-alignment name (str "handoff.attributes.stroke.alignment.") (t locale))]
[:& copy-button {:data (copy-stroke-data shape)}]]]))
(let [{:keys [stroke-style stroke-alignment]} shape
stroke-style (if (= stroke-style :svg) :solid stroke-style)
stroke-alignment (or stroke-alignment :center)]
[:div.attributes-stroke-row
[:div.attributes-label (t locale "handoff.attributes.stroke.width")]
[:div.attributes-value (mth/precision (:stroke-width shape) 2) "px"]
[:div.attributes-value (->> stroke-style d/name (str "handoff.attributes.stroke.style.") (t locale))]
[:div.attributes-label (->> stroke-alignment d/name (str "handoff.attributes.stroke.alignment.") (t locale))]
[:& copy-button {:data (copy-stroke-data shape)}]])]))
(mf/defc stroke-panel
[{:keys [shapes locale]}]

View file

@ -0,0 +1,57 @@
;; 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.handoff.attributes.svg
(:require
[rumext.alpha :as mf]
[app.common.data :as d]
[cuerdas.core :as str]
[app.util.i18n :refer [tr]]
#_[app.common.math :as mth]
#_[app.main.ui.icons :as i]
#_[app.util.code-gen :as cg]
[app.main.ui.components.copy-button :refer [copy-button]]))
(defn map->css [attr]
(->> attr
(map (fn [[attr-key attr-value]] (str (d/name attr-key) ":" attr-value)))
(str/join "; ")))
(mf/defc svg-attr [{:keys [attr value]}]
(if (map? value)
[:*
[:div.attributes-block-title
[:div.attributes-block-title-text (d/name attr)]
[:& copy-button {:data (map->css value)}]]
(for [[attr-key attr-value] value]
[:& svg-attr {:attr attr-key :value attr-value}])]
[:div.attributes-unit-row
[:div.attributes-label (d/name attr)]
[:div.attributes-value (str value)]
[:& copy-button {:data (d/name value)}]]))
(mf/defc svg-block
[{:keys [shape]}]
[:*
(for [[attr-key attr-value] (:svg-attrs shape)]
[:& svg-attr {:attr attr-key :value attr-value}])] )
(mf/defc svg-panel
[{:keys [shapes]}]
(let [shape (first shapes)]
(when (and (:svg-attrs shape) (not (empty? (:svg-attrs shape))))
[:div.attributes-block
[:div.attributes-block-title
[:div.attributes-block-title-text (tr "workspace.sidebar.options.svg-attrs.title")]]
[:& svg-block {:shape shape}]])))

View file

@ -114,7 +114,7 @@
[:input.input-text {:on-change (partial on-suffix-change index)
:value (:suffix export)}]
[:select.input-select {:on-change (partial on-type-change index)
:value (name (:type export))}
:value (d/name (:type export))}
[:option {:value "png"} "PNG"]
[:option {:value "jpeg"} "JPEG"]
[:option {:value "svg"} "SVG"]]

View file

@ -12,6 +12,7 @@
[rumext.alpha :as mf]
[okulary.core :as l]
[app.util.i18n :refer [t] :as i18n]
[app.common.data :as d]
[app.main.store :as st]
[app.main.ui.icons :as i]
[app.main.ui.components.tab-container :refer [tab-container tab-element]]
@ -49,7 +50,7 @@
[:*
[:span.tool-window-bar-icon
[:& element-icon {:shape (-> shapes first)}]]
[:span.tool-window-bar-title (->> selected-type name (str "handoff.tabs.code.selected.") (t locale))]])
[:span.tool-window-bar-title (->> selected-type d/name (str "handoff.tabs.code.selected.") (t locale))]])
]
[:div.tool-window-content
[:& tab-container {:on-change-tab #(do

View file

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

View file

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

View file

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

View file

@ -0,0 +1,107 @@
;; 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 (contains? usvg/gradient-tags tag)
(= "userSpaceOnUse" (get attrs :gradientUnits "objectBoundingBox")))
transform-pattern? (and (= :pattern tag)
(= "userSpaceOnUse" (get attrs :patternUnits "userSpaceOnUse")))
transform-clippath? (and (= :clipPath tag)
(= "userSpaceOnUse" (get attrs :clipPathUnits "userSpaceOnUse")))
transform-filter? (and (contains? usvg/filter-tags tag)
(= "userSpaceOnUse" (get attrs :filterUnits "objectBoundingBox")))
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-clippath? (add-matrix :transform 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)))
;; Paths doesn't have transform so we have to transform its gradients
transform (if (contains? shape :svg-transform)
(gmt/multiply transform (or (:svg-transform shape) (gmt/matrix)))
transform)
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

@ -13,7 +13,6 @@
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.common.uuid :as uuid]
[app.main.ui.shapes.attrs :as usa]
[app.util.data :as ud]
[app.util.object :as obj]
@ -21,35 +20,15 @@
[cuerdas.core :as str]
[rumext.alpha :as mf]))
;; Graphic tags
(defonce graphic-element? #{:circle :ellipse :image :line :path :polygon :polyline :rect :text :use})
;; Context to store a re-mapping of the ids
(def svg-ids-ctx (mf/create-context nil))
(defn generate-id-mapping [content]
(letfn [(visit-node [result node]
(let [element-id (get-in node [:attrs :id])
result (cond-> result
element-id (assoc element-id (str (uuid/next))))]
(reduce visit-node result (:content node))))]
(visit-node {} content)))
(defonce replace-regex #"[^#]*#([^)\s]+).*")
(defn replace-attrs-ids
"Replaces the ids inside a property"
[ids-mapping attrs]
(letfn [(replace-ids [key val]
(if (map? val)
(cd/mapm replace-ids val)
(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)))
(defn set-styles [attrs shape]
(let [custom-attrs (usa/extract-style-attrs shape)
(let [custom-attrs (-> (usa/extract-style-attrs shape)
(obj/without ["transform"]))
attrs (cond-> attrs
(string? (:style attrs)) usvg/clean-attrs)
style (obj/merge! (clj->js (:style attrs {}))
@ -58,6 +37,14 @@
(obj/merge! custom-attrs)
(obj/set! "style" style))))
(defn translate-shape [attrs shape]
(let [transform (str (usvg/svg-transform-matrix shape)
" "
(:transform attrs ""))]
(cond-> attrs
(and (:svg-viewbox shape) (graphic-element? (-> shape :content :tag)))
(assoc :transform transform))))
(mf/defc svg-root
{::mf/wrap-props false}
[props]
@ -68,7 +55,7 @@
{:keys [x y width height]} shape
{:keys [tag attrs] :as content} (:content shape)
ids-mapping (mf/use-memo #(generate-id-mapping content))
ids-mapping (mf/use-memo #(usvg/generate-id-mapping content))
attrs (-> (set-styles attrs shape)
(obj/set! "x" x)
@ -91,11 +78,16 @@
{:keys [attrs tag]} content
ids-mapping (mf/use-ctx svg-ids-ctx)
attrs (mf/use-memo #(replace-attrs-ids ids-mapping attrs))
element-id (get-in content [:attrs :id])
attrs (mf/use-memo #(usvg/replace-attrs-ids attrs ids-mapping))
attrs (translate-shape attrs shape)
element-id (get-in content [:attrs :id])
attrs (cond-> (set-styles attrs shape)
element-id (obj/set! "id" (get ids-mapping element-id)))]
(and element-id (contains? ids-mapping element-id))
(obj/set! "id" (get ids-mapping element-id)))
{:keys [x y width height]} (:selrect shape)]
[:> (name tag) attrs children]))
(defn svg-raw-shape [shape-wrapper]

View file

@ -44,8 +44,6 @@
:style {:stroke color
:fill "transparent"
:stroke-width "1px"
:stroke-opacity 0.5
:stroke-dasharray 4
:pointer-events "none"}}])
(mf/defc render-rect-points [{:keys [points color]}]
@ -62,7 +60,7 @@
[props]
(let [shape (-> (unchecked-get props "shape"))
frame (unchecked-get props "frame")
selrect (gsh/points->selrect (-> shape :points))
bounding-box (gsh/points->selrect (-> shape :points))
shape-center (gsh/center-shape shape)
line-color (rdcolor #js {:seed (str (:id shape))})
zoom (mf/deref refs/selected-zoom)
@ -71,25 +69,30 @@
(map gsh/transform-shape))]
[:g.bounding-box
[:text {:x (:x selrect)
:y (- (:y selrect) 5)
[:text {:x (:x bounding-box)
:y (- (:y bounding-box) 5)
:font-size 10
:fill line-color
:stroke "white"
:stroke-width 0.1}
(str/format "%s - (%s, %s)" (str/slice (str (:id shape)) 0 8) (fixed (:x selrect)) (fixed (:y selrect)))]
(str/format "%s - (%s, %s)" (str/slice (str (:id shape)) 0 8) (fixed (:x bounding-box)) (fixed (:y bounding-box)))]
[:& cross-point {:point shape-center
:zoom zoom
:color line-color}]
[:g.center
[:& cross-point {:point shape-center
:zoom zoom
:color line-color}]]
[:g.points
(for [point (:points shape)]
[:& cross-point {:point point
:zoom zoom
:color line-color}])
#_[:& render-rect-points {:points (:points shape)
:color line-color}]]
(for [point (:points shape)]
[:& cross-point {:point point
:zoom zoom
:color line-color}])
[:& render-rect-points {:points (:points shape)
:color line-color}]
[:& render-rect {:rect selrect
:color line-color}]]))
[:g.selrect
[:& render-rect {:rect (:selrect shape)
;; :transform (gsh/transform-matrix shape)
:color line-color}]
#_[:& render-rect {:rect bounding-box
:color line-color}]]]))

View file

@ -33,7 +33,6 @@
{::mf/wrap-props false}
[props]
(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 (mf/deref content-modifiers-ref)
editing-id (mf/deref refs/selected-edition)

View file

@ -19,7 +19,7 @@
;; This is a list of svg tags that can be grouped in shape-container
;; 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 :circle :ellipse :image :line :path :polygon :polyline :rect :symbol :text :textPath :use})
(defn svg-raw-wrapper-factory
[shape-wrapper]
@ -57,19 +57,19 @@
:shape shape
:childs childs}]
(when (= tag :svg)
[:rect.group-actions
{:x x
:y y
:transform transform
:width width
:height height
:fill "transparent"
:on-mouse-down handle-mouse-down
:on-double-click handle-double-click
:on-context-menu handle-context-menu
:on-pointer-over handle-pointer-enter
:on-pointer-out handle-pointer-leave}])]
[:rect.actions
{:x x
:y y
:transform transform
:width width
:height height
:fill "transparent"
:stroke "none"
:on-mouse-down handle-mouse-down
:on-double-click handle-double-click
:on-context-menu handle-context-menu
:on-pointer-over handle-pointer-enter
:on-pointer-out handle-pointer-leave}]]
;; We cannot wrap inside groups the shapes that go inside the defs tag
;; we use the context so we know when we should not render the container

View file

@ -14,7 +14,8 @@
[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.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
[{:keys [shape] :as props}]
@ -36,4 +37,6 @@
[:& shadow-menu {:ids ids
:values (select-keys shape [:shadow])}]
[:& 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

@ -19,7 +19,8 @@
[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.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/wrap [mf/memo]
@ -36,6 +37,7 @@
[blur-ids blur-values] (get-attrs [shape] objects :blur)
[stroke-ids stroke-values] (get-attrs [shape] objects :stroke)
[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)]]
[:div.options
@ -55,6 +57,10 @@
[:& stroke-menu {:type type :ids stroke-ids :values stroke-values}])
(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.stroke :refer [stroke-attrs stroke-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
[{:keys [shape] :as props}]
@ -36,4 +37,6 @@
[:& shadow-menu {:ids ids
:values (select-keys shape [:shadow])}]
[:& 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

@ -14,7 +14,8 @@
[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.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/wrap [mf/memo]}
@ -41,4 +42,7 @@
:values (select-keys shape [:shadow])}]
[:& 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
:on-change on-change}]
:text
[:input {:value value
:class "input-text"
:on-change on-change} ]
[:> numeric-input {:placeholder placeholder
:min min
:max max

View file

@ -0,0 +1,96 @@
;; 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
[cuerdas.core :as str]
[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]
[app.main.ui.icons :as i]))
(mf/defc attribute-value [{:keys [attr value on-change on-delete] :as props}]
(let [handle-change
(mf/use-callback
(mf/deps attr on-change)
(fn [event]
(on-change attr (dom/get-target-val event))))
handle-delete
(mf/use-callback
(mf/deps attr on-delete)
(fn []
(on-delete attr)))
label (->> attr last d/name)]
[:div.element-set-content
(if (string? value)
[:div.row-flex.row-flex-removable
[:& input-row {:label label
:type :text
:class "large"
:value (str value)
:on-change handle-change}]
[:div.element-set-actions
[:div.element-set-actions-button {:on-click handle-delete}
i/minus]]]
[:*
[:div.element-set-title
{:style {:border-bottom "1px solid #444" :margin-bottom "0.5rem"}}
[:span (str (d/name (last attr)))]]
(for [[key value] value]
[:& attribute-value {:key key
:attr (conj attr key)
:value value
:on-change on-change
:on-delete on-delete}])])]))
(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 (concat [:svg-attrs] attr) value))]
(st/emit! (dwc/update-shapes ids update-fn)))))
handle-delete
(mf/use-callback
(mf/deps ids)
(fn [attr]
(let [update-fn
(fn [shape]
(let [update-path (concat [:svg-attrs] (butlast attr))
shape (update-in shape update-path dissoc (last attr))
shape (cond-> shape
(empty? (get-in shape [:svg-attrs :style]))
(update :svg-attrs dissoc :style))]
shape))]
(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
:attr [attr-key]
:value attr-value
:on-change handle-change
:on-delete handle-delete}])])))

View file

@ -17,11 +17,12 @@
[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.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 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 shorthex->longhex [hex]
@ -99,11 +100,9 @@
(when (contains? svg-elements tag)
[:*
(when (= tag :svg)
[:*
[:& measures-menu {:ids ids
:type type
:values measure-values}]])
[:& measures-menu {:ids ids
:type type
:values measure-values}]
[:& fill-menu {:ids ids
:type type
@ -115,4 +114,7 @@
:values (select-keys shape [:shadow])}]
[:& 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)
(= 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]
(let [result (gcolor/parse color-str)]
(str (.-hex ^js result))))

View file

@ -9,12 +9,22 @@
(ns app.util.geom.path
(:require
[cuerdas.core :as str]
[app.common.data :as cd]
[app.util.data :as d]
[app.common.data :as cd]
[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]
[app.util.svg :as usvg]
[cuerdas.core :as str]))
(defn calculate-opposite-handler
"Given a point and its handler, gives the symetric handler"
[point handler]
(let [handler-vector (gpt/to-vec point handler)]
(gpt/add point (gpt/negate handler-vector))))
;;;
(defn simplify
([points]
@ -28,23 +38,37 @@
;; Matches numbers for path values allows values like... -.01, 10, +12.22
;; 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]
(re-pattern (str "(?i)[a-z]\\s*"
(->> (range size)
(map #(identity num-regex))
(str/join "\\s+")))))
(defn extract-params [cmd-str extract-commands]
(loop [result []
extract-idx 0
current {}
remain (-> cmd-str (subs 1) (str/trim))]
(let [[param type] (nth extract-commands extract-idx)
regex (case type
:flag flag-regex
#_:number num-regex)
match (re-find regex remain)]
(defn parse-params [cmd-str num-params]
(let [fix-starting-dot (fn [arg] (str/replace arg #"([^\d]|^)\." "$10."))]
(->> (re-seq num-regex cmd-str)
(map first)
(map fix-starting-dot)
(map d/read-string)
(partition num-params))))
(if match
(let [value (-> match first usvg/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]}]
(case command
@ -73,96 +97,99 @@
(defmethod parse-command "M" [cmd]
(let [relative (str/starts-with? cmd "m")
params (parse-params cmd 2)]
(for [[x y] params]
param-list (extract-params cmd [[:x :number]
[:y :number]])]
(for [params param-list]
{:command :move-to
:relative relative
:params {:x x :y y}})))
:params params})))
(defmethod parse-command "Z" [cmd]
[{:command :close-path}])
(defmethod parse-command "L" [cmd]
(let [relative (str/starts-with? cmd "l")
params (parse-params cmd 2)]
(for [[x y] params]
param-list (extract-params cmd [[:x :number]
[:y :number]])]
(for [params param-list]
{:command :line-to
:relative relative
:params {:x x :y y}})))
:params params})))
(defmethod parse-command "H" [cmd]
(let [relative (str/starts-with? cmd "h")
params (parse-params cmd 1)]
(for [[value] params]
param-list (extract-params cmd [[:value :number]])]
(for [params param-list]
{:command :line-to-horizontal
:relative relative
:params {:value value}})))
:params params})))
(defmethod parse-command "V" [cmd]
(let [relative (str/starts-with? cmd "v")
params (parse-params cmd 1)]
(for [[value] params]
param-list (extract-params cmd [[:value :number]])]
(for [params param-list]
{:command :line-to-vertical
:relative relative
:params {:value value}})))
:params params})))
(defmethod parse-command "C" [cmd]
(let [relative (str/starts-with? cmd "c")
params (parse-params cmd 6)]
(for [[c1x c1y c2x c2y x y] params]
param-list (extract-params cmd [[:c1x :number]
[:c1y :number]
[:c2x :number]
[:c2y :number]
[:x :number]
[:y :number]])
]
(for [params param-list]
{:command :curve-to
:relative relative
:params {:c1x c1x
:c1y c1y
:c2x c2x
:c2y c2y
:x x
:y y}})))
:params params})))
(defmethod parse-command "S" [cmd]
(let [relative (str/starts-with? cmd "s")
params (parse-params cmd 4)]
(for [[cx cy x y] params]
param-list (extract-params cmd [[:cx :number]
[:cy :number]
[:x :number]
[:y :number]])]
(for [params param-list]
{:command :smooth-curve-to
:relative relative
:params {:cx cx
:cy cy
:x x
:y y}})))
:params params})))
(defmethod parse-command "Q" [cmd]
(let [relative (str/starts-with? cmd "s")
params (parse-params cmd 4)]
(for [[cx cy x y] params]
param-list (extract-params cmd [[:cx :number]
[:cy :number]
[:x :number]
[:y :number]])]
(for [params param-list]
{:command :quadratic-bezier-curve-to
:relative relative
:params {:cx cx
:cy cy
:x x
:y y}})))
:params params})))
(defmethod parse-command "T" [cmd]
(let [relative (str/starts-with? cmd "t")
params (parse-params cmd (coord-n 2))]
(for [[cx cy x y] params]
param-list (extract-params cmd [[:x :number]
[:y :number]])]
(for [params param-list]
{:command :smooth-quadratic-bezier-curve-to
:relative relative
:params {:x x
:y y}})))
:params params})))
(defmethod parse-command "A" [cmd]
(let [relative (str/starts-with? cmd "a")
params (parse-params cmd 7)]
(for [[rx ry x-axis-rotation large-arc-flag sweep-flag x y] params]
param-list (extract-params cmd [[:rx :number]
[:ry :number]
[:x-axis-rotation :number]
[:large-arc-flag :flag]
[:sweep-flag :flag]
[:x :number]
[:y :number]])]
(for [params param-list]
{:command :elliptical-arc
:relative relative
:params {:rx rx
:ry ry
:x-axis-rotation x-axis-rotation
:large-arc-flag large-arc-flag
:sweep-flag sweep-flag
:x x
:y y}})))
:params params})))
(defn command->string [{:keys [command relative params] :as entry}]
(let [command-str (case command
@ -180,46 +207,143 @@
param-list (command->param-list entry)]
(str/fmt "%s%s" command-str (str/join " " param-list))))
(defn cmd-pos [{:keys [params]}]
(when (and (contains? params :x)
(contains? params :y))
(gpt/point params)))
(defn cmd-pos [prev-pos {:keys [relative params]}]
(let [{:keys [x y] :or {x (:x prev-pos) y (:y prev-pos)}} params]
(if relative
(-> prev-pos (update :x + x) (update :y + y))
(gpt/point x y))))
(defn arc->beziers [from-p 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} from-p
{: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 smooth->curve
[{:keys [params]} pos handler]
(let [{c1x :x c1y :y} (calculate-opposite-handler pos handler)]
{:c1x c1x
:c1y c1y
:c2x (:cx params)
:c2y (:cy params)}))
(defn quadratic->curve
[sp ep cp]
(let [cp1 (-> (gpt/to-vec sp cp)
(gpt/scale (/ 2 3))
(gpt/add sp))
cp2 (-> (gpt/to-vec ep cp)
(gpt/scale (/ 2 3))
(gpt/add ep))]
{:c1x (:x cp1)
:c1y (:y cp1)
:c2x (:x cp2)
:c2y (:y cp2)}))
(defn simplify-commands
"Removes some commands and convert relative to absolute coordinates"
[commands]
(let [simplify-command
(fn [[pos result] [command prev]]
;; prev-cc : previous command control point for cubic beziers
;; prev-qc : previous command control point for quadratic curves
(fn [[pos result prev-cc prev-qc] [command prev]]
(let [command
(cond-> command
(:relative command)
(-> (assoc :relative false)
(cd/update-in-when [:params :c1x] + (:x pos))
(cd/update-in-when [:params :c1y] + (:y pos))
(cd/update-in-when [:params :c2x] + (:x pos))
(cd/update-in-when [:params :c2y] + (:y pos))
(cd/update-in-when [:params :cx] + (:x pos))
(cd/update-in-when [:params :cy] + (:y pos))
(cd/update-in-when [:params :x] + (:x pos))
(cd/update-in-when [:params :y] + (:y pos))
(cond->
(= :line-to-horizontal (:command command))
(cd/update-in-when [:params :value] + (:x pos))
(= :line-to-vertical (:command command))
(cd/update-in-when [:params :value] + (:y pos)))))
params (:params command)
orig-command command
command
(cond-> command
(= :line-to-horizontal (:command command))
(-> (assoc :command :line-to)
(update :params dissoc :value)
(assoc-in [:params :x] (get-in command [:params :value]))
(assoc-in [:params :y] (if (:relative command) 0 (:y pos))))
(assoc-in [:params :x] (:value params))
(assoc-in [:params :y] (:y pos)))
(= :line-to-vertical (:command command))
(-> (assoc :command :line-to)
(update :params dissoc :value)
(assoc-in [:params :y] (get-in command [:params :value]))
(assoc-in [:params :x] (if (:relative command) 0 (:x pos))))
(assoc-in [:params :y] (:value params))
(assoc-in [:params :x] (:x pos)))
(:relative command)
(-> (assoc :relative false)
(cd/update-in-when [:params :x] + (:x pos))
(cd/update-in-when [:params :y] + (:y pos)))
(= :smooth-curve-to (:command command))
(-> (assoc :command :curve-to)
(update :params dissoc :cx :cy)
(update :params merge (smooth->curve command pos prev-cc)))
(= :quadratic-bezier-curve-to (:command command))
(-> (assoc :command :curve-to)
(update :params dissoc :cx :cy)
(update :params merge (quadratic->curve pos (gpt/point params) (gpt/point (:cx params) (:cy params)))))
)]
[(cmd-pos command) (conj result command)]))
(= :smooth-quadratic-bezier-curve-to (:command command))
(-> (assoc :command :curve-to)
(update :params merge (quadratic->curve pos (gpt/point params) (calculate-opposite-handler pos prev-qc)))))
result (if (= :elliptical-arc (:command command))
(cd/concat result (arc->beziers pos command))
(conj result command))
prev-cc (case (:command orig-command)
:smooth-curve-to
(gpt/point (get-in orig-command [:params :cx]) (get-in orig-command [:params :cy]))
:curve-to
(gpt/point (get-in orig-command [:params :c2x]) (get-in orig-command [:params :c2y]))
(:line-to-horizontal :line-to-vertical)
(gpt/point (get-in command [:params :x]) (get-in command [:params :y]))
(gpt/point (get-in orig-command [:params :x]) (get-in orig-command [:params :y])))
prev-qc (case (:command orig-command)
:quadratic-bezier-curve-to
(gpt/point (get-in orig-command [:params :cx]) (get-in orig-command [:params :cy]))
:smooth-quadratic-bezier-curve-to
(calculate-opposite-handler pos prev-qc)
(gpt/point (get-in orig-command [:params :x]) (get-in orig-command [:params :y])))]
[(cmd-pos pos command) result prev-cc prev-qc]))
start (first commands)
start-pos (cmd-pos start)]
start-pos (gpt/point (:params start))]
(->> (map vector (rest commands) commands)
(reduce simplify-command [start-pos [start]])
(reduce simplify-command [start-pos [start] start-pos start-pos])
(second))))
(defn path->content [string]

View file

@ -9,13 +9,61 @@
(ns app.util.svg
(:require
[app.common.uuid :as uuid]
[app.common.data :as d]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.common.math :as mth]
[cuerdas.core :as str]))
;; Regex for XML ids per Spec
;; https://www.w3.org/TR/2008/REC-xml-20081126/#sec-common-syn
(defonce xml-id-regex #"#([:A-Z_a-z\xC0-\xD6\xD8-\xF6\xF8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\u10000-\uEFFFF][\.\-\:0-9\xB7A-Z_a-z\xC0-\xD6\xD8-\xF6\xF8-\u02FF\u0300-\u036F\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\u10000-\uEFFFF]*)")
(defonce matrices-regex #"(matrix|translate|scale|rotate|skewX|skewY)\(([^\)]*)\)")
(defonce number-regex #"[+-]?\d*(\.\d+)?(e[+-]?\d+)?")
(defn extract-ids [val]
(->> (re-seq xml-id-regex val)
(mapv second)))
(defn fix-dot-number
"Fixes decimal numbers starting in dot but without leading 0"
[num-str]
(cond
(str/starts-with? num-str ".")
(str "0" num-str)
(str/starts-with? num-str "-.")
(str "-0" (subs num-str 1))
:else
num-str))
(defn format-styles
"Transforms attributes to their react equivalent"
[attrs]
(letfn [(format-styles [style-str]
(if (string? style-str)
(->> (str/split style-str ";")
(map str/trim)
(map #(str/split % ":"))
(group-by first)
(map (fn [[key val]]
(vector (keyword key) (second (first val)))))
(into {}))
style-str))]
(cond-> attrs
(contains? attrs :style)
(update :style format-styles))))
(defn clean-attrs
"Transforms attributes to their react equivalent"
[attrs]
(letfn [(transform-key [key]
(-> (name key)
(-> (d/name key)
(str/replace ":" "-")
(str/camel)
(keyword)))
@ -32,11 +80,468 @@
(into {})))
(map-fn [[key val]]
(cond
(= key :class) [:className val]
(and (= key :style) (string? val)) [key (format-styles val)]
:else (vector (transform-key key) val)))]
(let [key (keyword key)]
(cond
(= key :class) [:className val]
(and (= key :style) (string? val)) [key (format-styles val)]
(and (= key :style) (map? val)) [key (clean-attrs val)]
:else (vector (transform-key key) val))))]
(->> attrs
(map map-fn)
(into {}))))
(defn update-attr-ids
"Replaces the ids inside a property"
[attrs replace-fn]
(letfn [(update-ids [key val]
(cond
(map? val)
(d/mapm update-ids val)
(= key :id)
(replace-fn val)
:else
(let [replace-id
(fn [result it]
(let [to-replace (replace-fn it)]
(str/replace result (str "#" it) (str "#" to-replace))))]
(reduce replace-id val (extract-ids val)))))]
(d/mapm update-ids attrs)))
(defn replace-attrs-ids
"Replaces the ids inside a property"
[attrs ids-mapping]
(if (and ids-mapping (not (empty? ids-mapping)))
(update-attr-ids attrs (fn [id] (get ids-mapping id id)))
;; Ids-mapping is null
attrs))
(defn generate-id-mapping [content]
(letfn [(visit-node [result node]
(let [element-id (get-in node [:attrs :id])
result (cond-> result
element-id (assoc element-id (str (uuid/next))))]
(reduce visit-node result (:content node))))]
(visit-node {} content)))
(def remove-tags #{:defs :linearGradient})
(defn extract-defs [{:keys [tag attrs content] :as node}]
(if-not (map? node)
[{} node]
(let [remove-node? (fn [{:keys [tag]}] (contains? remove-tags tag))
rec-result (->> (:content node) (map extract-defs))
node (assoc node :content (->> rec-result (map second) (filterv (comp not remove-node?))))
current-node-defs (if (contains? attrs :id)
(hash-map (:id attrs) node)
(hash-map))
node-defs (->> rec-result (map first) (reduce merge current-node-defs))]
[ node-defs node ])))
(defn find-attr-references [attrs]
(->> attrs
(mapcat (fn [[_ attr-value]]
(if (string? attr-value)
(extract-ids attr-value)
(find-attr-references attr-value))))))
(defn find-node-references [node]
(let [current (->> (find-attr-references (:attrs node)) (into #{}))
children (->> (:content node) (map find-node-references) (flatten) (into #{}))]
(-> (d/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)
pending (concat pending new-refs)]
(recur (d/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)
;; Paths doesn't have transform so we have to transform its gradients
(if (= :path (:type shape))
(gsh/transform-matrix shape)
(gmt/matrix))
(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)))
;; Parse transform attributes to native matrix format so we can transform paths instead of
;; relying in SVG transformation. This is necessary to import SVG's and not to break path tooling
;;
;; Transforms spec:
;; https://www.w3.org/TR/SVG11/single-page.html#coords-TransformAttribute
(defn format-translate-params [params]
(assert (or (= (count params) 1) (= (count params) 2)))
(if (= (count params) 1)
[(gpt/point (nth params 0) 0)]
[(gpt/point (nth params 0) (nth params 1))]))
(defn format-scale-params [params]
(assert (or (= (count params) 1) (= (count params) 2)))
(if (= (count params) 1)
[(gpt/point (nth params 0))]
[(gpt/point (nth params 0) (nth params 1))]))
(defn format-rotate-params [params]
(assert (or (= (count params) 1) (= (count params) 3)) (str "??" (count params)))
(if (= (count params) 1)
[(nth params 0) (gpt/point 0 0)]
[(nth params 0) (gpt/point (nth params 1) (nth params 2))]))
(defn format-skew-x-params [params]
(assert (= (count params) 1))
[(nth params 0) 0])
(defn format-skew-y-params [params]
(assert (= (count params) 1))
[0 (nth params 0)])
(defn to-matrix [{:keys [type params]}]
(assert (#{"matrix" "translate" "scale" "rotate" "skewX" "skewY"} type))
(case type
"matrix" (apply gmt/matrix params)
"translate" (apply gmt/translate-matrix (format-translate-params params))
"scale" (apply gmt/scale-matrix (format-scale-params params))
"rotate" (apply gmt/rotate-matrix (format-rotate-params params))
"skewX" (apply gmt/skew-matrix (format-skew-x-params params))
"skewY" (apply gmt/skew-matrix (format-skew-y-params params))))
(defn parse-transform [transform-attr]
(if transform-attr
(let [process-matrix
(fn [[_ type params]]
(let [params (->> (re-seq number-regex params)
(filter #(-> % first empty? not))
(map (comp d/parse-double first)))]
{:type type :params params}))
matrices (->> (re-seq matrices-regex transform-attr)
(map process-matrix)
(map to-matrix))]
(reduce gmt/multiply (gmt/matrix) matrices))
(gmt/matrix)))
(defn format-move [[x y]] (str "M" x " " y))
(defn format-line [[x y]] (str "L" x " " y))
(defn points->path [points-str]
(let [points (->> points-str
(re-seq number-regex)
(filter (comp not empty? first))
(mapv (comp d/parse-double first))
(partition 2))
head (first points)
other (rest points)]
(str (format-move head)
(->> other (map format-line) (str/join " ")))))
(defn polyline->path [{:keys [attrs tag] :as node}]
(let [tag :path
attrs (-> attrs
(dissoc :points)
(assoc :d (points->path (:points attrs))))]
(assoc node :attrs attrs :tag tag)))
(defn polygon->path [{:keys [attrs tag] :as node}]
(let [tag :path
attrs (-> attrs
(dissoc :points)
(assoc :d (str (points->path (:points attrs)) "Z")))]
(assoc node :attrs attrs :tag tag)))
(defn line->path [{:keys [attrs tag] :as node}]
(let [tag :path
{:keys [x1 y1 x2 y2]} attrs
attrs (-> attrs
(dissoc :x1 :x2 :y1 :y2)
(assoc :d (str "M" x1 "," y1 " L" x2 "," y2)))]
(assoc node :attrs attrs :tag tag)))
(defn add-transform [attrs transform]
(letfn [(append-transform [old-transform]
(if (or (nil? old-transform) (empty? old-transform))
transform
(str transform " " old-transform)))]
(cond-> attrs
transform
(update :transform append-transform))))
(defonce inheritable-props
[:clip-rule
:color
:color-interpolation
:color-interpolation-filters
:color-profile
:color-rendering
:cursor
:direction
:dominant-baseline
:fill
:fill-opacity
:fill-rule
:font
:font-family
:font-size
:font-size-adjust
:font-stretch
:font-style
:font-variant
:font-weight
:glyph-orientation-horizontal
:glyph-orientation-vertical
:image-rendering
:letter-spacing
:marker
:marker-end
:marker-mid
:marker-start
:paint-order
:pointer-events
:shape-rendering
:stroke
:stroke-dasharray
:stroke-dashoffset
:stroke-linecap
:stroke-linejoin
:stroke-miterlimit
:stroke-opacity
:stroke-width
:text-anchor
:text-rendering
:transform
:visibility
:word-spacing
:writing-mode])
(defonce gradient-tags
#{:linearGradient
:radialGradient})
(defonce filter-tags
#{:filter
:feBlend
:feColorMatrix
:feComponentTransfer
:feComposite
:feConvolveMatrix
:feDiffuseLighting
:feDisplacementMap
:feFlood
:feGaussianBlur
:feImage
:feMerge
:feMorphology
:feOffset
:feSpecularLighting
:feTile
:feTurbulence})
(defn inherit-attributes [group-attrs {:keys [attrs] :as node}]
(if (map? node)
(let [attrs (-> (format-styles attrs)
(add-transform (:transform group-attrs)))
attrs (d/deep-merge (select-keys group-attrs inheritable-props) attrs)]
(assoc node :attrs attrs))
node))
(defn map-nodes [mapfn node]
(let [update-content
(fn [content] (cond->> content
(vector? content)
(mapv (partial map-nodes mapfn))))]
(cond-> node
(map? node)
(-> (mapfn)
(d/update-when :content update-content)))))
(defn reduce-nodes [redfn value node]
(let [reduce-content
(fn [value content]
(loop [current (first content)
content (rest content)
value value]
(if (nil? current)
value
(recur (first content)
(rest content)
(reduce-nodes redfn value current)))))]
(if (map? node)
(-> (redfn value node)
(reduce-content (:content node)))
value)))
;; Defaults for some tags per spec https://www.w3.org/TR/SVG11/single-page.html
;; they are basicaly the defaults that can be percents and we need to replace because
;; otherwise won't work as expected in the workspace
(defonce svg-tag-defaults
(let [filter-default {:units :filterUnits
:default "objectBoundingBox"
"objectBoundingBox" {}
"userSpaceOnUse" {:x "-10%" :y "-10%" :width "120%" :height "120%"}}
filter-values (->> filter-tags
(reduce #(merge %1 (hash-map %2 filter-default)) {}))]
(merge {:linearGradient {:units :gradientUnits
:default "objectBoundingBox"
"objectBoundingBox" {}
"userSpaceOnUse" {:x1 "0%" :y1 "0%" :x2 "100%" :y2 "0%"}}
:radialGradient {:units :gradientUnits
:default "objectBoundingBox"
"objectBoundingBox" {}
"userSpaceOnUse" {:cx "50%" :cy "50%" :r "50%"}}
:mask {:units :maskUnits
:default "userSpaceOnUse"
"objectBoundingBox" {}
"userSpaceOnUse" {:x "-10%" :y "-10%" :width "120%" :height "120%"}}}
filter-values)))
(defn fix-default-values
"Gives values to some SVG elements which defaults won't work when imported into the platform"
[svg-data]
(let [add-defaults
(fn [{:keys [tag attrs] :as node}]
(let [prop (get-in svg-tag-defaults [tag :units])
default-units (get-in svg-tag-defaults [tag :default])
units (get attrs prop default-units)
tag-default (get-in svg-tag-defaults [tag units])]
(d/update-when node :attrs #(merge tag-default %))))
fix-node-defaults
(fn [node]
(cond-> node
(contains? svg-tag-defaults (:tag node))
(add-defaults)))]
(->> svg-data (map-nodes fix-node-defaults))))
(defn calculate-ratio
;; sqrt((actual-width)**2 + (actual-height)**2)/sqrt(2).
[width height]
(/ (mth/sqrt (+ (mth/pow width 2)
(mth/pow height 2)))
(mth/sqrt 2)))
(defn fix-percents
"Changes percents to a value according to the size of the svg imported"
[svg-data]
;; https://www.w3.org/TR/SVG11/single-page.html#coords-Units
(let [viewbox {:x (:offset-x svg-data)
:y (:offset-y svg-data)
:width (:width svg-data)
:height (:height svg-data)
:ratio (calculate-ratio (:width svg-data) (:height svg-data))}]
(letfn [(fix-length [prop-length val]
(* (get viewbox prop-length) (/ val 100.)))
(fix-coord [prop-coord prop-length val]
(+ (get viewbox prop-coord)
(fix-length prop-length val)))
(fix-percent-attr-viewbox [attr-key attr-val]
(let [is-percent? (str/ends-with? attr-val "%")
is-x? #{:x :x1 :x2 :cx}
is-y? #{:y :y1 :y2 :cy}
is-width? #{:width}
is-height? #{:height}
is-other? #{:r :stroke-width}]
(if is-percent?
;; JS parseFloat removes the % symbol
(let [attr-num (d/parse-double attr-val)]
(str (cond
(is-x? attr-key) (fix-coord :x :width attr-num)
(is-y? attr-key) (fix-coord :y :height attr-num)
(is-width? attr-key) (fix-length :width attr-num)
(is-height? attr-key) (fix-length :height attr-num)
(is-other? attr-key) (fix-length :ratio attr-num)
:else attr-val)))
attr-val)))
(fix-percent-attrs-viewbox [attrs]
(d/mapm fix-percent-attr-viewbox attrs))
(fix-percent-attr-numeric [attr-key attr-val]
(let [is-percent? (str/ends-with? attr-val "%")]
(if is-percent?
(str (let [attr-num (d/parse-double attr-val)]
(/ attr-num 100)))
attr-val)))
(fix-percent-attrs-numeric [attrs]
(d/mapm fix-percent-attr-numeric attrs))
(fix-percent-values [node]
(let [units (or (get-in node [:attrs :filterUnits])
(get-in node [:attrs :gradientUnits])
(get-in node [:attrs :patternUnits])
(get-in node [:attrs :clipUnits]))]
(cond-> node
(= "objectBoundingBox" units)
(update :attrs fix-percent-attrs-numeric)
(not= "objectBoundingBox" units)
(update :attrs fix-percent-attrs-viewbox))))]
(->> svg-data (map-nodes fix-percent-values)))))
(defn collect-images [svg-data]
(let [redfn (fn [acc {:keys [tag attrs]}]
(cond-> acc
(= :image tag)
(conj (:xlink:href attrs))))]
(reduce-nodes redfn [] svg-data )))

View file

@ -0,0 +1,37 @@
;; 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.util.uri
(:require
[cuerdas.core :as str]
[app.util.object :as obj]))
(defn uri-name [url]
(let [query-idx (str/last-index-of url "?")
url (if (> query-idx 0) (subs url 0 query-idx) url)
filename (->> (str/split url "/") (last))
ext-idx (str/last-index-of filename ".")]
(if (> ext-idx 0) (subs filename 0 ext-idx) filename)))
(defn data-uri->blob
[data-uri]
(let [[mtype b64-data] (str/split data-uri ";base64,")
mtype (subs mtype (inc (str/index-of mtype ":")))
decoded (.atob js/window b64-data)
size (.-length decoded)
content (js/Uint8Array. size)]
(doseq [i (range 0 size)]
(obj/set! content i (.charCodeAt decoded i)))
(js/Blob. #js [content] #js {"type" mtype})))

View file

@ -1,16 +1,41 @@
const plugins = [
{removeDimensions: true},
{removeScriptElement: true},
{removeViewBox: false},
{moveElemsAttrsToGroup: false},
{convertStyleToAttrs: false},
{convertPathData: {
lineShorthands: false,
curveSmoothShorthands: false,
forceAbsolutePath: true,
}}
{ "minifyStyles" : false },
{ "convertStyleToAttrs" : false },
{
"cleanupIDs" : {
remove: false,
minify: false,
force: false
}
},
{ "cleanupListOfValues" : true },
{ "removeUnknownsAndDefaults" : false },
{ "removeViewBox" : false },
{ "convertShapeToPath" : false },
{ "convertEllipseToCircle" : false },
{ "moveElemsAttrsToGroup" : false },
{ "moveGroupAttrsToElems" : false },
{ "collapseGroups" : false },
{
"convertPathData" : {
lineShorthands: false,
curveSmoothShorthands: false,
forceAbsolutePath: true,
}
},
{ "convertTransform" : false },
{ "removeEmptyContainers" : false },
{ "mergePaths" : false },
{ "sortDefsChildren" : false },
{ "removeDimensions" : true },
{ "removeStyleElement" : false },
{ "removeScriptElement" : true },
{ "removeOffCanvasPaths" : false },
{ "cleanupNumericValues": true}
];
const svgc = require("./src/svgclean.js");
const inst = svgc.configure({plugins});

View file

@ -8,7 +8,7 @@ exports.description = 'rounds numeric values to the fixed precision, removes def
exports.params = {
floatPrecision: 3,
leadingZero: true,
leadingZero: false,
defaultPx: true,
convertToPx: true
};
@ -20,7 +20,8 @@ var regNumericValues = /^([\-+]?\d*\.?\d+([eE][\-+]?\d+)?)(px|pt|pc|mm|cm|m|in|f
mm: 96/25.4,
in: 96,
pt: 4/3,
pc: 16
pc: 16,
em: 16
};
/**
@ -61,12 +62,8 @@ exports.fn = function(item, params) {
// convert absolute values to pixels
if (params.convertToPx && units && (units in absoluteLengths)) {
var pxNum = +(absoluteLengths[units] * match[1]).toFixed(floatPrecision);
if (String(pxNum).length < match[0].length) {
num = pxNum;
units = 'px';
}
num = +(absoluteLengths[units] * match[1]).toFixed(floatPrecision);
units = 'px';
}
// and remove leading zero

View file

@ -8,7 +8,8 @@ exports.description = 'moves some group attributes to the content elements';
var collections = require('./_collections.js'),
pathElems = collections.pathElems.concat(['g', 'text']),
referencesProps = collections.referencesProps;
referencesProps = collections.referencesProps,
inheritableAttrs = collections.inheritableAttrs;
/**
* Move group attrs to the content elements.
@ -31,33 +32,33 @@ var collections = require('./_collections.js'),
*/
exports.fn = function(item) {
// move group transform attr to content's pathElems
if (
item.isElem('g') &&
item.hasAttr('transform') &&
!item.isEmpty() &&
!item.someAttr(function(attr) {
return ~referencesProps.indexOf(attr.name) && ~attr.value.indexOf('url(');
}) &&
item.content.every(function(inner) {
return inner.isElem(pathElems) && !inner.hasAttr('id');
})
) {
item.content.forEach(function(inner) {
var attr = item.attr('transform');
if (inner.hasAttr('transform')) {
inner.attr('transform').value = attr.value + ' ' + inner.attr('transform').value;
} else {
inner.addAttr({
'name': attr.name,
'local': attr.local,
'prefix': attr.prefix,
'value': attr.value
if (item.isElem('g') && !item.isEmpty()) {
inheritableAttrs.forEach(function(currentAttr) {
if (item.hasAttr(currentAttr)) {
var attr = item.attr(currentAttr);
item.content.forEach(function(inner) {
if (currentAttr === 'transform' && inner.hasAttr(currentAttr)) {
// if attr is transform and the inner has transform we concatenate it
inner.attr(currentAttr).value = attr.value + ' ' + inner.attr(currentAttr).value;
} else if (!inner.hasAttr(currentAttr)){
// If the inner has the attr already we don't override it
inner.addAttr({
...attr
});
}
});
item.removeAttr(currentAttr);
}
});
item.removeAttr('transform');
}
};