mirror of
https://github.com/penpot/penpot.git
synced 2025-04-12 15:01:28 -05:00
Merge pull request #483 from penpot/fixes/performance-improvements
Performance improvements
This commit is contained in:
commit
1ce68cb1cf
25 changed files with 604 additions and 582 deletions
|
@ -9,71 +9,68 @@
|
|||
|
||||
(ns app.common.attrs)
|
||||
|
||||
;; Extract some attributes of a list of shapes.
|
||||
;; For each attribute, if the value is the same in all shapes,
|
||||
;; wll take this value. If there is any shape that is different,
|
||||
;; the value of the attribute will be the keyword :multiple.
|
||||
;;
|
||||
;; If some shape has the value nil in any attribute, it's
|
||||
;; considered a different value. If the shape does not contain
|
||||
;; the attribute, it's ignored in the final result.
|
||||
;;
|
||||
;; Example:
|
||||
;; (def shapes [{:stroke-color "#ff0000"
|
||||
;; :stroke-width 3
|
||||
;; :fill-color "#0000ff"
|
||||
;; :x 1000 :y 2000 :rx nil}
|
||||
;; {:stroke-width "#ff0000"
|
||||
;; :stroke-width 5
|
||||
;; :x 1500 :y 2000}])
|
||||
;;
|
||||
;; (get-attrs-multi shapes [:stroke-color
|
||||
;; :stroke-width
|
||||
;; :fill-color
|
||||
;; :rx
|
||||
;; :ry])
|
||||
;; >>> {:stroke-color "#ff0000"
|
||||
;; :stroke-width :multiple
|
||||
;; :fill-color "#0000ff"
|
||||
;; :rx nil
|
||||
;; :ry nil}
|
||||
;;
|
||||
|
||||
(defn get-attrs-multi
|
||||
([shapes attrs] (get-attrs-multi shapes attrs = identity))
|
||||
([shapes attrs eq-fn sel-fn]
|
||||
;; Extract some attributes of a list of shapes.
|
||||
;; For each attribute, if the value is the same in all shapes,
|
||||
;; wll take this value. If there is any shape that is different,
|
||||
;; the value of the attribute will be the keyword :multiple.
|
||||
;;
|
||||
;; If some shape has the value nil in any attribute, it's
|
||||
;; considered a different value. If the shape does not contain
|
||||
;; the attribute, it's ignored in the final result.
|
||||
;;
|
||||
;; Example:
|
||||
;; (def shapes [{:stroke-color "#ff0000"
|
||||
;; :stroke-width 3
|
||||
;; :fill-color "#0000ff"
|
||||
;; :x 1000 :y 2000 :rx nil}
|
||||
;; {:stroke-width "#ff0000"
|
||||
;; :stroke-width 5
|
||||
;; :x 1500 :y 2000}])
|
||||
;;
|
||||
;; (get-attrs-multi shapes [:stroke-color
|
||||
;; :stroke-width
|
||||
;; :fill-color
|
||||
;; :rx
|
||||
;; :ry])
|
||||
;; >>> {:stroke-color "#ff0000"
|
||||
;; :stroke-width :multiple
|
||||
;; :fill-color "#0000ff"
|
||||
;; :rx nil
|
||||
;; :ry nil}
|
||||
;;
|
||||
(let [defined-shapes (filter some? shapes)
|
||||
([objs attrs]
|
||||
(get-attrs-multi objs attrs = identity))
|
||||
|
||||
combine-value (fn [v1 v2]
|
||||
(cond
|
||||
(and (= v1 :undefined) (= v2 :undefined)) :undefined
|
||||
(= v1 :undefined) (if (= v2 :multiple) :multiple (sel-fn v2))
|
||||
(= v2 :undefined) (if (= v1 :multiple) :multiple (sel-fn v1))
|
||||
(or (= v1 :multiple) (= v2 :multiple)) :multiple
|
||||
(eq-fn v1 v2) (sel-fn v1)
|
||||
:else :multiple))
|
||||
([objs attrs eqfn sel]
|
||||
|
||||
combine-values (fn [attrs shape values]
|
||||
(map #(combine-value (get shape % :undefined)
|
||||
(get values % :undefined)) attrs))
|
||||
(loop [attr (first attrs)
|
||||
attrs (rest attrs)
|
||||
result (transient {})]
|
||||
|
||||
select-attrs (fn [shape attrs]
|
||||
(zipmap attrs (map #(get shape % :undefined) attrs)))
|
||||
(if attr
|
||||
(let [value
|
||||
(loop [curr (first objs)
|
||||
objs (rest objs)
|
||||
value ::undefined]
|
||||
|
||||
reducer (fn [result shape]
|
||||
(zipmap attrs (combine-values attrs shape result)))
|
||||
(if (and curr (not= value :multiple))
|
||||
;;
|
||||
(let [new-val (get curr attr ::undefined)
|
||||
value (cond
|
||||
(= new-val ::undefined) value
|
||||
(= value ::undefined) (sel new-val)
|
||||
(eqfn new-val value) value
|
||||
:else :multiple)]
|
||||
(recur (first objs) (rest objs) value))
|
||||
;;
|
||||
value))]
|
||||
(recur (first attrs)
|
||||
(rest attrs)
|
||||
(cond-> result
|
||||
(not= value ::undefined)
|
||||
(assoc! attr value))))
|
||||
|
||||
combined (reduce reducer
|
||||
(select-attrs (first defined-shapes) attrs)
|
||||
(rest defined-shapes))
|
||||
(persistent! result)))))
|
||||
|
||||
cleanup-value (fn [value]
|
||||
(if (= value :undefined) nil value))
|
||||
|
||||
cleanup (fn [result]
|
||||
(->> attrs
|
||||
(map #(get result %))
|
||||
(zipmap attrs)
|
||||
(filter #(not= (second %) :undefined))
|
||||
(into {})))]
|
||||
|
||||
(cleanup combined))))
|
||||
|
|
|
@ -124,9 +124,6 @@
|
|||
[shape {:keys [x y]}]
|
||||
(move shape (gpt/point (- x) (- y))))
|
||||
|
||||
(defn translate-from-frame
|
||||
[shape {:keys [x y]}]
|
||||
(move shape (gpt/point x y)))
|
||||
|
||||
;; --- Helpers
|
||||
|
||||
|
|
|
@ -29,12 +29,15 @@
|
|||
(defmulti process-operation (fn [_ op] (:type op)))
|
||||
|
||||
(defn process-changes
|
||||
[data items]
|
||||
(->> (us/verify ::spec/changes items)
|
||||
(reduce #(do
|
||||
;; (prn "process-change" (:type %2) (:id %2))
|
||||
(or (process-change %1 %2) %1))
|
||||
data)))
|
||||
([data items] (process-changes data items true))
|
||||
([data items verify?]
|
||||
;; When verify? false we spec the schema validation. Currently used to make just
|
||||
;; 1 validation even if the changes are applied twice
|
||||
(when verify?
|
||||
(us/verify ::spec/changes items))
|
||||
|
||||
(->> items
|
||||
(reduce #(or (process-change %1 %2) %1) data))))
|
||||
|
||||
(defmethod process-change :set-option
|
||||
[data {:keys [page-id option value]}]
|
||||
|
@ -91,7 +94,7 @@
|
|||
(let [update-fn (fn [objects]
|
||||
(if-let [obj (get objects id)]
|
||||
(let [result (reduce process-operation obj operations)]
|
||||
(us/verify ::spec/shape result)
|
||||
#?(:clj (us/verify ::spec/shape result))
|
||||
(assoc objects id result))
|
||||
objects))]
|
||||
(if page-id
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
funcool/okulary {:mvn/version "2020.04.14-0"}
|
||||
funcool/potok {:mvn/version "3.2.0"}
|
||||
funcool/promesa {:mvn/version "6.0.0"}
|
||||
funcool/rumext {:mvn/version "2020.11.27-0"}
|
||||
funcool/rumext {:mvn/version "2021.01.26-0"}
|
||||
|
||||
lambdaisland/uri {:mvn/version "1.4.54"
|
||||
:exclusions [org.clojure/data.json]}
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
[app.common.geom.proportions :as gpr]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.pages :as cp]
|
||||
[app.common.pages.spec :as spec]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.worker :as uw]
|
||||
|
@ -93,9 +94,10 @@
|
|||
[:workspace-data]
|
||||
[:workspace-libraries file-id :data])]
|
||||
(try
|
||||
(let [state (update-in state path1 cp/process-changes changes)]
|
||||
(us/verify ::spec/changes changes)
|
||||
(let [state (update-in state path1 cp/process-changes changes false)]
|
||||
(cond-> state
|
||||
commit-local? (update-in path2 cp/process-changes changes)))
|
||||
commit-local? (update-in path2 cp/process-changes changes false)))
|
||||
(catch :default e
|
||||
(vreset! error e)
|
||||
state))))
|
||||
|
|
|
@ -221,40 +221,93 @@
|
|||
(defn not-changed? [old-dim new-dim]
|
||||
(> (mth/abs (- old-dim new-dim)) 0.1))
|
||||
|
||||
(defn resize-text [id new-width new-height]
|
||||
(ptk/reify ::resize-text
|
||||
(defn resize-text-batch [changes]
|
||||
(ptk/reify ::resize-text-batch
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [page-id (:current-page-id state)
|
||||
shape (get-in state [:workspace-data :pages-index page-id :objects id])
|
||||
{:keys [selrect grow-type overflow-text]} (gsh/transform-shape shape)
|
||||
{shape-width :width shape-height :height} selrect
|
||||
(let [page-id (:current-page-id state)
|
||||
|
||||
objects0 (get-in state [:workspace-file :data :pages-index page-id :objects])
|
||||
objects1 (get-in state [:workspace-data :pages-index page-id :objects])
|
||||
|
||||
change-text-shape
|
||||
(fn [objects [id [new-width new-height]]]
|
||||
|
||||
(let [shape (get objects id)
|
||||
{:keys [selrect grow-type overflow-text]} (gsh/transform-shape shape)
|
||||
{shape-width :width shape-height :height} selrect
|
||||
|
||||
modifier-width (gsh/resize-modifiers shape :width new-width)
|
||||
modifier-height (gsh/resize-modifiers shape :height new-height)
|
||||
|
||||
shape (cond-> shape
|
||||
(and overflow-text (not= :fixed grow-type))
|
||||
(assoc :overflow-text false)
|
||||
|
||||
(and (= :fixed grow-type) (not overflow-text) (> new-height shape-height))
|
||||
(assoc :overflow-text true)
|
||||
|
||||
(and (= :fixed grow-type) overflow-text (<= new-height shape-height))
|
||||
(assoc :overflow-text true)
|
||||
|
||||
(and (not-changed? shape-width new-width) (= grow-type :auto-width))
|
||||
(-> (assoc :modifiers modifier-width)
|
||||
(gsh/transform-shape))
|
||||
|
||||
(and (not-changed? shape-height new-height)
|
||||
(or (= grow-type :auto-height) (= grow-type :auto-width)))
|
||||
(-> (assoc :modifiers modifier-height)
|
||||
(gsh/transform-shape)))]
|
||||
(assoc objects id shape)))
|
||||
|
||||
undo-transaction (get-in state [:workspace-undo :transaction])
|
||||
objects2 (->> changes (reduce change-text-shape objects1))
|
||||
|
||||
events
|
||||
(cond-> []
|
||||
(and overflow-text (not= :fixed grow-type))
|
||||
(conj (update-overflow-text id false))
|
||||
regchg {:type :reg-objects
|
||||
:page-id page-id
|
||||
:shapes (vec (keys changes))}
|
||||
|
||||
(and (= :fixed grow-type) (not overflow-text) (> new-height shape-height))
|
||||
(conj (update-overflow-text id true))
|
||||
rchanges (dwc/generate-changes page-id objects1 objects2)
|
||||
uchanges (dwc/generate-changes page-id objects2 objects0)]
|
||||
|
||||
(and (= :fixed grow-type) overflow-text (<= new-height shape-height))
|
||||
(conj (update-overflow-text id false))
|
||||
|
||||
(and (or (not-changed? shape-width new-width) (not-changed? shape-height new-height))
|
||||
(= grow-type :auto-width))
|
||||
(conj (dwt/update-dimensions [id] :width new-width)
|
||||
(dwt/update-dimensions [id] :height new-height))
|
||||
|
||||
(and (not-changed? shape-height new-height)
|
||||
(= grow-type :auto-height))
|
||||
(conj (dwt/update-dimensions [id] :height new-height)))]
|
||||
|
||||
(if (not (empty? events))
|
||||
(if (seq rchanges)
|
||||
(rx/concat
|
||||
(when (not undo-transaction)
|
||||
(when-not undo-transaction
|
||||
(rx/of (dwc/start-undo-transaction)))
|
||||
(rx/from events)
|
||||
(when (not undo-transaction)
|
||||
(rx/of (dwc/commit-changes (conj rchanges regchg) (conj uchanges regchg) {:commit-local? true}))
|
||||
(when-not undo-transaction
|
||||
(rx/of (dwc/discard-undo-transaction)))))))))
|
||||
|
||||
;; When a resize-event arrives we start "buffering" for a time
|
||||
;; after that time we invoke `resize-text-batch` with all the changes
|
||||
;; together. This improves the performance because we only re-render the
|
||||
;; resized components once even if there are changes that applies to
|
||||
;; lots of texts like changing a font
|
||||
(defn resize-text [id new-width new-height]
|
||||
(ptk/reify ::resize-text
|
||||
IDeref
|
||||
(-deref [_]
|
||||
{:id id :width new-width :height new-height})
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [;; This stream aggregates the events of "resizing"
|
||||
resize-events (rx/merge
|
||||
(->> (rx/of (resize-text id new-width new-height)))
|
||||
(->> stream (rx/filter (ptk/type? ::resize-text))))
|
||||
|
||||
;; Stop buffering after time without resizes
|
||||
stop-buffer (->> resize-events (rx/debounce 100))]
|
||||
|
||||
(if-not (::handling-texts state)
|
||||
(->> (rx/concat
|
||||
(rx/of #(assoc % ::handling-texts true))
|
||||
(->> resize-events
|
||||
(rx/take-until stop-buffer)
|
||||
(rx/reduce (fn [acc event]
|
||||
(assoc acc (:id @event) [(:width @event) (:height @event)]))
|
||||
{id [new-width new-height]})
|
||||
(rx/map #(resize-text-batch %)))
|
||||
|
||||
(rx/of #(dissoc % ::handling-texts))))
|
||||
(rx/empty))))))
|
||||
|
|
|
@ -159,51 +159,45 @@
|
|||
ids))
|
||||
workspace-page-objects =))
|
||||
|
||||
(def selected-data
|
||||
(l/derived #(let [selected (get-in % [:workspace-local :selected])
|
||||
page-id (:current-page-id %)
|
||||
objects (get-in % [:workspace-data :pages-index page-id :objects])]
|
||||
(hash-map :selected selected
|
||||
:page-id page-id
|
||||
:objects objects))
|
||||
st/state =))
|
||||
|
||||
(defn is-child-selected?
|
||||
[id]
|
||||
(letfn [(selector [state]
|
||||
(let [page-id (:current-page-id state)
|
||||
objects (get-in state [:workspace-data :pages-index page-id :objects])
|
||||
selected (get-in state [:workspace-local :selected])
|
||||
children (cp/get-children id objects)]
|
||||
(letfn [(selector [{:keys [selected page-id objects]}]
|
||||
(let [children (cp/get-children id objects)]
|
||||
(some #(contains? selected %) children)))]
|
||||
(l/derived selector st/state)))
|
||||
(l/derived selector selected-data =)))
|
||||
|
||||
|
||||
;; TODO: can be replaced by objects-by-id
|
||||
(def selected-objects
|
||||
(letfn [(selector [state]
|
||||
(let [selected (get-in state [:workspace-local :selected])
|
||||
page-id (:current-page-id state)
|
||||
objects (get-in state [:workspace-data :pages-index page-id :objects])]
|
||||
(->> selected
|
||||
(map #(get objects %))
|
||||
(filterv (comp not nil?)))))]
|
||||
(l/derived selector st/state =)))
|
||||
(letfn [(selector [{:keys [selected page-id objects]}]
|
||||
(->> selected
|
||||
(map #(get objects %))
|
||||
(filterv (comp not nil?))))]
|
||||
(l/derived selector selected-data =)))
|
||||
|
||||
(def selected-shapes-with-children
|
||||
(letfn [(selector [state]
|
||||
(let [selected (get-in state [:workspace-local :selected])
|
||||
page-id (:current-page-id state)
|
||||
objects (get-in state [:workspace-data :pages-index page-id :objects])
|
||||
children (->> selected
|
||||
(letfn [(selector [{:keys [selected page-id objects]}]
|
||||
(let [children (->> selected
|
||||
(mapcat #(cp/get-children % objects))
|
||||
(filterv (comp not nil?)))]
|
||||
(into selected children)))]
|
||||
(l/derived selector st/state =)))
|
||||
|
||||
(l/derived selector selected-data =)))
|
||||
|
||||
(def selected-objects-with-children
|
||||
(letfn [(selector [state]
|
||||
(let [selected (get-in state [:workspace-local :selected])
|
||||
page-id (:current-page-id state)
|
||||
objects (get-in state [:workspace-data :pages-index page-id :objects])
|
||||
children (->> selected
|
||||
(letfn [(selector [{:keys [selected page-id objects]}]
|
||||
(let [children (->> selected
|
||||
(mapcat #(cp/get-children % objects))
|
||||
(filterv (comp not nil?)))
|
||||
shapes (into selected children)]
|
||||
(mapv #(get objects %) shapes)))]
|
||||
(l/derived selector st/state =)))
|
||||
(l/derived selector selected-data =)))
|
||||
|
||||
;; ---- Viewer refs
|
||||
|
||||
|
|
|
@ -24,7 +24,8 @@
|
|||
(let [shape (unchecked-get props "shape")
|
||||
background? (unchecked-get props "background?")
|
||||
{:keys [id x y width height]} (:selrect shape)
|
||||
pdata (ugp/content->path (:content shape))
|
||||
content (:content shape)
|
||||
pdata (mf/use-memo (mf/deps content) #(ugp/content->path content))
|
||||
props (-> (attrs/extract-style-attrs shape)
|
||||
(obj/merge!
|
||||
#js {:d pdata}))]
|
||||
|
|
|
@ -23,33 +23,6 @@
|
|||
;; Context to store a re-mapping of the ids
|
||||
(def svg-ids-ctx (mf/create-context nil))
|
||||
|
||||
(defn vbox->rect
|
||||
"Converts the viewBox into a rectangle"
|
||||
[vbox]
|
||||
(when vbox
|
||||
(let [[x y width height] (map ud/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 generate-id-mapping [content]
|
||||
(letfn [(visit-node [result node]
|
||||
(let [element-id (get-in node [:attrs :id])
|
||||
|
@ -58,79 +31,102 @@
|
|||
(reduce visit-node result (:content node))))]
|
||||
(visit-node {} content)))
|
||||
|
||||
|
||||
(defonce replace-regex #"[^#]*#([^)\s]+).*")
|
||||
|
||||
(defn replace-attrs-ids
|
||||
"Replaces the ids inside a property"
|
||||
[ids-mapping attrs]
|
||||
|
||||
(letfn [(replace-ids [key val]
|
||||
(if (map? val)
|
||||
(cd/mapm replace-ids val)
|
||||
(let [[_ from-id] (re-matches replace-regex val)]
|
||||
(if (and from-id (contains? ids-mapping from-id))
|
||||
(str/replace val from-id (get ids-mapping from-id))
|
||||
val))))]
|
||||
(cd/mapm replace-ids attrs)))
|
||||
|
||||
(mf/defc svg-root
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
|
||||
(let [shape (unchecked-get props "shape")
|
||||
children (unchecked-get props "children")
|
||||
|
||||
{:keys [x y width height]} shape
|
||||
{:keys [tag attrs] :as content} (:content shape)
|
||||
|
||||
ids-mapping (mf/use-memo #(generate-id-mapping content))
|
||||
|
||||
attrs (-> (clj->js attrs)
|
||||
(obj/set! "x" x)
|
||||
(obj/set! "y" y)
|
||||
(obj/set! "width" width)
|
||||
(obj/set! "height" height)
|
||||
(obj/set! "preserveAspectRatio" "none"))]
|
||||
|
||||
[:& (mf/provider svg-ids-ctx) {:value ids-mapping}
|
||||
[:g.svg-raw {:transform (gsh/transform-matrix shape)}
|
||||
[:> "svg" attrs children]]]))
|
||||
|
||||
(mf/defc svg-element
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [shape (unchecked-get props "shape")
|
||||
children (unchecked-get props "children")
|
||||
|
||||
{:keys [content]} shape
|
||||
{:keys [attrs tag]} content
|
||||
|
||||
ids-mapping (mf/use-ctx svg-ids-ctx)
|
||||
attrs (mf/use-memo #(replace-attrs-ids ids-mapping attrs))
|
||||
custom-attrs (usa/extract-style-attrs shape)
|
||||
|
||||
element-id (get-in content [:attrs :id])
|
||||
|
||||
style (obj/merge! (clj->js (:style attrs {}))
|
||||
(obj/get custom-attrs "style"))
|
||||
|
||||
attrs (-> (clj->js attrs)
|
||||
(obj/merge! custom-attrs)
|
||||
(obj/set! "style" style))
|
||||
|
||||
attrs (cond-> attrs
|
||||
element-id (obj/set! "id" (get ids-mapping element-id)))
|
||||
]
|
||||
[:> (name tag) attrs children]))
|
||||
|
||||
(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")
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
|
||||
{:keys [tag attrs] :as content} (:content shape)
|
||||
(let [frame (unchecked-get props "frame")
|
||||
shape (unchecked-get props "shape")
|
||||
childs (unchecked-get props "childs")
|
||||
|
||||
new-mapping (mf/use-memo #(when (= tag :svg) (generate-id-mapping content)))
|
||||
ids-mapping (if (= tag :svg)
|
||||
new-mapping
|
||||
(mf/use-ctx svg-ids-ctx))
|
||||
{:keys [content]} shape
|
||||
{:keys [tag]} content
|
||||
|
||||
rex #"[^#]*#([^)\s]+).*"
|
||||
svg-root? (and (map? content) (= tag :svg))
|
||||
svg-tag? (map? content)
|
||||
svg-leaf? (string? content)]
|
||||
|
||||
;; Replaces the attributes ID's so there are no collisions between shapes
|
||||
replace-ids
|
||||
(fn replace-ids [key val]
|
||||
(if (map? val)
|
||||
(cd/mapm replace-ids val)
|
||||
(let [[_ from-id] (re-matches rex val)]
|
||||
(if (and from-id (contains? ids-mapping from-id))
|
||||
(str/replace val from-id (get ids-mapping from-id))
|
||||
val))))
|
||||
(cond
|
||||
svg-root?
|
||||
[:& svg-root {:shape shape}
|
||||
(for [item childs]
|
||||
[:& shape-wrapper {:frame frame :shape item :key (:id item)}])]
|
||||
|
||||
attrs (cd/mapm replace-ids attrs)
|
||||
svg-tag?
|
||||
[:& svg-element {:shape shape}
|
||||
(for [item childs]
|
||||
[:& shape-wrapper {:frame frame :shape item :key (:id item)}])]
|
||||
|
||||
custom-attrs (usa/extract-style-attrs shape)
|
||||
svg-leaf?
|
||||
content
|
||||
|
||||
style (obj/merge! (clj->js (:style attrs {}))
|
||||
(obj/get custom-attrs "style"))
|
||||
|
||||
attrs (-> (clj->js attrs)
|
||||
(obj/merge! custom-attrs)
|
||||
(obj/set! "style" style))
|
||||
|
||||
element-id (get-in content [:attrs :id])]
|
||||
|
||||
(cond
|
||||
;; Root SVG TAG
|
||||
(and (map? content) (= tag :svg))
|
||||
(let [{: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)))]
|
||||
|
||||
[:& (mf/provider svg-ids-ctx) {:value ids-mapping}
|
||||
[: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)
|
||||
(let [attrs (cond-> attrs
|
||||
element-id (obj/set! "id" (get ids-mapping element-id)))]
|
||||
[:> (name tag) attrs
|
||||
(for [item childs]
|
||||
[:& shape-wrapper {:frame frame
|
||||
:shape item
|
||||
:key (:id item)}])])
|
||||
|
||||
;; String content
|
||||
(string? content) content
|
||||
|
||||
:else nil))))
|
||||
:else nil))))
|
||||
|
||||
|
||||
|
|
|
@ -18,64 +18,90 @@
|
|||
[app.util.object :as obj]
|
||||
[app.util.color :as uc]
|
||||
[app.main.ui.shapes.text.styles :as sts]
|
||||
[app.main.ui.shapes.text.embed :as ste]))
|
||||
[app.main.ui.shapes.text.embed :as ste]
|
||||
[app.util.perf :as perf]))
|
||||
|
||||
(mf/defc render-text
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [node (obj/get props "node")
|
||||
text (:text node)
|
||||
style (sts/generate-text-styles props)]
|
||||
[:span {:style style
|
||||
:className (when (:fill-color-gradient node) "gradient")}
|
||||
(if (= text "") "\u00A0" text)]))
|
||||
|
||||
(mf/defc render-root
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [node (obj/get props "node")
|
||||
embed-fonts? (obj/get props "embed-fonts?")
|
||||
children (obj/get props "children")
|
||||
style (sts/generate-root-styles props)]
|
||||
[:div.root.rich-text
|
||||
{:style style
|
||||
:xmlns "http://www.w3.org/1999/xhtml"}
|
||||
[:*
|
||||
[:style ".gradient { background: var(--text-color); -webkit-text-fill-color: transparent; -webkit-background-clip: text;"]
|
||||
(when embed-fonts?
|
||||
[ste/embed-fontfaces-style {:node node}])]
|
||||
children]))
|
||||
|
||||
(mf/defc render-paragraph-set
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [node (obj/get props "node")
|
||||
children (obj/get props "children")
|
||||
style (sts/generate-paragraph-set-styles props)]
|
||||
[:div.paragraph-set {:style style} children]))
|
||||
|
||||
(mf/defc render-paragraph
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [node (obj/get props "node")
|
||||
children (obj/get props "children")
|
||||
style (sts/generate-paragraph-styles props)]
|
||||
[:p.paragraph {:style style} children]))
|
||||
|
||||
;; -- Text nodes
|
||||
(mf/defc text-node
|
||||
[{:keys [node index shape] :as props}]
|
||||
(let [embed-resources? (mf/use-ctx muc/embed-ctx)
|
||||
{:keys [type text children]} node
|
||||
props #js {:shape shape}
|
||||
render-node
|
||||
(fn [index node]
|
||||
(mf/element text-node {:index index
|
||||
:node node
|
||||
:key index
|
||||
:shape shape}))]
|
||||
|
||||
(mf/defc render-node
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [node (obj/get props "node")
|
||||
index (obj/get props "index")
|
||||
{:keys [type text children]} node]
|
||||
(if (string? text)
|
||||
(let [style (sts/generate-text-styles (clj->js node) props)]
|
||||
[:span {:style style
|
||||
:className (when (:fill-color-gradient node) "gradient")}
|
||||
(if (= text "") "\u00A0" text)])
|
||||
[:> render-text props]
|
||||
|
||||
(let [children (map-indexed render-node children)]
|
||||
(case type
|
||||
"root"
|
||||
(let [style (sts/generate-root-styles (clj->js node) props)]
|
||||
[:div.root.rich-text
|
||||
{:key index
|
||||
:style style
|
||||
:xmlns "http://www.w3.org/1999/xhtml"}
|
||||
[:*
|
||||
[:style ".gradient { background: var(--text-color); -webkit-text-fill-color: transparent; -webkit-background-clip: text;"]
|
||||
(when embed-resources?
|
||||
[ste/embed-fontfaces-style {:node node}])]
|
||||
children])
|
||||
|
||||
"paragraph-set"
|
||||
(let [style (sts/generate-paragraph-set-styles (clj->js node) props)]
|
||||
[:div.paragraph-set {:key index :style style} children])
|
||||
|
||||
"paragraph"
|
||||
(let [style (sts/generate-paragraph-styles (clj->js node) props)]
|
||||
[:p.paragraph {:key index :style style} children])
|
||||
|
||||
nil)))))
|
||||
(let [component (case type
|
||||
"root" render-root
|
||||
"paragraph-set" render-paragraph-set
|
||||
"paragraph" render-paragraph
|
||||
nil)]
|
||||
(when component
|
||||
[:> component (obj/set! props "key" index)
|
||||
(for [[index child] (d/enumerate children)]
|
||||
(let [props (-> (obj/clone props)
|
||||
(obj/set! "node" child)
|
||||
(obj/set! "index" index)
|
||||
(obj/set! "key" index))]
|
||||
[:> render-node props]))])))))
|
||||
|
||||
(mf/defc text-content
|
||||
{::mf/wrap-props false
|
||||
::mf/wrap [mf/memo]}
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [root (obj/get props "content")
|
||||
shape (obj/get props "shape")]
|
||||
[:& text-node {:index 0
|
||||
:node root
|
||||
:shape shape}]))
|
||||
shape (obj/get props "shape")
|
||||
embed-fonts? (obj/get props "embed-fonts?")]
|
||||
[:& render-node {:index 0
|
||||
:node root
|
||||
:shape shape
|
||||
:embed-fonts? embed-fonts?}]))
|
||||
|
||||
(defn- retrieve-colors
|
||||
[shape]
|
||||
(let [colors (->> shape :content
|
||||
(let [colors (->> shape
|
||||
:content
|
||||
(tree-seq map? :children)
|
||||
(into #{} (comp (map :fill-color) (filter string?))))]
|
||||
(if (empty? colors)
|
||||
|
@ -87,8 +113,8 @@
|
|||
::mf/forward-ref true}
|
||||
[props ref]
|
||||
(let [shape (unchecked-get props "shape")
|
||||
selected? (unchecked-get props "selected?")
|
||||
grow-type (unchecked-get props "grow-type")
|
||||
embed-fonts? (mf/use-ctx muc/embed-ctx)
|
||||
{:keys [id x y width height content]} shape]
|
||||
[:foreignObject {:x x
|
||||
:y y
|
||||
|
@ -99,4 +125,5 @@
|
|||
:height (if (#{:auto-height :auto-width} grow-type) 10000 height)
|
||||
:ref ref}
|
||||
[:& text-content {:shape shape
|
||||
:content (:content shape)}]]))
|
||||
:content (:content shape)
|
||||
:embed-fonts? embed-fonts?}]]))
|
||||
|
|
|
@ -17,111 +17,115 @@
|
|||
[app.util.text :as ut]))
|
||||
|
||||
(defn generate-root-styles
|
||||
[data props]
|
||||
(let [valign (obj/get data "vertical-align" "top")
|
||||
talign (obj/get data "text-align" "flex-start")
|
||||
shape (obj/get props "shape")
|
||||
base #js {:height (or (:height shape) "100%")
|
||||
:width (or (:width shape) "100%")
|
||||
:display "flex"}]
|
||||
(cond-> base
|
||||
(= valign "top") (obj/set! "alignItems" "flex-start")
|
||||
(= valign "center") (obj/set! "alignItems" "center")
|
||||
(= valign "bottom") (obj/set! "alignItems" "flex-end")
|
||||
([props] (generate-root-styles (clj->js (obj/get props "node")) props))
|
||||
([data props]
|
||||
(let [valign (obj/get data "vertical-align" "top")
|
||||
talign (obj/get data "text-align" "flex-start")
|
||||
shape (obj/get props "shape")
|
||||
base #js {:height (or (:height shape) "100%")
|
||||
:width (or (:width shape) "100%")
|
||||
:display "flex"}]
|
||||
(cond-> base
|
||||
(= valign "top") (obj/set! "alignItems" "flex-start")
|
||||
(= valign "center") (obj/set! "alignItems" "center")
|
||||
(= valign "bottom") (obj/set! "alignItems" "flex-end")
|
||||
|
||||
(= talign "left") (obj/set! "justifyContent" "flex-start")
|
||||
(= talign "center") (obj/set! "justifyContent" "center")
|
||||
(= talign "right") (obj/set! "justifyContent" "flex-end")
|
||||
(= talign "justify") (obj/set! "justifyContent" "stretch"))))
|
||||
(= talign "left") (obj/set! "justifyContent" "flex-start")
|
||||
(= talign "center") (obj/set! "justifyContent" "center")
|
||||
(= talign "right") (obj/set! "justifyContent" "flex-end")
|
||||
(= talign "justify") (obj/set! "justifyContent" "stretch")))))
|
||||
|
||||
(defn generate-paragraph-set-styles
|
||||
[data props]
|
||||
;; The position absolute is used so the paragraph is "outside"
|
||||
;; the normal layout and can grow outside its parent
|
||||
;; We use this element to measure the size of the text
|
||||
(let [base #js {:display "inline-block"}]
|
||||
base))
|
||||
([props] (generate-paragraph-set-styles nil props))
|
||||
([data props]
|
||||
;; The position absolute is used so the paragraph is "outside"
|
||||
;; the normal layout and can grow outside its parent
|
||||
;; We use this element to measure the size of the text
|
||||
(let [base #js {:display "inline-block"}]
|
||||
base)))
|
||||
|
||||
(defn generate-paragraph-styles
|
||||
[data props]
|
||||
(let [shape (obj/get props "shape")
|
||||
grow-type (:grow-type shape)
|
||||
base #js {:fontSize "14px"
|
||||
:margin "inherit"
|
||||
:lineHeight "1.2"}
|
||||
lh (obj/get data "line-height")
|
||||
ta (obj/get data "text-align")]
|
||||
(cond-> base
|
||||
ta (obj/set! "textAlign" ta)
|
||||
lh (obj/set! "lineHeight" lh)
|
||||
(= grow-type :auto-width) (obj/set! "whiteSpace" "pre"))))
|
||||
([props] (generate-paragraph-styles (clj->js (obj/get props "node")) props))
|
||||
([data props]
|
||||
(let [shape (obj/get props "shape")
|
||||
grow-type (:grow-type shape)
|
||||
base #js {:fontSize "14px"
|
||||
:margin "inherit"
|
||||
:lineHeight "1.2"}
|
||||
lh (obj/get data "line-height")
|
||||
ta (obj/get data "text-align")]
|
||||
(cond-> base
|
||||
ta (obj/set! "textAlign" ta)
|
||||
lh (obj/set! "lineHeight" lh)
|
||||
(= grow-type :auto-width) (obj/set! "whiteSpace" "pre")))))
|
||||
|
||||
(defn generate-text-styles
|
||||
[data props]
|
||||
(let [letter-spacing (obj/get data "letter-spacing")
|
||||
text-decoration (obj/get data "text-decoration")
|
||||
text-transform (obj/get data "text-transform")
|
||||
line-height (obj/get data "line-height")
|
||||
([props] (generate-text-styles (clj->js (obj/get props "node")) props))
|
||||
([data props]
|
||||
(let [letter-spacing (obj/get data "letter-spacing")
|
||||
text-decoration (obj/get data "text-decoration")
|
||||
text-transform (obj/get data "text-transform")
|
||||
line-height (obj/get data "line-height")
|
||||
|
||||
font-id (obj/get data "font-id" (:font-id ut/default-text-attrs))
|
||||
font-variant-id (obj/get data "font-variant-id")
|
||||
font-id (obj/get data "font-id" (:font-id ut/default-text-attrs))
|
||||
font-variant-id (obj/get data "font-variant-id")
|
||||
|
||||
font-family (obj/get data "font-family")
|
||||
font-size (obj/get data "font-size")
|
||||
font-family (obj/get data "font-family")
|
||||
font-size (obj/get data "font-size")
|
||||
|
||||
;; Old properties for backwards compatibility
|
||||
fill (obj/get data "fill")
|
||||
opacity (obj/get data "opacity" 1)
|
||||
;; Old properties for backwards compatibility
|
||||
fill (obj/get data "fill")
|
||||
opacity (obj/get data "opacity" 1)
|
||||
|
||||
fill-color (obj/get data "fill-color" fill)
|
||||
fill-opacity (obj/get data "fill-opacity" opacity)
|
||||
fill-color-gradient (obj/get data "fill-color-gradient" nil)
|
||||
fill-color-gradient (when fill-color-gradient
|
||||
(-> (js->clj fill-color-gradient :keywordize-keys true)
|
||||
(update :type keyword)))
|
||||
fill-color (obj/get data "fill-color" fill)
|
||||
fill-opacity (obj/get data "fill-opacity" opacity)
|
||||
fill-color-gradient (obj/get data "fill-color-gradient" nil)
|
||||
fill-color-gradient (when fill-color-gradient
|
||||
(-> (js->clj fill-color-gradient :keywordize-keys true)
|
||||
(update :type keyword)))
|
||||
|
||||
;; Uncomment this to allow to remove text colors. This could break the texts that already exist
|
||||
;;[r g b a] (if (nil? fill-color)
|
||||
;; [0 0 0 0] ;; Transparent color
|
||||
;; (uc/hex->rgba fill-color fill-opacity))
|
||||
;; Uncomment this to allow to remove text colors. This could break the texts that already exist
|
||||
;;[r g b a] (if (nil? fill-color)
|
||||
;; [0 0 0 0] ;; Transparent color
|
||||
;; (uc/hex->rgba fill-color fill-opacity))
|
||||
|
||||
[r g b a] (uc/hex->rgba fill-color fill-opacity)
|
||||
[r g b a] (uc/hex->rgba fill-color fill-opacity)
|
||||
|
||||
text-color (if fill-color-gradient
|
||||
(uc/gradient->css (js->clj fill-color-gradient))
|
||||
(str/format "rgba(%s, %s, %s, %s)" r g b a))
|
||||
text-color (if fill-color-gradient
|
||||
(uc/gradient->css (js->clj fill-color-gradient))
|
||||
(str/format "rgba(%s, %s, %s, %s)" r g b a))
|
||||
|
||||
fontsdb (deref fonts/fontsdb)
|
||||
fontsdb (deref fonts/fontsdb)
|
||||
|
||||
base #js {:textDecoration text-decoration
|
||||
:textTransform text-transform
|
||||
:lineHeight (or line-height "inherit")
|
||||
:color text-color
|
||||
"--text-color" text-color}]
|
||||
base #js {:textDecoration text-decoration
|
||||
:textTransform text-transform
|
||||
:lineHeight (or line-height "inherit")
|
||||
:color text-color
|
||||
"--text-color" text-color}]
|
||||
|
||||
(when (and (string? letter-spacing)
|
||||
(pos? (alength letter-spacing)))
|
||||
(obj/set! base "letterSpacing" (str letter-spacing "px")))
|
||||
(when (and (string? letter-spacing)
|
||||
(pos? (alength letter-spacing)))
|
||||
(obj/set! base "letterSpacing" (str letter-spacing "px")))
|
||||
|
||||
(when (and (string? font-size)
|
||||
(pos? (alength font-size)))
|
||||
(obj/set! base "fontSize" (str font-size "px")))
|
||||
(when (and (string? font-size)
|
||||
(pos? (alength font-size)))
|
||||
(obj/set! base "fontSize" (str font-size "px")))
|
||||
|
||||
(when (and (string? font-id)
|
||||
(pos? (alength font-id)))
|
||||
(fonts/ensure-loaded! font-id)
|
||||
(let [font (get fontsdb font-id)]
|
||||
(let [font-family (or (:family font)
|
||||
(obj/get data "fontFamily"))
|
||||
font-variant (d/seek #(= font-variant-id (:id %))
|
||||
(:variants font))
|
||||
font-style (or (:style font-variant)
|
||||
(obj/get data "fontStyle"))
|
||||
font-weight (or (:weight font-variant)
|
||||
(obj/get data "fontWeight"))]
|
||||
(obj/set! base "fontFamily" font-family)
|
||||
(obj/set! base "fontStyle" font-style)
|
||||
(obj/set! base "fontWeight" font-weight))))
|
||||
(when (and (string? font-id)
|
||||
(pos? (alength font-id)))
|
||||
(fonts/ensure-loaded! font-id)
|
||||
(let [font (get fontsdb font-id)]
|
||||
(let [font-family (or (:family font)
|
||||
(obj/get data "fontFamily"))
|
||||
font-variant (d/seek #(= font-variant-id (:id %))
|
||||
(:variants font))
|
||||
font-style (or (:style font-variant)
|
||||
(obj/get data "fontStyle"))
|
||||
font-weight (or (:weight font-variant)
|
||||
(obj/get data "fontWeight"))]
|
||||
(obj/set! base "fontFamily" font-family)
|
||||
(obj/set! base "fontStyle" font-style)
|
||||
(obj/set! base "fontWeight" font-weight))))
|
||||
|
||||
|
||||
base))
|
||||
base)))
|
||||
|
|
|
@ -15,35 +15,36 @@
|
|||
[app.common.math :as mth]
|
||||
[app.common.pages :as cp]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.util.geom.grid :as gg]))
|
||||
[app.util.geom.grid :as gg]
|
||||
[app.common.uuid :as uuid]))
|
||||
|
||||
(mf/defc square-grid [{:keys [frame zoom grid] :as props}]
|
||||
(let [{:keys [color size] :as params} (-> grid :params)
|
||||
(let [grid-id (mf/use-memo #(uuid/next))
|
||||
{:keys [color size] :as params} (-> grid :params)
|
||||
{color-value :color color-opacity :opacity} (-> grid :params :color)
|
||||
;; Support for old color format
|
||||
color-value (or color-value (:value (get-in grid [:params :color :value])))
|
||||
{frame-width :width frame-height :height :keys [x y]} frame]
|
||||
(when (> size 0)
|
||||
[:g.grid
|
||||
[:*
|
||||
(for [xs (range size frame-width size)]
|
||||
[:line {:key (str (:id frame) "-y-" xs)
|
||||
:x1 (mth/round (+ x xs))
|
||||
:y1 (mth/round y)
|
||||
:x2 (mth/round (+ x xs))
|
||||
:y2 (mth/round (+ y frame-height))
|
||||
:style {:stroke color-value
|
||||
:stroke-opacity color-opacity
|
||||
:stroke-width (str (/ 1 zoom))}}])
|
||||
(for [ys (range size frame-height size)]
|
||||
[:line {:key (str (:id frame) "-x-" ys)
|
||||
:x1 (mth/round x)
|
||||
:y1 (mth/round (+ y ys))
|
||||
:x2 (mth/round (+ x frame-width))
|
||||
:y2 (mth/round (+ y ys))
|
||||
:style {:stroke color-value
|
||||
:stroke-opacity color-opacity
|
||||
:stroke-width (str (/ 1 zoom))}}])]])))
|
||||
color-value (or color-value (:value (get-in grid [:params :color :value])))]
|
||||
|
||||
[:g.grid
|
||||
[:defs
|
||||
[:pattern {:id grid-id
|
||||
:x (:x frame)
|
||||
:y (:y frame)
|
||||
:width size
|
||||
:height size
|
||||
:pattern-units "userSpaceOnUse"}
|
||||
[:path {:d (str "M " size " " 0 " "
|
||||
"L " 0 " " 0 " " 0 " " size " ")
|
||||
:style {:fill "none"
|
||||
:stroke color-value
|
||||
:stroke-opacity color-opacity
|
||||
:stroke-width (str (/ 1 zoom))}}]]]
|
||||
|
||||
[:rect {:x (:x frame)
|
||||
:y (:y frame)
|
||||
:width (:width frame)
|
||||
:height (:height frame)
|
||||
:fill (str "url(#" grid-id ")")}]]))
|
||||
|
||||
(mf/defc layout-grid [{:keys [key frame zoom grid]}]
|
||||
(let [{color-value :color color-opacity :opacity} (-> grid :params :color)
|
||||
|
|
|
@ -359,6 +359,7 @@
|
|||
:zoom zoom}])]))
|
||||
|
||||
(mf/defc selection-handlers
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [selected edition zoom show-distances] :as props}]
|
||||
(let [;; We need remove posible nil values because on shape
|
||||
;; deletion many shape will reamin selected and deleted
|
||||
|
|
|
@ -46,17 +46,6 @@
|
|||
(def image-wrapper (common/generic-wrapper-factory image/image-shape))
|
||||
(def rect-wrapper (common/generic-wrapper-factory rect/rect-shape))
|
||||
|
||||
(defn- shape-wrapper-memo-equals?
|
||||
[np op]
|
||||
(let [n-shape (obj/get np "shape")]
|
||||
(if (= (:type n-shape) :group)
|
||||
false
|
||||
(let [o-shape (obj/get op "shape")
|
||||
n-frame (obj/get np "frame")
|
||||
o-frame (obj/get op "frame")]
|
||||
(and (identical? n-shape o-shape)
|
||||
(identical? n-frame o-frame))))))
|
||||
|
||||
(defn make-is-moving-ref
|
||||
[id]
|
||||
(fn []
|
||||
|
@ -66,7 +55,7 @@
|
|||
(l/derived check-moving refs/workspace-local))))
|
||||
|
||||
(mf/defc shape-wrapper
|
||||
{::mf/wrap [#(mf/memo' % shape-wrapper-memo-equals?)]
|
||||
{::mf/wrap [#(mf/memo' % (mf/check-props ["shape" "frame"]))]
|
||||
::mf/wrap-props false}
|
||||
[props]
|
||||
(let [shape (obj/get props "shape")
|
||||
|
@ -77,8 +66,6 @@
|
|||
opts #js {:shape shape
|
||||
:frame frame}
|
||||
|
||||
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)
|
||||
svg-element? (and (= (:type shape) :svg-raw)
|
||||
|
@ -88,8 +75,7 @@
|
|||
(when (and shape (not (:hidden shape)))
|
||||
[:*
|
||||
(if-not svg-element?
|
||||
[:g.shape-wrapper {:style {:display (when hide-moving? "none")
|
||||
:cursor (if alt? cur/duplicate nil)}}
|
||||
[:g.shape-wrapper {:style {:display (when hide-moving? "none")}}
|
||||
(case (:type shape)
|
||||
:path [:> path/path-wrapper opts]
|
||||
:text [:> text/text-wrapper opts]
|
||||
|
|
|
@ -25,24 +25,6 @@
|
|||
[rumext.alpha :as mf]
|
||||
[app.main.ui.context :as muc]))
|
||||
|
||||
(defn- frame-wrapper-factory-equals?
|
||||
[np op]
|
||||
(let [n-shape (aget np "shape")
|
||||
o-shape (aget op "shape")
|
||||
n-objs (aget np "objects")
|
||||
o-objs (aget op "objects")
|
||||
|
||||
ids (:shapes n-shape)]
|
||||
(and (identical? n-shape o-shape)
|
||||
(loop [id (first ids)
|
||||
ids (rest ids)]
|
||||
(if (nil? id)
|
||||
true
|
||||
(if (identical? (get n-objs id)
|
||||
(get o-objs id))
|
||||
(recur (first ids) (rest ids))
|
||||
false))))))
|
||||
|
||||
(defn use-select-shape [{:keys [id]} edition]
|
||||
(mf/use-callback
|
||||
(mf/deps id edition)
|
||||
|
@ -121,7 +103,7 @@
|
|||
[shape-wrapper]
|
||||
(let [frame-shape (frame/frame-shape shape-wrapper)]
|
||||
(mf/fnc frame-wrapper
|
||||
{::mf/wrap [#(mf/memo' % frame-wrapper-factory-equals?) custom-deferred]
|
||||
{::mf/wrap [#(mf/memo' % (mf/check-props ["shape" "objects"])) custom-deferred]
|
||||
::mf/wrap-props false}
|
||||
[props]
|
||||
(let [shape (unchecked-get props "shape")
|
||||
|
|
|
@ -21,15 +21,6 @@
|
|||
[app.common.geom.shapes :as gsh]
|
||||
[app.util.debug :refer [debug?]]))
|
||||
|
||||
(defn- group-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 use-double-click [{:keys [id]}]
|
||||
(mf/use-callback
|
||||
(mf/deps id)
|
||||
|
@ -42,7 +33,7 @@
|
|||
[shape-wrapper]
|
||||
(let [group-shape (group/group-shape shape-wrapper)]
|
||||
(mf/fnc group-wrapper
|
||||
{::mf/wrap [#(mf/memo' % group-wrapper-factory-equals?)]
|
||||
{::mf/wrap [#(mf/memo' % (mf/check-props ["shape" "frame"]))]
|
||||
::mf/wrap-props false}
|
||||
[props]
|
||||
(let [shape (unchecked-get props "shape")
|
||||
|
|
|
@ -21,20 +21,11 @@
|
|||
;; 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 [#(mf/memo' % (mf/check-props ["shape" "frame"]))]
|
||||
::mf/wrap-props false}
|
||||
[props]
|
||||
(let [shape (unchecked-get props "shape")
|
||||
|
|
|
@ -34,46 +34,33 @@
|
|||
|
||||
;; --- Events
|
||||
|
||||
(defn use-double-click [{:keys [id]} selected?]
|
||||
(defn use-double-click [{:keys [id]}]
|
||||
(mf/use-callback
|
||||
(mf/deps id selected?)
|
||||
(mf/deps id)
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(dom/prevent-default event)
|
||||
(when selected?
|
||||
(st/emit! (dw/start-edition-mode id))))))
|
||||
(st/emit! (dw/start-edition-mode id)))))
|
||||
|
||||
;; --- Text Wrapper for workspace
|
||||
|
||||
(mf/defc text-wrapper
|
||||
(mf/defc text-static-content
|
||||
[{:keys [shape]}]
|
||||
[:& text/text-shape {:shape shape
|
||||
:grow-type (:grow-type shape)}])
|
||||
|
||||
(mf/defc text-resize-content
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [{:keys [id name x y width height grow-type] :as shape} (unchecked-get props "shape")
|
||||
ghost? (mf/use-ctx muc/ghost-ctx)
|
||||
selected-iref (mf/use-memo (mf/deps (:id shape))
|
||||
#(refs/make-selected-ref (:id shape)))
|
||||
selected? (mf/deref selected-iref)
|
||||
edition (mf/deref refs/selected-edition)
|
||||
current-transform (mf/deref refs/current-transform)
|
||||
|
||||
render-editor (mf/use-state false)
|
||||
|
||||
edition? (= edition id)
|
||||
embed-resources? (mf/use-ctx muc/embed-ctx)
|
||||
|
||||
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)
|
||||
handle-double-click (use-double-click shape selected?)
|
||||
|
||||
(let [shape (obj/get props "shape")
|
||||
{:keys [id name x y grow-type]} shape
|
||||
paragraph-ref (mf/use-state nil)
|
||||
|
||||
handle-resize-text
|
||||
(mf/use-callback
|
||||
(mf/deps id)
|
||||
(fn [entries]
|
||||
(when (and (not ghost?) (seq entries))
|
||||
(when (seq entries)
|
||||
;; RequestAnimationFrame so the "loop limit error" error is not thrown
|
||||
;; https://stackoverflow.com/questions/49384120/resizeobserver-loop-limit-exceeded
|
||||
(timers/raf
|
||||
|
@ -97,24 +84,41 @@
|
|||
(mf/use-effect
|
||||
(mf/deps @paragraph-ref handle-resize-text grow-type)
|
||||
(fn []
|
||||
(when (not ghost?)
|
||||
(when-let [paragraph-node @paragraph-ref]
|
||||
(let [observer (js/ResizeObserver. handle-resize-text)]
|
||||
(log/debug :msg "Attach resize observer" :shape-id id :shape-name name)
|
||||
(.observe observer paragraph-node)
|
||||
#(.disconnect observer))))))
|
||||
(when-let [paragraph-node @paragraph-ref]
|
||||
(let [observer (js/ResizeObserver. handle-resize-text)]
|
||||
(log/debug :msg "Attach resize observer" :shape-id id :shape-name name)
|
||||
(.observe observer paragraph-node)
|
||||
#(.disconnect observer)))))
|
||||
|
||||
[:& text/text-shape {:ref text-ref-cb
|
||||
:shape shape
|
||||
:grow-type (:grow-type shape)}]))
|
||||
|
||||
(mf/defc text-wrapper
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [{:keys [id x y width height] :as shape} (unchecked-get props "shape")
|
||||
ghost? (mf/use-ctx muc/ghost-ctx)
|
||||
edition (mf/deref refs/selected-edition)
|
||||
edition? (= edition id)
|
||||
|
||||
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)
|
||||
handle-double-click (use-double-click shape)]
|
||||
|
||||
[:> shape-container {:shape shape}
|
||||
;; We keep hidden the shape when we're editing so it keeps track of the size
|
||||
;; and updates the selrect acordingly
|
||||
[:g.text-shape {:opacity (when edition? 0)
|
||||
:pointer-events "none"}
|
||||
[:& text/text-shape {:key (str "text-shape" (:id shape))
|
||||
:ref text-ref-cb
|
||||
:shape shape
|
||||
:selected? selected?
|
||||
:grow-type (:grow-type shape)}]]
|
||||
|
||||
(if ghost?
|
||||
[:& text-static-content {:shape shape}]
|
||||
[:& text-resize-content {:shape shape}])]
|
||||
|
||||
|
||||
(when (and (not ghost?) edition?)
|
||||
[:& editor/text-shape-edit {:key (str "editor" (:id shape))
|
||||
:shape shape}])
|
||||
|
|
|
@ -237,40 +237,13 @@
|
|||
:objects objects
|
||||
:key (:id item)}]))])]))
|
||||
|
||||
(defn frame-wrapper-memo-equals?
|
||||
[oprops nprops]
|
||||
(let [new-sel (unchecked-get nprops "selected")
|
||||
old-sel (unchecked-get oprops "selected")
|
||||
new-itm (unchecked-get nprops "item")
|
||||
old-itm (unchecked-get oprops "item")
|
||||
new-idx (unchecked-get nprops "index")
|
||||
old-idx (unchecked-get oprops "index")
|
||||
new-obs (unchecked-get nprops "objects")
|
||||
old-obs (unchecked-get oprops "objects")]
|
||||
(and (= new-itm old-itm)
|
||||
(identical? new-idx old-idx)
|
||||
(let [childs (cp/get-children (:id new-itm) new-obs)
|
||||
childs' (conj childs (:id new-itm))]
|
||||
(and (or (= new-sel old-sel)
|
||||
(not (or (boolean (some new-sel childs'))
|
||||
(boolean (some old-sel childs')))))
|
||||
(loop [ids (rest childs)
|
||||
id (first childs)]
|
||||
(if (nil? id)
|
||||
true
|
||||
(if (= (get new-obs id)
|
||||
(get old-obs id))
|
||||
(recur (rest ids)
|
||||
(first ids))
|
||||
false))))))))
|
||||
|
||||
;; This components is a piece for sharding equality check between top
|
||||
;; level frames and try to avoid rerender frames that are does not
|
||||
;; affected by the selected set.
|
||||
|
||||
(mf/defc frame-wrapper
|
||||
{::mf/wrap-props false
|
||||
::mf/wrap [#(mf/memo' % frame-wrapper-memo-equals?)
|
||||
::mf/wrap [#(mf/memo' % (mf/check-props ["selected" "item" "index" "objects"]))
|
||||
#(mf/deferred % ts/idle-then-raf)]}
|
||||
[props]
|
||||
[:> layer-item props])
|
||||
|
|
|
@ -28,20 +28,8 @@
|
|||
:fill-color-ref-file
|
||||
:fill-color-gradient])
|
||||
|
||||
(defn- fill-menu-props-equals?
|
||||
[np op]
|
||||
(let [new-ids (obj/get np "ids")
|
||||
old-ids (obj/get op "ids")
|
||||
new-editor (obj/get np "editor")
|
||||
old-editor (obj/get op "editor")
|
||||
new-values (obj/get np "values")
|
||||
old-values (obj/get op "values")]
|
||||
(and (= new-ids old-ids)
|
||||
(= new-editor old-editor)
|
||||
(every? #(identical? (% new-values) (% old-values)) fill-attrs))))
|
||||
|
||||
(mf/defc fill-menu
|
||||
{::mf/wrap [#(mf/memo' % fill-menu-props-equals?)]}
|
||||
{::mf/wrap [#(mf/memo' % (mf/check-props ["ids" "editor" "values"]))]}
|
||||
[{:keys [ids type values editor] :as props}]
|
||||
(let [locale (mf/deref i18n/locale)
|
||||
show? (or (not (nil? (:fill-color values)))
|
||||
|
|
|
@ -154,7 +154,7 @@
|
|||
(reduce extract-attrs [] shapes)))
|
||||
|
||||
(mf/defc options
|
||||
{::mf/wrap [mf/memo]
|
||||
{::mf/wrap [#(mf/memo' % (mf/check-props ["shape" "shapes-with-children"]))]
|
||||
::mf/wrap-props false}
|
||||
[props]
|
||||
(let [shapes (unchecked-get props "shapes")
|
||||
|
|
|
@ -33,15 +33,6 @@
|
|||
:stroke-opacity
|
||||
:stroke-color-gradient])
|
||||
|
||||
(defn- stroke-menu-props-equals?
|
||||
[np op]
|
||||
(let [new-ids (obj/get np "ids")
|
||||
old-ids (obj/get op "ids")
|
||||
new-values (obj/get np "values")
|
||||
old-values (obj/get op "values")]
|
||||
(and (= new-ids old-ids)
|
||||
(every? #(identical? (% new-values) (% old-values)) stroke-attrs))))
|
||||
|
||||
(defn- width->string [width]
|
||||
(if (= width :multiple)
|
||||
""
|
||||
|
@ -55,7 +46,7 @@
|
|||
(pr-str value)))
|
||||
|
||||
(mf/defc stroke-menu
|
||||
{::mf/wrap [#(mf/memo' % stroke-menu-props-equals?)]}
|
||||
{::mf/wrap [#(mf/memo' % (mf/check-props ["ids" "values" "type"]))]}
|
||||
[{:keys [ids type values] :as props}]
|
||||
(let [locale (i18n/use-locale)
|
||||
label (case type
|
||||
|
|
|
@ -267,11 +267,9 @@
|
|||
(mf/defc snap-distances
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [layout (unchecked-get props "layout")
|
||||
page-id (unchecked-get props "page-id")
|
||||
(let [page-id (unchecked-get props "page-id")
|
||||
zoom (unchecked-get props "zoom")
|
||||
selected (unchecked-get props "selected")
|
||||
transform (unchecked-get props "transform")
|
||||
selected-shapes (mf/deref (refs/objects-by-id selected))
|
||||
frame-id (-> selected-shapes first :frame-id)
|
||||
frame (mf/deref (refs/object-by-id frame-id))
|
||||
|
@ -280,23 +278,20 @@
|
|||
update-shape (fn [shape] (-> shape
|
||||
(update :modifiers merge (:modifiers local))
|
||||
gsh/transform-shape))]
|
||||
(when (and (contains? layout :dynamic-alignment)
|
||||
(= transform :move)
|
||||
(not (empty? selected)))
|
||||
(let [selrect (->> selected-shapes (map update-shape) gsh/selection-rect)
|
||||
key (->> selected (map str) (str/join "-"))]
|
||||
[:g.distance
|
||||
[:& shape-distance
|
||||
{:selrect selrect
|
||||
:page-id page-id
|
||||
:frame frame
|
||||
:zoom zoom
|
||||
:coord :x
|
||||
:selected selected}]
|
||||
[:& shape-distance
|
||||
{:selrect selrect
|
||||
:page-id page-id
|
||||
:frame frame
|
||||
:zoom zoom
|
||||
:coord :y
|
||||
:selected selected}]]))))
|
||||
(let [selrect (->> selected-shapes (map update-shape) gsh/selection-rect)
|
||||
key (->> selected (map str) (str/join "-"))]
|
||||
[:g.distance
|
||||
[:& shape-distance
|
||||
{:selrect selrect
|
||||
:page-id page-id
|
||||
:frame frame
|
||||
:zoom zoom
|
||||
:coord :x
|
||||
:selected selected}]
|
||||
[:& shape-distance
|
||||
{:selrect selrect
|
||||
:page-id page-id
|
||||
:frame frame
|
||||
:zoom zoom
|
||||
:coord :y
|
||||
:selected selected}]])))
|
||||
|
|
|
@ -188,7 +188,8 @@
|
|||
:edition edition}]]))
|
||||
|
||||
(mf/defc ghost-frames
|
||||
{::mf/wrap-props false}
|
||||
{::mf/wrap [mf/memo]
|
||||
::mf/wrap-props false}
|
||||
[props]
|
||||
(let [modifiers (obj/get props "modifiers")
|
||||
selected (obj/get props "selected")
|
||||
|
@ -249,6 +250,7 @@
|
|||
(gsh/selection-rect))
|
||||
|
||||
alt? (mf/use-state false)
|
||||
cursor (mf/use-state cur/pointer-inner)
|
||||
viewport-ref (mf/use-ref nil)
|
||||
zoom-view-ref (mf/use-ref nil)
|
||||
last-position (mf/use-var nil)
|
||||
|
@ -260,6 +262,13 @@
|
|||
drawing-path? (and edition (= :draw (get-in edit-path [edition :edit-mode])))
|
||||
zoom (or zoom 1)
|
||||
|
||||
show-grids? (contains? layout :display-grid)
|
||||
show-snap-points? (and (contains? layout :dynamic-alignment)
|
||||
(or drawing-obj (:transform local)))
|
||||
show-snap-distance? (and (contains? layout :dynamic-alignment)
|
||||
(= (:transform local) :move)
|
||||
(not (empty? selected)))
|
||||
|
||||
on-mouse-down
|
||||
(mf/use-callback
|
||||
(mf/deps drawing-tool edition)
|
||||
|
@ -590,6 +599,28 @@
|
|||
;; We schedule the event so it fires after `initialize-page` event
|
||||
(timers/schedule #(st/emit! (dw/initialize-viewport size))))))
|
||||
|
||||
;; This change is in an effect to minimize the sideffects of the cursor chaning
|
||||
;; Changing a cursor will produce a "reflow" so we defer it until the component is rendered
|
||||
(mf/use-layout-effect
|
||||
(mf/deps @cursor @alt? panning drawing-tool drawing-path?)
|
||||
(fn []
|
||||
(let [new-cursor
|
||||
(cond
|
||||
panning cur/hand
|
||||
(= drawing-tool :comments) cur/comments
|
||||
(= drawing-tool :frame) cur/create-artboard
|
||||
(= drawing-tool :rect) cur/create-rectangle
|
||||
(= drawing-tool :circle) cur/create-ellipse
|
||||
(or (= drawing-tool :path) drawing-path?) cur/pen
|
||||
(= drawing-tool :curve) cur/pencil
|
||||
drawing-tool cur/create-shape
|
||||
@alt? cur/duplicate
|
||||
:else cur/pointer-inner)]
|
||||
|
||||
(when (not= @cursor new-cursor)
|
||||
(timers/raf
|
||||
#(reset! cursor new-cursor))))))
|
||||
|
||||
(mf/use-layout-effect (mf/deps layout) on-resize)
|
||||
(hooks/use-stream ms/keyboard-alt #(reset! alt? %))
|
||||
|
||||
|
@ -619,16 +650,7 @@
|
|||
:view-box (format-viewbox vbox)
|
||||
:ref viewport-ref
|
||||
:class (when drawing-tool "drawing")
|
||||
:style {:cursor (cond
|
||||
panning cur/hand
|
||||
(= drawing-tool :comments) cur/comments
|
||||
(= drawing-tool :frame) cur/create-artboard
|
||||
(= drawing-tool :rect) cur/create-rectangle
|
||||
(= drawing-tool :circle) cur/create-ellipse
|
||||
(or (= drawing-tool :path) drawing-path?) cur/pen
|
||||
(= drawing-tool :curve) cur/pencil
|
||||
drawing-tool cur/create-shape
|
||||
:else cur/pointer-inner)
|
||||
:style {:cursor @cursor
|
||||
:background-color (get options :background "#E8E9EA")}
|
||||
:on-context-menu on-context-menu
|
||||
:on-click on-click
|
||||
|
@ -671,22 +693,24 @@
|
|||
:tool drawing-tool
|
||||
:modifiers (:modifiers local)}])
|
||||
|
||||
(when (contains? layout :display-grid)
|
||||
(when show-grids?
|
||||
[:& frame-grid {:zoom zoom}])
|
||||
|
||||
[:& snap-points {:layout layout
|
||||
:transform (:transform local)
|
||||
:drawing drawing-obj
|
||||
:zoom zoom
|
||||
:page-id page-id
|
||||
:selected selected
|
||||
:local local}]
|
||||
(when show-snap-points?
|
||||
[:& snap-points {:layout layout
|
||||
:transform (:transform local)
|
||||
:drawing drawing-obj
|
||||
:zoom zoom
|
||||
:page-id page-id
|
||||
:selected selected
|
||||
:local local}])
|
||||
|
||||
[:& snap-distances {:layout layout
|
||||
:zoom zoom
|
||||
:transform (:transform local)
|
||||
:selected selected
|
||||
:page-id page-id}]
|
||||
(when show-snap-distance?
|
||||
[:& snap-distances {:layout layout
|
||||
:zoom zoom
|
||||
:transform (:transform local)
|
||||
:selected selected
|
||||
:page-id page-id}])
|
||||
|
||||
(when tooltip
|
||||
[:& cursor-tooltip {:zoom zoom :tooltip tooltip}])]
|
||||
|
@ -697,7 +721,9 @@
|
|||
[:& interactions {:selected selected}])]]))
|
||||
|
||||
|
||||
(mf/defc viewport-actions []
|
||||
(mf/defc viewport-actions
|
||||
{::mf/wrap [mf/memo]}
|
||||
[]
|
||||
(let [edition (mf/deref refs/selected-edition)
|
||||
selected (mf/deref refs/selected-objects)
|
||||
shape (-> selected first)]
|
||||
|
|
|
@ -93,12 +93,31 @@
|
|||
(rec-fn {} node)))
|
||||
|
||||
|
||||
(defn content->nodes [node]
|
||||
(loop [result (transient [])
|
||||
curr node
|
||||
pending (transient [])]
|
||||
|
||||
(let [result (conj! result curr)]
|
||||
;; Adds children to the pending list
|
||||
(let [children (:children curr)
|
||||
pending (loop [child (first children)
|
||||
children (rest children)
|
||||
pending pending]
|
||||
(if child
|
||||
(recur (first children)
|
||||
(rest children)
|
||||
(conj! pending child))
|
||||
pending))]
|
||||
|
||||
(if (= 0 (count pending))
|
||||
(persistent! result)
|
||||
;; Iterates with the next value in pending
|
||||
(let [next (get pending (dec (count pending)))]
|
||||
(recur result next (pop! pending))))))))
|
||||
|
||||
(defn get-text-attrs-multi
|
||||
[node attrs]
|
||||
(let [rec-fn
|
||||
(fn rec-fn [current node]
|
||||
(let [current (reduce rec-fn current (:children node []))]
|
||||
(get-attrs-multi [current node] attrs)))]
|
||||
(merge (select-keys default-text-attrs attrs)
|
||||
(rec-fn {} node))))
|
||||
(let [nodes (content->nodes node)]
|
||||
(get-attrs-multi nodes attrs)))
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue