✨ Improvements on the dimension badge (#11884)

* 🎉 Add flip option to measures badge

* 🎉 Show dimension badge while resizing

* 🎉 Show dimension badge while moving

* 🎉 Hide badge when is smaller than shape

* ♻️ Clean format

* ♻️ Reduce comments

* ♻️ Add memoization to selected-shapes
This commit is contained in:
Eva Marco 2026-09-25 09:37:29 +02:00 committed by GitHub
parent aeeca59b1a
commit e4a8aa5c62
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 97 additions and 35 deletions

View File

@ -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.

View File

@ -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 ")")}

View File

@ -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*

View File

@ -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*