mirror of
https://github.com/penpot/penpot.git
synced 2026-05-24 09:23:40 +00:00
* 🐛 Fix crash in apply-text-modifier with nil selrect or modifier Guard apply-text-modifier against nil text-modifier and nil selrect to prevent the 'invalid arguments (on pointer constructor)' error thrown by gpt/point when called with an invalid map. - In text-wrapper: only call apply-text-modifier when text-modifier is not nil (avoids unnecessary processing) - In apply-text-modifier: handle nil text-modifier by returning shape unchanged; guard selrect access before calling gpt/point * 📚 Add tests for apply-text-modifier in workspace texts Add exhaustive unit tests covering all paths of apply-text-modifier: - nil modifier returns shape unchanged (identity) - modifier with no recognised keys leaves shape unchanged - :width / :height modifiers resize shape correctly - nil :width / :height keys are skipped - both dimensions applied simultaneously - :position-data is set and nil-guarded - position-data coordinates translated by delta on resize - shape with nil selrect + nil modifier does not throw - position-data-only modifier on shape without selrect is safe - selrect origin preserved when no dimension changes - result always carries required shape keys * 🐛 Fix zero-dimension selrect crash in change-dimensions-modifiers When a text shape is decoded from the server via map->Rect (which bypasses make-rect's 0.01 minimum enforcement), its selrect can have width or height of exactly 0. change-dimensions-modifiers and change-size were dividing by these values, producing Infinity scale factors that propagated through the transform pipeline until calculate-selrect / center->rect returned nil, causing gpt/point to throw 'invalid arguments (on pointer constructor)'. Fix: before computing scale factors, guard sr-width / sr-height (and old-width / old-height in change-size) against zero/negative and non-finite values. When degenerate, fall back to the shape's own top-level :width/:height so the denominator and proportion-lock base remain consistent. Also simplify apply-text-modifier's delta calculation now that the transform pipeline is guaranteed to produce a valid selrect, and update the test suite to test the exact degenerate-selrect scenario that triggered the original crash. Signed-off-by: Andrey Antukh <niwi@niwi.nz> * ♻️ Simplify change-dimensions-modifiers internal logic - Remove the intermediate 'size' map ({:width sr-width :height sr-height}) that was built only to be assoc'd and immediately destructured back into width/height; compute both values directly instead. - Replace the double-negated condition 'if-not (and (not ignore-lock?) …)' with a clear positive 'locked?' binding, and flatten the three-branch if-not/if tree into two independent if expressions keyed on 'attr'. - Call safe-size-rect once and reuse its result for both the fallback sizes and the scale computation, eliminating a redundant call. - Access :transform and :transform-inverse via direct map lookup rather than destructuring in the function signature, consistent with how the rest of the let-block reads shape keys. - Clean up change-size to use the same destructuring style as the updated function ({sr-width :width sr-height :height}). - Fix typo in comment: 'havig' -> 'having'. * ✨ Add tests for change-size and change-dimensions-modifiers Cover the main behavioural contract of both functions: change-size: - Scales both axes to the requested target dimensions. - Sets the resize origin to the shape's top-left point. - Nil width/height each fall back to the current dimension (scale 1 on that axis); both nil produces an identity resize that is optimised away. - Propagates the shape's transform and transform-inverse matrices into the resulting GeometricOperation. change-dimensions-modifiers: - Changing :width without proportion-lock only scales the x-axis (y scale stays 1), and vice-versa for :height. - With proportion-lock enabled, changing :width adjusts height via the inverse proportion, and changing :height adjusts width via the proportion. - ignore-lock? true bypasses proportion-lock regardless of shape state. - Values below 0.01 are clamped to 0.01 before computing the scale. - End-to-end: applying the returned modifiers via gsh/transform-shape yields the expected selrect dimensions. * ✨ Harden safe-size-rect with additional fallbacks The previous implementation could still return an invalid rect in several edge cases. The new version tries four sources in order, accepting each only if it passes a dedicated safe-size-rect? predicate: 1. :selrect – used when width and height are finite, positive and within [-max-safe-int, max-safe-int]. 2. points->rect – computed from the shape corner points; subject to the same predicate. 3. Top-level shape fields (:x :y :width :height) – present on all rect, frame, image, and component shape types. 4. grc/empty-rect – a 0,0 0.01×0.01 unit rect used as last resort so callers always receive a usable, non-crashing value. The out-of-range check (> max-safe-int) is new: it rejects coordinates that pass d/num? (finite) but exceed the platform integer boundary defined in app.common.schema, which previously slipped through undetected. Tests cover all four fallback paths, including the NaN, zero-dimension, and max-safe-int overflow cases. * ⚡ Optimise safe-size-rect for ClojureScript performance - Replace (when (some? rect) ...) with (and ^boolean (some? rect) ...) to keep the entire predicate as a single boolean expression without introducing an implicit conditional branch. - Replace keyword access (:width rect) / (:height rect) with dm/get-prop calls, consistent with the hot-path style used throughout the rest of the namespace. - Add ^boolean type hints to every sub-expression of the and chain in safe-size-rect? (d/num?, pos?, <=) so the ClojureScript compiler emits raw JS boolean operations instead of boxing the results through cljs.core/truth_. - Replace (when (safe-size-rect? ...) value) in safe-size-rect with (and ^boolean (safe-size-rect? ...) value), avoiding an extra conditional and keeping the or fallback chain free of allocated intermediate objects. * ✨ Use safe-size-rect in apply-text-modifier delta-move computation safe-size-rect was already used inside change-dimensions-modifiers to guard the resize scale computation. However, apply-text-modifier in texts.cljs was still reading (:selrect shape) and (:selrect new-shape) directly to build the delta-move vector via gpt/point. gpt/point raises "invalid arguments (on pointer constructor)" when given a nil value or a map with non-finite :x/:y, which can happen when a shape's selrect is missing or degenerate (e.g. decoded from the server via map->Rect, bypassing make-rect's 0.01 floor). Changes: - Promote safe-size-rect from defn- to defn in app.common.types.modifiers so it can be reused by consumers outside the namespace. - Replace the two raw (:selrect …) accesses in the delta-move computation with (ctm/safe-size-rect …), which always returns a valid, finite rect through the established four-step fallback chain. - Add two frontend tests covering the delta-move path with a fully degenerate (zero-dimension) selrect, ensuring neither a bare position-data modifier nor a combined width+position-data modifier throws. * ♻️ Ensure all test shapes are proper Shape records in modifiers-test All shapes in safe-size-rect-fallbacks tests now start from a proper Shape record built by cts/setup-shape (via make-shape) instead of plain hash-maps. Each test that mutates geometry fields (selrect, points, width, height) does so via assoc on the already-initialised record, which preserves the correct type while isolating the field under test. A (cts/shape? shape) assertion is added to each fallback test to make the type guarantee explicit and guard against regressions. The unused shape-with-selrect helper (which built a bare map) is removed. * 🔥 Remove dead code and tighten visibility in app.common.types.modifiers Dead functions removed (zero callers across the entire codebase): - modifiers->transform-old: superseded by modifiers->transform; only ever appeared in a commented-out dev/bench.cljs entry. - change-recursive-property: no callers anywhere. - move-parent-modifiers, resize-parent-modifiers: convenience wrappers for the parent-geometry builder functions; never called. - remove-children-modifiers, add-children-modifiers, scale-content-modifiers: single-op convenience builders; never called. - select-structure: projection helper; only referenced by select-child-geometry-modifiers which is itself dead. - select-child-geometry-modifiers: no callers anywhere. Functions narrowed from defn to defn- (used only within this namespace): - valid-vector?: assertion helper called only by move/resize builders. - increase-order: called only by add-modifiers. - transform-move!, transform-resize!, transform-rotate!, transform!: steps of the modifiers->transform pipeline. - modifiers->transform1: immediate helper for modifiers->transform; the doc-string describing it as 'multiplatform' was also removed since it is an implementation detail. - transform-text-node, transform-paragraph-node: leaf helpers for scale-text-content. - update-text-content, scale-text-content, apply-scale-content: internal scale-content pipeline; all called only by apply-modifier. - remove-children-set: called only by apply-modifier. - select-structure: demoted to defn- rather than deleted because it is still called by select-child-structre-modifiers, which has external callers. * ✨ Add more tests for modifiers --------- Signed-off-by: Andrey Antukh <niwi@niwi.nz>
995 lines
38 KiB
Clojure
995 lines
38 KiB
Clojure
;; 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) KALEIDOS INC
|
|
|
|
(ns app.main.data.workspace.texts
|
|
(:require
|
|
["@penpot/text-editor" :as editor.v2]
|
|
[app.common.attrs :as attrs]
|
|
[app.common.data :as d]
|
|
[app.common.data.macros :as dm]
|
|
[app.common.files.helpers :as cfh]
|
|
[app.common.geom.point :as gpt]
|
|
[app.common.geom.shapes :as gsh]
|
|
[app.common.math :as mth]
|
|
[app.common.types.fills :as types.fills]
|
|
[app.common.types.modifiers :as ctm]
|
|
[app.common.types.shape.layout :as ctl]
|
|
[app.common.types.text :as txt]
|
|
[app.common.uuid :as uuid]
|
|
[app.main.data.event :as ev]
|
|
[app.main.data.helpers :as dsh]
|
|
[app.main.data.workspace.common :as dwc]
|
|
[app.main.data.workspace.libraries :as dwl]
|
|
[app.main.data.workspace.modifiers :as dwm]
|
|
[app.main.data.workspace.selection :as dws]
|
|
[app.main.data.workspace.shapes :as dwsh]
|
|
[app.main.data.workspace.transforms :as dwt]
|
|
[app.main.data.workspace.undo :as dwu]
|
|
[app.main.data.workspace.wasm-text :as dwwt]
|
|
[app.main.features :as features]
|
|
[app.main.fonts :as fonts]
|
|
[app.main.router :as rt]
|
|
[app.render-wasm.api :as wasm.api]
|
|
[app.util.text-editor :as ted]
|
|
[app.util.text.content.styles :as styles]
|
|
[app.util.timers :as ts]
|
|
[beicon.v2.core :as rx]
|
|
[cuerdas.core :as str]
|
|
[potok.v2.core :as ptk]))
|
|
|
|
;; -- V2 Editor Helpers
|
|
|
|
(def ^function create-root-from-string editor.v2/createRootFromString)
|
|
(def ^function create-root-from-html editor.v2/createRootFromHTML)
|
|
(def ^function create-editor editor.v2/create)
|
|
(def ^function set-editor-root! editor.v2/setRoot)
|
|
(def ^function get-editor-root editor.v2/getRoot)
|
|
(def ^function is-empty? editor.v2/isEmpty)
|
|
(def ^function dispose! editor.v2/dispose)
|
|
|
|
(declare v2-update-text-shape-content)
|
|
(declare v2-update-text-editor-styles)
|
|
|
|
;; -- Content helpers
|
|
|
|
(defn- v2-content-has-text?
|
|
[content]
|
|
(boolean
|
|
(when content
|
|
(some (fn [node]
|
|
(not (str/blank? (:text node ""))))
|
|
(txt/node-seq txt/is-text-node? content)))))
|
|
|
|
;; -- Editor
|
|
|
|
(defn update-editor
|
|
[editor]
|
|
(ptk/reify ::update-editor
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(if (some? editor)
|
|
(assoc state :workspace-editor editor)
|
|
(dissoc state :workspace-editor)))))
|
|
|
|
(defn focus-editor
|
|
[]
|
|
(ptk/reify ::focus-editor
|
|
ptk/EffectEvent
|
|
(effect [_ state _]
|
|
(let [editor (:workspace-editor state)
|
|
element (when editor (.-element editor))]
|
|
(when (and element (.-focus element))
|
|
(ts/schedule #(.focus ^js element)))))))
|
|
|
|
(defn gen-name
|
|
[editor]
|
|
(when (some? editor)
|
|
(let [result
|
|
(-> (ted/get-editor-current-plain-text editor)
|
|
(txt/generate-shape-name))]
|
|
(when (not= result "") result))))
|
|
|
|
(defn update-editor-state
|
|
[{:keys [id] :as shape} editor-state]
|
|
(ptk/reify ::update-editor-state
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(if (some? editor-state)
|
|
(update state :workspace-editor-state assoc id editor-state)
|
|
(update state :workspace-editor-state dissoc id)))))
|
|
|
|
(defn finalize-editor-state
|
|
[id update-name?]
|
|
(ptk/reify ::finalize-editor-state
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(when (dwc/initialized? state)
|
|
(let [objects (dsh/lookup-page-objects state)
|
|
shape (get objects id)
|
|
editor-state (get-in state [:workspace-editor-state id])
|
|
content (-> editor-state
|
|
(ted/get-editor-current-content))
|
|
name (gen-name editor-state)
|
|
|
|
new-shape? (nil? (:content shape))]
|
|
(if (ted/content-has-text? content)
|
|
(if (features/active-feature? state "render-wasm/v1")
|
|
(let [content (d/merge (ted/export-content content)
|
|
(dissoc (:content shape) :children))]
|
|
(rx/merge
|
|
(rx/of (update-editor-state shape nil))
|
|
(when (and (not= content (:content shape))
|
|
(some? (:current-page-id state))
|
|
(some? shape))
|
|
(rx/of
|
|
(dwsh/update-shapes
|
|
[id]
|
|
(fn [shape]
|
|
(-> shape
|
|
(assoc :content content)
|
|
(cond-> (and update-name? (some? name))
|
|
(assoc :name name))))
|
|
{:undo-group (when new-shape? id)})
|
|
|
|
(dwm/apply-wasm-modifiers
|
|
(dwwt/resize-wasm-text-modifiers shape content)
|
|
{:undo-group (when new-shape? id)})))))
|
|
|
|
(let [content (d/merge (ted/export-content content)
|
|
(dissoc (:content shape) :children))
|
|
modifiers (get-in state [:workspace-text-modifier id])]
|
|
(rx/merge
|
|
(rx/of (update-editor-state shape nil))
|
|
(when (and (not= content (:content shape))
|
|
(some? (:current-page-id state))
|
|
(some? shape))
|
|
(rx/of
|
|
(dwsh/update-shapes
|
|
[id]
|
|
(fn [shape]
|
|
(let [{:keys [width height position-data]} modifiers]
|
|
(-> shape
|
|
(assoc :content content)
|
|
(cond-> position-data
|
|
(assoc :position-data position-data))
|
|
(cond-> (and update-name? (some? name))
|
|
(assoc :name name))
|
|
(cond-> (or (some? width) (some? height))
|
|
(gsh/transform-shape (ctm/change-size shape width height))))))
|
|
{:undo-group (when new-shape? id)}))))))
|
|
|
|
(when (some? id)
|
|
(rx/of (dws/deselect-shape id)
|
|
(dwsh/delete-shapes #{id})))))))))
|
|
|
|
(defn initialize-editor-state
|
|
[{:keys [id name content] :as shape} decorator]
|
|
(ptk/reify ::initialize-editor-state
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(let [text-state (some->> content ted/import-content)
|
|
attrs (merge (txt/get-default-text-attrs)
|
|
(get-in state [:workspace-global :default-font]))
|
|
editor (cond-> (ted/create-editor-state text-state decorator)
|
|
(and (nil? content) (some? attrs))
|
|
(ted/update-editor-current-block-data attrs))]
|
|
(-> state
|
|
(assoc-in [:workspace-editor-state id] editor))))
|
|
|
|
ptk/WatchEvent
|
|
(watch [_ state stream]
|
|
;; We need to finalize editor on two main events: (1) when user
|
|
;; explicitly navigates to other section or page; (2) when user
|
|
;; leaves the editor.
|
|
(let [editor (dm/get-in state [:workspace-editor-state id])
|
|
update-name? (or (nil? content) (= name (gen-name editor)))]
|
|
(->> (rx/merge
|
|
(rx/filter (ptk/type? ::rt/navigate) stream)
|
|
(rx/filter #(= ::finalize-editor-state %) stream))
|
|
(rx/take 1)
|
|
(rx/map #(finalize-editor-state id update-name?)))))))
|
|
|
|
(defn select-all
|
|
"Select all content of the current editor. When not editor found this
|
|
event is noop."
|
|
[{:keys [id] :as shape}]
|
|
(ptk/reify ::editor-select-all
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(d/update-in-when state [:workspace-editor-state id] ted/editor-select-all))))
|
|
|
|
(defn cursor-to-end
|
|
[{:keys [id] :as shape}]
|
|
(ptk/reify ::cursor-to-end
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(d/update-in-when state [:workspace-editor-state id] ted/cursor-to-end))))
|
|
|
|
;; --- Helpers
|
|
|
|
(defn- to-new-fills
|
|
[data]
|
|
;; FIXME: maybe export this as a specific helper ?
|
|
(types.fills/create
|
|
(d/without-nils (select-keys data types.fills/fill-attrs))))
|
|
|
|
(defn- shape-current-values
|
|
[shape pred attrs]
|
|
(let [root (:content shape)
|
|
nodes (->> (txt/node-seq pred root)
|
|
(map (fn [node]
|
|
(if (txt/is-text-node? node)
|
|
(let [default-text-attrs
|
|
(txt/get-default-text-attrs)
|
|
|
|
fills
|
|
(cond
|
|
(types.fills/has-valid-fill-attrs? node)
|
|
(to-new-fills node)
|
|
|
|
(some? (:fills node))
|
|
(:fills node)
|
|
|
|
:else
|
|
(:fills default-text-attrs))]
|
|
|
|
(-> (merge default-text-attrs node)
|
|
(assoc :fills fills)))
|
|
node))))]
|
|
(attrs/get-attrs-multi nodes attrs)))
|
|
|
|
(defn current-root-values
|
|
[{:keys [attrs shape]}]
|
|
(shape-current-values shape txt/is-root-node? attrs))
|
|
|
|
(defn v2-current-text-values
|
|
[{:keys [editor-instance attrs]}]
|
|
(let [result (-> (.-currentStyle editor-instance)
|
|
(styles/get-styles-from-style-declaration)
|
|
(select-keys attrs))
|
|
result (if (empty? result) txt/default-text-attrs result)]
|
|
result))
|
|
|
|
(defn v1-current-paragraph-values
|
|
[{:keys [editor-state attrs shape]}]
|
|
(if editor-state
|
|
(-> (ted/get-editor-current-block-data editor-state)
|
|
(select-keys attrs))
|
|
(shape-current-values shape txt/is-paragraph-node? attrs)))
|
|
|
|
(defn current-paragraph-values
|
|
[{:keys [editor-state editor-instance attrs shape] :as options}]
|
|
(cond
|
|
(some? editor-instance) (v2-current-text-values options)
|
|
(some? editor-state) (v1-current-paragraph-values options)
|
|
:else (shape-current-values shape txt/is-paragraph-node? attrs)))
|
|
|
|
(defn v1-current-text-values
|
|
[{:keys [editor-state attrs]}]
|
|
(let [result (-> (ted/get-editor-current-inline-styles editor-state)
|
|
(select-keys attrs))
|
|
result (if (empty? result)
|
|
(txt/get-default-text-attrs)
|
|
result)]
|
|
result))
|
|
|
|
(defn current-text-values
|
|
[{:keys [editor-state editor-instance attrs shape] :as options}]
|
|
(cond
|
|
(some? editor-instance) (v2-current-text-values options)
|
|
(some? editor-state) (v1-current-text-values options)
|
|
:else (shape-current-values shape txt/is-text-node? attrs)))
|
|
|
|
;; --- TEXT EDITION IMPL
|
|
|
|
(defn count-node-chars
|
|
([node]
|
|
(count-node-chars node false))
|
|
([node last?]
|
|
(case (:type node)
|
|
("root" "paragraph-set")
|
|
(apply + (concat (map count-node-chars (drop-last (:children node)))
|
|
(map #(count-node-chars % true) (take-last 1 (:children node)))))
|
|
|
|
"paragraph"
|
|
(+ (apply + (map count-node-chars (:children node))) (if last? 0 1))
|
|
|
|
(count (:text node)))))
|
|
|
|
|
|
(defn decorate-range-info
|
|
"Adds information about ranges inside the metadata of the text nodes"
|
|
[content]
|
|
(->> (with-meta content {:start 0 :end (count-node-chars content)})
|
|
(txt/transform-nodes
|
|
(fn [node]
|
|
(d/update-when
|
|
node
|
|
:children
|
|
(fn [children]
|
|
(let [start (-> node meta (:start 0))]
|
|
(->> children
|
|
(reduce (fn [[result start] node]
|
|
(let [end (+ start (count-node-chars node))]
|
|
[(-> result
|
|
(conj (with-meta node {:start start :end end})))
|
|
end]))
|
|
[[] start])
|
|
(first)))))))))
|
|
|
|
(defn split-content-at
|
|
[content position]
|
|
(->> content
|
|
(txt/transform-nodes
|
|
(fn [node]
|
|
(and (txt/is-paragraph-node? node)
|
|
(< (-> node meta :start) position (-> node meta :end))))
|
|
(fn [node]
|
|
(letfn
|
|
[(process-node [child]
|
|
(let [start (-> child meta :start)
|
|
end (-> child meta :end)]
|
|
(if (< start position end)
|
|
[(-> child
|
|
(vary-meta assoc :end position)
|
|
(update :text subs 0 (- position start)))
|
|
(-> child
|
|
(vary-meta assoc :start position)
|
|
(update :text subs (- position start)))]
|
|
[child])))]
|
|
(-> node
|
|
(d/update-when :children #(into [] (mapcat process-node) %))))))))
|
|
|
|
(defn update-content-range
|
|
[content start end attrs]
|
|
(->> content
|
|
(txt/transform-nodes
|
|
(fn [node]
|
|
(and (txt/is-text-node? node)
|
|
(and (>= (-> node meta :start) start)
|
|
(<= (-> node meta :end) end))))
|
|
#(d/patch-object % attrs))))
|
|
|
|
(defn- update-text-range-attrs
|
|
[shape start end attrs]
|
|
(let [new-content (-> (:content shape)
|
|
(decorate-range-info)
|
|
(split-content-at start)
|
|
(split-content-at end)
|
|
(update-content-range start end attrs))]
|
|
(assoc shape :content new-content)))
|
|
|
|
(defn update-text-range
|
|
[id start end attrs]
|
|
(ptk/reify ::update-text-range
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [objects (dsh/lookup-page-objects state)
|
|
shape (get objects id)
|
|
|
|
update-fn
|
|
(fn [shape]
|
|
(cond-> shape
|
|
(cfh/text-shape? shape)
|
|
(update-text-range-attrs start end attrs)))
|
|
|
|
shape-ids (cond (cfh/text-shape? shape) [id]
|
|
(cfh/group-shape? shape) (cfh/get-children-ids objects id))]
|
|
|
|
(rx/of (dwsh/update-shapes shape-ids update-fn))))))
|
|
|
|
(defn update-root-attrs
|
|
[{:keys [id attrs]}]
|
|
(ptk/reify ::update-root-attrs
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [objects (dsh/lookup-page-objects state)
|
|
shape (get objects id)
|
|
|
|
update-fn
|
|
(fn [shape]
|
|
(if (some? (:content shape))
|
|
(txt/update-text-content shape txt/is-root-node? d/txt-merge attrs)
|
|
(assoc shape :content (d/txt-merge {:type "root"} attrs))))
|
|
|
|
shape-ids
|
|
(cond (cfh/text-shape? shape) [id]
|
|
(cfh/group-shape? shape) (cfh/get-children-ids objects id))]
|
|
|
|
(rx/of (dwsh/update-shapes shape-ids update-fn))))))
|
|
|
|
(defn update-paragraph-attrs
|
|
[{:keys [id attrs]}]
|
|
(let [attrs (d/without-nils attrs)]
|
|
(ptk/reify ::update-paragraph-attrs
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(d/update-in-when state [:workspace-editor-state id] ted/update-editor-current-block-data attrs))
|
|
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(when-not (some? (get-in state [:workspace-editor-state id]))
|
|
(let [objects (dsh/lookup-page-objects state)
|
|
shape (get objects id)
|
|
|
|
merge-fn (fn [node attrs]
|
|
(reduce-kv
|
|
(fn [node k v] (assoc node k v))
|
|
node
|
|
attrs))
|
|
|
|
update-fn #(txt/update-text-content % txt/is-paragraph-node? merge-fn attrs)
|
|
shape-ids (cond
|
|
(cfh/text-shape? shape) [id]
|
|
(cfh/group-shape? shape) (cfh/get-children-ids objects id))]
|
|
|
|
(rx/of (dwsh/update-shapes shape-ids update-fn))))))))
|
|
|
|
(defn update-text-attrs
|
|
[{:keys [id attrs]}]
|
|
(ptk/reify ::update-text-attrs
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(d/update-in-when state [:workspace-editor-state id] ted/update-editor-current-inline-styles attrs))
|
|
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(when-not (some? (get-in state [:workspace-editor-state id]))
|
|
(let [objects (dsh/lookup-page-objects state)
|
|
shape (get objects id)
|
|
update-node? (fn [node]
|
|
(or (txt/is-text-node? node)
|
|
(txt/is-paragraph-node? node)))
|
|
shape-ids (cond
|
|
(cfh/text-shape? shape) [id]
|
|
(cfh/group-shape? shape) (cfh/get-children-ids objects id))]
|
|
(rx/of (dwsh/update-shapes shape-ids #(txt/update-text-content % update-node? d/txt-merge attrs))))))))
|
|
|
|
(defn migrate-node
|
|
[node]
|
|
(let [color-attrs (not-empty (select-keys node types.fills/fill-attrs))]
|
|
(cond-> node
|
|
(nil? (:fills node))
|
|
(assoc :fills (types.fills/create))
|
|
|
|
;; Migrate old colors and remove the old fromat
|
|
color-attrs
|
|
(-> (dissoc :fill-color :fill-opacity :fill-color-ref-id :fill-color-ref-file :fill-color-gradient)
|
|
(update :fills types.fills/update conj color-attrs))
|
|
|
|
;; We don't have the fills attribute. It's an old text without color
|
|
;; so need to be black
|
|
(and (nil? (:fills node)) (empty? color-attrs))
|
|
(assoc :fills (txt/get-default-text-fills)))))
|
|
|
|
(defn migrate-content
|
|
[content]
|
|
(txt/transform-nodes (some-fn txt/is-text-node? txt/is-paragraph-node?) migrate-node content))
|
|
|
|
(defn update-text-with-function
|
|
([id update-node-fn] (update-text-with-function id update-node-fn nil))
|
|
([id update-node-fn options]
|
|
(ptk/reify ::update-text-with-function
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(d/update-in-when state [:workspace-editor-state id] ted/update-editor-current-inline-styles-fn (comp update-node-fn migrate-node)))
|
|
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(when (or
|
|
(and (features/active-feature? state "text-editor/v2") (nil? (:workspace-editor state)))
|
|
(and (not (features/active-feature? state "text-editor/v2")) (nil? (get-in state [:workspace-editor-state id]))))
|
|
(let [page-id (or (get options :page-id)
|
|
(get state :current-page-id))
|
|
objects (dsh/lookup-page-objects state page-id)
|
|
shape (get objects id)
|
|
update-node? (some-fn txt/is-text-node? txt/is-paragraph-node?)
|
|
|
|
shape-ids
|
|
(cond
|
|
(cfh/text-shape? shape) [id]
|
|
(cfh/group-shape? shape) (cfh/get-children-ids objects id))
|
|
|
|
update-content
|
|
(fn [content]
|
|
(->> content
|
|
(migrate-content)
|
|
(txt/transform-nodes update-node? update-node-fn)))
|
|
|
|
update-shape
|
|
(fn [shape]
|
|
(-> shape
|
|
(dissoc :fills)
|
|
(d/update-when :content update-content)))]
|
|
(rx/of (dwsh/update-shapes shape-ids update-shape options)))))
|
|
|
|
ptk/EffectEvent
|
|
(effect [_ state _]
|
|
(when (features/active-feature? state "text-editor/v2")
|
|
(when-let [instance (:workspace-editor state)]
|
|
(let [styles (some-> (editor.v2/getCurrentStyle instance)
|
|
(styles/get-styles-from-style-declaration :removed-mixed true)
|
|
((comp update-node-fn migrate-node))
|
|
(styles/attrs->styles))]
|
|
(editor.v2/applyStylesToSelection instance styles))))))))
|
|
|
|
;; --- RESIZE UTILS
|
|
|
|
(def start-edit-if-selected
|
|
(ptk/reify ::start-edit-if-selected
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(let [objects (dsh/lookup-page-objects state)
|
|
selected (->> state dsh/lookup-selected (mapv #(get objects %)))]
|
|
(cond-> state
|
|
(and (= 1 (count selected))
|
|
(= (-> selected first :type) :text))
|
|
(assoc-in [:workspace-local :edition] (-> selected first :id)))))))
|
|
|
|
(defn not-changed? [old-dim new-dim]
|
|
(> (mth/abs (- old-dim new-dim)) 0.1))
|
|
|
|
(defn commit-resize-text
|
|
[]
|
|
(ptk/reify ::commit-resize-text
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [props (::resize-text-debounce-props state)
|
|
objects (dsh/lookup-page-objects state)
|
|
undo-id (js/Symbol)]
|
|
|
|
(letfn [(changed-text? [id]
|
|
(let [shape (get objects id)
|
|
[new-width new-height] (get props id)]
|
|
(or (and (not-changed? (:width shape) new-width) (= (:grow-type shape) :auto-width))
|
|
(and (not-changed? (:height shape) new-height)
|
|
(or (= (:grow-type shape) :auto-height) (= (:grow-type shape) :auto-width))))))
|
|
|
|
(update-fn [{:keys [id selrect grow-type] :as shape}]
|
|
(let [{shape-width :width shape-height :height} selrect
|
|
[new-width new-height] (get props id)
|
|
|
|
shape
|
|
(cond-> shape
|
|
(and (or (not (ctl/any-layout-immediate-child? objects shape))
|
|
(not (ctl/fill-width? shape)))
|
|
(not-changed? shape-width new-width)
|
|
(= grow-type :auto-width))
|
|
(gsh/transform-shape (ctm/change-dimensions-modifiers shape :width new-width {:ignore-lock? true})))
|
|
|
|
shape
|
|
(cond-> shape
|
|
(and (or (not (ctl/any-layout-immediate-child? objects shape))
|
|
(not (ctl/fill-height? shape)))
|
|
(not-changed? shape-height new-height)
|
|
(or (= grow-type :auto-height) (= grow-type :auto-width)))
|
|
(gsh/transform-shape (ctm/change-dimensions-modifiers shape :height new-height {:ignore-lock? true})))]
|
|
|
|
shape))]
|
|
|
|
(let [ids (into #{} (filter changed-text?) (keys props))]
|
|
(rx/of (dwu/start-undo-transaction undo-id)
|
|
(dwsh/update-shapes ids update-fn {:with-objects? true
|
|
:reg-objects? true
|
|
:stack-undo? true
|
|
:ignore-touched true})
|
|
(ptk/data-event :layout/update {:ids ids})
|
|
(dwu/commit-undo-transaction undo-id))))))))
|
|
|
|
(defn resize-text
|
|
[id new-width new-height]
|
|
|
|
(let [cur-event (js/Symbol)]
|
|
(ptk/reify ::resize-text
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(-> state
|
|
(update ::resize-text-debounce-props (fnil assoc {}) id [new-width new-height])
|
|
(cond-> (nil? (::resize-text-debounce-event state))
|
|
(assoc ::resize-text-debounce-event cur-event))))
|
|
|
|
ptk/WatchEvent
|
|
(watch [_ state stream]
|
|
(if (= (::resize-text-debounce-event state) cur-event)
|
|
(let [stopper (->> stream (rx/filter (ptk/type? :app.main.data.workspace/finalize)))]
|
|
(rx/concat
|
|
(rx/merge
|
|
(->> stream
|
|
(rx/filter (ptk/type? ::resize-text))
|
|
(rx/debounce 50)
|
|
(rx/take 1)
|
|
(rx/map #(commit-resize-text))
|
|
(rx/take-until stopper))
|
|
(rx/of (resize-text id new-width new-height)))
|
|
(rx/of #(dissoc % ::resize-text-debounce-props ::resize-text-debounce-event))))
|
|
(rx/empty))))))
|
|
|
|
(defn save-font
|
|
[data]
|
|
(ptk/reify ::save-font
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(let [multiple? (->> data vals (d/seek #(= % :multiple)))]
|
|
(cond-> state
|
|
(not multiple?)
|
|
(assoc-in [:workspace-global :default-font] data))))))
|
|
|
|
(defn apply-text-modifier
|
|
[shape text-modifier]
|
|
|
|
(if (some? text-modifier)
|
|
(let [{:keys [width height position-data]} text-modifier
|
|
new-shape
|
|
(cond-> shape
|
|
(some? width)
|
|
(gsh/transform-shape (ctm/change-dimensions-modifiers shape :width width {:ignore-lock? true}))
|
|
|
|
(some? height)
|
|
(gsh/transform-shape (ctm/change-dimensions-modifiers shape :height height {:ignore-lock? true}))
|
|
|
|
(some? position-data)
|
|
(assoc :position-data position-data))
|
|
|
|
delta-move
|
|
(gpt/subtract (gpt/point (ctm/safe-size-rect new-shape))
|
|
(gpt/point (ctm/safe-size-rect shape)))
|
|
|
|
new-shape
|
|
(update new-shape :position-data gsh/move-position-data delta-move)]
|
|
|
|
new-shape)
|
|
shape))
|
|
|
|
(defn commit-update-text-modifier
|
|
[]
|
|
(ptk/reify ::commit-update-text-modifier
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [ids (::update-text-modifier-debounce-ids state)
|
|
modif-tree (dwm/create-modif-tree ids (ctm/reflow-modifiers))]
|
|
(rx/of (dwm/update-modifiers modif-tree false true))))))
|
|
|
|
(defn update-text-modifier
|
|
[id props]
|
|
|
|
(let [cur-event (js/Symbol)]
|
|
(ptk/reify ::update-text-modifier
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(-> state
|
|
(update-in [:workspace-text-modifier id] (fnil merge {}) props)
|
|
(update ::update-text-modifier-debounce-ids (fnil conj #{}) id)
|
|
(cond-> (nil? (::update-text-modifier-debounce-event state))
|
|
(assoc ::update-text-modifier-debounce-event cur-event))))
|
|
|
|
ptk/WatchEvent
|
|
(watch [_ state stream]
|
|
(if (= (::update-text-modifier-debounce-event state) cur-event)
|
|
(let [stopper (->> stream (rx/filter (ptk/type? :app.main.data.workspace/finalize)))]
|
|
(rx/concat
|
|
(rx/merge
|
|
(->> stream
|
|
(rx/filter (ptk/type? ::update-text-modifier))
|
|
(rx/debounce 50)
|
|
(rx/take 1)
|
|
(rx/map #(commit-update-text-modifier))
|
|
(rx/take-until stopper))
|
|
(rx/of (update-text-modifier id props)))
|
|
(rx/of #(dissoc % ::update-text-modifier-debounce-event ::update-text-modifier-debounce-ids))))
|
|
(rx/empty))))))
|
|
|
|
(defn clean-text-modifier
|
|
[id]
|
|
(ptk/reify ::clean-text-modifier
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [current-value (dm/get-in state [:workspace-text-modifier id])]
|
|
;; We only dissocc the value when hasn't change after a time
|
|
(->> (rx/of (fn [state]
|
|
(cond-> state
|
|
(identical? (dm/get-in state [:workspace-text-modifier id]) current-value)
|
|
(update :workspace-text-modifier dissoc id))))
|
|
(rx/delay 100))))))
|
|
|
|
(defn remove-text-modifier
|
|
[id]
|
|
(ptk/reify ::remove-text-modifier
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(d/dissoc-in state [:workspace-text-modifier id]))
|
|
|
|
ptk/WatchEvent
|
|
(watch [_ _ _]
|
|
(rx/of (dwm/apply-modifiers {:stack-undo? true})))))
|
|
|
|
(defn commit-position-data
|
|
[]
|
|
(ptk/reify ::commit-position-data
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(let [ids (keys (::update-position-data state))]
|
|
(update state :workspace-text-modifier #(apply dissoc % ids))))
|
|
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [position-data (::update-position-data state)]
|
|
(rx/concat
|
|
(rx/of (dwsh/update-shapes
|
|
(keys position-data)
|
|
(fn [shape]
|
|
(-> shape
|
|
(assoc :position-data (get position-data (:id shape)))))
|
|
{:stack-undo? true :reg-objects? false}))
|
|
(rx/of (fn [state]
|
|
(dissoc state ::update-position-data-debounce ::update-position-data))))))))
|
|
|
|
(defn update-position-data
|
|
[id position-data]
|
|
|
|
(let [cur-event (js/Symbol)]
|
|
(ptk/reify ::update-position-data
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(let [state (assoc-in state [:workspace-text-modifier id :position-data] position-data)]
|
|
(if (nil? (::update-position-data-debounce state))
|
|
(assoc state ::update-position-data-debounce cur-event)
|
|
(assoc-in state [::update-position-data id] position-data))))
|
|
|
|
ptk/WatchEvent
|
|
(watch [_ state stream]
|
|
(if (= (::update-position-data-debounce state) cur-event)
|
|
(let [stopper (->> stream (rx/filter (ptk/type? :app.main.data.workspace/finalize)))]
|
|
(rx/merge
|
|
(->> stream
|
|
(rx/filter (ptk/type? ::update-position-data))
|
|
(rx/debounce 50)
|
|
(rx/take 1)
|
|
(rx/map #(commit-position-data))
|
|
(rx/take-until stopper))
|
|
(rx/of (update-position-data id position-data))))
|
|
(rx/empty))))))
|
|
|
|
(defn update-attrs
|
|
[id attrs]
|
|
(ptk/reify ::update-attrs
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [text-editor-instance (:workspace-editor state)]
|
|
(if (and (features/active-feature? state "text-editor/v2")
|
|
(some? text-editor-instance))
|
|
(rx/empty)
|
|
(rx/concat
|
|
(let [attrs (select-keys attrs txt/root-attrs)]
|
|
(if-not (empty? attrs)
|
|
(rx/of (update-root-attrs {:id id :attrs attrs}))
|
|
(rx/empty)))
|
|
|
|
(let [attrs (select-keys attrs txt/paragraph-attrs)]
|
|
(if-not (empty? attrs)
|
|
(rx/of (update-paragraph-attrs {:id id :attrs attrs}))
|
|
(rx/empty)))
|
|
|
|
(let [attrs (select-keys attrs txt/text-node-attrs)]
|
|
(if-not (empty? attrs)
|
|
(rx/of (update-text-attrs {:id id :attrs attrs}))
|
|
(rx/empty)))
|
|
|
|
(when (features/active-feature? state "text-editor/v2")
|
|
(rx/of (v2-update-text-editor-styles id attrs)))
|
|
|
|
(when (features/active-feature? state "render-wasm/v1")
|
|
(rx/concat
|
|
;; Apply style to selected spans and sync content
|
|
(when (wasm.api/text-editor-is-active?)
|
|
(let [span-attrs (select-keys attrs txt/text-node-attrs)]
|
|
(when (not (empty? span-attrs))
|
|
(let [result (wasm.api/apply-style-to-selection span-attrs)]
|
|
(when result
|
|
(rx/of (v2-update-text-shape-content
|
|
(:shape-id result) (:content result)
|
|
:update-name? true)))))))
|
|
;; Resize (with delay for font-id changes)
|
|
(cond->> (rx/of (dwwt/resize-wasm-text id))
|
|
(contains? attrs :font-id)
|
|
(rx/delay 200))))))))
|
|
|
|
ptk/EffectEvent
|
|
(effect [_ state _]
|
|
(when (features/active-feature? state "text-editor/v2")
|
|
(when-let [instance (:workspace-editor state)]
|
|
(let [attrs-to-override (some-> (editor.v2/getCurrentStyle instance)
|
|
(styles/get-styles-from-style-declaration))
|
|
overriden-attrs (merge attrs-to-override attrs)
|
|
styles (styles/attrs->styles overriden-attrs)]
|
|
(editor.v2/applyStylesToSelection instance styles)))))))
|
|
|
|
(defn update-all-attrs
|
|
[ids attrs]
|
|
(ptk/reify ::update-all-attrs
|
|
ptk/WatchEvent
|
|
(watch [_ _ _]
|
|
(let [undo-id (js/Symbol)]
|
|
(rx/concat
|
|
(rx/of (dwu/start-undo-transaction undo-id))
|
|
(->> (rx/from ids)
|
|
(rx/map #(update-attrs % attrs)))
|
|
(rx/of (dwu/commit-undo-transaction undo-id)))))))
|
|
|
|
(defn apply-typography
|
|
"A higher level event that has the resposability of to apply the
|
|
specified typography to the selected shapes."
|
|
([typography file-id]
|
|
(apply-typography nil typography file-id))
|
|
|
|
([ids typography file-id]
|
|
(assert (or (nil? ids) (and (set? ids) (every? uuid? ids))))
|
|
(ptk/reify ::apply-typography
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [editor-state (:workspace-editor-state state)
|
|
ids (d/nilv ids (dsh/lookup-selected state))
|
|
attrs (-> typography
|
|
(assoc :typography-ref-file file-id)
|
|
(assoc :typography-ref-id (:id typography))
|
|
(dissoc :id :name))
|
|
undo-id (js/Symbol)]
|
|
|
|
(rx/concat
|
|
(rx/of (dwu/start-undo-transaction undo-id))
|
|
(->> (rx/from (seq ids))
|
|
(rx/map (fn [id]
|
|
(let [editor (get editor-state id)]
|
|
(update-text-attrs {:id id :editor editor :attrs attrs})))))
|
|
(rx/of (dwu/commit-undo-transaction undo-id))))))))
|
|
|
|
(defn generate-typography-name
|
|
[{:keys [font-id font-variant-id] :as typography}]
|
|
(let [{:keys [name]} (fonts/get-font-data font-id)]
|
|
(assoc typography :name (str name " " (str/title font-variant-id)))))
|
|
|
|
(defn add-typography
|
|
"A higher level version of dwl/add-typography, and has mainly two
|
|
responsabilities: add the typography to the library and apply it to
|
|
the currently selected text shapes (being aware of the open text
|
|
editors."
|
|
[file-id]
|
|
(ptk/reify ::add-typography
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [selected (dsh/lookup-selected state)
|
|
objects (dsh/lookup-page-objects state)
|
|
|
|
xform (comp (keep (d/getf objects))
|
|
(filter cfh/text-shape?))
|
|
shapes (into [] xform selected)
|
|
shape (first shapes)
|
|
|
|
values (current-text-values
|
|
{:editor-state (dm/get-in state [:workspace-editor-state (:id shape)])
|
|
:shape shape
|
|
:attrs txt/text-node-attrs})
|
|
|
|
multiple? (or (> 1 (count shapes))
|
|
(d/seek (partial = :multiple)
|
|
(vals values)))
|
|
|
|
values (-> (d/without-nils values)
|
|
(select-keys
|
|
(d/concat-vec txt/text-font-attrs
|
|
txt/text-spacing-attrs
|
|
txt/text-transform-attrs)))
|
|
|
|
typ-id (uuid/next)
|
|
typ (-> (if multiple?
|
|
txt/default-typography
|
|
(merge txt/default-typography values))
|
|
(generate-typography-name)
|
|
(assoc :id typ-id))]
|
|
|
|
(rx/concat
|
|
(rx/of (dwl/add-typography typ)
|
|
(ptk/event ::ev/event {::ev/name "add-asset-to-library"
|
|
:asset-type "typography"}))
|
|
|
|
(when (not multiple?)
|
|
(rx/of (update-attrs (:id shape)
|
|
{:typography-ref-id typ-id
|
|
:typography-ref-file file-id}))))))))
|
|
|
|
;; -- New Editor
|
|
|
|
(defn v2-update-text-editor-styles
|
|
[id new-styles]
|
|
(ptk/reify ::v2-update-text-editor-styles
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(let [merged-styles (merge (txt/get-default-text-attrs)
|
|
(get-in state [:workspace-global :default-font])
|
|
new-styles)]
|
|
(update-in state [:workspace-v2-editor-state id] (fnil merge {}) merged-styles)))))
|
|
|
|
(defn v2-update-text-shape-position-data
|
|
[shape-id position-data]
|
|
(ptk/reify ::v2-update-text-shape-position-data
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(update-in state [:workspace-text-modifier shape-id] {:position-data position-data}))))
|
|
|
|
(defn v2-update-text-shape-content
|
|
[id content & {:keys [update-name? name finalize? save-undo?]
|
|
:or {update-name? false name nil finalize? false save-undo? true}}]
|
|
(ptk/reify ::v2-update-text-shape-content
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(if (features/active-feature? state "render-wasm/v1")
|
|
(let [objects (dsh/lookup-page-objects state)
|
|
shape (get objects id)
|
|
new-shape? (nil? (:content shape))
|
|
prev-content (:content shape)
|
|
has-prev-content? (not (nil? (:prev-content shape)))
|
|
has-content? (when-not new-shape?
|
|
(v2-content-has-text? content))
|
|
did-has-content? (when-not new-shape?
|
|
(v2-content-has-text? prev-content))]
|
|
|
|
(rx/concat
|
|
(rx/of
|
|
(dwsh/update-shapes
|
|
[id]
|
|
(fn [shape]
|
|
(let [new-shape (-> shape
|
|
(assoc :content content)
|
|
(cond-> (and has-content?
|
|
has-prev-content?)
|
|
(dissoc :prev-content))
|
|
(cond-> (and did-has-content?
|
|
(not has-content?))
|
|
(assoc :prev-content prev-content))
|
|
(cond-> (and update-name? (some? name))
|
|
(assoc :name name)))]
|
|
new-shape))
|
|
{:save-undo? save-undo? :undo-group (when new-shape? id)})
|
|
|
|
(let [modifiers (dwwt/resize-wasm-text-modifiers shape content)
|
|
options {:undo-group (when new-shape? id)}]
|
|
(if (and (not= :fixed (:grow-type shape)) finalize?)
|
|
(dwm/apply-wasm-modifiers modifiers options)
|
|
(dwm/set-wasm-modifiers modifiers options))))
|
|
|
|
(when finalize?
|
|
(rx/concat
|
|
(when (and (not has-content?) (some? id))
|
|
(rx/of
|
|
(when has-prev-content?
|
|
(dwsh/update-shapes
|
|
[id]
|
|
(fn [shape]
|
|
(let [new-shape (-> shape
|
|
(assoc :content (:prev-content shape)))]
|
|
new-shape))
|
|
{:save-undo? false}))
|
|
(dws/deselect-shape id)
|
|
(dwsh/delete-shapes #{id})))
|
|
(rx/of (dwt/finish-transform))))))
|
|
|
|
(let [objects (dsh/lookup-page-objects state)
|
|
shape (get objects id)
|
|
modifiers (get-in state [:workspace-text-modifier id])
|
|
new-shape? (nil? (:content shape))]
|
|
(rx/of
|
|
(dwsh/update-shapes [id]
|
|
(fn [shape]
|
|
(let [{:keys [width height position-data]} modifiers]
|
|
(-> shape
|
|
(assoc :content content)
|
|
(cond-> position-data
|
|
(assoc :position-data position-data))
|
|
(cond-> (and update-name? (some? name))
|
|
(assoc :name name))
|
|
(cond-> (or (some? width) (some? height))
|
|
(gsh/transform-shape (ctm/change-size shape width height))))))
|
|
{:undo-group (when new-shape? id)})))))))
|