0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-01-04 13:50:12 -05:00

♻️ Make svg to shapes conversion code multiplatform

- Move clojure code to common
- Rewrite some native-js code into optimized clojure
This commit is contained in:
Andrey Antukh 2023-10-11 13:39:56 +02:00 committed by Andrés Moya
parent 44845d5d94
commit 3ceb4cf895
62 changed files with 2037 additions and 1011 deletions

View file

@ -26,6 +26,7 @@
com.cognitect/transit-cljs {:mvn/version "0.8.280"}
java-http-clj/java-http-clj {:mvn/version "0.4.3"}
funcool/tubax {:mvn/version "2021.05.20-0"}
funcool/cuerdas {:mvn/version "2022.06.16-403"}
funcool/promesa {:mvn/version "11.0.678"}
funcool/datoteka {:mvn/version "3.0.66"
@ -43,7 +44,7 @@
fipp/fipp {:mvn/version "0.6.26"}
io.aviso/pretty {:mvn/version "1.4.4"}
environ/environ {:mvn/version "1.2.0"}}
:paths ["src" "target/classes"]
:paths ["src" "vendor" "target/classes"]
:aliases
{:dev
{:extra-deps

View file

@ -4,7 +4,8 @@
"main": "index.js",
"license": "MPL-2.0",
"dependencies": {
"luxon": "^3.4.2"
"luxon": "^3.4.2",
"sax": "^1.2.4"
},
"scripts": {
"test:watch": "clojure -M:dev:shadow-cljs watch test",

View file

@ -5,7 +5,12 @@
;; Copyright (c) KALEIDOS INC
(ns app.common.colors
(:refer-clojure :exclude [test]))
(:refer-clojure :exclude [test])
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.math :as mth]
[cuerdas.core :as str]))
(def black "#000000")
(def canvas "#E8E9EA")
@ -27,3 +32,428 @@
(def new-danger "#ff4986")
(def new-warning "#ff9b49")
(def canvas-background "#1d1f20")
(def names
{"aliceblue" "#f0f8ff"
"antiquewhite" "#faebd7"
"aqua" "#00ffff"
"aquamarine" "#7fffd4"
"azure" "#f0ffff"
"beige" "#f5f5dc"
"bisque" "#ffe4c4"
"black" "#000000"
"blanchedalmond" "#ffebcd"
"blue" "#0000ff"
"blueviolet" "#8a2be2"
"brown" "#a52a2a"
"burlywood" "#deb887"
"cadetblue" "#5f9ea0"
"chartreuse" "#7fff00"
"chocolate" "#d2691e"
"coral" "#ff7f50"
"cornflowerblue" "#6495ed"
"cornsilk" "#fff8dc"
"crimson" "#dc143c"
"cyan" "#00ffff"
"darkblue" "#00008b"
"darkcyan" "#008b8b"
"darkgoldenrod" "#b8860b"
"darkgray" "#a9a9a9"
"darkgreen" "#006400"
"darkgrey" "#a9a9a9"
"darkkhaki" "#bdb76b"
"darkmagenta" "#8b008b"
"darkolivegreen" "#556b2f"
"darkorange" "#ff8c00"
"darkorchid" "#9932cc"
"darkred" "#8b0000"
"darksalmon" "#e9967a"
"darkseagreen" "#8fbc8f"
"darkslateblue" "#483d8b"
"darkslategray" "#2f4f4f"
"darkslategrey" "#2f4f4f"
"darkturquoise" "#00ced1"
"darkviolet" "#9400d3"
"deeppink" "#ff1493"
"deepskyblue" "#00bfff"
"dimgray" "#696969"
"dimgrey" "#696969"
"dodgerblue" "#1e90ff"
"firebrick" "#b22222"
"floralwhite" "#fffaf0"
"forestgreen" "#228b22"
"fuchsia" "#ff00ff"
"gainsboro" "#dcdcdc"
"ghostwhite" "#f8f8ff"
"gold" "#ffd700"
"goldenrod" "#daa520"
"gray" "#808080"
"green" "#008000"
"greenyellow" "#adff2f"
"grey" "#808080"
"honeydew" "#f0fff0"
"hotpink" "#ff69b4"
"indianred" "#cd5c5c"
"indigo" "#4b0082"
"ivory" "#fffff0"
"khaki" "#f0e68c"
"lavender" "#e6e6fa"
"lavenderblush" "#fff0f5"
"lawngreen" "#7cfc00"
"lemonchiffon" "#fffacd"
"lightblue" "#add8e6"
"lightcoral" "#f08080"
"lightcyan" "#e0ffff"
"lightgoldenrodyellow" "#fafad2"
"lightgray" "#d3d3d3"
"lightgreen" "#90ee90"
"lightgrey" "#d3d3d3"
"lightpink" "#ffb6c1"
"lightsalmon" "#ffa07a"
"lightseagreen" "#20b2aa"
"lightskyblue" "#87cefa"
"lightslategray" "#778899"
"lightslategrey" "#778899"
"lightsteelblue" "#b0c4de"
"lightyellow" "#ffffe0"
"lime" "#00ff00"
"limegreen" "#32cd32"
"linen" "#faf0e6"
"magenta" "#ff00ff"
"maroon" "#800000"
"mediumaquamarine" "#66cdaa"
"mediumblue" "#0000cd"
"mediumorchid" "#ba55d3"
"mediumpurple" "#9370db"
"mediumseagreen" "#3cb371"
"mediumslateblue" "#7b68ee"
"mediumspringgreen" "#00fa9a"
"mediumturquoise" "#48d1cc"
"mediumvioletred" "#c71585"
"midnightblue" "#191970"
"mintcream" "#f5fffa"
"mistyrose" "#ffe4e1"
"moccasin" "#ffe4b5"
"navajowhite" "#ffdead"
"navy" "#000080"
"oldlace" "#fdf5e6"
"olive" "#808000"
"olivedrab" "#6b8e23"
"orange" "#ffa500"
"orangered" "#ff4500"
"orchid" "#da70d6"
"palegoldenrod" "#eee8aa"
"palegreen" "#98fb98"
"paleturquoise" "#afeeee"
"palevioletred" "#db7093"
"papayawhip" "#ffefd5"
"peachpuff" "#ffdab9"
"peru" "#cd853f"
"pink" "#ffc0cb"
"plum" "#dda0dd"
"powderblue" "#b0e0e6"
"purple" "#800080"
"red" "#ff0000"
"rosybrown" "#bc8f8f"
"royalblue" "#4169e1"
"saddlebrown" "#8b4513"
"salmon" "#fa8072"
"sandybrown" "#f4a460"
"seagreen" "#2e8b57"
"seashell" "#fff5ee"
"sienna" "#a0522d"
"silver" "#c0c0c0"
"skyblue" "#87ceeb"
"slateblue" "#6a5acd"
"slategray" "#708090"
"slategrey" "#708090"
"snow" "#fffafa"
"springgreen" "#00ff7f"
"steelblue" "#4682b4"
"tan" "#d2b48c"
"teal" "#008080"
"thistle" "#d8bfd8"
"tomato" "#ff6347"
"turquoise" "#40e0d0"
"violet" "#ee82ee"
"wheat" "#f5deb3"
"white" "#ffffff"
"whitesmoke" "#f5f5f5"
"yellow" "#ffff00"
"yellowgreen" "#9acd32"})
(def ^:private hex-color-re
#"\#[0-9a-fA-F]{3,6}")
(def ^:private rgb-color-re
#"(?:|rgb)\((\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\)")
(defn valid-hex-color?
[color]
(and (string? color)
(some? (re-matches hex-color-re color))))
(defn parse-rgb
[color]
(let [result (re-matches rgb-color-re color)]
(when (some? result)
(let [r (parse-long (nth result 1))
g (parse-long (nth result 2))
b (parse-long (nth result 3))]
(when (and (<= 0 r 255) (<= 0 g 255) (<= 0 b 255))
[r g b])))))
(defn valid-rgb-color?
[color]
(if (string? color)
(let [result (parse-rgb color)]
(some? result))
false))
(defn- normalize-hex
[color]
(if (= (count color) 4) ; of the form #RGB
(-> color
(str/replace #"\#(.)(.)(.)" "#$1$1$2$2$3$3")
(str/lower))
(str/lower color)))
(defn rgb->str
[[r g b a]]
(if (some? a)
(str/ffmt "rgba(%,%,%,%)" r g b a)
(str/ffmt "rgb(%,%,%)" r g b)))
(defn rgb->hsv
[[red green blue]]
(let [max (d/max red green blue)
min (d/min red green blue)
val max]
(if (= min max)
[0 0 val]
(let [delta (- max min)
sat (/ delta max)
hue (if (= red max)
(/ (- green blue) delta)
(if (= green max)
(+ 2 (/ (- blue red) delta))
(+ 4 (/ (- red green) delta))))
hue (* 60 hue)
hue (if (< hue 0)
(+ hue 360)
hue)
hue (if (> hue 360)
(- hue 360)
hue)]
[hue sat val]))))
(defn hsv->rgb
[[h s brightness]]
(if (= s 0)
[brightness brightness brightness]
(let [sextant (int (mth/floor (/ h 60)))
remainder (- (/ h 60) sextant)
val1 (int (* brightness (- 1 s)))
val2 (int (* brightness (- 1 (* s remainder))))
val3 (int (* brightness (- 1 (* s (- 1 remainder)))))]
(case sextant
1 [val2 brightness val1]
2 [val1 brightness val3]
3 [val1 val2 brightness]
4 [val3 val1 brightness]
5 [brightness val1 val2]
6 [brightness val3 val1]
0 [brightness val3 val1]))))
(defn hex->rgb
[color]
(try
(let [rgb #?(:clj (Integer/parseInt (subs color 1) 16)
:cljs (js/parseInt (subs color 1) 16))
r (bit-shift-right rgb 16)
g (bit-and (bit-shift-right rgb 8) 255)
b (bit-and rgb 255)]
[r g b])
(catch #?(:clj Throwable :cljs :default) _cause
[0 0 0])))
(defn- int->hex
"Convert integer to hex string"
[v]
#?(:clj (Integer/toHexString v)
:cljs (.toString v 16)))
(defn rgb->hex
[[r g b]]
(let [r (int r)
g (int g)
b (int b)]
(if (or (not= r (bit-and r 255))
(not= g (bit-and g 255))
(not= b (bit-and b 255)))
(throw (ex-info "not valid rgb" {:r r :g g :b b}))
(let [rgb (bit-or (bit-shift-left r 16)
(bit-shift-left g 8) b)]
(if (< r 16)
(dm/str "#" (subs (int->hex (bit-or 0x1000000 rgb)) 1))
(dm/str "#" (int->hex rgb)))))))
(defn rgb->hsl
[[r g b]]
(let [norm-r (/ r 255.0)
norm-g (/ g 255.0)
norm-b (/ b 255.0)
max (d/max norm-r norm-g norm-b)
min (d/min norm-r norm-g norm-b)
l (/ (+ max min) 2.0)
h (if (= max min) 0
(if (= max norm-r)
(* 60 (/ (- norm-g norm-b) (- max min)))
(if (= max norm-g)
(+ 120 (* 60 (/ (- norm-b norm-r) (- max min))))
(+ 240 (* 60 (/ (- norm-r norm-g) (- max min)))))))
s (if (and (> l 0) (<= l 0.5))
(/ (- max min) (* 2 l))
(/ (- max min) (- 2 (* 2 l))))]
[(mod (+ h 360) 360) s l]))
(defn hex->hsv
[v]
(-> v hex->rgb rgb->hsv))
(defn hex->rgba
[data opacity]
(-> (hex->rgb data)
(conj opacity)))
(defn hex->hsl [hex]
(try
(-> hex hex->rgb rgb->hsl)
(catch #?(:clj Throwable :cljs :default) _e
[0 0 0])))
(defn hex->hsla
[data opacity]
(-> (hex->hsl data)
(conj opacity)))
#?(:cljs
(defn format-hsla
[[h s l a]]
(let [precision 2
rounded-s (* 100 (parse-double (d/format-precision s precision)))
rounded-l (* 100 (parse-double (d/format-precision l precision)))]
(str/concat "" h ", " rounded-s "%, " rounded-l "%, " a))))
(defn- hue->rgb
"Helper for hsl->rgb"
[v1 v2 vh]
(let [vh (if (< vh 0)
(+ vh 1)
(if (> vh 1)
(- vh 1)
vh))]
(cond
(< (* 6 vh) 1) (+ v1 (* (- v2 v1) 6 vh))
(< (* 2 vh) 1) v2
(< (* 3 vh) 2) (+ v1 (* (- v2 v1) (- (/ 2 3) vh) 6))
:else v1)))
(defn hsl->rgb
[[h s l]]
(if (= s 0)
(let [o (* l 255)]
[o o o])
(let [norm-h (/ h 360.0)
temp2 (if (< l 0.5)
(* l (+ 1 s))
(- (+ l s)
(* s l)))
temp1 (- (* l 2) temp2)]
[(mth/round (* 255 (hue->rgb temp1 temp2 (+ norm-h (/ 1 3)))))
(mth/round (* 255 (hue->rgb temp1 temp2 norm-h)))
(mth/round (* 255 (hue->rgb temp1 temp2 (- norm-h (/ 1 3)))))])))
(defn hsl->hex
[v]
(-> v hsl->rgb rgb->hex))
(defn hsl->hsv
[hsl]
(-> hsl hsl->rgb rgb->hsv))
(defn hsv->hex
[hsv]
(-> hsv hsv->rgb rgb->hex))
(defn hsv->hsl
[hsv]
(-> hsv hsv->hex hex->hsl))
(defn expand-hex
[v]
(cond
(re-matches #"^[0-9A-Fa-f]$" v)
(dm/str v v v v v v)
(re-matches #"^[0-9A-Fa-f]{2}$" v)
(dm/str v v v)
(re-matches #"^[0-9A-Fa-f]{3}$" v)
(let [a (nth v 0)
b (nth v 1)
c (nth v 2)]
(dm/str a a b b c c))
:else
v))
(defn prepend-hash
[color]
(if (= "#" (subs color 0 1))
color
(dm/str "#" color)))
(defn remove-hash
[color]
(if (str/starts-with? color "#")
(subs color 1)
color))
(defn color-string?
[color]
(and (string? color)
(or (valid-hex-color? color)
(valid-rgb-color? color)
(contains? names color))))
(defn parse
[color]
(when (string? color)
(if (valid-hex-color? color)
(normalize-hex color)
(or (some-> (parse-rgb color) (rgb->hex))
(get names (str/lower color))))))
(def color-names
(into [] (keys names)))
(def empty-color
(into {} (map #(vector % nil)) [:color :id :file-id :gradient :opacity]))
(defn next-rgb
"Given a color in rgb returns the next color"
[[r g b]]
(cond
(and (= 255 r) (= 255 g) (= 255 b))
(throw (ex-info "cannot get next color" {:r r :g g :b b}))
(and (= 255 g) (= 255 b))
[(inc r) 0 0]
(= 255 b)
[r (inc g) 0]
:else
[r g (inc b)]))

View file

@ -0,0 +1,91 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.common.files.libraries-helpers
(:require
[app.common.pages.changes-builder :as pcb]
[app.common.pages.helpers :as cph]
[app.common.types.component :as ctk]
[app.common.types.container :as ctn]
[app.common.uuid :as uuid]))
(defn generate-add-component-changes
[changes root objects file-id page-id components-v2]
(let [name (:name root)
[path name] (cph/parse-path-name name)
[root-shape new-shapes updated-shapes]
(if-not components-v2
(ctn/make-component-shape root objects file-id components-v2)
(let [new-id (uuid/next)]
[(assoc root :id new-id)
nil
[(assoc root
:component-id new-id
:component-file file-id
:component-root true
:main-instance true)]]))
changes (-> changes
(pcb/add-component (:id root-shape)
path
name
new-shapes
updated-shapes
(:id root)
page-id))]
[root-shape changes]))
(defn generate-add-component
"If there is exactly one id, and it's a frame (or a group in v1), and not already a
component, use it as root. Otherwise, create a frame (v2) or group (v1) that contains
all ids. Then, make a component with it, and link all shapes to their corresponding one
in the component."
[it shapes objects page-id file-id components-v2 prepare-create-group prepare-create-board]
(let [changes (pcb/empty-changes it page-id)
[root changes old-root-ids]
(if (and (= (count shapes) 1)
(or (and (= (:type (first shapes)) :group) (not components-v2))
(= (:type (first shapes)) :frame))
(not (ctk/instance-head? (first shapes))))
[(first shapes)
(-> (pcb/empty-changes it page-id)
(pcb/with-objects objects))
(:shapes (first shapes))]
(let [root-name (if (= 1 (count shapes))
(:name (first shapes))
"Component 1")
[root changes] (if-not components-v2
(prepare-create-group it ; These functions needs to be passed as argument
objects ; to avoid a circular dependence
page-id
shapes
root-name
(not (ctk/instance-head? (first shapes))))
(prepare-create-board changes
(uuid/next)
(:parent-id (first shapes))
objects
(map :id shapes)
nil
root-name
true))]
[root changes (map :id shapes)]))
[root-shape changes] (generate-add-component-changes changes root objects file-id page-id components-v2)
changes (pcb/update-shapes changes
old-root-ids
#(dissoc % :component-root)
[:component-root])]
[root (:id root-shape) changes]))

View file

@ -0,0 +1,110 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.common.files.shapes-helpers
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.geom.shapes :as gsh]
[app.common.pages.changes-builder :as pcb]
[app.common.pages.helpers :as cph]
[app.common.schema :as sm]
[app.common.types.shape :as cts]
[app.common.types.shape.layout :as ctl]
[app.common.uuid :as uuid]))
(def valid-shape-map?
(sm/pred-fn ::cts/shape))
(defn prepare-add-shape
[changes shape objects _selected]
(let [index (:index (meta shape))
id (:id shape)
mod? (:mod? (meta shape))
[row column :as cell] (when-not mod? (:cell (meta shape)))
changes (-> changes
(pcb/with-objects objects)
(cond-> (some? index)
(pcb/add-object shape {:index index}))
(cond-> (nil? index)
(pcb/add-object shape))
(cond-> (some? (:parent-id shape))
(pcb/change-parent (:parent-id shape) [shape] index))
(cond-> (some? cell)
(pcb/update-shapes [(:parent-id shape)] #(ctl/push-into-cell % [id] row column)))
(cond-> (ctl/grid-layout? objects (:parent-id shape))
(pcb/update-shapes [(:parent-id shape)] ctl/assign-cells)))]
[shape changes]))
(defn prepare-move-shapes-into-frame
[changes frame-id shapes objects]
(let [ordered-indexes (cph/order-by-indexed-shapes objects shapes)
parent-id (get-in objects [frame-id :parent-id])
ordered-indexes (->> ordered-indexes (remove #(= % parent-id)))
to-move-shapes (map (d/getf objects) ordered-indexes)]
(if (d/not-empty? to-move-shapes)
(-> changes
(cond-> (not (ctl/any-layout? objects frame-id))
(pcb/update-shapes ordered-indexes ctl/remove-layout-item-data))
(pcb/update-shapes ordered-indexes #(cond-> % (cph/frame-shape? %) (assoc :hide-in-viewer true)))
(pcb/change-parent frame-id to-move-shapes 0)
(cond-> (ctl/grid-layout? objects frame-id)
(pcb/update-shapes [frame-id] ctl/assign-cells))
(pcb/reorder-grid-children [frame-id]))
changes)))
(defn prepare-create-artboard-from-selection
[changes id parent-id objects selected index frame-name without-fill?]
(let [selected-objs (map #(get objects %) selected)
new-index (or index
(cph/get-index-replacement selected objects))]
(when (d/not-empty? selected)
(let [srect (gsh/shapes->rect selected-objs)
selected-id (first selected)
frame-id (dm/get-in objects [selected-id :frame-id])
parent-id (or parent-id (dm/get-in objects [selected-id :parent-id]))
attrs {:type :frame
:x (:x srect)
:y (:y srect)
:width (:width srect)
:height (:height srect)}
shape (cts/setup-shape
(cond-> attrs
(some? id)
(assoc :id id)
(some? frame-name)
(assoc :name frame-name)
:always
(assoc :frame-id frame-id
:parent-id parent-id)
:always
(with-meta {:index new-index})
(or (not= frame-id uuid/zero) without-fill?)
(assoc :fills [] :hide-in-viewer true)))
[shape changes]
(prepare-add-shape changes shape objects selected)
changes
(prepare-move-shapes-into-frame changes (:id shape) selected objects)
changes
(cond-> changes
(ctl/grid-layout? objects (:parent-id shape))
(-> (pcb/update-shapes [(:parent-id shape)] ctl/assign-cells)
(pcb/reorder-grid-children [(:parent-id shape)])))]
[shape changes]))))

View file

@ -7,8 +7,8 @@
(ns app.common.geom.shapes.bool
(:require
[app.common.data :as d]
[app.common.path.bool :as pb]
[app.common.path.shapes-to-path :as stp]))
[app.common.svg.path.bool :as pb]
[app.common.svg.path.shapes-to-path :as stp]))
(defn calc-bool-content
[shape objects]

View file

@ -12,8 +12,8 @@
[app.common.geom.rect :as grc]
[app.common.geom.shapes.common :as gco]
[app.common.math :as mth]
[app.common.path.commands :as upc]
[app.common.path.subpaths :as sp]))
[app.common.svg.path.command :as upc]
[app.common.svg.path.subpath :as sp]))
(def ^:const curve-curve-precision 0.1)
(def ^:const curve-range-precision 2)

View file

@ -133,9 +133,10 @@
(defn ceil
"Returns the smallest integer greater than
or equal to a given number."
^double
[v]
#?(:cljs (js/Math.ceil v)
:clj (Math/ceil v)))
:clj (Math/ceil ^double v)))
(defn precision
[v n]

View file

@ -7,6 +7,8 @@
(ns app.common.svg
(:require
#?(:cljs ["./svg_optimizer.js" :as svgo])
#?(:clj [clojure.xml :as xml]
:cljs [tubax.core :as tubax])
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.geom.matrix :as gmt]
@ -14,7 +16,15 @@
[app.common.geom.shapes :as gsh]
[app.common.math :as mth]
[app.common.uuid :as uuid]
[cuerdas.core :as str]))
[cuerdas.core :as str])
#?(:clj
(:import
javax.xml.XMLConstants
java.io.InputStream
javax.xml.parsers.SAXParserFactory
clojure.lang.XMLHandler
org.apache.commons.io.IOUtils)))
;; Regex for XML ids per Spec
;; https://www.w3.org/TR/2008/REC-xml-20081126/#sec-common-syn
@ -1043,3 +1053,26 @@
([input] (optimize input nil))
([input options]
(svgo/optimize input (clj->js options)))))
#?(:clj
(defn- secure-parser-factory
[^InputStream input ^XMLHandler handler]
(.. (doto (SAXParserFactory/newInstance)
(.setFeature XMLConstants/FEATURE_SECURE_PROCESSING true)
(.setFeature "http://apache.org/xml/features/disallow-doctype-decl" true))
(newSAXParser)
(parse input handler))))
(defn strip-doctype
[data]
(cond-> data
(str/includes? data "<!DOCTYPE")
(str/replace #"<\!DOCTYPE[^>]*>" "")))
(defn parse
[text]
#?(:cljs (tubax/xml->clj text)
:clj (let [text (strip-doctype text)]
(dm/with-open [istream (IOUtils/toInputStream text "UTF-8")]
(xml/parse istream secure-parser-factory)))))

View file

@ -4,55 +4,80 @@
;;
;; Copyright (c) KALEIDOS INC
(ns app.util.path.parser
(ns app.common.svg.path
(:require
[app.common.data :as d]
[app.common.geom.point :as gpt]
[app.common.geom.shapes.path :as upg]
[app.common.path.commands :as upc]
[app.common.math :as mth]
[app.common.svg :as csvg]
[app.util.path.arc-to-curve :refer [a2c]]
[app.common.svg.path.command :as upc]
[cuerdas.core :as str]))
;;
(def commands-regex #"(?i)[mzlhvcsqta][^mzlhvcsqta]*")
(def regex #"[+-]?(\d+(\.\d+)?|\.\d+)(e[+-]?\d+)?")
;; 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 #"[+-]?(\d+(\.\d+)?|\.\d+)(e[+-]?\d+)?")
(def flag-regex #"[01]")
(defn extract-params [cmd-str extract-commands]
(loop [result []
extract-idx 0
(defn extract-params
[data pattern]
(loop [result []
ptt-idx 0
current {}
remain (-> cmd-str (subs 1) (str/trim))]
entries (re-seq regex data)
match (ffirst entries)]
(let [[param type] (nth extract-commands extract-idx)
regex (case type
:flag flag-regex
#_:number num-regex)
match (re-find regex remain)]
(if match
(let [[attr-name attr-type] (nth pattern ptt-idx)
ptt-idx (inc ptt-idx)
ptt-cnt (count pattern)
(if match
(let [value (-> match first csvg/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
(seq current) (conj current))))))
value (if (= attr-type :flag)
(if (= 1 (count match))
(d/parse-integer match)
(d/parse-integer (subs match 0 1)))
(-> match csvg/fix-dot-number d/parse-double))
current (assoc current attr-name value)
result (if (>= ptt-idx ptt-cnt)
(conj result current)
result)
current (if (>= ptt-idx ptt-cnt)
{}
current)
match (if (and (= attr-type :flag)
(> (count match) 1))
(subs match 1)
nil)
entries (if match
entries
(rest entries))
match (if match
match
(ffirst entries))
ptt-idx (if (>= ptt-idx ptt-cnt)
0
ptt-idx)]
(recur result
ptt-idx
current
entries
match))
(if (seq current)
(conj result current)
result))))
;; Path specification
;; https://www.w3.org/TR/SVG11/paths.html
(defmulti parse-command (comp str/upper first))
(defmulti parse-command
(fn [cmd]
(str/upper (subs cmd 0 1))))
(defmethod parse-command "M" [cmd]
(let [relative (str/starts-with? cmd "m")
@ -125,8 +150,8 @@
(let [relative (str/starts-with? cmd "q")
param-list (extract-params cmd [[:cx :number]
[:cy :number]
[:x :number]
[:y :number]])]
[:x :number]
[:y :number]])]
(for [params param-list]
{:command :quadratic-bezier-curve-to
:relative relative
@ -142,7 +167,7 @@
:params params})))
(defmethod parse-command "A" [cmd]
(let [relative (str/starts-with? cmd "a")
(let [relative (str/starts-with? cmd "a")
param-list (extract-params cmd [[:rx :number]
[:ry :number]
[:x-axis-rotation :number]
@ -178,7 +203,120 @@
:c2x (:x cp2)
:c2y (:y cp2)}))
(defn arc->beziers [from-p command]
(defn- unit-vector-angle
[ux uy vx vy]
(let [sign (if (> 0 (- (* ux vy) (* uy vx))) -1.0 1.0)
dot (+ (* ux vx) (* uy vy))
dot (cond
(> dot 1.0) 1.0
(< dot -1.0) -1.0
:else dot)]
(* sign (mth/acos dot))))
(defn- get-arc-center [x1 y1 x2 y2 fa fs rx ry sin-phi cos-phi]
(let [x1p (+ (* cos-phi (/ (- x1 x2) 2)) (* sin-phi (/ (- y1 y2) 2)))
y1p (+ (* (- sin-phi) (/ (- x1 x2) 2)) (* cos-phi (/ (- y1 y2) 2)))
rx-sq (* rx rx)
ry-sq (* ry ry)
x1p-sq (* x1p x1p)
y1p-sq (* y1p y1p)
radicant (- (* rx-sq ry-sq)
(* rx-sq y1p-sq)
(* ry-sq x1p-sq))
radicant (if (< radicant 0) 0 radicant)
radicant (/ radicant (+ (* rx-sq y1p-sq) (* ry-sq x1p-sq)))
radicant (* (mth/sqrt radicant) (if (= fa fs) -1 1))
cxp (* radicant (/ rx ry) y1p)
cyp (* radicant (/ (- ry) rx) x1p)
cx (+ (- (* cos-phi cxp)
(* sin-phi cyp))
(/ (+ x1 x2) 2))
cy (+ (* sin-phi cxp)
(* cos-phi cyp)
(/ (+ y1 y2) 2))
v1x (/ (- x1p cxp) rx)
v1y (/ (- y1p cyp) ry)
v2x (/ (- (- x1p) cxp) rx)
v2y (/ (- (- y1p) cyp) ry)
theta1 (unit-vector-angle 1 0 v1x v1y)
dtheta (unit-vector-angle v1x v1y v2x v2y)
dtheta (if (and (= fs 0) (> dtheta 0)) (- dtheta (* mth/PI 2)) dtheta)
dtheta (if (and (= fs 1) (< dtheta 0)) (+ dtheta (* mth/PI 2)) dtheta)]
[cx cy theta1 dtheta]))
(defn approximate-unit-arc
[theta1 dtheta]
(let [alpha (* (/ 4 3) (mth/tan (/ dtheta 4)))
x1 (mth/cos theta1)
y1 (mth/sin theta1)
x2 (mth/cos (+ theta1 dtheta))
y2 (mth/sin (+ theta1 dtheta))]
[x1 y1 (- x1 (* y1 alpha)) (+ y1 (* x1 alpha)) (+ x2 (* y2 alpha)) (- y2 (* x2 alpha)) x2 y2]))
(defn- process-curve
[curve cc rx ry sin-phi cos-phi]
(reduce (fn [curve i]
(let [x (nth curve i)
y (nth curve (inc i))
x (* x rx)
y (* y ry)
xp (- (* cos-phi x) (* sin-phi y))
yp (+ (* sin-phi x) (* cos-phi y))]
(-> curve
(assoc i (+ xp (nth cc 0)))
(assoc (inc i) (+ yp (nth cc 1))))))
curve
(range 0 (count curve) 2)))
(defn arc->beziers*
[x1 y1 x2 y2 fa fs rx ry phi]
(let [tau (* mth/PI 2)
phi-tau (/ (* phi tau) 360)
sin-phi (mth/sin phi-tau)
cos-phi (mth/cos phi-tau)
x1p (+ (/ (* cos-phi (- x1 x2)) 2)
(/ (* sin-phi (- y1 y2)) 2))
y1p (+ (/ (* (- sin-phi) (- x1 x2)) 2)
(/ (* cos-phi (- y1 y2)) 2))]
(if (or (zero? x1p)
(zero? y1p)
(zero? rx)
(zero? ry))
[]
(let [
rx (mth/abs rx)
ry (mth/abs ry)
lambda (+ (/ (* x1p x1p) (* rx rx))
(/ (* y1p y1p) (* ry ry)))
rx (if (> lambda 1) (* rx (mth/sqrt lambda)) rx)
ry (if (> lambda 1) (* ry (mth/sqrt lambda)) ry)
cc (get-arc-center x1 y1 x2 y2 fa fs rx ry sin-phi cos-phi)
theta1 (nth cc 2)
dtheta (nth cc 3)
segments (mth/max (mth/ceil (/ (mth/abs dtheta) (/ tau 4))) 1)
dtheta (/ dtheta segments)]
(loop [i 0.0
t (double theta1)
r []]
(if (< i segments)
(let [curve (approximate-unit-arc t dtheta)
curve (process-curve curve cc rx ry sin-phi cos-phi)]
(recur (inc i)
(+ t dtheta)
(conj r curve)))
r))))))
(defn arc->beziers
[from-p {:keys [params] :as command}]
(let [to-command
(fn [[_ _ c1x c1y c2x c2y x y]]
{:command :curve-to
@ -188,8 +326,16 @@
: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)]
x (get params :x 0.0)
y (get params :y 0.0)
rx (get params :rx 0.0)
ry (get params :ry 0.0)
x-axis-rotation (get params :x-axis-rotation 0)
large-arc-flag (get params :large-arc-flag 0)
sweep-flag (get params :sweep-flag 0)
result (arc->beziers* from-x from-y x y large-arc-flag sweep-flag rx ry x-axis-rotation)]
(mapv to-command result)))
(defn simplify-commands
@ -202,7 +348,6 @@
;; prev-qc : previous command control point for quadratic curves
(fn [[result prev-pos prev-start prev-cc prev-qc] [command _prev]]
(let [command (assoc command :prev-pos prev-pos)
command
(cond-> command
(:relative command)
@ -288,6 +433,7 @@
next-start (if (= :move-to (:command command)) next-pos prev-start)]
[result next-pos next-start next-cc next-qc]))
start (first commands)
@ -301,17 +447,10 @@
(reduce simplify-command [[start] start-pos start-pos start-pos start-pos])
(first))))
(defn parse-path [path-str]
(defn parse
[path-str]
(if (empty? path-str)
path-str
(let [clean-path-str
(-> path-str
(str/trim)
;; Change "commas" for spaces
(str/replace #"," " ")
;; Remove all consecutive spaces
(str/replace #"\s+" " "))
commands (re-seq commands-regex clean-path-str)]
(let [commands (re-seq commands-regex path-str)]
(-> (mapcat parse-command commands)
(simplify-commands)))))

View file

@ -4,14 +4,14 @@
;;
;; Copyright (c) KALEIDOS INC
(ns app.common.path.bool
(ns app.common.svg.path.bool
(:require
[app.common.data :as d]
[app.common.geom.point :as gpt]
[app.common.geom.rect :as grc]
[app.common.geom.shapes.path :as gsp]
[app.common.path.commands :as upc]
[app.common.path.subpaths :as ups]))
[app.common.svg.path.command :as upc]
[app.common.svg.path.subpath :as ups]))
(defn add-previous
([content]

View file

@ -4,7 +4,7 @@
;;
;; Copyright (c) KALEIDOS INC
(ns app.common.path.commands
(ns app.common.svg.path.command
(:require
[app.common.data :as d]
[app.common.geom.point :as gpt]))

View file

@ -4,7 +4,7 @@
;;
;; Copyright (c) KALEIDOS INC
(ns app.common.path.shapes-to-path
(ns app.common.svg.path.shapes-to-path
(:require
[app.common.colors :as clr]
[app.common.data :as d]
@ -13,8 +13,8 @@
[app.common.geom.shapes.common :as gco]
[app.common.geom.shapes.corners :as gso]
[app.common.geom.shapes.path :as gsp]
[app.common.path.bool :as pb]
[app.common.path.commands :as pc]
[app.common.svg.path.bool :as pb]
[app.common.svg.path.command :as pc]
[app.common.types.shape.radius :as ctsr]))
(def ^:const bezier-circle-c 0.551915024494)

View file

@ -4,11 +4,11 @@
;;
;; Copyright (c) KALEIDOS INC
(ns app.common.path.subpaths
(ns app.common.svg.path.subpath
(:require
[app.common.data :as d]
[app.common.geom.point :as gpt]
[app.common.path.commands :as upc]))
[app.common.svg.path.command :as upc]))
(defn pt=
"Check if two points are close"

View file

@ -0,0 +1,534 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.common.svg.shapes-builder
"A SVG to Shapes builder."
(:require
[app.common.colors :as clr]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.helpers :as cfh]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.geom.rect :as grc]
[app.common.geom.shapes :as gsh]
[app.common.geom.shapes.common :as gsc]
[app.common.geom.shapes.transforms :as gst]
[app.common.math :as mth]
[app.common.schema :as sm :refer [max-safe-int min-safe-int]]
[app.common.svg :as csvg]
[app.common.svg.path :as path]
[app.common.types.shape :as cts]
[cuerdas.core :as str]))
(def default-rect
{:x 0 :y 0 :width 1 :height 1})
(defn- assert-valid-num [attr num]
(dm/verify!
["%1 attribute has invalid value: %2" (d/name attr) num]
(and (d/num? num)
(<= num max-safe-int)
(>= num min-safe-int)))
(cond
(and (> num 0) (< num 1)) 1
(and (< num 0) (> num -1)) -1
:else num))
(defn- assert-valid-pos-num
[attr num]
(dm/verify!
["%1 attribute should be positive" (d/name attr)]
(pos? num))
num)
(defn- assert-valid-blend-mode
[mode]
(let [clean-value (-> mode str/trim str/lower keyword)]
(dm/verify!
["%1 is not a valid blend mode" clean-value]
(contains? cts/blend-modes clean-value))
clean-value))
(defn- svg-dimensions
[data]
(let [width (dm/get-in data [:attrs :width] 100)
height (dm/get-in data [:attrs :height] 100)
viewbox (or (dm/get-in data [:attrs :viewBox])
(dm/str "0 0 " width " " height))
[x y width height] (->> (str/split viewbox #"\s+")
(map d/parse-double))
width (if (= width 0) 1 width)
height (if (= height 0) 1 height)]
[(assert-valid-num :x x)
(assert-valid-num :y y)
(assert-valid-pos-num :width width)
(assert-valid-pos-num :height height)]))
(declare create-svg-root)
(declare create-svg-children)
(declare parse-svg-element)
(defn create-svg-shapes
[svg-data {:keys [x y]} objects frame-id parent-id selected center?]
(let [[vb-x vb-y vb-width vb-height] (svg-dimensions svg-data)
unames (cfh/get-used-names objects)
svg-name (str/replace (:name svg-data) ".svg" "")
svg-data (-> svg-data
(assoc :x (mth/round
(if center?
(- x vb-x (/ vb-width 2))
x)))
(assoc :y (mth/round
(if center?
(- y vb-y (/ vb-height 2))
y)))
(assoc :offset-x vb-x)
(assoc :offset-y vb-y)
(assoc :width vb-width)
(assoc :height vb-height)
(assoc :name svg-name))
[def-nodes svg-data]
(-> svg-data
(csvg/fix-default-values)
(csvg/fix-percents)
(csvg/extract-defs))
;; In penpot groups have the size of their children. To
;; respect the imported svg size and empty space let's create
;; a transparent shape as background to respect the imported
;; size
background
{:tag :rect
:attrs {:x (dm/str vb-x)
:y (dm/str vb-y)
:width (dm/str vb-width)
:height (dm/str vb-height)
:fill "none"
:id "base-background"}
:hidden true
:content []}
svg-data (-> svg-data
(assoc :defs def-nodes)
(assoc :content (into [background] (:content svg-data))))
root-shape (create-svg-root frame-id parent-id svg-data)
root-id (:id root-shape)
;; Create the root shape
root-attrs (-> (:attrs svg-data)
(csvg/format-styles))
[_ children]
(reduce (partial create-svg-children objects selected frame-id root-id svg-data)
[unames []]
(d/enumerate (->> (:content svg-data)
(mapv #(csvg/inherit-attributes root-attrs %)))))]
[root-shape children]))
(defn create-raw-svg
[name frame-id {:keys [x y width height offset-x offset-y]} {:keys [attrs] :as data}]
(let [props (csvg/attrs->props attrs)
vbox (grc/make-rect offset-x offset-y width height)]
(cts/setup-shape
{:type :svg-raw
:name name
:frame-id frame-id
:width width
:height height
:x x
:y y
:content data
:svg-attrs props
:svg-viewbox vbox})))
(defn create-svg-root
[frame-id parent-id {:keys [name x y width height offset-x offset-y attrs]}]
(let [props (-> (dissoc attrs :viewBox :view-box :xmlns)
(d/without-keys csvg/inheritable-props)
(csvg/attrs->props))]
(cts/setup-shape
{:type :group
:name name
:frame-id frame-id
:parent-id parent-id
:width width
:height height
:x (+ x offset-x)
:y (+ y offset-y)
:svg-attrs props})))
(defn create-svg-children
[objects selected frame-id parent-id svg-data [unames children] [_index svg-element]]
(let [[shape new-children] (parse-svg-element frame-id svg-data svg-element unames)]
(if (some? shape)
(let [shape-id (:id shape)
shape (-> shape
(assoc :frame-id frame-id)
(assoc :parent-id parent-id))
children (conj children shape)
unames (conj unames (:name shape))]
(reduce (partial create-svg-children objects selected frame-id shape-id svg-data)
[unames children]
(d/enumerate new-children)))
[unames children])))
(defn create-group
[name frame-id {:keys [x y width height offset-x offset-y] :as svg-data} {:keys [attrs]}]
(let [transform (csvg/parse-transform (:transform attrs))
attrs (-> (d/without-keys attrs csvg/inheritable-props)
(csvg/attrs->props))
vbox (grc/make-rect offset-x offset-y width height)]
(cts/setup-shape
{:type :group
:name name
:frame-id frame-id
:x (+ x offset-x)
:y (+ y offset-y)
:width width
:height height
:svg-transform transform
:svg-attrs attrs
:svg-viewbox vbox})))
(defn create-path-shape [name frame-id svg-data {:keys [attrs] :as data}]
(when (and (contains? attrs :d) (seq (:d attrs)))
(let [transform (csvg/parse-transform (:transform attrs))
content (cond-> (path/parse (:d attrs))
(some? transform)
(gsh/transform-content transform))
selrect (gsh/content->selrect content)
points (grc/rect->points selrect)
origin (gpt/negate (gpt/point svg-data))
attrs (-> (dissoc attrs :d :transform)
(csvg/attrs->props))]
(-> (cts/setup-shape
{:type :path
:name name
:frame-id frame-id
:content content
:selrect selrect
:points points
:svg-viewbox selrect
:svg-attrs attrs
:svg-transform transform
:fills []})
(gsh/translate-to-frame origin)))))
(defn calculate-rect-metadata
[rect transform]
(let [points (-> rect
(grc/rect->points)
(gsh/transform-points transform))
center (gsc/points->center points)
selrect (gst/calculate-selrect points center)
transform (gst/calculate-transform points center selrect)]
{:x (:x selrect)
:y (:y selrect)
:width (:width selrect)
:height (:height selrect)
:selrect selrect
:points points
:transform transform
:transform-inverse (when (some? transform)
(gmt/inverse transform))}))
(defn- parse-rect-attrs
[{:keys [x y width height]}]
(grc/make-rect
(d/parse-double x 0)
(d/parse-double y 0)
(d/parse-double width 1)
(d/parse-double height 1)))
(defn create-rect-shape [name frame-id svg-data {:keys [attrs] :as data}]
(let [transform (->> (csvg/parse-transform (:transform attrs))
(gmt/transform-in (gpt/point svg-data)))
origin (gpt/negate (gpt/point svg-data))
rect (-> (parse-rect-attrs attrs)
(update :x - (:x origin))
(update :y - (:y origin)))
props (-> (dissoc attrs :x :y :width :height :rx :ry :transform)
(csvg/attrs->props))]
(cts/setup-shape
(-> (calculate-rect-metadata rect transform)
(assoc :type :rect)
(assoc :name name)
(assoc :frame-id frame-id)
(assoc :svg-viewbox rect)
(assoc :svg-attrs props)
;; We need to ensure fills are empty on import process
;; because setup-shape assings one by default.
(assoc :fills [])
(cond-> (contains? attrs :rx)
(assoc :rx (d/parse-double (:rx attrs) 0)))
(cond-> (contains? attrs :ry)
(assoc :ry (d/parse-double (:ry attrs) 0)))))))
(defn- parse-circle-attrs
[attrs]
(into [] (comp (map (d/getf attrs))
(map d/parse-double))
[:cx :cy :r :rx :ry]))
(defn create-circle-shape
[name frame-id svg-data {:keys [attrs] :as data}]
(let [[cx cy r rx ry]
(parse-circle-attrs attrs)
transform (->> (csvg/parse-transform (:transform attrs))
(gmt/transform-in (gpt/point svg-data)))
rx (d/nilv r rx)
ry (d/nilv r ry)
origin (gpt/negate (gpt/point svg-data))
rect (grc/make-rect
(- cx rx (:x origin))
(- cy ry (:y origin))
(* 2 rx)
(* 2 ry))
props (-> (dissoc attrs :cx :cy :r :rx :ry :transform)
(csvg/attrs->props))]
(cts/setup-shape
(-> (calculate-rect-metadata rect transform)
(assoc :type :circle)
(assoc :name name)
(assoc :frame-id frame-id)
(assoc :svg-viewbox rect)
(assoc :svg-attrs props)
(assoc :fills [])))))
(defn create-image-shape
[name frame-id svg-data {:keys [attrs] :as data}]
(let [transform (->> (csvg/parse-transform (:transform attrs))
(gmt/transform-in (gpt/point svg-data)))
image-url (or (:href attrs) (:xlink:href attrs))
image-data (dm/get-in svg-data [:image-data image-url])
metadata {:width (:width image-data)
:height (:height image-data)
:mtype (:mtype image-data)
:id (:id image-data)}
origin (gpt/negate (gpt/point svg-data))
rect (-> (parse-rect-attrs attrs)
(update :x - (:x origin))
(update :y - (:y origin)))
props (-> (dissoc attrs :x :y :width :height :href :xlink:href)
(csvg/attrs->props))]
(when (some? image-data)
(cts/setup-shape
(-> (calculate-rect-metadata rect transform)
(assoc :type :image)
(assoc :name name)
(assoc :frame-id frame-id)
(assoc :metadata metadata)
(assoc :svg-viewbox rect)
(assoc :svg-attrs props))))))
(defn setup-fill
[shape]
(let [color-attr (str/trim (dm/get-in shape [:svg-attrs :fill]))
color-attr (if (= color-attr "currentColor") clr/black color-attr)
color-style (str/trim (dm/get-in shape [:svg-attrs :style :fill]))
color-style (if (= color-style "currentColor") clr/black color-style)]
(cond-> shape
;; Color present as attribute
(clr/color-string? color-attr)
(-> (update :svg-attrs dissoc :fill)
(update-in [:svg-attrs :style] dissoc :fill)
(assoc-in [:fills 0 :fill-color] (clr/parse color-attr)))
;; Color present as style
(clr/color-string? color-style)
(-> (update-in [:svg-attrs :style] dissoc :fill)
(update :svg-attrs dissoc :fill)
(assoc-in [:fills 0 :fill-color] (clr/parse color-style)))
(dm/get-in shape [:svg-attrs :fillOpacity])
(-> (update :svg-attrs dissoc :fillOpacity)
(update-in [:svg-attrs :style] dissoc :fillOpacity)
(assoc-in [:fills 0 :fill-opacity] (-> (dm/get-in shape [:svg-attrs :fillOpacity])
(d/parse-double 1))))
(dm/get-in shape [:svg-attrs :style :fillOpacity])
(-> (update-in [:svg-attrs :style] dissoc :fillOpacity)
(update :svg-attrs dissoc :fillOpacity)
(assoc-in [:fills 0 :fill-opacity] (-> (dm/get-in shape [:svg-attrs :style :fillOpacity])
(d/parse-double 1)))))))
(defn- setup-stroke
[shape]
(let [attrs (get shape :svg-attrs)
style (get attrs :style)
stroke (or (str/trim (:stroke attrs))
(str/trim (:stroke style)))
color (cond
(= stroke "currentColor") clr/black
(= stroke "none") nil
:else (clr/parse stroke))
opacity (when (some? color)
(d/parse-double
(or (:strokeOpacity attrs)
(:strokeOpacity style))
1))
width (when (some? color)
(d/parse-double
(or (:strokeWidth attrs)
(:strokeWidth style))
1))
linecap (or (get attrs :strokeLinecap)
(get style :strokeLinecap))
linecap (some-> linecap str/trim keyword)
attrs (-> attrs
(dissoc :stroke)
(dissoc :strokeWidth)
(dissoc :strokeOpacity)
(update :style (fn [style]
(-> style
(dissoc :stroke)
(dissoc :strokeLinecap)
(dissoc :strokeWidth)
(dissoc :strokeOpacity)))))]
(cond-> (assoc shape :svg-attrs attrs)
(some? color)
(assoc-in [:strokes 0 :stroke-color] color)
(and (some? color) (some? opacity))
(assoc-in [:strokes 0 :stroke-opacity] opacity)
(and (some? color) (some? width))
(assoc-in [:strokes 0 :stroke-width] width)
(and (some? linecap) (= (:type shape) :path)
(or (= linecap :round) (= linecap :square)))
(assoc :stroke-cap-start linecap
:stroke-cap-end linecap)
(d/any-key? (dm/get-in shape [:strokes 0])
:strokeColor :strokeOpacity :strokeWidth
:strokeCapStart :strokeCapEnd)
(assoc-in [:strokes 0 :stroke-style] :svg))))
(defn setup-opacity [shape]
(cond-> shape
(dm/get-in shape [:svg-attrs :opacity])
(-> (update :svg-attrs dissoc :opacity)
(assoc :opacity (-> (dm/get-in shape [:svg-attrs :opacity])
(d/parse-double 1))))
(dm/get-in shape [:svg-attrs :style :opacity])
(-> (update-in [:svg-attrs :style] dissoc :opacity)
(assoc :opacity (-> (dm/get-in shape [:svg-attrs :style :opacity])
(d/parse-double 1))))
(dm/get-in shape [:svg-attrs :mixBlendMode])
(-> (update :svg-attrs dissoc :mixBlendMode)
(assoc :blend-mode (-> (dm/get-in shape [:svg-attrs :mixBlendMode]) assert-valid-blend-mode)))
(dm/get-in shape [:svg-attrs :style :mixBlendMode])
(-> (update-in [:svg-attrs :style] dissoc :mixBlendMode)
(assoc :blend-mode (-> (dm/get-in shape [:svg-attrs :style :mixBlendMode]) assert-valid-blend-mode)))))
(defn tag->name
"Given a tag returns its layer name"
[tag]
(let [suffix (cond
(string? tag) tag
(keyword? tag) (d/name tag)
(nil? tag) "node"
:else (dm/str tag))]
(dm/str "svg-" suffix)))
(defn parse-svg-element
[frame-id svg-data {:keys [tag attrs hidden] :as element} unames]
;; FIXME: there are cases where element is directly a string, I
;; think we should handle this case early and avoid some code
;; execution
(let [name (or (:id attrs) (tag->name tag))
att-refs (csvg/find-attr-references attrs)
defs (get svg-data :defs)
references (csvg/find-def-references defs att-refs)
href-id (-> (or (:href attrs) (:xlink:href attrs) " ") (subs 1))
use-tag? (and (= :use tag) (contains? defs href-id))]
(if use-tag?
(let [;; Merge the data of the use definition with the properties passed as attributes
use-data (-> (get defs href-id)
(update :attrs #(d/deep-merge % (dissoc attrs :xlink:href :href))))
displacement (gpt/point (d/parse-double (:x attrs "0")) (d/parse-double (:y attrs "0")))
disp-matrix (dm/str (gmt/translate-matrix displacement))
element (-> element
(assoc :tag :g)
(update :attrs dissoc :x :y :width :height :href :xlink:href :transform)
(update :attrs csvg/add-transform disp-matrix)
(assoc :content [use-data]))]
(parse-svg-element frame-id svg-data element unames))
(let [;; SVG graphic elements
;; :circle :ellipse :image :line :path :polygon :polyline :rect :text :use
shape (case tag
(:g :a :svg) (create-group name frame-id svg-data element)
:rect (create-rect-shape name frame-id svg-data element)
(:circle
:ellipse) (create-circle-shape name frame-id svg-data element)
:path (create-path-shape name frame-id svg-data element)
:polyline (create-path-shape name frame-id svg-data (-> element csvg/polyline->path))
:polygon (create-path-shape name frame-id svg-data (-> element csvg/polygon->path))
:line (create-path-shape name frame-id svg-data (-> element csvg/line->path))
:image (create-image-shape name frame-id svg-data element)
#_other (create-raw-svg name frame-id svg-data element))]
(when (some? shape)
(let [shape (-> shape
(assoc :svg-defs (select-keys defs references))
(setup-fill)
(setup-stroke)
(setup-opacity)
(update :svg-attrs (fn [attrs]
(if (empty? (:style attrs))
(dissoc attrs :style)
attrs))))]
[(cond-> shape
hidden (assoc :hidden true))
(cond->> (:content element)
(contains? csvg/parent-tags tag)
(mapv #(csvg/inherit-attributes attrs %)))]))))))

View file

@ -0,0 +1,208 @@
/**
* Arc to Bezier curves transformer
*
* Is a modified and google closure compatible version of the a2c
* functions by https://github.com/fontello/svgpath
*
* @author KALEIDOS INC
* @license MIT License <https://opensource.org/licenses/MIT>
*/
"use strict";
goog.provide("common_tests.arc_to_bezier");
// https://raw.githubusercontent.com/fontello/svgpath/master/lib/a2c.js
goog.scope(function() {
const self = common_tests.arc_to_bezier;
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++) {
var item = approximate_unit_arc(theta1, delta_theta);
result.push(item);
theta1 += delta_theta;
}
// We have a bezier approximation of a unit circle,
// now need to transform back to the original ellipse
//
return result.map(function (curve) {
for (var i = 0; i < curve.length; i += 2) {
var x = curve[i + 0];
var y = curve[i + 1];
// scale
x *= rx;
y *= ry;
// rotate
var xp = cos_phi*x - sin_phi*y;
var yp = sin_phi*x + cos_phi*y;
// translate
curve[i + 0] = xp + cc[0];
curve[i + 1] = yp + cc[1];
}
return curve;
});
}
self.a2c = a2c;
});

View file

@ -0,0 +1,99 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns common-tests.colors-test
(:require
[app.common.data :as d]
[app.common.colors :as colors]
[clojure.test :as t]
#?(:cljs [goog.color :as gcolors])))
(t/deftest valid-hex-color
(t/is (false? (colors/valid-hex-color? nil)))
(t/is (false? (colors/valid-hex-color? "")))
(t/is (false? (colors/valid-hex-color? "#")))
(t/is (false? (colors/valid-hex-color? "#qqqqqq")))
(t/is (true? (colors/valid-hex-color? "#aaa")))
(t/is (true? (colors/valid-hex-color? "#fabada")))
)
(t/deftest valid-rgb-color
(t/is (false? (colors/valid-rgb-color? nil)))
(t/is (false? (colors/valid-rgb-color? "")))
(t/is (false? (colors/valid-rgb-color? "()")))
(t/is (true? (colors/valid-rgb-color? "(255, 30, 30)")))
(t/is (true? (colors/valid-rgb-color? "rgb(255, 30, 30)")))
)
(t/deftest rgb-to-str
(t/is (= "rgb(1,2,3)" (colors/rgb->str [1 2 3])))
(t/is (= "rgba(1,2,3,4)" (colors/rgb->str [1 2 3 4]))))
(t/deftest rgb-to-hsv
;; (prn (colors/rgb->hsv [1 2 3]))
;; (prn (gcolors/rgbToHsv 1 2 3))
(t/is (= [210.0 0.6666666666666666 3.0] (colors/rgb->hsv [1.0 2.0 3.0])))
#?(:cljs (t/is (= (colors/rgb->hsv [1 2 3]) (vec (gcolors/rgbToHsv 1 2 3)))))
)
(t/deftest hsv-to-rgb
(t/is (= [1 2 3]
(colors/hsv->rgb [210 0.6666666666666666 3])))
#?(:cljs
(t/is (= (colors/hsv->rgb [210 0.6666666666666666 3])
(vec (gcolors/hsvToRgb 210 0.6666666666666666 3)))))
)
(t/deftest rgb-to-hex
(t/is (= "#010203" (colors/rgb->hex [1 2 3]))))
(t/deftest hex-to-rgb
(t/is (= [0 0 0] (colors/hex->rgb "#kkk")))
(t/is (= [1 2 3] (colors/hex->rgb "#010203"))))
#?(:cljs
(t/deftest format-hsla
(t/is (= "210, 50%, 1%, 1" (colors/format-hsla [210.0 0.5 0.00784313725490196 1])))))
(t/deftest rgb-to-hsl
(t/is (= [210.0 0.5 0.00784313725490196] (colors/rgb->hsl [1 2 3])))
#?(:cljs (t/is (= (colors/rgb->hsl [1 2 3])
(vec (gcolors/rgbToHsl 1 2 3))))))
(t/deftest hsl-to-rgb
(t/is (= [1 2 3] (colors/hsl->rgb [210.0 0.5 0.00784313725490196])))
(t/is (= [210.0 0.5 0.00784313725490196] (colors/rgb->hsl [1 2 3])))
#?(:cljs (t/is (= (colors/hsl->rgb [210 0.5 0.00784313725490196])
(vec (gcolors/hslToRgb 210 0.5 0.00784313725490196)))))
)
(t/deftest expand-hex
(t/is (= "aaaaaa" (colors/expand-hex "a")))
(t/is (= "aaaaaa" (colors/expand-hex "aa")))
(t/is (= "aaaaaa" (colors/expand-hex "aaa")))
(t/is (= "aaaa" (colors/expand-hex "aaaa"))))
(t/deftest prepend-hash
(t/is "#aaa" (colors/prepend-hash "aaa"))
(t/is "#aaa" (colors/prepend-hash "#aaa")))
(t/deftest remove-hash
(t/is "aaa" (colors/remove-hash "aaa"))
(t/is "aaa" (colors/remove-hash "#aaa")))
(t/deftest color-string-pred
(t/is (true? (colors/color-string? "#aaa")))
(t/is (true? (colors/color-string? "(10,10,10)")))
(t/is (true? (colors/color-string? "rgb(10,10,10)")))
(t/is (true? (colors/color-string? "magenta")))
(t/is (false? (colors/color-string? nil)))
(t/is (false? (colors/color-string? "")))
(t/is (false? (colors/color-string? "kkkkkk")))
)

View file

@ -6,7 +6,7 @@
(ns common-tests.helpers.files
(:require
[app.common.files.features :as ffeat]
[app.common.features :as ffeat]
[app.common.geom.point :as gpt]
[app.common.types.colors-list :as ctcl]
[app.common.types.components-list :as ctkl]

View file

@ -6,7 +6,7 @@
(ns common-tests.pages-test
(:require
[app.common.files.features :as ffeat]
[app.common.features :as ffeat]
[app.common.pages :as cp]
[app.common.types.file :as ctf]
[app.common.types.shape :as cts]

View file

@ -0,0 +1,111 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns common-tests.svg-path-test
(:require
[app.common.data :as d]
[app.common.math :as mth]
[app.common.svg.path :as svg.path]
[clojure.test :as t]
#?(:cljs [common-tests.arc-to-bezier :as impl])))
(t/deftest arc-to-bezier-1
(let [expected1 [-1.6697754290362354e-13
-5.258016244624741e-13
182.99396814652343
578.9410968299095
338.05561855139365
1059.4584670906731
346.33988979885567
1073.265585836443]
expected2 [346.33988979885567
1073.265585836443
354.6241610463177
1087.0727045822134
212.99396814652377
628.9410968299106
30.00000000000016
50.000000000000504]]
(let [[result1 result2 :as total] (svg.path/arc->beziers* 0 0 30 50 0 0 1 162.55 162.45)]
(t/is (= (count total) 2))
(dotimes [i (count result1)]
(t/is (mth/close? (nth result1 i)
(nth expected1 i)
0.000000000001)))
(dotimes [i (count result2)]
(t/is (mth/close? (nth result2 i)
(nth expected2 i)
0.000000000001))))
))
;; "m -994.563 4564.1423 149.3086 -52.8821 30.1828 -1.9265 5.2446 -117.5157 98.6828 -43.7312 219.9492 9.5361 9.0977 121.0797 115.0586 12.7148 -1.1774 75.7109 134.7524 3.1787 -6.1008 85.0544 -137.3211 59.9137 -301.293 -1.0595 -51.375 25.7186 -261.0492 -7.706 " [[:x :number] [:y :number]]
(t/deftest extract-params-1
(let [expected [{:x -994.563, :y 4564.1423}
{:x 149.3086, :y -52.8821}
{:x 30.1828, :y -1.9265}
{:x 5.2446, :y -117.5157}
{:x 98.6828, :y -43.7312}
{:x 219.9492, :y 9.5361}
{:x 9.0977, :y 121.0797}
{:x 115.0586, :y 12.7148}
{:x -1.1774, :y 75.7109}
{:x 134.7524, :y 3.1787}
{:x -6.1008, :y 85.0544}
{:x -137.3211, :y 59.9137}
{:x -301.293, :y -1.0595}
{:x -51.375, :y 25.7186}
{:x -261.0492, :y -7.706}]
cmdstr (str "m -994.563 4564.1423 149.3086 -52.8821 30.1828 "
"-1.9265 5.2446 -117.5157 98.6828 -43.7312 219.9492 "
"9.5361 9.0977 121.0797 115.0586 12.7148 -1.1774 "
"75.7109 134.7524 3.1787 -6.1008 85.0544 -137.3211 "
"59.9137 -301.293 -1.0595 -51.375 25.7186 -261.0492 -7.706 ")
pattern [[:x :number] [:y :number]]]
(t/is (= expected (svg.path/extract-params cmdstr pattern)))))
(t/deftest extract-params-2
(let [expected [{:x -994.563, :y 4564.1423 :r 0}]
cmdstr (str "m -994.563 4564.1423 0")
pattern [[:x :number] [:y :number] [:r :flag]]]
(t/is (= expected (svg.path/extract-params cmdstr pattern)))))
(t/deftest extract-params-3
(let [cmdstr (str "a1.42 1.42 0 00-1.415-1.416 1.42 1.42 0 00-1.416 1.416 "
"1.42 1.42 0 001.416 1.415 1.42 1.42 0 001.415-1.415")
expected [{:rx 1.42, :ry 1.42, :x-axis-rotation 0.0, :large-arc-flag 0, :sweep-flag 0, :x -1.415, :y -1.416}
{:rx 1.42, :ry 1.42, :x-axis-rotation 0.0, :large-arc-flag 0, :sweep-flag 0, :x -1.416, :y 1.416}
{:rx 1.42, :ry 1.42, :x-axis-rotation 0.0, :large-arc-flag 0, :sweep-flag 0, :x 1.416, :y 1.415}
{:rx 1.42, :ry 1.42, :x-axis-rotation 0.0, :large-arc-flag 0, :sweep-flag 0, :x 1.415, :y -1.415}]
pattern [[:rx :number]
[:ry :number]
[:x-axis-rotation :number]
[:large-arc-flag :flag]
[:sweep-flag :flag]
[:x :number]
[:y :number]]
result (svg.path/extract-params cmdstr pattern)]
(t/is (= (nth result 0)
(nth expected 0)))
(t/is (= (nth result 1)
(nth expected 1)))
(t/is (= (nth result 2)
(nth expected 2)))
(t/is (= (nth result 3)
(nth expected 3)))
))
;; FOR POSSIBLE FUTURE TEST CASES
;; (str "M259.958 89.134c-6.88-.354-10.484-1.241-12.44-3.064-1.871-1.743-6.937-3.098-15.793-4.226-7.171-.913-17.179-2.279-22.24-3.034-5.06-.755-15.252-2.016-22.648-2.8-18.685-1.985-35.63-4.223-38.572-5.096-3.655-1.084-3.016-3.548.708-2.726 1.751.387 13.376 1.701 25.833 2.922 12.456 1.22 29.018 3.114 36.803 4.208 29.94 4.206 29.433 4.204 34.267.136 3.787-3.186 5.669-3.669 14.303-3.669 14.338 0 17.18 1.681 12.182 7.205-2.053 2.268-1.994 2.719.707 5.42 3.828 3.827 3.74 5.846-.238 5.5-1.752-.153-7.544-.502-12.872-.776zm7.563-3.194c0-.778-1.751-1.352-3.892-1.274l-3.893.141 3.539 1.133c1.946.624 3.698 1.197 3.893 1.275.194.077.354-.496.354-1.275zm-15.899-8.493c1.43-2.29 1.414-2.83-.084-2.83-2.05 0-5.25 2.76-5.25 4.529 0 2.226 3.599 1.08 5.334-1.699zm8.114 0c2.486-2.746 2.473-2.83-.438-2.83-1.65 0-3.683 1.273-4.516 2.83-1.175 2.196-1.077 2.831.438 2.831 1.075 0 3.107-1.274 4.516-2.83zm7.814.674c2.858-3.444.476-4.085-3.033-.816-2.451 2.284-2.677 2.973-.975 2.973 1.22 0 3.023-.97 4.008-2.157zm-49.571-4.509c-1.168-.43-3.294-1.802-4.725-3.051-2.112-1.843-9.304-2.595-38.219-3.994-46.474-2.25-63-4.077-60.27-6.665.324-.308 9.507.261 20.406 1.264 10.9 1.003 31.16 2.258 45.024 2.789l25.207.964 4.625-3.527c4.313-3.29 5.41-3.474 16.24-2.732 6.389.438 11.981 1.388 12.428 2.111.447.723-.517 2.73-2.141 4.46l-2.954 3.144c1.607 1.697 3.308 3.289 5.049 4.845 3.248 2.189-5.438 1.289-8.678 1.284-5.428-.061-10.825-.463-11.992-.892zm12.74-3.242c-1.123-.694-2.36-.943-2.75-.554-.389.39.21 1.275 1.334 1.97 1.122.693 2.36.942 2.749.553.389-.39-.21-1.275-1.334-1.97zm-5.663 0a1.42 1.42 0 00-1.415-1.416 1.42 1.42 0 00-1.416 1.416 1.42 1.42 0 001.416 1.415 1.42 1.42 0 001.415-1.415zm-8.464-6.404c.984-1.187 1.35-2.598.813-3.135-1.181-1.18-5.408 1.297-6.184 3.624-.806 2.42 3.265 2.048 5.37-.49zm6.863.258c.867-1.045 1.163-2.313.658-2.819-1.063-1.062-4.719 1.631-4.719 3.476 0 1.864 2.274 1.496 4.061-.657zm8.792-.36c1.637-1.972 1.448-2.197-1.486-1.77-1.848.27-3.622 1.287-3.943 2.26-.838 2.547 3.212 2.181 5.429-.49zm32.443-4.11c-6.156-2.228-67.1-6.138-119.124-7.642-39.208-1.134-72.072-.928-94.618.593-6.617.446-19.681 1.16-29.03 1.587-15.798.72-17.183.573-19.588-2.085-4.498-4.97-2.544-7.857 6.39-9.44 4.394-.778 9.164-2.436 10.6-3.685 5.44-4.729 20.332-14.06 31.14-19.509C65.717 11.88 78.955 7.79 103.837 3.08 121.686-.3 125.552-.642 129.318.82c2.44.948 12.4 1.948 22.132 2.221 15.37.432 20.004 1.18 35.294 5.698 22.36 6.606 39.732 15.1 56.55 27.653 7.307 5.452 14.086 9.913 15.066 9.913.98 0 2.148.956 2.596 2.124.55 1.432 2.798 2.123 6.914 2.123 6.213 0 12.4 3.046 12.38 6.096-.012 1.75-6.502 5.353-9.118 5.063-.818-.09-3.717-.972-6.442-1.958zm-16.986-7.436c0-1.575-33.326-18.118-43.173-21.43-23.008-7.739-54.084-12.922-77.136-12.866-16.863.041-37.877 3.628-52.465 8.956-18.062 6.596-26.563 10.384-29.181 13.002-1.205 1.205-5.306 3.769-9.112 5.698-7.754 3.929-8.841 5.482-3.029 4.325 13.494-2.685 66.794-3.773 110.913-2.264 38.005 1.3 96.812 4.435 102.122 5.443.584.111 1.061-.277 1.061-.864zm-236.39-3.18c0-.78-1.592-1.416-3.539-1.416-1.946 0-3.538.637-3.538 1.415 0 .779 1.592 1.416 3.538 1.416 1.947 0 3.54-.637 3.54-1.416zm7.078-1.416c0-.779-.956-1.416-2.124-1.416-1.167 0-2.123.637-2.123 1.416 0 .778.956 1.415 2.123 1.415 1.168 0 2.124-.637 2.124-1.415zm11.734-4.437c3.278-1.661 6.278-3.483 6.667-4.048 1.366-1.98 20.645-11.231 32.557-15.622 11.862-4.372 36.546-9.865 44.327-9.865 3.485 0 3.867-.404 3.012-3.185-.538-1.752-1.177-3.41-1.42-3.685-.907-1.026-36.72 7.16-45.065 10.302-17.226 6.484-47.566 24.27-47.566 27.886 0 1.786.845 1.585 7.488-1.783zm206.254-5.577c-12.298-10.518-53.842-27.166-70.896-28.41-5.526-.404-6.3-.097-6.695 2.655-.33 2.307.402 3.275 2.831 3.742 32.436 6.237 52.205 12.315 66.975 20.594 11.904 6.673 14.477 7.141 7.785 1.419zM150.1 11.04c-1.949-3.64-7.568-4.078-6.886-.538.256 1.329 2.054 2.817 3.997 3.309 4.498 1.137 4.816.832 2.888-2.771zm6.756.94c-.248-1.752-1.026-3.185-1.727-3.185-.7 0-1.493 1.433-1.76 3.185-.328 2.152.232 3.185 1.727 3.185 1.485 0 2.064-1.047 1.76-3.185zm-30.178-2.458c0-2.303-.908-3.694-2.627-4.025-3.6-.694-5.23 1.301-4.22 5.166 1.216 4.647 6.847 3.709 6.847-1.14zm12.544 2.104c-.448-1.168-1.224-2.132-1.725-2.142-.5-.013-2.343-.404-4.095-.873-2.569-.689-3.185-.274-3.185 2.142 0 2.476.854 2.996 4.91 2.996 3.783 0 4.723-.487 4.095-2.123z")

View file

@ -0,0 +1,38 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns common-tests.svg-test
(:require
[app.common.data :as d]
[app.common.svg :as svg]
[clojure.test :as t]))
(t/deftest clean-attrs-1
(let [attrs {:class "foobar"}
result (svg/clean-attrs attrs)]
(t/is (= result {:className "foobar"}))))
(t/deftest clean-attrs-2
(let [attrs {:overline-position "top"
:style {:fill "none"
:stroke-dashoffset 1}}
result (svg/clean-attrs attrs true)]
(t/is (= result {:overlinePosition "top", :style {:fill "none", :strokeDashoffset 1}}))))
(t/deftest clean-attrs-3
(let [attrs {:overline-position "top"
:style (str "fill:#00801b;fill-opacity:1;stroke:none;stroke-width:2749.72;"
"stroke-linecap:round;stroke-dasharray:none;stop-color:#000000")}
result (svg/clean-attrs attrs true)]
(t/is (= result {:overlinePosition "top",
:style {:fill "#00801b",
:fillOpacity "1",
:stroke "none",
:strokeWidth "2749.72",
:strokeLinecap "round",
:strokeDasharray "none",
:stopColor "#000000"}}))))

4
common/vendor/beicon/impl/rxjs.cljs vendored Normal file
View file

@ -0,0 +1,4 @@
(ns beicon.impl.rxjs
(:require ["rxjs" :as rx]))
(goog/exportSymbol "rxjsMain" rx)

View file

@ -0,0 +1,4 @@
(ns beicon.impl.rxjs-operators
(:require ["rxjs/operators" :as rxop]))
(goog/exportSymbol "rxjsOperators" rxop)

4
common/vendor/tubax/saxjs.cljs vendored Normal file
View file

@ -0,0 +1,4 @@
(ns tubax.saxjs
(:require ["sax" :as sax]))
(goog/exportSymbol "sax" sax)

View file

@ -515,6 +515,11 @@ safer-buffer@^2.1.0:
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
sax@^1.2.4:
version "1.3.0"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.3.0.tgz#a5dbe77db3be05c9d1ee7785dbd3ea9de51593d0"
integrity sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==
setimmediate@^1.0.4:
version "1.0.5"
resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"

View file

@ -10,7 +10,6 @@
funcool/beicon {:mvn/version "2021.07.05-1"}
funcool/okulary {:mvn/version "2022.04.11-16"}
funcool/potok {:mvn/version "2022.12.16-71"}
funcool/tubax {:mvn/version "2021.05.20-0"}
funcool/rumext
{:git/tag "v2.7"

View file

@ -11,6 +11,8 @@
[app.common.data.macros :as dm]
[app.common.files.features :as ffeat]
[app.common.files.helpers :as cfh]
[app.common.files.libraries-helpers :as cflh]
[app.common.files.shapes-helpers :as cfsh]
[app.common.geom.align :as gal]
[app.common.geom.point :as gpt]
[app.common.geom.proportions :as gpp]
@ -55,7 +57,6 @@
[app.main.data.workspace.layers :as dwly]
[app.main.data.workspace.layout :as layout]
[app.main.data.workspace.libraries :as dwl]
[app.main.data.workspace.libraries-helpers :as dwlh]
[app.main.data.workspace.media :as dwm]
[app.main.data.workspace.notifications :as dwn]
[app.main.data.workspace.path :as dwdp]
@ -2077,14 +2078,14 @@
page
(cons shape children))
[_ _ changes2] (dwlh/generate-add-component it
[_ _ changes2] (cflh/generate-add-component it
[shape]
(:objects page')
(:id page)
(:id file-data)
true
nil
dwsh/prepare-create-artboard-from-selection)
cfsh/prepare-create-artboard-from-selection)
changes (pcb/concat-changes changes1 changes2)]

View file

@ -10,7 +10,7 @@
[app.common.geom.shapes :as gsh]
[app.common.pages.changes-builder :as pcb]
[app.common.pages.helpers :as cph]
[app.common.path.shapes-to-path :as stp]
[app.common.svg.path.shapes-to-path :as stp]
[app.common.types.container :as ctn]
[app.common.types.shape :as cts]
[app.common.types.shape.layout :as ctl]

View file

@ -6,7 +6,7 @@
(ns app.main.data.workspace.colors
(:require
[app.common.colors :as colors]
[app.common.colors :as cc]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.pages.helpers :as cph]
@ -355,7 +355,7 @@
(assoc-in [:workspace-global :picking-color?] true)
(assoc ::md/modal {:id (random-uuid)
:type :colorpicker
:props {:data {:color colors/black
:props {:data {:color cc/black
:opacity 1}
:disable-opacity false
:disable-gradient false
@ -438,9 +438,9 @@
(defn split-color-components
[{:keys [color opacity] :as data}]
(let [value (if (uc/hex? color) color colors/black)
[r g b] (uc/hex->rgb value)
[h s v] (uc/hex->hsv value)]
(let [value (if (cc/valid-hex-color? color) color cc/black)
[r g b] (cc/hex->rgb value)
[h s v] (cc/hex->hsv value)]
(merge data
{:hex (or value "000000")
:alpha (or opacity 1)

View file

@ -9,6 +9,8 @@
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.features :as ffeat]
[app.common.files.libraries-helpers :as cflh]
[app.common.files.shapes-helpers :as cfsh]
[app.common.geom.point :as gpt]
[app.common.logging :as log]
[app.common.pages :as cp]
@ -305,9 +307,9 @@
parents (into #{} (map :parent-id) shapes)]
(when-not (empty? shapes)
(let [[root _ changes]
(dwlh/generate-add-component it shapes objects page-id file-id components-v2
(cflh/generate-add-component it shapes objects page-id file-id components-v2
dwg/prepare-create-group
dwsh/prepare-create-artboard-from-selection)]
cfsh/prepare-create-artboard-from-selection)]
(when-not (empty? (:redo-changes changes))
(rx/of (dch/commit-changes changes)
(dws/select-shapes (d/ordered-set (:id root)))

View file

@ -25,7 +25,6 @@
[app.common.types.shape-tree :as ctst]
[app.common.types.shape.layout :as ctl]
[app.common.types.typography :as cty]
[app.common.uuid :as uuid]
[app.main.data.workspace.state-helpers :as wsh]
[cljs.spec.alpha :as s]
[clojure.set :as set]))
@ -63,75 +62,6 @@
;; ---- Components and instances creation ----
(defn generate-add-component-changes
[changes root objects file-id page-id components-v2]
(let [name (:name root)
[path name] (cph/parse-path-name name)
[root-shape new-shapes updated-shapes]
(if-not components-v2
(ctn/make-component-shape root objects file-id components-v2)
(ctn/convert-shape-in-component root objects file-id))
changes (-> changes
(pcb/add-component (:id root-shape)
path
name
new-shapes
updated-shapes
(:id root)
page-id))]
[root-shape changes]))
(defn generate-add-component
"If there is exactly one id, and it's a frame (or a group in v1), and not already a component,
use it as root. Otherwise, create a frame (v2) or group (v1) that contains all ids. Then, make a
component with it, and link all shapes to their corresponding one in the component."
[it shapes objects page-id file-id components-v2 prepare-create-group prepare-create-board]
(let [changes (pcb/empty-changes it page-id)
[root changes old-root-ids]
(if (and (= (count shapes) 1)
(or (and (= (:type (first shapes)) :group) (not components-v2))
(= (:type (first shapes)) :frame))
(not (ctk/instance-head? (first shapes))))
[(first shapes)
(-> (pcb/empty-changes it page-id)
(pcb/with-objects objects))
(:shapes (first shapes))]
(let [root-name (if (= 1 (count shapes))
(:name (first shapes))
"Component 1")
[root changes] (if-not components-v2
(prepare-create-group it ; These functions needs to be passed as argument
objects ; to avoid a circular dependence
page-id
shapes
root-name
(not (ctk/instance-head? (first shapes))))
(prepare-create-board changes
(uuid/next)
(:parent-id (first shapes))
objects
(map :id shapes)
nil
root-name
true))]
[root changes (map :id shapes)]))
[root-shape changes] (generate-add-component-changes changes root objects file-id page-id components-v2)
changes (pcb/update-shapes changes
old-root-ids
#(dissoc % :component-root)
[:component-root])]
[root (:id root-shape) changes]))
(defn duplicate-component
"Clone the root shape of the component and all children. Generate new
ids from all of them."
@ -141,7 +71,10 @@
(let [main-instance-page (ctf/get-component-page library-data component)
main-instance-shape (ctf/get-component-root library-data component)
position (gpt/point (+ (:x main-instance-shape) (:width main-instance-shape) 50) (:y main-instance-shape))
position (gpt/point (+ (:x main-instance-shape)
(:width main-instance-shape)
50)
(:y main-instance-shape))
options (if components-v2 {:main-instance? true} {})
[new-instance-shape new-instance-shapes]

View file

@ -14,6 +14,7 @@
[app.common.pages.changes-builder :as pcb]
[app.common.schema :as sm]
[app.common.svg :refer [optimize]]
[app.common.svg.shapes-builder :as csvg.shapes-builder]
[app.common.types.container :as ctn]
[app.common.types.shape :as cts]
[app.common.types.shape-tree :as ctst]
@ -274,7 +275,7 @@
process-svg
(fn [svg-data]
(let [[shape children]
(svg/create-svg-shapes svg-data pos objects uuid/zero nil #{} false)]
(csvg.shapes-builder/create-svg-shapes svg-data pos objects uuid/zero nil #{} false)]
[shape children]))]
(->> (upload-images svg-data)

View file

@ -9,8 +9,8 @@
[app.common.data.macros :as dm]
[app.common.geom.point :as gpt]
[app.common.geom.shapes.flex-layout :as gsl]
[app.common.path.commands :as upc]
[app.common.path.shapes-to-path :as upsp]
[app.common.svg.path.command :as upc]
[app.common.svg.path.shapes-to-path :as upsp]
[app.common.types.container :as ctn]
[app.common.types.shape :as cts]
[app.common.types.shape-tree :as ctst]

View file

@ -11,9 +11,9 @@
[app.common.geom.point :as gpt]
[app.common.geom.shapes.path :as upg]
[app.common.pages.helpers :as cph]
[app.common.path.commands :as upc]
[app.common.path.shapes-to-path :as upsp]
[app.common.path.subpaths :as ups]
[app.common.svg.path.command :as upc]
[app.common.svg.path.shapes-to-path :as upsp]
[app.common.svg.path.subpath :as ups]
[app.main.data.workspace.changes :as dch]
[app.main.data.workspace.common :as dwc]
[app.main.data.workspace.edition :as dwe]

View file

@ -11,8 +11,8 @@
[app.common.geom.rect :as grc]
[app.common.geom.shapes :as gsh]
[app.common.math :as mth]
[app.common.path.commands :as upc]
[app.common.path.subpaths :as ups]
[app.common.svg.path.command :as upc]
[app.common.svg.path.subpath :as ups]
[app.main.data.workspace.path.common :as common]
[app.main.streams :as ms]
[potok.core :as ptk]))

View file

@ -8,7 +8,7 @@
(:require
[app.common.pages.changes-builder :as pcb]
[app.common.pages.helpers :as cph]
[app.common.path.shapes-to-path :as upsp]
[app.common.svg.path.shapes-to-path :as upsp]
[app.common.types.container :as ctn]
[app.main.data.workspace.changes :as dch]
[app.main.data.workspace.state-helpers :as wsh]

View file

@ -6,7 +6,7 @@
(ns app.main.data.workspace.path.state
(:require
[app.common.path.shapes-to-path :as upsp]))
[app.common.svg.path.shapes-to-path :as upsp]))
(defn get-path-id
"Retrieves the currently editing path id"

View file

@ -6,8 +6,8 @@
(ns app.main.data.workspace.path.tools
(:require
[app.common.path.shapes-to-path :as upsp]
[app.common.path.subpaths :as ups]
[app.common.svg.path.shapes-to-path :as upsp]
[app.common.svg.path.subpath :as ups]
[app.main.data.workspace.changes :as dch]
[app.main.data.workspace.edition :as dwe]
[app.main.data.workspace.path.changes :as changes]

View file

@ -9,6 +9,7 @@
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.helpers :as cfh]
[app.common.files.libraries-helpers :as cflh]
[app.common.geom.point :as gpt]
[app.common.geom.rect :as grc]
[app.common.geom.shapes :as gsh]
@ -408,6 +409,7 @@
(instantiate-component))]
changes))
;; TODO: move to common.files.shape-helpers
(defn- prepare-duplicate-shape-change
([changes objects page unames update-unames! ids-map obj delta libraries library-data it file-id]
(prepare-duplicate-shape-change changes objects page unames update-unames! ids-map obj delta libraries library-data it file-id (:frame-id obj) (:parent-id obj) false false))
@ -437,7 +439,7 @@
regenerate-component
(fn [changes shape]
(let [components-v2 (dm/get-in library-data [:options :components-v2])
[_ changes] (dwlh/generate-add-component-changes changes shape objects file-id (:id page) components-v2)]
[_ changes] (cflh/generate-add-component-changes changes shape objects file-id (:id page) components-v2)]
changes))
new-obj

View file

@ -8,7 +8,7 @@
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.geom.shapes :as gsh]
[app.common.files.shapes-helpers :as cfsh]
[app.common.pages.changes-builder :as pcb]
[app.common.pages.helpers :as cph]
[app.common.schema :as sm]
@ -17,8 +17,6 @@
[app.common.types.shape :as cts]
[app.common.types.shape-tree :as ctst]
[app.common.types.shape.interactions :as ctsi]
[app.common.types.shape.layout :as ctl]
[app.common.uuid :as uuid]
[app.main.data.comments :as dc]
[app.main.data.workspace.changes :as dch]
[app.main.data.workspace.edition :as dwe]
@ -32,28 +30,6 @@
(def valid-shape-map?
(sm/pred-fn ::cts/shape))
(defn prepare-add-shape
[changes shape objects _selected]
(let [index (:index (meta shape))
id (:id shape)
mod? (:mod? (meta shape))
[row column :as cell] (when-not mod? (:cell (meta shape)))
changes (-> changes
(pcb/with-objects objects)
(cond-> (some? index)
(pcb/add-object shape {:index index}))
(cond-> (nil? index)
(pcb/add-object shape))
(cond-> (some? (:parent-id shape))
(pcb/change-parent (:parent-id shape) [shape] index))
(cond-> (some? cell)
(pcb/update-shapes [(:parent-id shape)] #(ctl/push-into-cell % [id] row column)))
(cond-> (ctl/grid-layout? objects (:parent-id shape))
(pcb/update-shapes [(:parent-id shape)] ctl/assign-cells)))]
[shape changes]))
(defn add-shape
([shape]
(add-shape shape {}))
@ -73,7 +49,7 @@
[shape changes]
(-> (pcb/empty-changes it page-id)
(pcb/with-objects objects)
(prepare-add-shape shape objects selected))
(cfsh/prepare-add-shape shape objects selected))
changes (cond-> changes
(cph/text-shape? shape)
@ -93,23 +69,6 @@
(->> (rx/of (dwe/start-edition-mode (:id shape)))
(rx/observe-on :async)))))))))
(defn prepare-move-shapes-into-frame
[changes frame-id shapes objects]
(let [ordered-indexes (cph/order-by-indexed-shapes objects shapes)
parent-id (get-in objects [frame-id :parent-id])
ordered-indexes (->> ordered-indexes (remove #(= % parent-id)))
to-move-shapes (map (d/getf objects) ordered-indexes)]
(if (d/not-empty? to-move-shapes)
(-> changes
(cond-> (not (ctl/any-layout? objects frame-id))
(pcb/update-shapes ordered-indexes ctl/remove-layout-item-data))
(pcb/update-shapes ordered-indexes #(cond-> % (cph/frame-shape? %) (assoc :hide-in-viewer true)))
(pcb/change-parent frame-id to-move-shapes 0)
(cond-> (ctl/grid-layout? objects frame-id)
(pcb/update-shapes [frame-id] ctl/assign-cells))
(pcb/reorder-grid-children [frame-id]))
changes)))
(defn move-shapes-into-frame
[frame-id shapes]
(ptk/reify ::move-shapes-into-frame
@ -120,10 +79,10 @@
shapes (->> shapes (remove #(dm/get-in objects [% :blocked])))
changes (-> (pcb/empty-changes it page-id)
(pcb/with-objects objects))
changes (prepare-move-shapes-into-frame changes
frame-id
shapes
objects)]
changes (cfsh/prepare-move-shapes-into-frame changes
frame-id
shapes
objects)]
(if (some? changes)
(rx/of (dch/commit-changes changes))
(rx/empty))))))
@ -366,58 +325,6 @@
;; Artboard
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; FIXME: looks
(defn prepare-create-artboard-from-selection
[changes id parent-id objects selected index frame-name without-fill?]
(let [selected-objs (map #(get objects %) selected)
new-index (or index
(cph/get-index-replacement selected objects))]
(when (d/not-empty? selected)
(let [srect (gsh/shapes->rect selected-objs)
selected-id (first selected)
frame-id (dm/get-in objects [selected-id :frame-id])
parent-id (or parent-id (dm/get-in objects [selected-id :parent-id]))
attrs {:type :frame
:x (:x srect)
:y (:y srect)
:width (:width srect)
:height (:height srect)}
shape (cts/setup-shape
(cond-> attrs
(some? id)
(assoc :id id)
(some? frame-name)
(assoc :name frame-name)
:always
(assoc :frame-id frame-id
:parent-id parent-id)
:always
(with-meta {:index new-index})
(or (not= frame-id uuid/zero) without-fill?)
(assoc :fills [] :hide-in-viewer true)))
[shape changes]
(prepare-add-shape changes shape objects selected)
changes
(prepare-move-shapes-into-frame changes (:id shape) selected objects)
changes
(cond-> changes
(ctl/grid-layout? objects (:parent-id shape))
(-> (pcb/update-shapes [(:parent-id shape)] ctl/assign-cells)
(pcb/reorder-grid-children [(:parent-id shape)])))]
[shape changes]))))
(defn create-artboard-from-selection
([]
(create-artboard-from-selection nil))
@ -438,14 +345,14 @@
(pcb/with-objects objects))
[frame-shape changes]
(prepare-create-artboard-from-selection changes
id
parent-id
objects
selected
index
nil
false)
(cfsh/prepare-create-artboard-from-selection changes
id
parent-id
objects
selected
index
nil
false)
undo-id (js/Symbol)]

View file

@ -11,7 +11,7 @@
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.common.pages.helpers :as cph]
[app.common.path.commands :as upc]
[app.common.svg.path.command :as upc]
[app.common.uuid :as uuid]))
(defn lookup-page

View file

@ -6,467 +6,23 @@
(ns app.main.data.workspace.svg-upload
(:require
[app.common.colors :as clr]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.helpers :as cfh]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.geom.rect :as grc]
[app.common.geom.shapes :as gsh]
[app.common.geom.shapes.common :as gsc]
[app.common.geom.shapes.transforms :as gst]
[app.common.math :as mth]
[app.common.pages.changes-builder :as pcb]
[app.common.pages.helpers :as cph]
[app.common.schema :as sm :refer [max-safe-int min-safe-int]]
[app.common.svg :as csvg]
[app.common.types.shape :as cts]
[app.common.svg.shapes-builder :as csvg.shapes-builder]
[app.common.types.shape-tree :as ctst]
[app.main.data.workspace.changes :as dch]
[app.main.data.workspace.selection :as dws]
[app.main.data.workspace.state-helpers :as wsh]
[app.main.data.workspace.undo :as dwu]
[app.main.repo :as rp]
[app.util.color :as uc]
[app.util.path.parser :as upp]
[app.util.webapi :as wapi]
[beicon.core :as rx]
[cuerdas.core :as str]
[potok.core :as ptk]))
(def default-rect
{:x 0 :y 0 :width 1 :height 1})
(defn- assert-valid-num [attr num]
(dm/verify!
["%1 attribute has invalid value: %2" (d/name attr) num]
(and (d/num? num)
(<= num max-safe-int)
(>= num min-safe-int)))
(cond
(and (> num 0) (< num 1)) 1
(and (< num 0) (> num -1)) -1
:else num))
(defn- assert-valid-pos-num
[attr num]
(dm/verify!
["%1 attribute should be positive" (d/name attr)]
(pos? num))
num)
(defn- assert-valid-blend-mode
[mode]
(let [clean-value (-> mode str/trim str/lower keyword)]
(dm/verify!
["%1 is not a valid blend mode" clean-value]
(contains? cts/blend-modes clean-value))
clean-value))
(defn- svg-dimensions
[data]
(let [width (dm/get-in data [:attrs :width] 100)
height (dm/get-in data [:attrs :height] 100)
viewbox (dm/get-in data [:attrs :viewBox] (str "0 0 " width " " height))
[x y width height] (->> (str/split viewbox #"\s+")
(map d/parse-double))
width (if (= width 0) 1 width)
height (if (= height 0) 1 height)]
[(assert-valid-num :x x)
(assert-valid-num :y y)
(assert-valid-pos-num :width width)
(assert-valid-pos-num :height height)]))
(defn tag->name
"Given a tag returns its layer name"
[tag]
(let [suffix (cond
(string? tag) tag
(keyword? tag) (d/name tag)
(nil? tag) "node"
:else (dm/str tag))]
(dm/str "svg-" suffix)))
(defn setup-fill
[shape]
(let [color-attr (str/trim (dm/get-in shape [:svg-attrs :fill]))
color-attr (if (= color-attr "currentColor") clr/black color-attr)
color-style (str/trim (dm/get-in shape [:svg-attrs :style :fill]))
color-style (if (= color-style "currentColor") clr/black color-style)]
(cond-> shape
;; Color present as attribute
(uc/color? color-attr)
(-> (update :svg-attrs dissoc :fill)
(update-in [:svg-attrs :style] dissoc :fill)
(assoc-in [:fills 0 :fill-color] (uc/parse-color color-attr)))
;; Color present as style
(uc/color? color-style)
(-> (update-in [:svg-attrs :style] dissoc :fill)
(update :svg-attrs dissoc :fill)
(assoc-in [:fills 0 :fill-color] (uc/parse-color color-style)))
(dm/get-in shape [:svg-attrs :fillOpacity])
(-> (update :svg-attrs dissoc :fillOpacity)
(update-in [:svg-attrs :style] dissoc :fillOpacity)
(assoc-in [:fills 0 :fill-opacity] (-> (dm/get-in shape [:svg-attrs :fillOpacity])
(d/parse-double 1))))
(dm/get-in shape [:svg-attrs :style :fillOpacity])
(-> (update-in [:svg-attrs :style] dissoc :fillOpacity)
(update :svg-attrs dissoc :fillOpacity)
(assoc-in [:fills 0 :fill-opacity] (-> (dm/get-in shape [:svg-attrs :style :fillOpacity])
(d/parse-double 1)))))))
(defn- setup-stroke
[shape]
(let [attrs (get shape :svg-attrs)
style (get attrs :style)
stroke (or (str/trim (:stroke attrs))
(str/trim (:stroke style)))
color (cond
(= stroke "currentColor") clr/black
(= stroke "none") nil
:else (uc/parse-color stroke))
opacity (when (some? color)
(d/parse-double
(or (:strokeOpacity attrs)
(:strokeOpacity style))
1))
width (when (some? color)
(d/parse-double
(or (:strokeWidth attrs)
(:strokeWidth style))
1))
linecap (or (get attrs :strokeLinecap)
(get style :strokeLinecap))
linecap (some-> linecap str/trim keyword)
attrs (-> attrs
(dissoc :stroke)
(dissoc :strokeWidth)
(dissoc :strokeOpacity)
(update :style (fn [style]
(-> style
(dissoc :stroke)
(dissoc :strokeLinecap)
(dissoc :strokeWidth)
(dissoc :strokeOpacity)))))]
(cond-> (assoc shape :svg-attrs attrs)
(some? color)
(assoc-in [:strokes 0 :stroke-color] color)
(and (some? color) (some? opacity))
(assoc-in [:strokes 0 :stroke-opacity] opacity)
(and (some? color) (some? width))
(assoc-in [:strokes 0 :stroke-width] width)
(and (some? linecap) (= (:type shape) :path)
(or (= linecap :round) (= linecap :square)))
(assoc :stroke-cap-start linecap
:stroke-cap-end linecap)
(d/any-key? (dm/get-in shape [:strokes 0])
:strokeColor :strokeOpacity :strokeWidth
:strokeCapStart :strokeCapEnd)
(assoc-in [:strokes 0 :stroke-style] :svg))))
(defn setup-opacity [shape]
(cond-> shape
(dm/get-in shape [:svg-attrs :opacity])
(-> (update :svg-attrs dissoc :opacity)
(assoc :opacity (-> (dm/get-in shape [:svg-attrs :opacity])
(d/parse-double 1))))
(dm/get-in shape [:svg-attrs :style :opacity])
(-> (update-in [:svg-attrs :style] dissoc :opacity)
(assoc :opacity (-> (dm/get-in shape [:svg-attrs :style :opacity])
(d/parse-double 1))))
(dm/get-in shape [:svg-attrs :mixBlendMode])
(-> (update :svg-attrs dissoc :mixBlendMode)
(assoc :blend-mode (-> (dm/get-in shape [:svg-attrs :mixBlendMode]) assert-valid-blend-mode)))
(dm/get-in shape [:svg-attrs :style :mixBlendMode])
(-> (update-in [:svg-attrs :style] dissoc :mixBlendMode)
(assoc :blend-mode (-> (dm/get-in shape [:svg-attrs :style :mixBlendMode]) assert-valid-blend-mode)))))
(defn create-raw-svg
[name frame-id {:keys [x y width height offset-x offset-y]} {:keys [attrs] :as data}]
(let [props (csvg/attrs->props attrs)
vbox (grc/make-rect offset-x offset-y width height)]
(cts/setup-shape
{:type :svg-raw
:name name
:frame-id frame-id
:width width
:height height
:x x
:y y
:content data
:svg-attrs props
:svg-viewbox vbox})))
(defn create-svg-root
[frame-id parent-id {:keys [name x y width height offset-x offset-y attrs]}]
(let [props (-> (dissoc attrs :viewBox :view-box :xmlns)
(d/without-keys csvg/inheritable-props)
(csvg/attrs->props))]
(cts/setup-shape
{:type :group
:name name
:frame-id frame-id
:parent-id parent-id
:width width
:height height
:x (+ x offset-x)
:y (+ y offset-y)
:svg-attrs props})))
(defn create-group
[name frame-id {:keys [x y width height offset-x offset-y] :as svg-data} {:keys [attrs]}]
(let [transform (csvg/parse-transform (:transform attrs))
attrs (-> (d/without-keys attrs csvg/inheritable-props)
(csvg/attrs->props))
vbox (grc/make-rect offset-x offset-y width height)]
(cts/setup-shape
{:type :group
:name name
:frame-id frame-id
:x (+ x offset-x)
:y (+ y offset-y)
:width width
:height height
:svg-transform transform
:svg-attrs attrs
:svg-viewbox vbox})))
(defn create-path-shape [name frame-id svg-data {:keys [attrs] :as data}]
(when (and (contains? attrs :d) (seq (:d attrs)))
(let [transform (csvg/parse-transform (:transform attrs))
content (cond-> (upp/parse-path (:d attrs))
(some? transform)
(gsh/transform-content transform))
selrect (gsh/content->selrect content)
points (grc/rect->points selrect)
origin (gpt/negate (gpt/point svg-data))
attrs (-> (dissoc attrs :d :transform)
(csvg/attrs->props))]
(-> (cts/setup-shape
{:type :path
:name name
:frame-id frame-id
:content content
:selrect selrect
:points points
:svg-viewbox selrect
:svg-attrs attrs
:svg-transform transform
:fills []})
(gsh/translate-to-frame origin)))))
(defn calculate-rect-metadata
[rect transform]
(let [points (-> rect
(grc/rect->points)
(gsh/transform-points transform))
center (gsc/points->center points)
selrect (gst/calculate-selrect points center)
transform (gst/calculate-transform points center selrect)]
{:x (:x selrect)
:y (:y selrect)
:width (:width selrect)
:height (:height selrect)
:selrect selrect
:points points
:transform transform
:transform-inverse (when (some? transform)
(gmt/inverse transform))}))
(defn- parse-rect-attrs
[{:keys [x y width height]}]
(grc/make-rect
(d/parse-double x 0)
(d/parse-double y 0)
(d/parse-double width 1)
(d/parse-double height 1)))
(defn create-rect-shape [name frame-id svg-data {:keys [attrs] :as data}]
(let [transform (->> (csvg/parse-transform (:transform attrs))
(gmt/transform-in (gpt/point svg-data)))
origin (gpt/negate (gpt/point svg-data))
rect (-> (parse-rect-attrs attrs)
(update :x - (:x origin))
(update :y - (:y origin)))
props (-> (dissoc attrs :x :y :width :height :rx :ry :transform)
(csvg/attrs->props))]
(cts/setup-shape
(-> (calculate-rect-metadata rect transform)
(assoc :type :rect)
(assoc :name name)
(assoc :frame-id frame-id)
(assoc :svg-viewbox rect)
(assoc :svg-attrs props)
;; We need to ensure fills are empty on import process
;; because setup-shape assings one by default.
(assoc :fills [])
(cond-> (contains? attrs :rx)
(assoc :rx (d/parse-double (:rx attrs) 0)))
(cond-> (contains? attrs :ry)
(assoc :ry (d/parse-double (:ry attrs) 0)))))))
(defn- parse-circle-attrs
[attrs]
(into [] (comp (map (d/getf attrs))
(map d/parse-double))
[:cx :cy :r :rx :ry]))
(defn create-circle-shape
[name frame-id svg-data {:keys [attrs] :as data}]
(let [[cx cy r rx ry]
(parse-circle-attrs attrs)
transform (->> (csvg/parse-transform (:transform attrs))
(gmt/transform-in (gpt/point svg-data)))
rx (d/nilv r rx)
ry (d/nilv r ry)
origin (gpt/negate (gpt/point svg-data))
rect (grc/make-rect
(- cx rx (:x origin))
(- cy ry (:y origin))
(* 2 rx)
(* 2 ry))
props (-> (dissoc attrs :cx :cy :r :rx :ry :transform)
(csvg/attrs->props))]
(cts/setup-shape
(-> (calculate-rect-metadata rect transform)
(assoc :type :circle)
(assoc :name name)
(assoc :frame-id frame-id)
(assoc :svg-viewbox rect)
(assoc :svg-attrs props)
(assoc :fills [])))))
(defn create-image-shape
[name frame-id svg-data {:keys [attrs] :as data}]
(let [transform (->> (csvg/parse-transform (:transform attrs))
(gmt/transform-in (gpt/point svg-data)))
image-url (or (:href attrs) (:xlink:href attrs))
image-data (dm/get-in svg-data [:image-data image-url])
metadata {:width (:width image-data)
:height (:height image-data)
:mtype (:mtype image-data)
:id (:id image-data)}
origin (gpt/negate (gpt/point svg-data))
rect (-> (parse-rect-attrs attrs)
(update :x - (:x origin))
(update :y - (:y origin)))
props (-> (dissoc attrs :x :y :width :height :href :xlink:href)
(csvg/attrs->props))]
(when (some? image-data)
(cts/setup-shape
(-> (calculate-rect-metadata rect transform)
(assoc :type :image)
(assoc :name name)
(assoc :frame-id frame-id)
(assoc :metadata metadata)
(assoc :svg-viewbox rect)
(assoc :svg-attrs props))))))
(defn parse-svg-element
[frame-id svg-data {:keys [tag attrs hidden] :as element} unames]
(let [name (or (:id attrs) (tag->name tag))
att-refs (csvg/find-attr-references attrs)
defs (get svg-data :defs)
references (csvg/find-def-references defs att-refs)
href-id (-> (or (:href attrs) (:xlink:href attrs) "") (subs 1))
use-tag? (and (= :use tag) (contains? defs href-id))]
(if use-tag?
(let [;; Merge the data of the use definition with the properties passed as attributes
use-data (-> (get defs href-id)
(update :attrs #(d/deep-merge % (dissoc attrs :xlink:href :href))))
displacement (gpt/point (d/parse-double (:x attrs "0")) (d/parse-double (:y attrs "0")))
disp-matrix (dm/str (gmt/translate-matrix displacement))
element (-> element
(assoc :tag :g)
(update :attrs dissoc :x :y :width :height :href :xlink:href :transform)
(update :attrs csvg/add-transform disp-matrix)
(assoc :content [use-data]))]
(parse-svg-element frame-id svg-data element unames))
(let [;; SVG graphic elements
;; :circle :ellipse :image :line :path :polygon :polyline :rect :text :use
shape (case tag
(:g :a :svg) (create-group name frame-id svg-data element)
:rect (create-rect-shape name frame-id svg-data element)
(:circle
:ellipse) (create-circle-shape name frame-id svg-data element)
:path (create-path-shape name frame-id svg-data element)
:polyline (create-path-shape name frame-id svg-data (-> element csvg/polyline->path))
:polygon (create-path-shape name frame-id svg-data (-> element csvg/polygon->path))
:line (create-path-shape name frame-id svg-data (-> element csvg/line->path))
:image (create-image-shape name frame-id svg-data element)
#_other (create-raw-svg name frame-id svg-data element))]
(when (some? shape)
(let [shape (-> shape
(assoc :svg-defs (select-keys defs references))
(setup-fill)
(setup-stroke)
(setup-opacity)
(update :svg-attrs (fn [attrs]
(if (empty? (:style attrs))
(dissoc attrs :style)
attrs))))]
[(cond-> shape
hidden (assoc :hidden true))
(cond->> (:content element)
(contains? csvg/parent-tags tag)
(mapv #(csvg/inherit-attributes attrs %)))]))))))
(defn create-svg-children
[objects selected frame-id parent-id svg-data [unames children] [_index svg-element]]
(let [[shape new-children] (parse-svg-element frame-id svg-data svg-element unames)]
(if (some? shape)
(let [shape-id (:id shape)
shape (-> shape
(assoc :frame-id frame-id)
(assoc :parent-id parent-id))
children (conj children shape)
unames (conj unames (:name shape))]
(reduce (partial create-svg-children objects selected frame-id shape-id svg-data)
[unames children]
(d/enumerate new-children)))
[unames children])))
(defn extract-name [url]
(let [query-idx (str/last-index-of url "?")
url (if (> query-idx 0) (subs url 0 query-idx) url)
@ -499,70 +55,6 @@
(rx/map #(vector (:url uri-data) %)))))
(rx/reduce (fn [acc [url image]] (assoc acc url image)) {})))
(defn create-svg-shapes
[svg-data {:keys [x y]} objects frame-id parent-id selected center?]
(let [[vb-x vb-y vb-width vb-height] (svg-dimensions svg-data)
unames (cfh/get-used-names objects)
svg-name (str/replace (:name svg-data) ".svg" "")
svg-data (-> svg-data
(assoc :x (mth/round
(if center?
(- x vb-x (/ vb-width 2))
x)))
(assoc :y (mth/round
(if center?
(- y vb-y (/ vb-height 2))
y)))
(assoc :offset-x vb-x)
(assoc :offset-y vb-y)
(assoc :width vb-width)
(assoc :height vb-height)
(assoc :name svg-name))
[def-nodes svg-data]
(-> svg-data
(csvg/fix-default-values)
(csvg/fix-percents)
(csvg/extract-defs))
;; In penpot groups have the size of their children. To
;; respect the imported svg size and empty space let's create
;; a transparent shape as background to respect the imported
;; size
background
{:tag :rect
:attrs {:x (dm/str vb-x)
:y (dm/str vb-y)
:width (dm/str vb-width)
:height (dm/str vb-height)
:fill "none"
:id "base-background"}
:hidden true
:content []}
svg-data (-> svg-data
(assoc :defs def-nodes)
(assoc :content (into [background] (:content svg-data))))
root-shape (create-svg-root frame-id parent-id svg-data)
root-id (:id root-shape)
;; Create the root shape
root-attrs (-> (:attrs svg-data)
(csvg/format-styles))
[_ children]
(reduce (partial create-svg-children objects selected frame-id root-id svg-data)
[unames []]
(d/enumerate (->> (:content svg-data)
(mapv #(csvg/inherit-attributes root-attrs %)))))]
[root-shape children]))
(defn add-svg-shapes
[svg-data position]
(ptk/reify ::add-svg-shapes
@ -584,7 +76,7 @@
(:parent-id base))
[new-shape new-children]
(create-svg-shapes svg-data position objects frame-id parent-id selected true)
(csvg.shapes-builder/create-svg-shapes svg-data position objects frame-id parent-id selected true)
changes (-> (pcb/empty-changes it page-id)
(pcb/with-objects objects)

View file

@ -6,8 +6,8 @@
(ns app.main.ui.components.color-input
(:require
[app.common.colors :as cc]
[app.common.data :as d]
[app.util.color :as uc]
[app.util.dom :as dom]
[app.util.globals :as globals]
[app.util.i18n :as i18n :refer [tr]]
@ -19,9 +19,9 @@
(defn clean-color
[value]
(-> value
(uc/expand-hex)
(uc/parse-color)
(uc/prepend-hash)))
(cc/expand-hex)
(cc/parse)
(cc/prepend-hash)))
(mf/defc color-input*
{::mf/wrap-props false
@ -62,14 +62,14 @@
(mf/deps ref)
(fn [new-value]
(let [input-node (mf/ref-val ref)]
(dom/set-value! input-node (uc/remove-hash new-value)))))
(dom/set-value! input-node (cc/remove-hash new-value)))))
apply-value
(mf/use-fn
(mf/deps on-change update-input)
(fn [new-value]
(mf/set-ref-val! dirty-ref false)
(when (and new-value (not= (uc/remove-hash new-value) value))
(when (and new-value (not= (cc/remove-hash new-value) value))
(when on-change
(on-change new-value))
(update-input new-value))))
@ -170,7 +170,7 @@
[:> :input props]
;; FIXME: this causes some weird interactions because of using apply-value
;; [:datalist {:id list-id}
;; (for [color-name uc/color-names]
;; (for [color-name cc/color-names]
;; [:option color-name])]
]))

View file

@ -6,11 +6,11 @@
(ns app.main.ui.shapes.filters
(:require
[app.common.colors :as cc]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.geom.shapes.bounds :as gsb]
[app.common.uuid :as uuid]
[app.util.color :as color]
[cuerdas.core :as str]
[rumext.v2 :as mf]))
@ -26,7 +26,7 @@
(mf/defc color-matrix
[{:keys [color]}]
(let [{:keys [color opacity]} color
[r g b a] (color/hex->rgba color opacity)
[r g b a] (cc/hex->rgba color opacity)
[r g b] [(/ r 255) (/ g 255) (/ b 255)]]
[:feColorMatrix
{:type "matrix"

View file

@ -6,12 +6,11 @@
(ns app.main.ui.shapes.text.fo-text
(:require
[app.common.colors :as clr]
[app.common.colors :as cc]
[app.common.data :as d]
[app.common.geom.shapes :as gsh]
[app.main.ui.shapes.attrs :as attrs]
[app.main.ui.shapes.text.styles :as sts]
[app.util.color :as uc]
[app.util.object :as obj]
[cuerdas.core :as str]
[rumext.v2 :as mf]))
@ -85,9 +84,9 @@
[colors]
(assert (set? colors))
(loop [current-rgb [0 0 0]]
(let [current-hex (uc/rgb->hex current-rgb)]
(let [current-hex (cc/rgb->hex current-rgb)]
(if (contains? colors current-hex)
(recur (uc/next-rgb current-rgb))
(recur (cc/next-rgb current-rgb))
current-hex))))
(defn- fill->color
@ -122,7 +121,7 @@
(filter some?))
colors (->> color-data
(into #{clr/black}
(into #{cc/black}
(comp (filter #(= :solid (:type %)))
(map :hex))))

View file

@ -6,6 +6,7 @@
(ns app.main.ui.shapes.text.styles
(:require
[app.common.colors :as cc]
[app.common.data :as d]
[app.common.text :as txt]
[app.common.transit :as transit]
@ -76,7 +77,7 @@
fill-opacity (or (-> data :fills first :fill-opacity) (:fill-opacity data))
fill-gradient (or (-> data :fills first :fill-color-gradient) (:fill-color-gradient data))
[r g b a] (uc/hex->rgba fill-color fill-opacity)
[r g b a] (cc/hex->rgba fill-color fill-opacity)
text-color (when (and (some? fill-color) (some? fill-opacity))
(str/format "rgba(%s, %s, %s, %s)" r g b a))

View file

@ -7,6 +7,7 @@
(ns app.main.ui.viewer.inspect.attributes.common
(:require-macros [app.main.style :as stl])
(:require
[app.common.colors :as cc]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.color-bullet :refer [color-bullet color-name]]
@ -14,7 +15,6 @@
[app.main.ui.components.copy-button :refer [copy-button]]
[app.main.ui.components.select :refer [select]]
[app.main.ui.context :as ctx]
[app.util.color :as uc]
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
[cuerdas.core :as str]
@ -85,10 +85,10 @@
(case format
:hex [:& cbn/color-name {:color color
:size 80}]
:rgba (let [[r g b a] (uc/hex->rgba (:color color) (:opacity color))]
:rgba (let [[r g b a] (cc/hex->rgba (:color color) (:opacity color))]
[:* (str/fmt "%s, %s, %s, %s" r g b a)])
:hsla (let [[h s l a] (uc/hex->hsla (:color color) (:opacity color))
result (uc/format-hsla [h s l a])]
:hsla (let [[h s l a] (cc/hex->hsla (:color color) (:opacity color))
result (cc/format-hsla [h s l a])]
[:* result])))]
(when-not (:gradient color)
@ -111,10 +111,10 @@
(case format
:hex [:& cbn/color-name {:color color
:size 80}]
:rgba (let [[r g b a] (uc/hex->rgba (:color color) (:opacity color))]
:rgba (let [[r g b a] (cc/hex->rgba (:color color) (:opacity color))]
[:* (str/fmt "%s, %s, %s, %s" r g b a)])
:hsla (let [[h s l a] (uc/hex->hsla (:color color) (:opacity color))
result (uc/format-hsla [h s l a])]
:hsla (let [[h s l a] (cc/hex->hsla (:color color) (:opacity color))
result (cc/format-hsla [h s l a])]
[:* result])))]
(when-not (:gradient color)
@ -135,10 +135,10 @@
;; (case format
;; :hex [:& cbn/color-name {:color color
;; :size 80}]
;; :rgba (let [[r g b a] (uc/hex->rgba (:color color) (:opacity color))]
;; :rgba (let [[r g b a] (cc/hex->rgba (:color color) (:opacity color))]
;; [:* (str/fmt "%s, %s, %s, %s" r g b a)])
;; :hsla (let [[h s l a] (uc/hex->hsla (:color color) (:opacity color))
;; result (uc/format-hsla [h s l a])]
;; :hsla (let [[h s l a] (cc/hex->hsla (:color color) (:opacity color))
;; result (cc/format-hsla [h s l a])]
;; [:* result])))]
;; (when color-library-name
@ -163,10 +163,10 @@
(if (:gradient color)
[:& color-name {:color color}]
(case format
:rgba (let [[r g b a] (uc/hex->rgba (:color color) (:opacity color))]
:rgba (let [[r g b a] (cc/hex->rgba (:color color) (:opacity color))]
[:div (str/fmt "%s, %s, %s, %s" r g b a)])
:hsla (let [[h s l a] (uc/hex->hsla (:color color) (:opacity color))
result (uc/format-hsla [h s l a])]
:hsla (let [[h s l a] (cc/hex->hsla (:color color) (:opacity color))
result (cc/format-hsla [h s l a])]
[:div result])
[:*
[:& color-name {:color color}]

View file

@ -7,6 +7,7 @@
(ns app.main.ui.workspace.colorpicker
(:require-macros [app.main.style :as stl])
(:require
[app.common.colors :as cc]
[app.main.data.modal :as modal]
[app.main.data.workspace.colors :as dc]
[app.main.data.workspace.libraries :as dwl]
@ -22,7 +23,6 @@
[app.main.ui.workspace.colorpicker.hsva :refer [hsva-selector]]
[app.main.ui.workspace.colorpicker.libraries :refer [libraries]]
[app.main.ui.workspace.colorpicker.ramp :refer [ramp-selector]]
[app.util.color :as uc]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[cuerdas.core :as str]
@ -190,9 +190,9 @@
(let [node (mf/ref-val node-ref)
{:keys [r g b h v]} current-color
rgb [r g b]
hue-rgb (uc/hsv->rgb [h 1.0 255])
hsl-from (uc/hsv->hsl [h 0.0 v])
hsl-to (uc/hsv->hsl [h 1.0 v])
hue-rgb (cc/hsv->rgb [h 1.0 255])
hsl-from (cc/hsv->hsl [h 0.0 v])
hsl-to (cc/hsv->hsl [h 1.0 v])
format-hsl (fn [[h s l]]
(str/fmt "hsl(%s, %s, %s)"
@ -208,8 +208,8 @@
(mf/with-effect [picking-color? picked-color picked-color-select]
(when (and picking-color? picked-color picked-color-select)
(let [[r g b alpha] picked-color
hex (uc/rgb->hex [r g b])
[h s v] (uc/hex->hsv hex)]
hex (cc/rgb->hex [r g b])
[h s v] (cc/hex->hsv hex)]
(handle-change-color {:hex hex
:r r :g g :b b
:h h :s s :v v

View file

@ -7,10 +7,10 @@
(ns app.main.ui.workspace.colorpicker.color-inputs
(:require-macros [app.main.style :as stl])
(:require
[app.common.colors :as cc]
[app.common.data :as d]
[app.common.math :as mth]
[app.main.ui.context :as ctx]
[app.util.color :as uc]
[app.util.dom :as dom]
[rumext.v2 :as mf]))
@ -45,23 +45,27 @@
setup-hex-color
(fn [hex]
(let [[r g b] (uc/hex->rgb hex)
[h s v] (uc/hex->hsv hex)]
(let [[r g b] (cc/hex->rgb hex)
[h s v] (cc/hex->hsv hex)]
(on-change {:hex hex
:h h :s s :v v
:r r :g g :b b})))
on-change-hex
(fn [e]
(let [val (-> e dom/get-target-val parse-hex)]
(when (uc/hex? val)
(when (cc/valid-hex-color? val)
(setup-hex-color val))))
on-blur-hex
(fn [e]
(let [val (-> e dom/get-target-val)
;; FIXME: looks redundant, cc/parse already handles
;; hex colors; also it performs the parse-hex twice
;; that is completly unnecessary
val (cond
(uc/color? val) (uc/parse-color val)
(uc/hex? (parse-hex val)) (parse-hex val))]
(cc/color-string? val) (cc/parse val)
(cc/valid-hex-color? (parse-hex val)) (parse-hex val))]
(when (some? val)
(setup-hex-color val))))
@ -76,15 +80,15 @@
(when (not (nil? val))
(if (#{:r :g :b} property)
(let [{:keys [r g b]} (merge color (hash-map property val))
hex (uc/rgb->hex [r g b])
[h s v] (uc/hex->hsv hex)]
hex (cc/rgb->hex [r g b])
[h s v] (cc/hex->hsv hex)]
(on-change {:hex hex
:h h :s s :v v
:r r :g g :b b}))
(let [{:keys [h s v]} (merge color (hash-map property val))
hex (uc/hsv->hex [h s v])
[r g b] (uc/hex->rgb hex)]
hex (cc/hsv->hex [h s v])
[r g b] (cc/hex->rgb hex)]
(on-change {:hex hex
:h h :s s :v v
:r r :g g :b b})))))))

View file

@ -7,11 +7,11 @@
(ns app.main.ui.workspace.colorpicker.harmony
(:require-macros [app.main.style :as stl])
(:require
[app.common.colors :as cc]
[app.common.geom.point :as gpt]
[app.common.math :as mth]
[app.main.ui.context :as ctx]
[app.main.ui.workspace.colorpicker.slider-selector :refer [slider-selector]]
[app.util.color :as uc]
[app.util.dom :as dom]
[app.util.object :as obj]
[cuerdas.core :as str]
@ -82,8 +82,8 @@
angle (mth/degrees (mth/atan2 px py))
new-hue (mod (- angle 90) 360)
new-saturation (mth/clamp (mth/distance [px py] [0 0]) 0 1)
hex (uc/hsv->hex [new-hue new-saturation value])
[r g b] (uc/hex->rgb hex)]
hex (cc/hsv->hex [new-hue new-saturation value])
[r g b] (cc/hex->rgb hex)]
(on-change {:hex hex
:r r :g g :b b
:h new-hue
@ -108,15 +108,15 @@
(on-finish-drag))))
on-change-value (fn [new-value]
(let [hex (uc/hsv->hex [hue saturation new-value])
[r g b] (uc/hex->rgb hex)]
(let [hex (cc/hsv->hex [hue saturation new-value])
[r g b] (cc/hex->rgb hex)]
(on-change {:hex hex
:r r :g g :b b
:v new-value})))
on-complement-click (fn [_]
(let [new-hue (mod (+ hue 180) 360)
hex (uc/hsv->hex [new-hue saturation value])
[r g b] (uc/hex->rgb hex)]
hex (cc/hsv->hex [new-hue saturation value])
[r g b] (cc/hex->rgb hex)]
(on-change {:hex hex
:r r :g g :b b
:h new-hue

View file

@ -7,9 +7,9 @@
(ns app.main.ui.workspace.colorpicker.hsva
(:require-macros [app.main.style :as stl])
(:require
[app.common.colors :as cc]
[app.main.ui.context :as ctx]
[app.main.ui.workspace.colorpicker.slider-selector :refer [slider-selector]]
[app.util.color :as uc]
[rumext.v2 :as mf]))
(mf/defc hsva-selector [{:keys [color disable-opacity on-change on-start-drag on-finish-drag]}]
@ -19,8 +19,8 @@
(fn [new-value]
(let [change (hash-map key new-value)
{:keys [h s v]} (merge color change)
hex (uc/hsv->hex [h s v])
[r g b] (uc/hex->rgb hex)]
hex (cc/hsv->hex [h s v])
[r g b] (cc/hex->rgb hex)]
(on-change (merge change
{:hex hex
:r r :g g :b b})))))

View file

@ -7,12 +7,12 @@
(ns app.main.ui.workspace.colorpicker.ramp
(:require-macros [app.main.style :as stl])
(:require
[app.common.colors :as cc]
[app.common.math :as mth]
[app.main.ui.components.color-bullet :refer [color-bullet]]
[app.main.ui.components.color-bullet-new :as cb]
[app.main.ui.context :as ctx]
[app.main.ui.workspace.colorpicker.slider-selector :refer [slider-selector]]
[app.util.color :as uc]
[app.util.dom :as dom]
[rumext.v2 :as mf]))
@ -72,8 +72,8 @@
on-change-value-saturation
(fn [new-saturation new-value]
(let [hex (uc/hsv->hex [hue new-saturation new-value])
[r g b] (uc/hex->rgb hex)]
(let [hex (cc/hsv->hex [hue new-saturation new-value])
[r g b] (cc/hex->rgb hex)]
(on-change {:hex hex
:r r :g g :b b
:s new-saturation
@ -81,8 +81,8 @@
on-change-hue
(fn [new-hue]
(let [hex (uc/hsv->hex [new-hue saturation value])
[r g b] (uc/hex->rgb hex)]
(let [hex (cc/hsv->hex [new-hue saturation value])
[r g b] (cc/hex->rgb hex)]
(on-change {:hex hex
:r r :g g :b b
:h new-hue})))

View file

@ -6,7 +6,7 @@
(ns app.main.ui.workspace.shapes.path
(:require
[app.common.path.commands :as upc]
[app.common.svg.path.command :as upc]
[app.main.data.workspace.path.helpers :as helpers]
[app.main.refs :as refs]
[app.main.ui.shapes.path :as path]

View file

@ -10,8 +10,8 @@
[app.common.data.macros :as dm]
[app.common.geom.point :as gpt]
[app.common.geom.shapes.path :as gsp]
[app.common.path.commands :as upc]
[app.common.path.shapes-to-path :as ups]
[app.common.svg.path.command :as upc]
[app.common.svg.path.shapes-to-path :as ups]
[app.main.data.workspace.path :as drp]
[app.main.snap :as snap]
[app.main.store :as st]

View file

@ -7,6 +7,7 @@
(ns app.main.ui.workspace.sidebar.options.rows.color-row
(:require-macros [app.main.style :as stl])
(:require
[app.common.colors :as cc]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.types.shape.attrs :refer [default-color]]
@ -222,7 +223,7 @@
[:span {:class (stl/css :color-input-wrapper)}
[:> color-input* {:value (if multiple-colors?
""
(-> color :color uc/remove-hash))
(-> color :color cc/remove-hash))
:placeholder (tr "settings.multiple")
:className (stl/css :color-input)
:on-focus on-focus
@ -301,7 +302,7 @@
[:div.color-info
[:> color-input* {:value (if multiple-colors?
""
(-> color :color uc/remove-hash))
(-> color :color cc/remove-hash))
:placeholder (tr "settings.multiple")
:on-focus on-focus
:on-blur on-blur

View file

@ -6,7 +6,7 @@
(ns app.main.ui.workspace.sidebar.options.shapes.svg-raw
(:require
[app.common.colors :as clr]
[app.common.colors :as cc]
[app.common.data :as d]
[app.common.types.shape.layout :as ctl]
[app.main.refs :as refs]
@ -21,7 +21,6 @@
[app.main.ui.workspace.sidebar.options.menus.shadow :refer [shadow-menu]]
[app.main.ui.workspace.sidebar.options.menus.stroke :refer [stroke-attrs stroke-menu]]
[app.main.ui.workspace.sidebar.options.menus.svg-attrs :refer [svg-attrs-menu]]
[app.util.color :as uc]
[cuerdas.core :as str]
[rumext.v2 :as mf]))
@ -45,7 +44,7 @@
{:color :multiple
:opacity :multiple}
:else {:color (uc/parse-color color)
:else {:color (cc/parse color)
:opacity 1})
(catch :default e
@ -71,7 +70,7 @@
(get-in shape [:content :attrs :style :stroke]))
(parse-color))
stroke-color (:color color clr/black)
stroke-color (:color color cc/black)
stroke-opacity (:opacity color 1)
stroke-style (-> (or (get-in shape [:content :attrs :stroke-style])
(get-in shape [:content :attrs :style :stroke-style])

View file

@ -5,125 +5,20 @@
;; Copyright (c) KALEIDOS INC
(ns app.util.color
"Color conversion utils."
"FIXME: this is legacy namespace, all functions of this ns should be
relocated under app.common.types on the respective colors related
namespace. All generic color conversion and other helpers are moved to
app.common.colors namespace."
(:require
[app.common.colors :as cc]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.util.i18n :as i18n :refer [tr]]
[app.util.object :as obj]
[app.util.strings :as ust]
[cuerdas.core :as str]
[goog.color :as gcolor]))
(defn rgb->str
[color]
{:pre [(vector? color)]}
(if (= (count color) 3)
(apply str/format "rgb(%s,%s,%s)" color)
(apply str/format "rgba(%s,%s,%s,%s)" color)))
(defn rgb->hsv
[[r g b]]
(into [] (gcolor/rgbToHsv r g b)))
(defn hsv->rgb
[[h s v]]
(into [] (gcolor/hsvToRgb h s v)))
(defn hex->rgb
[v]
(try
(into [] (gcolor/hexToRgb v))
(catch :default _e [0 0 0])))
(defn rgb->hex
[[r g b]]
(gcolor/rgbToHex r g b))
(defn hex->hsv
[v]
(into [] (gcolor/hexToHsv v)))
(defn hex->rgba
[^string data ^number opacity]
(-> (hex->rgb data)
(conj opacity)))
(defn hex->hsl [hex]
(try
(into [] (gcolor/hexToHsl hex))
(catch :default _e [0 0 0])))
(defn hex->hsla
[^string data ^number opacity]
(-> (hex->hsl data)
(conj opacity)))
(defn format-hsla
[[h s l a]]
(let [precision 2
rounded-s (* 100 (ust/format-precision s precision))
rounded-l (* 100 (ust/format-precision l precision))]
(str/fmt "%s, %s%, %s%, %s" h rounded-s rounded-l a)))
(defn hsl->rgb
[[h s l]]
(gcolor/hslToRgb h s l))
(defn hsl->hex
[[h s l]]
(gcolor/hslToHex h s l))
(defn hex?
[v]
(and (string? v)
(re-seq #"^#[0-9A-Fa-f]{6}$" v)))
(defn hsl->hsv
[[h s l]]
(gcolor/hslToHsv h s l))
(defn hsv->hex
[[h s v]]
(gcolor/hsvToHex h s v))
(defn hsv->hsl
[hsv]
(hex->hsl (hsv->hex hsv)))
(defn expand-hex
[v]
(cond
(re-matches #"^[0-9A-Fa-f]$" v)
(str v v v v v v)
(re-matches #"^[0-9A-Fa-f]{2}$" v)
(str v v v)
(re-matches #"^[0-9A-Fa-f]{3}$" v)
(let [a (nth v 0)
b (nth v 1)
c (nth v 2)]
(str a a b b c c))
:else
v))
(defn prepend-hash
[color]
(gcolor/prependHashIfNecessaryHelper color))
(defn remove-hash
[color]
(if (str/starts-with? color "#")
(subs color 1)
color))
[cuerdas.core :as str]))
(defn gradient->css [{:keys [type stops]}]
(let [parse-stop
(fn [{:keys [offset color opacity]}]
(let [[r g b] (hex->rgb color)]
(let [[r g b] (cc/hex->rgb color)]
(str/fmt "rgba(%s, %s, %s, %s) %s" r g b opacity (str (* offset 100) "%"))))
stops-css (str/join "," (map parse-stop stops))]
@ -147,7 +42,7 @@
(gradient->css gradient)
(not= color :multiple)
(let [[r g b] (hex->rgb (or color value))]
(let [[r g b] (cc/hex->rgb (or color value))]
(str/fmt "rgba(%s, %s, %s, %s)" r g b opacity))
:else "transparent")))
@ -160,56 +55,27 @@
(not= color :multiple)
(case format
:rgba (let [[r g b] (hex->rgb color)]
:rgba (let [[r g b] (cc/hex->rgb color)]
(str/fmt "rgba(%s, %s, %s, %s)" r g b opacity))
:hsla (let [[h s l] (hex->hsl color)]
:hsla (let [[h s l] (cc/hex->hsl color)]
(str/fmt "hsla(%s, %s, %s, %s)" h (* 100 s) (* 100 l) opacity))
:hex (str color (str/upper (d/opacity-to-hex opacity))))
:else "transparent")))
(defn multiple? [{:keys [id file-id value color gradient]}]
(defn multiple?
[{:keys [id file-id value color gradient]}]
(or (= value :multiple)
(= color :multiple)
(= gradient :multiple)
(= id :multiple)
(= file-id :multiple)))
(defn color?
[color]
(and (string? color)
(gcolor/isValidColor color)))
(defn parse-color
[color]
(when (color? color)
(let [result (gcolor/parse color)]
(dm/str (.-hex ^js result)))))
(def color-names
(obj/get-keys ^js gcolor/names))
(def empty-color
(into {} (map #(vector % nil)) [:color :id :file-id :gradient :opacity]))
(defn next-rgb
"Given a color in rgb returns the next color"
[[r g b]]
(cond
(and (= 255 r) (= 255 g) (= 255 b))
(throw (ex-info "cannot get next color" {:r r :g g :b b}))
(and (= 255 g) (= 255 b))
[(inc r) 0 0]
(= 255 b)
[r (inc g) 0]
:else
[r g (inc b)]))
(defn get-color-name
[color]
(or (:color-library-name color)

View file

@ -6,15 +6,15 @@
(ns app.util.import.parser
(:require
[app.common.colors :as cc]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.svg.path :as svg.path]
[app.common.types.shape.interactions :as ctsi]
[app.common.uuid :as uuid]
[app.util.color :as uc]
[app.util.json :as json]
[app.util.path.parser :as upp]
[cuerdas.core :as str]))
(def url-regex
@ -278,7 +278,7 @@
(defn parse-path
[props center svg-data]
(let [content (upp/parse-path (:d svg-data))]
(let [content (svg.path/parse (:d svg-data))]
(-> props
(assoc :content content)
(assoc :center center))))
@ -454,7 +454,7 @@
:fill-color nil
:fill-opacity nil)
(uc/hex? fill)
(cc/valid-hex-color? fill)
(assoc :fill-color fill
:fill-opacity (-> svg-data (:fill-opacity "1") d/parse-double))

View file

@ -6,8 +6,8 @@
(ns app.util.path.format
(:require
[app.common.path.commands :as upc]
[app.common.path.subpaths :refer [pt=]]
[app.common.svg.path.command :as upc]
[app.common.svg.path.subpath :refer [pt=]]
[app.util.array :as arr]))
(def path-precision 3)

View file

@ -9,7 +9,7 @@
[app.common.data :as d]
[app.common.geom.point :as gpt]
[app.common.geom.shapes.path :as upg]
[app.common.path.commands :as upc]
[app.common.svg.path.command :as upc]
[clojure.set :as set]))
(defn remove-line-curves

View file

@ -7,9 +7,13 @@
(ns frontend-tests.helpers.pages
(:require
[app.common.data :as d]
[app.common.files.libraries-helpers :as cflh]
[app.common.files.shapes-helpers :as cfsh]
[app.common.geom.point :as gpt]
[app.common.pages :as cp]
[app.common.pages.changes-builder :as pcb]
[app.common.files.libraries-helpers :as cflh]
[app.common.files.shapes-helpers :as cfsh]
[app.common.pages.helpers :as cph]
[app.common.types.shape :as cts]
[app.common.uuid :as uuid]
@ -113,7 +117,7 @@
(if (empty? shapes)
state
(let [[frame changes]
(dwsh/prepare-create-artboard-from-selection changes
(cfsh/prepare-create-artboard-from-selection changes
nil
nil
(:objects page)
@ -133,14 +137,14 @@
shapes (dwg/shapes-for-grouping objects shape-ids)
[group component-id changes]
(dwlh/generate-add-component nil
(cflh/generate-add-component nil
shapes
(:objects page)
(:id page)
current-file-id
true
dwg/prepare-create-group
dwsh/prepare-create-artboard-from-selection)]
cfsh/prepare-create-artboard-from-selection)]
(swap! idmap assoc instance-label (:id group)
component-label component-id)

View file

@ -201,7 +201,9 @@
(t/is (= (:shape-ref c-shape1) nil))
(t/is (= (:name c-shape2) "Rect 2"))
(t/is (= (:touched c-shape2) nil))
(t/is (= (:shape-ref c-shape2) nil)))))]
(t/is (= (:shape-ref c-shape2) nil))
)))]
(ptk/emit!
store