From e4a8aa5c628ad79577998053d819b1f4da22e2ab Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Fri, 25 Sep 2026 09:37:29 +0200 Subject: [PATCH] :sparkles: Improvements on the dimension badge (#11884) * :tada: Add flip option to measures badge * :tada: Show dimension badge while resizing * :tada: Show dimension badge while moving * :tada: Hide badge when is smaller than shape * :recycle: Clean format * :recycle: Reduce comments * :recycle: Add memoization to selected-shapes --- frontend/src/app/main/ui/hooks.cljs | 11 +++ frontend/src/app/main/ui/measurements.cljs | 84 ++++++++++++++----- .../src/app/main/ui/workspace/viewport.cljs | 19 +++-- .../app/main/ui/workspace/viewport_wasm.cljs | 18 ++-- 4 files changed, 97 insertions(+), 35 deletions(-) diff --git a/frontend/src/app/main/ui/hooks.cljs b/frontend/src/app/main/ui/hooks.cljs index 3a4e76edeb..3225932153 100644 --- a/frontend/src/app/main/ui/hooks.cljs +++ b/frontend/src/app/main/ui/hooks.cljs @@ -310,6 +310,17 @@ (update-fn value)) state)) +(defn use-throttle + "Like `use-debounce`, but updates at most once per `ms` instead of only + after the value stops changing: the first and the last value of each burst + are both applied, so it stays live during continuous updates (e.g. a drag)." + [ms value] + (let [[state update-state-fn] (mf/useState value) + update-fn (mf/use-memo (mf/deps ms) #(f/throttle update-state-fn ms))] + (mf/with-effect [value] + (update-fn value)) + state)) + (defn use-shared-state "A specialized hook that adds persistence and inter-context reactivity to the default mf/use-state hook. diff --git a/frontend/src/app/main/ui/measurements.cljs b/frontend/src/app/main/ui/measurements.cljs index 9ba9226c81..b722bf8e1c 100644 --- a/frontend/src/app/main/ui/measurements.cljs +++ b/frontend/src/app/main/ui/measurements.cljs @@ -206,8 +206,41 @@ :top [p0 p1] :left [p3 p0]))) +(defn- opposite-edge + [edge] + (case edge + :bottom :top + :top :bottom + :left :right + :right :left)) + +(defn- edge-rot-offset + [edge] + (case edge + :bottom 0 + :right 270 + :top 180 + :left 90)) + +(defn- edge-badge-position + "Badge center for a badge anchored to `edge` of the rotated shape." + [points edge offset] + (let [[ep1 ep2] (get-edge-points points edge) + mid-point (gpt/lerp ep1 ep2 0.5) + normal (gpt/normal-right (gpt/subtract ep2 ep1))] + {:cx (+ (:x mid-point) (* (:x normal) offset)) + :cy (+ (:y mid-point) (* (:y normal) offset))})) + +(defn- fits-above-vbox-bottom? + "True when a badge centered at `cy` (with `badge-height`) still lands above + the visible bottom edge of `vbox`." + [cy badge-height vbox] + (or (nil? vbox) + (<= (+ cy (/ badge-height 2)) + (+ (:y vbox) (:height vbox))))) + (mf/defc selection-size-badge* - [{:keys [zoom shapes]}] + [{:keys [zoom shapes vbox]}] (let [badge-height (/ selection-badge-height zoom) badge-padding-x (/ selection-badge-padding-x zoom) badge-gap (/ selection-badge-vertical-gap zoom) @@ -250,30 +283,32 @@ text (dm/str (fmt/format-number shape-width) " x " (fmt/format-number shape-height)) text-width (* (count text) badge-char-width) - badge-width (+ text-width (* 2 badge-padding-x))] + badge-width (+ text-width (* 2 badge-padding-x)) - (when-not ^boolean single-line? + hidden? (or single-line? + (< shape-width badge-width) + (< shape-height badge-height))] + + (when-not ^boolean hidden? (if has-rotation? - (let [edge (get-edge-for-badge rotation) - points (dm/get-prop single-shape :points) - - [ep1 ep2] (get-edge-points points edge) - - mid-point (gpt/lerp ep1 ep2 0.5) - normal (gpt/normal-right (gpt/subtract ep2 ep1)) - - rot-offset (case edge - :bottom 0 - :right 270 - :top 180 - :left 90) - badge-rot (+ rotation rot-offset) + (let [edge (get-edge-for-badge rotation) + points (dm/get-prop single-shape :points) offset (+ badge-gap (/ badge-height 2)) + ;; Rotation follows the original edge even if the position + ;; flips to the opposite one for lack of space, so the text + ;; stays upright. + badge-rot (+ rotation (edge-rot-offset edge)) + + position (edge-badge-position points edge offset) + position (if (fits-above-vbox-bottom? (:cy position) badge-height vbox) + position + (edge-badge-position points (opposite-edge edge) offset)) + badge-x (- (/ badge-width 2)) badge-y (- (/ badge-height 2)) - badge-cx (+ (:x mid-point) (* (:x normal) offset)) - badge-cy (+ (:y mid-point) (* (:y normal) offset))] + badge-cx (:cx position) + badge-cy (:cy position)] [:g.selection-size-badge {:pointer-events "none" :transform (dm/str "translate(" badge-cx "," badge-cy ") rotate(" badge-rot ")")} @@ -291,10 +326,13 @@ :dominant-baseline "middle"} text]]) - (let [badge-x (- (/ badge-width 2)) - badge-y (- (/ badge-height 2)) - badge-cx (+ (:x selrect) (/ (:width selrect) 2)) - badge-cy (+ (:y selrect) (:height selrect) badge-gap (/ badge-height 2))] + (let [badge-x (- (/ badge-width 2)) + badge-y (- (/ badge-height 2)) + badge-cx (+ (:x selrect) (/ (:width selrect) 2)) + below-cy (+ (:y selrect) (:height selrect) badge-gap (/ badge-height 2)) + badge-cy (if (fits-above-vbox-bottom? below-cy badge-height vbox) + below-cy + (- (:y selrect) badge-gap (/ badge-height 2)))] [:g.selection-size-badge {:pointer-events "none" :transform (dm/str "translate(" badge-cx "," badge-cy ")")} diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index ee858a6182..cd2ef3ca88 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -125,9 +125,10 @@ objects-modified (mf/with-memo [base-objects text-modifiers modifiers] (apply-modifiers-to-selected selected base-objects text-modifiers modifiers)) - selected-shapes (->> selected - (into [] (keep (d/getf objects-modified))) - (not-empty)) + selected-shapes (mf/with-memo [selected objects-modified] + (->> selected + (into [] (keep (d/getf objects-modified))) + (not-empty))) ;; True when at least one selected shape can be alt-duplicated. ;; Inner shapes of component copies are excluded by ctk/allow-duplicate? @@ -135,6 +136,11 @@ (some #(ctk/allow-duplicate? base-objects %) (map (d/getf base-objects) selected))) + selected-shapes' (ui-hooks/use-throttle 100 selected-shapes) + badge-shapes (if (= transform :resize) + selected-shapes' + selected-shapes) + ;; STATE alt? (mf/use-state false) shift? (mf/use-state false) @@ -517,14 +523,15 @@ :modifiers modifiers}]) (when (and (seq selected-shapes) - (not transform) + (or (not transform) (contains? #{:resize :move} transform)) (not text-editing?) (not edition) (not read-only?) (not mode-inspect?)) [:> msr/selection-size-badge* - {:shapes selected-shapes - :zoom zoom}]) + {:shapes badge-shapes + :zoom zoom + :vbox vbox}]) (when show-measures? [:> msr/measurement* diff --git a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs index d696a91bed..39bbef583b 100644 --- a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs +++ b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs @@ -201,9 +201,10 @@ [base-objects wasm-modifiers] (apply-modifiers-to-selected selected base-objects wasm-modifiers)) - selected-shapes (->> selected - (into [] (keep (d/getf objects-modified))) - (not-empty)) + selected-shapes (mf/with-memo [selected objects-modified] + (->> selected + (into [] (keep (d/getf objects-modified))) + (not-empty))) ;; True when at least one selected shape can be alt-duplicated. ;; Inner shapes of component copies are excluded by ctk/allow-duplicate? @@ -211,6 +212,10 @@ (some #(ctk/allow-duplicate? base-objects %) (map (d/getf base-objects) selected))) + selected-shapes' (ui-hooks/use-throttle 100 selected-shapes) + badge-shapes (if (= transform :resize) + selected-shapes' + selected-shapes) ;; STATE alt? (mf/use-state false) shift? (mf/use-state false) @@ -813,15 +818,16 @@ :zoom zoom}]) (when (and (seq selected-shapes) - (not transform) + (or (not transform) (contains? #{:resize :move} transform)) (not text-editing?) (not edition) (not read-only?) (not mode-inspect?) (not page-transition?)) [:> msr/selection-size-badge* - {:shapes selected-shapes - :zoom zoom}]) + {:shapes badge-shapes + :zoom zoom + :vbox vbox}]) (when show-measures? [:> msr/measurement*