diff --git a/frontend/src/uxbox/main/data/shapes.cljs b/frontend/src/uxbox/main/data/shapes.cljs index 9bf90c5cf..c202d8252 100644 --- a/frontend/src/uxbox/main/data/shapes.cljs +++ b/frontend/src/uxbox/main/data/shapes.cljs @@ -49,6 +49,8 @@ (s/def ::hidden boolean?) (s/def ::blocked boolean?) (s/def ::locked boolean?) +(s/def ::width number?) +(s/def ::height number?) (s/def ::x1 number?) (s/def ::y1 number?) (s/def ::x2 number?) @@ -103,11 +105,12 @@ ptk/UpdateEvent (update [_ state] (let [shape (geom/setup-proportions data) - page (l/focus ul/selected-page state)] + page (get-in state [:workspace :page])] (impl/assoc-shape-to-page state shape page)))) (defn add-shape [data] + {:pre [(us/valid? ::shape data)]} (AddShape. data)) ;; --- Delete Shape @@ -125,14 +128,18 @@ {:pre [(uuid? id)]} (DeleteShape. id)) -(defn update-shape - "Just updates in place the shape." - [{:keys [id] :as shape}] - (reify - udp/IPageUpdate - ptk/UpdateEvent - (update [_ state] - (update-in state [:shapes id] merge shape)))) +;; --- Rename Shape + +(deftype RenameShape [id name] + udp/IPageUpdate + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:shapes id :name] name))) + +(defn rename-shape + [id name] + {:pre [(uuid? id) (string? name)]} + (RenameShape. id name)) ;; --- Shape Transformations @@ -142,7 +149,6 @@ (declare apply-temporal-displacement) - (deftype InitialShapeAlign [id] ptk/WatchEvent (watch [_ state s] @@ -160,29 +166,40 @@ {:pre [(uuid? id)]} (InitialShapeAlign. id)) +;; --- Update Rotation + +(deftype UpdateShapeRotation [id rotation] + udp/IPageUpdate + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:shapes id :rotation] rotation))) + (defn update-rotation - [sid rotation] - {:pre [(number? rotation) + [id rotation] + {:pre [(uuid? id) + (number? rotation) (>= rotation 0) (>= 360 rotation)]} - (reify - udp/IPageUpdate - ptk/UpdateEvent - (update [_ state] - (update-in state [:shapes sid] - geom/rotate rotation)))) + (UpdateShapeRotation. id rotation)) -(defn update-size +;; --- Update Dimensions + +(deftype UpdateDimensions [id dimensions] + udp/IPageUpdate + ptk/UpdateEvent + (update [_ state] + (update-in state [:shapes id] geom/resize-dim dimensions))) + +(s/def ::update-dimensions-opts + (s/keys :opt-un [::width ::height])) + +(defn update-dimensions "A helper event just for update the position of the shape using the width and height attrs instread final point of coordinates." - [sid opts] - {:pre [(uuid? sid)]} - (reify - udp/IPageUpdate - ptk/UpdateEvent - (update [_ state] - (update-in state [:shapes sid] geom/resize-dim opts)))) + [id opts] + {:pre [(uuid? id) (us/valid? ::update-dimensions-opts opts)]} + (UpdateDimensions. id opts)) ;; --- Apply Temporal Displacement @@ -246,22 +263,32 @@ {:pre [(uuid? id)]} (ApplyResize. id)) +;; --- Update Shape Position + +(deftype UpdateShapePosition [id point] + udp/IPageUpdate + ptk/UpdateEvent + (update [_ state] + (update-in state [:shapes id] geom/absolute-move point))) + (defn update-position "Update the start position coordenate of the shape." - [sid {:keys [x y] :as opts}] - (reify - ptk/UpdateEvent - (update [_ state] - (update-in state [:shapes sid] geom/absolute-move opts)))) + [id point] + {:pre [(uuid? id) (gpt/point? point)]} + (UpdateShapePosition. id point)) + +;; --- Update Shape Text + +(deftype UpdateShapeTextContent [id text] + udp/IPageUpdate + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:shapes id :content] text))) (defn update-text - [sid {:keys [content]}] - {:pre [(string? content)]} - (reify - udp/IPageUpdate - ptk/UpdateEvent - (update [_ state] - (assoc-in state [:shapes sid :content] content)))) + [id text] + {:pre [(uuid? id) (string? text)]} + (UpdateShapeTextContent. id text)) ;; --- Update Shape Attrs @@ -283,173 +310,206 @@ ;; --- Shape Proportions +(deftype LockShapeProportions [id] + udp/IPageUpdate + ptk/UpdateEvent + (update [_ state] + (let [[width height] (-> (get-in state [:shapes id]) + (geom/size) + (keep [:width :height])) + proportion (/ width height)] + (update-in state [:shapes id] assoc + :proportion proportion + :proportion-lock true)))) + (defn lock-proportions "Mark proportions of the shape locked and save the current proportion as additional precalculated property." - [sid] - {:pre [(uuid? sid)]} - (reify - udp/IPageUpdate - ptk/UpdateEvent - (update [_ state] - (let [[width height] (-> (get-in state [:shapes sid]) - (geom/size) - (keep [:width :height])) - proportion (/ width height)] - (update-in state [:shapes sid] assoc - :proportion proportion - :proportion-lock true))))) + [id] + {:pre [(uuid? id)]} + (LockShapeProportions. id)) + +(deftype UnlockShapeProportions [id] + udp/IPageUpdate + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:shapes id :proportion-lock] true))) (defn unlock-proportions - [sid] - {:pre [(uuid? sid)]} - (reify - udp/IPageUpdate - ptk/UpdateEvent - (update [_ state] - (update-in state [:shapes sid] assoc - :proportion-lock false)))) + [id] + {:pre [(uuid? id)]} + (UnlockShapeProportions. id)) ;; --- Group Collapsing +(deftype CollapseGroupShape [id] + udp/IPageUpdate + ptk/UpdateEvent + (update [_ state] + (update-in state [:shapes id] assoc :collapsed true))) + (defn collapse-shape [id] {:pre [(uuid? id)]} - (reify - udp/IPageUpdate - ptk/UpdateEvent - (update [_ state] - (update-in state [:shapes id] assoc :collapsed true)))) + (CollapseGroupShape. id)) + +(deftype UncollapseGroupShape [id] + udp/IPageUpdate + ptk/UpdateEvent + (update [_ state] + (update-in state [:shapes id] assoc :collapsed false))) (defn uncollapse-shape [id] {:pre [(uuid? id)]} - (reify - udp/IPageUpdate - ptk/UpdateEvent - (update [_ state] - (update-in state [:shapes id] assoc :collapsed false)))) + (UncollapseGroupShape. id)) ;; --- Shape Visibility -(defn hide-shape - [sid] - (reify - udp/IPageUpdate - ptk/UpdateEvent - (update [_ state] - (assoc-in state [:shapes sid :hidden] true)) +(deftype HideShape [id] + udp/IPageUpdate + ptk/UpdateEvent + (update [_ state] + (letfn [(mark-hidden [state id] + (let [shape (get-in state [:shapes id])] + (if (= :group (:type shape)) + (as-> state $ + (assoc-in $ [:shapes id :hidden] true) + (reduce mark-hidden $ (:items shape))) + (assoc-in state [:shapes id :hidden] true))))] + (mark-hidden state id)))) - ptk/WatchEvent - (watch [_ state s] - (let [shape (get-in state [:shapes sid])] - (if-not (= (:type shape) :group) - (rx/empty) - (rx/from-coll - (map hide-shape (:items shape)))))))) +(defn hide-shape + [id] + {:pre [(uuid? id)]} + (HideShape. id)) + +(deftype ShowShape [id] + udp/IPageUpdate + ptk/UpdateEvent + (update [_ state] + (letfn [(mark-visible [state id] + (let [shape (get-in state [:shapes id])] + (if (= :group (:type shape)) + (as-> state $ + (assoc-in $ [:shapes id :hidden] false) + (reduce mark-visible $ (:items shape))) + (assoc-in state [:shapes id :hidden] false))))] + (mark-visible state id)))) (defn show-shape - [sid] - (reify - udp/IPageUpdate - ptk/UpdateEvent - (update [_ state] - (assoc-in state [:shapes sid :hidden] false)) + [id] + {:pre [(uuid? id)]} + (ShowShape. id)) - ptk/WatchEvent - (watch [_ state s] - (let [shape (get-in state [:shapes sid])] - (if-not (= (:type shape) :group) - (rx/empty) - (rx/from-coll - (map show-shape (:items shape)))))))) +;; --- Shape Blocking + +(deftype BlockShape [id] + udp/IPageUpdate + ptk/UpdateEvent + (update [_ state] + (letfn [(mark-blocked [state id] + (let [shape (get-in state [:shapes id])] + (if (= :group (:type shape)) + (as-> state $ + (assoc-in $ [:shapes id :blocked] true) + (reduce mark-blocked $ (:items shape))) + (assoc-in state [:shapes id :blocked] true))))] + (mark-blocked state id)))) (defn block-shape - [sid] - (reify - udp/IPageUpdate - ptk/UpdateEvent - (update [_ state] - (assoc-in state [:shapes sid :blocked] true)) + [id] + {:pre [(uuid? id)]} + (BlockShape. id)) - ptk/WatchEvent - (watch [_ state s] - (let [shape (get-in state [:shapes sid])] - (if-not (= (:type shape) :group) - (rx/empty) - (rx/from-coll - (map block-shape (:items shape)))))))) +(deftype UnblockShape [id] + udp/IPageUpdate + ptk/UpdateEvent + (update [_ state] + (letfn [(mark-unblocked [state id] + (let [shape (get-in state [:shapes id])] + (if (= :group (:type shape)) + (as-> state $ + (assoc-in $ [:shapes id :blocked] false) + (reduce mark-unblocked $ (:items shape))) + (assoc-in state [:shapes id :blocked] false))))] + (mark-unblocked state id)))) (defn unblock-shape - [sid] - (reify - udp/IPageUpdate - ptk/UpdateEvent - (update [_ state] - (assoc-in state [:shapes sid :blocked] false)) + [id] + {:pre [(uuid? id)]} + (UnblockShape. id)) - ptk/WatchEvent - (watch [_ state s] - (let [shape (get-in state [:shapes sid])] - (if-not (= (:type shape) :group) - (rx/empty) - (rx/from-coll - (map unblock-shape (:items shape)))))))) +;; --- Shape Locking + +(deftype LockShape [id] + udp/IPageUpdate + ptk/UpdateEvent + (update [_ state] + (letfn [(mark-locked [state id] + (let [shape (get-in state [:shapes id])] + (if (= :group (:type shape)) + (as-> state $ + (assoc-in $ [:shapes id :locked] true) + (reduce mark-locked $ (:items shape))) + (assoc-in state [:shapes id :locked] true))))] + (mark-locked state id)))) (defn lock-shape - [sid] - (reify - udp/IPageUpdate - ptk/UpdateEvent - (update [_ state] - (assoc-in state [:shapes sid :locked] true)) + [id] + {:pre [(uuid? id)]} + (LockShape. id)) - ptk/WatchEvent - (watch [_ state s] - (let [shape (get-in state [:shapes sid])] - (if-not (= (:type shape) :group) - (rx/empty) - (rx/from-coll - (map lock-shape (:items shape)))))))) +(deftype UnlockShape [id] + udp/IPageUpdate + ptk/UpdateEvent + (update [_ state] + (letfn [(mark-unlocked [state id] + (let [shape (get-in state [:shapes id])] + (if (= :group (:type shape)) + (as-> state $ + (assoc-in $ [:shapes id :locked] false) + (reduce mark-unlocked $ (:items shape))) + (assoc-in state [:shapes id :locked] false))))] + (mark-unlocked state id)))) (defn unlock-shape - [sid] - (reify - udp/IPageUpdate - ptk/UpdateEvent - (update [_ state] - (assoc-in state [:shapes sid :locked] false)) + [id] + {:pre [(uuid? id)]} + (UnlockShape. id)) - ptk/WatchEvent - (watch [_ state s] - (let [shape (get-in state [:shapes sid])] - (if-not (= (:type shape) :group) - (rx/empty) - (rx/from-coll - (map unlock-shape (:items shape)))))))) +;; --- Drop Shape + +(deftype DropShape [sid tid loc] + udp/IPageUpdate + ptk/UpdateEvent + (update [_ state] + (impl/drop-shape state sid tid loc))) (defn drop-shape "Event used in drag and drop for transfer shape from one position to an other." [sid tid loc] - {:pre [(not (nil? tid)) - (not (nil? sid))]} - (reify - udp/IPageUpdate - ptk/UpdateEvent - (update [_ state] - (impl/drop-shape state sid tid loc)))) + {:pre [(uuid? sid) + (uuid? tid) + (keyword? loc)]} + (DropShape. sid tid loc)) + +;; --- Select First Shape + +(deftype SelectFirstShape [] + ptk/UpdateEvent + (update [_ state] + (let [page (get-in state [:workspace :page]) + id (first (get-in state [:pages page :shapes]))] + (assoc-in state [:workspace :selected] #{id})))) (defn select-first-shape "Mark a shape selected for drawing in the canvas." [] - (reify - ptk/UpdateEvent - (update [_ state] - (let [page (get-in state [:workspace :page]) - id (first (get-in state [:pages page :shapes]))] - (assoc-in state [:workspace :selected] #{id}))))) + (SelectFirstShape.)) +;; --- Mark Shape Selected (deftype SelectShape [id] ptk/UpdateEvent @@ -466,7 +526,7 @@ {:pre [(uuid? id)]} (SelectShape. id)) -;; --- Select Shapes +;; --- Select Shapes (By selrect) (deftype SelectShapesBySelrect [selrect] ptk/UpdateEvent @@ -578,25 +638,37 @@ [] (DeselectAll.)) +;; --- Group Selected Shapes + +(deftype GroupSelectedShapes [] + udp/IPageUpdate + ptk/UpdateEvent + (update [_ state] + (let [id (get-in state [:workspace :page]) + selected (get-in state [:workspace :selected])] + (assert (not (empty? selected)) "selected set is empty") + (assert (uuid? id) "selected page is not an uuid") + (impl/group-shapes state selected id)))) + (defn group-selected [] - (reify - udp/IPageUpdate - ptk/UpdateEvent - (update [_ state] - (let [pid (get-in state [:workspace :page]) - selected (get-in state [:workspace :selected])] - (impl/group-shapes state selected pid))))) + (GroupSelectedShapes.)) -(defn degroup-selected +;; --- Ungroup Selected Shapes + +(deftype UngroupSelectedShapes [] + udp/IPageUpdate + ptk/UpdateEvent + (update [_ state] + (let [id (get-in state [:workspace :page]) + selected (get-in state [:workspace :selected])] + (assert (not (empty? selected)) "selected set is empty") + (assert (uuid? id) "selected page is not an uuid") + (impl/degroup-shapes state selected id)))) + +(defn ungroup-selected [] - (reify - udp/IPageUpdate - ptk/UpdateEvent - (update [_ state] - (let [pid (get-in state [:workspace :page]) - selected (get-in state [:workspace :selected])] - (impl/degroup-shapes state selected pid))))) + (UngroupSelectedShapes.)) ;; --- Duplicate Selected diff --git a/frontend/src/uxbox/main/geom.cljs b/frontend/src/uxbox/main/geom.cljs index 21287c907..91dc55fd1 100644 --- a/frontend/src/uxbox/main/geom.cljs +++ b/frontend/src/uxbox/main/geom.cljs @@ -108,6 +108,7 @@ ;; TODO: maybe we can consider apply the rotation ;; directly to the shape coordinates? +;; FIXME: deprecated, should be removed (defn rotate "Apply the rotation to the shape." diff --git a/frontend/src/uxbox/main/ui/shapes/text.cljs b/frontend/src/uxbox/main/ui/shapes/text.cljs index e7699fa79..bc7ae4547 100644 --- a/frontend/src/uxbox/main/ui/shapes/text.cljs +++ b/frontend/src/uxbox/main/ui/shapes/text.cljs @@ -121,7 +121,7 @@ props {:x x1 :y y1 :width width :height height}] (letfn [(on-input [ev] (let [content (dom/event->inner-text ev)] - (st/emit! (uds/update-text id {:content content}))))] + (st/emit! (uds/update-text id content))))] [:foreignObject props [:div {:style style :ref "container" diff --git a/frontend/src/uxbox/main/ui/workspace/shortcuts.cljs b/frontend/src/uxbox/main/ui/workspace/shortcuts.cljs index 745481639..3dcc60a5e 100644 --- a/frontend/src/uxbox/main/ui/workspace/shortcuts.cljs +++ b/frontend/src/uxbox/main/ui/workspace/shortcuts.cljs @@ -28,7 +28,7 @@ (defonce +shortcuts+ {:shift+g #(st/emit! (dw/toggle-flag :grid)) :ctrl+g #(st/emit! (uds/group-selected)) - :ctrl+shift+g #(st/emit! (uds/degroup-selected)) + :ctrl+shift+g #(st/emit! (uds/ungroup-selected)) :ctrl+shift+m #(st/emit! (dw/toggle-flag :sitemap)) :ctrl+shift+f #(st/emit! (dw/toggle-flag :drawtools)) :ctrl+shift+i #(st/emit! (dw/toggle-flag :icons)) diff --git a/frontend/src/uxbox/main/ui/workspace/sidebar/layers.cljs b/frontend/src/uxbox/main/ui/workspace/sidebar/layers.cljs index 5f7cb9c05..0a6d56acd 100644 --- a/frontend/src/uxbox/main/ui/workspace/sidebar/layers.cljs +++ b/frontend/src/uxbox/main/ui/workspace/sidebar/layers.cljs @@ -92,14 +92,13 @@ "A generic component that displays the shape name if it is available and allows inline edition of it." {:mixins [mx/static (mx/local)]} - [{:keys [rum/local]} shape] + [{:keys [rum/local]} {:keys [id] :as shape}] (letfn [(on-blur [event] (let [target (dom/event->target event) parent (.-parentNode target) - data {:id (:id shape) - :name (dom/get-value target)}] + name (dom/get-value target)] (set! (.-draggable parent) true) - (st/emit! (uds/update-shape data)) + (st/emit! (uds/rename-shape id name)) (swap! local assoc :edition false))) (on-key-down [event] (js/console.log event) @@ -295,8 +294,8 @@ groups (into #{} xform selected)] (= 1 (count groups)))) -(defn- allow-degrouping? - "Check if the current situation allows degrouping +(defn- allow-ungrouping? + "Check if the current situation allows ungrouping of the currently selected shapes." [selected shapes-map] (let [xform (comp (map shapes-map) @@ -310,11 +309,11 @@ [selected shapes-map] (let [duplicate #(st/emit! (uds/duplicate-selected)) group #(st/emit! (uds/group-selected)) - degroup #(st/emit! (uds/degroup-selected)) + ungroup #(st/emit! (uds/ungroup-selected)) delete #(st/emit! (uds/delete-selected)) allow-grouping? (allow-grouping? selected shapes-map) - allow-degrouping? (allow-degrouping? selected shapes-map) + allow-ungrouping? (allow-ungrouping? selected shapes-map) allow-duplicate? (= 1 (count selected)) allow-deletion? (pos? (count selected))] [:div.layers-tools @@ -331,8 +330,8 @@ i/folder] [:li.degroup-layer.tooltip.tooltip-top {:alt "Ungroup" - :class (when-not allow-degrouping? "disable") - :on-click degroup} + :class (when-not allow-ungrouping? "disable") + :on-click ungroup} i/ungroup] [:li.delete-layer.tooltip.tooltip-top {:alt "Delete" diff --git a/frontend/src/uxbox/main/ui/workspace/sidebar/options/circle_measures.cljs b/frontend/src/uxbox/main/ui/workspace/sidebar/options/circle_measures.cljs index 2becac5bf..7f90d1a04 100644 --- a/frontend/src/uxbox/main/ui/workspace/sidebar/options/circle_measures.cljs +++ b/frontend/src/uxbox/main/ui/workspace/sidebar/options/circle_measures.cljs @@ -19,8 +19,9 @@ [uxbox.util.mixins :as mx :include-macros true] [uxbox.main.geom :as geom] [uxbox.util.dom :as dom] - [uxbox.util.math :refer (precision-or-0)] - [uxbox.util.data :refer (parse-int parse-float read-string)])) + [uxbox.util.geom.point :as gpt] + [uxbox.util.data :refer (parse-int parse-float read-string)] + [uxbox.util.math :refer (precision-or-0)])) (mx/defc circle-measures-menu {:mixins [mx/static]} @@ -30,7 +31,7 @@ value (parse-int value 0) sid (:id shape) props {attr value}] - (st/emit! (uds/update-size sid props)))) + (st/emit! (uds/update-dimensions sid props)))) (on-rotation-change [event] (let [value (dom/event->value event) value (parse-int value 0) @@ -40,8 +41,8 @@ (let [value (dom/event->value event) value (parse-int value nil) sid (:id shape) - props {attr value}] - (st/emit! (uds/update-position sid props)))) + point (gpt/point {attr value})] + (st/emit! (uds/update-position sid point)))) (on-proportion-lock-change [event] (if (:proportion-lock shape) (st/emit! (uds/unlock-proportions id)) diff --git a/frontend/src/uxbox/main/ui/workspace/sidebar/options/icon_measures.cljs b/frontend/src/uxbox/main/ui/workspace/sidebar/options/icon_measures.cljs index 267265065..adb9494a3 100644 --- a/frontend/src/uxbox/main/ui/workspace/sidebar/options/icon_measures.cljs +++ b/frontend/src/uxbox/main/ui/workspace/sidebar/options/icon_measures.cljs @@ -19,8 +19,9 @@ [uxbox.util.mixins :as mx :include-macros true] [uxbox.main.geom :as geom] [uxbox.util.dom :as dom] - [uxbox.util.math :refer (precision-or-0)] - [uxbox.util.data :refer (parse-int parse-float read-string)])) + [uxbox.util.geom.point :as gpt] + [uxbox.util.data :refer (parse-int parse-float read-string)] + [uxbox.util.math :refer (precision-or-0)])) (defn- icon-measures-menu-render [own menu shape] @@ -29,7 +30,7 @@ value (parse-int value 0) sid (:id shape) props {attr value}] - (st/emit! (uds/update-size sid props)))) + (st/emit! (uds/update-dimensions sid props)))) (on-rotation-change [event] (let [value (dom/event->value event) value (parse-int value 0) @@ -39,8 +40,8 @@ (let [value (dom/event->value event) value (parse-int value nil) sid (:id shape) - props {attr value}] - (st/emit! (uds/update-position sid props)))) + point (gpt/point {attr value})] + (st/emit! (uds/update-position sid point)))) (on-proportion-lock-change [event] (if (:proportion-lock shape) (st/emit! (uds/unlock-proportions (:id shape))) diff --git a/frontend/src/uxbox/main/ui/workspace/sidebar/options/rect_measures.cljs b/frontend/src/uxbox/main/ui/workspace/sidebar/options/rect_measures.cljs index 24090d8be..98054a4a3 100644 --- a/frontend/src/uxbox/main/ui/workspace/sidebar/options/rect_measures.cljs +++ b/frontend/src/uxbox/main/ui/workspace/sidebar/options/rect_measures.cljs @@ -7,7 +7,7 @@ (ns uxbox.main.ui.workspace.sidebar.options.rect-measures (:require [lentes.core :as l] - [uxbox.util.i18n :refer (tr)] + [uxbox.util.i18n :refer [tr]] [uxbox.util.router :as r] [potok.core :as ptk] [uxbox.main.store :as st] @@ -17,8 +17,9 @@ [uxbox.util.mixins :as mx :include-macros true] [uxbox.main.geom :as geom] [uxbox.util.dom :as dom] - [uxbox.util.math :refer (precision-or-0)] - [uxbox.util.data :refer (parse-int parse-float read-string)])) + [uxbox.util.geom.point :as gpt] + [uxbox.util.data :refer [parse-int parse-float read-string]] + [uxbox.util.math :refer [precision-or-0]])) (mx/defc rect-measures-menu {:mixins [mx/static]} @@ -26,15 +27,16 @@ (letfn [(on-size-change [event attr] (let [value (-> (dom/event->value event) (parse-int 0))] - (st/emit! (uds/update-size id {attr value})))) + (st/emit! (uds/update-dimensions id {attr value})))) (on-rotation-change [event] (let [value (-> (dom/event->value event) (parse-int 0))] (st/emit! (uds/update-rotation id value)))) (on-pos-change [event attr] (let [value (-> (dom/event->value event) - (parse-int nil))] - (st/emit! (uds/update-position id {attr value})))) + (parse-int nil)) + point (gpt/point {attr value})] + (st/emit! (uds/update-position id point)))) (on-proportion-lock-change [event] (if (:proportion-lock shape) (st/emit! (uds/unlock-proportions id))