From 4d0dcc587609ca08218f7daf0f58b3af0f22d8cb Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 17 Jun 2021 16:24:39 +0200 Subject: [PATCH] :sparkles: Process interactions on import --- common/src/app/common/file_builder.cljc | 25 +++ frontend/src/app/main/ui/shapes/export.cljs | 159 +++++++++++--------- frontend/src/app/util/import/parser.cljc | 27 +++- frontend/src/app/worker/import.cljs | 76 +++++++--- 4 files changed, 192 insertions(+), 95 deletions(-) diff --git a/common/src/app/common/file_builder.cljc b/common/src/app/common/file_builder.cljc index 17d2fef26b..3b0c077baf 100644 --- a/common/src/app/common/file_builder.cljc +++ b/common/src/app/common/file_builder.cljc @@ -291,6 +291,31 @@ (-> file (update :parent-stack pop))) +(defn add-interaction + [file action-type event-type from-id destination-id] + + (assert (some? (lookup-shape file from-id)) (str "Cannot locate shape with id " from-id)) + (assert (some? (lookup-shape file destination-id)) (str "Cannot locate shape with id " destination-id)) + + (let [interactions (->> (lookup-shape file from-id) + :interactions + (filterv #(or (not= (:action-type %) action-type) + (not= (:event-type %) event-type)))) + conj (fnil conj []) + interactions (-> interactions + (conj + {:action-type action-type + :event-type event-type + :destination destination-id}))] + (commit-change + file + {:type :mod-obj + :page-id (:current-page-id file) + :id from-id + + :operations + [{:type :set :attr :interactions :val interactions}]}))) + (defn generate-changes [file] (:changes file)) diff --git a/frontend/src/app/main/ui/shapes/export.cljs b/frontend/src/app/main/ui/shapes/export.cljs index c7d60b3c2d..a27009fd19 100644 --- a/frontend/src/app/main/ui/shapes/export.cljs +++ b/frontend/src/app/main/ui/shapes/export.cljs @@ -91,20 +91,22 @@ [(str "penpot:" (d/name k)) v])] (into {} (map prefix-entry) m))) + (mf/defc export-grid-data [{:keys [grids]}] - [:> "penpot:grids" #js {} - (for [{:keys [type display params]} grids] - (let [props (->> (d/without-keys params [:color]) - (prefix-keys) - (clj->js))] - [:> "penpot:grid" - (-> props - (obj/set! "penpot:color" (get-in params [:color :color])) - (obj/set! "penpot:opacity" (get-in params [:color :opacity])) - (obj/set! "penpot:type" (d/name type)) - (cond-> (some? display) - (obj/set! "penpot:display" (str display))))]))]) + (when-not (empty? grids) + [:> "penpot:grids" #js {} + (for [{:keys [type display params]} grids] + (let [props (->> (d/without-keys params [:color]) + (prefix-keys) + (clj->js))] + [:> "penpot:grid" + (-> props + (obj/set! "penpot:color" (get-in params [:color :color])) + (obj/set! "penpot:opacity" (get-in params [:color :opacity])) + (obj/set! "penpot:type" (d/name type)) + (cond-> (some? display) + (obj/set! "penpot:display" (str display))))]))])) (mf/defc export-page [{:keys [options]}] @@ -117,62 +119,85 @@ [:> "penpot:page" #js {} [:& export-grid-data {:grids grids}]])))) +(mf/defc export-shadow-data + [{:keys [shadow]}] + (for [{:keys [style hidden color offset-x offset-y blur spread]} shadow] + [:> "penpot:shadow" + #js {:penpot:shadow-type (d/name style) + :penpot:hidden (str hidden) + :penpot:color (str (:color color)) + :penpot:opacity (str (:opacity color)) + :penpot:offset-x (str offset-x) + :penpot:offset-y (str offset-y) + :penpot:blur (str blur) + :penpot:spread (str spread)}])) + +(mf/defc export-blur-data [{:keys [blur]}] + (when (some? blur) + (let [{:keys [type hidden value]} blur] + [:> "penpot:blur" + #js {:penpot:blur-type (d/name type) + :penpot:hidden (str hidden) + :penpot:value (str value)}]))) + +(mf/defc export-exports-data [{:keys [exports]}] + (for [{:keys [scale suffix type]} exports] + [:> "penpot:export" + #js {:penpot:type (d/name type) + :penpot:suffix suffix + :penpot:scale (str scale)}])) + +(mf/defc export-svg-data [shape] + [:* + (when (contains? shape :svg-attrs) + (let [svg-transform (get shape :svg-transform) + svg-attrs (->> shape :svg-attrs keys (mapv d/name) (str/join ",") ) + svg-defs (->> shape :svg-defs keys (mapv d/name) (str/join ","))] + [:> "penpot:svg-import" + #js {:penpot:svg-attrs (when-not (empty? svg-attrs) svg-attrs) + :penpot:svg-defs (when-not (empty? svg-defs) svg-defs) + :penpot:svg-transform (when svg-transform (str svg-transform)) + :penpot:svg-viewbox-x (get-in shape [:svg-viewbox :x]) + :penpot:svg-viewbox-y (get-in shape [:svg-viewbox :y]) + :penpot:svg-viewbox-width (get-in shape [:svg-viewbox :width]) + :penpot:svg-viewbox-height (get-in shape [:svg-viewbox :height])} + (for [[def-id def-xml] (:svg-defs shape)] + [:> "penpot:svg-def" #js {:def-id def-id} + [:& render-xml {:xml def-xml}]])])) + + (when (= (:type shape) :svg-raw) + (let [props + (-> (obj/new) + (obj/set! "penpot:x" (:x shape)) + (obj/set! "penpot:y" (:y shape)) + (obj/set! "penpot:width" (:width shape)) + (obj/set! "penpot:height" (:height shape)) + (obj/set! "penpot:tag" (-> (get-in shape [:content :tag]) d/name)) + (obj/merge! (-> (get-in shape [:content :attrs]) + (clj->js))))] + [:> "penpot:svg-content" props + (for [leaf (->> shape :content :content (filter string?))] + [:> "penpot:svg-child" {} leaf])]))]) + +(mf/defc export-interactions-data + [{:keys [interactions]}] + (when-not (empty? interactions) + [:> "penpot:interactions" #js {} + (for [{:keys [action-type destination event-type]} interactions] + [:> "penpot:interaction" + #js {:penpot:action-type (d/name action-type) + :penpot:destination (str destination) + :penpot:event-type (d/name event-type)}])])) + (mf/defc export-data [{:keys [shape]}] - (let [props (-> (obj/new) - (add-data shape))] + (let [props (-> (obj/new) (add-data shape)) + frame? (= (:type shape) :frame)] [:> "penpot:shape" props - (for [{:keys [style hidden color offset-x offset-y blur spread]} (:shadow shape)] - [:> "penpot:shadow" #js {:penpot:shadow-type (d/name style) - :penpot:hidden (str hidden) - :penpot:color (str (:color color)) - :penpot:opacity (str (:opacity color)) - :penpot:offset-x (str offset-x) - :penpot:offset-y (str offset-y) - :penpot:blur (str blur) - :penpot:spread (str spread)}]) - - (when (some? (:blur shape)) - (let [{:keys [type hidden value]} (:blur shape)] - [:> "penpot:blur" #js {:penpot:blur-type (d/name type) - :penpot:hidden (str hidden) - :penpot:value (str value)}])) - - (for [{:keys [scale suffix type]} (:exports shape)] - [:> "penpot:export" #js {:penpot:type (d/name type) - :penpot:suffix suffix - :penpot:scale (str scale)}]) - - (when (contains? shape :svg-attrs) - (let [svg-transform (get shape :svg-transform) - svg-attrs (->> shape :svg-attrs keys (mapv d/name) (str/join ",") ) - svg-defs (->> shape :svg-defs keys (mapv d/name) (str/join ","))] - [:> "penpot:svg-import" #js {:penpot:svg-attrs (when-not (empty? svg-attrs) svg-attrs) - :penpot:svg-defs (when-not (empty? svg-defs) svg-defs) - :penpot:svg-transform (when svg-transform (str svg-transform)) - :penpot:svg-viewbox-x (get-in shape [:svg-viewbox :x]) - :penpot:svg-viewbox-y (get-in shape [:svg-viewbox :y]) - :penpot:svg-viewbox-width (get-in shape [:svg-viewbox :width]) - :penpot:svg-viewbox-height (get-in shape [:svg-viewbox :height])} - (for [[def-id def-xml] (:svg-defs shape)] - [:> "penpot:svg-def" #js {:def-id def-id} - [:& render-xml {:xml def-xml}]])])) - - (when (= (:type shape) :svg-raw) - (let [props (-> (obj/new) - (obj/set! "penpot:x" (:x shape)) - (obj/set! "penpot:y" (:y shape)) - (obj/set! "penpot:width" (:width shape)) - (obj/set! "penpot:height" (:height shape)) - (obj/set! "penpot:tag" (-> (get-in shape [:content :tag]) d/name)) - (obj/merge! (-> (get-in shape [:content :attrs]) - (clj->js))))] - [:> "penpot:svg-content" props - (for [leaf (->> shape :content :content (filter string?))] - [:> "penpot:svg-child" {} leaf])])) - - - (when (and (= (:type shape) :frame) - (seq (:grids shape))) - [:& export-grid-data {:grids (:grids shape)}])])) + [:& export-shadow-data shape] + [:& export-blur-data shape] + [:& export-exports-data shape] + [:& export-svg-data shape] + [:& export-interactions-data shape] + [:& export-grid-data shape]])) diff --git a/frontend/src/app/util/import/parser.cljc b/frontend/src/app/util/import/parser.cljc index 1ffc490d46..84af3163bc 100644 --- a/frontend/src/app/util/import/parser.cljc +++ b/frontend/src/app/util/import/parser.cljc @@ -15,6 +15,12 @@ [app.util.path.parser :as upp] [cuerdas.core :as str])) +(def url-regex + #"url\(#([^\)]*)\)") + +(def uuid-regex + #"\w{8}-\w{4}-\w{4}-\w{4}-\w{12}") + (defn valid? [root] (contains? (:attrs root) :xmlns:penpot)) @@ -41,7 +47,7 @@ (defn find-all-nodes [node tag] (when (some? node) - (->> node :content (filterv #(= (:tag %) :defs))))) + (->> node :content (filterv #(= (:tag %) tag))))) (defn get-data ([node] @@ -65,6 +71,11 @@ (or (close? node) (some? (get-data node)))) +(defn get-id + [node] + (when-let [id (re-find uuid-regex (get-in node [:attrs :id]))] + (uuid/uuid id))) + (defn str->bool [val] (when (some? val) (= val "true"))) @@ -193,9 +204,6 @@ (assoc :content content) (assoc :center center)))) -(def url-regex #"url\(#([^\)]*)\)") - - (defn parse-stops [gradient-node] (->> gradient-node @@ -483,7 +491,7 @@ (defn remove-prefix [s] (cond-> s (string? s) - (str/replace #"\w{8}-\w{4}-\w{4}-\w{4}-\w{12}-" ""))) + (str/replace (re-pattern (str uuid-regex "-")) ""))) (defn get-svg-attrs [svg-data svg-attrs] @@ -640,3 +648,12 @@ (not (empty? grids)) (assoc-in [:options :saved-grids] grids)))) + +(defn parse-interactions + [node] + (let [interactions-node (get-data node :penpot:interactions)] + (->> (find-all-nodes interactions-node :penpot:interaction) + (mapv (fn [node] + {:destination (get-meta node :destination uuid/uuid) + :action-type (get-meta node :action-type keyword) + :event-type (get-meta node :event-type keyword)}))))) diff --git a/frontend/src/app/worker/import.cljs b/frontend/src/app/worker/import.cljs index f9ee680fa3..e54098b432 100644 --- a/frontend/src/app/worker/import.cljs +++ b/frontend/src/app/worker/import.cljs @@ -78,36 +78,65 @@ (defn add-shape-file [file node] - (let [type (cip/get-type node) - close? (cip/close? node) - data (cip/parse-data type node)] - + (let [type (cip/get-type node) + close? (cip/close? node)] (if close? (case type - :frame - (fb/close-artboard file) + :frame (fb/close-artboard file) + :group (fb/close-group file) + :svg-raw (fb/close-svg-raw file) + #_default file) - :group - (fb/close-group file) + (let [data (cip/parse-data type node) + old-id (cip/get-id node) + interactions (cip/parse-interactions node) - :svg-raw - (fb/close-svg-raw file) + file (case type + :frame (fb/add-artboard file data) + :group (fb/add-group file data) + :rect (fb/create-rect file data) + :circle (fb/create-circle file data) + :path (fb/create-path file data) + :text (fb/create-text file data) + :image (fb/create-image file data) + :svg-raw (fb/create-svg-raw file data) + #_default file)] - ;; default - file) + (assert (some? old-id) "ID not found") - (case type - :frame (fb/add-artboard file data) - :group (fb/add-group file data) - :rect (fb/create-rect file data) - :circle (fb/create-circle file data) - :path (fb/create-path file data) - :text (fb/create-text file data) - :image (fb/create-image file data) - :svg-raw (fb/create-svg-raw file data) + ;; We store this data for post-processing after every shape has been + ;; added + (cond-> file + (some? (:last-id file)) + (assoc-in [:id-mapping old-id] (:last-id file)) - ;; default - file)))) + (not (empty? interactions)) + (assoc-in [:interactions old-id] interactions)))))) + +(defn post-process-file + [file] + + (letfn [(add-interaction + [id file {:keys [action-type event-type destination] :as interaction}] + (fb/add-interaction file action-type event-type id destination)) + + (add-interactions + [file [old-id interactions]] + (let [id (get-in file [:id-mapping old-id])] + (->> interactions + (mapv (fn [interaction] + (let [id (get-in file [:id-mapping (:destination interaction)])] + (assoc interaction :destination id)))) + (reduce + (partial add-interaction id) file)))) + + (process-interactions + [file] + (reduce add-interactions file (:interactions file)))] + + (-> file + (process-interactions) + (dissoc :id-mapping :interactions)))) (defn merge-reduce [f seed ob] (->> (rx/concat @@ -145,6 +174,7 @@ (rx/filter cip/shape?) (rx/mapcat (partial resolve-images file-id)) (rx/reduce add-shape-file (fb/add-page file page-data)) + (rx/map post-process-file) (rx/map fb/close-page))) (rx/empty)))