mirror of
https://github.com/penpot/penpot.git
synced 2025-04-16 17:01:33 -05:00
Merge pull request #1819 from penpot/alotor-performance-improvements
Frames performance improvements
This commit is contained in:
commit
b239a9b09e
39 changed files with 1225 additions and 951 deletions
|
@ -133,6 +133,7 @@
|
|||
(dm/export gtr/transform-bounds)
|
||||
(dm/export gtr/modifiers->transform)
|
||||
(dm/export gtr/empty-modifiers?)
|
||||
(dm/export gtr/move-position-data)
|
||||
|
||||
;; Constratins
|
||||
(dm/export gct/calc-child-modifiers)
|
||||
|
|
|
@ -139,6 +139,18 @@
|
|||
(:shapes)
|
||||
(keep lookup)))))
|
||||
|
||||
(defn get-frames-ids
|
||||
"Retrieves all frame objects as vector. It is not implemented in
|
||||
function of `get-immediate-children` for performance reasons. This
|
||||
function is executed in the render hot path."
|
||||
[objects]
|
||||
(let [lookup (d/getf objects)
|
||||
xform (comp (keep lookup)
|
||||
(filter frame-shape?)
|
||||
(map :id))]
|
||||
(->> (:shapes (lookup uuid/zero))
|
||||
(into [] xform))))
|
||||
|
||||
(defn get-frames
|
||||
"Retrieves all frame objects as vector. It is not implemented in
|
||||
function of `get-immediate-children` for performance reasons. This
|
||||
|
@ -468,3 +480,25 @@
|
|||
(let [path-split (split-path path)]
|
||||
(merge-path-item (first path-split) name)))
|
||||
|
||||
|
||||
(defn get-frame-objects
|
||||
"Retrieves a new objects map only with the objects under frame-id (with frame-id)"
|
||||
[objects frame-id]
|
||||
(let [ids (concat [frame-id] (get-children-ids objects frame-id))]
|
||||
(select-keys objects ids)))
|
||||
|
||||
(defn objects-by-frame
|
||||
"Returns a map of the `objects` grouped by frame. Every value of the map has
|
||||
the same format as objects id->shape-data"
|
||||
[objects]
|
||||
;; Implemented with transients for performance. 30~50% better
|
||||
(letfn [(process-shape [objects [id shape]]
|
||||
(let [frame-id (if (= :frame (:type shape)) id (:frame-id shape))
|
||||
cur (-> (or (get objects frame-id) (transient {}))
|
||||
(assoc! id shape))]
|
||||
(assoc! objects frame-id cur)))]
|
||||
(d/update-vals
|
||||
(->> objects
|
||||
(reduce process-shape (transient {}))
|
||||
(persistent!))
|
||||
persistent!)))
|
||||
|
|
|
@ -43,6 +43,7 @@
|
|||
[app.main.data.workspace.selection :as dws]
|
||||
[app.main.data.workspace.state-helpers :as wsh]
|
||||
[app.main.data.workspace.svg-upload :as svg]
|
||||
[app.main.data.workspace.thumbnails :as dwth]
|
||||
[app.main.data.workspace.transforms :as dwt]
|
||||
[app.main.data.workspace.undo :as dwu]
|
||||
[app.main.data.workspace.zoom :as dwz]
|
||||
|
@ -195,7 +196,8 @@
|
|||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(if (contains? (get-in state [:workspace-data :pages-index]) page-id)
|
||||
(rx/of (dwp/preload-data-uris))
|
||||
(rx/of (dwp/preload-data-uris)
|
||||
(dwth/watch-state-changes))
|
||||
(let [default-page-id (get-in state [:workspace-data :pages 0])]
|
||||
(rx/of (go-to-page default-page-id)))))
|
||||
|
||||
|
@ -1767,3 +1769,7 @@
|
|||
(dm/export dwz/decrease-zoom)
|
||||
(dm/export dwz/increase-zoom)
|
||||
(dm/export dwz/set-zoom)
|
||||
|
||||
;; Thumbnails
|
||||
(dm/export dwth/update-thumbnail)
|
||||
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.pages :as cp]
|
||||
[app.common.pages.helpers :as cph]
|
||||
[app.common.spec :as us]
|
||||
[app.common.spec.change :as spec.change]
|
||||
[app.common.spec.file :as spec.file]
|
||||
|
@ -26,7 +25,6 @@
|
|||
[app.main.data.workspace.selection :as dws]
|
||||
[app.main.data.workspace.state-helpers :as wsh]
|
||||
[app.main.data.workspace.svg-upload :as svg]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.store :as st]
|
||||
[app.util.http :as http]
|
||||
|
@ -35,7 +33,6 @@
|
|||
[app.util.uri :as uu]
|
||||
[beicon.core :as rx]
|
||||
[cljs.spec.alpha :as s]
|
||||
[clojure.set :as set]
|
||||
[cuerdas.core :as str]
|
||||
[potok.core :as ptk]
|
||||
[promesa.core :as p]
|
||||
|
@ -552,136 +549,6 @@
|
|||
(update-in [:workspace-file :pages] #(filterv (partial not= id) %))
|
||||
(update :workspace-pages dissoc id)))
|
||||
|
||||
(def update-frame-thumbnail? (ptk/type? ::update-frame-thumbnail))
|
||||
|
||||
(defn remove-thumbnails
|
||||
[ids]
|
||||
(ptk/reify ::remove-thumbnails
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
;; Removes the thumbnail while it's regenerated
|
||||
(let [moving? (= :move (get-in state [:workspace-local :transform]))
|
||||
selected? (wsh/lookup-selected state)
|
||||
;; When we're moving the current frame it's safe to keep the thumbnail
|
||||
;; if it's resize we need to remove it immeditely
|
||||
ids (cond->> ids moving? (remove selected?))]
|
||||
|
||||
(if (empty? ids)
|
||||
(rx/empty)
|
||||
(rx/of (dch/update-shapes ids #(dissoc % :thumbnail) {:save-undo? false})))))))
|
||||
|
||||
(defn update-frame-thumbnail
|
||||
[frame-id]
|
||||
(ptk/event ::update-frame-thumbnail {:frame-id frame-id}))
|
||||
|
||||
(defn update-shape-thumbnail
|
||||
"An event that is succeptible to be executed out of the main flow, so
|
||||
it need to correctly handle the situation that there are no page-id
|
||||
or file-is loaded."
|
||||
[shape-id thumbnail-data]
|
||||
(ptk/reify ::update-shape-thumbnail
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(when (and (dwc/initialized? state)
|
||||
(uuid? shape-id))
|
||||
(rx/of (dch/update-shapes [shape-id]
|
||||
#(assoc % :thumbnail thumbnail-data)
|
||||
{:save-undo? false}))))))
|
||||
|
||||
(defn- extract-frame-changes
|
||||
"Process a changes set in a commit to extract the frames that are changing"
|
||||
[[event [old-objects new-objects]]]
|
||||
(let [changes (-> event deref :changes)
|
||||
|
||||
extract-ids
|
||||
(fn [{type :type :as change}]
|
||||
(case type
|
||||
:add-obj [(:id change)]
|
||||
:mod-obj [(:id change)]
|
||||
:del-obj [(:id change)]
|
||||
:reg-objects (:shapes change)
|
||||
:mov-objects (:shapes change)
|
||||
[]))
|
||||
|
||||
get-frame-id
|
||||
(fn [id]
|
||||
(let [shape (or (get new-objects id)
|
||||
(get old-objects id))]
|
||||
|
||||
(or (and (= :frame (:type shape)) id)
|
||||
(:frame-id shape))))
|
||||
|
||||
;; Extracts the frames and then removes nils and the root frame
|
||||
xform (comp (mapcat extract-ids)
|
||||
(map get-frame-id)
|
||||
(remove nil?)
|
||||
(filter #(not= uuid/zero %))
|
||||
(filter #(contains? new-objects %)))]
|
||||
|
||||
(into #{} xform changes)))
|
||||
|
||||
(defn thumbnail-change?
|
||||
"Checks if a event is only updating thumbnails to ignore in the thumbnail generation process"
|
||||
[event]
|
||||
(let [changes (-> event deref :changes)
|
||||
|
||||
is-thumbnail-op?
|
||||
(fn [{type :type attr :attr}]
|
||||
(and (= type :set)
|
||||
(= attr :thumbnail)))
|
||||
|
||||
is-thumbnail-change?
|
||||
(fn [change]
|
||||
(and (= (:type change) :mod-obj)
|
||||
(->> change :operations (every? is-thumbnail-op?))))]
|
||||
|
||||
(->> changes (every? is-thumbnail-change?))))
|
||||
|
||||
(defn watch-state-changes []
|
||||
(ptk/reify ::watch-state-changes
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [stopper (->> stream
|
||||
(rx/filter #(or (= :app.main.data.workspace/finalize-page (ptk/type %))
|
||||
(= ::watch-state-changes (ptk/type %)))))
|
||||
|
||||
objects-stream (->> (rx/concat
|
||||
(rx/of nil)
|
||||
(rx/from-atom refs/workspace-page-objects {:emit-current-value? true}))
|
||||
;; We need to keep the old-objects so we can check the frame for the
|
||||
;; deleted objects
|
||||
(rx/buffer 2 1))
|
||||
|
||||
frame-changes (->> stream
|
||||
(rx/filter dch/commit-changes?)
|
||||
|
||||
;; Async so we wait for additional side-effects of commit-changes
|
||||
(rx/observe-on :async)
|
||||
(rx/filter (comp not thumbnail-change?))
|
||||
(rx/with-latest-from objects-stream)
|
||||
(rx/map extract-frame-changes)
|
||||
(rx/share))
|
||||
|
||||
frames (-> state wsh/lookup-page-objects cph/get-frames)
|
||||
no-thumb-frames (->> frames
|
||||
(filter (comp nil? :thumbnail))
|
||||
(mapv :id))]
|
||||
|
||||
(rx/concat
|
||||
(->> (rx/from no-thumb-frames)
|
||||
(rx/map #(update-frame-thumbnail %)))
|
||||
|
||||
;; We remove the thumbnails immediately but defer their generation
|
||||
(rx/merge
|
||||
(->> frame-changes
|
||||
(rx/take-until stopper)
|
||||
(rx/map #(remove-thumbnails %)))
|
||||
|
||||
(->> frame-changes
|
||||
(rx/take-until stopper)
|
||||
(rx/buffer-until (->> frame-changes (rx/debounce 1000)))
|
||||
(rx/flat-map #(reduce set/union %))
|
||||
(rx/map #(update-frame-thumbnail %)))))))))
|
||||
|
||||
(defn preload-data-uris
|
||||
"Preloads the image data so it's ready when necesary"
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
[app.main.data.workspace.changes :as dch]
|
||||
[app.main.data.workspace.common :as dwc]
|
||||
[app.main.data.workspace.state-helpers :as wsh]
|
||||
[app.main.data.workspace.thumbnails :as dwt]
|
||||
[app.main.data.workspace.zoom :as dwz]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.streams :as ms]
|
||||
|
@ -495,18 +496,30 @@
|
|||
|
||||
id-original (first selected)
|
||||
|
||||
selected (->> changes
|
||||
new-selected (->> changes
|
||||
:redo-changes
|
||||
(filter #(= (:type %) :add-obj))
|
||||
(filter #(selected (:old-id %)))
|
||||
(map #(get-in % [:obj :id]))
|
||||
(into (d/ordered-set)))
|
||||
|
||||
id-duplicated (first selected)]
|
||||
dup-frames (->> changes
|
||||
:redo-changes
|
||||
(filter #(= (:type %) :add-obj))
|
||||
(filter #(selected (:old-id %)))
|
||||
(filter #(= :frame (get-in % [:obj :type])))
|
||||
(map #(vector (:old-id %) (get-in % [:obj :id]))))
|
||||
|
||||
id-duplicated (first new-selected)]
|
||||
|
||||
(rx/concat
|
||||
(->> (rx/from dup-frames)
|
||||
(rx/map (fn [[old-id new-id]] (dwt/duplicate-thumbnail old-id new-id))))
|
||||
|
||||
;; Warning: This order is important for the focus mode.
|
||||
(rx/of (dch/commit-changes changes)
|
||||
(select-shapes selected)
|
||||
(memorize-duplicated id-original id-duplicated)))))))))
|
||||
(rx/of (dch/commit-changes changes)
|
||||
(select-shapes new-selected)
|
||||
(memorize-duplicated id-original id-duplicated))))))))))
|
||||
|
||||
(defn change-hover-state
|
||||
[id value]
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
(:require
|
||||
[app.common.attrs :as attrs]
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.math :as mth]
|
||||
[app.common.pages.helpers :as cph]
|
||||
|
@ -303,83 +304,27 @@
|
|||
(defn not-changed? [old-dim new-dim]
|
||||
(> (mth/abs (- old-dim new-dim)) 0.1))
|
||||
|
||||
(defn resize-text-batch [changes]
|
||||
(ptk/reify ::resize-text-batch
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [page-id (:current-page-id state)
|
||||
objects (get-in state [:workspace-data :pages-index page-id :objects])]
|
||||
(if-not (every? #(contains? objects(first %)) changes)
|
||||
(rx/empty)
|
||||
|
||||
(let [changes-map (->> changes (into {}))
|
||||
ids (keys changes-map)
|
||||
update-fn
|
||||
(fn [shape]
|
||||
(let [[new-width new-height] (get changes-map (:id shape))
|
||||
{:keys [selrect grow-type]} (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)]
|
||||
|
||||
(cond-> shape
|
||||
(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)))))]
|
||||
|
||||
(rx/of (dch/update-shapes ids update-fn {:reg-objects? true}))))))))
|
||||
|
||||
;; 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))))
|
||||
(watch [_ _ _]
|
||||
(letfn [(update-fn [shape]
|
||||
(let [{:keys [selrect grow-type]} 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)]
|
||||
(cond-> shape
|
||||
(and (not-changed? shape-width new-width) (= grow-type :auto-width))
|
||||
(-> (assoc :modifiers modifier-width)
|
||||
(gsh/transform-shape))
|
||||
|
||||
;; Stop buffering after time without resizes
|
||||
stop-buffer (->> resize-events (rx/debounce 100))
|
||||
(and (not-changed? shape-height new-height)
|
||||
(or (= grow-type :auto-height) (= grow-type :auto-width)))
|
||||
(-> (assoc :modifiers modifier-height)
|
||||
(gsh/transform-shape)))))]
|
||||
|
||||
;; Aggregates the resizes so only send the resize when the sizes are stable
|
||||
resize-batch
|
||||
(->> 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 %)))
|
||||
|
||||
;; This stream retrieves the changes of page so we cancel the agregation
|
||||
change-page
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? :app.main.data.workspace/finalize-page))
|
||||
(rx/take 1)
|
||||
(rx/ignore))]
|
||||
|
||||
(if-not (::handling-texts state)
|
||||
(->> (rx/concat
|
||||
(rx/of #(assoc % ::handling-texts true))
|
||||
(rx/race resize-batch change-page)
|
||||
(rx/of #(dissoc % ::handling-texts))))
|
||||
(rx/empty))))))
|
||||
(rx/of (dch/update-shapes [id] update-fn {:reg-objects? true :save-undo? false}))))))
|
||||
|
||||
(defn save-font
|
||||
[data]
|
||||
|
@ -391,3 +336,46 @@
|
|||
(not multiple?)
|
||||
(assoc-in [:workspace-global :default-font] data))))))
|
||||
|
||||
(defn apply-text-modifier
|
||||
[shape {:keys [width height position-data]}]
|
||||
|
||||
(let [modifier-width (when width (gsh/resize-modifiers shape :width width))
|
||||
modifier-height (when height (gsh/resize-modifiers shape :height height))
|
||||
|
||||
new-shape
|
||||
(cond-> shape
|
||||
(some? modifier-width)
|
||||
(-> (assoc :modifiers modifier-width)
|
||||
(gsh/transform-shape))
|
||||
|
||||
(some? modifier-height)
|
||||
(-> (assoc :modifiers modifier-height)
|
||||
(gsh/transform-shape))
|
||||
|
||||
(some? position-data)
|
||||
(assoc :position-data position-data))
|
||||
|
||||
delta-move
|
||||
(gpt/subtract (gpt/point (:selrect new-shape))
|
||||
(gpt/point (:selrect shape)))
|
||||
|
||||
|
||||
new-shape
|
||||
(update new-shape :position-data gsh/move-position-data (:x delta-move) (:y delta-move))]
|
||||
|
||||
|
||||
new-shape))
|
||||
|
||||
(defn update-text-modifier
|
||||
[id props]
|
||||
(ptk/reify ::update-text-modifier
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update-in state [:workspace-text-modifier id] (fnil merge {}) props))))
|
||||
|
||||
(defn remove-text-modifier
|
||||
[id]
|
||||
(ptk/reify ::remove-text-modifier
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(d/dissoc-in state [:workspace-text-modifier id]))))
|
||||
|
|
171
frontend/src/app/main/data/workspace/thumbnails.cljs
Normal file
171
frontend/src/app/main/data/workspace/thumbnails.cljs
Normal file
|
@ -0,0 +1,171 @@
|
|||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.main.data.workspace.thumbnails
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.pages.helpers :as cph]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.data.workspace.changes :as dch]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.store :as st]
|
||||
[beicon.core :as rx]
|
||||
[potok.core :as ptk]))
|
||||
|
||||
(defn force-render-stream
|
||||
"Stream that will inform the frame-wrapper to mount into memory"
|
||||
[id]
|
||||
(->> st/stream
|
||||
(rx/filter (ptk/type? ::force-render))
|
||||
(rx/map deref)
|
||||
(rx/filter #(= % id))
|
||||
(rx/take 1)))
|
||||
|
||||
(defn update-thumbnail
|
||||
"Updates the thumbnail information for the given frame `id`"
|
||||
[id data]
|
||||
(let [lock (uuid/next)]
|
||||
(ptk/reify ::update-thumbnail
|
||||
IDeref
|
||||
(-deref [_] {:id id :data data})
|
||||
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(assoc-in [:workspace-file :thumbnails id] data)
|
||||
(cond-> (nil? (get-in state [::update-thumbnail-lock id]))
|
||||
(assoc-in [::update-thumbnail-lock id] lock))))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(when (= lock (get-in state [::update-thumbnail-lock id]))
|
||||
(let [stopper (->> stream (rx/filter (ptk/type? :app.main.data.workspace/finalize)))
|
||||
params {:file-id (:current-file-id state)
|
||||
:object-id id}]
|
||||
;; Sends the first event and debounce the rest. Will only make one update once
|
||||
;; the 2 second debounce is finished
|
||||
(rx/merge
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::update-thumbnail))
|
||||
(rx/map deref)
|
||||
(rx/filter #(= id (:id %)))
|
||||
(rx/debounce 2000)
|
||||
(rx/take 1)
|
||||
(rx/map :data)
|
||||
(rx/flat-map #(rp/mutation! :upsert-file-object-thumbnail (assoc params :data %)))
|
||||
(rx/map #(fn [state] (d/dissoc-in state [::update-thumbnail-lock id])))
|
||||
(rx/take-until stopper))
|
||||
|
||||
(->> (rx/of (update-thumbnail id data))
|
||||
(rx/observe-on :async)))))))))
|
||||
|
||||
(defn remove-thumbnail
|
||||
[id]
|
||||
(ptk/reify ::remove-thumbnail
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state (d/dissoc-in [:workspace-file :thumbnails id])))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [params {:file-id (:current-file-id state)
|
||||
:object-id id
|
||||
:data nil}]
|
||||
(->> (rp/mutation! :upsert-file-object-thumbnail params)
|
||||
(rx/ignore))))))
|
||||
|
||||
(defn- extract-frame-changes
|
||||
"Process a changes set in a commit to extract the frames that are changing"
|
||||
[[event [old-objects new-objects]]]
|
||||
(let [changes (-> event deref :changes)
|
||||
|
||||
extract-ids
|
||||
(fn [{type :type :as change}]
|
||||
(case type
|
||||
:add-obj [(:id change)]
|
||||
:mod-obj [(:id change)]
|
||||
:del-obj [(:id change)]
|
||||
:reg-objects (:shapes change)
|
||||
:mov-objects (:shapes change)
|
||||
[]))
|
||||
|
||||
get-frame-id
|
||||
(fn [id]
|
||||
(let [shape (or (get new-objects id)
|
||||
(get old-objects id))]
|
||||
(or (and (cph/frame-shape? shape) id) (:frame-id shape))))
|
||||
|
||||
;; Extracts the frames and then removes nils and the root frame
|
||||
xform (comp (mapcat extract-ids)
|
||||
(map get-frame-id)
|
||||
(remove nil?)
|
||||
(filter #(not= uuid/zero %))
|
||||
(filter #(contains? new-objects %)))]
|
||||
|
||||
(into #{} xform changes)))
|
||||
|
||||
(defn thumbnail-change?
|
||||
"Checks if a event is only updating thumbnails to ignore in the thumbnail generation process"
|
||||
[event]
|
||||
(let [changes (-> event deref :changes)
|
||||
|
||||
is-thumbnail-op?
|
||||
(fn [{type :type attr :attr}]
|
||||
(and (= type :set)
|
||||
(= attr :thumbnail)))
|
||||
|
||||
is-thumbnail-change?
|
||||
(fn [change]
|
||||
(and (= (:type change) :mod-obj)
|
||||
(->> change :operations (every? is-thumbnail-op?))))]
|
||||
|
||||
(->> changes (every? is-thumbnail-change?))))
|
||||
|
||||
(defn watch-state-changes
|
||||
"Watch the state for changes inside frames. If a change is detected will force a rendering
|
||||
of the frame data so the thumbnail can be updated."
|
||||
[]
|
||||
(ptk/reify ::watch-state-changes
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ stream]
|
||||
(let [stopper (->> stream
|
||||
(rx/filter #(or (= :app.main.data.workspace/finalize-page (ptk/type %))
|
||||
(= ::watch-state-changes (ptk/type %)))))
|
||||
|
||||
objects-stream (->> (rx/concat
|
||||
(rx/of nil)
|
||||
(rx/from-atom refs/workspace-page-objects {:emit-current-value? true}))
|
||||
;; We need to keep the old-objects so we can check the frame for the
|
||||
;; deleted objects
|
||||
(rx/buffer 2 1))
|
||||
|
||||
frame-changes (->> stream
|
||||
(rx/filter dch/commit-changes?)
|
||||
|
||||
;; Async so we wait for additional side-effects of commit-changes
|
||||
(rx/observe-on :async)
|
||||
(rx/filter (complement thumbnail-change?))
|
||||
(rx/with-latest-from objects-stream)
|
||||
(rx/map extract-frame-changes)
|
||||
(rx/share))]
|
||||
|
||||
(->> frame-changes
|
||||
(rx/flat-map
|
||||
(fn [ids]
|
||||
(->> (rx/from ids)
|
||||
(rx/map #(ptk/data-event ::force-render %)))))
|
||||
(rx/take-until stopper))))))
|
||||
|
||||
(defn duplicate-thumbnail
|
||||
[old-id new-id]
|
||||
(ptk/reify ::duplicate-thumbnail
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [old-shape-thumbnail (get-in state [:workspace-file :thumbnails old-id])]
|
||||
(-> state (assoc-in [:workspace-file :thumbnails new-id] old-shape-thumbnail))))))
|
||||
|
||||
|
|
@ -248,11 +248,28 @@
|
|||
(dm/get-in data [:pages-index page-id])))
|
||||
st/state))
|
||||
|
||||
(defn workspace-page-objects-by-id
|
||||
[page-id]
|
||||
(l/derived #(wsh/lookup-page-objects % page-id) st/state =))
|
||||
|
||||
(def workspace-page-objects
|
||||
(l/derived wsh/lookup-page-objects st/state =))
|
||||
|
||||
(def workspace-modifiers
|
||||
(l/derived :workspace-modifiers st/state))
|
||||
(defn object-by-id
|
||||
[id]
|
||||
(l/derived #(get % id) workspace-page-objects))
|
||||
|
||||
(defn objects-by-id
|
||||
[ids]
|
||||
(l/derived #(into [] (keep (d/getf %)) ids) workspace-page-objects =))
|
||||
|
||||
(defn children-objects
|
||||
[id]
|
||||
(l/derived
|
||||
(fn [objects]
|
||||
(let [children-ids (get-in objects [id :shapes])]
|
||||
(into [] (keep (d/getf objects)) children-ids)))
|
||||
workspace-page-objects =))
|
||||
|
||||
(def workspace-page-options
|
||||
(l/derived :options workspace-page))
|
||||
|
@ -266,13 +283,35 @@
|
|||
(def workspace-editor-state
|
||||
(l/derived :workspace-editor-state st/state))
|
||||
|
||||
(defn object-by-id
|
||||
[id]
|
||||
(l/derived #(get % id) workspace-page-objects))
|
||||
(def workspace-modifiers
|
||||
(l/derived :workspace-modifiers st/state))
|
||||
|
||||
(defn objects-by-id
|
||||
(defn workspace-modifiers-by-id
|
||||
[ids]
|
||||
(l/derived #(into [] (keep (d/getf %)) ids) workspace-page-objects))
|
||||
(l/derived #(select-keys % ids) workspace-modifiers))
|
||||
|
||||
|
||||
(def workspace-modifiers-with-objects
|
||||
(l/derived
|
||||
(fn [state]
|
||||
{:modifiers (:workspace-modifiers state)
|
||||
:objects (wsh/lookup-page-objects state)})
|
||||
st/state
|
||||
(fn [a b]
|
||||
(and (= (:modifiers a) (:modifiers b))
|
||||
(identical? (:objects a) (:objects b))))))
|
||||
|
||||
(defn workspace-modifiers-by-frame-id
|
||||
[frame-id]
|
||||
(l/derived
|
||||
(fn [{:keys [modifiers objects]}]
|
||||
(let [keys (->> modifiers
|
||||
(keys)
|
||||
(filter #(or (= frame-id %)
|
||||
(= frame-id (get-in objects [% :frame-id])))))]
|
||||
(select-keys modifiers keys)))
|
||||
workspace-modifiers-with-objects
|
||||
=))
|
||||
|
||||
(defn- set-content-modifiers [state]
|
||||
(fn [id shape]
|
||||
|
@ -355,3 +394,16 @@
|
|||
(l/derived (fn [state]
|
||||
(dm/get-in state [:viewer-local :fullscreen?]))
|
||||
st/state))
|
||||
|
||||
(def thumbnail-data
|
||||
(l/derived #(dm/get-in % [:workspace-file :thumbnails] {}) st/state))
|
||||
|
||||
(defn thumbnail-frame-data
|
||||
[frame-id]
|
||||
(l/derived #(get % frame-id) thumbnail-data))
|
||||
|
||||
(def workspace-text-modifier
|
||||
(l/derived :workspace-text-modifier st/state))
|
||||
|
||||
(defn workspace-text-modifier-by-id [id]
|
||||
(l/derived #(get % id) workspace-text-modifier))
|
||||
|
|
|
@ -243,8 +243,9 @@
|
|||
|
||||
(let [shapes (->> shapes
|
||||
(remove cph/frame-shape?)
|
||||
(mapcat #(cph/get-children-with-self objects (:id %))))]
|
||||
[:& ff/fontfaces-style {:shapes shapes}])
|
||||
(mapcat #(cph/get-children-with-self objects (:id %))))
|
||||
fonts (ff/shapes->fonts shapes)]
|
||||
[:& ff/fontfaces-style {:fonts fonts}])
|
||||
|
||||
(for [item shapes]
|
||||
(let [frame? (= (:type item) :frame)]
|
||||
|
@ -401,8 +402,8 @@
|
|||
:style {:-webkit-print-color-adjust :exact}
|
||||
:fill "none"}
|
||||
|
||||
(let [shapes (cph/get-children objects object-id)]
|
||||
[:& ff/fontfaces-style {:shapes shapes}])
|
||||
(let [fonts (ff/frame->fonts object-id objects)]
|
||||
[:& ff/fontfaces-style {:fonts fonts}])
|
||||
|
||||
(case (:type object)
|
||||
:frame [:& frame-wrapper {:shape object :view-box vbox}]
|
||||
|
|
|
@ -213,6 +213,15 @@
|
|||
(mf/set-ref-val! ref value)))
|
||||
(mf/ref-val ref)))
|
||||
|
||||
(defn use-update-var
|
||||
[value]
|
||||
(let [ref (mf/use-var value)]
|
||||
(mf/use-effect
|
||||
(mf/deps value)
|
||||
(fn []
|
||||
(reset! ref value)))
|
||||
ref))
|
||||
|
||||
(defn use-equal-memo
|
||||
[val]
|
||||
(let [ref (mf/use-ref nil)]
|
||||
|
@ -248,3 +257,5 @@
|
|||
(mf/deps focus objects)
|
||||
#(cp/focus-objects objects focus))]
|
||||
objects)))
|
||||
|
||||
|
||||
|
|
|
@ -46,7 +46,10 @@
|
|||
:characterData true}
|
||||
mutation-obs (js/MutationObserver. on-mutation)]
|
||||
(mf/set-ref-val! prev-obs-ref mutation-obs)
|
||||
(.observe mutation-obs node options))))))]
|
||||
(.observe mutation-obs node options))))
|
||||
|
||||
;; Return node so it's more composable
|
||||
node))]
|
||||
|
||||
(mf/with-effect
|
||||
(fn []
|
||||
|
|
|
@ -220,8 +220,9 @@
|
|||
|
||||
(mf/defc selection-guides [{:keys [bounds selrect zoom]}]
|
||||
[:g.selection-guides
|
||||
(for [[x1 y1 x2 y2] (calculate-guides bounds selrect)]
|
||||
[:line {:x1 x1
|
||||
(for [[idx [x1 y1 x2 y2]] (d/enumerate (calculate-guides bounds selrect))]
|
||||
[:line {:key (dm/str "guide-" idx)
|
||||
:x1 x1
|
||||
:y1 y1
|
||||
:x2 x2
|
||||
:y2 y2
|
||||
|
|
|
@ -423,8 +423,9 @@
|
|||
shape (obj/get props "shape")
|
||||
elem-name (obj/get child "type")
|
||||
render-id (mf/use-ctx muc/render-ctx)
|
||||
stroke-id (dm/fmt "strokes-%" (:id shape))
|
||||
stroke-props (-> (obj/new)
|
||||
(obj/set! "id" (dm/fmt "strokes-%" (:id shape)))
|
||||
(obj/set! "id" stroke-id)
|
||||
(cond->
|
||||
;; There is a blur
|
||||
(and (:blur shape) (not (cph/frame-shape? shape)) (-> shape :blur :hidden not))
|
||||
|
@ -440,7 +441,7 @@
|
|||
(for [[index value] (-> (d/enumerate (:strokes shape)) reverse)]
|
||||
(let [props (build-stroke-props index child value render-id)
|
||||
shape (assoc value :points (:points shape))]
|
||||
[:& shape-custom-stroke {:shape shape :index index}
|
||||
[:& shape-custom-stroke {:shape shape :index index :key (dm/str index "-" stroke-id)}
|
||||
[:> elem-name props]]))])]))
|
||||
|
||||
(mf/defc shape-custom-strokes
|
||||
|
|
|
@ -58,37 +58,38 @@
|
|||
(defn frame-shape
|
||||
[shape-wrapper]
|
||||
(mf/fnc frame-shape
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [childs (unchecked-get props "childs")
|
||||
shape (unchecked-get props "shape")
|
||||
{:keys [x y width height]} shape
|
||||
transform (gsh/transform-matrix shape)
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [childs (unchecked-get props "childs")
|
||||
shape (unchecked-get props "shape")
|
||||
{:keys [x y width height]} shape
|
||||
|
||||
props (-> (attrs/extract-style-attrs shape)
|
||||
(obj/merge!
|
||||
#js {:x x
|
||||
:y y
|
||||
:transform transform
|
||||
:width width
|
||||
:height height
|
||||
:className "frame-background"}))
|
||||
path? (some? (.-d props))
|
||||
render-id (mf/use-ctx muc/render-ctx)]
|
||||
transform (gsh/transform-matrix shape)
|
||||
|
||||
[:*
|
||||
[:g {:clip-path (frame-clip-url shape render-id)}
|
||||
[:*
|
||||
[:& shape-fills {:shape shape}
|
||||
(if path?
|
||||
[:> :path props]
|
||||
[:> :rect props])]
|
||||
props (-> (attrs/extract-style-attrs shape)
|
||||
(obj/merge!
|
||||
#js {:x x
|
||||
:y y
|
||||
:transform (str transform)
|
||||
:width width
|
||||
:height height
|
||||
:className "frame-background"}))
|
||||
path? (some? (.-d props))
|
||||
render-id (mf/use-ctx muc/render-ctx)]
|
||||
|
||||
(for [item childs]
|
||||
[:& shape-wrapper {:shape item
|
||||
:key (dm/str (:id item))}])
|
||||
[:& shape-strokes {:shape shape}
|
||||
(if path?
|
||||
[:> :path props]
|
||||
[:> :rect props])]]]])))
|
||||
[:*
|
||||
[:g {:clip-path (frame-clip-url shape render-id)}
|
||||
[:*
|
||||
[:& shape-fills {:shape shape}
|
||||
(if path?
|
||||
[:> :path props]
|
||||
[:> :rect props])]
|
||||
|
||||
(for [item childs]
|
||||
[:& shape-wrapper {:shape item
|
||||
:key (dm/str (:id item))}])
|
||||
[:& shape-strokes {:shape shape}
|
||||
(if path?
|
||||
[:> :path props]
|
||||
[:> :rect props])]]]])))
|
||||
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.geom.shapes.text :as gst]
|
||||
[app.main.ui.context :as muc]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.alpha :as mf]))
|
||||
|
@ -51,42 +50,30 @@
|
|||
render-id (mf/use-ctx muc/render-ctx)
|
||||
svg-text? (and (= :text (:type mask)) (some? (:position-data mask)))
|
||||
|
||||
mask (cond-> mask svg-text? set-white-fill)
|
||||
|
||||
mask-bb
|
||||
(cond
|
||||
svg-text?
|
||||
(gst/position-data-points mask)
|
||||
(-> (gsh/transform-shape mask)
|
||||
(:points))]
|
||||
[:defs
|
||||
[:filter {:id (filter-id render-id mask)}
|
||||
[:feFlood {:flood-color "white"
|
||||
:result "FloodResult"}]
|
||||
[:feComposite {:in "FloodResult"
|
||||
:in2 "SourceGraphic"
|
||||
:operator "in"
|
||||
:result "comp"}]]
|
||||
;; Clip path is necessary so the elements inside the mask won't affect
|
||||
;; the events outside. Clip hides the elements but mask doesn't (like display vs visibility)
|
||||
;; we cannot use clips instead of mask because clips can only be simple shapes
|
||||
[:clipPath {:class "mask-clip-path"
|
||||
:id (clip-id render-id mask)}
|
||||
[:polyline {:points (->> mask-bb
|
||||
(map #(str (:x %) "," (:y %)))
|
||||
(str/join " "))}]]
|
||||
|
||||
:else
|
||||
(-> (gsh/transform-shape mask)
|
||||
(:points)))]
|
||||
[:*
|
||||
[:g {:opacity 0}
|
||||
[:g {:id (str "shape-" (mask-id render-id mask))}
|
||||
[:& shape-wrapper {:shape (dissoc mask :shadow :blur)}]]]
|
||||
|
||||
[:defs
|
||||
[:filter {:id (filter-id render-id mask)}
|
||||
[:feFlood {:flood-color "white"
|
||||
:result "FloodResult"}]
|
||||
[:feComposite {:in "FloodResult"
|
||||
:in2 "SourceGraphic"
|
||||
:operator "in"
|
||||
:result "comp"}]]
|
||||
;; Clip path is necessary so the elements inside the mask won't affect
|
||||
;; the events outside. Clip hides the elements but mask doesn't (like display vs visibility)
|
||||
;; we cannot use clips instead of mask because clips can only be simple shapes
|
||||
[:clipPath {:class "mask-clip-path"
|
||||
:id (clip-id render-id mask)}
|
||||
[:polyline {:points (->> mask-bb
|
||||
(map #(str (:x %) "," (:y %)))
|
||||
(str/join " "))}]]
|
||||
|
||||
[:mask {:class "mask-shape"
|
||||
:id (mask-id render-id mask)}
|
||||
;; SVG texts are broken in Firefox with the filter. When the masking shapes is a text
|
||||
;; we use the `set-white-fill` instead of using the filter
|
||||
[:g {:filter (when-not svg-text? (filter-url render-id mask))}
|
||||
[:use {:href (str "#shape-" (mask-id render-id mask))}]]]]])))
|
||||
;; When te shape is a text we pass to the shape the info and disable the filter.
|
||||
;; There is a bug in Firefox with filters and texts. We change the text to white at shape level
|
||||
[:mask {:class "mask-shape"
|
||||
:id (mask-id render-id mask)}
|
||||
[:g {:filter (when-not svg-text? (filter-url render-id mask))}
|
||||
[:& shape-wrapper {:shape (-> mask (dissoc :shadow :blur) (assoc :is-mask? true))}]]]])))
|
||||
|
||||
|
|
|
@ -34,9 +34,9 @@
|
|||
(obj/set! "style" style))))
|
||||
|
||||
(defn translate-shape [attrs shape]
|
||||
(let [transform (str (usvg/svg-transform-matrix shape)
|
||||
" "
|
||||
(:transform attrs ""))]
|
||||
(let [transform (dm/str (usvg/svg-transform-matrix shape)
|
||||
" "
|
||||
(:transform attrs ""))]
|
||||
(cond-> attrs
|
||||
(and (:svg-viewbox shape) (graphic-element? (-> shape :content :tag)))
|
||||
(assoc :transform transform))))
|
||||
|
@ -47,7 +47,6 @@
|
|||
|
||||
(let [shape (unchecked-get props "shape")
|
||||
children (unchecked-get props "children")
|
||||
|
||||
{:keys [x y width height]} shape
|
||||
{:keys [attrs] :as content} (:content shape)
|
||||
|
||||
|
@ -61,7 +60,7 @@
|
|||
(obj/set! "preserveAspectRatio" "none"))]
|
||||
|
||||
[:& (mf/provider svg-ids-ctx) {:value ids-mapping}
|
||||
[:g.svg-raw {:transform (gsh/transform-matrix shape)}
|
||||
[:g.svg-raw {:transform (dm/str (gsh/transform-matrix shape))}
|
||||
[:> "svg" attrs children]]]))
|
||||
|
||||
(mf/defc svg-element
|
||||
|
|
|
@ -20,9 +20,13 @@
|
|||
(mf/defc render-text
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [node (obj/get props "node")
|
||||
text (:text node)
|
||||
style (sts/generate-text-styles node)]
|
||||
(let [node (obj/get props "node")
|
||||
parent (obj/get props "parent")
|
||||
shape (obj/get props "shape")
|
||||
text (:text node)
|
||||
style (if (= text "")
|
||||
(sts/generate-text-styles shape parent)
|
||||
(sts/generate-text-styles shape node))]
|
||||
[:span.text-node {:style style}
|
||||
(if (= text "") "\u00A0" text)]))
|
||||
|
||||
|
@ -60,7 +64,7 @@
|
|||
(mf/defc render-node
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [{:keys [type text children] :as node} (obj/get props "node")]
|
||||
(let [{:keys [type text children]} (obj/get props "node")]
|
||||
(if (string? text)
|
||||
[:> render-text props]
|
||||
(let [component (case type
|
||||
|
|
|
@ -73,16 +73,25 @@
|
|||
(when (d/not-empty? style)
|
||||
[:style style])))
|
||||
|
||||
(defn frame->fonts
|
||||
[frame objects]
|
||||
(->> (cph/get-children objects (:id frame))
|
||||
(filter cph/text-shape?)
|
||||
(map (comp fonts/get-content-fonts :content))
|
||||
(reduce set/union #{})))
|
||||
|
||||
(defn shapes->fonts
|
||||
[shapes]
|
||||
(->> shapes
|
||||
(filter cph/text-shape?)
|
||||
(map (comp fonts/get-content-fonts :content))
|
||||
(reduce set/union #{})))
|
||||
|
||||
(mf/defc fontfaces-style
|
||||
{::mf/wrap-props false
|
||||
::mf/wrap [#(mf/memo' % (mf/check-props ["shapes"]))]}
|
||||
::mf/wrap [#(mf/memo' % (mf/check-props ["fonts"]))]}
|
||||
[props]
|
||||
(let [;; Retrieve the fonts ids used by the text shapes
|
||||
fonts (->> (obj/get props "shapes")
|
||||
(filterv cph/text-shape?)
|
||||
(mapv (comp fonts/get-content-fonts :content))
|
||||
(reduce set/union #{})
|
||||
(hooks/use-equal-memo))]
|
||||
|
||||
fonts (obj/get props "fonts")]
|
||||
(when (d/not-empty? fonts)
|
||||
[:> fontfaces-style-render {:fonts fonts}])))
|
||||
|
|
|
@ -15,9 +15,8 @@
|
|||
[cuerdas.core :as str]))
|
||||
|
||||
(defn generate-root-styles
|
||||
[shape node]
|
||||
[{:keys [width height]} node]
|
||||
(let [valign (:vertical-align node "top")
|
||||
{:keys [width height]} shape
|
||||
base #js {:height height
|
||||
:width width
|
||||
:fontFamily "sourcesanspro"
|
||||
|
@ -57,10 +56,10 @@
|
|||
(some? text-align) (obj/set! "textAlign" text-align))))
|
||||
|
||||
(defn generate-text-styles
|
||||
([data]
|
||||
(generate-text-styles data nil))
|
||||
([shape data]
|
||||
(generate-text-styles shape data nil))
|
||||
|
||||
([data {:keys [show-text?] :or {show-text? true}}]
|
||||
([{:keys [grow-type] :as shape} data {:keys [show-text?] :or {show-text? true}}]
|
||||
(let [letter-spacing (:letter-spacing data 0)
|
||||
text-decoration (:text-decoration data)
|
||||
text-transform (:text-transform data)
|
||||
|
@ -81,7 +80,7 @@
|
|||
|
||||
base #js {:textDecoration text-decoration
|
||||
:textTransform text-transform
|
||||
:lineHeight (or line-height "inherit")
|
||||
:lineHeight (or line-height "1.2")
|
||||
:color (if show-text? text-color "transparent")
|
||||
:caretColor (or text-color "black")
|
||||
:overflowWrap "initial"}
|
||||
|
@ -99,33 +98,35 @@
|
|||
(nil? (:fills data))
|
||||
[{:fill-color "#000000" :fill-opacity 1}])
|
||||
|
||||
base (cond-> base
|
||||
(some? fills)
|
||||
(obj/set! "--fills" (transit/encode-str fills)))]
|
||||
|
||||
(when (and (string? letter-spacing)
|
||||
(pos? (alength letter-spacing)))
|
||||
(obj/set! base "letterSpacing" (str letter-spacing "px")))
|
||||
font (when (and (string? font-id) (pos? (alength font-id)))
|
||||
(get fontsdb font-id))
|
||||
|
||||
(when (and (string? font-size)
|
||||
(pos? (alength font-size)))
|
||||
(obj/set! base "fontSize" (str font-size "px")))
|
||||
[font-family font-style font-weight]
|
||||
(when (some? font)
|
||||
(fonts/ensure-loaded! font-id)
|
||||
(let [font-variant (d/seek #(= font-variant-id (:id %)) (:variants font))]
|
||||
[(str/quote (or (:family font) (:font-family data)))
|
||||
(or (:style font-variant) (:font-style data))
|
||||
(or (:weight font-variant) (:font-weight data))]))]
|
||||
|
||||
(when (and (string? font-id)
|
||||
(pos? (alength font-id)))
|
||||
(fonts/ensure-loaded! font-id)
|
||||
(let [font (get fontsdb font-id)
|
||||
font-family (str/quote
|
||||
(or (:family font)
|
||||
(:font-family data)))
|
||||
font-variant (d/seek #(= font-variant-id (:id %))
|
||||
(:variants font))
|
||||
font-style (or (:style font-variant)
|
||||
(:font-style data))
|
||||
font-weight (or (:weight font-variant)
|
||||
(:font-weight data))]
|
||||
(obj/set! base "fontFamily" font-family)
|
||||
(obj/set! base "fontStyle" font-style)
|
||||
(obj/set! base "fontWeight" font-weight)))
|
||||
(cond-> base
|
||||
(some? fills)
|
||||
(obj/set! "--fills" (transit/encode-str fills))
|
||||
|
||||
base)))
|
||||
(and (string? letter-spacing) (pos? (alength letter-spacing)))
|
||||
(obj/set! "letterSpacing" (str letter-spacing "px"))
|
||||
|
||||
(and (string? font-size) (pos? (alength font-size)))
|
||||
(obj/set! "fontSize" (str font-size "px"))
|
||||
|
||||
(some? font)
|
||||
(-> (obj/set! "fontFamily" font-family)
|
||||
(obj/set! "fontStyle" font-style)
|
||||
(obj/set! "fontWeight" font-weight))
|
||||
|
||||
(= grow-type :auto-width)
|
||||
(obj/set! "whiteSpace" "pre")
|
||||
|
||||
(not= grow-type :auto-width)
|
||||
(obj/set! "whiteSpace" "pre-wrap")))))
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
(ns app.main.ui.shapes.text.svg-text
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.config :as cfg]
|
||||
[app.main.ui.context :as muc]
|
||||
|
@ -18,13 +19,27 @@
|
|||
|
||||
(def fill-attrs [:fill-color :fill-color-gradient :fill-opacity])
|
||||
|
||||
(defn set-white-fill
|
||||
[shape]
|
||||
(let [update-color
|
||||
(fn [data]
|
||||
(-> data
|
||||
(dissoc :fill-color :fill-opacity :fill-color-gradient)
|
||||
(assoc :fills [{:fill-color "#FFFFFF" :fill-opacity 1}])))]
|
||||
(-> shape
|
||||
(d/update-when :position-data #(mapv update-color %))
|
||||
(assoc :stroke-color "#FFFFFF" :stroke-opacity 1))))
|
||||
|
||||
(mf/defc text-shape
|
||||
{::mf/wrap-props false
|
||||
::mf/wrap [mf/memo]}
|
||||
[props]
|
||||
|
||||
(let [render-id (mf/use-ctx muc/render-ctx)
|
||||
{:keys [x y width height position-data] :as shape} (obj/get props "shape")
|
||||
shape (obj/get props "shape")
|
||||
shape (cond-> shape (:is-mask? shape) set-white-fill)
|
||||
|
||||
{:keys [x y width height position-data]} shape
|
||||
|
||||
transform (str (gsh/transform-matrix shape))
|
||||
|
||||
|
@ -57,7 +72,8 @@
|
|||
|
||||
alignment-bl (when (cfg/check-browser? :safari) "text-before-edge")
|
||||
dominant-bl (when-not (cfg/check-browser? :safari) "ideographic")
|
||||
props (-> #js {:x (:x data)
|
||||
props (-> #js {:key (dm/str "text-" (:id shape) "-" index)
|
||||
:x (:x data)
|
||||
:y y
|
||||
:alignmentBaseline alignment-bl
|
||||
:dominantBaseline dominant-bl
|
||||
|
|
|
@ -44,7 +44,14 @@
|
|||
[props]
|
||||
(let [objects (obj/get props "objects")
|
||||
active-frames (obj/get props "active-frames")
|
||||
shapes (cph/get-immediate-children objects)]
|
||||
shapes (cph/get-immediate-children objects)
|
||||
|
||||
;; We group the objects together per frame-id so if an object of a different
|
||||
;; frame changes won't affect the rendering frame
|
||||
frame-objects
|
||||
(mf/use-memo
|
||||
(mf/deps objects)
|
||||
#(cph/objects-by-frame objects))]
|
||||
[:*
|
||||
;; Render font faces only for shapes that are part of the root
|
||||
;; frame but don't belongs to any other frame.
|
||||
|
@ -57,7 +64,7 @@
|
|||
(if (cph/frame-shape? item)
|
||||
[:& frame-wrapper {:shape item
|
||||
:key (:id item)
|
||||
:objects objects
|
||||
:objects (get frame-objects (:id item))
|
||||
:thumbnail? (not (get active-frames (:id item) false))}]
|
||||
|
||||
[:& shape-wrapper {:shape item
|
||||
|
@ -67,9 +74,8 @@
|
|||
{::mf/wrap [#(mf/memo' % (mf/check-props ["shape"]))]
|
||||
::mf/wrap-props false}
|
||||
[props]
|
||||
(let [shape (obj/get props "shape")
|
||||
(let [shape (obj/get props "shape")
|
||||
opts #js {:shape shape}]
|
||||
|
||||
(when (and (some? shape) (not (:hidden shape)))
|
||||
[:*
|
||||
(case (:type shape)
|
||||
|
|
|
@ -6,125 +6,117 @@
|
|||
|
||||
(ns app.main.ui.workspace.shapes.frame
|
||||
(:require
|
||||
[app.common.colors :as cc]
|
||||
[app.common.data :as d]
|
||||
[app.common.pages.helpers :as cph]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.main.data.workspace.thumbnails :as dwt]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.ui.hooks :as hooks]
|
||||
[app.main.ui.shapes.embed :as embed]
|
||||
[app.main.ui.shapes.frame :as frame]
|
||||
[app.main.ui.shapes.shape :refer [shape-container]]
|
||||
[app.main.ui.shapes.text.fontfaces :as ff]
|
||||
[app.util.object :as obj]
|
||||
[app.util.timers :as ts]
|
||||
[app.main.ui.workspace.shapes.frame.dynamic-modifiers :as fdm]
|
||||
[app.main.ui.workspace.shapes.frame.node-store :as fns]
|
||||
[app.main.ui.workspace.shapes.frame.thumbnail-render :as ftr]
|
||||
[beicon.core :as rx]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(defn check-frame-props
|
||||
"Checks for changes in the props of a frame"
|
||||
[new-props old-props]
|
||||
(let [new-shape (unchecked-get new-props "shape")
|
||||
old-shape (unchecked-get old-props "shape")
|
||||
|
||||
new-thumbnail? (unchecked-get new-props "thumbnail?")
|
||||
old-thumbnail? (unchecked-get old-props "thumbnail?")
|
||||
|
||||
new-objects (unchecked-get new-props "objects")
|
||||
old-objects (unchecked-get old-props "objects")
|
||||
|
||||
new-children (->> new-shape :shapes (mapv #(get new-objects %)))
|
||||
old-children (->> old-shape :shapes (mapv #(get old-objects %)))]
|
||||
(and (= new-shape old-shape)
|
||||
(= new-thumbnail? old-thumbnail?)
|
||||
(= new-children old-children))))
|
||||
|
||||
(mf/defc frame-placeholder
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [{:keys [x y width height fill-color] :as shape} (obj/get props "shape")]
|
||||
(if (some? (:thumbnail shape))
|
||||
[:& frame/frame-thumbnail {:shape shape}]
|
||||
[:rect.frame-thumbnail {:x x :y y :width width :height height :style {:fill (or fill-color cc/white)}}])))
|
||||
|
||||
(defn custom-deferred
|
||||
[component]
|
||||
(mf/fnc deferred
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [shape (-> (obj/get props "shape")
|
||||
(select-keys [:x :y :width :height])
|
||||
(hooks/use-equal-memo))
|
||||
|
||||
tmp (mf/useState false)
|
||||
^boolean render? (aget tmp 0)
|
||||
^js set-render (aget tmp 1)
|
||||
prev-shape-ref (mf/use-ref shape)]
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps shape)
|
||||
(fn []
|
||||
(mf/set-ref-val! prev-shape-ref shape)
|
||||
(set-render false)))
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps render? shape)
|
||||
(fn []
|
||||
(when-not render?
|
||||
(let [sem (ts/schedule-on-idle #(set-render true))]
|
||||
#(rx/dispose! sem)))))
|
||||
|
||||
(if (and render? (= shape (mf/ref-val prev-shape-ref)))
|
||||
(mf/jsx component props mf/undefined)
|
||||
(mf/jsx frame-placeholder props mf/undefined)))))
|
||||
|
||||
;; Draw the frame proper as a deferred component
|
||||
(defn deferred-frame-shape-factory
|
||||
(defn frame-shape-factory
|
||||
[shape-wrapper]
|
||||
(let [frame-shape (frame/frame-shape shape-wrapper)]
|
||||
(mf/fnc defered-frame-wrapper
|
||||
{::mf/wrap-props false
|
||||
::mf/wrap [#(mf/memo' % (mf/check-props ["shape" "childs"]))
|
||||
custom-deferred]}
|
||||
[props]
|
||||
(let [shape (unchecked-get props "shape")
|
||||
childs (unchecked-get props "childs")]
|
||||
[:& frame-shape {:shape shape
|
||||
:childs childs}]))))
|
||||
(mf/fnc frame-shape-inner
|
||||
{::mf/wrap [#(mf/memo' % (mf/check-props ["shape" "fonts"]))]
|
||||
::mf/wrap-props false
|
||||
::mf/forward-ref true}
|
||||
[props ref]
|
||||
|
||||
(let [shape (unchecked-get props "shape")
|
||||
fonts (unchecked-get props "fonts")
|
||||
childs-ref (mf/use-memo (mf/deps (:id shape)) #(refs/children-objects (:id shape)))
|
||||
childs (mf/deref childs-ref)]
|
||||
|
||||
[:& (mf/provider embed/context) {:value true}
|
||||
[:& shape-container {:shape shape :ref ref}
|
||||
[:& ff/fontfaces-style {:fonts fonts}]
|
||||
[:& frame-shape {:shape shape :childs childs} ]]]))))
|
||||
|
||||
(defn check-props
|
||||
[new-props old-props]
|
||||
(and
|
||||
(= (unchecked-get new-props "thumbnail?") (unchecked-get old-props "thumbnail?"))
|
||||
(= (unchecked-get new-props "shape") (unchecked-get old-props "shape"))
|
||||
(= (unchecked-get new-props "objects") (unchecked-get old-props "objects"))))
|
||||
|
||||
(defn frame-wrapper-factory
|
||||
[shape-wrapper]
|
||||
(let [deferred-frame-shape (deferred-frame-shape-factory shape-wrapper)]
|
||||
|
||||
(let [frame-shape (frame-shape-factory shape-wrapper)]
|
||||
(mf/fnc frame-wrapper
|
||||
{::mf/wrap [#(mf/memo' % check-frame-props)]
|
||||
{::mf/wrap [#(mf/memo' % check-props)]
|
||||
::mf/wrap-props false}
|
||||
[props]
|
||||
|
||||
(when-let [shape (unchecked-get props "shape")]
|
||||
(let [objects (unchecked-get props "objects")
|
||||
thumbnail? (unchecked-get props "thumbnail?")
|
||||
(let [shape (unchecked-get props "shape")
|
||||
thumbnail? (unchecked-get props "thumbnail?")
|
||||
objects (unchecked-get props "objects")
|
||||
|
||||
children
|
||||
(-> (mapv (d/getf objects) (:shapes shape))
|
||||
(hooks/use-equal-memo))
|
||||
fonts (mf/use-memo (mf/deps shape objects) #(ff/frame->fonts shape objects))
|
||||
fonts (-> fonts (hooks/use-equal-memo))
|
||||
|
||||
all-children
|
||||
(-> (cph/get-children objects (:id shape))
|
||||
(hooks/use-equal-memo))
|
||||
force-render (mf/use-state false)
|
||||
|
||||
all-svg-text?
|
||||
(mf/use-memo
|
||||
(mf/deps all-children)
|
||||
(fn []
|
||||
(->> all-children
|
||||
(filter #(and (= :text (:type %)) (not (:hidden %))))
|
||||
(every? #(some? (:position-data %))))))
|
||||
;; Thumbnail data
|
||||
frame-id (:id shape)
|
||||
thumbnail-data-ref (mf/use-memo (mf/deps frame-id) #(refs/thumbnail-frame-data frame-id))
|
||||
thumbnail-data (mf/deref thumbnail-data-ref)
|
||||
thumbnail? (and thumbnail? (or (some? (:thumbnail shape)) (some? thumbnail-data)))
|
||||
|
||||
show-thumbnail? (and thumbnail? (some? (:thumbnail shape)) all-svg-text?)]
|
||||
;; References to the current rendered node and the its parentn
|
||||
node-ref (mf/use-var nil)
|
||||
|
||||
[:g.frame-wrapper {:display (when (:hidden shape) "none")}
|
||||
[:> shape-container {:shape shape}
|
||||
[:& ff/fontfaces-style {:shapes all-children}]
|
||||
(if show-thumbnail?
|
||||
[:& frame/frame-thumbnail {:shape shape}]
|
||||
[:& deferred-frame-shape
|
||||
{:shape shape
|
||||
:childs children}])]])))))
|
||||
;; when `true` we've called the mount for the frame
|
||||
rendered? (mf/use-var false)
|
||||
|
||||
;; Modifiers
|
||||
modifiers-ref (mf/use-memo (mf/deps frame-id) #(refs/workspace-modifiers-by-frame-id frame-id))
|
||||
modifiers (mf/deref modifiers-ref)
|
||||
|
||||
disable-thumbnail? (d/not-empty? (dm/get-in modifiers [(:id shape) :modifiers]))
|
||||
|
||||
[on-load-frame-dom thumb-renderer]
|
||||
(ftr/use-render-thumbnail shape node-ref rendered? thumbnail? disable-thumbnail?)
|
||||
|
||||
on-frame-load
|
||||
(fns/use-node-store thumbnail? node-ref rendered?)]
|
||||
|
||||
(fdm/use-dynamic-modifiers objects @node-ref modifiers)
|
||||
|
||||
(mf/use-effect
|
||||
(fn []
|
||||
;; When a change in the data is received a "force-render" event is emited
|
||||
;; that will force the component to be mounted in memory
|
||||
(let [sub
|
||||
(->> (dwt/force-render-stream (:id shape))
|
||||
(rx/take-while #(not @rendered?))
|
||||
(rx/subs #(reset! force-render true)))]
|
||||
#(when sub
|
||||
(rx/dispose! sub)))))
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps shape fonts thumbnail? on-load-frame-dom @force-render)
|
||||
(fn []
|
||||
(when (and (some? @node-ref) (or @rendered? (not thumbnail?) @force-render))
|
||||
(mf/mount
|
||||
(mf/element frame-shape
|
||||
#js {:ref on-load-frame-dom :shape shape :fonts fonts})
|
||||
|
||||
@node-ref)
|
||||
(when (not @rendered?) (reset! rendered? true)))))
|
||||
|
||||
[:g.frame-container {:key "frame-container" :ref on-frame-load}
|
||||
thumb-renderer
|
||||
|
||||
[:g.frame-thumbnail
|
||||
[:> frame/frame-thumbnail {:shape (cond-> shape
|
||||
(some? thumbnail-data)
|
||||
(assoc :thumbnail thumbnail-data))}]]]))))
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.workspace.shapes.frame.dynamic-modifiers
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.main.ui.workspace.viewport.utils :as utils]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(defn use-dynamic-modifiers
|
||||
[objects node modifiers]
|
||||
|
||||
(let [transforms
|
||||
(mf/use-memo
|
||||
(mf/deps modifiers)
|
||||
(fn []
|
||||
(when (some? modifiers)
|
||||
(d/mapm (fn [id {modifiers :modifiers}]
|
||||
(let [center (gsh/center-shape (get objects id))]
|
||||
(gsh/modifiers->transform center modifiers)))
|
||||
modifiers))))
|
||||
|
||||
shapes
|
||||
(mf/use-memo
|
||||
(mf/deps transforms)
|
||||
(fn []
|
||||
(->> (keys transforms)
|
||||
(mapv (d/getf objects)))))
|
||||
|
||||
prev-shapes (mf/use-var nil)
|
||||
prev-modifiers (mf/use-var nil)
|
||||
prev-transforms (mf/use-var nil)]
|
||||
|
||||
(mf/use-layout-effect
|
||||
(mf/deps transforms)
|
||||
(fn []
|
||||
(when (and (nil? @prev-transforms)
|
||||
(some? transforms))
|
||||
(utils/start-transform! node shapes))
|
||||
|
||||
(when (some? modifiers)
|
||||
(utils/update-transform! node shapes transforms modifiers))
|
||||
|
||||
(when (and (some? @prev-modifiers)
|
||||
(empty? modifiers))
|
||||
(utils/remove-transform! node @prev-shapes))
|
||||
|
||||
(reset! prev-modifiers modifiers)
|
||||
(reset! prev-transforms transforms)
|
||||
(reset! prev-shapes shapes)))))
|
|
@ -0,0 +1,47 @@
|
|||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.workspace.shapes.frame.node-store
|
||||
(:require
|
||||
[app.util.dom :as dom]
|
||||
[app.util.globals :as globals]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(defn use-node-store
|
||||
"Hook responsible of storing the rendered DOM node in memory while not being used"
|
||||
[thumbnail? node-ref rendered?]
|
||||
|
||||
(let [;; when `true` the node is in memory
|
||||
in-memory? (mf/use-var true)
|
||||
|
||||
;; State just for re-rendering
|
||||
re-render (mf/use-state 0)
|
||||
|
||||
parent-ref (mf/use-var nil)
|
||||
|
||||
on-frame-load
|
||||
(mf/use-callback
|
||||
(fn [node]
|
||||
(when (and (some? node) (nil? @node-ref))
|
||||
(let [content (-> (.createElementNS globals/document "http://www.w3.org/2000/svg" "g")
|
||||
(dom/add-class! "frame-content"))]
|
||||
;;(.appendChild node content)
|
||||
(reset! node-ref content)
|
||||
(reset! parent-ref node)
|
||||
(swap! re-render inc)))))]
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps thumbnail?)
|
||||
(fn []
|
||||
(when (and (some? @parent-ref) (some? @node-ref) @rendered? thumbnail?)
|
||||
(.removeChild @parent-ref @node-ref)
|
||||
(reset! in-memory? true))
|
||||
|
||||
(when (and (some? @node-ref) @in-memory? (not thumbnail?))
|
||||
(.appendChild @parent-ref @node-ref)
|
||||
(reset! in-memory? false))))
|
||||
|
||||
on-frame-load))
|
|
@ -0,0 +1,121 @@
|
|||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.workspace.shapes.frame.thumbnail-render
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.math :as mth]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.hooks :as hooks]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.object :as obj]
|
||||
[app.util.timers :as ts]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(defn- draw-thumbnail-canvas
|
||||
[canvas-node img-node]
|
||||
(let [canvas-context (.getContext canvas-node "2d")
|
||||
canvas-width (.-width canvas-node)
|
||||
canvas-height (.-height canvas-node)]
|
||||
(.clearRect canvas-context 0 0 canvas-width canvas-height)
|
||||
(.drawImage canvas-context img-node 0 0 canvas-width canvas-height)
|
||||
(.toDataURL canvas-node "image/jpeg" 0.8)))
|
||||
|
||||
(defn use-render-thumbnail
|
||||
"Hook that will create the thumbnail thata"
|
||||
[{:keys [id x y width height] :as shape} node-ref rendered? thumbnail? disable?]
|
||||
|
||||
(let [frame-canvas-ref (mf/use-ref nil)
|
||||
frame-image-ref (mf/use-ref nil)
|
||||
|
||||
disable-ref? (mf/use-var disable?)
|
||||
|
||||
fixed-width (mth/clamp (:width shape) 250 2000)
|
||||
fixed-height (/ (* (:height shape) fixed-width) (:width shape))
|
||||
|
||||
image-url (mf/use-state nil)
|
||||
observer-ref (mf/use-var nil)
|
||||
|
||||
shape-ref (hooks/use-update-var shape)
|
||||
|
||||
thumbnail-ref? (mf/use-var thumbnail?)
|
||||
|
||||
on-image-load
|
||||
(mf/use-callback
|
||||
(fn []
|
||||
(let [canvas-node (mf/ref-val frame-canvas-ref)
|
||||
img-node (mf/ref-val frame-image-ref)]
|
||||
(ts/raf
|
||||
#(let [thumb-data (draw-thumbnail-canvas canvas-node img-node)]
|
||||
(st/emit! (dw/update-thumbnail id thumb-data))
|
||||
(reset! image-url nil))))))
|
||||
|
||||
on-change
|
||||
(mf/use-callback
|
||||
(fn []
|
||||
(when (and (some? @node-ref) (not @disable-ref?))
|
||||
(let [node @node-ref]
|
||||
(ts/schedule-on-idle
|
||||
#(let [frame-html (dom/node->xml node)
|
||||
{:keys [x y width height]} @shape-ref
|
||||
svg-node
|
||||
(-> (dom/make-node "http://www.w3.org/2000/svg" "svg")
|
||||
(dom/set-property! "version" "1.1")
|
||||
(dom/set-property! "viewBox" (dm/str x " " y " " width " " height))
|
||||
(dom/set-property! "width" width)
|
||||
(dom/set-property! "height" height)
|
||||
(dom/set-property! "fill" "none")
|
||||
(obj/set! "innerHTML" frame-html))
|
||||
|
||||
img-src (-> svg-node dom/node->xml dom/svg->data-uri)]
|
||||
(reset! image-url img-src)))))))
|
||||
|
||||
on-load-frame-dom
|
||||
(mf/use-callback
|
||||
(fn [node]
|
||||
(when (and (some? node) (nil? @observer-ref))
|
||||
(on-change [])
|
||||
(let [observer (js/MutationObserver. on-change)]
|
||||
(.observe observer node #js {:childList true :attributes true :characterData true :subtree true})
|
||||
(reset! observer-ref observer)))))]
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps disable?)
|
||||
(fn []
|
||||
(reset! disable-ref? disable?)))
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps thumbnail?)
|
||||
(fn []
|
||||
(reset! thumbnail-ref? thumbnail?)))
|
||||
|
||||
(mf/use-effect
|
||||
(fn []
|
||||
#(when (and (some? @node-ref) @rendered?)
|
||||
(mf/unmount @node-ref)
|
||||
(reset! node-ref nil)
|
||||
(reset! rendered? false)
|
||||
(when (some? @observer-ref)
|
||||
(.disconnect @observer-ref)
|
||||
(reset! observer-ref nil)))))
|
||||
|
||||
[on-load-frame-dom
|
||||
(when (some? @image-url)
|
||||
(mf/html
|
||||
[:g.thumbnail-rendering {:opacity 0}
|
||||
[:foreignObject {:x x :y y :width width :height height}
|
||||
[:canvas {:ref frame-canvas-ref
|
||||
:width fixed-width
|
||||
:height fixed-height}]]
|
||||
|
||||
[:image {:ref frame-image-ref
|
||||
:x (:x shape)
|
||||
:y (:y shape)
|
||||
:xlinkHref @image-url
|
||||
:width (:width shape)
|
||||
:height (:height shape)
|
||||
:on-load on-image-load}]]))]))
|
|
@ -30,9 +30,9 @@
|
|||
{::mf/wrap [#(mf/memo' % (mf/check-props ["shape"]))]
|
||||
::mf/wrap-props false}
|
||||
[props]
|
||||
(let [shape (unchecked-get props "shape")
|
||||
childs-ref (mf/use-memo (mf/deps shape) #(refs/objects-by-id (:shapes shape)))
|
||||
childs (mf/deref childs-ref)]
|
||||
(let [shape (unchecked-get props "shape")
|
||||
childs-ref (mf/use-memo (mf/deps (:id shape)) #(refs/children-objects (:id shape)))
|
||||
childs (mf/deref childs-ref)]
|
||||
|
||||
[:> shape-container {:shape shape}
|
||||
[:& group-shape
|
||||
|
|
|
@ -20,9 +20,9 @@
|
|||
::mf/wrap-props false}
|
||||
[props]
|
||||
(let [shape (unchecked-get props "shape")
|
||||
childs-ref (mf/use-memo (mf/deps shape) #(refs/objects-by-id (:shapes shape)))
|
||||
childs-ref (mf/use-memo (mf/deps (:id shape)) #(refs/children-objects (:id shape)))
|
||||
childs (mf/deref childs-ref)
|
||||
svg-tag (get-in shape [:content :tag])]
|
||||
svg-tag (get-in shape [:content :tag])]
|
||||
(if (contains? usvg/svg-group-safe-tags svg-tag)
|
||||
[:> shape-container {:shape shape}
|
||||
[:& svg-raw-shape {:shape shape
|
||||
|
|
|
@ -6,228 +6,37 @@
|
|||
|
||||
(ns app.main.ui.workspace.shapes.text
|
||||
(:require
|
||||
[app.common.attrs :as attrs]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.logging :as log]
|
||||
[app.common.data :as d]
|
||||
[app.common.math :as mth]
|
||||
[app.main.data.workspace.changes :as dch]
|
||||
[app.main.data.workspace.texts :as dwt]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.hooks.mutable-observer :refer [use-mutable-observer]]
|
||||
[app.main.ui.shapes.shape :refer [shape-container]]
|
||||
[app.main.ui.shapes.text.fo-text :as fo]
|
||||
[app.main.ui.shapes.text.svg-text :as svg]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.object :as obj]
|
||||
[app.util.svg :as usvg]
|
||||
[app.util.text-editor :as ted]
|
||||
[app.util.text-svg-position :as utp]
|
||||
[app.util.timers :as timers]
|
||||
[app.util.webapi :as wapi]
|
||||
[beicon.core :as rx]
|
||||
[app.main.ui.shapes.text :as text]
|
||||
[debug :refer [debug?]]
|
||||
[okulary.core :as l]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
;; Change this to :info :debug or :trace to debug this module
|
||||
(log/set-level! :warn)
|
||||
|
||||
;; --- Text Wrapper for workspace
|
||||
|
||||
(mf/defc text-static-content
|
||||
[{:keys [shape]}]
|
||||
[:& fo/text-shape {:shape shape
|
||||
:grow-type (:grow-type shape)}])
|
||||
|
||||
(defn- update-with-current-editor-state
|
||||
[{:keys [id] :as shape}]
|
||||
(let [editor-state-ref (mf/use-memo (mf/deps id) #(l/derived (l/key id) refs/workspace-editor-state))
|
||||
editor-state (mf/deref editor-state-ref)
|
||||
|
||||
content (:content shape)
|
||||
editor-content
|
||||
(when editor-state
|
||||
(-> editor-state
|
||||
(ted/get-editor-current-content)
|
||||
(ted/export-content)))]
|
||||
|
||||
(cond-> shape
|
||||
(some? editor-content)
|
||||
(assoc :content (attrs/merge content editor-content)))))
|
||||
|
||||
(mf/defc text-resize-content
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [{:keys [id name grow-type] :as shape} (obj/get props "shape")
|
||||
|
||||
;; NOTE: this breaks the hooks rule of "no hooks inside
|
||||
;; conditional code"; but we ensure that this component will
|
||||
;; not reused if edition flag is changed with `:key` prop.
|
||||
;; Without the `:key` prop combining the shape-id and the
|
||||
;; edition flag, this will result in a react error. This is
|
||||
;; done for performance reason; with this change only the
|
||||
;; shape with edition flag is watching the editor state ref.
|
||||
shape (cond-> shape
|
||||
(true? (obj/get props "edition?"))
|
||||
(update-with-current-editor-state))
|
||||
|
||||
mnt (mf/use-ref true)
|
||||
paragraph-ref (mf/use-state nil)
|
||||
|
||||
handle-resize-text
|
||||
(mf/use-callback
|
||||
(mf/deps id)
|
||||
(fn [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
|
||||
#(let [width (obj/get-in entries [0 "contentRect" "width"])
|
||||
height (obj/get-in entries [0 "contentRect" "height"])]
|
||||
(when (and (not (mth/almost-zero? width)) (not (mth/almost-zero? height)))
|
||||
(log/debug :msg "Resize detected" :shape-id id :width width :height height)
|
||||
(st/emit! (dwt/resize-text id (mth/ceil width) (mth/ceil height)))))))))
|
||||
|
||||
text-ref-cb
|
||||
(mf/use-callback
|
||||
(mf/deps handle-resize-text)
|
||||
(fn [node]
|
||||
(when node
|
||||
(timers/schedule
|
||||
#(when (mf/ref-val mnt)
|
||||
(when-let [ps-node (dom/query node ".paragraph-set")]
|
||||
(reset! paragraph-ref ps-node)))))))]
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps @paragraph-ref handle-resize-text grow-type)
|
||||
(fn []
|
||||
(when-let [paragraph-node @paragraph-ref]
|
||||
(let [sub (->> (wapi/observe-resize paragraph-node)
|
||||
(rx/observe-on :af)
|
||||
(rx/subs handle-resize-text))]
|
||||
(log/debug :msg "Attach resize observer" :shape-id id :shape-name name)
|
||||
(fn []
|
||||
(rx/dispose! sub))))))
|
||||
|
||||
(mf/use-effect
|
||||
(fn [] #(mf/set-ref-val! mnt false)))
|
||||
|
||||
[:& fo/text-shape {:ref text-ref-cb
|
||||
:shape shape
|
||||
:grow-type (:grow-type shape)
|
||||
:key (str "shape-" (:id shape))}]))
|
||||
|
||||
|
||||
(mf/defc text-wrapper
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [{:keys [id position-data] :as shape} (unchecked-get props "shape")
|
||||
edition-ref (mf/use-memo (mf/deps id) #(l/derived (fn [o] (= id (:edition o))) refs/workspace-local))
|
||||
edition? (mf/deref edition-ref)
|
||||
(let [shape (unchecked-get props "shape")
|
||||
|
||||
local-position-data (mf/use-state nil)
|
||||
text-modifier-ref
|
||||
(mf/use-memo (mf/deps (:id shape)) #(refs/workspace-text-modifier-by-id (:id shape)))
|
||||
|
||||
sid-ref (mf/use-ref nil)
|
||||
text-modifier
|
||||
(mf/deref text-modifier-ref)
|
||||
|
||||
handle-change-foreign-object
|
||||
(mf/use-callback
|
||||
(fn [node]
|
||||
(when-let [position-data (utp/calc-position-data node)]
|
||||
(let [parent (dom/get-parent node)
|
||||
parent-transform (dom/get-attribute parent "transform")
|
||||
node-transform (dom/get-attribute node "transform")
|
||||
|
||||
parent-mtx (usvg/parse-transform parent-transform)
|
||||
node-mtx (usvg/parse-transform node-transform)
|
||||
|
||||
;; We need to see what transformation is applied in the DOM to reverse it
|
||||
;; before calculating the position data
|
||||
mtx (-> (gmt/multiply parent-mtx node-mtx)
|
||||
(gmt/inverse))
|
||||
|
||||
position-data
|
||||
(->> position-data
|
||||
(mapv #(merge % (-> (select-keys % [:x :y :width :height])
|
||||
(gsh/transform-rect mtx)))))]
|
||||
(reset! local-position-data position-data)))))
|
||||
|
||||
[node-ref on-change-node] (use-mutable-observer handle-change-foreign-object)
|
||||
|
||||
show-svg-text? (or (some? position-data) (some? @local-position-data))
|
||||
|
||||
shape
|
||||
(cond-> shape
|
||||
(some? @local-position-data)
|
||||
(assoc :position-data @local-position-data))
|
||||
|
||||
update-position-data
|
||||
(fn []
|
||||
(when (some? @local-position-data)
|
||||
(reset! local-position-data nil)
|
||||
(st/emit! (dch/update-shapes
|
||||
[id]
|
||||
(fn [shape]
|
||||
(-> shape
|
||||
(assoc :position-data @local-position-data)))
|
||||
{:save-undo? false}))))]
|
||||
|
||||
(mf/use-layout-effect
|
||||
(mf/deps @local-position-data)
|
||||
(fn []
|
||||
;; Timer to update the shape. We do this so a lot of changes won't produce
|
||||
;; a lot of updates (kind of a debounce)
|
||||
(let [sid (timers/schedule 50 update-position-data)]
|
||||
(fn []
|
||||
(rx/dispose! sid)))))
|
||||
|
||||
(mf/use-layout-effect
|
||||
(mf/deps show-svg-text?)
|
||||
(fn []
|
||||
(when-not show-svg-text?
|
||||
;; There is no position data we need to calculate it even if no change has happened
|
||||
;; this usualy happens the first time a text is rendered
|
||||
(let [update-data
|
||||
(fn update-data []
|
||||
(let [node (mf/ref-val node-ref)]
|
||||
(if (some? node)
|
||||
(let [position-data (utp/calc-position-data node)]
|
||||
(reset! local-position-data position-data))
|
||||
|
||||
;; No node present, we need to keep waiting
|
||||
(do (when-let [sid (mf/ref-val sid-ref)] (rx/dispose! sid))
|
||||
(when-not @local-position-data
|
||||
(mf/set-ref-val! sid-ref (timers/schedule 100 update-data)))))))]
|
||||
(mf/set-ref-val! sid-ref (timers/schedule 100 update-data))))
|
||||
|
||||
(fn []
|
||||
(when-let [sid (mf/ref-val sid-ref)]
|
||||
(rx/dispose! sid)))))
|
||||
shape (cond-> shape
|
||||
(some? text-modifier)
|
||||
(dwt/apply-text-modifier text-modifier))]
|
||||
|
||||
[:> shape-container {:shape shape}
|
||||
;; We keep hidden the shape when we're editing so it keeps track of the size
|
||||
;; and updates the selrect accordingly
|
||||
[:*
|
||||
[:g.text-shape {:ref on-change-node
|
||||
:opacity (when show-svg-text? 0)
|
||||
:pointer-events "none"}
|
||||
[:g.text-shape
|
||||
[:& text/text-shape {:shape shape}]]
|
||||
|
||||
;; The `:key` prop here is mandatory because the
|
||||
;; text-resize-content breaks a hooks rule and we can't reuse
|
||||
;; the component if the edition flag changes.
|
||||
[:& text-resize-content {:shape
|
||||
(cond-> shape
|
||||
show-svg-text?
|
||||
(dissoc :transform :transform-inverse))
|
||||
:edition? edition?
|
||||
:key (str id edition?)}]]
|
||||
|
||||
(when show-svg-text?
|
||||
[:g.text-svg {:pointer-events "none"}
|
||||
[:& svg/text-shape {:shape shape}]])
|
||||
|
||||
(when (debug? :text-outline)
|
||||
(when (and (debug? :text-outline) (d/not-empty? (:position-data shape)))
|
||||
(for [data (:position-data shape)]
|
||||
(let [{:keys [x y width height]} data]
|
||||
[:*
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
(ns app.main.ui.workspace.shapes.text.editor
|
||||
(:require
|
||||
["draft-js" :as draft]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.text :as txt]
|
||||
|
@ -57,14 +58,13 @@
|
|||
:shape shape}}
|
||||
nil)))
|
||||
|
||||
(defn styles-fn [styles content]
|
||||
(if (= (.getText content) "")
|
||||
(-> (.getData content)
|
||||
(.toJS)
|
||||
(js->clj :keywordize-keys true)
|
||||
(sts/generate-text-styles {:show-text? false}))
|
||||
(-> (txt/styles-to-attrs styles)
|
||||
(sts/generate-text-styles {:show-text? false}))))
|
||||
(defn styles-fn [shape styles content]
|
||||
(let [data (if (= (.getText content) "")
|
||||
(-> (.getData content)
|
||||
(.toJS)
|
||||
(js->clj :keywordize-keys true))
|
||||
(txt/styles-to-attrs styles))]
|
||||
(sts/generate-text-styles shape data {:show-text? false})))
|
||||
|
||||
(def default-decorator
|
||||
(ted/create-decorator "PENPOT_SELECTION" selection-component))
|
||||
|
@ -96,6 +96,16 @@
|
|||
state (get state-map id empty-editor-state)
|
||||
self-ref (mf/use-ref)
|
||||
|
||||
text-modifier-ref
|
||||
(mf/use-memo (mf/deps (:id shape)) #(refs/workspace-text-modifier-by-id (:id shape)))
|
||||
|
||||
text-modifier
|
||||
(mf/deref text-modifier-ref)
|
||||
|
||||
shape (cond-> shape
|
||||
(some? text-modifier)
|
||||
(dwt/apply-text-modifier text-modifier))
|
||||
|
||||
blurred (mf/use-var false)
|
||||
|
||||
on-key-up
|
||||
|
@ -227,7 +237,7 @@
|
|||
:handle-return handle-return
|
||||
:strip-pasted-styles true
|
||||
:handle-pasted-text handle-pasted-text
|
||||
:custom-style-fn styles-fn
|
||||
:custom-style-fn (partial styles-fn shape)
|
||||
:block-renderer-fn #(render-block % shape)
|
||||
:ref on-editor
|
||||
:editor-state state}]]))
|
||||
|
@ -252,15 +262,20 @@
|
|||
position
|
||||
(-> (gpt/point (-> shape :selrect :x)
|
||||
(-> shape :selrect :y))
|
||||
(translate-point-from-viewport (mf/ref-val viewport-ref) zoom))]
|
||||
(translate-point-from-viewport (mf/ref-val viewport-ref) zoom))
|
||||
|
||||
top-left-corner (gpt/point (/ (:width shape) 2) (/ (:height shape) 2))
|
||||
|
||||
transform
|
||||
(-> (gmt/matrix)
|
||||
(gmt/scale (gpt/point zoom))
|
||||
(gmt/multiply (gsh/transform-matrix shape nil top-left-corner)))]
|
||||
|
||||
[:div {:style {:position "absolute"
|
||||
:left (str (:x position) "px")
|
||||
:top (str (:y position) "px")
|
||||
:pointer-events "all"
|
||||
:transform (str (gsh/transform-matrix shape nil (gpt/point 0 0)))
|
||||
:transform-origin "center center"}}
|
||||
:transform (str transform)
|
||||
:transform-origin "left top"}}
|
||||
|
||||
[:div {:style {:transform (str "scale(" zoom ")")
|
||||
:transform-origin "top left"}}
|
||||
[:& text-shape-edit-html {:shape shape :key (str (:id shape))}]]]))
|
||||
[:& text-shape-edit-html {:shape shape :key (str (:id shape))}]]))
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.workspace.shapes.text.text-edition-outline
|
||||
(:require
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.main.data.workspace.texts :as dwt]
|
||||
[app.main.refs :as refs]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(mf/defc text-edition-outline
|
||||
[{:keys [shape zoom]}]
|
||||
(let [text-modifier-ref
|
||||
(mf/use-memo (mf/deps (:id shape)) #(refs/workspace-text-modifier-by-id (:id shape)))
|
||||
|
||||
text-modifier
|
||||
(mf/deref text-modifier-ref)
|
||||
|
||||
shape (cond-> shape
|
||||
(some? text-modifier)
|
||||
(dwt/apply-text-modifier text-modifier))
|
||||
|
||||
transform (gsh/transform-matrix shape {:no-flip true})
|
||||
{:keys [x y width height]} shape]
|
||||
|
||||
[:rect.main.viewport-selrect
|
||||
{:x x
|
||||
:y y
|
||||
:width width
|
||||
:height height
|
||||
:transform (str transform)
|
||||
:style {:stroke "var(--color-select)"
|
||||
:stroke-width (/ 1 zoom)
|
||||
:fill "none"}}]))
|
|
@ -0,0 +1,207 @@
|
|||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.workspace.shapes.text.viewport-texts
|
||||
(:require
|
||||
[app.common.attrs :as attrs]
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.math :as mth]
|
||||
[app.common.pages.helpers :as cph]
|
||||
[app.common.text :as txt]
|
||||
[app.main.data.workspace.changes :as dch]
|
||||
[app.main.data.workspace.texts :as dwt]
|
||||
[app.main.fonts :as fonts]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.hooks :as hooks]
|
||||
[app.main.ui.shapes.text.fo-text :as fo]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.object :as obj]
|
||||
[app.util.text-editor :as ted]
|
||||
[app.util.text-svg-position :as utp]
|
||||
[app.util.timers :as ts]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(defn- update-with-editor-state
|
||||
"Updates the shape with the current state in the editor"
|
||||
[shape editor-state]
|
||||
(let [content (:content shape)
|
||||
editor-content
|
||||
(when editor-state
|
||||
(-> editor-state
|
||||
(ted/get-editor-current-content)
|
||||
(ted/export-content)))]
|
||||
|
||||
(cond-> shape
|
||||
(and (some? shape) (some? editor-content))
|
||||
(assoc :content (attrs/merge content editor-content)))))
|
||||
|
||||
(defn- update-text-shape
|
||||
[{:keys [grow-type id]} node]
|
||||
;; Check if we need to update the size because it's auto-width or auto-height
|
||||
(when (contains? #{:auto-height :auto-width} grow-type)
|
||||
(let [{:keys [width height]}
|
||||
(-> (dom/query node ".paragraph-set")
|
||||
(dom/get-client-size))
|
||||
width (mth/ceil width)
|
||||
height (mth/ceil height)]
|
||||
(when (and (not (mth/almost-zero? width)) (not (mth/almost-zero? height)))
|
||||
(st/emit! (dwt/resize-text id width height)))))
|
||||
|
||||
;; Update the position-data of every text fragment
|
||||
(let [position-data (utp/calc-position-data node)]
|
||||
(st/emit! (dch/update-shapes
|
||||
[id]
|
||||
(fn [shape]
|
||||
(-> shape
|
||||
(assoc :position-data position-data)))
|
||||
{:save-undo? false}))))
|
||||
|
||||
(defn- update-text-modifier
|
||||
[{:keys [grow-type id]} node]
|
||||
|
||||
(let [position-data (utp/calc-position-data node)
|
||||
props {:position-data position-data}
|
||||
|
||||
props
|
||||
(if (contains? #{:auto-height :auto-width} grow-type)
|
||||
(let [{:keys [width height]} (-> (dom/query node ".paragraph-set") (dom/get-client-size))
|
||||
width (mth/ceil width)
|
||||
height (mth/ceil height)]
|
||||
(if (and (not (mth/almost-zero? width)) (not (mth/almost-zero? height)))
|
||||
(assoc props :width width :height height)
|
||||
props))
|
||||
props)]
|
||||
|
||||
(st/emit! (dwt/update-text-modifier id props))))
|
||||
|
||||
(mf/defc text-container
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [shape (obj/get props "shape")
|
||||
on-update (obj/get props "on-update")
|
||||
watch-edits (obj/get props "watch-edits")
|
||||
|
||||
handle-update
|
||||
(mf/use-callback
|
||||
(mf/deps shape on-update)
|
||||
(fn [node]
|
||||
(when (some? node)
|
||||
(on-update shape node))))
|
||||
|
||||
text-modifier-ref
|
||||
(mf/use-memo
|
||||
(mf/deps (:id shape))
|
||||
#(refs/workspace-text-modifier-by-id (:id shape)))
|
||||
|
||||
text-modifier
|
||||
(when watch-edits (mf/deref text-modifier-ref))
|
||||
|
||||
shape (cond-> shape
|
||||
(some? text-modifier)
|
||||
(dwt/apply-text-modifier text-modifier))]
|
||||
|
||||
[:& fo/text-shape {:key (str "shape-" (:id shape))
|
||||
:ref handle-update
|
||||
:shape shape
|
||||
:grow-type (:grow-type shape)}]))
|
||||
|
||||
(mf/defc viewport-texts-wrapper
|
||||
{::mf/wrap-props false
|
||||
::mf/wrap [mf/memo #(mf/deferred % ts/idle-then-raf)]}
|
||||
[props]
|
||||
(let [text-shapes (obj/get props "text-shapes")
|
||||
prev-text-shapes (hooks/use-previous text-shapes)
|
||||
|
||||
;; A change in position-data won't be a "real" change
|
||||
text-change?
|
||||
(fn [id]
|
||||
(let [old-shape (get prev-text-shapes id)
|
||||
new-shape (get text-shapes id)]
|
||||
(and (not (identical? old-shape new-shape))
|
||||
(not= old-shape new-shape))))
|
||||
|
||||
changed-texts
|
||||
(mf/use-memo
|
||||
(mf/deps text-shapes)
|
||||
#(->> (keys text-shapes)
|
||||
(filter text-change?)
|
||||
(map (d/getf text-shapes))))
|
||||
|
||||
handle-update-shape (mf/use-callback update-text-shape)]
|
||||
|
||||
[:*
|
||||
(for [{:keys [id] :as shape} changed-texts]
|
||||
[:& text-container {:shape shape
|
||||
:on-update handle-update-shape
|
||||
:key (str (dm/str "text-container-" id))}])]))
|
||||
|
||||
(defn strip-position-data [[id shape]]
|
||||
(let [shape (dissoc shape :position-data :transform :transform-inverse)]
|
||||
[id shape]))
|
||||
|
||||
|
||||
(mf/defc viewport-text-editing
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
|
||||
(let [shape (obj/get props "shape")
|
||||
|
||||
;; Join current objects with the state of the editor
|
||||
editor-state
|
||||
(-> (mf/deref refs/workspace-editor-state)
|
||||
(get (:id shape)))
|
||||
|
||||
shape (cond-> shape
|
||||
(some? editor-state)
|
||||
(update-with-editor-state editor-state))
|
||||
|
||||
handle-update-shape (mf/use-callback update-text-modifier)]
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps (:id shape))
|
||||
(fn []
|
||||
#(st/emit! (dwt/remove-text-modifier (:id shape)))))
|
||||
|
||||
[:& text-container {:shape shape
|
||||
:watch-edits true
|
||||
:on-update handle-update-shape}]))
|
||||
|
||||
(defn check-props
|
||||
[new-props old-props]
|
||||
(and (identical? (unchecked-get new-props "objects") (unchecked-get old-props "objects"))
|
||||
(= (unchecked-get new-props "edition") (unchecked-get old-props "edition"))))
|
||||
|
||||
(mf/defc viewport-texts
|
||||
{::mf/wrap-props false
|
||||
::mf/wrap [#(mf/memo' % check-props)]}
|
||||
[props]
|
||||
(let [objects (obj/get props "objects")
|
||||
edition (obj/get props "edition")
|
||||
|
||||
xf-texts (comp (filter (comp cph/text-shape? second))
|
||||
(map strip-position-data))
|
||||
|
||||
text-shapes
|
||||
(mf/use-memo
|
||||
(mf/deps objects)
|
||||
#(into {} xf-texts objects))
|
||||
|
||||
editing-shape (get text-shapes edition)]
|
||||
|
||||
;; We only need the effect to run on "mount" because the next fonts will be changed when the texts are
|
||||
;; edited
|
||||
(mf/use-effect
|
||||
(fn []
|
||||
(let [text-nodes (->> text-shapes (vals)(mapcat #(txt/node-seq txt/is-text-node? (:content %))))
|
||||
fonts (into #{} (keep :font-id) text-nodes)]
|
||||
(run! fonts/ensure-loaded! fonts))))
|
||||
|
||||
[:*
|
||||
(when editing-shape
|
||||
[:& viewport-text-editing {:shape editing-shape}])
|
||||
[:& viewport-texts-wrapper {:text-shapes text-shapes}]]))
|
|
@ -245,7 +245,7 @@
|
|||
|
||||
(mf/defc frame-wrapper
|
||||
{::mf/wrap-props false
|
||||
::mf/wrap [#(mf/memo' % (mf/check-props ["selected" "item" "index" "objects"]))
|
||||
::mf/wrap [mf/memo
|
||||
#(mf/deferred % ts/idle-then-raf)]}
|
||||
[props]
|
||||
[:> layer-item props])
|
||||
|
@ -274,33 +274,6 @@
|
|||
:objects objects
|
||||
:key id}])))]]))
|
||||
|
||||
(defn- strip-obj-data [obj]
|
||||
(select-keys obj [:id
|
||||
:name
|
||||
:blocked
|
||||
:hidden
|
||||
:shapes
|
||||
:type
|
||||
:content
|
||||
:parent-id
|
||||
:component-id
|
||||
:component-file
|
||||
:shape-ref
|
||||
:touched
|
||||
:metadata
|
||||
:masked-group?
|
||||
:bool-type]))
|
||||
|
||||
(defn- strip-objects
|
||||
"Remove unnecesary data from objects map"
|
||||
[objects]
|
||||
(persistent!
|
||||
(->> objects
|
||||
(reduce-kv
|
||||
(fn [res id obj]
|
||||
(assoc! res id (strip-obj-data obj)))
|
||||
(transient {})))))
|
||||
|
||||
(mf/defc layers-tree-wrapper
|
||||
{::mf/wrap-props false
|
||||
::mf/wrap [mf/memo #(mf/throttle % 200)]}
|
||||
|
@ -312,10 +285,8 @@
|
|||
filters)
|
||||
objects (-> (obj/get props "objects")
|
||||
(hooks/use-equal-memo))
|
||||
objects (mf/use-memo
|
||||
(mf/deps objects)
|
||||
#(strip-objects objects))
|
||||
|
||||
;; TODO: Fix performance
|
||||
reparented-objects (d/mapm (fn [_ val]
|
||||
(assoc val :parent-id uuid/zero :shapes nil))
|
||||
objects)
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
(:require
|
||||
[app.common.colors :as clr]
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.main.data.workspace.colors :as dc]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.hooks :as h]
|
||||
|
@ -171,7 +172,8 @@
|
|||
(seq (:strokes values))
|
||||
[:& h/sortable-container {}
|
||||
(for [[index value] (d/enumerate (:strokes values []))]
|
||||
[:& stroke-row {:stroke value
|
||||
[:& stroke-row {:key (dm/str "stroke-" index)
|
||||
:stroke value
|
||||
:title (tr "workspace.options.stroke-color")
|
||||
:index index
|
||||
:show-caps show-caps
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
(:require
|
||||
["react-virtualized" :as rvt]
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.pages.helpers :as cph]
|
||||
[app.common.text :as txt]
|
||||
|
@ -193,8 +194,8 @@
|
|||
[:hr]
|
||||
[*
|
||||
[:p.title (tr "workspace.options.recent-fonts")]
|
||||
(for [font recent-fonts]
|
||||
[:& font-item {:key (:id font)
|
||||
(for [[idx font] (d/enumerate recent-fonts)]
|
||||
[:& font-item {:key (dm/str "font-" idx)
|
||||
:font font
|
||||
:style {}
|
||||
:on-click on-select-and-close
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
(:require
|
||||
[app.common.colors :as clr]
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.ui.context :as ctx]
|
||||
|
@ -17,6 +18,8 @@
|
|||
[app.main.ui.shapes.export :as use]
|
||||
[app.main.ui.workspace.shapes :as shapes]
|
||||
[app.main.ui.workspace.shapes.text.editor :as editor]
|
||||
[app.main.ui.workspace.shapes.text.text-edition-outline :refer [text-edition-outline]]
|
||||
[app.main.ui.workspace.shapes.text.viewport-texts :as stv]
|
||||
[app.main.ui.workspace.viewport.actions :as actions]
|
||||
[app.main.ui.workspace.viewport.comments :as comments]
|
||||
[app.main.ui.workspace.viewport.drawarea :as drawarea]
|
||||
|
@ -33,7 +36,6 @@
|
|||
[app.main.ui.workspace.viewport.selection :as selection]
|
||||
[app.main.ui.workspace.viewport.snap-distances :as snap-distances]
|
||||
[app.main.ui.workspace.viewport.snap-points :as snap-points]
|
||||
[app.main.ui.workspace.viewport.thumbnail-renderer :as wtr]
|
||||
[app.main.ui.workspace.viewport.utils :as utils]
|
||||
[app.main.ui.workspace.viewport.widgets :as widgets]
|
||||
[beicon.core :as rx]
|
||||
|
@ -67,9 +69,13 @@
|
|||
drawing (mf/deref refs/workspace-drawing)
|
||||
options (mf/deref refs/workspace-page-options)
|
||||
focus (mf/deref refs/workspace-focus-selected)
|
||||
base-objects (-> (mf/deref refs/workspace-page-objects)
|
||||
|
||||
objects-ref (mf/use-memo #(refs/workspace-page-objects-by-id page-id))
|
||||
base-objects (-> (mf/deref objects-ref)
|
||||
(ui-hooks/with-focus-objects focus))
|
||||
|
||||
modifiers (mf/deref refs/workspace-modifiers)
|
||||
|
||||
objects-modified (mf/with-memo [base-objects modifiers]
|
||||
(gsh/merge-modifiers base-objects modifiers))
|
||||
|
||||
|
@ -154,14 +160,14 @@
|
|||
(>= zoom 8))
|
||||
show-presence? page-id
|
||||
show-prototypes? (= options-mode :prototype)
|
||||
show-selection-handlers? (seq selected)
|
||||
show-selection-handlers? (and (seq selected) (not edition))
|
||||
show-snap-distance? (and (contains? layout :dynamic-alignment)
|
||||
(= transform :move)
|
||||
(seq selected))
|
||||
show-snap-points? (and (or (contains? layout :dynamic-alignment)
|
||||
(contains? layout :snap-grid))
|
||||
(or drawing-obj transform))
|
||||
show-selrect? (and selrect (empty? drawing))
|
||||
show-selrect? (and selrect (empty? drawing) (not edition))
|
||||
show-measures? (and (not transform) (not node-editing?) show-distances?)
|
||||
show-artboard-names? (contains? layout :display-artboard-names)
|
||||
show-rules? (and (contains? layout :rules) (not (contains? layout :hide-ui)))
|
||||
|
@ -177,14 +183,10 @@
|
|||
(hooks/setup-hover-shapes page-id move-stream base-objects transform selected mod? hover hover-ids @hover-disabled? focus zoom)
|
||||
(hooks/setup-viewport-modifiers modifiers base-objects)
|
||||
(hooks/setup-shortcuts node-editing? drawing-path?)
|
||||
(hooks/setup-active-frames base-objects vbox hover active-frames)
|
||||
(hooks/setup-active-frames base-objects vbox hover active-frames zoom)
|
||||
|
||||
[:div.viewport
|
||||
[:div.viewport-overlays {:ref overlays-ref}
|
||||
|
||||
[:& wtr/frame-renderer {:objects base-objects
|
||||
:background background}]
|
||||
|
||||
(when show-text-editor?
|
||||
[:& editor/text-editor-viewport {:shape editing-shape
|
||||
:viewport-ref viewport-ref
|
||||
|
@ -230,6 +232,22 @@
|
|||
:objects base-objects
|
||||
:active-frames @active-frames}]]]]
|
||||
|
||||
[:svg.render-shapes
|
||||
{:id "text-position-layer"
|
||||
:xmlns "http://www.w3.org/2000/svg"
|
||||
:xmlnsXlink "http://www.w3.org/1999/xlink"
|
||||
:preserveAspectRatio "xMidYMid meet"
|
||||
:key (str "text-position-layer" page-id)
|
||||
:width (:width vport 0)
|
||||
:height (:height vport 0)
|
||||
:view-box (utils/format-viewbox vbox)}
|
||||
|
||||
[:g {:pointer-events "none" :opacity 0}
|
||||
[:& stv/viewport-texts {:key (dm/str "texts-" page-id)
|
||||
:page-id page-id
|
||||
:objects base-objects
|
||||
:edition edition}]]]
|
||||
|
||||
[:svg.viewport-controls
|
||||
{:xmlns "http://www.w3.org/2000/svg"
|
||||
:xmlnsXlink "http://www.w3.org/1999/xlink"
|
||||
|
@ -277,6 +295,10 @@
|
|||
:on-move-selected on-move-selected
|
||||
:on-context-menu on-menu-selected}])
|
||||
|
||||
(when show-text-editor?
|
||||
[:& text-edition-outline
|
||||
{:shape (get base-objects edition)}])
|
||||
|
||||
(when show-measures?
|
||||
[:& msr/measurement
|
||||
{:bounds vbox
|
||||
|
|
|
@ -6,10 +6,10 @@
|
|||
|
||||
(ns app.main.ui.workspace.viewport.hooks
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.pages :as cp]
|
||||
[app.common.pages.helpers :as cph]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.data.shortcuts :as dsc]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.data.workspace.path.shortcuts :as psc]
|
||||
|
@ -17,10 +17,12 @@
|
|||
[app.main.store :as st]
|
||||
[app.main.streams :as ms]
|
||||
[app.main.ui.hooks :as hooks]
|
||||
[app.main.ui.workspace.shapes.frame.dynamic-modifiers :as sfd]
|
||||
[app.main.ui.workspace.viewport.actions :as actions]
|
||||
[app.main.ui.workspace.viewport.utils :as utils]
|
||||
[app.main.worker :as uw]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.globals :as globals]
|
||||
[app.util.timers :as timers]
|
||||
[beicon.core :as rx]
|
||||
[goog.events :as events]
|
||||
|
@ -197,47 +199,18 @@
|
|||
|
||||
(defn setup-viewport-modifiers
|
||||
[modifiers objects]
|
||||
(let [transforms
|
||||
(let [root-frame-ids
|
||||
(mf/use-memo
|
||||
(mf/deps modifiers)
|
||||
(mf/deps objects)
|
||||
(fn []
|
||||
(when (some? modifiers)
|
||||
(d/mapm (fn [id {modifiers :modifiers}]
|
||||
(let [center (gsh/center-shape (get objects id))]
|
||||
(gsh/modifiers->transform center modifiers)))
|
||||
modifiers))))
|
||||
|
||||
shapes
|
||||
(mf/use-memo
|
||||
(mf/deps transforms)
|
||||
(fn []
|
||||
(->> (keys transforms)
|
||||
(mapv (d/getf objects)))))
|
||||
|
||||
prev-shapes (mf/use-var nil)
|
||||
prev-modifiers (mf/use-var nil)
|
||||
prev-transforms (mf/use-var nil)]
|
||||
|
||||
;; Layout effect is important so the code is executed before the modifiers
|
||||
;; are applied to the shape
|
||||
(mf/use-layout-effect
|
||||
(mf/deps transforms)
|
||||
(fn []
|
||||
(when (and (nil? @prev-transforms)
|
||||
(some? transforms))
|
||||
(utils/start-transform! shapes))
|
||||
|
||||
(when (some? modifiers)
|
||||
(utils/update-transform! shapes transforms modifiers))
|
||||
|
||||
|
||||
(when (and (some? @prev-modifiers)
|
||||
(not (some? modifiers)))
|
||||
(utils/remove-transform! @prev-shapes))
|
||||
|
||||
(reset! prev-modifiers modifiers)
|
||||
(reset! prev-transforms transforms)
|
||||
(reset! prev-shapes shapes)))))
|
||||
(let [frame? (into #{} (cph/get-frames-ids objects))
|
||||
;; Removes from zero/shapes attribute all the frames so we can ask only for
|
||||
;; the non-frame children
|
||||
objects (-> objects
|
||||
(update-in [uuid/zero :shapes] #(filterv (comp not frame?) %)))]
|
||||
(cph/get-children-ids objects uuid/zero))))
|
||||
modifiers (select-keys modifiers root-frame-ids)]
|
||||
(sfd/use-dynamic-modifiers objects globals/document modifiers)))
|
||||
|
||||
(defn inside-vbox [vbox objects frame-id]
|
||||
(let [frame (get objects frame-id)]
|
||||
|
@ -246,7 +219,7 @@
|
|||
(gsh/overlaps? frame vbox))))
|
||||
|
||||
(defn setup-active-frames
|
||||
[objects vbox hover active-frames]
|
||||
[objects vbox hover active-frames zoom]
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps vbox)
|
||||
|
@ -262,13 +235,16 @@
|
|||
(reduce-kv set-active-frames {} active-frames))))))
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps @hover @active-frames)
|
||||
(mf/deps @hover @active-frames zoom)
|
||||
(fn []
|
||||
(let [frame-id (if (= :frame (:type @hover))
|
||||
(:id @hover)
|
||||
(:frame-id @hover))]
|
||||
(when (not (contains? @active-frames frame-id))
|
||||
(swap! active-frames assoc frame-id true))))))
|
||||
(if (< zoom 0.25)
|
||||
(when (some? @active-frames)
|
||||
(reset! active-frames nil))
|
||||
(when (and (some? frame-id)(not (contains? @active-frames frame-id)))
|
||||
(reset! active-frames {frame-id true})))))))
|
||||
|
||||
;; NOTE: this is executed on each page change, maybe we need to move
|
||||
;; this shortcuts outside the viewport?
|
||||
|
|
|
@ -1,161 +0,0 @@
|
|||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.workspace.viewport.thumbnail-renderer
|
||||
(:require
|
||||
[app.common.math :as mth]
|
||||
[app.main.data.workspace.persistence :as dwp]
|
||||
[app.main.store :as st]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.object :as obj]
|
||||
[app.util.timers :as timers]
|
||||
[beicon.core :as rx]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(mf/defc frame-thumbnail
|
||||
"Renders the canvas and image for a frame thumbnail and stores its value into the shape"
|
||||
[{:keys [shape background on-thumbnail-data on-frame-not-found]}]
|
||||
|
||||
(let [thumbnail-img (mf/use-ref nil)
|
||||
thumbnail-canvas (mf/use-ref nil)
|
||||
|
||||
{:keys [width height]} shape
|
||||
fixed-width (mth/clamp width 250 2000)
|
||||
fixed-height (/ (* height fixed-width) width)
|
||||
|
||||
on-dom-rendered
|
||||
(mf/use-callback
|
||||
(mf/deps (:id shape))
|
||||
(fn [node]
|
||||
(when node
|
||||
(let [img-node (mf/ref-val thumbnail-img)]
|
||||
(timers/schedule-on-idle
|
||||
#(let [frame-node (dom/get-element (str "shape-" (:id shape)))
|
||||
thumb-node (dom/query frame-node ".frame-thumbnail")
|
||||
loading-node (dom/query frame-node "[data-loading=\"true\"]")]
|
||||
(if (and (some? frame-node)
|
||||
;; Not render if the thumbnail is in display
|
||||
(nil? thumb-node)
|
||||
;; Not render if some image is still loading
|
||||
(nil? loading-node))
|
||||
(let [frame-html (-> (js/XMLSerializer.)
|
||||
(.serializeToString frame-node))
|
||||
|
||||
;; We need to wrap the group node into a SVG with a viewbox that matches the selrect of the frame
|
||||
svg-node (.createElementNS js/document "http://www.w3.org/2000/svg" "svg")
|
||||
_ (.setAttribute svg-node "version" "1.1")
|
||||
_ (.setAttribute svg-node "viewBox" (str (:x shape) " " (:y shape) " " (:width shape) " " (:height shape)))
|
||||
_ (.setAttribute svg-node "width" (:width shape))
|
||||
_ (.setAttribute svg-node "height" (:height shape))
|
||||
_ (unchecked-set svg-node "innerHTML" frame-html)
|
||||
xml (-> (js/XMLSerializer.)
|
||||
(.serializeToString svg-node)
|
||||
js/encodeURIComponent
|
||||
js/unescape
|
||||
js/btoa)
|
||||
img-src (str "data:image/svg+xml;base64," xml)]
|
||||
(obj/set! img-node "src" img-src))
|
||||
|
||||
(on-frame-not-found (:id shape)))))))))
|
||||
|
||||
on-image-load
|
||||
(mf/use-callback
|
||||
(mf/deps on-thumbnail-data background)
|
||||
(fn []
|
||||
(let [canvas-node (mf/ref-val thumbnail-canvas)
|
||||
img-node (mf/ref-val thumbnail-img)
|
||||
|
||||
canvas-context (.getContext canvas-node "2d")
|
||||
canvas-width (.-width canvas-node)
|
||||
canvas-height (.-height canvas-node)
|
||||
|
||||
_ (.clearRect canvas-context 0 0 canvas-width canvas-height)
|
||||
_ (.rect canvas-context 0 0 canvas-width canvas-height)
|
||||
_ (set! (.-fillStyle canvas-context) background)
|
||||
_ (.fill canvas-context)
|
||||
_ (.drawImage canvas-context img-node 0 0 canvas-width canvas-height)
|
||||
|
||||
data (.toDataURL canvas-node "image/jpg" 1)]
|
||||
(on-thumbnail-data data))))]
|
||||
|
||||
[:div.frame-renderer {:ref on-dom-rendered
|
||||
:style {:display "none"}}
|
||||
[:img.thumbnail-img
|
||||
{:ref thumbnail-img
|
||||
:width width
|
||||
:height height
|
||||
:on-load on-image-load}]
|
||||
|
||||
[:canvas.thumbnail-canvas
|
||||
{:ref thumbnail-canvas
|
||||
:width fixed-width
|
||||
:height fixed-height}]]))
|
||||
|
||||
(mf/defc frame-renderer
|
||||
"Component in charge of creating thumbnails and storing them"
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [objects (obj/get props "objects")
|
||||
background (obj/get props "background")
|
||||
|
||||
;; Id of the current frame being rendered
|
||||
shape-id (mf/use-state nil)
|
||||
|
||||
;; This subject will emit a value every time there is a free "slot" to render
|
||||
;; a thumbnail
|
||||
next (mf/use-memo #(rx/behavior-subject :next))
|
||||
|
||||
render-frame
|
||||
(mf/use-callback
|
||||
(fn [frame-id]
|
||||
(reset! shape-id frame-id)))
|
||||
|
||||
updates-stream
|
||||
(mf/use-memo
|
||||
#(let [update-events (rx/filter dwp/update-frame-thumbnail? st/stream)]
|
||||
(->> (rx/zip update-events next)
|
||||
(rx/map first))))
|
||||
|
||||
on-thumbnail-data
|
||||
(mf/use-callback
|
||||
(mf/deps @shape-id)
|
||||
(fn [data]
|
||||
(reset! shape-id nil)
|
||||
(timers/schedule
|
||||
(fn []
|
||||
(st/emit! (dwp/update-shape-thumbnail @shape-id data))
|
||||
(rx/push! next :next)))))
|
||||
|
||||
on-frame-not-found
|
||||
(mf/use-callback
|
||||
(fn [frame-id]
|
||||
;; If we couldn't find the frame maybe is still rendering. We push the event again
|
||||
;; after a time
|
||||
(reset! shape-id nil)
|
||||
(rx/push! next :next)
|
||||
(timers/schedule-on-idle
|
||||
100
|
||||
(st/emitf (dwp/update-frame-thumbnail frame-id)))))]
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps render-frame)
|
||||
(fn []
|
||||
(let [sub (->> updates-stream
|
||||
(rx/subs #(render-frame (-> (deref %) :frame-id))))]
|
||||
|
||||
#(rx/dispose! sub))))
|
||||
|
||||
(mf/use-layout-effect
|
||||
(fn []
|
||||
(timers/schedule-on-idle
|
||||
#(st/emit! (dwp/watch-state-changes)))))
|
||||
|
||||
(when (and (some? @shape-id) (contains? objects @shape-id))
|
||||
[:& frame-thumbnail {:key (str "thumbnail-" @shape-id)
|
||||
:shape (get objects @shape-id)
|
||||
:background background
|
||||
:on-thumbnail-data on-thumbnail-data
|
||||
:on-frame-not-found on-frame-not-found}])))
|
|
@ -77,8 +77,8 @@
|
|||
|
||||
(defn get-nodes
|
||||
"Retrieve the DOM nodes to apply the matrix transformation"
|
||||
[{:keys [id type masked-group?]}]
|
||||
(let [shape-node (dom/get-element (str "shape-" id))
|
||||
[base-node {:keys [id type masked-group?]}]
|
||||
(let [shape-node (dom/query base-node (str "#shape-" id))
|
||||
|
||||
frame? (= :frame type)
|
||||
group? (= :group type)
|
||||
|
@ -86,7 +86,7 @@
|
|||
mask? (and group? masked-group?)
|
||||
|
||||
;; When the shape is a frame we maybe need to move its thumbnail
|
||||
thumb-node (when frame? (dom/get-element (str "thumbnail-" id)))]
|
||||
thumb-node (when frame? (dom/query (str "#thumbnail-" id)))]
|
||||
|
||||
(cond
|
||||
frame?
|
||||
|
@ -108,10 +108,7 @@
|
|||
|
||||
text?
|
||||
[shape-node
|
||||
(dom/query shape-node "foreignObject")
|
||||
(dom/query shape-node ".text-shape")
|
||||
(dom/query shape-node ".text-svg")
|
||||
(dom/query shape-node ".text-clip")]
|
||||
(dom/query shape-node ".text-shape")]
|
||||
|
||||
:else
|
||||
[shape-node])))
|
||||
|
@ -132,9 +129,9 @@
|
|||
(dom/set-attribute! node "height" height)))
|
||||
|
||||
(defn start-transform!
|
||||
[shapes]
|
||||
[base-node shapes]
|
||||
(doseq [shape shapes]
|
||||
(when-let [nodes (get-nodes shape)]
|
||||
(when-let [nodes (get-nodes base-node shape)]
|
||||
(doseq [node nodes]
|
||||
(let [old-transform (dom/get-attribute node "transform")]
|
||||
(when (some? old-transform)
|
||||
|
@ -168,37 +165,24 @@
|
|||
(dom/set-attribute! node att (str new-value))))
|
||||
|
||||
(defn update-transform!
|
||||
[shapes transforms modifiers]
|
||||
[base-node shapes transforms modifiers]
|
||||
(doseq [{:keys [id type] :as shape} shapes]
|
||||
(when-let [nodes (get-nodes shape)]
|
||||
(when-let [nodes (get-nodes base-node shape)]
|
||||
(let [transform (get transforms id)
|
||||
modifiers (get-in modifiers [id :modifiers])
|
||||
|
||||
[text-transform text-width text-height]
|
||||
[text-transform _text-width _text-height]
|
||||
(when (= :text type)
|
||||
(text-corrected-transform shape transform modifiers))
|
||||
|
||||
text-width (str text-width)
|
||||
text-height (str text-height)]
|
||||
(text-corrected-transform shape transform modifiers))]
|
||||
|
||||
(doseq [node nodes]
|
||||
(cond
|
||||
;; Text shapes need special treatment because their resize only change
|
||||
;; the text area, not the change size/position
|
||||
(or (dom/class? node "text-shape")
|
||||
(dom/class? node "text-svg"))
|
||||
(dom/class? node "text-shape")
|
||||
(when (some? text-transform)
|
||||
(set-transform-att! node "transform" text-transform))
|
||||
|
||||
(or (= (dom/get-tag-name node) "foreignObject")
|
||||
(dom/class? node "text-clip"))
|
||||
(let [cur-width (dom/get-attribute node "width")
|
||||
cur-height (dom/get-attribute node "height")]
|
||||
(when (and (some? text-width) (not= cur-width text-width))
|
||||
(dom/set-attribute! node "width" text-width))
|
||||
(when (and (some? text-height) (not= cur-height text-height))
|
||||
(dom/set-attribute! node "height" text-height)))
|
||||
|
||||
(or (= (dom/get-tag-name node) "mask")
|
||||
(= (dom/get-tag-name node) "filter"))
|
||||
(transform-region! node modifiers)
|
||||
|
@ -214,9 +198,9 @@
|
|||
(set-transform-att! node "transform" transform)))))))
|
||||
|
||||
(defn remove-transform!
|
||||
[shapes]
|
||||
[base-node shapes]
|
||||
(doseq [shape shapes]
|
||||
(when-let [nodes (get-nodes shape)]
|
||||
(when-let [nodes (get-nodes base-node shape)]
|
||||
(doseq [node nodes]
|
||||
(when (some? node)
|
||||
(cond
|
||||
|
|
|
@ -341,17 +341,41 @@
|
|||
{:pre [(blob? b)]}
|
||||
(js/URL.createObjectURL b))
|
||||
|
||||
(defn make-node
|
||||
([namespace name]
|
||||
(.createElementNS globals/document namespace name))
|
||||
|
||||
([name]
|
||||
(.createElement globals/document name)))
|
||||
|
||||
(defn node->xml
|
||||
[node]
|
||||
(-> (js/XMLSerializer.)
|
||||
(.serializeToString node)))
|
||||
|
||||
(defn svg->data-uri
|
||||
[svg]
|
||||
(assert (string? svg))
|
||||
(let [b64 (-> svg
|
||||
js/encodeURIComponent
|
||||
js/unescape
|
||||
js/btoa)]
|
||||
(dm/str "data:image/svg+xml;base64," b64)))
|
||||
|
||||
(defn set-property! [^js node property value]
|
||||
(when (some? node)
|
||||
(.setAttribute node property value)))
|
||||
(.setAttribute node property value))
|
||||
node)
|
||||
|
||||
(defn set-text! [^js node text]
|
||||
(when (some? node)
|
||||
(set! (.-textContent node) text)))
|
||||
(set! (.-textContent node) text))
|
||||
node)
|
||||
|
||||
(defn set-css-property! [^js node property value]
|
||||
(when (some? node)
|
||||
(.setProperty (.-style ^js node) property value)))
|
||||
(.setProperty (.-style ^js node) property value))
|
||||
node)
|
||||
|
||||
(defn capture-pointer [^js event]
|
||||
(when (some? event)
|
||||
|
@ -382,7 +406,8 @@
|
|||
(defn add-class! [^js node class-name]
|
||||
(when (some? node)
|
||||
(let [class-list (.-classList ^js node)]
|
||||
(.add ^js class-list class-name))))
|
||||
(.add ^js class-list class-name)))
|
||||
node)
|
||||
|
||||
(defn remove-class! [^js node class-name]
|
||||
(when (some? node)
|
||||
|
|
Loading…
Add table
Reference in a new issue