diff --git a/common/app/common/math.cljc b/common/app/common/math.cljc index dd16c402e..e9ed34a29 100644 --- a/common/app/common/math.cljc +++ b/common/app/common/math.cljc @@ -26,6 +26,10 @@ #?(:cljs (and (not (nil? v)) (js/isFinite v)) :clj (and (not (nil? v)) (Double/isFinite v)))) +(defn finite + [v default] + (if (finite? v) v default)) + (defn abs [v] #?(:cljs (js/Math.abs v) diff --git a/common/app/common/pages/helpers.cljc b/common/app/common/pages/helpers.cljc index 8a6df0a2a..b0e64e6c1 100644 --- a/common/app/common/pages/helpers.cljc +++ b/common/app/common/pages/helpers.cljc @@ -88,14 +88,29 @@ [component] (get-in component [:objects (:id component)])) +;; Implemented with transient for performance (defn get-children "Retrieve all children ids recursively for a given object" [id objects] - ;; TODO: find why does this sometimes come as a list instead of vector - (let [shapes (vec (get-in objects [id :shapes]))] - (if shapes - (d/concat shapes (mapcat #(get-children % objects) shapes)) - []))) + + (loop [result (transient []) + pending (transient []) + next id] + (let [children (get-in objects [next :shapes] []) + length (count children)] + (loop [i 0] + (when (< i length) + (let [child (nth children i)] + (conj! result child) + (conj! pending child) + (recur (inc i)))))) + + (let [length (count pending)] + (if (not= length 0) + (let [next (get pending (dec length))] + (pop! pending) + (recur result pending next)) + (persistent! result))))) (defn get-children-objects "Retrieve all children objects recursively for a given object" diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs index 36517b592..be14de47e 100644 --- a/frontend/src/app/main/data/workspace/texts.cljs +++ b/frontend/src/app/main/data/workspace/texts.cljs @@ -11,6 +11,7 @@ (:require ["slate" :as slate :refer [Editor Node Transforms Text]] ["slate-react" :as rslate] + [app.common.math :as mth] [app.common.attrs :as attrs] [app.common.geom.shapes :as gsh] [app.common.pages :as cp] @@ -217,6 +218,9 @@ (= (-> 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 resize-text [id new-width new-height] (ptk/reify ::resize-text ptk/WatchEvent @@ -238,12 +242,13 @@ (and (= :fixed grow-type) overflow-text (<= new-height shape-height)) (conj (update-overflow-text id false)) - (and (or (not= shape-width new-width) (not= shape-height new-height)) + (and (or (not-changed? shape-width new-width) (not-changed? shape-height new-height)) (= grow-type :auto-width)) (conj (dwt/update-dimensions [id] :width new-width) (dwt/update-dimensions [id] :height new-height)) - (and (not= shape-height new-height) (= grow-type :auto-height)) + (and (not-changed? shape-height new-height) + (= grow-type :auto-height)) (conj (dwt/update-dimensions [id] :height new-height)))] (if (not (empty? events)) diff --git a/frontend/src/app/main/store.cljs b/frontend/src/app/main/store.cljs index 18a30c99e..3f711e785 100644 --- a/frontend/src/app/main/store.cljs +++ b/frontend/src/app/main/store.cljs @@ -13,6 +13,7 @@ [cuerdas.core :as str] [app.common.data :as d] [app.common.pages :as cp] + [app.common.pages.helpers :as helpers] [app.common.uuid :as uuid] [app.util.storage :refer [storage]] [app.util.debug :refer [debug? debug-exclude-events logjs]])) diff --git a/frontend/src/app/main/ui/context.cljs b/frontend/src/app/main/ui/context.cljs index fcc077bc7..3cd342f1f 100644 --- a/frontend/src/app/main/ui/context.cljs +++ b/frontend/src/app/main/ui/context.cljs @@ -14,6 +14,7 @@ (def embed-ctx (mf/create-context false)) (def render-ctx (mf/create-context nil)) (def def-ctx (mf/create-context false)) +(def ghost-ctx (mf/create-context false)) (def current-route (mf/create-context nil)) (def current-team-id (mf/create-context nil)) diff --git a/frontend/src/app/main/ui/workspace/shapes.cljs b/frontend/src/app/main/ui/workspace/shapes.cljs index ebdd1a4e3..ef7094bf2 100644 --- a/frontend/src/app/main/ui/workspace/shapes.cljs +++ b/frontend/src/app/main/ui/workspace/shapes.cljs @@ -21,6 +21,7 @@ [app.main.streams :as ms] [app.main.ui.cursors :as cur] [app.main.ui.hooks :as hooks] + [app.main.ui.context :as muc] [app.main.ui.shapes.circle :as circle] [app.main.ui.shapes.image :as image] [app.main.ui.shapes.rect :as rect] @@ -52,12 +53,9 @@ false (let [o-shape (obj/get op "shape") n-frame (obj/get np "frame") - o-frame (obj/get op "frame") - n-ghost (obj/get np "ghost?") - o-ghost (obj/get op "ghost?")] + o-frame (obj/get op "frame")] (and (identical? n-shape o-shape) - (identical? n-frame o-frame) - (identical? n-ghost o-ghost)))))) + (identical? n-frame o-frame)))))) (defn make-is-moving-ref [id] @@ -73,7 +71,7 @@ [props] (let [shape (obj/get props "shape") frame (obj/get props "frame") - ghost? (obj/get props "ghost?") + ghost? (mf/use-ctx muc/ghost-ctx) shape (-> (geom/transform-shape shape) (geom/translate-to-frame frame)) opts #js {:shape shape @@ -84,14 +82,14 @@ moving-iref (mf/use-memo (mf/deps (:id shape)) (make-is-moving-ref (:id shape))) moving? (mf/deref moving-iref) svg-element? (and (= (:type shape) :svg-raw) - (not= :svg (get-in shape [:content :tag])))] + (not= :svg (get-in shape [:content :tag]))) + hide-moving? (and (not ghost?) moving?)] - (when (and shape - (or ghost? (not moving?)) - (not (:hidden shape))) + (when (and shape (not (:hidden shape))) [:* (if-not svg-element? - [:g.shape-wrapper {:style {:cursor (if alt? cur/duplicate nil)}} + [:g.shape-wrapper {:style {:display (when hide-moving? "none") + :cursor (if alt? cur/duplicate nil)}} (case (:type shape) :path [:> path/path-wrapper opts] :text [:> text/text-wrapper opts] diff --git a/frontend/src/app/main/ui/workspace/shapes/frame.cljs b/frontend/src/app/main/ui/workspace/shapes/frame.cljs index 24cacdcd5..72c2cbbb9 100644 --- a/frontend/src/app/main/ui/workspace/shapes/frame.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/frame.cljs @@ -22,7 +22,8 @@ [app.util.timers :as ts] [beicon.core :as rx] [okulary.core :as l] - [rumext.alpha :as mf])) + [rumext.alpha :as mf] + [app.main.ui.context :as muc])) (defn- frame-wrapper-factory-equals? [np op] @@ -100,14 +101,15 @@ (mf/fnc deferred {::mf/wrap-props false} [props] - (let [tmp (mf/useState false) + (let [ghost? (mf/use-ctx muc/ghost-ctx) + tmp (mf/useState false) ^boolean render? (aget tmp 0) ^js set-render (aget tmp 1)] (mf/use-layout-effect (fn [] (let [sem (ts/schedule-on-idle #(set-render true))] #(rx/dispose! sem)))) - (if (unchecked-get props "ghost?") + (if ghost? (mf/create-element component props) (when render? (mf/create-element component props)))))) @@ -120,7 +122,7 @@ [props] (let [shape (unchecked-get props "shape") objects (unchecked-get props "objects") - ghost? (unchecked-get props "ghost?") + ghost? (mf/use-ctx muc/ghost-ctx) moving-iref (mf/use-memo (mf/deps (:id shape)) #(make-is-moving-ref (:id shape))) @@ -136,15 +138,16 @@ handle-context-menu (we/use-context-menu shape) handle-double-click (use-select-shape shape) - handle-mouse-down (we/use-mouse-down shape)] + handle-mouse-down (we/use-mouse-down shape) - (when (and shape - (or ghost? (not moving?)) - (not (:hidden shape))) - [:g {:class (when selected? "selected") - :on-context-menu handle-context-menu - :on-double-click handle-double-click - :on-mouse-down handle-mouse-down} + hide-moving? (and (not ghost?) moving?)] + + (when (and shape (not (:hidden shape))) + [:g.frame-wrapper {:class (when selected? "selected") + :style {:display (when hide-moving? "none")} + :on-context-menu handle-context-menu + :on-double-click handle-double-click + :on-mouse-down handle-mouse-down} [:& frame-title {:frame shape}] diff --git a/frontend/src/app/main/ui/workspace/shapes/text.cljs b/frontend/src/app/main/ui/workspace/shapes/text.cljs index 4c894e4d0..1d10b2347 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text.cljs @@ -49,6 +49,7 @@ {::mf/wrap-props false} [props] (let [{:keys [id name x y width height grow-type] :as shape} (unchecked-get props "shape") + ghost? (mf/use-ctx muc/ghost-ctx) selected-iref (mf/use-memo (mf/deps (:id shape)) #(refs/make-selected-ref (:id shape))) selected? (mf/deref selected-iref) @@ -73,14 +74,15 @@ (mf/use-callback (mf/deps id) (fn [entries] - (when (seq entries) + (when (and (not ghost?) (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"])] - (log/debug :msg "Resize detected" :shape-id id :width width :height height) - (st/emit! (dwt/resize-text id (mth/ceil width) (mth/ceil height)))))))) + (when (and (not (mth/almost-zero? width)) (not (mth/almost-zero? height))) + (do (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 @@ -96,11 +98,12 @@ (mf/use-effect (mf/deps @paragraph-ref handle-resize-text grow-type) (fn [] - (when-let [paragraph-node @paragraph-ref] - (let [observer (js/ResizeObserver. handle-resize-text)] - (log/debug :msg "Attach resize observer" :shape-id id :shape-name name) - (.observe observer paragraph-node) - #(.disconnect observer))))) + (when (not ghost?) + (when-let [paragraph-node @paragraph-ref] + (let [observer (js/ResizeObserver. handle-resize-text)] + (log/debug :msg "Attach resize observer" :shape-id id :shape-name name) + (.observe observer paragraph-node) + #(.disconnect observer)))))) [:> shape-container {:shape shape} diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index 466e363e7..cf5417551 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -162,7 +162,6 @@ (let [hover (unchecked-get props "hover") selected (unchecked-get props "selected") ids (unchecked-get props "ids") - ghost? (unchecked-get props "ghost?") edition (unchecked-get props "edition") data (mf/deref refs/workspace-page) objects (:objects data) @@ -180,17 +179,14 @@ (if (= (:type item) :frame) [:& frame-wrapper {:shape item :key (:id item) - :objects objects - :ghost? ghost?}] + :objects objects}] [:& shape-wrapper {:shape item - :key (:id item) - :ghost? ghost?}]))] + :key (:id item)}]))] - (when (not ghost?) - [:& shape-outlines {:objects objects - :selected selected - :hover hover - :edition edition}])])) + [:& shape-outlines {:objects objects + :selected selected + :hover hover + :edition edition}]])) (mf/defc ghost-frames {::mf/wrap-props false} @@ -206,18 +202,22 @@ (map gsh/transform-shape)) selrect (->> (into [] xf sobjects) - (gsh/selection-rect))] - [:svg.ghost - {:x (:x selrect) - :y (:y selrect) - :width (:width selrect) - :height (:height selrect) - :style {:pointer-events "none"}} + (gsh/selection-rect)) - [:g {:transform (str/fmt "translate(%s,%s)" (- (:x selrect-orig)) (- (:y selrect-orig)))} - [:& frames - {:ids selected - :ghost? true}]]])) + transform (when (and (mth/finite? (:x selrect-orig)) + (mth/finite? (:y selrect-orig))) + (str/fmt "translate(%s,%s)" (- (:x selrect-orig)) (- (:y selrect-orig))))] + [:& (mf/provider ctx/ghost-ctx) {:value true} + [:svg.ghost + {:x (mth/finite (:x selrect) 0) + :y (mth/finite (:y selrect) 0) + :width (mth/finite (:width selrect) 100) + :height (mth/finite (:height selrect) 100) + :style {:pointer-events "none"}} + + [:g {:transform transform} + [:& frames + {:ids selected}]]]])) (defn format-viewbox [vbox] (str/join " " [(+ (:x vbox 0) (:left-offset vbox 0)) @@ -655,9 +655,9 @@ :selected selected :edition edition}] - (when (= :move (:transform local)) - [:& ghost-frames {:modifiers (:modifiers local) - :selected selected}]) + [:g {:style {:display (when (not= :move (:transform local)) "none")}} + [:& ghost-frames {:modifiers (:modifiers local) + :selected selected}]] (when (seq selected) [:& selection-handlers {:selected selected