Skip to content

Repeaters

i want a way to automagically add/remove repeated elements like stairs (range for rise), studs (local building code)

Examples

  • stairs: template = one step (w, rise, run) → count from gap range constraint
  • studs: template = one stud (w, h, thickness) → count from spacing constraint

Further improvements

  • fire blocks every 4' when wall height > 8'
  • PROPOSAL FOR: how can engine keep track of templates, clones and fire blocks?
  • stairs: add a new slider for "percentage nosing"
  • trig rise and run
  • diagonally positioned stud wall
  • angles

done

  • bug: "Adds a bookend clone at the envelope end" but does not increment displayed repeats count
  • show a dimensional on the first fireblock
  • show an extra dimensional on the final fireblock if it is a shortened one
  • add two new booleans to SO -- and to portable SO and bump current version
    • is_diagonal -- default value is null
      • becomes true when the user clicks "diagonal"
      • becomes false on "straight"
      • never goes back to null
    • is_repeating -- default value is false
      • becomes true when user clicks diagonal or straight
      • false when they click unrepeat
        • simply removes the clones
  • for non repeaters
    • if no child -> "need one child for the template"
    • otherwise -> two buttons "diagonal" and "straight"
      • diagonal is for stairs, straight for non-stairs
  • stairs
    • remove extra "run" at top
    • have no fire blocks (remove button)
    • adjust tread depth according to run
      • run is computed. depth should be run x 1.25
  • for non stairs
    • replace constraint seg control with a slider
      • sticky at 12, 16, 24 inches
      • range from 6" to 3'
      • turn thumb white with faint border on stuck values
      • label "spacing" below
      • value label above thumb
      • move to same row as fireblocks button
      • thumb ignores click in bottom half of thumb
      • values should be in whole units (eg. inches)
      • slider has two lines (the fat one is the right length but should be thin, the thin one protrudes to the right)
      • stickies do not stick, nor change color, nor are they the designated ones (12, 16, 24)
  • move [un]repeat button -> center
  • convert min max range to a "two thumbs" slider, range from 4" to 12"
    • defaults: min -> 6, max -> 9
    • add ticks for whole units
    • value labels should react to slider thumbs
      • shrink font -> 60%
    • center the thumbs on the slider line
    • whole units should be sticky not snapped
    • snap positions should turn thumb white, with faint border (exactly like angles)
    • allow user to choose 4 or 12 (not stop at 4 1/4 and 11 3/4)
    • add label "rise range" below slider
  • not show constraint for stairs
  • move axis seg control -> [un]repeat row, far left
  • move number of clones to same row as [un]repeat, at far left
  • only show dimensionals on one clone/template
  • rotate a template -> should instantly rotate the clones
  • angles axis choice is wrong
  • duplication should exclude repeater clones from the new subtree removed dead code (duplicate_selected was never called)

Defer

  • diagonal bracing: angled member across stud bay
  • diagonal stud lengths (rake walls)
  • repeated repeater
    • four (six?) walls
    • flooring sheets
  • repeating angles (hah!)

Phase 1 — Data model

  • add repeater to Smart_Object
  • add is_template: boolean flag removed — repeater always uses first child
  • serialize/deserialize repeater field in Smart_Object

Phase 2 — Engine: generate copies

  • Engine.sync_repeater(so) — marks first child as template, syncs clones
  • Engine.sync_repeater(so) — evaluates count, creates/removes cloned children to match
    • clones copy template's dimensions
    • each clone's position offset = template size × index (axis determined by template's largest dimension)
  • decompose: call sync_repeater after any propagation that touches a repeater SO

Phase 3 — Constraints integration

  • when propagate(so) runs and so is a repeater, call sync_repeater(so) after
  • when propagate_all() runs, sync all repeater SOs after

Phase 4 — UI

  • D_Selection: "repeat" button → marks SO as repeater, shows constraint options
  • D_Hierarchy: badge repeated children with dim styling (e.g. ×3)

Phase 5 — Examples / test

  • build a stairs preset in the library (step SO + repeater parent)
  • build a studs preset (stud SO + repeater parent with code-derived spacing)

Design insight — no modes

stairs are just linear repeaters. a staircase is a parent SO rotated at atan(height/length), with steps repeating linearly along the hypotenuse. studs, joists, and ramps are the same — linear repeat, different template shape and parent orientation. no mode field needed.

the user's mental model: "i want a reasonable staircase from origin to extent." they set the envelope (height × length), the system picks a count that puts the gap (rise) within an acceptable range. rise and run fall out of the geometry — they're feedback, not input.

what distinguishes use cases is the template shape + parent rotation + constraints:

ThingTemplate shapeParent rotationConstraint
Stairsstep (wide, shallow)atan(h/l) on repeat_axisgap_min/gap_max (rise range)
Studsstud (tall, narrow)nonespacing (discrete: 12/16/24 OC)
Floor joistsjoist (long, shallow)nonespacing (discrete: 12/16/24 OC)
Roof joistsjoist (long, shallow)pitch anglespacing (discrete: 12/16/24 OC)

validation — phantom stairs: extend the stair pattern by one in each direction. step[-1] should sit just below floor. step[count] should land just beyond the top. if both match, the geometry is right.


Phase 6 — Repeater expansion

Phase 6.1 — Data model

  • add repeat_axis?: 0 | 1 to repeater — which axis clones march along (never 2)
  • add gap_min?: number / gap_max?: number to repeater — constrained spacing range (mm)
  • add spacing?: number to repeater — discrete spacing quick-pick (mm)
  • no mode field — linear handles everything
  • serialize/deserialize new fields in Smart_Object
  • migrate: v6→v7, existing repeaters unchanged (new fields are all optional)

Phase 6.2 — Engine: resolve_gap + repeat_axis

  • resolve_gap(total_length, gap_min, gap_max) — find count where total_length / count falls within [min, max]; prefer even division, fall back to closest
  • sync_repeater uses repeat_axis when set (falls back to current auto-detect from largest template dimension)
  • when gap_min/gap_max set: count = resolve_gap(parent_length_along_repeat_axis, gap_min, gap_max)
  • when spacing set: count = parent_length / spacing (rounded)
  • repeater fields update via spread (not overwrite)

Phase 6.3 — UX

  • repeat_axis selector (x / y toggle) in D_Selection when repeater is active
  • gap range inputs (min/max) — shown when gap_min/gap_max are set; prefilled 152.4 / 203.2 mm for stairs
  • spacing quick-pick: segmented control (12" / 16" / 24") — shown when spacing is set
  • display: resolved gap, count, total length

Phase 6.4 — Presets

  • stairs .di file: root SO (stair envelope, rotated) + template step child + repeater with gap_min/gap_max
  • studs .di file: root SO (wall frame) + template stud child + repeater with spacing + repeat_axis

See [user manual/repeaters](../../user manual/repeaters.md)

Phase 6.5 — Test

  • stairs: phantom stair validation, gap clamping, envelope resize adjusts count
  • studs: three spacings, wall length changes, axis toggle
  • joists: same as studs but template is horizontal — verify repeat works
  • existing linear repeaters unchanged (regression)

Phase 7 — Improvements

  • firewall blocking: horizontal member at mid-height between studs
  • i want to rotate a stretch by swapping x and y axes
    • for the template and for the repeater
    • formulas and offsets
  • button "duplicate" along side "delete all children"
  • move repeaters UX -> below action row (delete ... show) in d hierarchy
  • change 13 × 1' 4" (x) = 7' 7 1/2" -> 7 clones, 6 fire blocks
  • stairs s&l increase to include "phantom" (hidden) top stair and bottom landing stair
  • bookend clone count: display uses natural counting (includes template)
  • fireblock dimensionals: first fireblock shows repeat-axis dimensional; last fireblock shows one too if shortened (different length than first)
  • witness lines: perspective-correct per-vertex 3D projection (not forced parallel)

Regenerating from the library

clones are not stored. only the parent (with its repeater config) and the template (first child) are saved. on load, propagate_all() fires the post-propagate hook, which calls sync_repeater on every repeater — regenerating all clones from the template.

how stairs.di encodes diagonal without a mode flag:

the repeater config has repeat_axis: 1 (y) and gap_axis: 2 (z). when those differ, the engine knows it's diagonal:

  • gap_length = parent z height — the rise envelope
  • count = resolve_gap(gap_length, gap_min, gap_max) — how many steps fit the rise range
  • because gap_ai !== repeat_ai, each clone is offset along two axes:
    • step along y (run) — distributes treads across the run
    • gap_step along z (rise) — distributes treads up the height
  • clone i: template_y + step × (i+1), template_z + gap_step × (i+1)

the diagonal is emergent — the engine doesn't know about "stairs." it just sees that the gap constraint axis differs from the repeat axis, so it offsets clones along both. each tread steps forward and up → staircase.

is_diagonal exists only for the UI — so the straight/diagonal segment knows which button to highlight, and which creation path to take when re-repeating.