Bonsai: add radial and helical arrays to bim.add_array - #9483
Open
theoryshaw wants to merge 1 commit into
Open
theoryshaw wants to merge 1 commit into
theoryshaw wants to merge 1 commit into
Conversation
Extends the parametric array with a RADIAL layer type that sweeps copies
about a centre and axis, optionally climbing along that axis to form a
helix (spiral stairs, helical ramps, corkscrew ramps, curved tiers).
Placement math is consolidated into tool.Array.child_matrix, which both
tool.Model._regenerate_array_body and ArrayPreviewDecorator now call. The
two previously carried separate hand-written copies of the offset formula,
so the drag-time ghost preview could drift from what Finish built.
New BBIM_Array.Data keys, all .get-defaulted so layers written before
radial existed place byte-for-byte as before, and bpy.ops.bim.add_array()
with no arguments is unchanged:
type, angle, rise, rise_method, axis, center, full_circle,
rotate_children
Notes on the design:
- The sweep is never wrapped or clamped. Multi-turn is a first-class case,
so unlike every other ANGLE property in model/prop.py the angle carries
no min/max — only a soft range to tame mouse drag. The damage from a
clamp is subtle: wrapping the final angle*i product is harmless (rotations
are periodic), but normalising the stored sweep before DISTRIBUTE divides
it changes the step (540/16 = 33.75 degrees becomes 180/16 = 11.25) while
the LAST instance still lands correctly, so end-position assertions cannot
detect it.
- angle and rise take their DISTRIBUTE decisions independently. A spiral
stair is normally known as one total and one per-copy quantity -- the
floor-to-floor height is fixed by the building while the tread angle is a
design choice, or vice versa -- and coupling the two modes makes that
ordinary specification unstatable. An absent rise_method inherits method.
- Whichever half of each pair the user types, the panel shows the other:
tool.Array.resolved_rise / resolved_angle return (per_copy, total) and
the edit body renders the derived one. The riser implied by a
floor-to-floor total is what building code actually constrains and it
exists nowhere else, so without this it is only discoverable by rebuilding
the array and measuring it. A total sweep past one turn is reported with
its turn count, since the bare angle is exactly what hides the wrap.
- The pivot is stored relative to the source instance, never as an absolute
world point. Children track the parent through the BBIM_Array CHILD_OF
constraint, so an absolute pivot would stay behind when the parent moved
and tear the ring away from its own contents.
- Switching a layer to radial seeds a pivot one bounding-box width off -X.
Left on the object's own origin, every copy would spin in place on top of
the original and the array would read as broken -- the same failure the
linear path avoids with its bbox-derived offset.
- Every new AddArray property is SKIP_SAVE. Blender replays an operator's
last-used values on the next interactive invocation and the Array panel's
"+" button passes no arguments at all, so without it a single radial add
would make every later "+" click silently produce a radial array.
- The display gap counts (angle_gap_count / rise_gap_count) are separate
from the DISTRIBUTE divisors, which floor at 1 to keep the division safe.
For a readout that floor would invent a phantom step on a lone instance.
Centre picking: bim.input_cursor_center_array takes the pivot from the 3D
cursor (Shift also takes the axis from the cursor's Z, which Shift+RMB
aligns to a surface normal), and bim.center_array_on_selected puts it on
another selected object's origin. The panel branches on type, the three
linear offset arrows hide in radial mode, and a type toggle joins the
gizmo row. Angle and centre gizmos are a follow-up.
Tests: 44 numeric cases on the placement and resolved-value functions, 5
AST forward-compat guards pinning the single-placement-site and no-clamp
contracts, and 6 BDD scenarios. One test checks the derived readout against
what child_matrix actually places so the display cannot drift from the
geometry. Tolerances are 1e-5 because mathutils is single-precision.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Closes #9482.
Adds a Radial array type to
bim.add_array, sweeping copies about a centre and axis and optionally climbing along that axis to form a helix — spiral stairs, helical ramps, curved seating tiers, radial façade fins.Placement math is now in one place
tool.Array.child_matrixis the single source of truth for instance transforms. Bothtool.Model._regenerate_array_bodyandArrayPreviewDecoratorcall it — previously each carried its own hand-written copy of the offset formula, so the drag-time ghost preview could disagree with what Finish actually built. Linear placement is unchanged; it just moved.Schema
New
BBIM_Array.Datakeys, all.get-defaulted:Layers written before this change have none of them and place byte-for-byte as before.
bpy.ops.bim.add_array()with no arguments behaves exactly as it always has.Three design points worth reviewing
The sweep is deliberately unclamped. Multi-turn is a first-class case, so unlike every other
subtype="ANGLE"property inmodel/prop.pythe angle carries nomin/max— only a soft range to tame mouse drag. The failure mode here is counter-intuitive and worth stating: wrapping the finalangle * iproduct is harmless, because rotations are periodic andRotation(630°)isRotation(270°). The damage happens upstream — clamping the stored property, or normalising the sweep beforeDISTRIBUTEdivides it, changes the step (540/16 = 33.75° becomes 180/16 = 11.25°) and quietly rebuilds a different array. Worse, the last instance still lands correctly either way, so end-position assertions cannot detect it. There is an AST guard pinning the no-clamp contract for exactly this reason.Angle and rise take their
DISTRIBUTEdecisions independently. A spiral stair is normally known as one total and one per-copy quantity: the floor-to-floor height is fixed by the building while the tread angle is a design choice, or the sweep is fixed by the plan while the riser is set by code. Coupling both to one switch makes that unstatable. An absentrise_methodinheritsmethod, so a partial spec stays coherent.The pivot is stored relative to the source instance, not as an absolute world point. Children track the parent through the existing
BBIM_ArrayCHILD_OFconstraint, so an absolute pivot would stay behind when the parent moved and tear the ring away from its own contents.Derived readouts
Whichever half of each pair you type, the panel shows the other — the riser implied by a floor-to-floor total is what building code constrains and it exists nowhere else:
Centre picking
bim.input_cursor_center_arraytakes the pivot from the 3D cursor — and with Shift, the rotation axis from the cursor's Z, whichShift+RMBaligns to a surface normal.bim.center_array_on_selectedputs it on another selected object's origin, which is the commonest real pick (select the newel column and a tread) and needs no snapping machinery.Switching a layer to radial seeds a pivot one bounding-box width off −X. Left on the object's own origin every copy would spin in place on top of the original and the array would read as broken — the same failure the linear path already avoids with its bbox-derived offset.
Tests
child_matrixand the resolved-value helpers: linear parity, legacy layers with notypekey, multi-turn integrity, helix coupling, all four mode combinations, degenerate axis,count == 1. Tolerances are1e-5becausemathutilsis single-precision.Not included
Angle and centre gizmos — radial parameters are panel-edited for now, and the three linear offset arrows hide in radial mode so a drag can't write a value the rebuild ignores. The type-toggle gizmo currently reuses the cycle glyph and would read better with a dedicated one.
🤖 Generated with Claude Code