0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-03-15 17:21:17 -05:00

Merge pull request #443 from penpot/feature/paste-svg

Upload SVG as shapes
This commit is contained in:
Andrey Antukh 2021-01-08 11:27:51 +01:00 committed by GitHub
commit 2d07df2541
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 922 additions and 328 deletions

View file

@ -125,7 +125,7 @@
(ex/raise :type :validation
:code :media-type-mismatch
:hint (str "Seems like you are uploading a file whose content does not match the extension."
"Expected: " mtype "Got: " mtype')))
"Expected: " mtype ". Got: " mtype')))
{:width (.getImageWidth instance)
:height (.getImageHeight instance)
:mtype mtype'})))

View file

@ -84,7 +84,7 @@
:thumbnail-id (:id thumb)
:width (:width source-info)
:height (:height source-info)
:mtype (:mtype source-info)})))
:mtype source-mtype})))
;; --- Create File Media Object (from URL)

View file

@ -11,6 +11,7 @@
(:require
[app.common.exceptions :as ex]
[app.common.spec :as us]
[cuerdas.core :as str]
[app.metrics :as mtx]
[clojure.java.io :as io]
[clojure.java.shell :as shell]
@ -25,10 +26,25 @@
[^String data]
(IOUtils/toInputStream data "UTF-8"))
(defn- stream->string
[input]
(with-open [istream (io/input-stream input)]
(-> (IOUtils/toString input "UTF-8"))))
(defn- clean-svg
[^InputStream input]
(let [result (shell/sh "svgcleaner" "-c" "-" :in input :out-enc :bytes)]
(when (not= 0 (:exit result))
(let [result (shell/sh
;; "svgcleaner" "--allow-bigger-file" "-c" "-"
"svgo"
"--enable=prefixIds,removeDimensions,removeXMLNS,removeScriptElement"
"--disable=removeViewBox,moveElemsAttrsToGroup"
"-i" "-" "-o" "-"
:in input :out-enc :bytes)
err-str (:err result)]
(when (or (not= 0 (:exit result))
;; svgcleaner returns 0 with some errors, we need to check
(and (not= err-str "") (not (nil? err-str)) (str/starts-with? err-str "Error")))
(ex/raise :type :validation
:code :unable-to-optimize
:hint (:err result)))

View file

@ -261,9 +261,13 @@
(d/export gco/center-selrect)
(d/export gco/center-rect)
(d/export gco/center-points)
(d/export gpr/rect->selrect)
(d/export gpr/rect->points)
(d/export gpr/points->selrect)
(d/export gpr/points->rect)
(d/export gpr/center->rect)
(d/export gtr/transform-shape)
(d/export gtr/transform-matrix)
(d/export gtr/inverse-transform-matrix)

View file

@ -58,3 +58,12 @@
:width (- maxx minx)
:height (- maxy miny)}))
(defn center->rect [center width height]
(assert (gpt/point center))
(assert (and (number? width) (> width 0)))
(assert (and (number? height) (> height 0)))
{:x (- (:x center) (/ width 2))
:y (- (:y center) (/ height 2))
:width width
:height height})

View file

@ -26,17 +26,17 @@
"Returns a transformation matrix without changing the shape properties.
The result should be used in a `transform` attribute in svg"
([shape] (transform-matrix shape nil))
([{:keys [flip-x flip-y] :as shape} {:keys [no-flip]}]
(let [shape-center (or (gco/center-shape shape)
(gpt/point 0 0))]
(-> (gmt/matrix)
(gmt/translate shape-center)
([shape params] (transform-matrix shape params (or (gco/center-shape shape)
(gpt/point 0 0))))
([{:keys [flip-x flip-y] :as shape} {:keys [no-flip]} shape-center]
(-> (gmt/matrix)
(gmt/translate shape-center)
(gmt/multiply (:transform shape (gmt/matrix)))
(cond->
(and (not no-flip) flip-x) (gmt/scale (gpt/point -1 1))
(and (not no-flip) flip-y) (gmt/scale (gpt/point 1 -1)))
(gmt/translate (gpt/negate shape-center))))))
(gmt/multiply (:transform shape (gmt/matrix)))
(cond->
(and (not no-flip) flip-x) (gmt/scale (gpt/point -1 1))
(and (not no-flip) flip-y) (gmt/scale (gpt/point 1 -1)))
(gmt/translate (gpt/negate shape-center)))))
(defn inverse-transform-matrix
([shape]

View file

@ -31,8 +31,7 @@
:pages-index {}})
(def default-shape-attrs
{:fill-color default-color
:fill-opacity 1})
{})
(def default-frame-attrs
{:frame-id uuid/zero
@ -55,8 +54,6 @@
{:type :image}
{:type :icon}
{:type :circle
:name "Circle"
:fill-color default-color
@ -89,7 +86,9 @@
{:type :text
:name "Text"
:content nil}])
:content nil}
{:type :svg-raw}])
(defn make-minimal-shape
[type]

View file

@ -133,7 +133,8 @@ RUN set -ex; \
mv /tmp/node/node-$NODE_VERSION-linux-x64 /usr/local/nodejs; \
chown -R root /usr/local/nodejs; \
/usr/local/nodejs/bin/npm install -g yarn; \
rm -rf /tmp/node;
/usr/local/nodejs/bin/npm install -g svgo; \
rm -rf /tmp/node;
RUN set -ex; \
cd /tmp; \

View file

@ -1282,6 +1282,12 @@
},
"unused" : true
},
"handoff.tabs.code.selected.svg-raw" : {
"translations" : {
"en" : "SVG"
},
"unused" : true
},
"handoff.tabs.info" : {
"used-in" : [ "src/app/main/ui/handoff/right_sidebar.cljs:59" ],
"translations" : {

View file

@ -553,35 +553,6 @@
(assoc :zoom zoom)
(update :vbox merge srect)))))))))))
;; --- Add shape to Workspace
(defn- viewport-center
[state]
(let [{:keys [x y width height]} (get-in state [:workspace-local :vbox])]
[(+ x (/ width 2)) (+ y (/ height 2))]))
(defn create-and-add-shape
[type frame-x frame-y data]
(ptk/reify ::create-and-add-shape
ptk/WatchEvent
(watch [_ state stream]
(let [{:keys [width height]} data
[vbc-x vbc-y] (viewport-center state)
x (:x data (- vbc-x (/ width 2)))
y (:y data (- vbc-y (/ height 2)))
page-id (:current-page-id state)
frame-id (-> (dwc/lookup-page-objects state page-id)
(cp/frame-id-by-position {:x frame-x :y frame-y}))
shape (-> (cp/make-minimal-shape type)
(merge data)
(merge {:x x :y y})
(assoc :frame-id frame-id)
(gsh/setup-selrect))]
(rx/of (dwc/add-shape shape))))))
;; --- Update Shape Attrs
@ -1417,20 +1388,6 @@
(dwc/add-shape shape)
(dwc/commit-undo-transaction))))))
(defn- image-uploaded
[image]
(let [{:keys [x y]} @ms/mouse-position
{:keys [width height]} image
shape {:name (:name image)
:width width
:height height
:x (- x (/ width 2))
:y (- y (/ height 2))
:metadata {:width width
:height height
:id (:id image)
:path (:path image)}}]
(st/emit! (create-and-add-shape :image x y shape))))
(defn- paste-image
[image]
@ -1439,11 +1396,8 @@
(watch [_ state stream]
(let [file-id (get-in state [:workspace-file :id])
params {:file-id file-id
:local? true
:data [image]}]
(rx/of (dwp/upload-media-objects
(with-meta params
{:on-success image-uploaded})))))))
(rx/of (dwp/upload-media-workspace params @ms/mouse-position))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Interactions
@ -1536,6 +1490,7 @@
:value previus-color}]
{:commit-local? true}))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Exports
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -1558,8 +1513,10 @@
(d/export dwp/fetch-shared-files)
(d/export dwp/link-file-to-library)
(d/export dwp/unlink-file-from-library)
(d/export dwp/upload-media-objects)
(d/export dwp/upload-media-asset)
(d/export dwp/upload-media-workspace)
(d/export dwp/clone-media-object)
(d/export dwc/image-uploaded)
;; Selection

View file

@ -21,6 +21,7 @@
[beicon.core :as rx]
[cljs.spec.alpha :as s]
[clojure.set :as set]
[cuerdas.core :as str]
[potok.core :as ptk]))
;; Change this to :info :debug or :trace to debug this module
@ -522,6 +523,27 @@
(update-in [:workspace-local :hover] disj id)
(update :workspace-local dissoc :edition))))))
(defn add-shape-changes
[page-id attrs]
(let [id (:id attrs)
frame-id (:frame-id attrs)
shape (gpr/setup-proportions attrs)
default-attrs (if (= :frame (:type shape))
cp/default-frame-attrs
cp/default-shape-attrs)
shape (merge default-attrs shape)
redo-changes [{:type :add-obj
:id id
:page-id page-id
:frame-id frame-id
:obj shape}]
undo-changes [{:type :del-obj
:page-id page-id
:id id}]]
[redo-changes undo-changes]))
(defn add-shape
[attrs]
@ -532,36 +554,21 @@
(let [page-id (:current-page-id state)
objects (lookup-page-objects state page-id)
id (or (:id attrs) (uuid/next))
shape (gpr/setup-proportions attrs)
unames (retrieve-used-names objects)
name (generate-unique-name unames (:name shape))
id (or (:id attrs) (uuid/next))
name (-> objects
(retrieve-used-names)
(generate-unique-name (:name attrs)))
frame-id (if (= :frame (:type attrs))
uuid/zero
(or (:frame-id attrs)
(cp/frame-id-by-position objects attrs)))
shape (merge
(if (= :frame (:type shape))
cp/default-frame-attrs
cp/default-shape-attrs)
(assoc shape
:id id
:name name))
rchange {:type :add-obj
:id id
:page-id page-id
:frame-id frame-id
:obj shape}
uchange {:type :del-obj
:page-id page-id
:id id}]
[rchanges uchanges] (add-shape-changes page-id (assoc attrs
:id id
:frame-id frame-id
:name name))]
(rx/concat
(rx/of (commit-changes [rchange] [uchange] {:commit-local? true})
(rx/of (commit-changes rchanges uchanges {:commit-local? true})
(select-shapes (d/ordered-set id)))
(when (= :text (:type attrs))
(->> (rx/of (start-edition-mode id))
@ -595,3 +602,123 @@
:index index
:shapes [shape-id]})))]
(rx/of (commit-changes rchanges uchanges {:commit-local? true}))))))
;; --- Add shape to Workspace
(defn- viewport-center
[state]
(let [{:keys [x y width height]} (get-in state [:workspace-local :vbox])]
[(+ x (/ width 2)) (+ y (/ height 2))]))
(defn create-and-add-shape
[type frame-x frame-y data]
(ptk/reify ::create-and-add-shape
ptk/WatchEvent
(watch [_ state stream]
(let [{:keys [width height]} data
[vbc-x vbc-y] (viewport-center state)
x (:x data (- vbc-x (/ width 2)))
y (:y data (- vbc-y (/ height 2)))
page-id (:current-page-id state)
frame-id (-> (lookup-page-objects state page-id)
(cp/frame-id-by-position {:x frame-x :y frame-y}))
shape (-> (cp/make-minimal-shape type)
(merge data)
(merge {:x x :y y})
(assoc :frame-id frame-id)
(gsh/setup-selrect))]
(rx/of (add-shape shape))))))
(defn image-uploaded [image x y]
(ptk/reify ::image-uploaded
ptk/WatchEvent
(watch [_ state stream]
(let [{:keys [name width height id mtype]} image
shape {:name name
:width width
:height height
:x (- x (/ width 2))
:y (- y (/ height 2))
:metadata {:width width
:height height
:mtype mtype
:id id}}]
(rx/of (create-and-add-shape :image x y shape))))))
(defn- svg-dimensions [data]
(let [width (get-in data [:attrs :width] 100)
height (get-in data [:attrs :height] 100)
viewbox (get-in data [:attrs :viewBox] (str "0 0 " width " " height))
[_ _ width-str height-str] (str/split viewbox " ")
width (d/parse-integer width-str)
height (d/parse-integer height-str)]
[width height]))
(defn svg-uploaded [data x y]
(ptk/reify ::svg-uploaded
ptk/WatchEvent
(watch [_ state stream]
(let [page-id (:current-page-id state)
objects (lookup-page-objects state page-id)
frame-id (cp/frame-id-by-position objects {:x x :y y})
[width height] (svg-dimensions data)
x (- x (/ width 2))
y (- y (/ height 2))
create-svg-raw
(fn [{:keys [tag] :as data} unames root-id]
(let [base (cond (string? tag) tag
(keyword? tag) (name tag)
(nil? tag) "node"
:else (str tag))]
(-> {:id (uuid/next)
:type :svg-raw
:name (generate-unique-name unames (str "svg-" base))
:frame-id frame-id
;; For svg children we set its coordinates as the root of the svg
:width width
:height height
:x x
:y y
:content data
:root-id root-id}
(gsh/setup-selrect))))
add-svg-child
(fn add-svg-child [parent-id root-id [unames [rchs uchs]] [index {:keys [content] :as data}]]
(let [shape (create-svg-raw data unames root-id)
shape-id (:id shape)
[rch1 uch1] (add-shape-changes page-id shape)
;; Mov-objects won't have undo because we "delete" the object in the undo of the
;; previous operation
rch2 [{:type :mov-objects
:parent-id parent-id
:frame-id frame-id
:page-id page-id
:index index
:shapes [shape-id]}]
;; Careful! the undo changes are concatenated reversed (we undo in reverse order
changes [(d/concat rchs rch1 rch2) (d/concat uch1 uchs)]
unames (conj unames (:name shape))]
(reduce (partial add-svg-child shape-id root-id) [unames changes] (d/enumerate (:content data)))))
unames (retrieve-used-names objects)
svg-name (->> (str/replace (:name data) ".svg" "")
(generate-unique-name unames))
root-shape (create-svg-raw data unames nil)
root-shape (-> root-shape
(assoc :name svg-name))
root-id (:id root-shape)
changes (add-shape-changes page-id root-shape)
[_ [rchanges uchanges]] (reduce (partial add-svg-child root-id root-id) [unames changes] (d/enumerate (:content data)))]
(rx/of (commit-changes rchanges uchanges {:commit-local? true})
(select-shapes (d/ordered-set root-id)))))))

View file

@ -9,6 +9,8 @@
(ns app.main.data.workspace.persistence
(:require
[cuerdas.core :as str]
[app.util.http :as http]
[app.common.data :as d]
[app.common.geom.point :as gpt]
[app.common.media :as cm]
@ -19,6 +21,7 @@
[app.main.data.media :as di]
[app.main.data.messages :as dm]
[app.main.data.workspace.common :as dwc]
[app.main.data.workspace.libraries :as dwl]
[app.main.repo :as rp]
[app.main.store :as st]
[app.util.i18n :as i18n :refer [tr]]
@ -29,7 +32,8 @@
[app.util.avatars :as avatars]
[beicon.core :as rx]
[cljs.spec.alpha :as s]
[potok.core :as ptk]))
[potok.core :as ptk]
[app.main.store :as st]))
(declare persist-changes)
(declare shapes-changes-persisted)
@ -345,38 +349,123 @@
(s/def ::name ::us/string)
(s/def ::uri ::us/string)
(s/def ::uris (s/coll-of ::uri))
(s/def ::mtype ::us/string)
(s/def ::upload-media-objects
(s/and
(s/keys :req-un [::file-id ::local?]
:opt-in [::name ::data ::uris])
:opt-in [::name ::data ::uris ::mtype])
(fn [props]
(or (contains? props :data)
(contains? props :uris)))))
(defn parse-svg [text]
(->> (http/send! {:method :post
:uri "/api/svg"
:headers {"content-type" "image/svg+xml"}
:body text})
(rx/map (fn [{:keys [status body]}]
(let [result (t/decode body)]
(if (= status 200)
result
(throw result)))))))
(defn fetch-svg [uri]
(->> (http/send! {:method :get :uri uri})
(rx/map :body)))
(defn url-name [url]
(let [query-idx (str/last-index-of url "?")
url (if (> query-idx 0) (subs url 0 query-idx) url)
filename (->> (str/split url "/") (last))
ext-idx (str/last-index-of filename ".")]
(if (> ext-idx 0) (subs filename 0 ext-idx) filename)))
(defn- handle-upload-error [on-error stream]
(->> stream
(rx/catch
(fn [error]
(cond
(= (:code error) :media-type-not-allowed)
(rx/of (dm/error (tr "errors.media-type-not-allowed")))
(= (:code error) :media-type-mismatch)
(rx/of (dm/error (tr "errors.media-type-mismatch")))
(= (:code error) :unable-to-optimize)
(rx/of (dm/error (:hint error)))
(fn? on-error)
(do
(on-error error)
(rx/empty))
:else
(rx/throw error))))))
(defn- upload-uris [file-id local? name uris mtype on-image on-svg]
(letfn [(svg-url? [url]
(or (and mtype (= mtype "image/svg+xml"))
(str/ends-with? url ".svg")))
(prepare-uri [uri]
{:file-id file-id
:is-local local?
:name (or name (url-name uri))
:url uri})]
(rx/merge
(->> (rx/from uris)
(rx/filter (comp not svg-url?))
(rx/map prepare-uri)
(rx/mapcat #(rp/mutation! :create-file-media-object-from-url %))
(rx/do on-image))
(->> (rx/from uris)
(rx/filter svg-url?)
(rx/merge-map fetch-svg)
(rx/merge-map parse-svg)
(rx/with-latest vector uris)
(rx/map #(assoc (first %) :name (or name (url-name (second %)))))
(rx/do on-svg)))))
(defn- upload-data [file-id local? name data force-media on-image on-svg]
(let [svg-blob? (fn [blob]
(and (not force-media)
(= (.-type blob) "image/svg+xml")))
prepare-file
(fn [blob]
(let [name (or name (if (di/file? blob) (.-name blob) "blob"))]
{:file-id file-id
:name name
:is-local local?
:content blob}))
file-stream (->> (rx/from data)
(rx/map di/validate-file))]
(rx/merge
(->> file-stream
(rx/filter (comp not svg-blob?))
(rx/map prepare-file)
(rx/mapcat #(rp/mutation! :upload-file-media-object %))
(rx/do on-image))
(->> file-stream
(rx/filter svg-blob?)
(rx/merge-map #(.text %))
(rx/merge-map parse-svg)
(rx/with-latest vector file-stream)
(rx/map #(assoc (first %) :name (.-name (second %))))
(rx/do on-svg)))))
(defn upload-media-objects
[{:keys [file-id local? data name uris] :as params}]
[{:keys [file-id local? data name uris mtype svg-as-images] :as params}]
(us/assert ::upload-media-objects params)
(ptk/reify ::upload-media-objects
ptk/WatchEvent
(watch [_ state stream]
(let [{:keys [on-success on-error]
:or {on-success identity}} (meta params)
prepare-file
(fn [blob]
(let [name (or name (if (di/file? blob) (.-name blob) "blob"))]
{:name name
:file-id file-id
:content blob
:is-local local?}))
prepare-uri
(fn [uri]
{:file-id file-id
:is-local local?
:url uri
:name name})]
(let [{:keys [on-image on-svg on-error]
:or {on-image identity
on-svg identity}} (meta params)]
(rx/concat
(rx/of (dm/show {:content (tr "media.loading")
@ -384,31 +473,33 @@
:timeout nil
:tag :media-loading}))
(->> (if (seq uris)
(->> (rx/from uris)
(rx/map prepare-uri)
(rx/mapcat #(rp/mutation! :create-file-media-object-from-url %)))
(->> (rx/from data)
(rx/map di/validate-file)
(rx/map prepare-file)
(rx/mapcat #(rp/mutation! :upload-file-media-object %))))
(rx/do on-success)
(rx/catch (fn [error]
(cond
(= (:code error) :media-type-not-allowed)
(rx/of (dm/error (tr "errors.media-type-not-allowed")))
;; Media objects is a list of URL's pointing to the path
(upload-uris file-id local? name uris mtype on-image on-svg)
;; Media objects are blob of data to be upload
(upload-data file-id local? name data svg-as-images on-image on-svg))
;; Every stream has its own sideffect. We need to ignore the result
(rx/ignore)
(handle-upload-error on-error)
(rx/finalize (st/emitf (dm/hide-tag :media-loading)))))))))
(= (:code error) :media-type-mismatch)
(rx/of (dm/error (tr "errors.media-type-mismatch")))
(defn upload-media-asset [params]
(let [params (-> params
(assoc :svg-as-images true)
(assoc :local? false)
(with-meta {:on-image #(st/emit! (dwl/add-media %))}))]
(upload-media-objects params)))
(fn? on-error)
(do
(on-error error)
(rx/empty))
:else
(rx/throw error))))
(rx/finalize (fn []
(st/emit! (dm/hide-tag :media-loading))))))))))
(defn upload-media-workspace
[params position]
(let [{:keys [x y]} position
params (-> params
(assoc :local? true)
(with-meta
{:on-image
#(st/emit! (dwc/image-uploaded % x y))
:on-svg
#(st/emit! (dwc/svg-uploaded % x y))}))]
(upload-media-objects params)))
;; --- Upload File Media objects
@ -416,10 +507,10 @@
(s/def ::object-id ::us/uuid)
(s/def ::clone-media-objects-params
(s/keys :req-un [::file-id ::local? ::object-id]))
(s/keys :req-un [::file-id ::object-id]))
(defn clone-media-object
[{:keys [file-id local? object-id] :as params}]
[{:keys [file-id object-id] :as params}]
(us/assert ::clone-media-objects-params params)
(ptk/reify ::clone-media-objects
ptk/WatchEvent
@ -427,7 +518,7 @@
(let [{:keys [on-success on-error]
:or {on-success identity
on-error identity}} (meta params)
params {:is-local local?
params {:is-local true
:file-id file-id
:id object-id}]

View file

@ -27,6 +27,7 @@
[app.main.ui.shapes.rect :as rect]
[app.main.ui.shapes.text :as text]
[app.main.ui.shapes.group :as group]
[app.main.ui.shapes.svg-raw :as svg-raw]
[app.main.ui.shapes.shape :refer [shape-container]]))
(def ^:private default-color "#E8E9EA") ;; $color-canvas
@ -77,26 +78,45 @@
:is-child-selected? true
:childs childs}]))))
(defn svg-raw-wrapper-factory
[objects]
(let [shape-wrapper (shape-wrapper-factory objects)
svg-raw-shape (svg-raw/svg-raw-shape shape-wrapper)]
(mf/fnc svg-raw-wrapper
[{:keys [shape frame] :as props}]
(let [childs (mapv #(get objects %) (:shapes shape))]
[:& svg-raw-shape {:frame frame
:shape shape
:childs childs}]))))
(defn shape-wrapper-factory
[objects]
(mf/fnc shape-wrapper
[{:keys [frame shape] :as props}]
(let [group-wrapper (mf/use-memo (mf/deps objects) #(group-wrapper-factory objects))
svg-raw-wrapper (mf/use-memo (mf/deps objects) #(svg-raw-wrapper-factory objects))
frame-wrapper (mf/use-memo (mf/deps objects) #(frame-wrapper-factory objects))]
(when (and shape (not (:hidden shape)))
(let [shape (-> (gsh/transform-shape shape)
(gsh/translate-to-frame frame))
opts #js {:shape shape}]
[:> shape-container {:shape shape}
(case (:type shape)
:text [:> text/text-shape opts]
:rect [:> rect/rect-shape opts]
:path [:> path/path-shape opts]
:image [:> image/image-shape opts]
:circle [:> circle/circle-shape opts]
:frame [:> frame-wrapper {:shape shape}]
:group [:> group-wrapper {:shape shape :frame frame}]
nil)])))))
opts #js {:shape shape}
svg-element? (and (= :svg-raw (:type shape))
(not= :svg (get-in shape [:content :tag])))]
(if-not svg-element?
[:> shape-container {:shape shape}
(case (:type shape)
:text [:> text/text-shape opts]
:rect [:> rect/rect-shape opts]
:path [:> path/path-shape opts]
:image [:> image/image-shape opts]
:circle [:> circle/circle-shape opts]
:frame [:> frame-wrapper {:shape shape}]
:group [:> group-wrapper {:shape shape :frame frame}]
:svg-raw [:> svg-raw-wrapper {:shape shape :frame frame}]
nil)]
;; Don't wrap svg elements inside a <g> otherwise some can break
[:> svg-raw-wrapper {:shape shape :frame frame}]))))))
(defn get-viewbox [{:keys [x y width height] :or {x 0 y 0 width 100 height 100}}]
(str/fmt "%s %s %s %s" x y width height))

View file

@ -13,6 +13,7 @@
(def embed-ctx (mf/create-context false))
(def render-ctx (mf/create-context nil))
(def def-ctx (mf/create-context false))
(def current-route (mf/create-context nil))
(def current-team-id (mf/create-context nil))

View file

@ -25,6 +25,7 @@
[app.main.ui.shapes.circle :as circle]
[app.main.ui.shapes.frame :as frame]
[app.main.ui.shapes.group :as group]
[app.main.ui.shapes.svg-raw :as svg-raw]
[app.main.ui.shapes.image :as image]
[app.main.ui.shapes.path :as path]
[app.main.ui.shapes.rect :as rect]
@ -61,16 +62,24 @@
[props]
(let [shape (unchecked-get props "shape")
childs (unchecked-get props "childs")
frame (unchecked-get props "frame")]
frame (unchecked-get props "frame")
svg-element? (and (= :svg-raw (:type shape))
(not= :svg (get-in shape [:content :tag])))]
[:> shape-container {:shape shape
:on-mouse-enter (handle-hover-shape shape true)
:on-mouse-leave (handle-hover-shape shape false)
:on-click (select-shape shape)}
[:& component {:shape shape
:frame frame
:childs childs
:is-child-selected? true}]])))
(if-not svg-element?
[:> shape-container {:shape shape
:on-mouse-enter (handle-hover-shape shape true)
:on-mouse-leave (handle-hover-shape shape false)
:on-click (select-shape shape)}
[:& component {:shape shape
:frame frame
:childs childs
:is-child-selected? true}]]
;; Don't wrap svg elements inside a <g> otherwise some can break
[:& component {:shape shape
:frame frame
:childs childs}]))))
(defn frame-container-factory
[objects]
@ -105,6 +114,21 @@
(obj/merge! #js {:childs childs}))]
[:> group-wrapper props]))))
(defn svg-raw-container-factory
[objects]
(let [shape-container (shape-container-factory objects)
svg-raw-shape (svg-raw/svg-raw-shape shape-container)
svg-raw-wrapper (shape-wrapper-factory svg-raw-shape)]
(mf/fnc group-container
{::mf/wrap-props false}
[props]
(let [shape (unchecked-get props "shape")
childs (mapv #(get objects %) (:shapes shape))
props (-> (obj/new)
(obj/merge! props)
(obj/merge! #js {:childs childs}))]
[:> svg-raw-wrapper props]))))
(defn shape-container-factory
[objects show-interactions?]
(let [path-wrapper (shape-wrapper-factory path/path-shape)
@ -119,19 +143,23 @@
frame (unchecked-get props "frame")
group-container (mf/use-memo
(mf/deps objects)
#(group-container-factory objects))]
#(group-container-factory objects))
svg-raw-container (mf/use-memo
(mf/deps objects)
#(svg-raw-container-factory objects))]
(when (and shape (not (:hidden shape)))
(let [shape (-> (geom/transform-shape shape)
(geom/translate-to-frame frame))
opts #js {:shape shape
:frame frame}]
(case (:type shape)
:text [:> text-wrapper opts]
:rect [:> rect-wrapper opts]
:path [:> path-wrapper opts]
:image [:> image-wrapper opts]
:circle [:> circle-wrapper opts]
:group [:> group-container opts])))))))
:text [:> text-wrapper opts]
:rect [:> rect-wrapper opts]
:path [:> path-wrapper opts]
:image [:> image-wrapper opts]
:circle [:> circle-wrapper opts]
:group [:> group-container opts]
:svg-raw [:> svg-raw-container opts])))))))
(defn adjust-frame-position [frame-id objects]
(let [frame (get objects frame-id)

View file

@ -23,15 +23,22 @@
nil))
(defn add-border-radius [attrs shape]
(obj/merge! attrs #js {:rx (:rx shape)
:ry (:ry shape)}))
(if (or (:rx shape) (:ry shape))
(obj/merge! attrs #js {:rx (:rx shape)
:ry (:ry shape)})
attrs))
(defn add-fill [attrs shape render-id]
(let [fill-color-gradient-id (str "fill-color-gradient_" render-id)]
(if (:fill-color-gradient shape)
(cond
(:fill-color-gradient shape)
(obj/merge! attrs #js {:fill (str/format "url(#%s)" fill-color-gradient-id)})
(or (:fill-color shape) (:fill-opacity shape))
(obj/merge! attrs #js {:fill (or (:fill-color shape) "transparent")
:fillOpacity (:fill-opacity shape nil)}))))
:fillOpacity (:fill-opacity shape nil)})
:else attrs)))
(defn add-stroke [attrs shape render-id]
(let [stroke-style (:stroke-style shape :none)

View file

@ -9,12 +9,12 @@
(ns app.main.ui.shapes.filters
(:require
[rumext.alpha :as mf]
[cuerdas.core :as str]
[app.util.color :as color]
[app.common.data :as d]
[app.common.math :as mth]
[app.common.uuid :as uuid]))
[app.common.uuid :as uuid]
[app.util.color :as color]
[cuerdas.core :as str]
[rumext.alpha :as mf]))
(defn get-filter-id []
(str "filter_" (uuid/next)))
@ -109,37 +109,6 @@
:in2 filter-in
:result filter-id}])
(defn filter-bounds [shape filter-entry]
(let [{:keys [x y width height]} (:selrect shape)
{:keys [offset-x offset-y blur spread] :or {offset-x 0 offset-y 0 blur 0 spread 0}} (:params filter-entry)
filter-x (min x (+ x offset-x (- spread) (- blur) -5))
filter-y (min y (+ y offset-y (- spread) (- blur) -5))
filter-width (+ width (mth/abs offset-x) (* spread 2) (* blur 2) 10)
filter-height (+ height (mth/abs offset-x) (* spread 2) (* blur 2) 10)]
{:x1 filter-x
:y1 filter-y
:x2 (+ filter-x filter-width)
:y2 (+ filter-y filter-height)}))
(defn get-filters-bounds
[shape filters blur-value]
(let [filter-bounds (->> filters
(filter #(= :drop-shadow (:type %)))
(map (partial filter-bounds shape) ))
;; We add the selrect so the minimum size will be the selrect
filter-bounds (conj filter-bounds (:selrect shape))
x1 (apply min (map :x1 filter-bounds))
y1 (apply min (map :y1 filter-bounds))
x2 (apply max (map :x2 filter-bounds))
y2 (apply max (map :y2 filter-bounds))
x1 (- x1 (* blur-value 2))
x2 (+ x2 (* blur-value 2))
y1 (- y1 (* blur-value 2))
y2 (+ y2 (* blur-value 2))]
[x1 y1 (- x2 x1) (- y2 y1)]))
(defn blur-filters [type value]
(->> [value]
(remove :hidden)
@ -185,18 +154,11 @@
(->> shape :blur (blur-filters :layer-blur)))
;; Adds the previous filter as `filter-in` parameter
filters (map #(assoc %1 :filter-in %2) filters (cons nil (map :id filters)))
[filter-x filter-y filter-width filter-height] (get-filters-bounds shape filters (or (-> shape :blur :value) 0))]
filters (map #(assoc %1 :filter-in %2) filters (cons nil (map :id filters)))]
[:*
(when (> (count filters) 2)
[:filter {:id filter-id
:x filter-x
:y filter-y
:width filter-width
:height filter-height
:filterUnits "userSpaceOnUse"
:color-interpolation-filters "sRGB"}
(for [entry filters]

View file

@ -0,0 +1,123 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.ui.shapes.svg-raw
(:require
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.main.ui.shapes.attrs :as usa]
[app.util.data :as d]
[app.util.object :as obj]
[cuerdas.core :as str]
[rumext.alpha :as mf]))
(defn clean-attrs
"Transforms attributes to their react equivalent"
[attrs]
(letfn [(transform-key [key]
(-> (name key)
(str/replace ":" "-")
(str/camel)
(keyword)))
(format-styles [style-str]
(->> (str/split style-str ";")
(map str/trim)
(map #(str/split % ":"))
(group-by first)
(map (fn [[key val]]
(vector
(transform-key key)
(second (first val)))))
(into {})))
(map-fn [[key val]]
(cond
(= key :style) [key (format-styles val)]
:else (vector (transform-key key) val)))]
(->> attrs
(map map-fn)
(into {}))))
(defn vbox->rect
"Converts the viewBox into a rectangle"
[vbox]
(when vbox
(let [[x y width height] (map d/parse-float (str/split vbox " "))]
{:x x :y y :width width :height height})))
(defn vbox-center [shape]
(let [vbox-rect (-> (get-in shape [:content :attrs :viewBox] "0 0 100 100")
(vbox->rect))]
(gsh/center-rect vbox-rect)))
(defn vbox-bounds [shape]
(let [vbox-rect (-> (get-in shape [:content :attrs :viewBox] "0 0 100 100")
(vbox->rect))
vbox-center (gsh/center-rect vbox-rect)
transform (gsh/transform-matrix shape nil vbox-center)]
(-> (gsh/rect->points vbox-rect)
(gsh/transform-points vbox-center transform)
(gsh/points->rect))) )
(defn transform-viewbox [shape]
(let [center (vbox-center shape)
bounds (vbox-bounds shape)
{:keys [x y width height]} (gsh/center->rect center (:width bounds) (:height bounds))]
(str x " " y " " width " " height)))
(defn svg-raw-shape [shape-wrapper]
(mf/fnc svg-raw-shape
{::mf/wrap-props false}
[props]
(let [frame (unchecked-get props "frame")
shape (unchecked-get props "shape")
childs (unchecked-get props "childs")
{:keys [tag attrs] :as content} (:content shape)
attrs (obj/merge! (clj->js (clean-attrs attrs))
(usa/extract-style-attrs shape))]
(cond
;; Root SVG TAG
(and (map? content) (= tag :svg))
(let [;; {:keys [x y width height]} (-> (:points shape) gsh/points->selrect)
{:keys [x y width height]} shape
attrs (-> attrs
(obj/set! "x" x)
(obj/set! "y" y)
(obj/set! "width" width)
(obj/set! "height" height)
(obj/set! "preserveAspectRatio" "none")
#_(obj/set! "viewBox" (transform-viewbox shape)))]
[:g.svg-raw {:transform (gsh/transform-matrix shape)}
[:> "svg" attrs
(for [item childs]
[:& shape-wrapper {:frame frame
:shape item
:key (:id item)}])]])
;; Other tags different than root
(map? content)
[:> (name tag) attrs
(for [item childs]
[:& shape-wrapper {:frame frame
:shape item
:key (:id item)}])]
;; String content
(string? content) content
:else nil))))

View file

@ -20,6 +20,7 @@
[app.main.ui.shapes.circle :as circle]
[app.main.ui.shapes.frame :as frame]
[app.main.ui.shapes.group :as group]
[app.main.ui.shapes.svg-raw :as svg-raw]
[app.main.ui.shapes.image :as image]
[app.main.ui.shapes.path :as path]
[app.main.ui.shapes.rect :as rect]
@ -54,24 +55,33 @@
on-mouse-down (mf/use-callback
(mf/deps shape)
#(on-mouse-down % shape))]
#(on-mouse-down % shape))
[:> shape-container {:shape shape
:on-mouse-down on-mouse-down
:cursor (when (seq (:interactions shape)) "pointer")}
[:& component {:shape shape
:frame frame
:childs childs
:is-child-selected? true}]
(when (and (:interactions shape) show-interactions?)
[:rect {:x (- x 1)
:y (- y 1)
:width (+ width 2)
:height (+ height 2)
:fill "#31EFB8"
:stroke "#31EFB8"
:stroke-width 1
:fill-opacity 0.2}])])))
svg-element? (and (= :svg-raw (:type shape))
(not= :svg (get-in shape [:content :tag])))]
(if-not svg-element?
[:> shape-container {:shape shape
:on-mouse-down on-mouse-down
:cursor (when (seq (:interactions shape)) "pointer")}
[:& component {:shape shape
:frame frame
:childs childs
:is-child-selected? true}]
(when (and (:interactions shape) show-interactions?)
[:rect {:x (- x 1)
:y (- y 1)
:width (+ width 2)
:height (+ height 2)
:fill "#31EFB8"
:stroke "#31EFB8"
:stroke-width 1
:fill-opacity 0.2}])]
;; Don't wrap svg elements inside a <g> otherwise some can break
[:& component {:shape shape
:frame frame
:childs childs}]))))
(defn frame-wrapper
[shape-container show-interactions?]
@ -81,6 +91,10 @@
[shape-container show-interactions?]
(generic-wrapper-factory (group/group-shape shape-container) show-interactions?))
(defn svg-raw-wrapper
[shape-container show-interactions?]
(generic-wrapper-factory (svg-raw/svg-raw-shape shape-container) show-interactions?))
(defn rect-wrapper
[show-interactions?]
(generic-wrapper-factory rect/rect-shape show-interactions?))
@ -133,6 +147,20 @@
:show-interactions? show-interactions?})]
[:> group-wrapper props]))))
(defn svg-raw-container-factory
[objects show-interactions?]
(let [shape-container (shape-container-factory objects show-interactions?)
svg-raw-wrapper (svg-raw-wrapper shape-container show-interactions?)]
(mf/fnc svg-raw-container
{::mf/wrap-props false}
[props]
(let [shape (unchecked-get props "shape")
childs (mapv #(get objects %) (:shapes shape))
props (obj/merge! #js {} props
#js {:childs childs
:show-interactions? show-interactions?})]
[:> svg-raw-wrapper props]))))
(defn shape-container-factory
[objects show-interactions?]
(let [path-wrapper (path-wrapper show-interactions?)
@ -144,8 +172,11 @@
{::mf/wrap-props false}
[props]
(let [group-container (mf/use-memo
(mf/deps objects)
#(group-container-factory objects show-interactions?))
(mf/deps objects)
#(group-container-factory objects show-interactions?))
svg-raw-container (mf/use-memo
(mf/deps objects)
#(svg-raw-container-factory objects show-interactions?))
shape (unchecked-get props "shape")
frame (unchecked-get props "frame")]
(when (and shape (not (:hidden shape)))
@ -153,15 +184,14 @@
(geom/translate-to-frame frame))
opts #js {:shape shape}]
(case (:type shape)
:frame [:g.empty]
:text [:> text-wrapper opts]
:rect [:> rect-wrapper opts]
:path [:> path-wrapper opts]
:image [:> image-wrapper opts]
:circle [:> circle-wrapper opts]
:group [:> group-container
{:shape shape
:frame frame}])))))))
:frame [:g.empty]
:text [:> text-wrapper opts]
:rect [:> rect-wrapper opts]
:path [:> path-wrapper opts]
:image [:> image-wrapper opts]
:circle [:> circle-wrapper opts]
:group [:> group-container {:shape shape :frame frame}]
:svg-raw [:> svg-raw-container {:shape shape :frame frame}])))))))
(mf/defc frame-svg
{::mf/wrap [mf/memo]}

View file

@ -9,16 +9,17 @@
(ns app.main.ui.workspace.left-toolbar
(:require
[rumext.alpha :as mf]
[app.common.geom.point :as gpt]
[app.common.media :as cm]
[app.main.refs :as refs]
[app.main.data.workspace :as dw]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.file-uploader :refer [file-uploader]]
[app.util.object :as obj]
[app.main.ui.icons :as i]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[app.main.ui.icons :as i]))
[app.util.object :as obj]
[rumext.alpha :as mf]))
(mf/defc image-upload
{::mf/wrap [mf/memo]}
@ -29,28 +30,13 @@
on-click
(mf/use-callback #(dom/click (mf/ref-val ref)))
on-uploaded
(mf/use-callback
(fn [image]
(->> {:name (:name image)
:width (:width image)
:height (:height image)
:metadata {:width (:width image)
:height (:height image)
:mtype (:mtype image)
:id (:id image)}}
(dw/create-and-add-shape :image 0 0)
(st/emit!))))
on-files-selected
(mf/use-callback
(mf/deps file)
(fn [blobs]
(st/emit! (dw/upload-media-objects
(with-meta {:file-id (:id file)
:local? true
:data (seq blobs)}
{:on-success on-uploaded})))))]
(let [params {:file-id (:id file)
:data (seq blobs)}]
(st/emit! (dw/upload-media-workspace params (gpt/point 0 0))))))]
[:li.tooltip.tooltip-right
{:alt (tr "workspace.toolbar.image")

View file

@ -28,6 +28,7 @@
[app.main.ui.workspace.shapes.common :as common]
[app.main.ui.workspace.shapes.frame :as frame]
[app.main.ui.workspace.shapes.group :as group]
[app.main.ui.workspace.shapes.svg-raw :as svg-raw]
[app.main.ui.workspace.shapes.path :as path]
[app.main.ui.workspace.shapes.text :as text]
[app.util.object :as obj]
@ -37,6 +38,7 @@
[rumext.alpha :as mf]))
(declare group-wrapper)
(declare svg-raw-wrapper)
(declare frame-wrapper)
(def circle-wrapper (common/generic-wrapper-factory circle/circle-shape))
@ -80,27 +82,37 @@
alt? (hooks/use-rxsub ms/keyboard-alt)
moving-iref (mf/use-memo (mf/deps (:id shape)) (make-is-moving-ref (:id shape)))
moving? (mf/deref moving-iref)]
moving? (mf/deref moving-iref)
svg-element? (and (= (:type shape) :svg-raw)
(not= :svg (get-in shape [:content :tag])))]
(when (and shape
(or ghost? (not moving?))
(not (:hidden shape)))
[:g.shape-wrapper {:style {:cursor (if alt? cur/duplicate nil)}}
(case (:type shape)
:path [:> path/path-wrapper opts]
:text [:> text/text-wrapper opts]
:group [:> group-wrapper opts]
:rect [:> rect-wrapper opts]
:image [:> image-wrapper opts]
:circle [:> circle-wrapper opts]
[:*
(if-not svg-element?
[:g.shape-wrapper {:style {:cursor (if alt? cur/duplicate nil)}}
(case (:type shape)
:path [:> path/path-wrapper opts]
:text [:> text/text-wrapper opts]
:group [:> group-wrapper opts]
:rect [:> rect-wrapper opts]
:image [:> image-wrapper opts]
:circle [:> circle-wrapper opts]
:svg-raw [:> svg-raw-wrapper opts]
;; Only used when drawing a new frame.
:frame [:> frame-wrapper {:shape shape}]
nil)
;; Only used when drawing a new frame.
:frame [:> frame-wrapper {:shape shape}]
nil)]
;; Don't wrap svg elements inside a <g> otherwise some can break
[:> svg-raw-wrapper opts])
(when (debug? :bounding-boxes)
[:& bounding-box {:shape shape :frame frame}])])))
(def group-wrapper (group/group-wrapper-factory shape-wrapper))
(def svg-raw-wrapper (svg-raw/svg-raw-wrapper-factory shape-wrapper))
(def frame-wrapper (frame/frame-wrapper-factory shape-wrapper))

View file

@ -0,0 +1,93 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.ui.workspace.shapes.svg-raw
(:require
[app.main.refs :as refs]
[app.main.ui.shapes.svg-raw :as svg-raw]
[app.main.ui.shapes.shape :refer [shape-container]]
[app.main.ui.workspace.effects :as we]
[rumext.alpha :as mf]
[app.common.geom.shapes :as gsh]
[app.main.ui.context :as muc]))
;; This is a list of svg tags that can be grouped in shape-container
;; this allows them to have gradients, shadows and masks
(def svg-elements #{:svg :circle :ellipse :image :line :path :polygon :polyline :rect :symbol :text :textPath})
(defn- svg-raw-wrapper-factory-equals?
[np op]
(let [n-shape (unchecked-get np "shape")
o-shape (unchecked-get op "shape")
n-frame (unchecked-get np "frame")
o-frame (unchecked-get op "frame")]
(and (= n-frame o-frame)
(= n-shape o-shape))))
(defn svg-raw-wrapper-factory
[shape-wrapper]
(let [svg-raw-shape (svg-raw/svg-raw-shape shape-wrapper)]
(mf/fnc svg-raw-wrapper
{::mf/wrap [#(mf/memo' % svg-raw-wrapper-factory-equals?)]
::mf/wrap-props false}
[props]
(let [shape (unchecked-get props "shape")
frame (unchecked-get props "frame")
{:keys [id x y width height]} shape
childs-ref (mf/use-memo (mf/deps shape) #(refs/objects-by-id (:shapes shape)))
childs (mf/deref childs-ref)
{:keys [id x y width height]} shape
transform (gsh/transform-matrix shape)
tag (get-in shape [:content :tag])
handle-mouse-down (we/use-mouse-down shape)
handle-context-menu (we/use-context-menu shape)
handle-pointer-enter (we/use-pointer-enter shape)
handle-pointer-leave (we/use-pointer-leave shape)
def-ctx? (mf/use-ctx muc/def-ctx)]
(cond
(and (contains? svg-elements tag) (not def-ctx?))
[:> shape-container { :shape shape }
[:& svg-raw-shape
{:frame frame
:shape shape
:childs childs}]
(when (= tag :svg)
[:rect.group-actions
{:x x
:y y
:transform transform
:width width
:height height
:fill "transparent"
:on-mouse-down handle-mouse-down
:on-context-menu handle-context-menu
:on-pointer-over handle-pointer-enter
:on-pointer-out handle-pointer-leave}])]
;; We cannot wrap inside groups the shapes that go inside the defs tag
;; we use the context so we know when we should not render the container
(= tag :defs)
[:& (mf/provider muc/def-ctx) {:value true}
[:& svg-raw-shape {:frame frame
:shape shape
:childs childs}]]
:else
[:& svg-raw-shape {:frame frame
:shape shape
:childs childs}])))))

View file

@ -164,11 +164,9 @@
(mf/use-callback
(mf/deps file-id)
(fn [blobs]
(let [params (with-meta {:file-id file-id
:local? false
:data (seq blobs)}
{:on-success on-media-uploaded})]
(st/emit! (dw/upload-media-objects params)))))
(let [params {:file-id file-id
:data (seq blobs)}]
(st/emit! (dw/upload-media-asset params)))))
on-delete
(mf/use-callback
@ -212,9 +210,10 @@
on-drag-start
(mf/use-callback
(fn [{:keys [name id]} event]
(fn [{:keys [name id mtype]} event]
(dnd/set-data! event "text/asset-id" (str id))
(dnd/set-data! event "text/asset-name" name)
(dnd/set-data! event "text/asset-type" mtype)
(dnd/set-allowed-effect! event "move")))]
[:div.asset-group

View file

@ -45,6 +45,7 @@
(if (:masked-group? shape)
i/mask
i/folder))
:svg-raw i/file-svg
nil))
;; --- Layer Name

View file

@ -29,7 +29,7 @@
[app.main.ui.workspace.sidebar.options.path :as path]
[app.main.ui.workspace.sidebar.options.rect :as rect]
[app.main.ui.workspace.sidebar.options.text :as text]
[app.main.ui.workspace.sidebar.options.text :as text]
[app.main.ui.workspace.sidebar.options.svg-raw :as svg-raw]
[app.util.i18n :as i18n :refer [tr t]]
[app.util.object :as obj]
[beicon.core :as rx]
@ -42,14 +42,15 @@
[{:keys [shape shapes-with-children page-id file-id]}]
[:*
(case (:type shape)
:frame [:& frame/options {:shape shape}]
:group [:& group/options {:shape shape :shape-with-children shapes-with-children}]
:text [:& text/options {:shape shape}]
:rect [:& rect/options {:shape shape}]
:icon [:& icon/options {:shape shape}]
:circle [:& circle/options {:shape shape}]
:path [:& path/options {:shape shape}]
:image [:& image/options {:shape shape}]
:frame [:& frame/options {:shape shape}]
:group [:& group/options {:shape shape :shape-with-children shapes-with-children}]
:text [:& text/options {:shape shape}]
:rect [:& rect/options {:shape shape}]
:icon [:& icon/options {:shape shape}]
:circle [:& circle/options {:shape shape}]
:path [:& path/options {:shape shape}]
:image [:& image/options {:shape shape}]
:svg-raw [:& svg-raw/options {:shape shape}]
nil)
[:& exports-menu
{:shape shape
@ -105,3 +106,4 @@
:page-id page-id
:section section}]))

View file

@ -72,6 +72,14 @@
:text :ignore}
:circle
{:measure :shape
:fill :shape
:shadow :shape
:blur :shape
:stroke :shape
:text :ignore}
:svg-raw
{:measure :shape
:fill :shape
:shadow :shape

View file

@ -0,0 +1,114 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.ui.workspace.sidebar.options.svg-raw
(:require
[rumext.alpha :as mf]
[cuerdas.core :as str]
[app.util.data :as d]
[app.main.ui.workspace.sidebar.options.measures :refer [measure-attrs measures-menu]]
[app.main.ui.workspace.sidebar.options.fill :refer [fill-attrs fill-menu]]
[app.main.ui.workspace.sidebar.options.stroke :refer [stroke-attrs stroke-menu]]
[app.main.ui.workspace.sidebar.options.shadow :refer [shadow-menu]]
[app.main.ui.workspace.sidebar.options.blur :refer [blur-menu]]))
;; This is a list of svg tags that can be grouped in shape-container
;; this allows them to have gradients, shadows and masks
(def svg-elements #{:svg :circle :ellipse :image :line :path :polygon :polyline :rect :symbol :text :textPath})
(defn hex->number [hex] 1)
(defn shorthex->longhex [hex]
(let [[_ r g b] hex]
(str "#" r r g g b b)))
(defn parse-color [color]
(cond
(or (not color) (= color "none")) nil
(and (str/starts-with? color "#") (= (count color) 4))
{:color (shorthex->longhex color)
:opacity 1}
(and (str/starts-with? color "#") (= (count color) 9))
{:color (subs color 1 6)
:opacity (-> (subs color 7 2) (hex->number))}
;; TODO CHECK IF IT'S A GRADIENT
:else nil))
(defn get-fill-values [shape]
(let [fill-values (or (select-keys shape fill-attrs))
color (-> (get-in shape [:content :attrs :fill])
(parse-color))
fill-values (if (and (empty? fill-values) color)
{:fill-color (:color color)
:fill-opacity (:opacity color)}
fill-values)]
fill-values))
(defn get-stroke-values [shape]
(let [stroke-values (or (select-keys shape stroke-attrs))
color (-> (get-in shape [:content :attrs :stroke])
(parse-color))
stroke-color (:color color "#000000")
stroke-opacity (:opacity color 1)
stroke-style (-> (get-in shape [:content :attrs :stroke-style] (if color "solid" "none"))
keyword)
stroke-alignment :center
stroke-width (-> (get-in shape [:content :attrs :stroke-width] "1")
(d/parse-int))
stroke-values (if (empty? stroke-values)
{:stroke-color stroke-color
:stroke-opacity stroke-opacity
:stroke-style stroke-style
:stroke-alignment stroke-alignment
:stroke-width stroke-width}
stroke-values)]
stroke-values))
(mf/defc options
{::mf/wrap [mf/memo]}
[{:keys [shape] :as props}]
(let [ids [(:id shape)]
type (:type shape)
{:keys [tag attrs] :as content} (:content shape)
measure-values (select-keys shape measure-attrs)
fill-values (get-fill-values shape)
stroke-values (get-stroke-values shape)]
(when (contains? svg-elements tag)
[:*
(cond
(= tag :svg)
[:*
[:& measures-menu {:ids ids
:type type
:values measure-values}]]
:else
[:*
[:& fill-menu {:ids ids
:type type
:values fill-values}]
[:& stroke-menu {:ids ids
:type type
:values stroke-values}]])
[:& shadow-menu {:ids ids
:values (select-keys shape [:shadow])}]
[:& blur-menu {:ids ids
:values (select-keys shape [:blur])}]])))

View file

@ -14,6 +14,7 @@
[app.common.geom.shapes :as gsh]
[app.common.math :as mth]
[app.common.uuid :as uuid]
[app.config :as cfg]
[app.main.constants :as c]
[app.main.data.colors :as dwc]
[app.main.data.fetch :as mdf]
@ -39,22 +40,22 @@
[app.main.ui.workspace.shapes :refer [shape-wrapper frame-wrapper]]
[app.main.ui.workspace.shapes.interactions :refer [interactions]]
[app.main.ui.workspace.shapes.outline :refer [outline]]
[app.main.ui.workspace.shapes.path.actions :refer [path-actions]]
[app.main.ui.workspace.snap-distances :refer [snap-distances]]
[app.main.ui.workspace.snap-points :refer [snap-points]]
[app.util.dom :as dom]
[app.util.dom.dnd :as dnd]
[app.util.http :as http]
[app.util.object :as obj]
[app.util.perf :as perf]
[app.util.timers :as timers]
[app.util.http :as http]
[beicon.core :as rx]
[clojure.set :as set]
[cuerdas.core :as str]
[goog.events :as events]
[potok.core :as ptk]
[promesa.core :as p]
[rumext.alpha :as mf]
[app.main.ui.workspace.shapes.path.actions :refer [path-actions]])
[rumext.alpha :as mf])
(:import goog.events.EventType))
;; --- Coordinates Widget
@ -452,28 +453,20 @@
(dnd/has-type? e "text/asset-id"))
(dom/prevent-default e))))
on-uploaded
on-image-uploaded
(mf/use-callback
(fn [image {:keys [x y]}]
(prn "on-uploaded" image x y)
(let [shape {:name (:name image)
:width (:width image)
:height (:height image)
:x (- x (/ (:width image) 2))
:y (- y (/ (:height image) 2))
:metadata {:width (:width image)
:height (:height image)
:name (:name image)
:id (:id image)
:mtype (:mtype image)}}]
(st/emit! (dw/create-and-add-shape :image x y shape)))))
(st/emit! (dw/image-uploaded image x y))))
on-drop
(mf/use-callback
(fn [event]
(dom/prevent-default event)
(let [point (gpt/point (.-clientX event) (.-clientY event))
viewport-coord (translate-point-to-viewport point)]
viewport-coord (translate-point-to-viewport point)
asset-id (-> (dnd/get-data event "text/asset-id") uuid/uuid)
asset-name (dnd/get-data event "text/asset-name")
asset-type (dnd/get-data event "text/asset-type")]
(cond
(dnd/has-type? event "app/shape")
(let [shape (dnd/get-data event "app/shape")
@ -493,40 +486,43 @@
(:id component)
(gpt/point final-x final-y))))
;; Will trigger when the user drags an image from a browser to the viewport
(dnd/has-type? event "text/uri-list")
(let [data (dnd/get-data event "text/uri-list")
name (dnd/get-data event "text/asset-name")
lines (str/lines data)
urls (filter #(and (not (str/blank? %))
(not (str/starts-with? % "#")))
lines)]
(st/emit!
(dw/upload-media-objects
(with-meta {:file-id (:id file)
:local? true
:uris urls
:name name}
{:on-success #(on-uploaded % viewport-coord)}))))
(dnd/has-type? event "text/asset-id")
(let [id (-> (dnd/get-data event "text/asset-id") uuid/uuid)
name (dnd/get-data event "text/asset-name")
lines)
params {:file-id (:id file)
:local? true
:object-id id
:name name}]
:uris urls}]
(st/emit! (dw/upload-media-workspace params viewport-coord)))
;; Will trigger when the user drags an SVG asset from the assets panel
(and (dnd/has-type? event "text/asset-id") (= asset-type "image/svg+xml"))
(let [path (cfg/resolve-file-media {:id asset-id})
params {:file-id (:id file)
:uris [path]
:name asset-name
:mtype asset-type}]
(st/emit! (dw/upload-media-workspace params viewport-coord)))
;; Will trigger when the user drags an image from the assets SVG
(dnd/has-type? event "text/asset-id")
(let [params {:file-id (:id file)
:object-id asset-id
:name asset-name}]
(st/emit! (dw/clone-media-object
(with-meta params
{:on-success #(on-uploaded % viewport-coord)}))))
{:on-success #(on-image-uploaded % viewport-coord)}))))
;; Will trigger when the user drags a file from their file explorer into the viewport
;; Or the user pastes an image
;; Or the user uploads an image using the image tool
:else
(let [files (dnd/get-files event)
params {:file-id (:id file)
:local? true
:data (seq files)}]
(st/emit! (dw/upload-media-objects
(with-meta params
{:on-success #(on-uploaded % viewport-coord)}))))))))
(st/emit! (dw/upload-media-workspace params viewport-coord)))))))
on-paste
(mf/use-callback
@ -591,7 +587,9 @@
:file-id (:id file)}])
[:svg.viewport
{:preserveAspectRatio "xMidYMid meet"
{:xmlns "http://www.w3.org/2000/svg"
:xmlnsXlink "http://www.w3.org/1999/xlink"
:preserveAspectRatio "xMidYMid meet"
:key page-id
:width (:width vport 0)
:height (:height vport 0)