Appearance
pack.bubbleLayout
Arrange sized bubbles so that wished-for neighbours end up near each other and nothing overlaps.
One call. Nodes carry an area (or a radius) and an optional pin; adjacencies carry a signed weight — positive pulls a pair together, negative pushes it apart. Each option is seeded from a strategy, relaxed by a fixed-count force pass, projected apart until no circle overlaps another, then scored. Options come back best first.
The module knows nothing about buildings. Room programmes, org charts, service diagrams and exhibition stands are the same call with different labels. One declared length unit, continuous weights, a real seed, caller-chosen strategies and option count, an optional site ring, and value semantics — the caller's arrays are never touched.
Pure module: no host, no DOM, no clock, no RNG. All lengths are in input.unit (areas in its square); the algorithm computes natively on the caller's numbers and only the module's own floors and tolerances are scaled to the unit. Plan coordinates are x/z; Y is up and never appears.
ts
// plugin worker
import { bubbleLayout } from "@snaptrude/plugin-toolkit/pack-bubble-layout";
// agent run_worker
modules.pack.bubbleLayout.bubbleLayout(...)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 |
|---|---|
| Areas plus adjacency wishes, with no boundary needed: room programmes, org charts, service diagrams, exhibition stands | Rectangles that tile with no leftover space — see pack.rooms |
| Pinning an entrance or a core and relaxing everything else around it | Corridors, walls, or node sizes that adapt to a footprint |
| Seeding a downstream packer with centres | A layout that stays stable across small edits, or more than ~300 nodes |
Inputs and outputs
The shapes every function below speaks. Each appears once; a field's note says what changing it does to the result.
BubbleLayoutInput
unit:LengthUnit— The unit every length in this input is in; areas are in its square. Echoed on the output.nodes:BubbleNode[]adjacencies?:BubbleAdjacency[]— Sparse list of pair wishes. Absent or empty is fine — you get a compact blob.boundary?:BubbleBoundary— Ring the bubbles should stay inside. Containment is applied before the final separation pass, so overlap-free wins over the boundary when the ring is too tight; nodes that could not fit are named innotes.padding?:number— Clearance kept between every pair on top ofrA + rB, inunit. Bigger padding gives a looser, larger diagram. Default 0.iterations?:number— Force-relax steps. No convergence test: the count is the budget. Fewer is faster and less settled; the layout is overlap-free either way. Cost isiterations · n². Default 260.seed?:number— Jitter stream. Same seed, same output, byte for byte. A different seed moves every seeded start slightly and so lands in a different local minimum. Default 1.optionCount?:number— How many options to return. Options beyondstrategies.lengthrepeat the strategies with a derived seed, so they are different layouts, not copies. Default isstrategies.length(3).strategies?:BubbleStrategy[]— Which seed strategies to try, in order. Default all three.
BubbleNode
One bubble: a room, a department, a team, a stand.
id:string— Caller's handle. Must be unique; used for pairing and for every tie-break.label?:string— Human name. Used for tie-breaks (label, then id) and nothing else. Renaming a node can reorder equal-degree seeds, and so change a layout. Ids alone are enough if you want labels to be free.area?:number— Floor area, inunit². Converted to a radius assqrt(area / π). Bigger nodes claim more room and push harder. Ignored whenradiusis given.radius?:number— Radius inunit, used directly. Wins overarea.fixed?:PlanPoint— Pin, inunit. The node sits exactly here in every option. Frozen through seed, relax, separation and containment — everything else moves around it. Two pins placed closer thanrA + rB + paddingstay overlapped; the option reportsoverlapFree: falseand a note names the pair.group?:string— Grouping tag (department, discipline, sponsor tier). Same-group pairs with no adjacency of their own get a mild pull together, and"grid-seeded"seeds a group into neighbouring cells.
BubbleAdjacency
One wish about a pair.
a:stringb:stringweight?:number— Signed strength.2pulls to touching,1to a comfortable walk,-1pushes apart; anything between interpolates. Wins overtype. Absent and with notypemeans1. Magnitudes above 2 (or below −2) clamp.type?:BubbleAdjacencyType— Shorthand forweight:adjacent= 2,near= 1,apart= −1.
BubbleBoundary
Site outline the bubbles should stay inside, in unit. Closed automatically; winding does not matter.
pts:PlanPoint[]
BubbleLayoutOutput
contractVersion:typeof BUBBLE_LAYOUT_CONTRACT_VERSIONunit:LengthUnit— The unit every length below is in — the same value the input declared.options:BubbleLayoutOption[]— Ranked best first byscore.overall. Length isoptionCount, or 0 when there are no nodes.notes:string[]— Anything the caller should know: dropped duplicates, floored radii, nodes that would not fit.
BubbleLayoutOption
id:string—o1,o2, … best first.strategy:BubbleStrategyseed:number— The seed this option actually ran with (seedplus its index).nodes:BubblePlacement[]score:BubbleScoreedges:BubbleEdgeResult[]— One entry per wish, input order. Filter onsatisfied: falseto explain a score.
BubblePlacement
Where one bubble ended up, in unit.
id:stringcentre:PlanPointradius:number
BubbleScore
satisfied:number— Wishes met.total:number— Wishes given (deduplicated pairs).overall:number— 0–100,satisfied-weighted adjacency and compactness combined. Ranks the options.adjacency:number— 0–100 adjacency term: a distance falloff per wish, weighted by |weight|.compactness:number— 0–100: bubble area over bounding-box area. High means a tight diagram.overlapFree:boolean— True in every option unless two pinned nodes were handed in overlapping.
BubbleEdgeResult
How one wish fared.
a:stringb:stringweight:number— The effective signed weight, aftertypeand clamping.satisfied:boolean— True when the pair is within (attract) or beyond (apart) its target distance.distance:number— Centre-to-centre distance in the finished layout, inunit.target:number— The distance the pair was judged against, inunit.
Functions
bubbleLayout(input)
Lay sized bubbles out so that wished-for neighbours end up near each other and no circle overlaps another. Returns optionCount whole-layout options, best first. Every length is read and written in input.unit.
- Parameters:
input:BubbleLayoutInput
- Returns:
BubbleLayoutOutput - Throws:
InvalidUnitError— wheninput.unitis missing or unknown. - Throws:
InvalidBubbleInputError— on a duplicate node id, a node with no size, or an adjacency naming an unknown node.
Guarantees: no two circles in a returned option overlap (unless two pinned nodes were handed in overlapping, which score.overlapFree reports); pinned nodes come back exactly where they were put; the same input and seed give byte-identical output; options are sorted by score.overall.
Examples
A brief and its adjacency wishes into three ranked options (both)
ts
// 1800 mm clearance between every pair; the nodes carry `area` in mm².
return bubbleLayout({
unit: "mm",
nodes,
adjacencies,
padding: 1800,
seed: 1
});Pick on a criterion the scorer does not have — north-facing daylight (agent)
ts
const wanted = new Set(wantsDaylight);
const { options } = bubbleLayout({
unit: "mm",
nodes,
adjacencies,
padding: 1800,
optionCount: 6
});
const daylight = (o: BubbleLayoutOption) =>
o.nodes.filter((p) => wanted.has(p.id) && p.centre.z < 0).length;
return options.reduce((a, b) => (daylight(b) > daylight(a) ? b : a));Pin the entrance and the core, relax everything else around them (both)
ts
// Pins are plan points in the same unit as the brief — mm here.
const pinned = nodes.map((n) => (pins[n.id] ? { ...n, fixed: pins[n.id] } : n));
return bubbleLayout({ unit: "mm", nodes: pinned, adjacencies, padding: 1800 });Keep the diagram inside the site outline (both)
ts
return bubbleLayout({
unit: "mm",
nodes,
adjacencies,
boundary: { pts: siteRing },
padding: 1200,
strategies: ["grid-seeded", "adjacency-first"]
});Errors
| Condition | Result |
|---|---|
bubbleLayout() — input.unit is missing or unknown. | throws InvalidUnitError |
bubbleLayout() — a duplicate node id, a node with no size, or an adjacency naming an unknown node. | throws InvalidBubbleInputError |
| two nodes with the same id throw InvalidBubbleInputError rather than silently pairing | pinned by duplicate-ids-error |
Related
pack.rooms— turn the centres into tiling cellsgeom.circlePacking— the separation and containment primitives