Appearance
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 for | Not for |
|---|---|
| Anything repeated along a line: openings, columns, posts, blades, panels, bins | Cells with area and depth on two sides of a line — see pack.corridorRuns |
| Merging wall or slab-edge segments into elevation runs | Items whose position is fixed by a plan constraint; solve those in plan |
| Carving an array around doors, columns and party walls | Doubly-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 withinnormalTolDeg(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 withincollinearTol."none"returns one run per segment.collinearTol?:number— Perpendicular offset tolerated by the collinear test. Looser gives fewer, longer runs. Default 150 mm expressed inunit.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 inunit.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 asRun.members[].segmentId, the id a caller uses to host an item.axis:SegmentAxislevel?: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 tomembers[].level.normal?:PlanVector— Outward normal, if the caller already knows it. Skips the enclosure test. WithgroupBy: "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 whenptsis given.pts?:PlanPoint[]— Sampled polyline for a curved segment. Two points or absent means straight.
RunsOutput
contractVersion:typeof LINEAR_ARRAY_CONTRACT_VERSIONunit:LengthUnit— The unit of every length inruns— the input'sunit.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 vianormal).developed:boolean— True whendis arc length across several distinct edges laid end to end.curved:booleannormal:PlanVector— Outward normal of the whole run.thickness:number— Max member thickness.members:RunMember[]frame:PolylineFrame— Arc-length frame: turn adinto a world point withframeAt.
RunMember
One input segment's place inside a run.
segmentId:stringlevel:number | nulld0:number— Span this member covers along the run, in run arc length.d1:numberoffset: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:PlanPointthickness: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.unitwhenrunscomes fromruns.runs:Array<Pick<Run, "id" | "length">>— Fromruns, or hand-built{ id, length }when there is no geometry yet.spec:LayoutSpecobstacles?: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 inunit.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 inunit.count?:number— Exact count for"count". @remarks Effect: a request, not a promise; read backitems.length.spacing?:number— Centre-to-centre distance for"spacing". @remarks Effect: floor 800 mm expressed inunit.kinds:ItemKind[]— One or two kinds. @remarks Effect: two kinds needkindRule; 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 leastbyLengthMinlong. Ignored with one kind. Default"alternate".byLengthMin?:number— Span length from which"byLength"switches to the second kind. Default 4000 mm expressed inunit.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"keepsminGapbetween 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 thancentreGap+ 1500 mm expressed inunit.
ItemKind
kind:stringwidth: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:stringd0:numberd1:number
LayoutOutput
contractVersion:typeof LINEAR_ARRAY_CONTRACT_VERSIONunit:LengthUnit— The unit of every length initems,batchesandunfilled— the input'sunit.items:Item[]batches:Batch[]unfilled:Unfilled[]
Item
runId:stringd:number— Centre of the item, run arc length.width:numberkind:string
Batch
Three or more identical items at constant step: create one, array the rest.
runId:stringkind:stringwidth:numberstart:number— Centre of the first item.step:numbern:number
Unfilled
A free span that received no item, and why.
runId:stringd0:numberd1:numberreason:"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:
input:RunsInput
- Returns:
RunsOutput - Throws:
InvalidUnitError— wheninput.unitis 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:stringd:numberlevel?:numberunit?: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:
input:LayoutInput
- Returns:
LayoutOutput - Throws:
InvalidUnitError— wheninput.unitis 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
| Condition | Result |
|---|---|
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 |
Related
pack.corridorRuns— cells with area and depth, on both sides of a spinegeom.polylineFrame— the arc-length frame a run is built ondesign.windows— hosts the items this module positions