Add a Layers axis (3D point lattice)#4
Merged
Conversation
The Iterations grid becomes Columns × Rows × Layers. The engine just emits the cells and exposes each cell's 3D address (`l`, `layers`, `tz`) to the formula scope — projection to 2D lives in formulas / library presets, not a built-in projection. Layers defaults to 1, so existing patterns are byte-identical. - engine: 3D flat-index → (l, r, c) mapping; buildScope gains l/layers/tz (l defaults to 0 and layers is optional, so 2D callers are untouched); n and the orchestrator/render loops span the whole cube; diff treats layers like cols/rows (full rebuild). - ui: a Layers slider in Iterations; the count chip reads cols × rows × layers. - library: optional `layers` field; new Cube preset (oblique projection driven by c/r/l) so designers get 3D without writing math. - docs: fx variable list, controls, formulas, LLM pattern guide, CHANGELOG. - tests: 3D scope (l/layers/tz, back-compat) and flat-index → 3D-address mapping.
…Truchet
- Halftone: scale now visibly shrinks dots with distance (was an additive
~1px no-op); opacity falloff unchanged.
- Tunability: wave, lissajous, damped-wave, phyllotaxis, polar-grid, ribbon
expose amplitude/spacing via {x:..}/{y:..} placeholders so the X/Y sliders
drive them (matching Spiral/Heart/etc.).
- New 3D presets (exercise the Layers axis): Sphere (longitude × latitude ×
shells), Helix (coil; Layers = strands → double helix), Torus (tilted donut).
- New 2D preset: Truchet (random quarter-turn rotations, rerolls with seed).
- Concentric Squares + Square Spiral: squircle map (cos/max(|cos|,|sin|)) traces true square outlines — nested rings and a boxy Archimedean spiral. - Cylinder: a 3D tube (Columns wrap, Rows climb, Layers = shells), tilted and depth-shaded so the near wall reads larger/brighter.
Addresses code-review findings on PR #4: - Thumbnail now iterates layers (was flattening every 3D preset to a single layer-0 slice — Cube's library card showed a flat grid instead of a cube). - Cube formulas use the normalized tz (0..1) instead of raw l, so its offset/scale/opacity stay bounded as the Layers slider grows. Previously the geometry exploded (~±830px at layers=50) and back-layer opacity underflowed to invisible past ~10 layers.
Code-review follow-ups (#3, #6): - Add cellCount() with a MAX_CELLS (10000) cap, used by the orchestrator and preview render loop, so a 50×50×50 config renders a truncated result instead of cloning 125k host nodes and freezing. - Library "every formula evaluates" test now iterates layers (passing l), so 3D presets like Cube are exercised at every depth, not just layer 0.
Reorganize the panel into Column, Row, Layer sections (plus Modulation). Each axis carries its count, step, angle (clone rotation), and a fade. Layer also gains an oblique depth step, a twist, an opacity fade, and a colour-by-depth toggle, and absorbs the fill/stroke ramps (the former Appearance section). - engine: per-axis transforms applied as an additive post-process in evaluateCell — rotation += c·columnAngle + r·rowAngle + l·layerAngle; oblique layer offset; per-axis opacity fade; layer colour sweeps the ramp by tz. All default to 0/false, so existing configs render identically; fx + modulation are untouched. - ui: new generic AxisSection (Column/Row); Layer folded into the former Appearance section; the standalone Iterations + Transform sections removed. - tests: per-axis transform math + back-compat no-op.
Each axis carries its own Random (jitter): Column jitters X, Row jitters Y (their step's existing per-property random), and Layer gets a new seeded layerRandom that scatters cells obliquely. Modulation's "Random ±" keeps the non-positional randoms (rotation, scale, opacity) and the sinusoidals.
Restructure the controls around one mental model: Column/Row/Layer say WHERE clones go (and how each axis ramps them), Appearance says what each clone looks like, Modulation oscillates. Concretely: - Split the old conflated "Layer" section in two. Layer is now depth-only (Count, Step, Direction, Twist, Scale, Fade, Random, Colour-by-depth, stack order). A new Appearance section surfaces the base per-clone transforms that had lost their UI home — Rotation and Size X/Y (scaleX/scaleY), each formula-capable — alongside Opacity, Fill, Stroke, Stroke width and Easing. - Add per-axis Scale on all three axes and a depth Direction control; rename the per-clone rotation from "Angle" to "Twist" so it stops colliding with the global grid angle. - Fix the depth z-order: a shared paintOrder() helper paints far layers first so the near layer lands on top, with a "Far layers in front" toggle to flip it. Fix the per-axis Scale: it was multiplying the additive size *delta* (0 by default), so it did nothing. Fold the multiplier through the source size so it scales the rendered size and works even at the default zero delta; at mul=1 it collapses back to the plain delta, leaving existing configs untouched. Engine semantics are otherwise unchanged, so all 33 library presets render identically. New tests cover the depth direction vectors, paint order, and the scale multiplier against rendered size.
- Add a one-line plain-language hint under each section title (e.g. Column "Repeats and ramps across columns.", Appearance "Each clone's base look"). - Dim and disable an axis's Step/Twist/Scale/Fade/Random while its Count is 1, so the controls visibly wait on Count (Layer starts this way at 1 layer). - Style the Layer checkboxes (Colour by depth / Far layers in front) as proper accent toggles instead of raw browser checkboxes. - Use human labels in Modulation's Random ± (Rotation / Size X / Size Y / Opacity) to match Appearance, and rename Layer "Step" to "Z step". - Collapse Layer by default (new users) since it's the advanced axis. - Add a dev:watch script that rebuilds ui + preview bundles on change.
…rder - Reconcile the three-way count mismatch (sliders capped at 50, schema at 100, presets up to 120). Introduce a single MAX_AXIS = 120 used by the Count sliders and paste-clamping, and raise the library schema to match — so high-count presets (rose, square-spiral) load without the slider clamping them away. - Harden config paste: sanitizePastedConfig fills missing fields from the defaults (a partial JSON can no longer crash the compiler) and clamps cols/rows/layers to [1, MAX_AXIS]. Replaces the old "cols/rows are numbers" check that pushed raw JSON straight into the engine. - Fix library thumbnail depth order: paint far layers first so 3D presets show the near layer on top, matching the live preview. Left two cosmetic review nits as-is: the fx affordance differs because sliders and ramp strips are genuinely different controls, and Direction is now contextualized by the Layer hint + dimming.
Real-Penpot testing showed the plugin is unusable while dragging sliders:
Penpot's reactive document + WASM render engine can't absorb a full
regenerate on every drag frame, so it falls into a React #185 ("max update
depth") loop and, at higher counts, a hard Internal Error.
Add two HostAdapter capabilities:
- liveUpdates — Figma true, Penpot false. host-loop now drops uncommitted
live-drag frames on non-live hosts, so on Penpot the sliders stay smooth
and the canvas regenerates once, on release (commit). Figma keeps its live
preview unchanged.
- maxCells — Figma 10_000, Penpot 1_000 (conservative crash guard for the
render engine; tune against real Penpot). The orchestrator clamps each
generate to it.
Tests cover both: a commit-only host drops live-drag updates and regenerates
on commit, and a generate is capped at maxCells.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Replaces the abandoned tilt approach in #3 with a true 3D lattice, and expands the library to use it.
Layers (3D lattice)
The Iterations grid becomes Columns × Rows × Layers — a cube of dots. The engine only emits the cells; it does not project. Each cell's 3D address (
l,layers,tz) is exposed to the formula scope, and projection to 2D lives in formulas and library presets, not a built-in projection mode.Layersdefaults to1, so existing patterns are byte-identical.i/nnow span the whole cube.Library
New 3D presets (exercise the Layers axis): Cube, Sphere (longitude × latitude × shells), Helix (Layers = strands → double helix), Torus (tilted donut), Cylinder (tube; Layers = shells).
New 2D presets: Truchet (random quarter-turns), Concentric Squares, Square Spiral (squircle map → true square outlines).
Fixes / polish:
{x:..}/{y:..}so the X/Y sliders drive them.Why not the tilt approach (#3)
#3's Tilt multiplied the depth angle by the cell index (angleZ * i), which scatters grids instead of tilting them. Superseded; #3 is closed.Verification
build:allgreen.