diff --git a/backend/src/app/media.clj b/backend/src/app/media.clj index 969138b24..9affa541c 100644 --- a/backend/src/app/media.clj +++ b/backend/src/app/media.clj @@ -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'}))) diff --git a/backend/src/app/rpc/mutations/media.clj b/backend/src/app/rpc/mutations/media.clj index f48befc59..66c332aa4 100644 --- a/backend/src/app/rpc/mutations/media.clj +++ b/backend/src/app/rpc/mutations/media.clj @@ -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) diff --git a/backend/src/app/svgparse.clj b/backend/src/app/svgparse.clj index 05d05dc9f..6bf81f2c7 100644 --- a/backend/src/app/svgparse.clj +++ b/backend/src/app/svgparse.clj @@ -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))) diff --git a/common/app/common/geom/shapes.cljc b/common/app/common/geom/shapes.cljc index f99994ee7..ccc26db3d 100644 --- a/common/app/common/geom/shapes.cljc +++ b/common/app/common/geom/shapes.cljc @@ -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) diff --git a/common/app/common/geom/shapes/rect.cljc b/common/app/common/geom/shapes/rect.cljc index b7a56e6fa..80e06ef11 100644 --- a/common/app/common/geom/shapes/rect.cljc +++ b/common/app/common/geom/shapes/rect.cljc @@ -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}) diff --git a/common/app/common/geom/shapes/transforms.cljc b/common/app/common/geom/shapes/transforms.cljc index 698524501..4e0743797 100644 --- a/common/app/common/geom/shapes/transforms.cljc +++ b/common/app/common/geom/shapes/transforms.cljc @@ -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] diff --git a/common/app/common/pages/init.cljc b/common/app/common/pages/init.cljc index 0eab6ae86..38267aaf5 100644 --- a/common/app/common/pages/init.cljc +++ b/common/app/common/pages/init.cljc @@ -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] diff --git a/docker/devenv/Dockerfile b/docker/devenv/Dockerfile index 036abeb9d..75f7b7ab1 100644 --- a/docker/devenv/Dockerfile +++ b/docker/devenv/Dockerfile @@ -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; \ diff --git a/frontend/resources/locales.json b/frontend/resources/locales.json index 1deab1ad9..90883d4eb 100644 --- a/frontend/resources/locales.json +++ b/frontend/resources/locales.json @@ -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" : { diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 198c814a4..39ad120c7 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -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 diff --git a/frontend/src/app/main/data/workspace/common.cljs b/frontend/src/app/main/data/workspace/common.cljs index 31597600b..c9cbee1a5 100644 --- a/frontend/src/app/main/data/workspace/common.cljs +++ b/frontend/src/app/main/data/workspace/common.cljs @@ -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))))))) diff --git a/frontend/src/app/main/data/workspace/persistence.cljs b/frontend/src/app/main/data/workspace/persistence.cljs index 8bc50dce4..df53e05b7 100644 --- a/frontend/src/app/main/data/workspace/persistence.cljs +++ b/frontend/src/app/main/data/workspace/persistence.cljs @@ -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}] diff --git a/frontend/src/app/main/exports.cljs b/frontend/src/app/main/exports.cljs index 8166ab334..5b3e5dcdb 100644 --- a/frontend/src/app/main/exports.cljs +++ b/frontend/src/app/main/exports.cljs @@ -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 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)) diff --git a/frontend/src/app/main/ui/context.cljs b/frontend/src/app/main/ui/context.cljs index 7e063282b..fcc077bc7 100644 --- a/frontend/src/app/main/ui/context.cljs +++ b/frontend/src/app/main/ui/context.cljs @@ -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)) diff --git a/frontend/src/app/main/ui/handoff/render.cljs b/frontend/src/app/main/ui/handoff/render.cljs index 7af944e29..a039dc336 100644 --- a/frontend/src/app/main/ui/handoff/render.cljs +++ b/frontend/src/app/main/ui/handoff/render.cljs @@ -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 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) diff --git a/frontend/src/app/main/ui/shapes/attrs.cljs b/frontend/src/app/main/ui/shapes/attrs.cljs index 27083b0ab..e536ea6e4 100644 --- a/frontend/src/app/main/ui/shapes/attrs.cljs +++ b/frontend/src/app/main/ui/shapes/attrs.cljs @@ -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) diff --git a/frontend/src/app/main/ui/shapes/filters.cljs b/frontend/src/app/main/ui/shapes/filters.cljs index 140b5fb57..9b3457a37 100644 --- a/frontend/src/app/main/ui/shapes/filters.cljs +++ b/frontend/src/app/main/ui/shapes/filters.cljs @@ -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] diff --git a/frontend/src/app/main/ui/shapes/svg_raw.cljs b/frontend/src/app/main/ui/shapes/svg_raw.cljs new file mode 100644 index 000000000..c79d848ed --- /dev/null +++ b/frontend/src/app/main/ui/shapes/svg_raw.cljs @@ -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)))) + + diff --git a/frontend/src/app/main/ui/viewer/shapes.cljs b/frontend/src/app/main/ui/viewer/shapes.cljs index d27dfe525..f01080e64 100644 --- a/frontend/src/app/main/ui/viewer/shapes.cljs +++ b/frontend/src/app/main/ui/viewer/shapes.cljs @@ -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 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]} diff --git a/frontend/src/app/main/ui/workspace/left_toolbar.cljs b/frontend/src/app/main/ui/workspace/left_toolbar.cljs index 12e3606e8..9685e102b 100644 --- a/frontend/src/app/main/ui/workspace/left_toolbar.cljs +++ b/frontend/src/app/main/ui/workspace/left_toolbar.cljs @@ -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") diff --git a/frontend/src/app/main/ui/workspace/shapes.cljs b/frontend/src/app/main/ui/workspace/shapes.cljs index ec3fb5d5c..ebdd1a4e3 100644 --- a/frontend/src/app/main/ui/workspace/shapes.cljs +++ b/frontend/src/app/main/ui/workspace/shapes.cljs @@ -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 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)) diff --git a/frontend/src/app/main/ui/workspace/shapes/svg_raw.cljs b/frontend/src/app/main/ui/workspace/shapes/svg_raw.cljs new file mode 100644 index 000000000..037b76e37 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/shapes/svg_raw.cljs @@ -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}]))))) + diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs index 43a79f4c9..760851967 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs @@ -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 diff --git a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs index 6fa7ecfad..ed2855291 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs @@ -45,6 +45,7 @@ (if (:masked-group? shape) i/mask i/folder)) + :svg-raw i/file-svg nil)) ;; --- Layer Name diff --git a/frontend/src/app/main/ui/workspace/sidebar/options.cljs b/frontend/src/app/main/ui/workspace/sidebar/options.cljs index 2e61b3da5..571e76de7 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options.cljs @@ -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}])) + diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/multiple.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/multiple.cljs index 1db91f69a..3dd87282b 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/multiple.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/multiple.cljs @@ -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 diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/svg_raw.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/svg_raw.cljs new file mode 100644 index 000000000..b570a3707 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/options/svg_raw.cljs @@ -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])}]]))) diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index a96a94df3..288ef617b 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -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)