mirror of
https://github.com/penpot/penpot.git
synced 2025-05-08 23:15:53 +02:00
Merge pull request #733 from penpot/feat/svg-native
Advanced SVG Import
This commit is contained in:
commit
99d9d77c63
49 changed files with 2148 additions and 510 deletions
|
@ -6,9 +6,9 @@
|
||||||
|
|
||||||
- Add more chinese translations [#726](https://github.com/penpot/penpot/pull/726)
|
- 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)
|
- 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)
|
- Improve french translations [#731](https://github.com/penpot/penpot/pull/731)
|
||||||
|
|
||||||
|
|
||||||
### :bug: Bugs fixed
|
### :bug: Bugs fixed
|
||||||
|
|
||||||
- Fix broken profile and profile options form.
|
- Fix broken profile and profile options form.
|
||||||
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
### :heart: Community contributions by (Thank you!)
|
### :heart: Community contributions by (Thank you!)
|
||||||
|
|
||||||
|
- iblueer [#731](https://github.com/penpot/penpot/pull/731)
|
||||||
|
|
||||||
## 1.3.0-alpha
|
## 1.3.0-alpha
|
||||||
|
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -14,7 +14,9 @@
|
||||||
[promesa.exec :as px]))
|
[promesa.exec :as px]))
|
||||||
|
|
||||||
(def default-client
|
(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!
|
(defn get!
|
||||||
[url opts]
|
[url opts]
|
||||||
|
|
|
@ -21,4 +21,4 @@
|
||||||
data (slurp (io/resource "app/tests/_files/sample1.svg"))
|
data (slurp (io/resource "app/tests/_files/sample1.svg"))
|
||||||
res (svgc data)]
|
res (svgc data)]
|
||||||
(t/is (string? res))
|
(t/is (string? res))
|
||||||
(t/is (= 2533 (count res)))))
|
(t/is (= 2609 (count res)))))
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
(ns app.common.data
|
(ns app.common.data
|
||||||
"Data manipulation and query helper functions."
|
"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
|
#?(:cljs
|
||||||
(:require-macros [app.common.data]))
|
(:require-macros [app.common.data]))
|
||||||
(:require
|
(:require
|
||||||
|
@ -132,8 +132,9 @@
|
||||||
"Return a map without the keys provided
|
"Return a map without the keys provided
|
||||||
in the `keys` parameter."
|
in the `keys` parameter."
|
||||||
[data keys]
|
[data keys]
|
||||||
|
(when data
|
||||||
(persistent!
|
(persistent!
|
||||||
(reduce #(dissoc! %1 %2) (transient data) keys)))
|
(reduce #(dissoc! %1 %2) (transient data) keys))))
|
||||||
|
|
||||||
(defn remove-at-index
|
(defn remove-at-index
|
||||||
[v index]
|
[v index]
|
||||||
|
@ -302,6 +303,14 @@
|
||||||
default
|
default
|
||||||
v))))
|
v))))
|
||||||
|
|
||||||
|
(defn num-string? [v]
|
||||||
|
;; https://stackoverflow.com/questions/175739/built-in-way-in-javascript-to-check-if-a-string-is-a-valid-number
|
||||||
|
#?(:cljs (and (string? v)
|
||||||
|
(not (js/isNaN v))
|
||||||
|
(not (js/isNaN (parse-double v))))
|
||||||
|
|
||||||
|
:clj (not= (parse-double v :nan) :nan)))
|
||||||
|
|
||||||
(defn read-string
|
(defn read-string
|
||||||
[v]
|
[v]
|
||||||
(r/read-string v))
|
(r/read-string v))
|
||||||
|
@ -350,7 +359,7 @@
|
||||||
;; Code for ClojureScript
|
;; Code for ClojureScript
|
||||||
(let [mdata (aapi/resolve &env v)
|
(let [mdata (aapi/resolve &env v)
|
||||||
arglists (second (get-in mdata [:meta :arglists]))
|
arglists (second (get-in mdata [:meta :arglists]))
|
||||||
sym (symbol (name v))
|
sym (symbol (core/name v))
|
||||||
andsym (symbol "&")
|
andsym (symbol "&")
|
||||||
procarg #(if (= % andsym) % (gensym "param"))]
|
procarg #(if (= % andsym) % (gensym "param"))]
|
||||||
(if (pos? (count arglists))
|
(if (pos? (count arglists))
|
||||||
|
@ -382,3 +391,16 @@
|
||||||
|
|
||||||
(defn any-key? [element & rest]
|
(defn any-key? [element & rest]
|
||||||
(some #(contains? 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)))
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,8 @@
|
||||||
(:require
|
(:require
|
||||||
#?(:cljs [cljs.pprint :as pp]
|
#?(:cljs [cljs.pprint :as pp]
|
||||||
:clj [clojure.pprint :as pp])
|
:clj [clojure.pprint :as pp])
|
||||||
|
[cuerdas.core :as str]
|
||||||
|
[app.common.data :as d]
|
||||||
[app.common.math :as mth]
|
[app.common.math :as mth]
|
||||||
[app.common.geom.point :as gpt]))
|
[app.common.geom.point :as gpt]))
|
||||||
|
|
||||||
|
@ -21,6 +23,13 @@
|
||||||
(toString [_]
|
(toString [_]
|
||||||
(str "matrix(" a "," b "," c "," d "," e "," f ")")))
|
(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
|
(defn multiply
|
||||||
([{m1a :a m1b :b m1c :c m1d :d m1e :e m1f :f}
|
([{m1a :a m1b :b m1c :c m1d :d m1e :e m1f :f}
|
||||||
{m2a :a m2b :b m2c :c m2d :d m2e :e m2f :f}]
|
{m2a :a m2b :b m2c :c m2d :d m2e :e m2f :f}]
|
||||||
|
@ -46,13 +55,6 @@
|
||||||
[v]
|
[v]
|
||||||
(instance? Matrix v))
|
(instance? Matrix v))
|
||||||
|
|
||||||
(defn matrix
|
|
||||||
"Create a new matrix instance."
|
|
||||||
([]
|
|
||||||
(Matrix. 1 0 0 1 0 0))
|
|
||||||
([a b c d e f]
|
|
||||||
(Matrix. a b c d e f)))
|
|
||||||
|
|
||||||
(def base (matrix))
|
(def base (matrix))
|
||||||
|
|
||||||
(defn base?
|
(defn base?
|
||||||
|
|
|
@ -204,6 +204,11 @@
|
||||||
(defn to-vec [p1 p2]
|
(defn to-vec [p1 p2]
|
||||||
(subtract p2 p1))
|
(subtract p2 p1))
|
||||||
|
|
||||||
|
(defn scale [v scalar]
|
||||||
|
(-> v
|
||||||
|
(update :x * scalar)
|
||||||
|
(update :y * scalar)))
|
||||||
|
|
||||||
(defn dot [{x1 :x y1 :y} {x2 :x y2 :y}]
|
(defn dot [{x1 :x y1 :y} {x2 :x y2 :y}]
|
||||||
(+ (* x1 x2) (* y1 y2)))
|
(+ (* x1 x2) (* y1 y2)))
|
||||||
|
|
||||||
|
|
|
@ -148,10 +148,10 @@
|
||||||
(update-in [:selrect :x2] - x)
|
(update-in [:selrect :x2] - x)
|
||||||
(update-in [:selrect :y2] - y)
|
(update-in [:selrect :y2] - y)
|
||||||
|
|
||||||
(d/update-when :points #(map move-point %))
|
(d/update-when :points #(mapv move-point %))
|
||||||
|
|
||||||
(cond-> (= :path type)
|
(cond-> (= :path type)
|
||||||
(d/update-when :content #(map move-segment %))))))
|
(d/update-when :content #(mapv move-segment %))))))
|
||||||
|
|
||||||
|
|
||||||
;; --- Helpers
|
;; --- Helpers
|
||||||
|
@ -281,6 +281,7 @@
|
||||||
(d/export gtr/transform-rect)
|
(d/export gtr/transform-rect)
|
||||||
(d/export gtr/update-group-selrect)
|
(d/export gtr/update-group-selrect)
|
||||||
(d/export gtr/transform-points)
|
(d/export gtr/transform-points)
|
||||||
|
(d/export gtr/calculate-adjust-matrix)
|
||||||
|
|
||||||
;; PATHS
|
;; PATHS
|
||||||
(d/export gsp/content->points)
|
(d/export gsp/content->points)
|
||||||
|
|
|
@ -139,11 +139,15 @@
|
||||||
|
|
||||||
(defn- calculate-height
|
(defn- calculate-height
|
||||||
"Calculates the height of a paralelogram given by the points"
|
"Calculates the height of a paralelogram given by the points"
|
||||||
[[p1 _ p3 p4]]
|
[[p1 _ _ p4]]
|
||||||
(let [v1 (gpt/to-vec p3 p4)
|
(-> (gpt/to-vec p4 p1)
|
||||||
v2 (gpt/to-vec p4 p1)
|
(gpt/length)))
|
||||||
angle (gpt/angle-with-other v1 v2)]
|
|
||||||
(* (gpt/length v2) (mth/sin (mth/radians angle)))))
|
(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
|
(defn- calculate-rotation
|
||||||
"Calculates the rotation between two shapes given the resize vector direction"
|
"Calculates the rotation between two shapes given the resize vector direction"
|
||||||
|
@ -173,7 +177,8 @@
|
||||||
"Calculates a matrix that is a series of transformations we have to do to the transformed rectangle so that
|
"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`.
|
after applying them the end result is the `shape-pathn-temp`.
|
||||||
This is compose of three transformations: skew, resize and rotation"
|
This is compose of three transformations: skew, resize and rotation"
|
||||||
[points-temp points-rec flip-x flip-y]
|
([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)
|
(let [center (gco/center-points points-temp)
|
||||||
|
|
||||||
stretch-matrix (gmt/matrix)
|
stretch-matrix (gmt/matrix)
|
||||||
|
@ -193,7 +198,12 @@
|
||||||
h3 (if-not (mth/almost-zero? h2) (/ h1 h2) 1)
|
h3 (if-not (mth/almost-zero? h2) (/ h1 h2) 1)
|
||||||
h3 (if (mth/nan? h3) 1 h3)
|
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)
|
||||||
|
|
||||||
|
stretch-matrix (gmt/multiply stretch-matrix (gmt/scale-matrix (gpt/point w3 h3)))
|
||||||
|
|
||||||
rotation-angle (calculate-rotation
|
rotation-angle (calculate-rotation
|
||||||
center
|
center
|
||||||
|
@ -204,13 +214,12 @@
|
||||||
|
|
||||||
stretch-matrix (gmt/multiply (gmt/rotate-matrix rotation-angle) stretch-matrix)
|
stretch-matrix (gmt/multiply (gmt/rotate-matrix rotation-angle) stretch-matrix)
|
||||||
|
|
||||||
|
|
||||||
;; This is the inverse to be able to remove the transformation
|
;; This is the inverse to be able to remove the transformation
|
||||||
stretch-matrix-inverse (-> (gmt/matrix)
|
stretch-matrix-inverse (-> (gmt/matrix)
|
||||||
(gmt/scale (gpt/point 1 (/ 1 h3)))
|
(gmt/scale (gpt/point (/ 1 w3) (/ 1 h3)))
|
||||||
(gmt/skew (- skew-angle) 0)
|
(gmt/skew (- skew-angle) 0)
|
||||||
(gmt/rotate (- rotation-angle)))]
|
(gmt/rotate (- rotation-angle)))]
|
||||||
[stretch-matrix stretch-matrix-inverse]))
|
[stretch-matrix stretch-matrix-inverse rotation-angle])))
|
||||||
|
|
||||||
(defn apply-transform
|
(defn apply-transform
|
||||||
"Given a new set of points transformed, set up the rectangle so it keeps
|
"Given a new set of points transformed, set up the rectangle so it keeps
|
||||||
|
@ -276,6 +285,24 @@
|
||||||
(dissoc :modifiers)))
|
(dissoc :modifiers)))
|
||||||
shape)))
|
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]
|
(defn update-group-selrect [group children]
|
||||||
(let [shape-center (gco/center-shape group)
|
(let [shape-center (gco/center-shape group)
|
||||||
transform (:transform group (gmt/matrix))
|
transform (:transform group (gmt/matrix))
|
||||||
|
@ -297,6 +324,7 @@
|
||||||
|
|
||||||
;; Updates the shape and the applytransform-rect will update the other properties
|
;; Updates the shape and the applytransform-rect will update the other properties
|
||||||
(-> group
|
(-> group
|
||||||
|
(update-group-viewbox new-selrect)
|
||||||
(assoc :selrect new-selrect)
|
(assoc :selrect new-selrect)
|
||||||
(assoc :points new-points)
|
(assoc :points new-points)
|
||||||
|
|
||||||
|
|
|
@ -229,7 +229,7 @@
|
||||||
(s/def :internal.shape/stroke-color-ref-file (s/nilable uuid?))
|
(s/def :internal.shape/stroke-color-ref-file (s/nilable uuid?))
|
||||||
(s/def :internal.shape/stroke-color-ref-id (s/nilable uuid?))
|
(s/def :internal.shape/stroke-color-ref-id (s/nilable uuid?))
|
||||||
(s/def :internal.shape/stroke-opacity ::safe-number)
|
(s/def :internal.shape/stroke-opacity ::safe-number)
|
||||||
(s/def :internal.shape/stroke-style #{:solid :dotted :dashed :mixed :none})
|
(s/def :internal.shape/stroke-style #{:solid :dotted :dashed :mixed :none :svg})
|
||||||
(s/def :internal.shape/stroke-width ::safe-number)
|
(s/def :internal.shape/stroke-width ::safe-number)
|
||||||
(s/def :internal.shape/stroke-alignment #{:center :inner :outer})
|
(s/def :internal.shape/stroke-alignment #{:center :inner :outer})
|
||||||
(s/def :internal.shape/text-align #{"left" "right" "center" "justify"})
|
(s/def :internal.shape/text-align #{"left" "right" "center" "justify"})
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -5505,5 +5505,45 @@
|
||||||
"zh_cn" : "单击以闭合路径"
|
"zh_cn" : "单击以闭合路径"
|
||||||
},
|
},
|
||||||
"unused" : true
|
"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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -449,6 +449,10 @@ ul.slider-dots {
|
||||||
content: "Y";
|
content: "Y";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.large {
|
||||||
|
min-width: 7rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input,
|
input,
|
||||||
|
|
|
@ -94,6 +94,10 @@
|
||||||
|
|
||||||
.attributes-label,
|
.attributes-label,
|
||||||
.attributes-value {
|
.attributes-value {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
width: 50%;
|
width: 50%;
|
||||||
}
|
}
|
||||||
.copy-button {
|
.copy-button {
|
||||||
|
|
|
@ -709,6 +709,9 @@
|
||||||
.element-set-content .input-row {
|
.element-set-content .input-row {
|
||||||
& .element-set-subtitle {
|
& .element-set-subtitle {
|
||||||
width: 5.5rem;
|
width: 5.5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -771,7 +774,6 @@
|
||||||
min-width: 56px;
|
min-width: 56px;
|
||||||
max-height: 10rem;
|
max-height: 10rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -911,6 +913,7 @@
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.row-flex-removable:hover .element-set-actions,
|
||||||
.element-set-options-group:hover .element-set-actions {
|
.element-set-options-group:hover .element-set-actions {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
|
|
|
@ -219,15 +219,19 @@
|
||||||
|
|
||||||
(defn generate-unique-name
|
(defn generate-unique-name
|
||||||
"A unique name generator"
|
"A unique name generator"
|
||||||
[used basename]
|
([used basename]
|
||||||
|
(generate-unique-name used basename false))
|
||||||
|
([used basename prefix-first?]
|
||||||
(s/assert ::set-of-string used)
|
(s/assert ::set-of-string used)
|
||||||
(s/assert ::us/string basename)
|
(s/assert ::us/string basename)
|
||||||
(let [[prefix initial] (extract-numeric-suffix basename)]
|
(let [[prefix initial] (extract-numeric-suffix basename)]
|
||||||
(loop [counter initial]
|
(loop [counter initial]
|
||||||
(let [candidate (str prefix "-" counter)]
|
(let [candidate (if (and (= 1 counter) prefix-first?)
|
||||||
|
(str prefix)
|
||||||
|
(str prefix "-" counter))]
|
||||||
(if (contains? used candidate)
|
(if (contains? used candidate)
|
||||||
(recur (inc counter))
|
(recur (inc counter))
|
||||||
candidate)))))
|
candidate))))))
|
||||||
|
|
||||||
;; --- Shape attrs (Layers Sidebar)
|
;; --- Shape attrs (Layers Sidebar)
|
||||||
|
|
||||||
|
@ -574,7 +578,9 @@
|
||||||
[frame-id parent-id (inc index)])))))
|
[frame-id parent-id (inc index)])))))
|
||||||
|
|
||||||
(defn add-shape-changes
|
(defn add-shape-changes
|
||||||
[page-id objects selected 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)
|
(let [id (:id attrs)
|
||||||
shape (gpr/setup-proportions attrs)
|
shape (gpr/setup-proportions attrs)
|
||||||
|
|
||||||
|
@ -589,21 +595,22 @@
|
||||||
|
|
||||||
[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
|
redo-changes (cond-> [{:type :add-obj
|
||||||
:id id
|
:id id
|
||||||
:page-id page-id
|
:page-id page-id
|
||||||
:frame-id frame-id
|
:frame-id frame-id
|
||||||
:parent-id parent-id
|
:parent-id parent-id
|
||||||
:index index
|
:index index
|
||||||
:obj shape}
|
:obj shape}]
|
||||||
{:type :reg-objects
|
reg-object?
|
||||||
|
(conj {:type :reg-objects
|
||||||
:page-id page-id
|
:page-id page-id
|
||||||
:shapes [id]}]
|
:shapes [id]}))
|
||||||
undo-changes [{:type :del-obj
|
undo-changes [{:type :del-obj
|
||||||
:page-id page-id
|
:page-id page-id
|
||||||
:id id}]]
|
:id id}]]
|
||||||
|
|
||||||
[redo-changes undo-changes]))
|
[redo-changes undo-changes])))
|
||||||
|
|
||||||
(defn add-shape
|
(defn add-shape
|
||||||
[attrs]
|
[attrs]
|
||||||
|
|
|
@ -32,10 +32,12 @@
|
||||||
[app.util.router :as rt]
|
[app.util.router :as rt]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
[app.util.transit :as t]
|
[app.util.transit :as t]
|
||||||
|
[app.util.uri :as uu]
|
||||||
[beicon.core :as rx]
|
[beicon.core :as rx]
|
||||||
[cljs.spec.alpha :as s]
|
[cljs.spec.alpha :as s]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[potok.core :as ptk]))
|
[potok.core :as ptk]
|
||||||
|
[promesa.core :as p]))
|
||||||
|
|
||||||
(declare persist-changes)
|
(declare persist-changes)
|
||||||
(declare persist-sychronous-changes)
|
(declare persist-sychronous-changes)
|
||||||
|
@ -392,7 +394,7 @@
|
||||||
(or (contains? props :data)
|
(or (contains? props :data)
|
||||||
(contains? props :uris)))))
|
(contains? props :uris)))))
|
||||||
|
|
||||||
(defn parse-svg [text]
|
(defn parse-svg [[name text]]
|
||||||
(->> (http/send! {:method :post
|
(->> (http/send! {:method :post
|
||||||
:uri "/api/svg"
|
:uri "/api/svg"
|
||||||
:headers {"content-type" "image/svg+xml"}
|
:headers {"content-type" "image/svg+xml"}
|
||||||
|
@ -400,19 +402,14 @@
|
||||||
(rx/map (fn [{:keys [status body]}]
|
(rx/map (fn [{:keys [status body]}]
|
||||||
(let [result (t/decode body)]
|
(let [result (t/decode body)]
|
||||||
(if (= status 200)
|
(if (= status 200)
|
||||||
result
|
(assoc result :name name)
|
||||||
(throw result)))))))
|
(throw result)))))))
|
||||||
|
|
||||||
(defn fetch-svg [uri]
|
(defn fetch-svg [name uri]
|
||||||
(->> (http/send! {:method :get :uri uri})
|
(->> (http/send! {:method :get :uri uri})
|
||||||
(rx/map :body)))
|
(rx/map #(vector
|
||||||
|
(or name (uu/uri-name uri))
|
||||||
(defn url-name [url]
|
(:body %)))))
|
||||||
(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- handle-upload-error [on-error stream]
|
(defn- handle-upload-error [on-error stream]
|
||||||
(->> stream
|
(->> stream
|
||||||
|
@ -456,7 +453,7 @@
|
||||||
(prepare-uri [uri]
|
(prepare-uri [uri]
|
||||||
{:file-id file-id
|
{:file-id file-id
|
||||||
:is-local local?
|
:is-local local?
|
||||||
:name (or name (url-name uri))
|
:name (or name (uu/uri-name uri))
|
||||||
:url uri})]
|
:url uri})]
|
||||||
(rx/merge
|
(rx/merge
|
||||||
(->> (rx/from uris)
|
(->> (rx/from uris)
|
||||||
|
@ -467,10 +464,8 @@
|
||||||
|
|
||||||
(->> (rx/from uris)
|
(->> (rx/from uris)
|
||||||
(rx/filter svg-url?)
|
(rx/filter svg-url?)
|
||||||
(rx/merge-map fetch-svg)
|
(rx/merge-map (partial fetch-svg name))
|
||||||
(rx/merge-map parse-svg)
|
(rx/merge-map parse-svg)
|
||||||
(rx/with-latest vector uris)
|
|
||||||
(rx/map #(assoc (first %) :name (or name (url-name (second %)))))
|
|
||||||
(rx/do on-svg)))))
|
(rx/do on-svg)))))
|
||||||
|
|
||||||
(defn- upload-data [file-id local? name data force-media on-image on-svg]
|
(defn- upload-data [file-id local? name data force-media on-image on-svg]
|
||||||
|
@ -485,6 +480,12 @@
|
||||||
:is-local local?
|
:is-local local?
|
||||||
:content blob}))
|
:content blob}))
|
||||||
|
|
||||||
|
extract-content
|
||||||
|
(fn [blob]
|
||||||
|
(let [name (or name (.-name blob))]
|
||||||
|
(-> (.text blob)
|
||||||
|
(p/then #(vector name %)))))
|
||||||
|
|
||||||
file-stream (->> (rx/from data)
|
file-stream (->> (rx/from data)
|
||||||
(rx/map di/validate-file))]
|
(rx/map di/validate-file))]
|
||||||
(rx/merge
|
(rx/merge
|
||||||
|
@ -496,10 +497,8 @@
|
||||||
|
|
||||||
(->> file-stream
|
(->> file-stream
|
||||||
(rx/filter svg-blob?)
|
(rx/filter svg-blob?)
|
||||||
(rx/merge-map #(.text %))
|
(rx/merge-map extract-content)
|
||||||
(rx/merge-map parse-svg)
|
(rx/merge-map parse-svg)
|
||||||
(rx/with-latest vector file-stream)
|
|
||||||
(rx/map #(assoc (first %) :name (.-name (second %))))
|
|
||||||
(rx/do on-svg)))))
|
(rx/do on-svg)))))
|
||||||
|
|
||||||
(defn- upload-media-objects
|
(defn- upload-media-objects
|
||||||
|
@ -538,7 +537,7 @@
|
||||||
[params position]
|
[params position]
|
||||||
(let [{:keys [x y]} position
|
(let [{:keys [x y]} position
|
||||||
mdata {:on-image #(st/emit! (dwc/image-uploaded % x y))
|
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)
|
params (-> (assoc params :local? true)
|
||||||
(with-meta mdata))]
|
(with-meta mdata))]
|
||||||
|
|
|
@ -10,60 +10,94 @@
|
||||||
(ns app.main.data.workspace.svg-upload
|
(ns app.main.data.workspace.svg-upload
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.util.data :as ud]
|
[app.common.geom.matrix :as gmt]
|
||||||
|
[app.common.geom.point :as gpt]
|
||||||
[app.common.geom.shapes :as gsh]
|
[app.common.geom.shapes :as gsh]
|
||||||
|
[app.common.geom.proportions :as gpr]
|
||||||
[app.common.pages :as cp]
|
[app.common.pages :as cp]
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
[app.main.data.workspace.common :as dwc]
|
[app.main.data.workspace.common :as dwc]
|
||||||
|
[app.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]
|
[beicon.core :as rx]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[potok.core :as ptk]
|
[potok.core :as ptk]
|
||||||
[app.util.svg :as usvg]
|
[promesa.core :as p]))
|
||||||
[app.util.geom.path :as ugp]))
|
|
||||||
|
(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]
|
(defn- svg-dimensions [data]
|
||||||
(let [width (get-in data [:attrs :width] 100)
|
(let [width (get-in data [:attrs :width] 100)
|
||||||
height (get-in data [:attrs :height] 100)
|
height (get-in data [:attrs :height] 100)
|
||||||
viewbox (get-in data [:attrs :viewBox] (str "0 0 " width " " height))
|
viewbox (get-in data [:attrs :viewBox] (str "0 0 " width " " height))
|
||||||
[_ _ width-str height-str] (str/split viewbox " ")
|
[x y width height] (->> (str/split viewbox " ")
|
||||||
width (d/parse-integer width-str)
|
(map d/parse-double))]
|
||||||
height (d/parse-integer height-str)]
|
[x y width height]))
|
||||||
[width height]))
|
|
||||||
|
|
||||||
(defn tag-name [{:keys [tag]}]
|
(defn tag->name
|
||||||
(cond (string? tag) tag
|
"Given a tag returns its layer name"
|
||||||
(keyword? tag) (name tag)
|
[tag]
|
||||||
|
(str "svg-" (cond (string? tag) tag
|
||||||
|
(keyword? tag) (d/name tag)
|
||||||
(nil? tag) "node"
|
(nil? tag) "node"
|
||||||
:else (str tag)))
|
:else (str tag))))
|
||||||
|
|
||||||
(defn setup-fill [shape attrs]
|
(defn setup-fill [shape]
|
||||||
(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]}]
|
|
||||||
(cond-> shape
|
(cond-> shape
|
||||||
(d/any-key? attrs :fill :fill-opacity)
|
;; Color present as attribute
|
||||||
(setup-fill attrs)
|
(uc/color? (get-in shape [:svg-attrs :fill]))
|
||||||
|
(-> (update :svg-attrs dissoc :fill)
|
||||||
|
(assoc :fill-color (-> (get-in shape [:svg-attrs :fill])
|
||||||
|
(uc/parse-color))))
|
||||||
|
|
||||||
(d/any-key? attrs :stroke :stroke-width :stroke-opacity)
|
;; Color present as style
|
||||||
(setup-stroke attrs)))
|
(uc/color? (get-in shape [:svg-attrs :style :fill]))
|
||||||
|
(-> (update-in [:svg-attrs :style] dissoc :fill)
|
||||||
|
(assoc :fill-color (-> (get-in shape [:svg-attrs :style :fill])
|
||||||
|
(uc/parse-color))))
|
||||||
|
|
||||||
(defn create-raw-svg [name frame-id x y width height data]
|
(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)
|
(-> {:id (uuid/next)
|
||||||
:type :svg-raw
|
:type :svg-raw
|
||||||
:name name
|
:name name
|
||||||
|
@ -72,13 +106,55 @@
|
||||||
:height height
|
:height height
|
||||||
:x x
|
:x x
|
||||||
:y y
|
:y y
|
||||||
:content (if (map? data) (update data :attrs usvg/clean-attrs) data)}
|
:content (cond-> data
|
||||||
(gsh/setup-selrect)))
|
(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)
|
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)
|
(-> {:id (uuid/next)
|
||||||
:type :path
|
:type :path
|
||||||
:name name
|
:name name
|
||||||
|
@ -86,43 +162,182 @@
|
||||||
:content content
|
:content content
|
||||||
:selrect selrect
|
:selrect selrect
|
||||||
:points points}
|
:points points}
|
||||||
|
(assoc :svg-viewbox (select-keys selrect [:x :y :width :height]))
|
||||||
|
(assoc :svg-attrs (dissoc attrs :d :transform))
|
||||||
|
(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]
|
center (gsh/center-points points)
|
||||||
(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
|
rect-shape (-> (gsh/make-centered-rect center (:width rect-data) (:height rect-data))
|
||||||
(case tag
|
(update :width max 1)
|
||||||
;; :rect (parse-rect data)
|
(update :height max 1))
|
||||||
;; :path (parse-path name frame-id data)
|
|
||||||
(create-raw-svg name frame-id x y width height data))]
|
|
||||||
|
|
||||||
(-> shape
|
selrect (gsh/rect->selrect rect-shape)
|
||||||
(assoc :svg-id root-id))))
|
|
||||||
|
|
||||||
(defn svg-uploaded [data x y]
|
rect-points (gsh/rect->points rect-shape)
|
||||||
(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])
|
|
||||||
|
|
||||||
[width height] (svg-dimensions data)
|
[shape-transform shape-transform-inv rotation]
|
||||||
x (- x (/ width 2))
|
(gsh/calculate-adjust-matrix points rect-points (neg? (:a transform)) (neg? (:d transform)))]
|
||||||
y (- y (/ height 2))
|
|
||||||
|
|
||||||
add-svg-child
|
(merge rect-shape
|
||||||
(fn add-svg-child [parent-id root-shape [unames [rchs uchs]] [index {:keys [content] :as data}]]
|
{:selrect selrect
|
||||||
(let [shape (parse-svg-element root-shape data unames)
|
: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)
|
shape-id (:id shape)
|
||||||
[rch1 uch1] (dwc/add-shape-changes page-id objects selected shape)
|
|
||||||
|
[rch1 uch1] (dwc/add-shape-changes page-id objects selected shape false)
|
||||||
|
|
||||||
;; Mov-objects won't have undo because we "delete" the object in the undo of the
|
;; Mov-objects won't have undo because we "delete" the object in the undo of the
|
||||||
;; previous operation
|
;; previous operation
|
||||||
|
@ -135,19 +350,95 @@
|
||||||
|
|
||||||
;; Careful! the undo changes are concatenated reversed (we undo in reverse order
|
;; Careful! the undo changes are concatenated reversed (we undo in reverse order
|
||||||
changes [(d/concat rchs rch1 rch2) (d/concat uch1 uchs)]
|
changes [(d/concat rchs rch1 rch2) (d/concat uch1 uchs)]
|
||||||
unames (conj unames (:name shape))]
|
unames (conj unames (:name shape))
|
||||||
(reduce (partial add-svg-child shape-id root-shape) [unames changes] (d/enumerate content))))
|
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 [images-to-upload (-> svg-data (usvg/collect-images))
|
||||||
|
|
||||||
|
prepare-uri
|
||||||
|
(fn [uri]
|
||||||
|
(merge
|
||||||
|
{:file-id file-id
|
||||||
|
:is-local true
|
||||||
|
:url uri}
|
||||||
|
|
||||||
|
(if (str/starts-with? uri "data:")
|
||||||
|
{:name "image"
|
||||||
|
:content (uu/data-uri->blob uri)}
|
||||||
|
{:name (uu/uri-name uri)})))]
|
||||||
|
|
||||||
|
(->> (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)))))))
|
||||||
|
|
||||||
|
(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])
|
||||||
|
|
||||||
|
[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))
|
||||||
|
|
||||||
unames (dwc/retrieve-used-names objects)
|
unames (dwc/retrieve-used-names objects)
|
||||||
|
|
||||||
svg-name (->> (str/replace (:name data) ".svg" "")
|
svg-name (->> (str/replace (:name svg-data) ".svg" "")
|
||||||
(dwc/generate-unique-name unames))
|
(dwc/generate-unique-name unames))
|
||||||
|
|
||||||
root-shape (create-raw-svg svg-name frame-id x y width height data)
|
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))
|
||||||
|
|
||||||
|
[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)
|
root-id (:id root-shape)
|
||||||
|
|
||||||
changes (dwc/add-shape-changes page-id objects selected 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)]
|
||||||
|
|
||||||
[_ [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})
|
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})
|
||||||
(dwc/select-shapes (d/ordered-set root-id)))))))
|
(dwc/select-shapes (d/ordered-set root-id))))
|
||||||
|
|
||||||
|
(catch :default e
|
||||||
|
(.error js/console "Error upload" e))))))
|
||||||
|
|
|
@ -78,7 +78,8 @@
|
||||||
(defn ^:export dump-object [name]
|
(defn ^:export dump-object [name]
|
||||||
(let [page-id (get @state :current-page-id)]
|
(let [page-id (get @state :current-page-id)]
|
||||||
(let [objects (get-in @state [:workspace-data :pages-index page-id :objects])
|
(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
|
(->> target
|
||||||
(logjs "state")))))
|
(logjs "state")))))
|
||||||
|
|
||||||
|
|
|
@ -19,16 +19,17 @@
|
||||||
[app.main.ui.handoff.attributes.shadow :refer [shadow-panel]]
|
[app.main.ui.handoff.attributes.shadow :refer [shadow-panel]]
|
||||||
[app.main.ui.handoff.attributes.blur :refer [blur-panel]]
|
[app.main.ui.handoff.attributes.blur :refer [blur-panel]]
|
||||||
[app.main.ui.handoff.attributes.image :refer [image-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
|
(def type->options
|
||||||
{:multiple [:fill :stroke :image :text :shadow :blur]
|
{:multiple [:fill :stroke :image :text :shadow :blur]
|
||||||
:frame [:layout :fill]
|
:frame [:layout :fill]
|
||||||
:group [:layout]
|
:group [:layout :svg]
|
||||||
:rect [:layout :fill :stroke :shadow :blur]
|
:rect [:layout :fill :stroke :shadow :blur :svg]
|
||||||
:circle [:layout :fill :stroke :shadow :blur]
|
:circle [:layout :fill :stroke :shadow :blur :svg]
|
||||||
:path [:layout :fill :stroke :shadow :blur]
|
:path [:layout :fill :stroke :shadow :blur :svg]
|
||||||
:image [:image :layout :shadow :blur]
|
:image [:image :layout :shadow :blur :svg]
|
||||||
:text [:layout :text :shadow :blur]})
|
:text [:layout :text :shadow :blur]})
|
||||||
|
|
||||||
(mf/defc attributes
|
(mf/defc attributes
|
||||||
|
@ -46,7 +47,8 @@
|
||||||
:shadow shadow-panel
|
:shadow shadow-panel
|
||||||
:blur blur-panel
|
:blur blur-panel
|
||||||
:image image-panel
|
:image image-panel
|
||||||
:text text-panel)
|
:text text-panel
|
||||||
|
:svg svg-panel)
|
||||||
{:shapes shapes
|
{:shapes shapes
|
||||||
:frame frame
|
:frame frame
|
||||||
:locale locale}])
|
:locale locale}])
|
||||||
|
|
|
@ -37,28 +37,30 @@
|
||||||
|
|
||||||
(mf/defc layout-block
|
(mf/defc layout-block
|
||||||
[{:keys [shape locale]}]
|
[{:keys [shape locale]}]
|
||||||
|
(let [selrect (:selrect shape)
|
||||||
|
{:keys [width height x y]} selrect]
|
||||||
[:*
|
[:*
|
||||||
[:div.attributes-unit-row
|
[:div.attributes-unit-row
|
||||||
[:div.attributes-label (t locale "handoff.attributes.layout.width")]
|
[:div.attributes-label (t locale "handoff.attributes.layout.width")]
|
||||||
[:div.attributes-value (mth/precision (:width shape) 2) "px"]
|
[:div.attributes-value (mth/precision width 2) "px"]
|
||||||
[:& copy-button {:data (copy-data shape :width)}]]
|
[:& copy-button {:data (copy-data selrect :width)}]]
|
||||||
|
|
||||||
[:div.attributes-unit-row
|
[:div.attributes-unit-row
|
||||||
[:div.attributes-label (t locale "handoff.attributes.layout.height")]
|
[:div.attributes-label (t locale "handoff.attributes.layout.height")]
|
||||||
[:div.attributes-value (mth/precision (:height shape) 2) "px"]
|
[:div.attributes-value (mth/precision height 2) "px"]
|
||||||
[:& copy-button {:data (copy-data shape :height)}]]
|
[:& copy-button {:data (copy-data selrect :height)}]]
|
||||||
|
|
||||||
(when (not= (:x shape) 0)
|
(when (not= (:x shape) 0)
|
||||||
[:div.attributes-unit-row
|
[:div.attributes-unit-row
|
||||||
[:div.attributes-label (t locale "handoff.attributes.layout.left")]
|
[:div.attributes-label (t locale "handoff.attributes.layout.left")]
|
||||||
[:div.attributes-value (mth/precision (:x shape) 2) "px"]
|
[:div.attributes-value (mth/precision x 2) "px"]
|
||||||
[:& copy-button {:data (copy-data shape :x)}]])
|
[:& copy-button {:data (copy-data selrect :x)}]])
|
||||||
|
|
||||||
(when (not= (:y shape) 0)
|
(when (not= (:y shape) 0)
|
||||||
[:div.attributes-unit-row
|
[:div.attributes-unit-row
|
||||||
[:div.attributes-label (t locale "handoff.attributes.layout.top")]
|
[:div.attributes-label (t locale "handoff.attributes.layout.top")]
|
||||||
[:div.attributes-value (mth/precision (:y shape) 2) "px"]
|
[:div.attributes-value (mth/precision y 2) "px"]
|
||||||
[:& copy-button {:data (copy-data shape :y)}]])
|
[:& copy-button {:data (copy-data selrect :y)}]])
|
||||||
|
|
||||||
(when (and (:rx shape) (not= (:rx shape) 0))
|
(when (and (:rx shape) (not= (:rx shape) 0))
|
||||||
[:div.attributes-unit-row
|
[:div.attributes-unit-row
|
||||||
|
@ -83,7 +85,7 @@
|
||||||
[:div.attributes-unit-row
|
[:div.attributes-unit-row
|
||||||
[:div.attributes-label (t locale "handoff.attributes.layout.rotation")]
|
[:div.attributes-label (t locale "handoff.attributes.layout.rotation")]
|
||||||
[:div.attributes-value (mth/precision (:rotation shape) 2) "deg"]
|
[:div.attributes-value (mth/precision (:rotation shape) 2) "deg"]
|
||||||
[:& copy-button {:data (copy-data shape :rotation)}]])])
|
[:& copy-button {:data (copy-data shape :rotation)}]])]))
|
||||||
|
|
||||||
|
|
||||||
(mf/defc layout-panel
|
(mf/defc layout-panel
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
(:require
|
(:require
|
||||||
[rumext.alpha :as mf]
|
[rumext.alpha :as mf]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
|
[app.common.data :as d]
|
||||||
[app.util.i18n :refer [t]]
|
[app.util.i18n :refer [t]]
|
||||||
[app.util.code-gen :as cg]
|
[app.util.code-gen :as cg]
|
||||||
[app.main.ui.icons :as i]
|
[app.main.ui.icons :as i]
|
||||||
|
@ -40,7 +41,7 @@
|
||||||
copy-data (shadow-copy-data shadow)]
|
copy-data (shadow-copy-data shadow)]
|
||||||
[:div.attributes-shadow-block
|
[:div.attributes-shadow-block
|
||||||
[:div.attributes-shadow-row
|
[: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-shadow
|
||||||
[:div.attributes-label (t locale "handoff.attributes.shadow.shorthand.offset-x")]
|
[:div.attributes-label (t locale "handoff.attributes.shadow.shorthand.offset-x")]
|
||||||
[:div.attributes-value (str (:offset-x shadow))]]
|
[:div.attributes-value (str (:offset-x shadow))]]
|
||||||
|
|
|
@ -11,6 +11,8 @@
|
||||||
(:require
|
(:require
|
||||||
[rumext.alpha :as mf]
|
[rumext.alpha :as mf]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
|
[app.common.data :as d]
|
||||||
|
[app.common.math :as mth]
|
||||||
[app.util.i18n :refer [t]]
|
[app.util.i18n :refer [t]]
|
||||||
[app.util.color :as uc]
|
[app.util.color :as uc]
|
||||||
[app.main.ui.icons :as i]
|
[app.main.ui.icons :as i]
|
||||||
|
@ -27,13 +29,15 @@
|
||||||
|
|
||||||
(defn format-stroke [shape]
|
(defn format-stroke [shape]
|
||||||
(let [width (:stroke-width 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)]
|
color (-> shape shape->color uc/color->background)]
|
||||||
(str/format "%spx %s %s" width style color)))
|
(str/format "%spx %s %s" width style color)))
|
||||||
|
|
||||||
(defn has-stroke? [shape]
|
(defn has-stroke? [{:keys [stroke-style]}]
|
||||||
(and (:stroke-style shape)
|
(and stroke-style
|
||||||
(not= (:stroke-style shape) :none)))
|
(and (not= stroke-style :none)
|
||||||
|
(not= stroke-style :svg))))
|
||||||
|
|
||||||
(defn copy-stroke-data [shape]
|
(defn copy-stroke-data [shape]
|
||||||
(cg/generate-css-props
|
(cg/generate-css-props
|
||||||
|
@ -59,12 +63,15 @@
|
||||||
:copy-data (copy-color-data shape)
|
:copy-data (copy-color-data shape)
|
||||||
:on-change-format #(reset! color-format %)}]
|
:on-change-format #(reset! color-format %)}]
|
||||||
|
|
||||||
|
(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-stroke-row
|
||||||
[:div.attributes-label (t locale "handoff.attributes.stroke.width")]
|
[:div.attributes-label (t locale "handoff.attributes.stroke.width")]
|
||||||
[:div.attributes-value (:stroke-width shape) "px"]
|
[:div.attributes-value (mth/precision (:stroke-width shape) 2) "px"]
|
||||||
[:div.attributes-value (->> shape :stroke-style name (str "handoff.attributes.stroke.style.") (t locale))]
|
[:div.attributes-value (->> stroke-style d/name (str "handoff.attributes.stroke.style.") (t locale))]
|
||||||
[:div.attributes-label (->> shape :stroke-alignment name (str "handoff.attributes.stroke.alignment.") (t locale))]
|
[:div.attributes-label (->> stroke-alignment d/name (str "handoff.attributes.stroke.alignment.") (t locale))]
|
||||||
[:& copy-button {:data (copy-stroke-data shape)}]]]))
|
[:& copy-button {:data (copy-stroke-data shape)}]])]))
|
||||||
|
|
||||||
(mf/defc stroke-panel
|
(mf/defc stroke-panel
|
||||||
[{:keys [shapes locale]}]
|
[{:keys [shapes locale]}]
|
||||||
|
|
57
frontend/src/app/main/ui/handoff/attributes/svg.cljs
Normal file
57
frontend/src/app/main/ui/handoff/attributes/svg.cljs
Normal 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}]])))
|
|
@ -114,7 +114,7 @@
|
||||||
[:input.input-text {:on-change (partial on-suffix-change index)
|
[:input.input-text {:on-change (partial on-suffix-change index)
|
||||||
:value (:suffix export)}]
|
:value (:suffix export)}]
|
||||||
[:select.input-select {:on-change (partial on-type-change index)
|
[: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 "png"} "PNG"]
|
||||||
[:option {:value "jpeg"} "JPEG"]
|
[:option {:value "jpeg"} "JPEG"]
|
||||||
[:option {:value "svg"} "SVG"]]
|
[:option {:value "svg"} "SVG"]]
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
[rumext.alpha :as mf]
|
[rumext.alpha :as mf]
|
||||||
[okulary.core :as l]
|
[okulary.core :as l]
|
||||||
[app.util.i18n :refer [t] :as i18n]
|
[app.util.i18n :refer [t] :as i18n]
|
||||||
|
[app.common.data :as d]
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
[app.main.ui.icons :as i]
|
[app.main.ui.icons :as i]
|
||||||
[app.main.ui.components.tab-container :refer [tab-container tab-element]]
|
[app.main.ui.components.tab-container :refer [tab-container tab-element]]
|
||||||
|
@ -49,7 +50,7 @@
|
||||||
[:*
|
[:*
|
||||||
[:span.tool-window-bar-icon
|
[:span.tool-window-bar-icon
|
||||||
[:& element-icon {:shape (-> shapes first)}]]
|
[:& 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
|
[:div.tool-window-content
|
||||||
[:& tab-container {:on-change-tab #(do
|
[:& tab-container {:on-change-tab #(do
|
||||||
|
|
|
@ -12,7 +12,8 @@
|
||||||
[rumext.alpha :as mf]
|
[rumext.alpha :as mf]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[app.util.object :as obj]
|
[app.util.object :as obj]
|
||||||
[app.main.ui.context :as muc]))
|
[app.main.ui.context :as muc]
|
||||||
|
[app.util.svg :as usvg]))
|
||||||
|
|
||||||
(defn- stroke-type->dasharray
|
(defn- stroke-type->dasharray
|
||||||
[style]
|
[style]
|
||||||
|
@ -74,45 +75,81 @@
|
||||||
attrs)))
|
attrs)))
|
||||||
|
|
||||||
(defn add-fill [attrs shape render-id]
|
(defn add-fill [attrs shape render-id]
|
||||||
|
(let [fill-attrs (cond
|
||||||
|
(contains? shape :fill-color-gradient)
|
||||||
(let [fill-color-gradient-id (str "fill-color-gradient_" render-id)]
|
(let [fill-color-gradient-id (str "fill-color-gradient_" render-id)]
|
||||||
(cond
|
{:fill (str/format "url(#%s)" fill-color-gradient-id)})
|
||||||
(:fill-color-gradient shape)
|
|
||||||
(obj/merge! attrs #js {:fill (str/format "url(#%s)" fill-color-gradient-id)})
|
|
||||||
|
|
||||||
(and (not= :svg-raw (:type shape))
|
(contains? shape :fill-color)
|
||||||
(not (:fill-color-gradient shape)))
|
{:fill (:fill-color shape)}
|
||||||
(obj/merge! attrs #js {:fill (or (:fill-color shape) "transparent")
|
|
||||||
:fillOpacity (:fill-opacity shape nil)})
|
|
||||||
|
|
||||||
(and (= :svg-raw (:type shape))
|
;; If contains svg-attrs the origin is svg. If it's not svg origin
|
||||||
(or (:fill-opacity shape) (:fill-color shape)))
|
;; we setup the default fill as transparent (instead of black)
|
||||||
(obj/merge! attrs #js {:fill (:fill-color shape)
|
(and (not (contains? shape :svg-attrs))
|
||||||
:fillOpacity (:fill-opacity shape nil)})
|
(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]
|
(defn add-stroke [attrs shape render-id]
|
||||||
(let [stroke-style (:stroke-style shape :none)
|
(let [stroke-style (:stroke-style shape :none)
|
||||||
stroke-color-gradient-id (str "stroke-color-gradient_" render-id)]
|
stroke-color-gradient-id (str "stroke-color-gradient_" render-id)]
|
||||||
(if (not= stroke-style :none)
|
(if (not= stroke-style :none)
|
||||||
(if (:stroke-color-gradient shape)
|
(let [stroke-attrs
|
||||||
(obj/merge! attrs
|
(cond-> {:strokeWidth (:stroke-width shape 1)}
|
||||||
#js {:stroke (str/format "url(#%s)" stroke-color-gradient-id)
|
(:stroke-color-gradient shape)
|
||||||
:strokeWidth (:stroke-width shape 1)
|
(assoc :stroke (str/format "url(#%s)" stroke-color-gradient-id))
|
||||||
:strokeDasharray (stroke-type->dasharray stroke-style)})
|
|
||||||
(obj/merge! attrs
|
(and (not (:stroke-color-gradient shape))
|
||||||
#js {:stroke (:stroke-color shape nil)
|
(:stroke-color shape nil))
|
||||||
:strokeWidth (:stroke-width shape 1)
|
(assoc :stroke (:stroke-color shape nil))
|
||||||
:strokeOpacity (:stroke-opacity shape nil)
|
|
||||||
:strokeDasharray (stroke-type->dasharray stroke-style)}))))
|
(and (not (:stroke-color-gradient shape))
|
||||||
attrs)
|
(: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
|
(defn extract-style-attrs
|
||||||
([shape]
|
([shape]
|
||||||
(let [render-id (mf/use-ctx muc/render-ctx)
|
(let [render-id (mf/use-ctx muc/render-ctx)
|
||||||
|
svg-defs (:svg-defs shape {})
|
||||||
|
svg-attrs (:svg-attrs shape {})
|
||||||
|
|
||||||
|
[svg-attrs svg-styles] (mf/use-memo
|
||||||
|
(mf/deps render-id svg-defs svg-attrs)
|
||||||
|
#(extract-svg-attrs render-id svg-defs svg-attrs))
|
||||||
|
|
||||||
styles (-> (obj/new)
|
styles (-> (obj/new)
|
||||||
|
(obj/merge! svg-styles)
|
||||||
(add-fill shape render-id)
|
(add-fill shape render-id)
|
||||||
(add-stroke shape render-id))]
|
(add-stroke shape render-id))]
|
||||||
|
|
||||||
(-> (obj/new)
|
(-> (obj/new)
|
||||||
|
(obj/merge! svg-attrs)
|
||||||
(add-border-radius shape)
|
(add-border-radius shape)
|
||||||
(obj/set! "style" styles)))))
|
(obj/set! "style" styles)))))
|
||||||
|
|
|
@ -9,7 +9,9 @@
|
||||||
|
|
||||||
(ns app.main.ui.shapes.group
|
(ns app.main.ui.shapes.group
|
||||||
(:require
|
(:require
|
||||||
|
[app.util.object :as obj]
|
||||||
[rumext.alpha :as mf]
|
[rumext.alpha :as mf]
|
||||||
|
[app.main.ui.shapes.attrs :as attrs]
|
||||||
[app.main.ui.shapes.mask :refer [mask-str mask-factory]]))
|
[app.main.ui.shapes.mask :refer [mask-str mask-factory]]))
|
||||||
|
|
||||||
(defn group-shape
|
(defn group-shape
|
||||||
|
@ -28,12 +30,14 @@
|
||||||
|
|
||||||
show-mask? (and (:masked-group? shape) (not expand-mask))
|
show-mask? (and (:masked-group? shape) (not expand-mask))
|
||||||
mask (when show-mask? (first childs))
|
mask (when show-mask? (first childs))
|
||||||
childs (if show-mask? (rest childs) childs)]
|
childs (if show-mask? (rest childs) childs)
|
||||||
|
|
||||||
[:g.group
|
props (-> (attrs/extract-style-attrs shape)
|
||||||
{:pointer-events pointer-events
|
(obj/merge!
|
||||||
:mask (when (and mask (not expand-mask)) (mask-str mask))}
|
#js {:pointerEvents pointer-events
|
||||||
|
:mask (when (and mask (not expand-mask)) (mask-str mask))}))]
|
||||||
|
|
||||||
|
[:> :g props
|
||||||
(when mask
|
(when mask
|
||||||
[:> render-mask #js {:frame frame :mask mask}])
|
[:> render-mask #js {:frame frame :mask mask}])
|
||||||
|
|
||||||
|
|
|
@ -9,13 +9,12 @@
|
||||||
|
|
||||||
(ns app.main.ui.shapes.shape
|
(ns app.main.ui.shapes.shape
|
||||||
(:require
|
(:require
|
||||||
[app.common.geom.shapes :as geom]
|
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
[app.main.ui.context :as muc]
|
[app.main.ui.context :as muc]
|
||||||
[app.main.ui.shapes.filters :as filters]
|
[app.main.ui.shapes.filters :as filters]
|
||||||
[app.main.ui.shapes.gradients :as grad]
|
[app.main.ui.shapes.gradients :as grad]
|
||||||
|
[app.main.ui.shapes.svg-defs :as defs]
|
||||||
[app.util.object :as obj]
|
[app.util.object :as obj]
|
||||||
[cuerdas.core :as str]
|
|
||||||
[rumext.alpha :as mf]))
|
[rumext.alpha :as mf]))
|
||||||
|
|
||||||
(mf/defc shape-container
|
(mf/defc shape-container
|
||||||
|
@ -48,6 +47,7 @@
|
||||||
[:& (mf/provider muc/render-ctx) {:value render-id}
|
[:& (mf/provider muc/render-ctx) {:value render-id}
|
||||||
[:> wrapper-tag group-props
|
[:> wrapper-tag group-props
|
||||||
[:defs
|
[:defs
|
||||||
|
[:& defs/svg-defs {:shape shape :render-id render-id}]
|
||||||
[:& filters/filters {:shape shape :filter-id filter-id}]
|
[:& filters/filters {:shape shape :filter-id filter-id}]
|
||||||
[:& grad/gradient {:shape shape :attr :fill-color-gradient}]
|
[:& grad/gradient {:shape shape :attr :fill-color-gradient}]
|
||||||
[:& grad/gradient {:shape shape :attr :stroke-color-gradient}]]
|
[:& grad/gradient {:shape shape :attr :stroke-color-gradient}]]
|
||||||
|
|
107
frontend/src/app/main/ui/shapes/svg_defs.cljs
Normal file
107
frontend/src/app/main/ui/shapes/svg_defs.cljs
Normal 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}]))))
|
||||||
|
|
|
@ -13,7 +13,6 @@
|
||||||
[app.common.geom.matrix :as gmt]
|
[app.common.geom.matrix :as gmt]
|
||||||
[app.common.geom.point :as gpt]
|
[app.common.geom.point :as gpt]
|
||||||
[app.common.geom.shapes :as gsh]
|
[app.common.geom.shapes :as gsh]
|
||||||
[app.common.uuid :as uuid]
|
|
||||||
[app.main.ui.shapes.attrs :as usa]
|
[app.main.ui.shapes.attrs :as usa]
|
||||||
[app.util.data :as ud]
|
[app.util.data :as ud]
|
||||||
[app.util.object :as obj]
|
[app.util.object :as obj]
|
||||||
|
@ -21,35 +20,15 @@
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[rumext.alpha :as mf]))
|
[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
|
;; Context to store a re-mapping of the ids
|
||||||
(def svg-ids-ctx (mf/create-context nil))
|
(def svg-ids-ctx (mf/create-context nil))
|
||||||
|
|
||||||
(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]
|
(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
|
attrs (cond-> attrs
|
||||||
(string? (:style attrs)) usvg/clean-attrs)
|
(string? (:style attrs)) usvg/clean-attrs)
|
||||||
style (obj/merge! (clj->js (:style attrs {}))
|
style (obj/merge! (clj->js (:style attrs {}))
|
||||||
|
@ -58,6 +37,14 @@
|
||||||
(obj/merge! custom-attrs)
|
(obj/merge! custom-attrs)
|
||||||
(obj/set! "style" style))))
|
(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/defc svg-root
|
||||||
{::mf/wrap-props false}
|
{::mf/wrap-props false}
|
||||||
[props]
|
[props]
|
||||||
|
@ -68,7 +55,7 @@
|
||||||
{:keys [x y width height]} shape
|
{:keys [x y width height]} shape
|
||||||
{:keys [tag attrs] :as content} (:content 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)
|
attrs (-> (set-styles attrs shape)
|
||||||
(obj/set! "x" x)
|
(obj/set! "x" x)
|
||||||
|
@ -91,11 +78,16 @@
|
||||||
{:keys [attrs tag]} content
|
{:keys [attrs tag]} content
|
||||||
|
|
||||||
ids-mapping (mf/use-ctx svg-ids-ctx)
|
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)
|
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]))
|
[:> (name tag) attrs children]))
|
||||||
|
|
||||||
(defn svg-raw-shape [shape-wrapper]
|
(defn svg-raw-shape [shape-wrapper]
|
||||||
|
|
|
@ -44,8 +44,6 @@
|
||||||
:style {:stroke color
|
:style {:stroke color
|
||||||
:fill "transparent"
|
:fill "transparent"
|
||||||
:stroke-width "1px"
|
:stroke-width "1px"
|
||||||
:stroke-opacity 0.5
|
|
||||||
:stroke-dasharray 4
|
|
||||||
:pointer-events "none"}}])
|
:pointer-events "none"}}])
|
||||||
|
|
||||||
(mf/defc render-rect-points [{:keys [points color]}]
|
(mf/defc render-rect-points [{:keys [points color]}]
|
||||||
|
@ -62,7 +60,7 @@
|
||||||
[props]
|
[props]
|
||||||
(let [shape (-> (unchecked-get props "shape"))
|
(let [shape (-> (unchecked-get props "shape"))
|
||||||
frame (unchecked-get props "frame")
|
frame (unchecked-get props "frame")
|
||||||
selrect (gsh/points->selrect (-> shape :points))
|
bounding-box (gsh/points->selrect (-> shape :points))
|
||||||
shape-center (gsh/center-shape shape)
|
shape-center (gsh/center-shape shape)
|
||||||
line-color (rdcolor #js {:seed (str (:id shape))})
|
line-color (rdcolor #js {:seed (str (:id shape))})
|
||||||
zoom (mf/deref refs/selected-zoom)
|
zoom (mf/deref refs/selected-zoom)
|
||||||
|
@ -71,25 +69,30 @@
|
||||||
(map gsh/transform-shape))]
|
(map gsh/transform-shape))]
|
||||||
|
|
||||||
[:g.bounding-box
|
[:g.bounding-box
|
||||||
[:text {:x (:x selrect)
|
[:text {:x (:x bounding-box)
|
||||||
:y (- (:y selrect) 5)
|
:y (- (:y bounding-box) 5)
|
||||||
:font-size 10
|
:font-size 10
|
||||||
:fill line-color
|
:fill line-color
|
||||||
:stroke "white"
|
:stroke "white"
|
||||||
:stroke-width 0.1}
|
: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)))]
|
||||||
|
|
||||||
|
[:g.center
|
||||||
[:& cross-point {:point shape-center
|
[:& cross-point {:point shape-center
|
||||||
:zoom zoom
|
:zoom zoom
|
||||||
:color line-color}]
|
:color line-color}]]
|
||||||
|
|
||||||
|
[:g.points
|
||||||
(for [point (:points shape)]
|
(for [point (:points shape)]
|
||||||
[:& cross-point {:point point
|
[:& cross-point {:point point
|
||||||
:zoom zoom
|
:zoom zoom
|
||||||
:color line-color}])
|
:color line-color}])
|
||||||
|
#_[:& render-rect-points {:points (:points shape)
|
||||||
|
:color line-color}]]
|
||||||
|
|
||||||
[:& render-rect-points {:points (:points shape)
|
[:g.selrect
|
||||||
|
[:& render-rect {:rect (:selrect shape)
|
||||||
|
;; :transform (gsh/transform-matrix shape)
|
||||||
:color line-color}]
|
:color line-color}]
|
||||||
|
#_[:& render-rect {:rect bounding-box
|
||||||
[:& render-rect {:rect selrect
|
:color line-color}]]]))
|
||||||
:color line-color}]]))
|
|
||||||
|
|
|
@ -33,7 +33,6 @@
|
||||||
{::mf/wrap-props false}
|
{::mf/wrap-props false}
|
||||||
[props]
|
[props]
|
||||||
(let [shape (unchecked-get props "shape")
|
(let [shape (unchecked-get props "shape")
|
||||||
hover? (or (mf/deref refs/current-hover) #{})
|
|
||||||
content-modifiers-ref (pc/make-content-modifiers-ref (:id shape))
|
content-modifiers-ref (pc/make-content-modifiers-ref (:id shape))
|
||||||
content-modifiers (mf/deref content-modifiers-ref)
|
content-modifiers (mf/deref content-modifiers-ref)
|
||||||
editing-id (mf/deref refs/selected-edition)
|
editing-id (mf/deref refs/selected-edition)
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
|
|
||||||
;; This is a list of svg tags that can be grouped in shape-container
|
;; This is a list of svg tags that can be grouped in shape-container
|
||||||
;; this allows them to have gradients, shadows and masks
|
;; this allows them to have gradients, shadows and masks
|
||||||
(def svg-elements #{:svg :circle :ellipse :image :line :path :polygon :polyline :rect :symbol :text :textPath})
|
(def svg-elements #{:svg :circle :ellipse :image :line :path :polygon :polyline :rect :symbol :text :textPath :use})
|
||||||
|
|
||||||
(defn svg-raw-wrapper-factory
|
(defn svg-raw-wrapper-factory
|
||||||
[shape-wrapper]
|
[shape-wrapper]
|
||||||
|
@ -57,19 +57,19 @@
|
||||||
:shape shape
|
:shape shape
|
||||||
:childs childs}]
|
:childs childs}]
|
||||||
|
|
||||||
(when (= tag :svg)
|
[:rect.actions
|
||||||
[:rect.group-actions
|
|
||||||
{:x x
|
{:x x
|
||||||
:y y
|
:y y
|
||||||
:transform transform
|
:transform transform
|
||||||
:width width
|
:width width
|
||||||
:height height
|
:height height
|
||||||
:fill "transparent"
|
:fill "transparent"
|
||||||
|
:stroke "none"
|
||||||
:on-mouse-down handle-mouse-down
|
:on-mouse-down handle-mouse-down
|
||||||
:on-double-click handle-double-click
|
:on-double-click handle-double-click
|
||||||
:on-context-menu handle-context-menu
|
:on-context-menu handle-context-menu
|
||||||
:on-pointer-over handle-pointer-enter
|
:on-pointer-over handle-pointer-enter
|
||||||
:on-pointer-out handle-pointer-leave}])]
|
:on-pointer-out handle-pointer-leave}]]
|
||||||
|
|
||||||
;; We cannot wrap inside groups the shapes that go inside the defs tag
|
;; 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
|
;; we use the context so we know when we should not render the container
|
||||||
|
|
|
@ -14,7 +14,8 @@
|
||||||
[app.main.ui.workspace.sidebar.options.fill :refer [fill-attrs fill-menu]]
|
[app.main.ui.workspace.sidebar.options.fill :refer [fill-attrs fill-menu]]
|
||||||
[app.main.ui.workspace.sidebar.options.stroke :refer [stroke-attrs stroke-menu]]
|
[app.main.ui.workspace.sidebar.options.stroke :refer [stroke-attrs stroke-menu]]
|
||||||
[app.main.ui.workspace.sidebar.options.shadow :refer [shadow-menu]]
|
[app.main.ui.workspace.sidebar.options.shadow :refer [shadow-menu]]
|
||||||
[app.main.ui.workspace.sidebar.options.blur :refer [blur-menu]]))
|
[app.main.ui.workspace.sidebar.options.blur :refer [blur-menu]]
|
||||||
|
[app.main.ui.workspace.sidebar.options.svg-attrs :refer [svg-attrs-menu]]))
|
||||||
|
|
||||||
(mf/defc options
|
(mf/defc options
|
||||||
[{:keys [shape] :as props}]
|
[{:keys [shape] :as props}]
|
||||||
|
@ -36,4 +37,6 @@
|
||||||
[:& shadow-menu {:ids ids
|
[:& shadow-menu {:ids ids
|
||||||
:values (select-keys shape [:shadow])}]
|
:values (select-keys shape [:shadow])}]
|
||||||
[:& blur-menu {:ids ids
|
[:& blur-menu {:ids ids
|
||||||
:values (select-keys shape [:blur])}]]))
|
:values (select-keys shape [:blur])}]
|
||||||
|
[:& svg-attrs-menu {:ids ids
|
||||||
|
:values (select-keys shape [:svg-attrs])}]]))
|
||||||
|
|
|
@ -19,7 +19,8 @@
|
||||||
[app.main.ui.workspace.sidebar.options.blur :refer [blur-menu]]
|
[app.main.ui.workspace.sidebar.options.blur :refer [blur-menu]]
|
||||||
[app.main.ui.workspace.sidebar.options.shadow :refer [shadow-menu]]
|
[app.main.ui.workspace.sidebar.options.shadow :refer [shadow-menu]]
|
||||||
[app.main.ui.workspace.sidebar.options.stroke :refer [stroke-menu]]
|
[app.main.ui.workspace.sidebar.options.stroke :refer [stroke-menu]]
|
||||||
[app.main.ui.workspace.sidebar.options.text :as ot]))
|
[app.main.ui.workspace.sidebar.options.text :as ot]
|
||||||
|
[app.main.ui.workspace.sidebar.options.svg-attrs :refer [svg-attrs-menu]]))
|
||||||
|
|
||||||
(mf/defc options
|
(mf/defc options
|
||||||
{::mf/wrap [mf/memo]
|
{::mf/wrap [mf/memo]
|
||||||
|
@ -36,6 +37,7 @@
|
||||||
[blur-ids blur-values] (get-attrs [shape] objects :blur)
|
[blur-ids blur-values] (get-attrs [shape] objects :blur)
|
||||||
[stroke-ids stroke-values] (get-attrs [shape] objects :stroke)
|
[stroke-ids stroke-values] (get-attrs [shape] objects :stroke)
|
||||||
[text-ids text-values] (get-attrs [shape] objects :text)
|
[text-ids text-values] (get-attrs [shape] objects :text)
|
||||||
|
[svg-ids svg-values] [[(:id shape)] (select-keys shape [:svg-attrs])]
|
||||||
[comp-ids comp-values] [[(:id shape)] (select-keys shape component-attrs)]]
|
[comp-ids comp-values] [[(:id shape)] (select-keys shape component-attrs)]]
|
||||||
|
|
||||||
[:div.options
|
[:div.options
|
||||||
|
@ -55,6 +57,10 @@
|
||||||
[:& stroke-menu {:type type :ids stroke-ids :values stroke-values}])
|
[:& stroke-menu {:type type :ids stroke-ids :values stroke-values}])
|
||||||
|
|
||||||
(when-not (empty? text-ids)
|
(when-not (empty? text-ids)
|
||||||
[:& ot/text-menu {:type type :ids text-ids :values text-values}])]))
|
[:& ot/text-menu {:type type :ids text-ids :values text-values}])
|
||||||
|
|
||||||
|
(when-not (empty? svg-values)
|
||||||
|
[:& svg-attrs-menu {:ids svg-ids
|
||||||
|
:values svg-values}])]))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,8 @@
|
||||||
[app.main.ui.workspace.sidebar.options.fill :refer [fill-attrs fill-menu]]
|
[app.main.ui.workspace.sidebar.options.fill :refer [fill-attrs fill-menu]]
|
||||||
[app.main.ui.workspace.sidebar.options.stroke :refer [stroke-attrs stroke-menu]]
|
[app.main.ui.workspace.sidebar.options.stroke :refer [stroke-attrs stroke-menu]]
|
||||||
[app.main.ui.workspace.sidebar.options.shadow :refer [shadow-menu]]
|
[app.main.ui.workspace.sidebar.options.shadow :refer [shadow-menu]]
|
||||||
[app.main.ui.workspace.sidebar.options.blur :refer [blur-menu]]))
|
[app.main.ui.workspace.sidebar.options.blur :refer [blur-menu]]
|
||||||
|
[app.main.ui.workspace.sidebar.options.svg-attrs :refer [svg-attrs-menu]]))
|
||||||
|
|
||||||
(mf/defc options
|
(mf/defc options
|
||||||
[{:keys [shape] :as props}]
|
[{:keys [shape] :as props}]
|
||||||
|
@ -36,4 +37,6 @@
|
||||||
[:& shadow-menu {:ids ids
|
[:& shadow-menu {:ids ids
|
||||||
:values (select-keys shape [:shadow])}]
|
:values (select-keys shape [:shadow])}]
|
||||||
[:& blur-menu {:ids ids
|
[:& blur-menu {:ids ids
|
||||||
:values (select-keys shape [:blur])}]]))
|
:values (select-keys shape [:blur])}]
|
||||||
|
[:& svg-attrs-menu {:ids ids
|
||||||
|
:values (select-keys shape [:svg-attrs])}]]))
|
||||||
|
|
|
@ -14,7 +14,8 @@
|
||||||
[app.main.ui.workspace.sidebar.options.fill :refer [fill-attrs fill-menu]]
|
[app.main.ui.workspace.sidebar.options.fill :refer [fill-attrs fill-menu]]
|
||||||
[app.main.ui.workspace.sidebar.options.stroke :refer [stroke-attrs stroke-menu]]
|
[app.main.ui.workspace.sidebar.options.stroke :refer [stroke-attrs stroke-menu]]
|
||||||
[app.main.ui.workspace.sidebar.options.shadow :refer [shadow-menu]]
|
[app.main.ui.workspace.sidebar.options.shadow :refer [shadow-menu]]
|
||||||
[app.main.ui.workspace.sidebar.options.blur :refer [blur-menu]]))
|
[app.main.ui.workspace.sidebar.options.blur :refer [blur-menu]]
|
||||||
|
[app.main.ui.workspace.sidebar.options.svg-attrs :refer [svg-attrs-menu]]))
|
||||||
|
|
||||||
(mf/defc options
|
(mf/defc options
|
||||||
{::mf/wrap [mf/memo]}
|
{::mf/wrap [mf/memo]}
|
||||||
|
@ -41,4 +42,7 @@
|
||||||
:values (select-keys shape [:shadow])}]
|
:values (select-keys shape [:shadow])}]
|
||||||
|
|
||||||
[:& blur-menu {:ids ids
|
[:& blur-menu {:ids ids
|
||||||
:values (select-keys shape [:blur])}]]))
|
:values (select-keys shape [:blur])}]
|
||||||
|
|
||||||
|
[:& svg-attrs-menu {:ids ids
|
||||||
|
:values (select-keys shape [:svg-attrs])}]]))
|
||||||
|
|
|
@ -36,6 +36,11 @@
|
||||||
:placeholder placeholder
|
:placeholder placeholder
|
||||||
:on-change on-change}]
|
:on-change on-change}]
|
||||||
|
|
||||||
|
:text
|
||||||
|
[:input {:value value
|
||||||
|
:class "input-text"
|
||||||
|
:on-change on-change} ]
|
||||||
|
|
||||||
[:> numeric-input {:placeholder placeholder
|
[:> numeric-input {:placeholder placeholder
|
||||||
:min min
|
:min min
|
||||||
:max max
|
:max max
|
||||||
|
|
|
@ -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}])])))
|
|
@ -17,11 +17,12 @@
|
||||||
[app.main.ui.workspace.sidebar.options.fill :refer [fill-attrs fill-menu]]
|
[app.main.ui.workspace.sidebar.options.fill :refer [fill-attrs fill-menu]]
|
||||||
[app.main.ui.workspace.sidebar.options.stroke :refer [stroke-attrs stroke-menu]]
|
[app.main.ui.workspace.sidebar.options.stroke :refer [stroke-attrs stroke-menu]]
|
||||||
[app.main.ui.workspace.sidebar.options.shadow :refer [shadow-menu]]
|
[app.main.ui.workspace.sidebar.options.shadow :refer [shadow-menu]]
|
||||||
[app.main.ui.workspace.sidebar.options.blur :refer [blur-menu]]))
|
[app.main.ui.workspace.sidebar.options.blur :refer [blur-menu]]
|
||||||
|
[app.main.ui.workspace.sidebar.options.svg-attrs :refer [svg-attrs-menu]]))
|
||||||
|
|
||||||
;; This is a list of svg tags that can be grouped in shape-container
|
;; This is a list of svg tags that can be grouped in shape-container
|
||||||
;; this allows them to have gradients, shadows and masks
|
;; this allows them to have gradients, shadows and masks
|
||||||
(def svg-elements #{:svg :circle :ellipse :image :line :path :polygon :polyline :rect :symbol :text :textPath})
|
(def svg-elements #{:svg :g :circle :ellipse :image :line :path :polygon :polyline :rect :symbol :text :textPath})
|
||||||
|
|
||||||
(defn hex->number [hex] 1)
|
(defn hex->number [hex] 1)
|
||||||
(defn shorthex->longhex [hex]
|
(defn shorthex->longhex [hex]
|
||||||
|
@ -98,12 +99,10 @@
|
||||||
stroke-values (get-stroke-values shape)]
|
stroke-values (get-stroke-values shape)]
|
||||||
|
|
||||||
(when (contains? svg-elements tag)
|
(when (contains? svg-elements tag)
|
||||||
[:*
|
|
||||||
(when (= tag :svg)
|
|
||||||
[:*
|
[:*
|
||||||
[:& measures-menu {:ids ids
|
[:& measures-menu {:ids ids
|
||||||
:type type
|
:type type
|
||||||
:values measure-values}]])
|
:values measure-values}]
|
||||||
|
|
||||||
[:& fill-menu {:ids ids
|
[:& fill-menu {:ids ids
|
||||||
:type type
|
:type type
|
||||||
|
@ -115,4 +114,7 @@
|
||||||
:values (select-keys shape [:shadow])}]
|
:values (select-keys shape [:shadow])}]
|
||||||
|
|
||||||
[:& blur-menu {:ids ids
|
[:& blur-menu {:ids ids
|
||||||
:values (select-keys shape [:blur])}]])))
|
:values (select-keys shape [:blur])}]
|
||||||
|
|
||||||
|
[:& svg-attrs-menu {:ids ids
|
||||||
|
:values (select-keys shape [:svg-attrs])}]])))
|
||||||
|
|
203
frontend/src/app/util/a2c.js
Normal file
203
frontend/src/app/util/a2c.js
Normal 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;
|
||||||
|
});
|
|
@ -116,6 +116,11 @@
|
||||||
(= id :multiple)
|
(= id :multiple)
|
||||||
(= file-id :multiple)))
|
(= file-id :multiple)))
|
||||||
|
|
||||||
|
(defn color? [^string color-str]
|
||||||
|
(and (not (nil? color-str))
|
||||||
|
(not (empty? color-str))
|
||||||
|
(gcolor/isValidColor color-str)))
|
||||||
|
|
||||||
(defn parse-color [^string color-str]
|
(defn parse-color [^string color-str]
|
||||||
(let [result (gcolor/parse color-str)]
|
(let [result (gcolor/parse color-str)]
|
||||||
(str (.-hex ^js result))))
|
(str (.-hex ^js result))))
|
||||||
|
|
|
@ -9,12 +9,22 @@
|
||||||
|
|
||||||
(ns app.util.geom.path
|
(ns app.util.geom.path
|
||||||
(:require
|
(:require
|
||||||
[cuerdas.core :as str]
|
|
||||||
[app.common.data :as cd]
|
[app.common.data :as cd]
|
||||||
[app.util.data :as d]
|
|
||||||
[app.common.data :as cd]
|
[app.common.data :as cd]
|
||||||
[app.common.geom.point :as gpt]
|
[app.common.geom.point :as gpt]
|
||||||
[app.util.geom.path-impl-simplify :as impl-simplify]))
|
[app.util.a2c :refer [a2c]]
|
||||||
|
[app.util.data :as d]
|
||||||
|
[app.util.geom.path-impl-simplify :as impl-simplify]
|
||||||
|
[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
|
(defn simplify
|
||||||
([points]
|
([points]
|
||||||
|
@ -28,23 +38,37 @@
|
||||||
|
|
||||||
;; Matches numbers for path values allows values like... -.01, 10, +12.22
|
;; Matches numbers for path values allows values like... -.01, 10, +12.22
|
||||||
;; 0 and 1 are special because can refer to flags
|
;; 0 and 1 are special because can refer to flags
|
||||||
(def num-regex #"([+-]?(([1-9]\d*(\.\d+)?)|(\.\d+)|0|1))")
|
(def num-regex #"[+-]?(\d+(\.\d+)?|\.\d+)")
|
||||||
|
|
||||||
|
(def flag-regex #"[01]")
|
||||||
|
|
||||||
(defn coord-n [size]
|
(defn extract-params [cmd-str extract-commands]
|
||||||
(re-pattern (str "(?i)[a-z]\\s*"
|
(loop [result []
|
||||||
(->> (range size)
|
extract-idx 0
|
||||||
(map #(identity num-regex))
|
current {}
|
||||||
(str/join "\\s+")))))
|
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]
|
(if match
|
||||||
(let [fix-starting-dot (fn [arg] (str/replace arg #"([^\d]|^)\." "$10."))]
|
(let [value (-> match first usvg/fix-dot-number d/read-string)
|
||||||
(->> (re-seq num-regex cmd-str)
|
remain (str/replace-first remain regex "")
|
||||||
(map first)
|
current (assoc current param value)
|
||||||
(map fix-starting-dot)
|
extract-idx (inc extract-idx)
|
||||||
(map d/read-string)
|
[result current extract-idx]
|
||||||
(partition num-params))))
|
(if (>= extract-idx (count extract-commands))
|
||||||
|
[(conj result current) {} 0]
|
||||||
|
[result current extract-idx])]
|
||||||
|
(recur result
|
||||||
|
extract-idx
|
||||||
|
current
|
||||||
|
remain))
|
||||||
|
(cond-> result
|
||||||
|
(not (empty? current)) (conj current))))))
|
||||||
|
|
||||||
(defn command->param-list [{:keys [command params]}]
|
(defn command->param-list [{:keys [command params]}]
|
||||||
(case command
|
(case command
|
||||||
|
@ -73,96 +97,99 @@
|
||||||
|
|
||||||
(defmethod parse-command "M" [cmd]
|
(defmethod parse-command "M" [cmd]
|
||||||
(let [relative (str/starts-with? cmd "m")
|
(let [relative (str/starts-with? cmd "m")
|
||||||
params (parse-params cmd 2)]
|
param-list (extract-params cmd [[:x :number]
|
||||||
(for [[x y] params]
|
[:y :number]])]
|
||||||
|
(for [params param-list]
|
||||||
{:command :move-to
|
{:command :move-to
|
||||||
:relative relative
|
:relative relative
|
||||||
:params {:x x :y y}})))
|
:params params})))
|
||||||
|
|
||||||
(defmethod parse-command "Z" [cmd]
|
(defmethod parse-command "Z" [cmd]
|
||||||
[{:command :close-path}])
|
[{:command :close-path}])
|
||||||
|
|
||||||
(defmethod parse-command "L" [cmd]
|
(defmethod parse-command "L" [cmd]
|
||||||
(let [relative (str/starts-with? cmd "l")
|
(let [relative (str/starts-with? cmd "l")
|
||||||
params (parse-params cmd 2)]
|
param-list (extract-params cmd [[:x :number]
|
||||||
(for [[x y] params]
|
[:y :number]])]
|
||||||
|
(for [params param-list]
|
||||||
{:command :line-to
|
{:command :line-to
|
||||||
:relative relative
|
:relative relative
|
||||||
:params {:x x :y y}})))
|
:params params})))
|
||||||
|
|
||||||
(defmethod parse-command "H" [cmd]
|
(defmethod parse-command "H" [cmd]
|
||||||
(let [relative (str/starts-with? cmd "h")
|
(let [relative (str/starts-with? cmd "h")
|
||||||
params (parse-params cmd 1)]
|
param-list (extract-params cmd [[:value :number]])]
|
||||||
(for [[value] params]
|
(for [params param-list]
|
||||||
{:command :line-to-horizontal
|
{:command :line-to-horizontal
|
||||||
:relative relative
|
:relative relative
|
||||||
:params {:value value}})))
|
:params params})))
|
||||||
|
|
||||||
(defmethod parse-command "V" [cmd]
|
(defmethod parse-command "V" [cmd]
|
||||||
(let [relative (str/starts-with? cmd "v")
|
(let [relative (str/starts-with? cmd "v")
|
||||||
params (parse-params cmd 1)]
|
param-list (extract-params cmd [[:value :number]])]
|
||||||
(for [[value] params]
|
(for [params param-list]
|
||||||
{:command :line-to-vertical
|
{:command :line-to-vertical
|
||||||
:relative relative
|
:relative relative
|
||||||
:params {:value value}})))
|
:params params})))
|
||||||
|
|
||||||
(defmethod parse-command "C" [cmd]
|
(defmethod parse-command "C" [cmd]
|
||||||
(let [relative (str/starts-with? cmd "c")
|
(let [relative (str/starts-with? cmd "c")
|
||||||
params (parse-params cmd 6)]
|
param-list (extract-params cmd [[:c1x :number]
|
||||||
(for [[c1x c1y c2x c2y x y] params]
|
[:c1y :number]
|
||||||
|
[:c2x :number]
|
||||||
|
[:c2y :number]
|
||||||
|
[:x :number]
|
||||||
|
[:y :number]])
|
||||||
|
]
|
||||||
|
(for [params param-list]
|
||||||
{:command :curve-to
|
{:command :curve-to
|
||||||
:relative relative
|
:relative relative
|
||||||
:params {:c1x c1x
|
:params params})))
|
||||||
:c1y c1y
|
|
||||||
:c2x c2x
|
|
||||||
:c2y c2y
|
|
||||||
:x x
|
|
||||||
:y y}})))
|
|
||||||
|
|
||||||
(defmethod parse-command "S" [cmd]
|
(defmethod parse-command "S" [cmd]
|
||||||
(let [relative (str/starts-with? cmd "s")
|
(let [relative (str/starts-with? cmd "s")
|
||||||
params (parse-params cmd 4)]
|
param-list (extract-params cmd [[:cx :number]
|
||||||
(for [[cx cy x y] params]
|
[:cy :number]
|
||||||
|
[:x :number]
|
||||||
|
[:y :number]])]
|
||||||
|
(for [params param-list]
|
||||||
{:command :smooth-curve-to
|
{:command :smooth-curve-to
|
||||||
:relative relative
|
:relative relative
|
||||||
:params {:cx cx
|
:params params})))
|
||||||
:cy cy
|
|
||||||
:x x
|
|
||||||
:y y}})))
|
|
||||||
|
|
||||||
(defmethod parse-command "Q" [cmd]
|
(defmethod parse-command "Q" [cmd]
|
||||||
(let [relative (str/starts-with? cmd "s")
|
(let [relative (str/starts-with? cmd "s")
|
||||||
params (parse-params cmd 4)]
|
param-list (extract-params cmd [[:cx :number]
|
||||||
(for [[cx cy x y] params]
|
[:cy :number]
|
||||||
|
[:x :number]
|
||||||
|
[:y :number]])]
|
||||||
|
(for [params param-list]
|
||||||
{:command :quadratic-bezier-curve-to
|
{:command :quadratic-bezier-curve-to
|
||||||
:relative relative
|
:relative relative
|
||||||
:params {:cx cx
|
:params params})))
|
||||||
:cy cy
|
|
||||||
:x x
|
|
||||||
:y y}})))
|
|
||||||
|
|
||||||
(defmethod parse-command "T" [cmd]
|
(defmethod parse-command "T" [cmd]
|
||||||
(let [relative (str/starts-with? cmd "t")
|
(let [relative (str/starts-with? cmd "t")
|
||||||
params (parse-params cmd (coord-n 2))]
|
param-list (extract-params cmd [[:x :number]
|
||||||
(for [[cx cy x y] params]
|
[:y :number]])]
|
||||||
|
(for [params param-list]
|
||||||
{:command :smooth-quadratic-bezier-curve-to
|
{:command :smooth-quadratic-bezier-curve-to
|
||||||
:relative relative
|
:relative relative
|
||||||
:params {:x x
|
:params params})))
|
||||||
:y y}})))
|
|
||||||
|
|
||||||
(defmethod parse-command "A" [cmd]
|
(defmethod parse-command "A" [cmd]
|
||||||
(let [relative (str/starts-with? cmd "a")
|
(let [relative (str/starts-with? cmd "a")
|
||||||
params (parse-params cmd 7)]
|
param-list (extract-params cmd [[:rx :number]
|
||||||
(for [[rx ry x-axis-rotation large-arc-flag sweep-flag x y] params]
|
[:ry :number]
|
||||||
|
[:x-axis-rotation :number]
|
||||||
|
[:large-arc-flag :flag]
|
||||||
|
[:sweep-flag :flag]
|
||||||
|
[:x :number]
|
||||||
|
[:y :number]])]
|
||||||
|
(for [params param-list]
|
||||||
{:command :elliptical-arc
|
{:command :elliptical-arc
|
||||||
:relative relative
|
:relative relative
|
||||||
:params {:rx rx
|
:params params})))
|
||||||
:ry ry
|
|
||||||
:x-axis-rotation x-axis-rotation
|
|
||||||
:large-arc-flag large-arc-flag
|
|
||||||
:sweep-flag sweep-flag
|
|
||||||
:x x
|
|
||||||
:y y}})))
|
|
||||||
|
|
||||||
(defn command->string [{:keys [command relative params] :as entry}]
|
(defn command->string [{:keys [command relative params] :as entry}]
|
||||||
(let [command-str (case command
|
(let [command-str (case command
|
||||||
|
@ -180,46 +207,143 @@
|
||||||
param-list (command->param-list entry)]
|
param-list (command->param-list entry)]
|
||||||
(str/fmt "%s%s" command-str (str/join " " param-list))))
|
(str/fmt "%s%s" command-str (str/join " " param-list))))
|
||||||
|
|
||||||
(defn cmd-pos [{:keys [params]}]
|
(defn cmd-pos [prev-pos {:keys [relative params]}]
|
||||||
(when (and (contains? params :x)
|
(let [{:keys [x y] :or {x (:x prev-pos) y (:y prev-pos)}} params]
|
||||||
(contains? params :y))
|
(if relative
|
||||||
(gpt/point params)))
|
(-> 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
|
(defn simplify-commands
|
||||||
"Removes some commands and convert relative to absolute coordinates"
|
"Removes some commands and convert relative to absolute coordinates"
|
||||||
[commands]
|
[commands]
|
||||||
|
|
||||||
(let [simplify-command
|
(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
|
(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
|
(cond-> command
|
||||||
(= :line-to-horizontal (:command command))
|
(= :line-to-horizontal (:command command))
|
||||||
(-> (assoc :command :line-to)
|
(-> (assoc :command :line-to)
|
||||||
(update :params dissoc :value)
|
(update :params dissoc :value)
|
||||||
(assoc-in [:params :x] (get-in command [:params :value]))
|
(assoc-in [:params :x] (:value params))
|
||||||
(assoc-in [:params :y] (if (:relative command) 0 (:y pos))))
|
(assoc-in [:params :y] (:y pos)))
|
||||||
|
|
||||||
(= :line-to-vertical (:command command))
|
(= :line-to-vertical (:command command))
|
||||||
(-> (assoc :command :line-to)
|
(-> (assoc :command :line-to)
|
||||||
(update :params dissoc :value)
|
(update :params dissoc :value)
|
||||||
(assoc-in [:params :y] (get-in command [:params :value]))
|
(assoc-in [:params :y] (:value params))
|
||||||
(assoc-in [:params :x] (if (:relative command) 0 (:x pos))))
|
(assoc-in [:params :x] (:x pos)))
|
||||||
|
|
||||||
(:relative command)
|
(= :smooth-curve-to (:command command))
|
||||||
(-> (assoc :relative false)
|
(-> (assoc :command :curve-to)
|
||||||
(cd/update-in-when [:params :x] + (:x pos))
|
(update :params dissoc :cx :cy)
|
||||||
(cd/update-in-when [:params :y] + (:y pos)))
|
(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)))))
|
||||||
|
|
||||||
)]
|
(= :smooth-quadratic-bezier-curve-to (:command command))
|
||||||
[(cmd-pos command) (conj result 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 (first commands)
|
||||||
start-pos (cmd-pos start)]
|
start-pos (gpt/point (:params start))]
|
||||||
|
|
||||||
|
|
||||||
(->> (map vector (rest commands) commands)
|
(->> (map vector (rest commands) commands)
|
||||||
(reduce simplify-command [start-pos [start]])
|
(reduce simplify-command [start-pos [start] start-pos start-pos])
|
||||||
(second))))
|
(second))))
|
||||||
|
|
||||||
(defn path->content [string]
|
(defn path->content [string]
|
||||||
|
|
|
@ -9,13 +9,61 @@
|
||||||
|
|
||||||
(ns app.util.svg
|
(ns app.util.svg
|
||||||
(:require
|
(: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]))
|
[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
|
(defn clean-attrs
|
||||||
"Transforms attributes to their react equivalent"
|
"Transforms attributes to their react equivalent"
|
||||||
[attrs]
|
[attrs]
|
||||||
(letfn [(transform-key [key]
|
(letfn [(transform-key [key]
|
||||||
(-> (name key)
|
(-> (d/name key)
|
||||||
(str/replace ":" "-")
|
(str/replace ":" "-")
|
||||||
(str/camel)
|
(str/camel)
|
||||||
(keyword)))
|
(keyword)))
|
||||||
|
@ -32,11 +80,468 @@
|
||||||
(into {})))
|
(into {})))
|
||||||
|
|
||||||
(map-fn [[key val]]
|
(map-fn [[key val]]
|
||||||
|
(let [key (keyword key)]
|
||||||
(cond
|
(cond
|
||||||
(= key :class) [:className val]
|
(= key :class) [:className val]
|
||||||
(and (= key :style) (string? val)) [key (format-styles val)]
|
(and (= key :style) (string? val)) [key (format-styles val)]
|
||||||
:else (vector (transform-key key) val)))]
|
(and (= key :style) (map? val)) [key (clean-attrs val)]
|
||||||
|
:else (vector (transform-key key) val))))]
|
||||||
|
|
||||||
(->> attrs
|
(->> attrs
|
||||||
(map map-fn)
|
(map map-fn)
|
||||||
(into {}))))
|
(into {}))))
|
||||||
|
|
||||||
|
(defn update-attr-ids
|
||||||
|
"Replaces the ids inside a property"
|
||||||
|
[attrs replace-fn]
|
||||||
|
(letfn [(update-ids [key val]
|
||||||
|
(cond
|
||||||
|
(map? val)
|
||||||
|
(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 )))
|
||||||
|
|
37
frontend/src/app/util/uri.cljs
Normal file
37
frontend/src/app/util/uri.cljs
Normal 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})))
|
39
vendor/svgclean/main.js
vendored
39
vendor/svgclean/main.js
vendored
|
@ -1,16 +1,41 @@
|
||||||
|
|
||||||
const plugins = [
|
const plugins = [
|
||||||
{removeDimensions: true},
|
{ "minifyStyles" : false },
|
||||||
{removeScriptElement: true},
|
{ "convertStyleToAttrs" : false },
|
||||||
{removeViewBox: false},
|
{
|
||||||
{moveElemsAttrsToGroup: false},
|
"cleanupIDs" : {
|
||||||
{convertStyleToAttrs: false},
|
remove: false,
|
||||||
{convertPathData: {
|
minify: false,
|
||||||
|
force: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ "cleanupListOfValues" : true },
|
||||||
|
{ "removeUnknownsAndDefaults" : false },
|
||||||
|
{ "removeViewBox" : false },
|
||||||
|
{ "convertShapeToPath" : false },
|
||||||
|
{ "convertEllipseToCircle" : false },
|
||||||
|
{ "moveElemsAttrsToGroup" : false },
|
||||||
|
{ "moveGroupAttrsToElems" : false },
|
||||||
|
{ "collapseGroups" : false },
|
||||||
|
{
|
||||||
|
"convertPathData" : {
|
||||||
lineShorthands: false,
|
lineShorthands: false,
|
||||||
curveSmoothShorthands: false,
|
curveSmoothShorthands: false,
|
||||||
forceAbsolutePath: true,
|
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 svgc = require("./src/svgclean.js");
|
||||||
const inst = svgc.configure({plugins});
|
const inst = svgc.configure({plugins});
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ exports.description = 'rounds numeric values to the fixed precision, removes def
|
||||||
|
|
||||||
exports.params = {
|
exports.params = {
|
||||||
floatPrecision: 3,
|
floatPrecision: 3,
|
||||||
leadingZero: true,
|
leadingZero: false,
|
||||||
defaultPx: true,
|
defaultPx: true,
|
||||||
convertToPx: 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,
|
mm: 96/25.4,
|
||||||
in: 96,
|
in: 96,
|
||||||
pt: 4/3,
|
pt: 4/3,
|
||||||
pc: 16
|
pc: 16,
|
||||||
|
em: 16
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -61,13 +62,9 @@ exports.fn = function(item, params) {
|
||||||
|
|
||||||
// convert absolute values to pixels
|
// convert absolute values to pixels
|
||||||
if (params.convertToPx && units && (units in absoluteLengths)) {
|
if (params.convertToPx && units && (units in absoluteLengths)) {
|
||||||
var pxNum = +(absoluteLengths[units] * match[1]).toFixed(floatPrecision);
|
num = +(absoluteLengths[units] * match[1]).toFixed(floatPrecision);
|
||||||
|
|
||||||
if (String(pxNum).length < match[0].length) {
|
|
||||||
num = pxNum;
|
|
||||||
units = 'px';
|
units = 'px';
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// and remove leading zero
|
// and remove leading zero
|
||||||
if (params.leadingZero) {
|
if (params.leadingZero) {
|
||||||
|
|
|
@ -8,7 +8,8 @@ exports.description = 'moves some group attributes to the content elements';
|
||||||
|
|
||||||
var collections = require('./_collections.js'),
|
var collections = require('./_collections.js'),
|
||||||
pathElems = collections.pathElems.concat(['g', 'text']),
|
pathElems = collections.pathElems.concat(['g', 'text']),
|
||||||
referencesProps = collections.referencesProps;
|
referencesProps = collections.referencesProps,
|
||||||
|
inheritableAttrs = collections.inheritableAttrs;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Move group attrs to the content elements.
|
* Move group attrs to the content elements.
|
||||||
|
@ -31,33 +32,33 @@ var collections = require('./_collections.js'),
|
||||||
*/
|
*/
|
||||||
exports.fn = function(item) {
|
exports.fn = function(item) {
|
||||||
|
|
||||||
// move group transform attr to content's pathElems
|
if (item.isElem('g') && !item.isEmpty()) {
|
||||||
if (
|
|
||||||
item.isElem('g') &&
|
inheritableAttrs.forEach(function(currentAttr) {
|
||||||
item.hasAttr('transform') &&
|
|
||||||
!item.isEmpty() &&
|
if (item.hasAttr(currentAttr)) {
|
||||||
!item.someAttr(function(attr) {
|
var attr = item.attr(currentAttr);
|
||||||
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) {
|
item.content.forEach(function(inner) {
|
||||||
var attr = item.attr('transform');
|
|
||||||
if (inner.hasAttr('transform')) {
|
if (currentAttr === 'transform' && inner.hasAttr(currentAttr)) {
|
||||||
inner.attr('transform').value = attr.value + ' ' + inner.attr('transform').value;
|
// if attr is transform and the inner has transform we concatenate it
|
||||||
} else {
|
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({
|
inner.addAttr({
|
||||||
'name': attr.name,
|
...attr
|
||||||
'local': attr.local,
|
|
||||||
'prefix': attr.prefix,
|
|
||||||
'value': attr.value
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
item.removeAttr(currentAttr);
|
||||||
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
item.removeAttr('transform');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
Loading…
Add table
Reference in a new issue