mirror of
https://github.com/penpot/penpot.git
synced 2025-03-13 16:21:57 -05:00
Merge pull request #733 from penpot/feat/svg-native
Advanced SVG Import
This commit is contained in:
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)
|
||||
- Duplicate and move files and projects [Taiga #267](https://tree.taiga.io/project/penpot/us/267)
|
||||
- Import SVG will create Penpot's shapes
|
||||
- Improve french translations [#731](https://github.com/penpot/penpot/pull/731)
|
||||
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix broken profile and profile options form.
|
||||
|
@ -16,6 +16,7 @@
|
|||
|
||||
### :heart: Community contributions by (Thank you!)
|
||||
|
||||
- iblueer [#731](https://github.com/penpot/penpot/pull/731)
|
||||
|
||||
## 1.3.0-alpha
|
||||
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -14,7 +14,9 @@
|
|||
[promesa.exec :as px]))
|
||||
|
||||
(def default-client
|
||||
(delay (http/build-client {:executor @px/default-executor})))
|
||||
(delay (http/build-client {:executor @px/default-executor
|
||||
:connect-timeout 10000 ;; 10s
|
||||
:follow-redirects :always})))
|
||||
|
||||
(defn get!
|
||||
[url opts]
|
||||
|
|
|
@ -21,4 +21,4 @@
|
|||
data (slurp (io/resource "app/tests/_files/sample1.svg"))
|
||||
res (svgc data)]
|
||||
(t/is (string? res))
|
||||
(t/is (= 2533 (count res)))))
|
||||
(t/is (= 2609 (count res)))))
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
(ns app.common.data
|
||||
"Data manipulation and query helper functions."
|
||||
(:refer-clojure :exclude [concat read-string hash-map merge])
|
||||
(:refer-clojure :exclude [concat read-string hash-map merge name])
|
||||
#?(:cljs
|
||||
(:require-macros [app.common.data]))
|
||||
(:require
|
||||
|
@ -132,8 +132,9 @@
|
|||
"Return a map without the keys provided
|
||||
in the `keys` parameter."
|
||||
[data keys]
|
||||
(persistent!
|
||||
(reduce #(dissoc! %1 %2) (transient data) keys)))
|
||||
(when data
|
||||
(persistent!
|
||||
(reduce #(dissoc! %1 %2) (transient data) keys))))
|
||||
|
||||
(defn remove-at-index
|
||||
[v index]
|
||||
|
@ -302,6 +303,14 @@
|
|||
default
|
||||
v))))
|
||||
|
||||
(defn num-string? [v]
|
||||
;; https://stackoverflow.com/questions/175739/built-in-way-in-javascript-to-check-if-a-string-is-a-valid-number
|
||||
#?(:cljs (and (string? v)
|
||||
(not (js/isNaN v))
|
||||
(not (js/isNaN (parse-double v))))
|
||||
|
||||
:clj (not= (parse-double v :nan) :nan)))
|
||||
|
||||
(defn read-string
|
||||
[v]
|
||||
(r/read-string v))
|
||||
|
@ -350,7 +359,7 @@
|
|||
;; Code for ClojureScript
|
||||
(let [mdata (aapi/resolve &env v)
|
||||
arglists (second (get-in mdata [:meta :arglists]))
|
||||
sym (symbol (name v))
|
||||
sym (symbol (core/name v))
|
||||
andsym (symbol "&")
|
||||
procarg #(if (= % andsym) % (gensym "param"))]
|
||||
(if (pos? (count arglists))
|
||||
|
@ -382,3 +391,16 @@
|
|||
|
||||
(defn any-key? [element & rest]
|
||||
(some #(contains? element %) rest))
|
||||
|
||||
(defn name
|
||||
"Improved version of name that won't fail if the input is not a keyword"
|
||||
[maybe-keyword]
|
||||
(cond
|
||||
(keyword? maybe-keyword)
|
||||
(core/name maybe-keyword)
|
||||
|
||||
(nil? maybe-keyword) nil
|
||||
|
||||
:else
|
||||
(str maybe-keyword)))
|
||||
|
||||
|
|
|
@ -11,6 +11,8 @@
|
|||
(:require
|
||||
#?(:cljs [cljs.pprint :as pp]
|
||||
:clj [clojure.pprint :as pp])
|
||||
[cuerdas.core :as str]
|
||||
[app.common.data :as d]
|
||||
[app.common.math :as mth]
|
||||
[app.common.geom.point :as gpt]))
|
||||
|
||||
|
@ -21,6 +23,13 @@
|
|||
(toString [_]
|
||||
(str "matrix(" a "," b "," c "," d "," e "," f ")")))
|
||||
|
||||
(defn matrix
|
||||
"Create a new matrix instance."
|
||||
([]
|
||||
(Matrix. 1 0 0 1 0 0))
|
||||
([a b c d e f]
|
||||
(Matrix. a b c d e f)))
|
||||
|
||||
(defn multiply
|
||||
([{m1a :a m1b :b m1c :c m1d :d m1e :e m1f :f}
|
||||
{m2a :a m2b :b m2c :c m2d :d m2e :e m2f :f}]
|
||||
|
@ -46,13 +55,6 @@
|
|||
[v]
|
||||
(instance? Matrix v))
|
||||
|
||||
(defn matrix
|
||||
"Create a new matrix instance."
|
||||
([]
|
||||
(Matrix. 1 0 0 1 0 0))
|
||||
([a b c d e f]
|
||||
(Matrix. a b c d e f)))
|
||||
|
||||
(def base (matrix))
|
||||
|
||||
(defn base?
|
||||
|
|
|
@ -204,6 +204,11 @@
|
|||
(defn to-vec [p1 p2]
|
||||
(subtract p2 p1))
|
||||
|
||||
(defn scale [v scalar]
|
||||
(-> v
|
||||
(update :x * scalar)
|
||||
(update :y * scalar)))
|
||||
|
||||
(defn dot [{x1 :x y1 :y} {x2 :x y2 :y}]
|
||||
(+ (* x1 x2) (* y1 y2)))
|
||||
|
||||
|
|
|
@ -148,10 +148,10 @@
|
|||
(update-in [:selrect :x2] - x)
|
||||
(update-in [:selrect :y2] - y)
|
||||
|
||||
(d/update-when :points #(map move-point %))
|
||||
(d/update-when :points #(mapv move-point %))
|
||||
|
||||
(cond-> (= :path type)
|
||||
(d/update-when :content #(map move-segment %))))))
|
||||
(d/update-when :content #(mapv move-segment %))))))
|
||||
|
||||
|
||||
;; --- Helpers
|
||||
|
@ -281,6 +281,7 @@
|
|||
(d/export gtr/transform-rect)
|
||||
(d/export gtr/update-group-selrect)
|
||||
(d/export gtr/transform-points)
|
||||
(d/export gtr/calculate-adjust-matrix)
|
||||
|
||||
;; PATHS
|
||||
(d/export gsp/content->points)
|
||||
|
|
|
@ -139,11 +139,15 @@
|
|||
|
||||
(defn- calculate-height
|
||||
"Calculates the height of a paralelogram given by the points"
|
||||
[[p1 _ p3 p4]]
|
||||
(let [v1 (gpt/to-vec p3 p4)
|
||||
v2 (gpt/to-vec p4 p1)
|
||||
angle (gpt/angle-with-other v1 v2)]
|
||||
(* (gpt/length v2) (mth/sin (mth/radians angle)))))
|
||||
[[p1 _ _ p4]]
|
||||
(-> (gpt/to-vec p4 p1)
|
||||
(gpt/length)))
|
||||
|
||||
(defn- calculate-width
|
||||
"Calculates the width of a paralelogram given by the points"
|
||||
[[p1 p2 _ _]]
|
||||
(-> (gpt/to-vec p1 p2)
|
||||
(gpt/length)))
|
||||
|
||||
(defn- calculate-rotation
|
||||
"Calculates the rotation between two shapes given the resize vector direction"
|
||||
|
@ -173,44 +177,49 @@
|
|||
"Calculates a matrix that is a series of transformations we have to do to the transformed rectangle so that
|
||||
after applying them the end result is the `shape-pathn-temp`.
|
||||
This is compose of three transformations: skew, resize and rotation"
|
||||
[points-temp points-rec flip-x flip-y]
|
||||
(let [center (gco/center-points points-temp)
|
||||
([points-temp points-rec] (calculate-adjust-matrix points-temp points-rec false false))
|
||||
([points-temp points-rec flip-x flip-y]
|
||||
(let [center (gco/center-points points-temp)
|
||||
|
||||
stretch-matrix (gmt/matrix)
|
||||
stretch-matrix (gmt/matrix)
|
||||
|
||||
skew-angle (calculate-skew-angle points-temp)
|
||||
skew-angle (calculate-skew-angle points-temp)
|
||||
|
||||
;; When one of the axis is flipped we have to reverse the skew
|
||||
;; skew-angle (if (neg? (* (:x resize-vector) (:y resize-vector))) (- skew-angle) skew-angle )
|
||||
skew-angle (if (and (or flip-x flip-y)
|
||||
(not (and flip-x flip-y))) (- skew-angle) skew-angle )
|
||||
skew-angle (if (mth/nan? skew-angle) 0 skew-angle)
|
||||
;; When one of the axis is flipped we have to reverse the skew
|
||||
;; skew-angle (if (neg? (* (:x resize-vector) (:y resize-vector))) (- skew-angle) skew-angle )
|
||||
skew-angle (if (and (or flip-x flip-y)
|
||||
(not (and flip-x flip-y))) (- skew-angle) skew-angle )
|
||||
skew-angle (if (mth/nan? skew-angle) 0 skew-angle)
|
||||
|
||||
stretch-matrix (gmt/multiply stretch-matrix (gmt/skew-matrix skew-angle 0))
|
||||
stretch-matrix (gmt/multiply stretch-matrix (gmt/skew-matrix skew-angle 0))
|
||||
|
||||
h1 (max 1 (calculate-height points-temp))
|
||||
h2 (max 1 (calculate-height (transform-points points-rec center stretch-matrix)))
|
||||
h3 (if-not (mth/almost-zero? h2) (/ h1 h2) 1)
|
||||
h3 (if (mth/nan? h3) 1 h3)
|
||||
h1 (max 1 (calculate-height points-temp))
|
||||
h2 (max 1 (calculate-height (transform-points points-rec center stretch-matrix)))
|
||||
h3 (if-not (mth/almost-zero? h2) (/ h1 h2) 1)
|
||||
h3 (if (mth/nan? h3) 1 h3)
|
||||
|
||||
stretch-matrix (gmt/multiply stretch-matrix (gmt/scale-matrix (gpt/point 1 h3)))
|
||||
w1 (max 1 (calculate-width points-temp))
|
||||
w2 (max 1 (calculate-width (transform-points points-rec center stretch-matrix)))
|
||||
w3 (if-not (mth/almost-zero? w2) (/ w1 w2) 1)
|
||||
w3 (if (mth/nan? w3) 1 w3)
|
||||
|
||||
rotation-angle (calculate-rotation
|
||||
center
|
||||
(transform-points points-rec (gco/center-points points-rec) stretch-matrix)
|
||||
points-temp
|
||||
flip-x
|
||||
flip-y)
|
||||
stretch-matrix (gmt/multiply stretch-matrix (gmt/scale-matrix (gpt/point w3 h3)))
|
||||
|
||||
stretch-matrix (gmt/multiply (gmt/rotate-matrix rotation-angle) stretch-matrix)
|
||||
rotation-angle (calculate-rotation
|
||||
center
|
||||
(transform-points points-rec (gco/center-points points-rec) stretch-matrix)
|
||||
points-temp
|
||||
flip-x
|
||||
flip-y)
|
||||
|
||||
stretch-matrix (gmt/multiply (gmt/rotate-matrix rotation-angle) stretch-matrix)
|
||||
|
||||
;; This is the inverse to be able to remove the transformation
|
||||
stretch-matrix-inverse (-> (gmt/matrix)
|
||||
(gmt/scale (gpt/point 1 (/ 1 h3)))
|
||||
(gmt/skew (- skew-angle) 0)
|
||||
(gmt/rotate (- rotation-angle)))]
|
||||
[stretch-matrix stretch-matrix-inverse]))
|
||||
;; This is the inverse to be able to remove the transformation
|
||||
stretch-matrix-inverse (-> (gmt/matrix)
|
||||
(gmt/scale (gpt/point (/ 1 w3) (/ 1 h3)))
|
||||
(gmt/skew (- skew-angle) 0)
|
||||
(gmt/rotate (- rotation-angle)))]
|
||||
[stretch-matrix stretch-matrix-inverse rotation-angle])))
|
||||
|
||||
(defn apply-transform
|
||||
"Given a new set of points transformed, set up the rectangle so it keeps
|
||||
|
@ -276,6 +285,24 @@
|
|||
(dissoc :modifiers)))
|
||||
shape)))
|
||||
|
||||
(defn update-group-viewbox
|
||||
"Updates the viewbox for groups imported from SVG's"
|
||||
[{:keys [selrect svg-viewbox] :as group} new-selrect]
|
||||
(let [;; Gets deltas for the selrect to update the svg-viewbox (for svg-imports)
|
||||
deltas {:x (- (:x new-selrect) (:x selrect))
|
||||
:y (- (:y new-selrect) (:y selrect))
|
||||
:width (- (:width new-selrect) (:width selrect))
|
||||
:height (- (:height new-selrect) (:height selrect))}]
|
||||
|
||||
(cond-> group
|
||||
svg-viewbox
|
||||
(update :svg-viewbox
|
||||
#(-> %
|
||||
(update :x + (:x deltas))
|
||||
(update :y + (:y deltas))
|
||||
(update :width + (:width deltas))
|
||||
(update :height + (:height deltas)))))))
|
||||
|
||||
(defn update-group-selrect [group children]
|
||||
(let [shape-center (gco/center-shape group)
|
||||
transform (:transform group (gmt/matrix))
|
||||
|
@ -297,6 +324,7 @@
|
|||
|
||||
;; Updates the shape and the applytransform-rect will update the other properties
|
||||
(-> group
|
||||
(update-group-viewbox new-selrect)
|
||||
(assoc :selrect new-selrect)
|
||||
(assoc :points new-points)
|
||||
|
||||
|
|
|
@ -229,7 +229,7 @@
|
|||
(s/def :internal.shape/stroke-color-ref-file (s/nilable uuid?))
|
||||
(s/def :internal.shape/stroke-color-ref-id (s/nilable uuid?))
|
||||
(s/def :internal.shape/stroke-opacity ::safe-number)
|
||||
(s/def :internal.shape/stroke-style #{:solid :dotted :dashed :mixed :none})
|
||||
(s/def :internal.shape/stroke-style #{:solid :dotted :dashed :mixed :none :svg})
|
||||
(s/def :internal.shape/stroke-width ::safe-number)
|
||||
(s/def :internal.shape/stroke-alignment #{:center :inner :outer})
|
||||
(s/def :internal.shape/text-align #{"left" "right" "center" "justify"})
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -5505,5 +5505,45 @@
|
|||
"zh_cn" : "单击以闭合路径"
|
||||
},
|
||||
"unused" : true
|
||||
},
|
||||
|
||||
"workspace.sidebar.options.svg-attrs.title": {
|
||||
"translations": {
|
||||
"en": "Imported SVG Attributes",
|
||||
"es": "Atributos del SVG Importado"
|
||||
}
|
||||
},
|
||||
|
||||
"handoff.attributes.stroke.alignment.inner" : {
|
||||
"translations" : {
|
||||
"en" : "Inside",
|
||||
"es" : "Interior",
|
||||
"fr" : "Intérieur",
|
||||
"ru" : "Внутрь",
|
||||
"zh_cn" : "内部"
|
||||
},
|
||||
"permanent": true
|
||||
},
|
||||
|
||||
"handoff.attributes.stroke.alignment.outer" : {
|
||||
"translations" : {
|
||||
"en" : "Outside",
|
||||
"es" : "Exterior",
|
||||
"fr" : "Extérieur",
|
||||
"ru" : "Наружу",
|
||||
"zh_cn" : "外部"
|
||||
},
|
||||
"permanent": true
|
||||
},
|
||||
|
||||
"handoff.attributes.stroke.alignment.center" : {
|
||||
"translations" : {
|
||||
"en" : "Center",
|
||||
"es" : "Centro",
|
||||
"fr" : "Centre",
|
||||
"ru" : "Центр",
|
||||
"zh_cn" : "居中"
|
||||
},
|
||||
"permanent": true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -449,6 +449,10 @@ ul.slider-dots {
|
|||
content: "Y";
|
||||
}
|
||||
}
|
||||
|
||||
&.large {
|
||||
min-width: 7rem;
|
||||
}
|
||||
}
|
||||
|
||||
input,
|
||||
|
|
|
@ -94,6 +94,10 @@
|
|||
|
||||
.attributes-label,
|
||||
.attributes-value {
|
||||
margin-right: 0.5rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
width: 50%;
|
||||
}
|
||||
.copy-button {
|
||||
|
|
|
@ -709,6 +709,9 @@
|
|||
.element-set-content .input-row {
|
||||
& .element-set-subtitle {
|
||||
width: 5.5rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -771,7 +774,6 @@
|
|||
min-width: 56px;
|
||||
max-height: 10rem;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -911,6 +913,7 @@
|
|||
|
||||
}
|
||||
|
||||
.row-flex-removable:hover .element-set-actions,
|
||||
.element-set-options-group:hover .element-set-actions {
|
||||
visibility: visible;
|
||||
}
|
||||
|
|
|
@ -219,15 +219,19 @@
|
|||
|
||||
(defn generate-unique-name
|
||||
"A unique name generator"
|
||||
[used basename]
|
||||
(s/assert ::set-of-string used)
|
||||
(s/assert ::us/string basename)
|
||||
(let [[prefix initial] (extract-numeric-suffix basename)]
|
||||
(loop [counter initial]
|
||||
(let [candidate (str prefix "-" counter)]
|
||||
(if (contains? used candidate)
|
||||
(recur (inc counter))
|
||||
candidate)))))
|
||||
([used basename]
|
||||
(generate-unique-name used basename false))
|
||||
([used basename prefix-first?]
|
||||
(s/assert ::set-of-string used)
|
||||
(s/assert ::us/string basename)
|
||||
(let [[prefix initial] (extract-numeric-suffix basename)]
|
||||
(loop [counter initial]
|
||||
(let [candidate (if (and (= 1 counter) prefix-first?)
|
||||
(str prefix)
|
||||
(str prefix "-" counter))]
|
||||
(if (contains? used candidate)
|
||||
(recur (inc counter))
|
||||
candidate))))))
|
||||
|
||||
;; --- Shape attrs (Layers Sidebar)
|
||||
|
||||
|
@ -574,36 +578,39 @@
|
|||
[frame-id parent-id (inc index)])))))
|
||||
|
||||
(defn add-shape-changes
|
||||
[page-id objects selected attrs]
|
||||
(let [id (:id attrs)
|
||||
shape (gpr/setup-proportions attrs)
|
||||
([page-id objects selected attrs]
|
||||
(add-shape-changes page-id objects selected attrs true))
|
||||
([page-id objects selected attrs reg-object?]
|
||||
(let [id (:id attrs)
|
||||
shape (gpr/setup-proportions attrs)
|
||||
|
||||
default-attrs (if (= :frame (:type shape))
|
||||
cp/default-frame-attrs
|
||||
cp/default-shape-attrs)
|
||||
default-attrs (if (= :frame (:type shape))
|
||||
cp/default-frame-attrs
|
||||
cp/default-shape-attrs)
|
||||
|
||||
shape (merge default-attrs shape)
|
||||
shape (merge default-attrs shape)
|
||||
|
||||
not-frame? #(not (= :frame (get-in objects [% :type])))
|
||||
selected (into #{} (filter not-frame?) selected)
|
||||
not-frame? #(not (= :frame (get-in objects [% :type])))
|
||||
selected (into #{} (filter not-frame?) selected)
|
||||
|
||||
[frame-id parent-id index] (get-shape-layer-position objects selected attrs)
|
||||
[frame-id parent-id index] (get-shape-layer-position objects selected attrs)
|
||||
|
||||
redo-changes [{:type :add-obj
|
||||
:id id
|
||||
:page-id page-id
|
||||
:frame-id frame-id
|
||||
:parent-id parent-id
|
||||
:index index
|
||||
:obj shape}
|
||||
{:type :reg-objects
|
||||
:page-id page-id
|
||||
:shapes [id]}]
|
||||
undo-changes [{:type :del-obj
|
||||
:page-id page-id
|
||||
:id id}]]
|
||||
redo-changes (cond-> [{:type :add-obj
|
||||
:id id
|
||||
:page-id page-id
|
||||
:frame-id frame-id
|
||||
:parent-id parent-id
|
||||
:index index
|
||||
:obj shape}]
|
||||
reg-object?
|
||||
(conj {:type :reg-objects
|
||||
:page-id page-id
|
||||
:shapes [id]}))
|
||||
undo-changes [{:type :del-obj
|
||||
:page-id page-id
|
||||
:id id}]]
|
||||
|
||||
[redo-changes undo-changes]))
|
||||
[redo-changes undo-changes])))
|
||||
|
||||
(defn add-shape
|
||||
[attrs]
|
||||
|
|
|
@ -32,10 +32,12 @@
|
|||
[app.util.router :as rt]
|
||||
[app.util.time :as dt]
|
||||
[app.util.transit :as t]
|
||||
[app.util.uri :as uu]
|
||||
[beicon.core :as rx]
|
||||
[cljs.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[potok.core :as ptk]))
|
||||
[potok.core :as ptk]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(declare persist-changes)
|
||||
(declare persist-sychronous-changes)
|
||||
|
@ -392,7 +394,7 @@
|
|||
(or (contains? props :data)
|
||||
(contains? props :uris)))))
|
||||
|
||||
(defn parse-svg [text]
|
||||
(defn parse-svg [[name text]]
|
||||
(->> (http/send! {:method :post
|
||||
:uri "/api/svg"
|
||||
:headers {"content-type" "image/svg+xml"}
|
||||
|
@ -400,19 +402,14 @@
|
|||
(rx/map (fn [{:keys [status body]}]
|
||||
(let [result (t/decode body)]
|
||||
(if (= status 200)
|
||||
result
|
||||
(assoc result :name name)
|
||||
(throw result)))))))
|
||||
|
||||
(defn fetch-svg [uri]
|
||||
(defn fetch-svg [name uri]
|
||||
(->> (http/send! {:method :get :uri uri})
|
||||
(rx/map :body)))
|
||||
|
||||
(defn url-name [url]
|
||||
(let [query-idx (str/last-index-of url "?")
|
||||
url (if (> query-idx 0) (subs url 0 query-idx) url)
|
||||
filename (->> (str/split url "/") (last))
|
||||
ext-idx (str/last-index-of filename ".")]
|
||||
(if (> ext-idx 0) (subs filename 0 ext-idx) filename)))
|
||||
(rx/map #(vector
|
||||
(or name (uu/uri-name uri))
|
||||
(:body %)))))
|
||||
|
||||
(defn- handle-upload-error [on-error stream]
|
||||
(->> stream
|
||||
|
@ -456,7 +453,7 @@
|
|||
(prepare-uri [uri]
|
||||
{:file-id file-id
|
||||
:is-local local?
|
||||
:name (or name (url-name uri))
|
||||
:name (or name (uu/uri-name uri))
|
||||
:url uri})]
|
||||
(rx/merge
|
||||
(->> (rx/from uris)
|
||||
|
@ -467,10 +464,8 @@
|
|||
|
||||
(->> (rx/from uris)
|
||||
(rx/filter svg-url?)
|
||||
(rx/merge-map fetch-svg)
|
||||
(rx/merge-map (partial fetch-svg name))
|
||||
(rx/merge-map parse-svg)
|
||||
(rx/with-latest vector uris)
|
||||
(rx/map #(assoc (first %) :name (or name (url-name (second %)))))
|
||||
(rx/do on-svg)))))
|
||||
|
||||
(defn- upload-data [file-id local? name data force-media on-image on-svg]
|
||||
|
@ -485,6 +480,12 @@
|
|||
:is-local local?
|
||||
:content blob}))
|
||||
|
||||
extract-content
|
||||
(fn [blob]
|
||||
(let [name (or name (.-name blob))]
|
||||
(-> (.text blob)
|
||||
(p/then #(vector name %)))))
|
||||
|
||||
file-stream (->> (rx/from data)
|
||||
(rx/map di/validate-file))]
|
||||
(rx/merge
|
||||
|
@ -496,10 +497,8 @@
|
|||
|
||||
(->> file-stream
|
||||
(rx/filter svg-blob?)
|
||||
(rx/merge-map #(.text %))
|
||||
(rx/merge-map extract-content)
|
||||
(rx/merge-map parse-svg)
|
||||
(rx/with-latest vector file-stream)
|
||||
(rx/map #(assoc (first %) :name (.-name (second %))))
|
||||
(rx/do on-svg)))))
|
||||
|
||||
(defn- upload-media-objects
|
||||
|
@ -538,7 +537,7 @@
|
|||
[params position]
|
||||
(let [{:keys [x y]} position
|
||||
mdata {:on-image #(st/emit! (dwc/image-uploaded % x y))
|
||||
:on-svg #(st/emit! (svg/svg-uploaded % x y))}
|
||||
:on-svg #(st/emit! (svg/svg-uploaded % (:file-id params) x y))}
|
||||
|
||||
params (-> (assoc params :local? true)
|
||||
(with-meta mdata))]
|
||||
|
|
|
@ -10,75 +10,151 @@
|
|||
(ns app.main.data.workspace.svg-upload
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.util.data :as ud]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.geom.proportions :as gpr]
|
||||
[app.common.pages :as cp]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.data.workspace.common :as dwc]
|
||||
[app.main.repo :as rp]
|
||||
[app.util.color :as uc]
|
||||
[app.util.geom.path :as ugp]
|
||||
[app.util.object :as obj]
|
||||
[app.util.svg :as usvg]
|
||||
[app.util.uri :as uu]
|
||||
[beicon.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
[potok.core :as ptk]
|
||||
[app.util.svg :as usvg]
|
||||
[app.util.geom.path :as ugp]))
|
||||
[promesa.core :as p]))
|
||||
|
||||
(defonce default-rect {:x 0 :y 0 :width 1 :height 1 :rx 0 :ry 0})
|
||||
(defonce default-circle {:r 0 :cx 0 :cy 0})
|
||||
(defonce default-image {:x 0 :y 0 :width 1 :height 1})
|
||||
|
||||
(defn- svg-dimensions [data]
|
||||
(let [width (get-in data [:attrs :width] 100)
|
||||
height (get-in data [:attrs :height] 100)
|
||||
viewbox (get-in data [:attrs :viewBox] (str "0 0 " width " " height))
|
||||
[_ _ width-str height-str] (str/split viewbox " ")
|
||||
width (d/parse-integer width-str)
|
||||
height (d/parse-integer height-str)]
|
||||
[width height]))
|
||||
[x y width height] (->> (str/split viewbox " ")
|
||||
(map d/parse-double))]
|
||||
[x y width height]))
|
||||
|
||||
(defn tag-name [{:keys [tag]}]
|
||||
(cond (string? tag) tag
|
||||
(keyword? tag) (name tag)
|
||||
(nil? tag) "node"
|
||||
:else (str tag)))
|
||||
(defn tag->name
|
||||
"Given a tag returns its layer name"
|
||||
[tag]
|
||||
(str "svg-" (cond (string? tag) tag
|
||||
(keyword? tag) (d/name tag)
|
||||
(nil? tag) "node"
|
||||
:else (str tag))))
|
||||
|
||||
(defn setup-fill [shape attrs]
|
||||
(let [fill-color (or (get-in attrs [:fill])
|
||||
(get-in attrs [:style :fill])
|
||||
"#000000")
|
||||
fill-opacity (ud/parse-float (or (get-in attrs [:fill-opacity])
|
||||
(get-in attrs [:style :fill-opacity])
|
||||
"1"))]
|
||||
(-> shape
|
||||
(assoc :fill-color fill-color)
|
||||
(assoc :fill-opacity fill-opacity))))
|
||||
|
||||
(defn setup-stroke [shape attrs]
|
||||
(-> shape
|
||||
(assoc :stroke-color (:stroke attrs "#000000"))
|
||||
(assoc :stroke-opacity (ud/parse-float (:stroke-opacity attrs 1)))
|
||||
(assoc :stroke-style :solid)
|
||||
(assoc :stroke-width (ud/parse-float (:stroke-width attrs "1")))
|
||||
(assoc :stroke-alignment :center)))
|
||||
|
||||
(defn add-style-attributes [shape {:keys [attrs]}]
|
||||
(defn setup-fill [shape]
|
||||
(cond-> shape
|
||||
(d/any-key? attrs :fill :fill-opacity)
|
||||
(setup-fill attrs)
|
||||
|
||||
(d/any-key? attrs :stroke :stroke-width :stroke-opacity)
|
||||
(setup-stroke attrs)))
|
||||
;; Color present as attribute
|
||||
(uc/color? (get-in shape [:svg-attrs :fill]))
|
||||
(-> (update :svg-attrs dissoc :fill)
|
||||
(assoc :fill-color (-> (get-in shape [:svg-attrs :fill])
|
||||
(uc/parse-color))))
|
||||
|
||||
(defn create-raw-svg [name frame-id x y width height data]
|
||||
(-> {:id (uuid/next)
|
||||
:type :svg-raw
|
||||
:name name
|
||||
:frame-id frame-id
|
||||
:width width
|
||||
:height height
|
||||
:x x
|
||||
:y y
|
||||
:content (if (map? data) (update data :attrs usvg/clean-attrs) data)}
|
||||
(gsh/setup-selrect)))
|
||||
;; Color present as style
|
||||
(uc/color? (get-in shape [:svg-attrs :style :fill]))
|
||||
(-> (update-in [:svg-attrs :style] dissoc :fill)
|
||||
(assoc :fill-color (-> (get-in shape [:svg-attrs :style :fill])
|
||||
(uc/parse-color))))
|
||||
|
||||
(get-in shape [:svg-attrs :fill-opacity])
|
||||
(-> (update :svg-attrs dissoc :fill-opacity)
|
||||
(assoc :fill-opacity (-> (get-in shape [:svg-attrs :fill-opacity])
|
||||
(d/parse-double))))
|
||||
|
||||
(get-in shape [:svg-attrs :style :fill-opacity])
|
||||
(-> (update-in [:svg-attrs :style] dissoc :fill-opacity)
|
||||
(assoc :fill-opacity (-> (get-in shape [:svg-attrs :style :fill-opacity])
|
||||
(d/parse-double))))))
|
||||
|
||||
(defn setup-stroke [shape]
|
||||
(let [shape
|
||||
(cond-> shape
|
||||
(uc/color? (get-in shape [:svg-attrs :stroke]))
|
||||
(-> (update :svg-attrs dissoc :stroke)
|
||||
(assoc :stroke-color (get-in shape [:svg-attrs :stroke])))
|
||||
|
||||
(uc/color? (get-in shape [:svg-attrs :style :stroke]))
|
||||
(-> (update-in [:svg-attrs :style] dissoc :stroke)
|
||||
(assoc :stroke-color (get-in shape [:svg-attrs :style :stroke])))
|
||||
|
||||
(get-in shape [:svg-attrs :stroke-width])
|
||||
(-> (update :svg-attrs dissoc :stroke-width)
|
||||
(assoc :stroke-width (-> (get-in shape [:svg-attrs :stroke-width])
|
||||
(d/parse-double))))
|
||||
|
||||
(get-in shape [:svg-attrs :style :stroke-width])
|
||||
(-> (update-in [:svg-attrs :style] dissoc :stroke-width)
|
||||
(assoc :stroke-width (-> (get-in shape [:svg-attrs :style :stroke-width])
|
||||
(d/parse-double)))))]
|
||||
(if (d/any-key? shape :stroke-color :stroke-opacity :stroke-width)
|
||||
(merge {:stroke-style :svg} shape)
|
||||
shape)))
|
||||
|
||||
(defn create-raw-svg [name frame-id svg-data {:keys [attrs] :as data}]
|
||||
(let [{:keys [x y width height offset-x offset-y]} svg-data]
|
||||
(-> {:id (uuid/next)
|
||||
:type :svg-raw
|
||||
:name name
|
||||
:frame-id frame-id
|
||||
:width width
|
||||
:height height
|
||||
:x x
|
||||
:y y
|
||||
:content (cond-> data
|
||||
(map? data) (update :attrs usvg/clean-attrs))}
|
||||
(assoc :svg-attrs attrs)
|
||||
(assoc :svg-viewbox (-> (select-keys svg-data [:width :height])
|
||||
(assoc :x offset-x :y offset-y)))
|
||||
(gsh/setup-selrect))))
|
||||
|
||||
(defn create-svg-root [frame-id svg-data]
|
||||
(let [{:keys [name x y width height offset-x offset-y]} svg-data]
|
||||
(-> {:id (uuid/next)
|
||||
:type :group
|
||||
:name name
|
||||
:frame-id frame-id
|
||||
:width width
|
||||
:height height
|
||||
:x (+ x offset-x)
|
||||
:y (+ y offset-y)}
|
||||
(gsh/setup-selrect)
|
||||
(assoc :svg-attrs (-> (:attrs svg-data)
|
||||
(dissoc :viewBox :xmlns))))))
|
||||
|
||||
(defn create-group [name frame-id svg-data {:keys [attrs]}]
|
||||
(let [svg-transform (usvg/parse-transform (:transform attrs))
|
||||
{:keys [x y width height offset-x offset-y]} svg-data]
|
||||
(-> {:id (uuid/next)
|
||||
:type :group
|
||||
:name name
|
||||
:frame-id frame-id
|
||||
:x (+ x offset-x)
|
||||
:y (+ y offset-y)
|
||||
:width width
|
||||
:height height}
|
||||
(assoc :svg-transform svg-transform)
|
||||
(assoc :svg-attrs (d/without-keys attrs usvg/inheritable-props))
|
||||
(assoc :svg-viewbox (-> (select-keys svg-data [:width :height])
|
||||
(assoc :x offset-x :y offset-y)))
|
||||
(gsh/setup-selrect))))
|
||||
|
||||
(defn create-path-shape [name frame-id svg-data {:keys [attrs] :as data}]
|
||||
(let [svg-transform (usvg/parse-transform (:transform attrs))
|
||||
path-content (ugp/path->content (:d attrs))
|
||||
content (cond-> path-content
|
||||
svg-transform
|
||||
(gsh/transform-content svg-transform))
|
||||
|
||||
(defn parse-path [name frame-id {:keys [attrs] :as data}]
|
||||
(let [content (ugp/path->content (:d attrs))
|
||||
selrect (gsh/content->selrect content)
|
||||
points (gsh/rect->points selrect)]
|
||||
points (gsh/rect->points selrect)
|
||||
|
||||
origin (gpt/negate (gpt/point svg-data))]
|
||||
(-> {:id (uuid/next)
|
||||
:type :path
|
||||
:name name
|
||||
|
@ -86,68 +162,283 @@
|
|||
:content content
|
||||
:selrect selrect
|
||||
:points points}
|
||||
(assoc :svg-viewbox (select-keys selrect [:x :y :width :height]))
|
||||
(assoc :svg-attrs (dissoc attrs :d :transform))
|
||||
(assoc :svg-transform svg-transform)
|
||||
(gsh/translate-to-frame origin))))
|
||||
|
||||
(add-style-attributes data))))
|
||||
(defn calculate-rect-metadata [rect-data transform]
|
||||
(let [points (-> (gsh/rect->points rect-data)
|
||||
(gsh/transform-points transform))
|
||||
|
||||
(defn parse-svg-element [root-shape data unames]
|
||||
(let [root-id (:id root-shape)
|
||||
frame-id (:frame-id root-shape)
|
||||
{:keys [x y width height]} (:selrect root-shape)
|
||||
{:keys [tag]} data
|
||||
name (dwc/generate-unique-name unames (str "svg-" (tag-name data)))
|
||||
|
||||
shape
|
||||
(case tag
|
||||
;; :rect (parse-rect data)
|
||||
;; :path (parse-path name frame-id data)
|
||||
(create-raw-svg name frame-id x y width height data))]
|
||||
|
||||
(-> shape
|
||||
(assoc :svg-id root-id))))
|
||||
center (gsh/center-points points)
|
||||
|
||||
(defn svg-uploaded [data x y]
|
||||
rect-shape (-> (gsh/make-centered-rect center (:width rect-data) (:height rect-data))
|
||||
(update :width max 1)
|
||||
(update :height max 1))
|
||||
|
||||
selrect (gsh/rect->selrect rect-shape)
|
||||
|
||||
rect-points (gsh/rect->points rect-shape)
|
||||
|
||||
[shape-transform shape-transform-inv rotation]
|
||||
(gsh/calculate-adjust-matrix points rect-points (neg? (:a transform)) (neg? (:d transform)))]
|
||||
|
||||
(merge rect-shape
|
||||
{:selrect selrect
|
||||
:points points
|
||||
:rotation rotation
|
||||
:transform shape-transform
|
||||
:transform-inverse shape-transform-inv})))
|
||||
|
||||
|
||||
(defn create-rect-shape [name frame-id svg-data {:keys [attrs] :as data}]
|
||||
(let [svg-transform (usvg/parse-transform (:transform attrs))
|
||||
transform (->> svg-transform
|
||||
(gmt/transform-in (gpt/point svg-data)))
|
||||
|
||||
rect (->> (select-keys attrs [:x :y :width :height])
|
||||
(d/mapm #(d/parse-double %2)))
|
||||
|
||||
origin (gpt/negate (gpt/point svg-data))
|
||||
|
||||
rect-data (-> (merge default-rect rect)
|
||||
(update :x - (:x origin))
|
||||
(update :y - (:y origin)))
|
||||
|
||||
metadata (calculate-rect-metadata rect-data transform)]
|
||||
(-> {:id (uuid/next)
|
||||
:type :rect
|
||||
:name name
|
||||
:frame-id frame-id}
|
||||
(cond->
|
||||
(contains? attrs :rx) (assoc :rx (d/parse-double (:rx attrs)))
|
||||
(contains? attrs :ry) (assoc :ry (d/parse-double (:ry attrs))))
|
||||
|
||||
(merge metadata)
|
||||
(assoc :svg-viewbox (select-keys rect [:x :y :width :height]))
|
||||
(assoc :svg-attrs (dissoc attrs :x :y :width :height :rx :ry :transform)))))
|
||||
|
||||
|
||||
(defn create-circle-shape [name frame-id svg-data {:keys [attrs] :as data}]
|
||||
(let [svg-transform (usvg/parse-transform (:transform attrs))
|
||||
transform (->> svg-transform
|
||||
(gmt/transform-in (gpt/point svg-data)))
|
||||
|
||||
circle (->> (select-keys attrs [:r :ry :rx :cx :cy])
|
||||
(d/mapm #(d/parse-double %2)))
|
||||
|
||||
{:keys [cx cy]} circle
|
||||
|
||||
rx (or (:r circle) (:rx circle))
|
||||
ry (or (:r circle) (:ry circle))
|
||||
|
||||
rect {:x (- cx rx)
|
||||
:y (- cy ry)
|
||||
:width (* 2 rx)
|
||||
:height (* 2 ry)}
|
||||
|
||||
origin (gpt/negate (gpt/point svg-data))
|
||||
|
||||
rect-data (-> rect
|
||||
(update :x - (:x origin))
|
||||
(update :y - (:y origin)))
|
||||
|
||||
metadata (calculate-rect-metadata rect-data transform)]
|
||||
(-> {:id (uuid/next)
|
||||
:type :circle
|
||||
:name name
|
||||
:frame-id frame-id}
|
||||
|
||||
(merge metadata)
|
||||
(assoc :svg-viewbox (select-keys rect [:x :y :width :height]))
|
||||
(assoc :svg-attrs (dissoc attrs :cx :cy :r :rx :ry :transform)))))
|
||||
|
||||
(defn create-image-shape [name frame-id svg-data {:keys [attrs] :as data}]
|
||||
(let [svg-transform (usvg/parse-transform (:transform attrs))
|
||||
transform (->> svg-transform
|
||||
(gmt/transform-in (gpt/point svg-data)))
|
||||
|
||||
image-url (:xlink:href attrs)
|
||||
image-data (get-in svg-data [:image-data image-url])
|
||||
|
||||
rect (->> (select-keys attrs [:x :y :width :height])
|
||||
(d/mapm #(d/parse-double %2)))
|
||||
|
||||
origin (gpt/negate (gpt/point svg-data))
|
||||
|
||||
rect-data (-> (merge default-image rect)
|
||||
(update :x - (:x origin))
|
||||
(update :y - (:y origin)))
|
||||
|
||||
rect-metadata (calculate-rect-metadata rect-data transform)]
|
||||
(-> {:id (uuid/next)
|
||||
:type :image
|
||||
:name name
|
||||
:frame-id frame-id
|
||||
:metadata {:width (:width image-data)
|
||||
:height (:height image-data)
|
||||
:mtype (:mtype image-data)
|
||||
:id (:id image-data)}}
|
||||
|
||||
(merge rect-metadata)
|
||||
(assoc :svg-viewbox (select-keys rect [:x :y :width :height]))
|
||||
(assoc :svg-attrs (dissoc attrs :x :y :width :height :xlink:href)))))
|
||||
|
||||
(defn parse-svg-element [frame-id svg-data element-data unames]
|
||||
(let [{:keys [tag attrs]} element-data
|
||||
attrs (usvg/format-styles attrs)
|
||||
element-data (cond-> element-data (map? element-data) (assoc :attrs attrs))
|
||||
name (dwc/generate-unique-name unames (or (:id attrs) (tag->name tag)) true)
|
||||
att-refs (usvg/find-attr-references attrs)
|
||||
references (usvg/find-def-references (:defs svg-data) att-refs)
|
||||
|
||||
href-id (-> (or (:href attrs) (:xlink:href attrs) "")
|
||||
(subs 1))
|
||||
defs (:defs svg-data)
|
||||
|
||||
use-tag? (and (= :use tag) (contains? defs href-id))]
|
||||
|
||||
(if use-tag?
|
||||
(let [use-data (get defs href-id)
|
||||
|
||||
displacement (gpt/point (d/parse-double (:x attrs "0")) (d/parse-double (:y attrs "0")))
|
||||
disp-matrix (str (gmt/translate-matrix displacement))
|
||||
element-data (-> element-data
|
||||
(assoc :tag :g)
|
||||
(update :attrs dissoc :x :y :width :height :href :xlink:href)
|
||||
(update :attrs usvg/add-transform disp-matrix)
|
||||
(assoc :content [use-data]))]
|
||||
(parse-svg-element frame-id svg-data element-data unames))
|
||||
|
||||
;; SVG graphic elements
|
||||
;; :circle :ellipse :image :line :path :polygon :polyline :rect :text :use
|
||||
(let [shape (-> (case tag
|
||||
(:g :a) (create-group name frame-id svg-data element-data)
|
||||
:rect (create-rect-shape name frame-id svg-data element-data)
|
||||
(:circle
|
||||
:ellipse) (create-circle-shape name frame-id svg-data element-data)
|
||||
:path (create-path-shape name frame-id svg-data element-data)
|
||||
:polyline (create-path-shape name frame-id svg-data (-> element-data usvg/polyline->path))
|
||||
:polygon (create-path-shape name frame-id svg-data (-> element-data usvg/polygon->path))
|
||||
:line (create-path-shape name frame-id svg-data (-> element-data usvg/line->path))
|
||||
:image (create-image-shape name frame-id svg-data element-data)
|
||||
#_other (create-raw-svg name frame-id svg-data element-data))
|
||||
|
||||
(assoc :svg-defs (select-keys (:defs svg-data) references))
|
||||
(setup-fill)
|
||||
(setup-stroke))
|
||||
children (cond->> (:content element-data)
|
||||
(= tag :g)
|
||||
(mapv #(usvg/inherit-attributes attrs %)))]
|
||||
[shape children]))))
|
||||
|
||||
(defn add-svg-child-changes [page-id objects selected frame-id parent-id svg-data [unames [rchs uchs]] [index data]]
|
||||
(let [[shape children] (parse-svg-element frame-id svg-data data unames)
|
||||
shape-id (:id shape)
|
||||
|
||||
[rch1 uch1] (dwc/add-shape-changes page-id objects selected shape false)
|
||||
|
||||
;; Mov-objects won't have undo because we "delete" the object in the undo of the
|
||||
;; previous operation
|
||||
rch2 [{:type :mov-objects
|
||||
:parent-id parent-id
|
||||
:frame-id frame-id
|
||||
:page-id page-id
|
||||
:index index
|
||||
:shapes [shape-id]}]
|
||||
|
||||
;; Careful! the undo changes are concatenated reversed (we undo in reverse order
|
||||
changes [(d/concat rchs rch1 rch2) (d/concat uch1 uchs)]
|
||||
unames (conj unames (:name shape))
|
||||
reducer-fn (partial add-svg-child-changes page-id objects selected frame-id shape-id svg-data)]
|
||||
(reduce reducer-fn [unames changes] (d/enumerate children))))
|
||||
|
||||
(declare create-svg-shapes)
|
||||
|
||||
(defn svg-uploaded [svg-data file-id x y]
|
||||
(ptk/reify ::svg-uploaded
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [page-id (:current-page-id state)
|
||||
objects (dwc/lookup-page-objects state page-id)
|
||||
frame-id (cp/frame-id-by-position objects {:x x :y y})
|
||||
selected (get-in state [:workspace-local :selected])
|
||||
(let [images-to-upload (-> svg-data (usvg/collect-images))
|
||||
|
||||
[width height] (svg-dimensions data)
|
||||
x (- x (/ width 2))
|
||||
y (- y (/ height 2))
|
||||
prepare-uri
|
||||
(fn [uri]
|
||||
(merge
|
||||
{:file-id file-id
|
||||
:is-local true
|
||||
:url uri}
|
||||
|
||||
add-svg-child
|
||||
(fn add-svg-child [parent-id root-shape [unames [rchs uchs]] [index {:keys [content] :as data}]]
|
||||
(let [shape (parse-svg-element root-shape data unames)
|
||||
shape-id (:id shape)
|
||||
[rch1 uch1] (dwc/add-shape-changes page-id objects selected shape)
|
||||
(if (str/starts-with? uri "data:")
|
||||
{:name "image"
|
||||
:content (uu/data-uri->blob uri)}
|
||||
{:name (uu/uri-name uri)})))]
|
||||
|
||||
;; Mov-objects won't have undo because we "delete" the object in the undo of the
|
||||
;; previous operation
|
||||
rch2 [{:type :mov-objects
|
||||
:parent-id parent-id
|
||||
:frame-id frame-id
|
||||
:page-id page-id
|
||||
:index index
|
||||
:shapes [shape-id]}]
|
||||
(->> (rx/from images-to-upload)
|
||||
(rx/map prepare-uri)
|
||||
(rx/mapcat (fn [uri-data]
|
||||
(->> (rp/mutation! (if (contains? uri-data :content)
|
||||
:upload-file-media-object
|
||||
:create-file-media-object-from-url) uri-data)
|
||||
(rx/map #(vector (:url uri-data) %)))))
|
||||
(rx/reduce (fn [acc [url image]] (assoc acc url image)) {})
|
||||
(rx/map #(create-svg-shapes (assoc svg-data :image-data %) x y)))))))
|
||||
|
||||
;; Careful! the undo changes are concatenated reversed (we undo in reverse order
|
||||
changes [(d/concat rchs rch1 rch2) (d/concat uch1 uchs)]
|
||||
unames (conj unames (:name shape))]
|
||||
(reduce (partial add-svg-child shape-id root-shape) [unames changes] (d/enumerate content))))
|
||||
(defn create-svg-shapes [svg-data x y]
|
||||
(ptk/reify ::create-svg-shapes
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(try
|
||||
(let [page-id (:current-page-id state)
|
||||
objects (dwc/lookup-page-objects state page-id)
|
||||
frame-id (cp/frame-id-by-position objects {:x x :y y})
|
||||
selected (get-in state [:workspace-local :selected])
|
||||
|
||||
unames (dwc/retrieve-used-names objects)
|
||||
[vb-x vb-y vb-width vb-height] (svg-dimensions svg-data)
|
||||
x (- x vb-x (/ vb-width 2))
|
||||
y (- y vb-y (/ vb-height 2))
|
||||
|
||||
svg-name (->> (str/replace (:name data) ".svg" "")
|
||||
(dwc/generate-unique-name unames))
|
||||
unames (dwc/retrieve-used-names objects)
|
||||
|
||||
root-shape (create-raw-svg svg-name frame-id x y width height data)
|
||||
root-id (:id root-shape)
|
||||
svg-name (->> (str/replace (:name svg-data) ".svg" "")
|
||||
(dwc/generate-unique-name unames))
|
||||
|
||||
changes (dwc/add-shape-changes page-id objects selected root-shape)
|
||||
svg-data (-> svg-data
|
||||
(assoc :x x
|
||||
:y y
|
||||
:offset-x vb-x
|
||||
:offset-y vb-y
|
||||
:width vb-width
|
||||
:height vb-height
|
||||
:name svg-name))
|
||||
|
||||
[_ [rchanges uchanges]] (reduce (partial add-svg-child root-id root-shape) [unames changes] (d/enumerate (:content data)))]
|
||||
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})
|
||||
(dwc/select-shapes (d/ordered-set root-id)))))))
|
||||
[def-nodes svg-data] (-> svg-data
|
||||
(usvg/fix-default-values)
|
||||
(usvg/fix-percents)
|
||||
(usvg/extract-defs))
|
||||
|
||||
svg-data (assoc svg-data :defs def-nodes)
|
||||
|
||||
root-shape (create-svg-root frame-id svg-data)
|
||||
root-id (:id root-shape)
|
||||
|
||||
;; Creates the root shape
|
||||
changes (dwc/add-shape-changes page-id objects selected root-shape false)
|
||||
|
||||
;; Reduces the children to create the changes to add the children shapes
|
||||
[_ [rchanges uchanges]]
|
||||
(reduce (partial add-svg-child-changes page-id objects selected frame-id root-id svg-data)
|
||||
[unames changes]
|
||||
(d/enumerate (:content svg-data)))
|
||||
|
||||
reg-objects-action {:type :reg-objects
|
||||
:page-id page-id
|
||||
:shapes (->> rchanges (filter #(= :add-obj (:type %))) (map :id) reverse vec)}
|
||||
|
||||
rchanges (conj rchanges reg-objects-action)]
|
||||
|
||||
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})
|
||||
(dwc/select-shapes (d/ordered-set root-id))))
|
||||
|
||||
(catch :default e
|
||||
(.error js/console "Error upload" e))))))
|
||||
|
|
|
@ -78,7 +78,8 @@
|
|||
(defn ^:export dump-object [name]
|
||||
(let [page-id (get @state :current-page-id)]
|
||||
(let [objects (get-in @state [:workspace-data :pages-index page-id :objects])
|
||||
target (d/seek (fn [[id shape]] (= name (:name shape))) objects)]
|
||||
target (or (d/seek (fn [[id shape]] (= name (:name shape))) objects)
|
||||
(get objects (uuid name)))]
|
||||
(->> target
|
||||
(logjs "state")))))
|
||||
|
||||
|
|
|
@ -19,16 +19,17 @@
|
|||
[app.main.ui.handoff.attributes.shadow :refer [shadow-panel]]
|
||||
[app.main.ui.handoff.attributes.blur :refer [blur-panel]]
|
||||
[app.main.ui.handoff.attributes.image :refer [image-panel]]
|
||||
[app.main.ui.handoff.attributes.text :refer [text-panel]]))
|
||||
[app.main.ui.handoff.attributes.text :refer [text-panel]]
|
||||
[app.main.ui.handoff.attributes.svg :refer [svg-panel]]))
|
||||
|
||||
(def type->options
|
||||
{:multiple [:fill :stroke :image :text :shadow :blur]
|
||||
:frame [:layout :fill]
|
||||
:group [:layout]
|
||||
:rect [:layout :fill :stroke :shadow :blur]
|
||||
:circle [:layout :fill :stroke :shadow :blur]
|
||||
:path [:layout :fill :stroke :shadow :blur]
|
||||
:image [:image :layout :shadow :blur]
|
||||
:group [:layout :svg]
|
||||
:rect [:layout :fill :stroke :shadow :blur :svg]
|
||||
:circle [:layout :fill :stroke :shadow :blur :svg]
|
||||
:path [:layout :fill :stroke :shadow :blur :svg]
|
||||
:image [:image :layout :shadow :blur :svg]
|
||||
:text [:layout :text :shadow :blur]})
|
||||
|
||||
(mf/defc attributes
|
||||
|
@ -46,7 +47,8 @@
|
|||
:shadow shadow-panel
|
||||
:blur blur-panel
|
||||
:image image-panel
|
||||
:text text-panel)
|
||||
:text text-panel
|
||||
:svg svg-panel)
|
||||
{:shapes shapes
|
||||
:frame frame
|
||||
:locale locale}])
|
||||
|
|
|
@ -37,53 +37,55 @@
|
|||
|
||||
(mf/defc layout-block
|
||||
[{:keys [shape locale]}]
|
||||
[:*
|
||||
[:div.attributes-unit-row
|
||||
[:div.attributes-label (t locale "handoff.attributes.layout.width")]
|
||||
[:div.attributes-value (mth/precision (:width shape) 2) "px"]
|
||||
[:& copy-button {:data (copy-data shape :width)}]]
|
||||
|
||||
[:div.attributes-unit-row
|
||||
[:div.attributes-label (t locale "handoff.attributes.layout.height")]
|
||||
[:div.attributes-value (mth/precision (:height shape) 2) "px"]
|
||||
[:& copy-button {:data (copy-data shape :height)}]]
|
||||
|
||||
(when (not= (:x shape) 0)
|
||||
(let [selrect (:selrect shape)
|
||||
{:keys [width height x y]} selrect]
|
||||
[:*
|
||||
[:div.attributes-unit-row
|
||||
[:div.attributes-label (t locale "handoff.attributes.layout.left")]
|
||||
[:div.attributes-value (mth/precision (:x shape) 2) "px"]
|
||||
[:& copy-button {:data (copy-data shape :x)}]])
|
||||
[:div.attributes-label (t locale "handoff.attributes.layout.width")]
|
||||
[:div.attributes-value (mth/precision width 2) "px"]
|
||||
[:& copy-button {:data (copy-data selrect :width)}]]
|
||||
|
||||
(when (not= (:y shape) 0)
|
||||
[:div.attributes-unit-row
|
||||
[:div.attributes-label (t locale "handoff.attributes.layout.top")]
|
||||
[:div.attributes-value (mth/precision (:y shape) 2) "px"]
|
||||
[:& copy-button {:data (copy-data shape :y)}]])
|
||||
[:div.attributes-label (t locale "handoff.attributes.layout.height")]
|
||||
[:div.attributes-value (mth/precision height 2) "px"]
|
||||
[:& copy-button {:data (copy-data selrect :height)}]]
|
||||
|
||||
(when (and (:rx shape) (not= (:rx shape) 0))
|
||||
[:div.attributes-unit-row
|
||||
[:div.attributes-label (t locale "handoff.attributes.layout.radius")]
|
||||
[:div.attributes-value (mth/precision (:rx shape) 2) "px"]
|
||||
[:& copy-button {:data (copy-data shape :rx)}]])
|
||||
(when (not= (:x shape) 0)
|
||||
[:div.attributes-unit-row
|
||||
[:div.attributes-label (t locale "handoff.attributes.layout.left")]
|
||||
[:div.attributes-value (mth/precision x 2) "px"]
|
||||
[:& copy-button {:data (copy-data selrect :x)}]])
|
||||
|
||||
(when (and (:r1 shape)
|
||||
(or (not= (:r1 shape) 0)
|
||||
(not= (:r2 shape) 0)
|
||||
(not= (:r3 shape) 0)
|
||||
(not= (:r4 shape) 0)))
|
||||
[:div.attributes-unit-row
|
||||
[:div.attributes-label (t locale "handoff.attributes.layout.radius")]
|
||||
[:div.attributes-value (mth/precision (:r1 shape) 2) ", "
|
||||
(mth/precision (:r2 shape) 2) ", "
|
||||
(mth/precision (:r3 shape) 2) ", "
|
||||
(mth/precision (:r4 shape) 2) "px"]
|
||||
[:& copy-button {:data (copy-data shape :r1)}]])
|
||||
(when (not= (:y shape) 0)
|
||||
[:div.attributes-unit-row
|
||||
[:div.attributes-label (t locale "handoff.attributes.layout.top")]
|
||||
[:div.attributes-value (mth/precision y 2) "px"]
|
||||
[:& copy-button {:data (copy-data selrect :y)}]])
|
||||
|
||||
(when (not= (:rotation shape 0) 0)
|
||||
[:div.attributes-unit-row
|
||||
[:div.attributes-label (t locale "handoff.attributes.layout.rotation")]
|
||||
[:div.attributes-value (mth/precision (:rotation shape) 2) "deg"]
|
||||
[:& copy-button {:data (copy-data shape :rotation)}]])])
|
||||
(when (and (:rx shape) (not= (:rx shape) 0))
|
||||
[:div.attributes-unit-row
|
||||
[:div.attributes-label (t locale "handoff.attributes.layout.radius")]
|
||||
[:div.attributes-value (mth/precision (:rx shape) 2) "px"]
|
||||
[:& copy-button {:data (copy-data shape :rx)}]])
|
||||
|
||||
(when (and (:r1 shape)
|
||||
(or (not= (:r1 shape) 0)
|
||||
(not= (:r2 shape) 0)
|
||||
(not= (:r3 shape) 0)
|
||||
(not= (:r4 shape) 0)))
|
||||
[:div.attributes-unit-row
|
||||
[:div.attributes-label (t locale "handoff.attributes.layout.radius")]
|
||||
[:div.attributes-value (mth/precision (:r1 shape) 2) ", "
|
||||
(mth/precision (:r2 shape) 2) ", "
|
||||
(mth/precision (:r3 shape) 2) ", "
|
||||
(mth/precision (:r4 shape) 2) "px"]
|
||||
[:& copy-button {:data (copy-data shape :r1)}]])
|
||||
|
||||
(when (not= (:rotation shape 0) 0)
|
||||
[:div.attributes-unit-row
|
||||
[:div.attributes-label (t locale "handoff.attributes.layout.rotation")]
|
||||
[:div.attributes-value (mth/precision (:rotation shape) 2) "deg"]
|
||||
[:& copy-button {:data (copy-data shape :rotation)}]])]))
|
||||
|
||||
|
||||
(mf/defc layout-panel
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
(:require
|
||||
[rumext.alpha :as mf]
|
||||
[cuerdas.core :as str]
|
||||
[app.common.data :as d]
|
||||
[app.util.i18n :refer [t]]
|
||||
[app.util.code-gen :as cg]
|
||||
[app.main.ui.icons :as i]
|
||||
|
@ -40,7 +41,7 @@
|
|||
copy-data (shadow-copy-data shadow)]
|
||||
[:div.attributes-shadow-block
|
||||
[:div.attributes-shadow-row
|
||||
[:div.attributes-label (->> shadow :style name (str "handoff.attributes.shadow.style.") (t locale))]
|
||||
[:div.attributes-label (->> shadow :style d/name (str "handoff.attributes.shadow.style.") (t locale))]
|
||||
[:div.attributes-shadow
|
||||
[:div.attributes-label (t locale "handoff.attributes.shadow.shorthand.offset-x")]
|
||||
[:div.attributes-value (str (:offset-x shadow))]]
|
||||
|
|
|
@ -11,6 +11,8 @@
|
|||
(:require
|
||||
[rumext.alpha :as mf]
|
||||
[cuerdas.core :as str]
|
||||
[app.common.data :as d]
|
||||
[app.common.math :as mth]
|
||||
[app.util.i18n :refer [t]]
|
||||
[app.util.color :as uc]
|
||||
[app.main.ui.icons :as i]
|
||||
|
@ -27,13 +29,15 @@
|
|||
|
||||
(defn format-stroke [shape]
|
||||
(let [width (:stroke-width shape)
|
||||
style (name (:stroke-style shape))
|
||||
style (d/name (:stroke-style shape))
|
||||
style (if (= style "svg") "solid" style)
|
||||
color (-> shape shape->color uc/color->background)]
|
||||
(str/format "%spx %s %s" width style color)))
|
||||
|
||||
(defn has-stroke? [shape]
|
||||
(and (:stroke-style shape)
|
||||
(not= (:stroke-style shape) :none)))
|
||||
(defn has-stroke? [{:keys [stroke-style]}]
|
||||
(and stroke-style
|
||||
(and (not= stroke-style :none)
|
||||
(not= stroke-style :svg))))
|
||||
|
||||
(defn copy-stroke-data [shape]
|
||||
(cg/generate-css-props
|
||||
|
@ -59,12 +63,15 @@
|
|||
:copy-data (copy-color-data shape)
|
||||
:on-change-format #(reset! color-format %)}]
|
||||
|
||||
[:div.attributes-stroke-row
|
||||
[:div.attributes-label (t locale "handoff.attributes.stroke.width")]
|
||||
[:div.attributes-value (:stroke-width shape) "px"]
|
||||
[:div.attributes-value (->> shape :stroke-style name (str "handoff.attributes.stroke.style.") (t locale))]
|
||||
[:div.attributes-label (->> shape :stroke-alignment name (str "handoff.attributes.stroke.alignment.") (t locale))]
|
||||
[:& copy-button {:data (copy-stroke-data shape)}]]]))
|
||||
(let [{:keys [stroke-style stroke-alignment]} shape
|
||||
stroke-style (if (= stroke-style :svg) :solid stroke-style)
|
||||
stroke-alignment (or stroke-alignment :center)]
|
||||
[:div.attributes-stroke-row
|
||||
[:div.attributes-label (t locale "handoff.attributes.stroke.width")]
|
||||
[:div.attributes-value (mth/precision (:stroke-width shape) 2) "px"]
|
||||
[:div.attributes-value (->> stroke-style d/name (str "handoff.attributes.stroke.style.") (t locale))]
|
||||
[:div.attributes-label (->> stroke-alignment d/name (str "handoff.attributes.stroke.alignment.") (t locale))]
|
||||
[:& copy-button {:data (copy-stroke-data shape)}]])]))
|
||||
|
||||
(mf/defc stroke-panel
|
||||
[{:keys [shapes locale]}]
|
||||
|
|
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)
|
||||
:value (:suffix export)}]
|
||||
[:select.input-select {:on-change (partial on-type-change index)
|
||||
:value (name (:type export))}
|
||||
:value (d/name (:type export))}
|
||||
[:option {:value "png"} "PNG"]
|
||||
[:option {:value "jpeg"} "JPEG"]
|
||||
[:option {:value "svg"} "SVG"]]
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
[rumext.alpha :as mf]
|
||||
[okulary.core :as l]
|
||||
[app.util.i18n :refer [t] :as i18n]
|
||||
[app.common.data :as d]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.main.ui.components.tab-container :refer [tab-container tab-element]]
|
||||
|
@ -49,7 +50,7 @@
|
|||
[:*
|
||||
[:span.tool-window-bar-icon
|
||||
[:& element-icon {:shape (-> shapes first)}]]
|
||||
[:span.tool-window-bar-title (->> selected-type name (str "handoff.tabs.code.selected.") (t locale))]])
|
||||
[:span.tool-window-bar-title (->> selected-type d/name (str "handoff.tabs.code.selected.") (t locale))]])
|
||||
]
|
||||
[:div.tool-window-content
|
||||
[:& tab-container {:on-change-tab #(do
|
||||
|
|
|
@ -12,7 +12,8 @@
|
|||
[rumext.alpha :as mf]
|
||||
[cuerdas.core :as str]
|
||||
[app.util.object :as obj]
|
||||
[app.main.ui.context :as muc]))
|
||||
[app.main.ui.context :as muc]
|
||||
[app.util.svg :as usvg]))
|
||||
|
||||
(defn- stroke-type->dasharray
|
||||
[style]
|
||||
|
@ -74,45 +75,81 @@
|
|||
attrs)))
|
||||
|
||||
(defn add-fill [attrs shape render-id]
|
||||
(let [fill-color-gradient-id (str "fill-color-gradient_" render-id)]
|
||||
(cond
|
||||
(:fill-color-gradient shape)
|
||||
(obj/merge! attrs #js {:fill (str/format "url(#%s)" fill-color-gradient-id)})
|
||||
(let [fill-attrs (cond
|
||||
(contains? shape :fill-color-gradient)
|
||||
(let [fill-color-gradient-id (str "fill-color-gradient_" render-id)]
|
||||
{:fill (str/format "url(#%s)" fill-color-gradient-id)})
|
||||
|
||||
(and (not= :svg-raw (:type shape))
|
||||
(not (:fill-color-gradient shape)))
|
||||
(obj/merge! attrs #js {:fill (or (:fill-color shape) "transparent")
|
||||
:fillOpacity (:fill-opacity shape nil)})
|
||||
(contains? shape :fill-color)
|
||||
{:fill (:fill-color shape)}
|
||||
|
||||
(and (= :svg-raw (:type shape))
|
||||
(or (:fill-opacity shape) (:fill-color shape)))
|
||||
(obj/merge! attrs #js {:fill (:fill-color shape)
|
||||
:fillOpacity (:fill-opacity shape nil)})
|
||||
;; If contains svg-attrs the origin is svg. If it's not svg origin
|
||||
;; we setup the default fill as transparent (instead of black)
|
||||
(and (not (contains? shape :svg-attrs))
|
||||
(not (= :svg-raw (:type shape))))
|
||||
{:fill "transparent"}
|
||||
|
||||
:else attrs)))
|
||||
:else
|
||||
{})
|
||||
|
||||
fill-attrs (cond-> fill-attrs
|
||||
(contains? shape :fill-opacity)
|
||||
(assoc :fillOpacity (:fill-opacity shape)))]
|
||||
|
||||
(obj/merge! attrs (clj->js fill-attrs))))
|
||||
|
||||
(defn add-stroke [attrs shape render-id]
|
||||
(let [stroke-style (:stroke-style shape :none)
|
||||
stroke-color-gradient-id (str "stroke-color-gradient_" render-id)]
|
||||
(if (not= stroke-style :none)
|
||||
(if (:stroke-color-gradient shape)
|
||||
(obj/merge! attrs
|
||||
#js {:stroke (str/format "url(#%s)" stroke-color-gradient-id)
|
||||
:strokeWidth (:stroke-width shape 1)
|
||||
:strokeDasharray (stroke-type->dasharray stroke-style)})
|
||||
(obj/merge! attrs
|
||||
#js {:stroke (:stroke-color shape nil)
|
||||
:strokeWidth (:stroke-width shape 1)
|
||||
:strokeOpacity (:stroke-opacity shape nil)
|
||||
:strokeDasharray (stroke-type->dasharray stroke-style)}))))
|
||||
attrs)
|
||||
(let [stroke-attrs
|
||||
(cond-> {:strokeWidth (:stroke-width shape 1)}
|
||||
(:stroke-color-gradient shape)
|
||||
(assoc :stroke (str/format "url(#%s)" stroke-color-gradient-id))
|
||||
|
||||
(and (not (:stroke-color-gradient shape))
|
||||
(:stroke-color shape nil))
|
||||
(assoc :stroke (:stroke-color shape nil))
|
||||
|
||||
(and (not (:stroke-color-gradient shape))
|
||||
(:stroke-opacity shape nil))
|
||||
(assoc :strokeOpacity (:stroke-opacity shape nil))
|
||||
|
||||
(not= stroke-style :svg)
|
||||
(assoc :strokeDasharray (stroke-type->dasharray stroke-style)))]
|
||||
(obj/merge! attrs (clj->js stroke-attrs)))
|
||||
attrs)))
|
||||
|
||||
(defn extract-svg-attrs
|
||||
[render-id svg-defs svg-attrs]
|
||||
(let [replace-id (fn [id]
|
||||
(if (contains? svg-defs id)
|
||||
(str render-id "-" id)
|
||||
id))
|
||||
svg-attrs (-> svg-attrs
|
||||
(usvg/clean-attrs)
|
||||
(usvg/update-attr-ids replace-id))
|
||||
|
||||
attrs (-> svg-attrs (dissoc :style) (clj->js))
|
||||
styles (-> svg-attrs (:style {}) (clj->js))]
|
||||
[attrs styles]))
|
||||
|
||||
(defn extract-style-attrs
|
||||
([shape]
|
||||
(let [render-id (mf/use-ctx muc/render-ctx)
|
||||
svg-defs (:svg-defs shape {})
|
||||
svg-attrs (:svg-attrs shape {})
|
||||
|
||||
[svg-attrs svg-styles] (mf/use-memo
|
||||
(mf/deps render-id svg-defs svg-attrs)
|
||||
#(extract-svg-attrs render-id svg-defs svg-attrs))
|
||||
|
||||
styles (-> (obj/new)
|
||||
(obj/merge! svg-styles)
|
||||
(add-fill shape render-id)
|
||||
(add-stroke shape render-id))]
|
||||
|
||||
(-> (obj/new)
|
||||
(obj/merge! svg-attrs)
|
||||
(add-border-radius shape)
|
||||
(obj/set! "style" styles)))))
|
||||
|
|
|
@ -9,7 +9,9 @@
|
|||
|
||||
(ns app.main.ui.shapes.group
|
||||
(:require
|
||||
[app.util.object :as obj]
|
||||
[rumext.alpha :as mf]
|
||||
[app.main.ui.shapes.attrs :as attrs]
|
||||
[app.main.ui.shapes.mask :refer [mask-str mask-factory]]))
|
||||
|
||||
(defn group-shape
|
||||
|
@ -28,12 +30,14 @@
|
|||
|
||||
show-mask? (and (:masked-group? shape) (not expand-mask))
|
||||
mask (when show-mask? (first childs))
|
||||
childs (if show-mask? (rest childs) childs)]
|
||||
childs (if show-mask? (rest childs) childs)
|
||||
|
||||
[:g.group
|
||||
{:pointer-events pointer-events
|
||||
:mask (when (and mask (not expand-mask)) (mask-str mask))}
|
||||
props (-> (attrs/extract-style-attrs shape)
|
||||
(obj/merge!
|
||||
#js {:pointerEvents pointer-events
|
||||
:mask (when (and mask (not expand-mask)) (mask-str mask))}))]
|
||||
|
||||
[:> :g props
|
||||
(when mask
|
||||
[:> render-mask #js {:frame frame :mask mask}])
|
||||
|
||||
|
|
|
@ -9,13 +9,12 @@
|
|||
|
||||
(ns app.main.ui.shapes.shape
|
||||
(:require
|
||||
[app.common.geom.shapes :as geom]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.ui.context :as muc]
|
||||
[app.main.ui.shapes.filters :as filters]
|
||||
[app.main.ui.shapes.gradients :as grad]
|
||||
[app.main.ui.shapes.svg-defs :as defs]
|
||||
[app.util.object :as obj]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(mf/defc shape-container
|
||||
|
@ -48,6 +47,7 @@
|
|||
[:& (mf/provider muc/render-ctx) {:value render-id}
|
||||
[:> wrapper-tag group-props
|
||||
[:defs
|
||||
[:& defs/svg-defs {:shape shape :render-id render-id}]
|
||||
[:& filters/filters {:shape shape :filter-id filter-id}]
|
||||
[:& grad/gradient {:shape shape :attr :fill-color-gradient}]
|
||||
[:& grad/gradient {:shape shape :attr :stroke-color-gradient}]]
|
||||
|
|
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.point :as gpt]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.ui.shapes.attrs :as usa]
|
||||
[app.util.data :as ud]
|
||||
[app.util.object :as obj]
|
||||
|
@ -21,35 +20,15 @@
|
|||
[cuerdas.core :as str]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
;; Graphic tags
|
||||
(defonce graphic-element? #{:circle :ellipse :image :line :path :polygon :polyline :rect :text :use})
|
||||
|
||||
;; Context to store a re-mapping of the ids
|
||||
(def svg-ids-ctx (mf/create-context nil))
|
||||
|
||||
(defn generate-id-mapping [content]
|
||||
(letfn [(visit-node [result node]
|
||||
(let [element-id (get-in node [:attrs :id])
|
||||
result (cond-> result
|
||||
element-id (assoc element-id (str (uuid/next))))]
|
||||
(reduce visit-node result (:content node))))]
|
||||
(visit-node {} content)))
|
||||
|
||||
|
||||
(defonce replace-regex #"[^#]*#([^)\s]+).*")
|
||||
|
||||
(defn replace-attrs-ids
|
||||
"Replaces the ids inside a property"
|
||||
[ids-mapping attrs]
|
||||
|
||||
(letfn [(replace-ids [key val]
|
||||
(if (map? val)
|
||||
(cd/mapm replace-ids val)
|
||||
(let [[_ from-id] (re-matches replace-regex val)]
|
||||
(if (and from-id (contains? ids-mapping from-id))
|
||||
(str/replace val from-id (get ids-mapping from-id))
|
||||
val))))]
|
||||
(cd/mapm replace-ids attrs)))
|
||||
|
||||
(defn set-styles [attrs shape]
|
||||
(let [custom-attrs (usa/extract-style-attrs shape)
|
||||
(let [custom-attrs (-> (usa/extract-style-attrs shape)
|
||||
(obj/without ["transform"]))
|
||||
attrs (cond-> attrs
|
||||
(string? (:style attrs)) usvg/clean-attrs)
|
||||
style (obj/merge! (clj->js (:style attrs {}))
|
||||
|
@ -58,6 +37,14 @@
|
|||
(obj/merge! custom-attrs)
|
||||
(obj/set! "style" style))))
|
||||
|
||||
(defn translate-shape [attrs shape]
|
||||
(let [transform (str (usvg/svg-transform-matrix shape)
|
||||
" "
|
||||
(:transform attrs ""))]
|
||||
(cond-> attrs
|
||||
(and (:svg-viewbox shape) (graphic-element? (-> shape :content :tag)))
|
||||
(assoc :transform transform))))
|
||||
|
||||
(mf/defc svg-root
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
|
@ -68,7 +55,7 @@
|
|||
{:keys [x y width height]} shape
|
||||
{:keys [tag attrs] :as content} (:content shape)
|
||||
|
||||
ids-mapping (mf/use-memo #(generate-id-mapping content))
|
||||
ids-mapping (mf/use-memo #(usvg/generate-id-mapping content))
|
||||
|
||||
attrs (-> (set-styles attrs shape)
|
||||
(obj/set! "x" x)
|
||||
|
@ -91,11 +78,16 @@
|
|||
{:keys [attrs tag]} content
|
||||
|
||||
ids-mapping (mf/use-ctx svg-ids-ctx)
|
||||
attrs (mf/use-memo #(replace-attrs-ids ids-mapping attrs))
|
||||
element-id (get-in content [:attrs :id])
|
||||
|
||||
attrs (mf/use-memo #(usvg/replace-attrs-ids attrs ids-mapping))
|
||||
|
||||
attrs (translate-shape attrs shape)
|
||||
element-id (get-in content [:attrs :id])
|
||||
attrs (cond-> (set-styles attrs shape)
|
||||
element-id (obj/set! "id" (get ids-mapping element-id)))]
|
||||
(and element-id (contains? ids-mapping element-id))
|
||||
(obj/set! "id" (get ids-mapping element-id)))
|
||||
|
||||
{:keys [x y width height]} (:selrect shape)]
|
||||
[:> (name tag) attrs children]))
|
||||
|
||||
(defn svg-raw-shape [shape-wrapper]
|
||||
|
|
|
@ -44,8 +44,6 @@
|
|||
:style {:stroke color
|
||||
:fill "transparent"
|
||||
:stroke-width "1px"
|
||||
:stroke-opacity 0.5
|
||||
:stroke-dasharray 4
|
||||
:pointer-events "none"}}])
|
||||
|
||||
(mf/defc render-rect-points [{:keys [points color]}]
|
||||
|
@ -62,7 +60,7 @@
|
|||
[props]
|
||||
(let [shape (-> (unchecked-get props "shape"))
|
||||
frame (unchecked-get props "frame")
|
||||
selrect (gsh/points->selrect (-> shape :points))
|
||||
bounding-box (gsh/points->selrect (-> shape :points))
|
||||
shape-center (gsh/center-shape shape)
|
||||
line-color (rdcolor #js {:seed (str (:id shape))})
|
||||
zoom (mf/deref refs/selected-zoom)
|
||||
|
@ -71,25 +69,30 @@
|
|||
(map gsh/transform-shape))]
|
||||
|
||||
[:g.bounding-box
|
||||
[:text {:x (:x selrect)
|
||||
:y (- (:y selrect) 5)
|
||||
[:text {:x (:x bounding-box)
|
||||
:y (- (:y bounding-box) 5)
|
||||
:font-size 10
|
||||
:fill line-color
|
||||
:stroke "white"
|
||||
:stroke-width 0.1}
|
||||
(str/format "%s - (%s, %s)" (str/slice (str (:id shape)) 0 8) (fixed (:x selrect)) (fixed (:y selrect)))]
|
||||
(str/format "%s - (%s, %s)" (str/slice (str (:id shape)) 0 8) (fixed (:x bounding-box)) (fixed (:y bounding-box)))]
|
||||
|
||||
[:& cross-point {:point shape-center
|
||||
:zoom zoom
|
||||
:color line-color}]
|
||||
[:g.center
|
||||
[:& cross-point {:point shape-center
|
||||
:zoom zoom
|
||||
:color line-color}]]
|
||||
|
||||
[:g.points
|
||||
(for [point (:points shape)]
|
||||
[:& cross-point {:point point
|
||||
:zoom zoom
|
||||
:color line-color}])
|
||||
#_[:& render-rect-points {:points (:points shape)
|
||||
:color line-color}]]
|
||||
|
||||
(for [point (:points shape)]
|
||||
[:& cross-point {:point point
|
||||
:zoom zoom
|
||||
:color line-color}])
|
||||
|
||||
[:& render-rect-points {:points (:points shape)
|
||||
:color line-color}]
|
||||
|
||||
[:& render-rect {:rect selrect
|
||||
:color line-color}]]))
|
||||
[:g.selrect
|
||||
[:& render-rect {:rect (:selrect shape)
|
||||
;; :transform (gsh/transform-matrix shape)
|
||||
:color line-color}]
|
||||
#_[:& render-rect {:rect bounding-box
|
||||
:color line-color}]]]))
|
||||
|
|
|
@ -33,7 +33,6 @@
|
|||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [shape (unchecked-get props "shape")
|
||||
hover? (or (mf/deref refs/current-hover) #{})
|
||||
content-modifiers-ref (pc/make-content-modifiers-ref (:id shape))
|
||||
content-modifiers (mf/deref content-modifiers-ref)
|
||||
editing-id (mf/deref refs/selected-edition)
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
|
||||
;; This is a list of svg tags that can be grouped in shape-container
|
||||
;; this allows them to have gradients, shadows and masks
|
||||
(def svg-elements #{:svg :circle :ellipse :image :line :path :polygon :polyline :rect :symbol :text :textPath})
|
||||
(def svg-elements #{:svg :circle :ellipse :image :line :path :polygon :polyline :rect :symbol :text :textPath :use})
|
||||
|
||||
(defn svg-raw-wrapper-factory
|
||||
[shape-wrapper]
|
||||
|
@ -57,19 +57,19 @@
|
|||
:shape shape
|
||||
:childs childs}]
|
||||
|
||||
(when (= tag :svg)
|
||||
[:rect.group-actions
|
||||
{:x x
|
||||
:y y
|
||||
:transform transform
|
||||
:width width
|
||||
:height height
|
||||
:fill "transparent"
|
||||
:on-mouse-down handle-mouse-down
|
||||
:on-double-click handle-double-click
|
||||
:on-context-menu handle-context-menu
|
||||
:on-pointer-over handle-pointer-enter
|
||||
:on-pointer-out handle-pointer-leave}])]
|
||||
[:rect.actions
|
||||
{:x x
|
||||
:y y
|
||||
:transform transform
|
||||
:width width
|
||||
:height height
|
||||
:fill "transparent"
|
||||
:stroke "none"
|
||||
:on-mouse-down handle-mouse-down
|
||||
:on-double-click handle-double-click
|
||||
:on-context-menu handle-context-menu
|
||||
:on-pointer-over handle-pointer-enter
|
||||
:on-pointer-out handle-pointer-leave}]]
|
||||
|
||||
;; We cannot wrap inside groups the shapes that go inside the defs tag
|
||||
;; we use the context so we know when we should not render the container
|
||||
|
|
|
@ -14,7 +14,8 @@
|
|||
[app.main.ui.workspace.sidebar.options.fill :refer [fill-attrs fill-menu]]
|
||||
[app.main.ui.workspace.sidebar.options.stroke :refer [stroke-attrs stroke-menu]]
|
||||
[app.main.ui.workspace.sidebar.options.shadow :refer [shadow-menu]]
|
||||
[app.main.ui.workspace.sidebar.options.blur :refer [blur-menu]]))
|
||||
[app.main.ui.workspace.sidebar.options.blur :refer [blur-menu]]
|
||||
[app.main.ui.workspace.sidebar.options.svg-attrs :refer [svg-attrs-menu]]))
|
||||
|
||||
(mf/defc options
|
||||
[{:keys [shape] :as props}]
|
||||
|
@ -36,4 +37,6 @@
|
|||
[:& shadow-menu {:ids ids
|
||||
:values (select-keys shape [:shadow])}]
|
||||
[:& blur-menu {:ids ids
|
||||
:values (select-keys shape [:blur])}]]))
|
||||
:values (select-keys shape [:blur])}]
|
||||
[:& svg-attrs-menu {:ids ids
|
||||
:values (select-keys shape [:svg-attrs])}]]))
|
||||
|
|
|
@ -19,7 +19,8 @@
|
|||
[app.main.ui.workspace.sidebar.options.blur :refer [blur-menu]]
|
||||
[app.main.ui.workspace.sidebar.options.shadow :refer [shadow-menu]]
|
||||
[app.main.ui.workspace.sidebar.options.stroke :refer [stroke-menu]]
|
||||
[app.main.ui.workspace.sidebar.options.text :as ot]))
|
||||
[app.main.ui.workspace.sidebar.options.text :as ot]
|
||||
[app.main.ui.workspace.sidebar.options.svg-attrs :refer [svg-attrs-menu]]))
|
||||
|
||||
(mf/defc options
|
||||
{::mf/wrap [mf/memo]
|
||||
|
@ -36,6 +37,7 @@
|
|||
[blur-ids blur-values] (get-attrs [shape] objects :blur)
|
||||
[stroke-ids stroke-values] (get-attrs [shape] objects :stroke)
|
||||
[text-ids text-values] (get-attrs [shape] objects :text)
|
||||
[svg-ids svg-values] [[(:id shape)] (select-keys shape [:svg-attrs])]
|
||||
[comp-ids comp-values] [[(:id shape)] (select-keys shape component-attrs)]]
|
||||
|
||||
[:div.options
|
||||
|
@ -55,6 +57,10 @@
|
|||
[:& stroke-menu {:type type :ids stroke-ids :values stroke-values}])
|
||||
|
||||
(when-not (empty? text-ids)
|
||||
[:& ot/text-menu {:type type :ids text-ids :values text-values}])]))
|
||||
[:& ot/text-menu {:type type :ids text-ids :values text-values}])
|
||||
|
||||
(when-not (empty? svg-values)
|
||||
[:& svg-attrs-menu {:ids svg-ids
|
||||
:values svg-values}])]))
|
||||
|
||||
|
||||
|
|
|
@ -15,7 +15,8 @@
|
|||
[app.main.ui.workspace.sidebar.options.fill :refer [fill-attrs fill-menu]]
|
||||
[app.main.ui.workspace.sidebar.options.stroke :refer [stroke-attrs stroke-menu]]
|
||||
[app.main.ui.workspace.sidebar.options.shadow :refer [shadow-menu]]
|
||||
[app.main.ui.workspace.sidebar.options.blur :refer [blur-menu]]))
|
||||
[app.main.ui.workspace.sidebar.options.blur :refer [blur-menu]]
|
||||
[app.main.ui.workspace.sidebar.options.svg-attrs :refer [svg-attrs-menu]]))
|
||||
|
||||
(mf/defc options
|
||||
[{:keys [shape] :as props}]
|
||||
|
@ -36,4 +37,6 @@
|
|||
[:& shadow-menu {:ids ids
|
||||
:values (select-keys shape [:shadow])}]
|
||||
[:& blur-menu {:ids ids
|
||||
:values (select-keys shape [:blur])}]]))
|
||||
:values (select-keys shape [:blur])}]
|
||||
[:& svg-attrs-menu {:ids ids
|
||||
:values (select-keys shape [:svg-attrs])}]]))
|
||||
|
|
|
@ -14,7 +14,8 @@
|
|||
[app.main.ui.workspace.sidebar.options.fill :refer [fill-attrs fill-menu]]
|
||||
[app.main.ui.workspace.sidebar.options.stroke :refer [stroke-attrs stroke-menu]]
|
||||
[app.main.ui.workspace.sidebar.options.shadow :refer [shadow-menu]]
|
||||
[app.main.ui.workspace.sidebar.options.blur :refer [blur-menu]]))
|
||||
[app.main.ui.workspace.sidebar.options.blur :refer [blur-menu]]
|
||||
[app.main.ui.workspace.sidebar.options.svg-attrs :refer [svg-attrs-menu]]))
|
||||
|
||||
(mf/defc options
|
||||
{::mf/wrap [mf/memo]}
|
||||
|
@ -41,4 +42,7 @@
|
|||
:values (select-keys shape [:shadow])}]
|
||||
|
||||
[:& blur-menu {:ids ids
|
||||
:values (select-keys shape [:blur])}]]))
|
||||
:values (select-keys shape [:blur])}]
|
||||
|
||||
[:& svg-attrs-menu {:ids ids
|
||||
:values (select-keys shape [:svg-attrs])}]]))
|
||||
|
|
|
@ -36,6 +36,11 @@
|
|||
:placeholder placeholder
|
||||
:on-change on-change}]
|
||||
|
||||
:text
|
||||
[:input {:value value
|
||||
:class "input-text"
|
||||
:on-change on-change} ]
|
||||
|
||||
[:> numeric-input {:placeholder placeholder
|
||||
:min min
|
||||
:max max
|
||||
|
|
|
@ -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.stroke :refer [stroke-attrs stroke-menu]]
|
||||
[app.main.ui.workspace.sidebar.options.shadow :refer [shadow-menu]]
|
||||
[app.main.ui.workspace.sidebar.options.blur :refer [blur-menu]]))
|
||||
[app.main.ui.workspace.sidebar.options.blur :refer [blur-menu]]
|
||||
[app.main.ui.workspace.sidebar.options.svg-attrs :refer [svg-attrs-menu]]))
|
||||
|
||||
;; This is a list of svg tags that can be grouped in shape-container
|
||||
;; this allows them to have gradients, shadows and masks
|
||||
(def svg-elements #{:svg :circle :ellipse :image :line :path :polygon :polyline :rect :symbol :text :textPath})
|
||||
(def svg-elements #{:svg :g :circle :ellipse :image :line :path :polygon :polyline :rect :symbol :text :textPath})
|
||||
|
||||
(defn hex->number [hex] 1)
|
||||
(defn shorthex->longhex [hex]
|
||||
|
@ -99,11 +100,9 @@
|
|||
|
||||
(when (contains? svg-elements tag)
|
||||
[:*
|
||||
(when (= tag :svg)
|
||||
[:*
|
||||
[:& measures-menu {:ids ids
|
||||
:type type
|
||||
:values measure-values}]])
|
||||
[:& measures-menu {:ids ids
|
||||
:type type
|
||||
:values measure-values}]
|
||||
|
||||
[:& fill-menu {:ids ids
|
||||
:type type
|
||||
|
@ -115,4 +114,7 @@
|
|||
:values (select-keys shape [:shadow])}]
|
||||
|
||||
[:& blur-menu {:ids ids
|
||||
:values (select-keys shape [:blur])}]])))
|
||||
:values (select-keys shape [:blur])}]
|
||||
|
||||
[:& svg-attrs-menu {:ids ids
|
||||
:values (select-keys shape [:svg-attrs])}]])))
|
||||
|
|
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)
|
||||
(= file-id :multiple)))
|
||||
|
||||
(defn color? [^string color-str]
|
||||
(and (not (nil? color-str))
|
||||
(not (empty? color-str))
|
||||
(gcolor/isValidColor color-str)))
|
||||
|
||||
(defn parse-color [^string color-str]
|
||||
(let [result (gcolor/parse color-str)]
|
||||
(str (.-hex ^js result))))
|
||||
|
|
|
@ -9,12 +9,22 @@
|
|||
|
||||
(ns app.util.geom.path
|
||||
(:require
|
||||
[cuerdas.core :as str]
|
||||
[app.common.data :as cd]
|
||||
[app.util.data :as d]
|
||||
[app.common.data :as cd]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.util.geom.path-impl-simplify :as impl-simplify]))
|
||||
[app.util.a2c :refer [a2c]]
|
||||
[app.util.data :as d]
|
||||
[app.util.geom.path-impl-simplify :as impl-simplify]
|
||||
[app.util.svg :as usvg]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(defn calculate-opposite-handler
|
||||
"Given a point and its handler, gives the symetric handler"
|
||||
[point handler]
|
||||
(let [handler-vector (gpt/to-vec point handler)]
|
||||
(gpt/add point (gpt/negate handler-vector))))
|
||||
|
||||
;;;
|
||||
|
||||
(defn simplify
|
||||
([points]
|
||||
|
@ -28,23 +38,37 @@
|
|||
|
||||
;; Matches numbers for path values allows values like... -.01, 10, +12.22
|
||||
;; 0 and 1 are special because can refer to flags
|
||||
(def num-regex #"([+-]?(([1-9]\d*(\.\d+)?)|(\.\d+)|0|1))")
|
||||
(def num-regex #"[+-]?(\d+(\.\d+)?|\.\d+)")
|
||||
|
||||
(def flag-regex #"[01]")
|
||||
|
||||
(defn coord-n [size]
|
||||
(re-pattern (str "(?i)[a-z]\\s*"
|
||||
(->> (range size)
|
||||
(map #(identity num-regex))
|
||||
(str/join "\\s+")))))
|
||||
(defn extract-params [cmd-str extract-commands]
|
||||
(loop [result []
|
||||
extract-idx 0
|
||||
current {}
|
||||
remain (-> cmd-str (subs 1) (str/trim))]
|
||||
|
||||
(let [[param type] (nth extract-commands extract-idx)
|
||||
regex (case type
|
||||
:flag flag-regex
|
||||
#_:number num-regex)
|
||||
match (re-find regex remain)]
|
||||
|
||||
(defn parse-params [cmd-str num-params]
|
||||
(let [fix-starting-dot (fn [arg] (str/replace arg #"([^\d]|^)\." "$10."))]
|
||||
(->> (re-seq num-regex cmd-str)
|
||||
(map first)
|
||||
(map fix-starting-dot)
|
||||
(map d/read-string)
|
||||
(partition num-params))))
|
||||
(if match
|
||||
(let [value (-> match first usvg/fix-dot-number d/read-string)
|
||||
remain (str/replace-first remain regex "")
|
||||
current (assoc current param value)
|
||||
extract-idx (inc extract-idx)
|
||||
[result current extract-idx]
|
||||
(if (>= extract-idx (count extract-commands))
|
||||
[(conj result current) {} 0]
|
||||
[result current extract-idx])]
|
||||
(recur result
|
||||
extract-idx
|
||||
current
|
||||
remain))
|
||||
(cond-> result
|
||||
(not (empty? current)) (conj current))))))
|
||||
|
||||
(defn command->param-list [{:keys [command params]}]
|
||||
(case command
|
||||
|
@ -73,96 +97,99 @@
|
|||
|
||||
(defmethod parse-command "M" [cmd]
|
||||
(let [relative (str/starts-with? cmd "m")
|
||||
params (parse-params cmd 2)]
|
||||
(for [[x y] params]
|
||||
param-list (extract-params cmd [[:x :number]
|
||||
[:y :number]])]
|
||||
(for [params param-list]
|
||||
{:command :move-to
|
||||
:relative relative
|
||||
:params {:x x :y y}})))
|
||||
:params params})))
|
||||
|
||||
(defmethod parse-command "Z" [cmd]
|
||||
[{:command :close-path}])
|
||||
|
||||
(defmethod parse-command "L" [cmd]
|
||||
(let [relative (str/starts-with? cmd "l")
|
||||
params (parse-params cmd 2)]
|
||||
(for [[x y] params]
|
||||
param-list (extract-params cmd [[:x :number]
|
||||
[:y :number]])]
|
||||
(for [params param-list]
|
||||
{:command :line-to
|
||||
:relative relative
|
||||
:params {:x x :y y}})))
|
||||
:params params})))
|
||||
|
||||
(defmethod parse-command "H" [cmd]
|
||||
(let [relative (str/starts-with? cmd "h")
|
||||
params (parse-params cmd 1)]
|
||||
(for [[value] params]
|
||||
param-list (extract-params cmd [[:value :number]])]
|
||||
(for [params param-list]
|
||||
{:command :line-to-horizontal
|
||||
:relative relative
|
||||
:params {:value value}})))
|
||||
:params params})))
|
||||
|
||||
(defmethod parse-command "V" [cmd]
|
||||
(let [relative (str/starts-with? cmd "v")
|
||||
params (parse-params cmd 1)]
|
||||
(for [[value] params]
|
||||
param-list (extract-params cmd [[:value :number]])]
|
||||
(for [params param-list]
|
||||
{:command :line-to-vertical
|
||||
:relative relative
|
||||
:params {:value value}})))
|
||||
:params params})))
|
||||
|
||||
(defmethod parse-command "C" [cmd]
|
||||
(let [relative (str/starts-with? cmd "c")
|
||||
params (parse-params cmd 6)]
|
||||
(for [[c1x c1y c2x c2y x y] params]
|
||||
param-list (extract-params cmd [[:c1x :number]
|
||||
[:c1y :number]
|
||||
[:c2x :number]
|
||||
[:c2y :number]
|
||||
[:x :number]
|
||||
[:y :number]])
|
||||
]
|
||||
(for [params param-list]
|
||||
{:command :curve-to
|
||||
:relative relative
|
||||
:params {:c1x c1x
|
||||
:c1y c1y
|
||||
:c2x c2x
|
||||
:c2y c2y
|
||||
:x x
|
||||
:y y}})))
|
||||
:params params})))
|
||||
|
||||
(defmethod parse-command "S" [cmd]
|
||||
(let [relative (str/starts-with? cmd "s")
|
||||
params (parse-params cmd 4)]
|
||||
(for [[cx cy x y] params]
|
||||
param-list (extract-params cmd [[:cx :number]
|
||||
[:cy :number]
|
||||
[:x :number]
|
||||
[:y :number]])]
|
||||
(for [params param-list]
|
||||
{:command :smooth-curve-to
|
||||
:relative relative
|
||||
:params {:cx cx
|
||||
:cy cy
|
||||
:x x
|
||||
:y y}})))
|
||||
:params params})))
|
||||
|
||||
(defmethod parse-command "Q" [cmd]
|
||||
(let [relative (str/starts-with? cmd "s")
|
||||
params (parse-params cmd 4)]
|
||||
(for [[cx cy x y] params]
|
||||
param-list (extract-params cmd [[:cx :number]
|
||||
[:cy :number]
|
||||
[:x :number]
|
||||
[:y :number]])]
|
||||
(for [params param-list]
|
||||
{:command :quadratic-bezier-curve-to
|
||||
:relative relative
|
||||
:params {:cx cx
|
||||
:cy cy
|
||||
:x x
|
||||
:y y}})))
|
||||
:params params})))
|
||||
|
||||
(defmethod parse-command "T" [cmd]
|
||||
(let [relative (str/starts-with? cmd "t")
|
||||
params (parse-params cmd (coord-n 2))]
|
||||
(for [[cx cy x y] params]
|
||||
param-list (extract-params cmd [[:x :number]
|
||||
[:y :number]])]
|
||||
(for [params param-list]
|
||||
{:command :smooth-quadratic-bezier-curve-to
|
||||
:relative relative
|
||||
:params {:x x
|
||||
:y y}})))
|
||||
:params params})))
|
||||
|
||||
(defmethod parse-command "A" [cmd]
|
||||
(let [relative (str/starts-with? cmd "a")
|
||||
params (parse-params cmd 7)]
|
||||
(for [[rx ry x-axis-rotation large-arc-flag sweep-flag x y] params]
|
||||
param-list (extract-params cmd [[:rx :number]
|
||||
[:ry :number]
|
||||
[:x-axis-rotation :number]
|
||||
[:large-arc-flag :flag]
|
||||
[:sweep-flag :flag]
|
||||
[:x :number]
|
||||
[:y :number]])]
|
||||
(for [params param-list]
|
||||
{:command :elliptical-arc
|
||||
:relative relative
|
||||
:params {:rx rx
|
||||
:ry ry
|
||||
:x-axis-rotation x-axis-rotation
|
||||
:large-arc-flag large-arc-flag
|
||||
:sweep-flag sweep-flag
|
||||
:x x
|
||||
:y y}})))
|
||||
:params params})))
|
||||
|
||||
(defn command->string [{:keys [command relative params] :as entry}]
|
||||
(let [command-str (case command
|
||||
|
@ -180,46 +207,143 @@
|
|||
param-list (command->param-list entry)]
|
||||
(str/fmt "%s%s" command-str (str/join " " param-list))))
|
||||
|
||||
(defn cmd-pos [{:keys [params]}]
|
||||
(when (and (contains? params :x)
|
||||
(contains? params :y))
|
||||
(gpt/point params)))
|
||||
(defn cmd-pos [prev-pos {:keys [relative params]}]
|
||||
(let [{:keys [x y] :or {x (:x prev-pos) y (:y prev-pos)}} params]
|
||||
(if relative
|
||||
(-> prev-pos (update :x + x) (update :y + y))
|
||||
(gpt/point x y))))
|
||||
|
||||
(defn arc->beziers [from-p command]
|
||||
(let [to-command
|
||||
(fn [[_ _ c1x c1y c2x c2y x y]]
|
||||
{:command :curve-to
|
||||
:relative (:relative command)
|
||||
:params {:c1x c1x :c1y c1y
|
||||
:c2x c2x :c2y c2y
|
||||
:x x :y y}})
|
||||
|
||||
{from-x :x from-y :y} from-p
|
||||
{:keys [rx ry x-axis-rotation large-arc-flag sweep-flag x y]} (:params command)
|
||||
result (a2c from-x from-y x y large-arc-flag sweep-flag rx ry x-axis-rotation)]
|
||||
|
||||
(mapv to-command result)))
|
||||
|
||||
(defn smooth->curve
|
||||
[{:keys [params]} pos handler]
|
||||
(let [{c1x :x c1y :y} (calculate-opposite-handler pos handler)]
|
||||
{:c1x c1x
|
||||
:c1y c1y
|
||||
:c2x (:cx params)
|
||||
:c2y (:cy params)}))
|
||||
|
||||
(defn quadratic->curve
|
||||
[sp ep cp]
|
||||
(let [cp1 (-> (gpt/to-vec sp cp)
|
||||
(gpt/scale (/ 2 3))
|
||||
(gpt/add sp))
|
||||
|
||||
cp2 (-> (gpt/to-vec ep cp)
|
||||
(gpt/scale (/ 2 3))
|
||||
(gpt/add ep))]
|
||||
|
||||
{:c1x (:x cp1)
|
||||
:c1y (:y cp1)
|
||||
:c2x (:x cp2)
|
||||
:c2y (:y cp2)}))
|
||||
|
||||
(defn simplify-commands
|
||||
"Removes some commands and convert relative to absolute coordinates"
|
||||
[commands]
|
||||
|
||||
(let [simplify-command
|
||||
(fn [[pos result] [command prev]]
|
||||
;; prev-cc : previous command control point for cubic beziers
|
||||
;; prev-qc : previous command control point for quadratic curves
|
||||
(fn [[pos result prev-cc prev-qc] [command prev]]
|
||||
(let [command
|
||||
(cond-> command
|
||||
(:relative command)
|
||||
(-> (assoc :relative false)
|
||||
(cd/update-in-when [:params :c1x] + (:x pos))
|
||||
(cd/update-in-when [:params :c1y] + (:y pos))
|
||||
|
||||
(cd/update-in-when [:params :c2x] + (:x pos))
|
||||
(cd/update-in-when [:params :c2y] + (:y pos))
|
||||
|
||||
(cd/update-in-when [:params :cx] + (:x pos))
|
||||
(cd/update-in-when [:params :cy] + (:y pos))
|
||||
|
||||
(cd/update-in-when [:params :x] + (:x pos))
|
||||
(cd/update-in-when [:params :y] + (:y pos))
|
||||
|
||||
(cond->
|
||||
(= :line-to-horizontal (:command command))
|
||||
(cd/update-in-when [:params :value] + (:x pos))
|
||||
|
||||
(= :line-to-vertical (:command command))
|
||||
(cd/update-in-when [:params :value] + (:y pos)))))
|
||||
|
||||
params (:params command)
|
||||
orig-command command
|
||||
|
||||
command
|
||||
(cond-> command
|
||||
(= :line-to-horizontal (:command command))
|
||||
(-> (assoc :command :line-to)
|
||||
(update :params dissoc :value)
|
||||
(assoc-in [:params :x] (get-in command [:params :value]))
|
||||
(assoc-in [:params :y] (if (:relative command) 0 (:y pos))))
|
||||
(assoc-in [:params :x] (:value params))
|
||||
(assoc-in [:params :y] (:y pos)))
|
||||
|
||||
(= :line-to-vertical (:command command))
|
||||
(-> (assoc :command :line-to)
|
||||
(update :params dissoc :value)
|
||||
(assoc-in [:params :y] (get-in command [:params :value]))
|
||||
(assoc-in [:params :x] (if (:relative command) 0 (:x pos))))
|
||||
(assoc-in [:params :y] (:value params))
|
||||
(assoc-in [:params :x] (:x pos)))
|
||||
|
||||
(:relative command)
|
||||
(-> (assoc :relative false)
|
||||
(cd/update-in-when [:params :x] + (:x pos))
|
||||
(cd/update-in-when [:params :y] + (:y pos)))
|
||||
(= :smooth-curve-to (:command command))
|
||||
(-> (assoc :command :curve-to)
|
||||
(update :params dissoc :cx :cy)
|
||||
(update :params merge (smooth->curve command pos prev-cc)))
|
||||
|
||||
(= :quadratic-bezier-curve-to (:command command))
|
||||
(-> (assoc :command :curve-to)
|
||||
(update :params dissoc :cx :cy)
|
||||
(update :params merge (quadratic->curve pos (gpt/point params) (gpt/point (:cx params) (:cy params)))))
|
||||
|
||||
)]
|
||||
[(cmd-pos command) (conj result command)]))
|
||||
(= :smooth-quadratic-bezier-curve-to (:command command))
|
||||
(-> (assoc :command :curve-to)
|
||||
(update :params merge (quadratic->curve pos (gpt/point params) (calculate-opposite-handler pos prev-qc)))))
|
||||
|
||||
result (if (= :elliptical-arc (:command command))
|
||||
(cd/concat result (arc->beziers pos command))
|
||||
(conj result command))
|
||||
|
||||
prev-cc (case (:command orig-command)
|
||||
:smooth-curve-to
|
||||
(gpt/point (get-in orig-command [:params :cx]) (get-in orig-command [:params :cy]))
|
||||
|
||||
:curve-to
|
||||
(gpt/point (get-in orig-command [:params :c2x]) (get-in orig-command [:params :c2y]))
|
||||
|
||||
(:line-to-horizontal :line-to-vertical)
|
||||
(gpt/point (get-in command [:params :x]) (get-in command [:params :y]))
|
||||
|
||||
(gpt/point (get-in orig-command [:params :x]) (get-in orig-command [:params :y])))
|
||||
|
||||
prev-qc (case (:command orig-command)
|
||||
:quadratic-bezier-curve-to
|
||||
(gpt/point (get-in orig-command [:params :cx]) (get-in orig-command [:params :cy]))
|
||||
|
||||
:smooth-quadratic-bezier-curve-to
|
||||
(calculate-opposite-handler pos prev-qc)
|
||||
|
||||
(gpt/point (get-in orig-command [:params :x]) (get-in orig-command [:params :y])))]
|
||||
[(cmd-pos pos command) result prev-cc prev-qc]))
|
||||
|
||||
start (first commands)
|
||||
start-pos (cmd-pos start)]
|
||||
start-pos (gpt/point (:params start))]
|
||||
|
||||
|
||||
(->> (map vector (rest commands) commands)
|
||||
(reduce simplify-command [start-pos [start]])
|
||||
(reduce simplify-command [start-pos [start] start-pos start-pos])
|
||||
(second))))
|
||||
|
||||
(defn path->content [string]
|
||||
|
|
|
@ -9,13 +9,61 @@
|
|||
|
||||
(ns app.util.svg
|
||||
(:require
|
||||
[app.common.uuid :as uuid]
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.math :as mth]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
;; Regex for XML ids per Spec
|
||||
;; https://www.w3.org/TR/2008/REC-xml-20081126/#sec-common-syn
|
||||
(defonce xml-id-regex #"#([:A-Z_a-z\xC0-\xD6\xD8-\xF6\xF8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\u10000-\uEFFFF][\.\-\:0-9\xB7A-Z_a-z\xC0-\xD6\xD8-\xF6\xF8-\u02FF\u0300-\u036F\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\u10000-\uEFFFF]*)")
|
||||
|
||||
(defonce matrices-regex #"(matrix|translate|scale|rotate|skewX|skewY)\(([^\)]*)\)")
|
||||
(defonce number-regex #"[+-]?\d*(\.\d+)?(e[+-]?\d+)?")
|
||||
|
||||
(defn extract-ids [val]
|
||||
(->> (re-seq xml-id-regex val)
|
||||
(mapv second)))
|
||||
|
||||
(defn fix-dot-number
|
||||
"Fixes decimal numbers starting in dot but without leading 0"
|
||||
[num-str]
|
||||
(cond
|
||||
(str/starts-with? num-str ".")
|
||||
(str "0" num-str)
|
||||
|
||||
(str/starts-with? num-str "-.")
|
||||
(str "-0" (subs num-str 1))
|
||||
|
||||
:else
|
||||
num-str))
|
||||
|
||||
(defn format-styles
|
||||
"Transforms attributes to their react equivalent"
|
||||
[attrs]
|
||||
(letfn [(format-styles [style-str]
|
||||
(if (string? style-str)
|
||||
(->> (str/split style-str ";")
|
||||
(map str/trim)
|
||||
(map #(str/split % ":"))
|
||||
(group-by first)
|
||||
(map (fn [[key val]]
|
||||
(vector (keyword key) (second (first val)))))
|
||||
(into {}))
|
||||
style-str))]
|
||||
|
||||
(cond-> attrs
|
||||
(contains? attrs :style)
|
||||
(update :style format-styles))))
|
||||
|
||||
(defn clean-attrs
|
||||
"Transforms attributes to their react equivalent"
|
||||
[attrs]
|
||||
(letfn [(transform-key [key]
|
||||
(-> (name key)
|
||||
(-> (d/name key)
|
||||
(str/replace ":" "-")
|
||||
(str/camel)
|
||||
(keyword)))
|
||||
|
@ -32,11 +80,468 @@
|
|||
(into {})))
|
||||
|
||||
(map-fn [[key val]]
|
||||
(cond
|
||||
(= key :class) [:className val]
|
||||
(and (= key :style) (string? val)) [key (format-styles val)]
|
||||
:else (vector (transform-key key) val)))]
|
||||
(let [key (keyword key)]
|
||||
(cond
|
||||
(= key :class) [:className val]
|
||||
(and (= key :style) (string? val)) [key (format-styles val)]
|
||||
(and (= key :style) (map? val)) [key (clean-attrs val)]
|
||||
:else (vector (transform-key key) val))))]
|
||||
|
||||
(->> attrs
|
||||
(map map-fn)
|
||||
(into {}))))
|
||||
|
||||
(defn update-attr-ids
|
||||
"Replaces the ids inside a property"
|
||||
[attrs replace-fn]
|
||||
(letfn [(update-ids [key val]
|
||||
(cond
|
||||
(map? val)
|
||||
(d/mapm update-ids val)
|
||||
|
||||
(= key :id)
|
||||
(replace-fn val)
|
||||
|
||||
:else
|
||||
(let [replace-id
|
||||
(fn [result it]
|
||||
(let [to-replace (replace-fn it)]
|
||||
(str/replace result (str "#" it) (str "#" to-replace))))]
|
||||
(reduce replace-id val (extract-ids val)))))]
|
||||
(d/mapm update-ids attrs)))
|
||||
|
||||
(defn replace-attrs-ids
|
||||
"Replaces the ids inside a property"
|
||||
[attrs ids-mapping]
|
||||
(if (and ids-mapping (not (empty? ids-mapping)))
|
||||
(update-attr-ids attrs (fn [id] (get ids-mapping id id)))
|
||||
;; Ids-mapping is null
|
||||
attrs))
|
||||
|
||||
(defn generate-id-mapping [content]
|
||||
(letfn [(visit-node [result node]
|
||||
(let [element-id (get-in node [:attrs :id])
|
||||
result (cond-> result
|
||||
element-id (assoc element-id (str (uuid/next))))]
|
||||
(reduce visit-node result (:content node))))]
|
||||
(visit-node {} content)))
|
||||
|
||||
(def remove-tags #{:defs :linearGradient})
|
||||
|
||||
(defn extract-defs [{:keys [tag attrs content] :as node}]
|
||||
(if-not (map? node)
|
||||
[{} node]
|
||||
|
||||
(let [remove-node? (fn [{:keys [tag]}] (contains? remove-tags tag))
|
||||
|
||||
rec-result (->> (:content node) (map extract-defs))
|
||||
node (assoc node :content (->> rec-result (map second) (filterv (comp not remove-node?))))
|
||||
|
||||
|
||||
current-node-defs (if (contains? attrs :id)
|
||||
(hash-map (:id attrs) node)
|
||||
(hash-map))
|
||||
|
||||
node-defs (->> rec-result (map first) (reduce merge current-node-defs))]
|
||||
|
||||
[ node-defs node ])))
|
||||
|
||||
(defn find-attr-references [attrs]
|
||||
(->> attrs
|
||||
(mapcat (fn [[_ attr-value]]
|
||||
(if (string? attr-value)
|
||||
(extract-ids attr-value)
|
||||
(find-attr-references attr-value))))))
|
||||
|
||||
(defn find-node-references [node]
|
||||
(let [current (->> (find-attr-references (:attrs node)) (into #{}))
|
||||
children (->> (:content node) (map find-node-references) (flatten) (into #{}))]
|
||||
(-> (d/concat current children)
|
||||
(vec))))
|
||||
|
||||
(defn find-def-references [defs references]
|
||||
(loop [result (into #{} references)
|
||||
checked? #{}
|
||||
to-check (first references)
|
||||
pending (rest references)]
|
||||
|
||||
(cond
|
||||
(nil? to-check)
|
||||
result
|
||||
|
||||
(checked? to-check)
|
||||
(recur result
|
||||
checked?
|
||||
(first pending)
|
||||
(rest pending))
|
||||
|
||||
:else
|
||||
(let [node (get defs to-check)
|
||||
new-refs (find-node-references node)
|
||||
pending (concat pending new-refs)]
|
||||
(recur (d/concat result new-refs)
|
||||
(conj checked? to-check)
|
||||
(first pending)
|
||||
(rest pending))))))
|
||||
|
||||
(defn svg-transform-matrix [shape]
|
||||
(if (:svg-viewbox shape)
|
||||
(let [{svg-x :x
|
||||
svg-y :y
|
||||
svg-width :width
|
||||
svg-height :height} (:svg-viewbox shape)
|
||||
{:keys [x y width height]} (:selrect shape)
|
||||
|
||||
scale-x (/ width svg-width)
|
||||
scale-y (/ height svg-height)]
|
||||
|
||||
(gmt/multiply
|
||||
(gmt/matrix)
|
||||
|
||||
;; Paths doesn't have transform so we have to transform its gradients
|
||||
(if (= :path (:type shape))
|
||||
(gsh/transform-matrix shape)
|
||||
(gmt/matrix))
|
||||
|
||||
(gmt/translate-matrix (gpt/point (- x (* scale-x svg-x)) (- y (* scale-y svg-y))))
|
||||
(gmt/scale-matrix (gpt/point scale-x scale-y))))
|
||||
|
||||
;; :else
|
||||
(gmt/matrix)))
|
||||
|
||||
;; Parse transform attributes to native matrix format so we can transform paths instead of
|
||||
;; relying in SVG transformation. This is necessary to import SVG's and not to break path tooling
|
||||
;;
|
||||
;; Transforms spec:
|
||||
;; https://www.w3.org/TR/SVG11/single-page.html#coords-TransformAttribute
|
||||
|
||||
|
||||
(defn format-translate-params [params]
|
||||
(assert (or (= (count params) 1) (= (count params) 2)))
|
||||
(if (= (count params) 1)
|
||||
[(gpt/point (nth params 0) 0)]
|
||||
[(gpt/point (nth params 0) (nth params 1))]))
|
||||
|
||||
(defn format-scale-params [params]
|
||||
(assert (or (= (count params) 1) (= (count params) 2)))
|
||||
(if (= (count params) 1)
|
||||
[(gpt/point (nth params 0))]
|
||||
[(gpt/point (nth params 0) (nth params 1))]))
|
||||
|
||||
(defn format-rotate-params [params]
|
||||
(assert (or (= (count params) 1) (= (count params) 3)) (str "??" (count params)))
|
||||
(if (= (count params) 1)
|
||||
[(nth params 0) (gpt/point 0 0)]
|
||||
[(nth params 0) (gpt/point (nth params 1) (nth params 2))]))
|
||||
|
||||
(defn format-skew-x-params [params]
|
||||
(assert (= (count params) 1))
|
||||
[(nth params 0) 0])
|
||||
|
||||
(defn format-skew-y-params [params]
|
||||
(assert (= (count params) 1))
|
||||
[0 (nth params 0)])
|
||||
|
||||
(defn to-matrix [{:keys [type params]}]
|
||||
(assert (#{"matrix" "translate" "scale" "rotate" "skewX" "skewY"} type))
|
||||
(case type
|
||||
"matrix" (apply gmt/matrix params)
|
||||
"translate" (apply gmt/translate-matrix (format-translate-params params))
|
||||
"scale" (apply gmt/scale-matrix (format-scale-params params))
|
||||
"rotate" (apply gmt/rotate-matrix (format-rotate-params params))
|
||||
"skewX" (apply gmt/skew-matrix (format-skew-x-params params))
|
||||
"skewY" (apply gmt/skew-matrix (format-skew-y-params params))))
|
||||
|
||||
(defn parse-transform [transform-attr]
|
||||
(if transform-attr
|
||||
(let [process-matrix
|
||||
(fn [[_ type params]]
|
||||
(let [params (->> (re-seq number-regex params)
|
||||
(filter #(-> % first empty? not))
|
||||
(map (comp d/parse-double first)))]
|
||||
{:type type :params params}))
|
||||
|
||||
matrices (->> (re-seq matrices-regex transform-attr)
|
||||
(map process-matrix)
|
||||
(map to-matrix))]
|
||||
(reduce gmt/multiply (gmt/matrix) matrices))
|
||||
(gmt/matrix)))
|
||||
|
||||
|
||||
|
||||
(defn format-move [[x y]] (str "M" x " " y))
|
||||
(defn format-line [[x y]] (str "L" x " " y))
|
||||
|
||||
(defn points->path [points-str]
|
||||
(let [points (->> points-str
|
||||
(re-seq number-regex)
|
||||
(filter (comp not empty? first))
|
||||
(mapv (comp d/parse-double first))
|
||||
(partition 2))
|
||||
|
||||
head (first points)
|
||||
other (rest points)]
|
||||
|
||||
(str (format-move head)
|
||||
(->> other (map format-line) (str/join " ")))))
|
||||
|
||||
(defn polyline->path [{:keys [attrs tag] :as node}]
|
||||
(let [tag :path
|
||||
attrs (-> attrs
|
||||
(dissoc :points)
|
||||
(assoc :d (points->path (:points attrs))))]
|
||||
|
||||
(assoc node :attrs attrs :tag tag)))
|
||||
|
||||
(defn polygon->path [{:keys [attrs tag] :as node}]
|
||||
(let [tag :path
|
||||
attrs (-> attrs
|
||||
(dissoc :points)
|
||||
(assoc :d (str (points->path (:points attrs)) "Z")))]
|
||||
(assoc node :attrs attrs :tag tag)))
|
||||
|
||||
(defn line->path [{:keys [attrs tag] :as node}]
|
||||
(let [tag :path
|
||||
{:keys [x1 y1 x2 y2]} attrs
|
||||
attrs (-> attrs
|
||||
(dissoc :x1 :x2 :y1 :y2)
|
||||
(assoc :d (str "M" x1 "," y1 " L" x2 "," y2)))]
|
||||
|
||||
(assoc node :attrs attrs :tag tag)))
|
||||
|
||||
(defn add-transform [attrs transform]
|
||||
(letfn [(append-transform [old-transform]
|
||||
(if (or (nil? old-transform) (empty? old-transform))
|
||||
transform
|
||||
(str transform " " old-transform)))]
|
||||
|
||||
(cond-> attrs
|
||||
transform
|
||||
(update :transform append-transform))))
|
||||
|
||||
(defonce inheritable-props
|
||||
[:clip-rule
|
||||
:color
|
||||
:color-interpolation
|
||||
:color-interpolation-filters
|
||||
:color-profile
|
||||
:color-rendering
|
||||
:cursor
|
||||
:direction
|
||||
:dominant-baseline
|
||||
:fill
|
||||
:fill-opacity
|
||||
:fill-rule
|
||||
:font
|
||||
:font-family
|
||||
:font-size
|
||||
:font-size-adjust
|
||||
:font-stretch
|
||||
:font-style
|
||||
:font-variant
|
||||
:font-weight
|
||||
:glyph-orientation-horizontal
|
||||
:glyph-orientation-vertical
|
||||
:image-rendering
|
||||
:letter-spacing
|
||||
:marker
|
||||
:marker-end
|
||||
:marker-mid
|
||||
:marker-start
|
||||
:paint-order
|
||||
:pointer-events
|
||||
:shape-rendering
|
||||
:stroke
|
||||
:stroke-dasharray
|
||||
:stroke-dashoffset
|
||||
:stroke-linecap
|
||||
:stroke-linejoin
|
||||
:stroke-miterlimit
|
||||
:stroke-opacity
|
||||
:stroke-width
|
||||
:text-anchor
|
||||
:text-rendering
|
||||
:transform
|
||||
:visibility
|
||||
:word-spacing
|
||||
:writing-mode])
|
||||
|
||||
(defonce gradient-tags
|
||||
#{:linearGradient
|
||||
:radialGradient})
|
||||
|
||||
(defonce filter-tags
|
||||
#{:filter
|
||||
:feBlend
|
||||
:feColorMatrix
|
||||
:feComponentTransfer
|
||||
:feComposite
|
||||
:feConvolveMatrix
|
||||
:feDiffuseLighting
|
||||
:feDisplacementMap
|
||||
:feFlood
|
||||
:feGaussianBlur
|
||||
:feImage
|
||||
:feMerge
|
||||
:feMorphology
|
||||
:feOffset
|
||||
:feSpecularLighting
|
||||
:feTile
|
||||
:feTurbulence})
|
||||
|
||||
(defn inherit-attributes [group-attrs {:keys [attrs] :as node}]
|
||||
(if (map? node)
|
||||
(let [attrs (-> (format-styles attrs)
|
||||
(add-transform (:transform group-attrs)))
|
||||
attrs (d/deep-merge (select-keys group-attrs inheritable-props) attrs)]
|
||||
(assoc node :attrs attrs))
|
||||
node))
|
||||
|
||||
(defn map-nodes [mapfn node]
|
||||
(let [update-content
|
||||
(fn [content] (cond->> content
|
||||
(vector? content)
|
||||
(mapv (partial map-nodes mapfn))))]
|
||||
|
||||
(cond-> node
|
||||
(map? node)
|
||||
(-> (mapfn)
|
||||
(d/update-when :content update-content)))))
|
||||
|
||||
(defn reduce-nodes [redfn value node]
|
||||
(let [reduce-content
|
||||
(fn [value content]
|
||||
(loop [current (first content)
|
||||
content (rest content)
|
||||
value value]
|
||||
(if (nil? current)
|
||||
value
|
||||
(recur (first content)
|
||||
(rest content)
|
||||
(reduce-nodes redfn value current)))))]
|
||||
|
||||
(if (map? node)
|
||||
(-> (redfn value node)
|
||||
(reduce-content (:content node)))
|
||||
value)))
|
||||
|
||||
;; Defaults for some tags per spec https://www.w3.org/TR/SVG11/single-page.html
|
||||
;; they are basicaly the defaults that can be percents and we need to replace because
|
||||
;; otherwise won't work as expected in the workspace
|
||||
(defonce svg-tag-defaults
|
||||
(let [filter-default {:units :filterUnits
|
||||
:default "objectBoundingBox"
|
||||
"objectBoundingBox" {}
|
||||
"userSpaceOnUse" {:x "-10%" :y "-10%" :width "120%" :height "120%"}}
|
||||
filter-values (->> filter-tags
|
||||
(reduce #(merge %1 (hash-map %2 filter-default)) {}))]
|
||||
|
||||
(merge {:linearGradient {:units :gradientUnits
|
||||
:default "objectBoundingBox"
|
||||
"objectBoundingBox" {}
|
||||
"userSpaceOnUse" {:x1 "0%" :y1 "0%" :x2 "100%" :y2 "0%"}}
|
||||
:radialGradient {:units :gradientUnits
|
||||
:default "objectBoundingBox"
|
||||
"objectBoundingBox" {}
|
||||
"userSpaceOnUse" {:cx "50%" :cy "50%" :r "50%"}}
|
||||
:mask {:units :maskUnits
|
||||
:default "userSpaceOnUse"
|
||||
"objectBoundingBox" {}
|
||||
"userSpaceOnUse" {:x "-10%" :y "-10%" :width "120%" :height "120%"}}}
|
||||
filter-values)))
|
||||
|
||||
(defn fix-default-values
|
||||
"Gives values to some SVG elements which defaults won't work when imported into the platform"
|
||||
[svg-data]
|
||||
(let [add-defaults
|
||||
(fn [{:keys [tag attrs] :as node}]
|
||||
(let [prop (get-in svg-tag-defaults [tag :units])
|
||||
default-units (get-in svg-tag-defaults [tag :default])
|
||||
units (get attrs prop default-units)
|
||||
tag-default (get-in svg-tag-defaults [tag units])]
|
||||
(d/update-when node :attrs #(merge tag-default %))))
|
||||
|
||||
fix-node-defaults
|
||||
(fn [node]
|
||||
(cond-> node
|
||||
(contains? svg-tag-defaults (:tag node))
|
||||
(add-defaults)))]
|
||||
|
||||
(->> svg-data (map-nodes fix-node-defaults))))
|
||||
|
||||
(defn calculate-ratio
|
||||
;; sqrt((actual-width)**2 + (actual-height)**2)/sqrt(2).
|
||||
[width height]
|
||||
(/ (mth/sqrt (+ (mth/pow width 2)
|
||||
(mth/pow height 2)))
|
||||
(mth/sqrt 2)))
|
||||
|
||||
(defn fix-percents
|
||||
"Changes percents to a value according to the size of the svg imported"
|
||||
[svg-data]
|
||||
;; https://www.w3.org/TR/SVG11/single-page.html#coords-Units
|
||||
(let [viewbox {:x (:offset-x svg-data)
|
||||
:y (:offset-y svg-data)
|
||||
:width (:width svg-data)
|
||||
:height (:height svg-data)
|
||||
:ratio (calculate-ratio (:width svg-data) (:height svg-data))}]
|
||||
(letfn [(fix-length [prop-length val]
|
||||
(* (get viewbox prop-length) (/ val 100.)))
|
||||
|
||||
(fix-coord [prop-coord prop-length val]
|
||||
(+ (get viewbox prop-coord)
|
||||
(fix-length prop-length val)))
|
||||
|
||||
(fix-percent-attr-viewbox [attr-key attr-val]
|
||||
(let [is-percent? (str/ends-with? attr-val "%")
|
||||
is-x? #{:x :x1 :x2 :cx}
|
||||
is-y? #{:y :y1 :y2 :cy}
|
||||
is-width? #{:width}
|
||||
is-height? #{:height}
|
||||
is-other? #{:r :stroke-width}]
|
||||
|
||||
(if is-percent?
|
||||
;; JS parseFloat removes the % symbol
|
||||
(let [attr-num (d/parse-double attr-val)]
|
||||
(str (cond
|
||||
(is-x? attr-key) (fix-coord :x :width attr-num)
|
||||
(is-y? attr-key) (fix-coord :y :height attr-num)
|
||||
(is-width? attr-key) (fix-length :width attr-num)
|
||||
(is-height? attr-key) (fix-length :height attr-num)
|
||||
(is-other? attr-key) (fix-length :ratio attr-num)
|
||||
:else attr-val)))
|
||||
attr-val)))
|
||||
|
||||
(fix-percent-attrs-viewbox [attrs]
|
||||
(d/mapm fix-percent-attr-viewbox attrs))
|
||||
|
||||
(fix-percent-attr-numeric [attr-key attr-val]
|
||||
(let [is-percent? (str/ends-with? attr-val "%")]
|
||||
(if is-percent?
|
||||
(str (let [attr-num (d/parse-double attr-val)]
|
||||
(/ attr-num 100)))
|
||||
attr-val)))
|
||||
|
||||
(fix-percent-attrs-numeric [attrs]
|
||||
(d/mapm fix-percent-attr-numeric attrs))
|
||||
|
||||
(fix-percent-values [node]
|
||||
(let [units (or (get-in node [:attrs :filterUnits])
|
||||
(get-in node [:attrs :gradientUnits])
|
||||
(get-in node [:attrs :patternUnits])
|
||||
(get-in node [:attrs :clipUnits]))]
|
||||
(cond-> node
|
||||
(= "objectBoundingBox" units)
|
||||
(update :attrs fix-percent-attrs-numeric)
|
||||
|
||||
(not= "objectBoundingBox" units)
|
||||
(update :attrs fix-percent-attrs-viewbox))))]
|
||||
|
||||
(->> svg-data (map-nodes fix-percent-values)))))
|
||||
|
||||
(defn collect-images [svg-data]
|
||||
(let [redfn (fn [acc {:keys [tag attrs]}]
|
||||
(cond-> acc
|
||||
(= :image tag)
|
||||
(conj (:xlink:href attrs))))]
|
||||
(reduce-nodes redfn [] svg-data )))
|
||||
|
|
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})))
|
45
vendor/svgclean/main.js
vendored
45
vendor/svgclean/main.js
vendored
|
@ -1,16 +1,41 @@
|
|||
|
||||
const plugins = [
|
||||
{removeDimensions: true},
|
||||
{removeScriptElement: true},
|
||||
{removeViewBox: false},
|
||||
{moveElemsAttrsToGroup: false},
|
||||
{convertStyleToAttrs: false},
|
||||
{convertPathData: {
|
||||
lineShorthands: false,
|
||||
curveSmoothShorthands: false,
|
||||
forceAbsolutePath: true,
|
||||
}}
|
||||
{ "minifyStyles" : false },
|
||||
{ "convertStyleToAttrs" : false },
|
||||
{
|
||||
"cleanupIDs" : {
|
||||
remove: false,
|
||||
minify: false,
|
||||
force: false
|
||||
}
|
||||
},
|
||||
{ "cleanupListOfValues" : true },
|
||||
{ "removeUnknownsAndDefaults" : false },
|
||||
{ "removeViewBox" : false },
|
||||
{ "convertShapeToPath" : false },
|
||||
{ "convertEllipseToCircle" : false },
|
||||
{ "moveElemsAttrsToGroup" : false },
|
||||
{ "moveGroupAttrsToElems" : false },
|
||||
{ "collapseGroups" : false },
|
||||
{
|
||||
"convertPathData" : {
|
||||
lineShorthands: false,
|
||||
curveSmoothShorthands: false,
|
||||
forceAbsolutePath: true,
|
||||
}
|
||||
},
|
||||
{ "convertTransform" : false },
|
||||
{ "removeEmptyContainers" : false },
|
||||
{ "mergePaths" : false },
|
||||
{ "sortDefsChildren" : false },
|
||||
{ "removeDimensions" : true },
|
||||
{ "removeStyleElement" : false },
|
||||
{ "removeScriptElement" : true },
|
||||
{ "removeOffCanvasPaths" : false },
|
||||
{ "cleanupNumericValues": true}
|
||||
];
|
||||
|
||||
|
||||
const svgc = require("./src/svgclean.js");
|
||||
const inst = svgc.configure({plugins});
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ exports.description = 'rounds numeric values to the fixed precision, removes def
|
|||
|
||||
exports.params = {
|
||||
floatPrecision: 3,
|
||||
leadingZero: true,
|
||||
leadingZero: false,
|
||||
defaultPx: true,
|
||||
convertToPx: true
|
||||
};
|
||||
|
@ -20,7 +20,8 @@ var regNumericValues = /^([\-+]?\d*\.?\d+([eE][\-+]?\d+)?)(px|pt|pc|mm|cm|m|in|f
|
|||
mm: 96/25.4,
|
||||
in: 96,
|
||||
pt: 4/3,
|
||||
pc: 16
|
||||
pc: 16,
|
||||
em: 16
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -61,12 +62,8 @@ exports.fn = function(item, params) {
|
|||
|
||||
// convert absolute values to pixels
|
||||
if (params.convertToPx && units && (units in absoluteLengths)) {
|
||||
var pxNum = +(absoluteLengths[units] * match[1]).toFixed(floatPrecision);
|
||||
|
||||
if (String(pxNum).length < match[0].length) {
|
||||
num = pxNum;
|
||||
units = 'px';
|
||||
}
|
||||
num = +(absoluteLengths[units] * match[1]).toFixed(floatPrecision);
|
||||
units = 'px';
|
||||
}
|
||||
|
||||
// and remove leading zero
|
||||
|
|
|
@ -8,7 +8,8 @@ exports.description = 'moves some group attributes to the content elements';
|
|||
|
||||
var collections = require('./_collections.js'),
|
||||
pathElems = collections.pathElems.concat(['g', 'text']),
|
||||
referencesProps = collections.referencesProps;
|
||||
referencesProps = collections.referencesProps,
|
||||
inheritableAttrs = collections.inheritableAttrs;
|
||||
|
||||
/**
|
||||
* Move group attrs to the content elements.
|
||||
|
@ -31,33 +32,33 @@ var collections = require('./_collections.js'),
|
|||
*/
|
||||
exports.fn = function(item) {
|
||||
|
||||
// move group transform attr to content's pathElems
|
||||
if (
|
||||
item.isElem('g') &&
|
||||
item.hasAttr('transform') &&
|
||||
!item.isEmpty() &&
|
||||
!item.someAttr(function(attr) {
|
||||
return ~referencesProps.indexOf(attr.name) && ~attr.value.indexOf('url(');
|
||||
}) &&
|
||||
item.content.every(function(inner) {
|
||||
return inner.isElem(pathElems) && !inner.hasAttr('id');
|
||||
})
|
||||
) {
|
||||
item.content.forEach(function(inner) {
|
||||
var attr = item.attr('transform');
|
||||
if (inner.hasAttr('transform')) {
|
||||
inner.attr('transform').value = attr.value + ' ' + inner.attr('transform').value;
|
||||
} else {
|
||||
inner.addAttr({
|
||||
'name': attr.name,
|
||||
'local': attr.local,
|
||||
'prefix': attr.prefix,
|
||||
'value': attr.value
|
||||
if (item.isElem('g') && !item.isEmpty()) {
|
||||
|
||||
inheritableAttrs.forEach(function(currentAttr) {
|
||||
|
||||
if (item.hasAttr(currentAttr)) {
|
||||
var attr = item.attr(currentAttr);
|
||||
|
||||
item.content.forEach(function(inner) {
|
||||
|
||||
if (currentAttr === 'transform' && inner.hasAttr(currentAttr)) {
|
||||
// if attr is transform and the inner has transform we concatenate it
|
||||
inner.attr(currentAttr).value = attr.value + ' ' + inner.attr(currentAttr).value;
|
||||
} else if (!inner.hasAttr(currentAttr)){
|
||||
// If the inner has the attr already we don't override it
|
||||
inner.addAttr({
|
||||
...attr
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
|
||||
item.removeAttr(currentAttr);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
item.removeAttr('transform');
|
||||
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
|
Loading…
Add table
Reference in a new issue