Skip to content

pack.linearArray ​

Fit items of known width along aligned segments.

Two calls. runs merges collinear or same-facing plan segments into continuous runs with a length and a member list. layout packs items along each run at a rhythm, a count or a spacing, backing the count off until everything fits. Nothing overflows.

The module knows nothing about walls or windows. Facade openings, column grids, balconies, louvres, fence posts, shelf bins and street furniture are the same call with different control values. Behaviour matches the shipped Facade Designer plugin (src/modules/model.ts + design.ts) where that plugin had an opinion, generalised where it did not.

Pure module: no host, no DOM, no clock, no RNG. All lengths are in input.unit: the algorithms compute natively on the caller's numbers and only the module's own defaults and tolerances (defined in millimetres) are expressed in that unit. Plan coordinates are x/z; Y is up and only appears as a vertical extent for the enclosure test.

ts
// plugin worker
import { runs, frameAt, layout } from "@snaptrude/plugin-toolkit/pack-linear-array";

// agent run_worker
modules.pack.linearArray.runs(...)
modules.pack.linearArray.frameAt(...)
modules.pack.linearArray.layout(...)

Every input carries unit ("mm" | "cm" | "m" | "ft" | "in"). All lengths in that call — input and output — are in it; angles are degrees, areas are that unit squared.

When to use it ​

Use it forNot for
Anything repeated along a line: openings, columns, posts, blades, panels, binsCells with area and depth on two sides of a line — see pack.corridorRuns
Merging wall or slab-edge segments into elevation runsItems whose position is fixed by a plan constraint; solve those in plan
Carving an array around doors, columns and party wallsDoubly-curved faces; the frame is 2-D in plan

Inputs and outputs ​

The shapes every function below speaks. Each appears once; a field's note says what changing it does to the result.

RunsInput ​

  • unit: LengthUnit — The length unit every number in this input is in; the output echoes it.
  • segments: Segment[]
  • groupBy: GroupBy — How segments merge into runs. "normal" merges segments whose outward normals agree within normalTolDeg (facade behaviour: set-back parallel faces fold into one flattened line, a chamfer stays its own run). "collinear" merges only segments on the same line within collinearTol. "none" returns one run per segment.
  • collinearTol?: number — Perpendicular offset tolerated by the collinear test. Looser gives fewer, longer runs. Default 150 mm expressed in unit.
  • maxGap?: number — Largest gap along the line across which two collinear segments still merge. Overlapping or touching segments always merge. "collinear" grouping only. Raise it to bridge doorways or missing pieces into one run; set 0 to merge only touching/overlapping segments. Default 300 mm expressed in unit.
  • normalTolDeg?: number — Angle within which outward normals count as the same facing. "normal" grouping only. Default 8.

Segment ​

One input segment: a wall, a slab edge, a fence line, a grid line, a kerb.

  • id: string — Caller's handle. Comes back as Run.members[].segmentId, the id a caller uses to host an item.
  • axis: SegmentAxis
  • level?: number — Level (storey) the segment belongs to. Members keep it so a caller can lay out each level's own span. Optional for single-level uses. None on grouping; carried through to members[].level.
  • normal?: PlanVector — Outward normal, if the caller already knows it. Skips the enclosure test. With groupBy: "normal", decides which run the segment joins (normals within ~8° merge). Wrong normals collapse distinct faces.
  • thickness?: number — Segment thickness. The run reports the max over its members; the layout never reads it.
  • base?: number — Vertical extent, for the enclosure test only. Two segments occlude one another only where they coexist in height, so a set-back tower face is tested against the tower alone and reads outward. Omit for a flat plan.
  • height?: number

SegmentAxis ​

Plan geometry of one segment. Give pts for a curve; a straight segment needs only the axis.

  • sx: number — Start point, plan x.
  • sz: number — Start point, plan z.
  • dx: number — Unit direction, plan x. Normalised on input.
  • dz: number — Unit direction, plan z. Normalised on input.
  • length: number — Length along the direction. Ignored when pts is given.
  • pts?: PlanPoint[] — Sampled polyline for a curved segment. Two points or absent means straight.

RunsOutput ​

  • contractVersion: typeof LINEAR_ARRAY_CONTRACT_VERSION
  • unit: LengthUnit — The unit of every length in runs — the input's unit.
  • runs: Run[]

Run ​

  • id: string — Stable key, r1, r2, … longest first.
  • length: number — Developed length: what the packer fills.
  • outward: boolean — True when the run faces open space (or the caller said so via normal).
  • developed: boolean — True when d is arc length across several distinct edges laid end to end.
  • curved: boolean
  • normal: PlanVector — Outward normal of the whole run.
  • thickness: number — Max member thickness.
  • members: RunMember[]
  • frame: PolylineFrame — Arc-length frame: turn a d into a world point with frameAt.

RunMember ​

One input segment's place inside a run.

  • segmentId: string
  • level: number | null
  • d0: number — Span this member covers along the run, in run arc length.
  • d1: number
  • offset: number — Perpendicular offset of the member from the run's reference line, positive towards the run normal.
  • worldStart: PlanPoint — The member's own world endpoints, not projected onto the run.
  • worldEnd: PlanPoint
  • thickness: number

LayoutInput ​

  • unit: LengthUnit — The length unit every number in this input is in; the output echoes it. Must be the unit the runs were built in — RunsOutput.unit when runs comes from runs.
  • runs: Array<Pick<Run, "id" | "length">> — From runs, or hand-built { id, length } when there is no geometry yet.
  • spec: LayoutSpec
  • obstacles?: Obstacle[]
  • clearance?: number — Inflation applied to obstacles before carving. Default 0.
  • minItem?: number — Smallest item the single-item fallback may shrink to, and the smallest span worth filling. Default 600 mm expressed in unit.
  • maxCount?: number — Cap on items per free span. Default 24.

LayoutSpec ​

  • mode: LayoutMode — "bays" targets a rhythm (bayTarget), "count" requests an exact number (count), "spacing" a centre-to-centre distance (spacing). All three still back off to fit.
  • bayTarget?: number — Bay rhythm for "bays". @remarks Effect: bigger bay, fewer and wider-spaced items. Floor 1000 mm expressed in unit.
  • count?: number — Exact count for "count". @remarks Effect: a request, not a promise; read back items.length.
  • spacing?: number — Centre-to-centre distance for "spacing". @remarks Effect: floor 800 mm expressed in unit.
  • kinds: ItemKind[] — One or two kinds. @remarks Effect: two kinds need kindRule; kind selects width.
  • kindRule?: KindRule — With two kinds: "alternate" gives A, B, A, …; "byLength" gives all of the second kind when the span is at least byLengthMin long. Ignored with one kind. Default "alternate".
  • byLengthMin?: number — Span length from which "byLength" switches to the second kind. Default 4000 mm expressed in unit.
  • edgeClear: number — Dead length at each end of the run. @remarks Effect: shortens the usable length by twice this.
  • minGap: number — Minimum solid between two items: pier, mullion, post spacing, aisle. Fails a count when the gaps would be narrower; the count backs off.
  • distribute?: Distribute — "even" spreads slack into equal gaps (default). "centred" keeps minGap between items and pushes slack to the ends.
  • centreGap?: number — Reserve a void at mid-run and push items outward around it. Applied as an obstacle at the run's midpoint. Skipped when the run is shorter than centreGap + 1500 mm expressed in unit.

ItemKind ​

  • kind: string
  • width: number — Item width along the run. @remarks Effect: the wider kind triggers back-off sooner.

Obstacle ​

A span along a run that items must avoid, in run arc length.

  • runId: string
  • d0: number
  • d1: number

LayoutOutput ​

  • contractVersion: typeof LINEAR_ARRAY_CONTRACT_VERSION
  • unit: LengthUnit — The unit of every length in items, batches and unfilled — the input's unit.
  • items: Item[]
  • batches: Batch[]
  • unfilled: Unfilled[]

Item ​

  • runId: string
  • d: number — Centre of the item, run arc length.
  • width: number
  • kind: string

Batch ​

Three or more identical items at constant step: create one, array the rest.

  • runId: string
  • kind: string
  • width: number
  • start: number — Centre of the first item.
  • step: number
  • n: number

Unfilled ​

A free span that received no item, and why.

  • runId: string
  • d0: number
  • d1: number
  • reason: "too-short" | "no-fit"

Functions ​

runs(input) ​

Group aligned segments into continuous runs. Every input segment lands in exactly one run. Runs come back longest first.

  • Parameters:
  • Returns: RunsOutput
  • Throws: InvalidUnitError — when input.unit is missing or unknown.

frameAt(runList, runId, d, level, unit) ​

World position and axes at d along a run, plus the member that hosts it. hostSegmentId is the member whose span contains d (the first one at that level if several levels share the span); null when d falls in a void.

unit is the unit the runs were built in; it only sizes the span-inclusion slack (1e-6 mm expressed in unit).

  • Parameters:
    • runList: Run[]
    • runId: string
    • d: number
    • level?: number
    • unit?: LengthUnit — Default "mm".
  • Returns: FrameSample & { hostSegmentId: string | null; level: number | null }

layout(input) ​

Pack items along runs. Count degrades one at a time until the gap arithmetic passes. Obstacles (and centreGap) split a run into free spans that are filled independently; a span too short for one item is reported in unfilled.

  • Parameters:
  • Returns: LayoutOutput
  • Throws: InvalidUnitError — when input.unit is missing or unknown.
  • Throws: InvalidSpecError — on an empty or malformed spec.

Examples ​

Windows on every outward elevation (both)

ts
const elevations = runs({ unit, segments: walls, groupBy: "normal" });
const outward = elevations.runs.filter((r) => r.outward);
const { items, batches } = layout({
  unit,
  runs: outward,
  spec: {
    mode: "bays",
    bayTarget: 3600,
    kinds: [{ kind: "win", width: 1500 }],
    edgeClear: 600,
    minGap: 700,
    distribute: "even"
  }
});
// The world placement is the caller's: evaluate the frame, then host on
// `hostSegmentId` at whatever sill height the caller wants.
const seats = items.map((it) => frameAt(elevations.runs, it.runId, it.d, undefined, unit));
return { items, batches, seats };

Column grid along a slab edge (both)

ts
const { runs: edges } = runs({
  unit,
  segments: slabEdges,
  groupBy: "collinear"
});
return layout({
  unit,
  runs: edges,
  spec: {
    mode: "spacing",
    spacing: 7800,
    kinds: [{ kind: "col", width: 400 }],
    edgeClear: 200,
    minGap: 7400,
    distribute: "centred"
  }
});

Balconies that step around the party walls (both)

ts
const obstacles: Obstacle[] = partyWallsAt.map((p) => ({
  runId: p.runId,
  d0: p.d - 150,
  d1: p.d + 150
}));
return layout({
  unit,
  runs: facade,
  obstacles,
  clearance: 100,
  spec: {
    mode: "bays",
    bayTarget: 6000,
    kinds: [{ kind: "balcony", width: 3600 }],
    edgeClear: 600,
    minGap: 900,
    distribute: "even"
  }
});

Fence posts on a site boundary — no levels, no normals (plugin)

ts
const { runs: boundary } = runs({
  unit,
  segments: boundaryEdges,
  groupBy: "collinear"
});
return layout({
  unit,
  runs: boundary,
  spec: {
    mode: "spacing",
    spacing: 2400,
    kinds: [{ kind: "post", width: 100 }],
    edgeClear: 0,
    minGap: 2000
  }
});

Errors ​

ConditionResult
runs() — input.unit is missing or unknown.throws InvalidUnitError
layout() — input.unit is missing or unknown.throws InvalidUnitError
layout() — an empty or malformed spec.throws InvalidSpecError