From b529d4ed5621462c224a467bf4575c355a4cfa78 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 4 Jun 2026 09:58:16 +0200 Subject: [PATCH] wip --- .../geometry-duplicated-in-render-wasm.md | 33 +++++++++++++++++++ common/src/app/common/geom/bounds_map.cljc | 24 +++++++++++++- .../app/common/geom/shapes/transforms.cljc | 22 ++++++++++++- .../common_tests/geom_bounds_map_test.cljc | 19 +++++++++++ .../test/common_tests/geom_shapes_test.cljc | 22 +++++++++++++ 5 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 .serena/memories/common/geometry-duplicated-in-render-wasm.md diff --git a/.serena/memories/common/geometry-duplicated-in-render-wasm.md b/.serena/memories/common/geometry-duplicated-in-render-wasm.md new file mode 100644 index 0000000000..7c5d5ee47d --- /dev/null +++ b/.serena/memories/common/geometry-duplicated-in-render-wasm.md @@ -0,0 +1,33 @@ +# Degenerate (collapsed) shape geometry: points vs selrect + +A shape can end up with a **degenerate `:points`** (collapsed to a zero-length basis — all corners +coincident on a line/point) while its `:selrect` still has a clamped non-zero size (e.g. width +`0.01`). This happens to auto-sized flex/grid containers that collapse to ~0 (e.g. after hiding all +their children). Such a shape is "frozen": you cannot resize it and it won't hug content, because +**scaling a zero-length basis stays zero** — applying any transform to its points is a no-op. + +## Where this bites and the fixes + +- Applying a transform/modifier to a shape: `app.common.geom.shapes.transforms/apply-transform-generic` + transforms the shape's `:points`. If they are degenerate it can never grow the shape. Fix: fall + back to `grc/rect->points (:selrect shape)` (a non-zero basis) when the points are degenerate, so + the transform lands and the points get repaired. This is the path the WASM renderer uses too: + `apply-wasm-modifiers` (frontend `data/workspace/modifiers.cljs`) computes the modifier in WASM, + then applies it on the CLJS side via `gsh/apply-transform` → `apply-transform-generic`. +- Layout auto-size content computation (SVG/CLJS path): `geom.bounds-map/objects->bounds-map` + derives a shape's bounds from `gco/shape->points` (its `:points`); a degenerate basis there makes + the projected content size ~0. Fix: fall back to the `selrect` when the points are degenerate. + +## render-wasm is NOT affected by the degenerate-points part + +render-wasm (`Shape::calculate_bounds` in `render-wasm/src/shapes.rs`) builds bounds from the +**`selrect`** (x/y/width/height), not from the stored points, so it computes the correct content +size and auto-resize scale even when the CLJS-side points are degenerate. Verified live via wasm +debug logging: for a collapsed auto container it reported `basis_x=0.01`, `auto_main=122`, +`after_w=122` — i.e. the WASM result was correct; the failure was purely the CLJS-side application +onto degenerate points. So a CLJS geometry bug here does NOT necessarily need a render-wasm change — +check which source (points vs selrect) each side uses before duplicating a fix. + +The flex/grid + bounds geometry is still duplicated between CLJC (`app.common.geom.shapes.**`, +`geom/modifiers.cljc`) and Rust (`render-wasm/src/math.rs` `Bounds`, `src/shapes/modifiers/**`); keep +that in mind, but confirm a layer is actually broken before changing it. See `mem:render-wasm/core`. diff --git a/common/src/app/common/geom/bounds_map.cljc b/common/src/app/common/geom/bounds_map.cljc index f9cffe73ab..9ffefb54f1 100644 --- a/common/src/app/common/geom/bounds_map.cljc +++ b/common/src/app/common/geom/bounds_map.cljc @@ -9,6 +9,8 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.files.helpers :as cfh] + [app.common.geom.point :as gpt] + [app.common.geom.rect :as grc] [app.common.geom.shapes.common :as gco] [app.common.geom.shapes.points :as gpo] [app.common.geom.shapes.transforms :as gtr] @@ -16,11 +18,31 @@ [app.common.types.modifiers :as ctm] [app.common.uuid :as uuid])) +(defn- degenerate-points? + "True when the points have a zero-length basis (their bounds collapsed to a line or a + point), so they can't be used as a coordinate frame for layout." + [[p1 p2 _ p4]] + (and (some? p1) (some? p2) (some? p4) + (or (mth/almost-zero? (gpt/length (gpt/to-vec p1 p2))) + (mth/almost-zero? (gpt/length (gpt/to-vec p1 p4)))))) + +(defn- shape->bounds-points + [{:keys [selrect] :as shape}] + (let [points (gco/shape->points shape)] + ;; A collapsed basis (produced e.g. by hiding every child of an auto-sized layout) makes + ;; the bounds unusable as a coordinate frame and prevents the layout from recovering its + ;; size on reflow: the auto-size is reapplied as a scale, and a zero basis can't be scaled + ;; back up. Fall back to the selrect (whose dimensions are clamped to a minimum) so the + ;; next reflow can recompute the real size from the visible children. + (if (and (some? selrect) (degenerate-points? points)) + (grc/rect->points selrect) + points))) + (defn objects->bounds-map [objects] (d/lazy-map (keys objects) - #(gco/shape->points (get objects %)))) + #(shape->bounds-points (get objects %)))) (defn- create-bounds "Create the bounds object for the current shape in this context" diff --git a/common/src/app/common/geom/shapes/transforms.cljc b/common/src/app/common/geom/shapes/transforms.cljc index d0ff71a609..85e4072bb0 100644 --- a/common/src/app/common/geom/shapes/transforms.cljc +++ b/common/src/app/common/geom/shapes/transforms.cljc @@ -335,11 +335,31 @@ (assoc :points points)))) +(defn- degenerate-points? + "True when the points collapsed to a zero-length basis (a line or a point), so they + can't be used as the source of a transform." + [[p1 p2 _ p4]] + (and (some? p1) (some? p2) (some? p4) + (or (mth/almost-zero? (gpt/length (gpt/to-vec p1 p2))) + (mth/almost-zero? (gpt/length (gpt/to-vec p1 p4)))))) + (defn- apply-transform-generic "Given a new set of points transformed, set up the rectangle so it keeps its properties. We adjust de x,y,width,height and create a custom transform" [shape transform-mtx] - (let [points (-> (dm/get-prop shape :points) + (let [shape-selrect (dm/get-prop shape :selrect) + stored-points (dm/get-prop shape :points) + + ;; A shape whose points collapsed to a zero-length basis (e.g. a layout container that + ;; was collapsed to ~0 size) can't be transformed: scaling a degenerate basis keeps it + ;; degenerate, so resizes and auto-size recoveries have no effect. Fall back to the + ;; selrect, which keeps a non-zero basis, so the transform applies and the points get + ;; repaired. + base-points (if (and (some? shape-selrect) (degenerate-points? stored-points)) + (grc/rect->points shape-selrect) + stored-points) + + points (-> base-points (gco/transform-points transform-mtx)) shape (adjust-shape-flips shape points) diff --git a/common/test/common_tests/geom_bounds_map_test.cljc b/common/test/common_tests/geom_bounds_map_test.cljc index 23b239eb43..51fb7bd24d 100644 --- a/common/test/common_tests/geom_bounds_map_test.cljc +++ b/common/test/common_tests/geom_bounds_map_test.cljc @@ -414,3 +414,22 @@ ;; Width and height should be clamped to at least 0.01 (t/is (>= (gpo/width-points bounds) 0.01)) (t/is (>= (gpo/height-points bounds) 0.01)))))) + +(t/deftest objects->bounds-map-degenerate-points-falls-back-to-selrect-test + (t/testing "A shape whose points collapsed to a zero-length basis falls back to its selrect" + ;; A collapsed basis (e.g. produced by hiding every child of an auto-sized layout) cannot + ;; be used as a coordinate frame: the auto-size is reapplied as a scale and a zero basis + ;; can't be scaled back up. The bounds-map must derive usable points from the selrect so the + ;; next reflow can recover the real size. + (let [id (uuid/next) + shape (-> (make-rect id 10 20 30 40) + ;; horizontal basis collapsed: all points share the same x + (assoc :points [(gpt/point 10 20) (gpt/point 10 20) + (gpt/point 10 60) (gpt/point 10 60)])) + bm (gbm/objects->bounds-map {id shape}) + bounds @(get bm id)] + ;; basis recovered from the selrect (30x40), not the collapsed points (0 width) + (t/is (mth/close? 30.0 (gpo/width-points bounds))) + (t/is (mth/close? 40.0 (gpo/height-points bounds))) + (t/is (mth/close? 10.0 (:x (gpo/origin bounds)))) + (t/is (mth/close? 20.0 (:y (gpo/origin bounds))))))) diff --git a/common/test/common_tests/geom_shapes_test.cljc b/common/test/common_tests/geom_shapes_test.cljc index 87805559e6..34569563c4 100644 --- a/common/test/common_tests/geom_shapes_test.cljc +++ b/common/test/common_tests/geom_shapes_test.cljc @@ -271,3 +271,25 @@ (t/is (true? (:flip-y result))) ;; rotation must not be negated when both axes are flipped (t/is (mth/close? 30 (:rotation result)))))) + +;; ---- transform recovery for degenerate (collapsed) points ---- + +(t/deftest resize-recovers-shape-with-degenerate-points + (t/testing "Resizing a shape whose points collapsed to a zero-length basis recovers its size" + ;; Simulate a collapsed layout container: selrect keeps a (clamped) 0.01 width, but the + ;; points degenerated to a vertical line (zero horizontal basis). A resize transform must + ;; still grow it, instead of staying collapsed (scaling a zero basis stays zero). + (let [base (create-test-shape :rect {}) + x (get-in base [:selrect :x]) + y (get-in base [:selrect :y]) + h (get-in base [:selrect :height]) + shape (assoc base + :selrect (grc/make-rect x y 0.01 h) + :width 0.01 + :points [(gpt/point x y) (gpt/point x y) + (gpt/point x (+ y h)) (gpt/point x (+ y h))]) + ;; scale x by 100/0.01 around the left edge -> target width 100 + mods (ctm/resize-modifiers (gpt/point (/ 100 0.01) 1) (gpt/point x y)) + result (gsh/transform-shape shape mods)] + (t/is (mth/close? 100 (:width result) 0.5)) + (t/is (mth/close? h (:height result) 0.5)))))