mirror of
https://github.com/penpot/penpot.git
synced 2026-06-17 21:02:05 +00:00
wip
This commit is contained in:
parent
f4844ba6c4
commit
b529d4ed56
@ -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`.
|
||||
@ -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"
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)))))))
|
||||
|
||||
@ -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)))))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user