diff --git a/common/app/common/data.cljc b/common/app/common/data.cljc index 871a13b4fb..994b0a82ef 100644 --- a/common/app/common/data.cljc +++ b/common/app/common/data.cljc @@ -177,15 +177,15 @@ (join [1 2 3] [:a :b]) => ([1 :a] [1 :b] [2 :a] [2 :b] [3 :a] [3 :b]) You can pass a function to merge the items. By default is `vector`: (join [1 2 3] [1 10 100] *) => (1 10 100 2 20 200 3 30 300)" - ([col1 col2] (join col1 col2 vector '())) - ([col1 col2 join-fn] (join col1 col2 join-fn '())) + ([col1 col2] (join col1 col2 vector [])) + ([col1 col2 join-fn] (join col1 col2 join-fn [])) ([col1 col2 join-fn acc] (cond (empty? col1) acc (empty? col2) acc :else (recur (rest col1) col2 join-fn - (core/concat acc (map (partial join-fn (first col1)) col2)))))) - + (let [other (mapv (partial join-fn (first col1)) col2)] + (concat acc other)))))) (def sentinel #?(:clj (Object.) diff --git a/common/app/common/math.cljc b/common/app/common/math.cljc index dd16c402e9..e9ed34a29e 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 8a6df0a2a4..b96ac8af32 100644 --- a/common/app/common/pages/helpers.cljc +++ b/common/app/common/pages/helpers.cljc @@ -88,14 +88,35 @@ [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] []) + [result pending] + ;; Iterate through children and add them to the result + ;; also add them in pending to check for their children + (loop [result result + pending pending + current (first children) + children (rest children)] + (if current + (recur (conj! result current) + (conj! pending current) + (first children) + (rest children)) + [result pending]))] + + ;; If we have still pending, advance the iterator + (let [length (count pending)] + (if (pos? length) + (let [next (get pending (dec length))] + (recur result (pop! 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 36517b592c..be14de47e6 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/snap.cljs b/frontend/src/app/main/snap.cljs index e20db5f631..d12ed81bfc 100644 --- a/frontend/src/app/main/snap.cljs +++ b/frontend/src/app/main/snap.cljs @@ -20,8 +20,8 @@ [app.main.refs :as refs] [app.util.geom.snap-points :as sp])) -(def ^:private snap-accuracy 5) -(def ^:private snap-distance-accuracy 10) +(defonce ^:private snap-accuracy 5) +(defonce ^:private snap-distance-accuracy 10) (defn- remove-from-snap-points [remove-id?] @@ -96,57 +96,69 @@ ;; snap-x is the second parameter because is the "source" to combine (rx/combine-latest snap->vector snap-y snap-x))) -(defn search-snap-distance [selrect coord shapes-lt shapes-gt] +(defn calculate-snap [coord selrect shapes-lt shapes-gt] (let [dist (fn [[sh1 sh2]] (-> sh1 (gsh/distance-shapes sh2) coord)) dist-lt (fn [other] (-> (:selrect other) (gsh/distance-selrect selrect) coord)) dist-gt (fn [other] (-> selrect (gsh/distance-selrect (:selrect other)) coord)) - ;; Calculates the distance between all the shapes given as argument - inner-distance (fn [shapes] - (->> shapes - (sort-by coord) - (d/map-perm vector) - (filter (fn [[sh1 sh2]] (gsh/overlap-coord? coord sh1 sh2))) - (map dist) - (filter #(> % 0)))) - ;; Calculates the snap distance when in the middle of two shapes - between-snap (fn [[sh-lt sh-gt]] - ;; To calculate the middle snap. - ;; Given x, the distance to a left shape and y to a right shape - ;; x - v = y + v => v = (x - y)/2 - ;; v will be the vector that we need to move the shape so it "snaps" - ;; in the middle - (/ (- (dist-gt sh-gt) - (dist-lt sh-lt)) 2)) - ] - (->> shapes-lt - (rx/combine-latest vector shapes-gt) - (rx/map (fn [[shapes-lt shapes-gt]] - (let [;; Distance between the elements in an area, these are the snap - ;; candidates to either side - lt-cand (inner-distance shapes-lt) - gt-cand (inner-distance shapes-gt) + between-snap + (fn [[sh-lt sh-gt]] + ;; To calculate the middle snap. + ;; Given x, the distance to a left shape and y to a right shape + ;; x - v = y + v => v = (x - y)/2 + ;; v will be the vector that we need to move the shape so it "snaps" + ;; in the middle + (/ (- (dist-gt sh-gt) + (dist-lt sh-lt)) 2)) - ;; Distance between the elements to either side and the current shape - ;; this is the distance that will "snap" - lt-dist (map dist-lt shapes-lt) - gt-dist (map dist-gt shapes-gt) + ;; Calculates the distance between all the shapes given as argument + inner-distance + (fn [shapes] + (->> shapes + (sort-by coord) + (d/map-perm vector) + (filter (fn [[sh1 sh2]] (gsh/overlap-coord? coord sh1 sh2))) + (map dist) + (filterv #(> % 0)))) - ;; Calculate the snaps, we need to reverse depending on area - lt-snap (d/join lt-cand lt-dist -) - gt-snap (d/join gt-dist gt-cand -) + best-snap + (fn [acc val] + ;; Using a number is faster than accesing the variable. + ;; Keep up to date with `snap-distance-accuracy` + (if (and (<= val 10) (>= val (- 10))) + (min acc val) + acc)) - ;; Calculate snap-between - between-snap (->> (d/join shapes-lt shapes-gt) - (map between-snap)) + ;; Distance between the elements in an area, these are the snap + ;; candidates to either side + lt-cand (inner-distance shapes-lt) + gt-cand (inner-distance shapes-gt) - ;; Search the minimum snap - min-snap (->> (concat lt-snap gt-snap between-snap) - (filter #(<= (mth/abs %) snap-distance-accuracy)) - (reduce min ##Inf))] + ;; Distance between the elements to either side and the current shape + ;; this is the distance that will "snap" + lt-dist (mapv dist-lt shapes-lt) + gt-dist (mapv dist-gt shapes-gt) - (if (mth/finite? min-snap) [0 min-snap] nil))))))) + ;; Calculate the snaps, we need to reverse depending on area + lt-snap (d/join lt-cand lt-dist -) + gt-snap (d/join gt-dist gt-cand -) + + ;; Calculate snap-between + between-snap (->> (d/join shapes-lt shapes-gt) + (map between-snap)) + + ;; Search the minimum snap + snap-list (-> [] (d/concat lt-snap) (d/concat gt-snap) (d/concat between-snap)) + min-snap (reduce best-snap ##Inf snap-list)] + + (if (mth/finite? min-snap) [0 min-snap] nil))) + +(defn search-snap-distance [selrect coord shapes-lt shapes-gt] + (->> shapes-lt + (rx/combine-latest vector shapes-gt) + (rx/map (fn [[shapes-lt shapes-gt]] + (calculate-snap coord selrect shapes-lt shapes-gt))))) (defn select-shapes-area [page-id shapes objects area-selrect] diff --git a/frontend/src/app/main/store.cljs b/frontend/src/app/main/store.cljs index 18a30c99e3..3f711e7852 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 fcc077bc77..3cd342f1f5 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 ebdd1a4e31..ef7094bf24 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 24cacdcd52..72c2cbbb9b 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 4c894e4d0c..1d10b23475 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/snap_distances.cljs b/frontend/src/app/main/ui/workspace/snap_distances.cljs index 139b9c289e..f501a71d69 100644 --- a/frontend/src/app/main/ui/workspace/snap_distances.cljs +++ b/frontend/src/app/main/ui/workspace/snap_distances.cljs @@ -112,52 +112,11 @@ :x2 x2 :y2 y2 :style {:stroke line-color :stroke-width (str (/ 1 zoom))}}])])) -(mf/defc shape-distance - {::mf/wrap-props false} - [props] - (let [frame (unchecked-get props "frame") - selrect (unchecked-get props "selrect") - page-id (unchecked-get props "page-id") - zoom (unchecked-get props "zoom") - coord (unchecked-get props "coord") - selected (unchecked-get props "selected") - - subject (mf/use-memo #(rx/subject)) - to-measure (mf/use-state []) - - pair->distance+pair +(defn calculate-segments [coord selrect lt-shapes gt-shapes] + (let [pair->distance+pair (fn [[sh1 sh2]] [(-> (gsh/distance-shapes sh1 sh2) coord (mth/precision 0)) [sh1 sh2]]) - contains-selected? - (fn [selected pairs] - (let [has-selected? - (fn [[_ [sh1 sh2]]] - (or (selected (:id sh1)) - (selected (:id sh2))))] - (some has-selected? pairs))) - - query-worker - (fn [[selrect selected frame]] - (let [lt-side (if (= coord :x) :left :top) - gt-side (if (= coord :x) :right :bottom) - container-selrec (or (:selrect frame) - (gsh/rect->selrect @refs/vbox)) - areas (gsh/selrect->areas container-selrec selrect) - query-side (fn [side] - (let [rect (gsh/pad-selrec (areas side))] - (if (and (> (:width rect) 0) (> (:height rect) 0)) - (->> (uw/ask! {:cmd :selection/query - :page-id page-id - :frame-id (:id frame) - :rect rect}) - (rx/map #(set/difference % selected)) - (rx/map #(->> % (map (partial get @refs/workspace-page-objects))))) - (rx/of nil))))] - - (->> (query-side lt-side) - (rx/combine-latest vector (query-side gt-side))))) - distance-to-selrect (fn [shape] (let [sr (:selrect shape)] @@ -183,7 +142,6 @@ (some #(<= (mth/abs (- value %)) 1)))) ;; Left/Top shapes and right/bottom shapes (depends on `coord` parameter - [lt-shapes gt-shapes] @to-measure ;; Gets the distance to the current selection lt-distances (->> lt-shapes (map distance-to-selrect) (filter pos?) (into #{})) @@ -225,6 +183,46 @@ (map #(vector selrect (:selrect %)))) segments-to-display (d/concat #{} other-shapes-segments selection-segments)] + segments-to-display)) + +(mf/defc shape-distance + {::mf/wrap-props false} + [props] + (let [frame (unchecked-get props "frame") + selrect (unchecked-get props "selrect") + page-id (unchecked-get props "page-id") + zoom (unchecked-get props "zoom") + coord (unchecked-get props "coord") + selected (unchecked-get props "selected") + + subject (mf/use-memo #(rx/subject)) + to-measure (mf/use-state []) + + query-worker + (fn [[selrect selected frame]] + (let [lt-side (if (= coord :x) :left :top) + gt-side (if (= coord :x) :right :bottom) + container-selrec (or (:selrect frame) + (gsh/rect->selrect @refs/vbox)) + areas (gsh/selrect->areas container-selrec selrect) + query-side (fn [side] + (let [rect (gsh/pad-selrec (areas side))] + (if (and (> (:width rect) 0) (> (:height rect) 0)) + (->> (uw/ask! {:cmd :selection/query + :page-id page-id + :frame-id (:id frame) + :rect rect}) + (rx/map #(set/difference % selected)) + (rx/map #(->> % (map (partial get @refs/workspace-page-objects))))) + (rx/of nil))))] + + (->> (query-side lt-side) + (rx/combine-latest vector (query-side gt-side))))) + + + [lt-shapes gt-shapes] @to-measure + + segments-to-display (calculate-segments coord selrect lt-shapes gt-shapes)] (mf/use-effect (fn [] diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index 466e363e7f..cf5417551b 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