Appearance
geom.circlePacking
Circle packing primitives: keep a set of plan circles from overlapping, and keep them inside a ring. Deterministic projection, no springs, no RNG — the hard-constraint half of any bubble / blob / disc layout.
Ring predicates (containment, nearest point, centroid) and the deterministic hash jitter come from geom/polygon.ts; this file is circles only.
Two flavours of every mutator. separateCircles / containCircles take a value and hand back a fresh array (the shape a module contract wants), while separateCirclesInto / containCirclesInto work in place on an array the caller owns — that is what an iteration loop calling this thousands of times should use.
Units: every length is in whatever unit the caller's coordinates are in; the algorithms compute natively on the caller's numbers. The touch tolerance and the default minimum radius are defined in millimetres and scaled with constMm to the optional trailing unit parameter (default "mm"). Pass the unit your coordinates are in.
Pure module: no host, no DOM, no clock, no RNG. Plan coordinates are x/z, Y is up and never appears here.
ts
// plugin worker
import { radiusFromArea, separateCirclesInto, separateCircles, overlapSeverityRatio, maxPenetration, circleBounds } from "@snaptrude/plugin-toolkit/geom-circle-packing";
// agent run_worker
modules.geom.circlePacking.radiusFromArea(...)
modules.geom.circlePacking.separateCirclesInto(...)
modules.geom.circlePacking.separateCircles(...)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 |
|---|---|
| Pushing a set of plan circles apart until nothing overlaps | Deciding where circles want to be; there are no attraction forces here — see pack.bubbleLayout |
| Keeping circles inside a ring | Rectangles or arbitrary shapes |
Measuring how crowded a circle layout is (overlapSeverityRatio) | Optimal packing; this is a deterministic projection, not a solver |
Inputs and outputs
The shapes every function below speaks. Each appears once; a field's note says what changing it does to the result.
Circle
One plan circle. fixed circles are never moved: a pair of fixed circles that starts overlapping stays overlapping (there is nothing to move).
id:stringx:numberz:numberradius:numberfixed?:boolean
SeparationResult
circles:Circle[]maxCorrection:number— Largest single push applied. 0 means nothing overlapped.lockedPairs:string[]— Ids of fixed pairs that stayed overlapped,"a|b", sorted.
ContainmentResult
circles:Circle[]outsideIds:string[]— Ids of circles that still do not fit fully inside the ring.
Functions
radiusFromArea(area, minRadius, unit)
Circle radius for an area: sqrt(area / π), floored so a degenerate area still has a body. minRadius defaults to 1 mm scaled to unit.
Defaults are defined in mm and scaled to unit; pass the unit your coordinates are in.
- Parameters:
area:numberminRadius?:numberunit?:LengthUnit— Default"mm".
- Returns:
number
separateCirclesInto(circles, padding, passes, lockedPairs, unit)
Push overlapping circles apart, in place, on an array the caller owns. Each penetrating pair is separated by the whole penetration along its centre line — half each, or all of it onto the free circle when the other is fixed. Repeats until a pass corrects nothing or passes is exhausted.
Tolerances are defined in mm and scaled to unit; pass the unit your coordinates are in.
- Parameters:
circles:Circle[]padding?:number— Default0.passes?:number— Default200.lockedPairs?:Set<string>unit?:LengthUnit— Default"mm".
- Returns:
number— the largest correction applied.
Effect: this, not any spring, is what guarantees no overlap. Run it last. padding is extra clearance kept on top of rA + rB.
separateCircles(circles, padding, passes, unit)
separateCirclesInto with value semantics: the input is never touched. Tolerances are defined in mm and scaled to unit.
- Parameters:
circles:Circle[]padding?:number— Default0.passes?:number— Default200.unit?:LengthUnit— Default"mm".
- Returns:
SeparationResult
overlapSeverityRatio(circles, padding)
Σ penetration / minimum distance over every pair. 0 means clean.
- Parameters:
circles:Circle[]padding?:number— Default0.
- Returns:
number
Effect: a scale-free crowding number — compare it across layouts of the same node set, not across different node counts.
maxPenetration(circles, padding)
Largest pair penetration, in the caller's unit. 0 means no circle overlaps another.
- Parameters:
circles:Circle[]padding?:number— Default0.
- Returns:
number
circleBounds(circles)
Axis-aligned plan bounds of a circle set, radii included. Empty input gives a zero box at the origin.
- Parameters:
circles:Circle[]
- Returns:
{ minX: number minZ: number maxX: number maxZ: number }
ringContainsCircle(ring, circle, padding, unit)
True when the whole circle (plus padding) sits inside the ring. Tolerances are defined in mm and scaled to unit; pass the unit your coordinates are in.
- Parameters:
ring:PlanPoint[]circle:Circlepadding?:number— Default0.unit?:LengthUnit— Default"mm".
- Returns:
boolean
containCirclesInto(circles, ring, padding, unit)
Move circles that poke out of the ring back inside, in place. A circle keeps its radius: it is pushed to the inward offset of the nearest boundary point.
Tolerances are defined in mm and scaled to unit; pass the unit your coordinates are in.
- Parameters:
circles:Circle[]ring:PlanPoint[]padding?:number— Default0.unit?:LengthUnit— Default"mm".
- Returns:
string[]— ids that still do not fit.
Effect: fixed circles are left where they are. A circle wider than the ring is locally impossible — it comes back in outsideIds rather than being shrunk.
containCircles(circles, ring, padding, unit)
containCirclesInto with value semantics: the input is never touched. Tolerances are defined in mm and scaled to unit.
- Parameters:
circles:Circle[]ring:PlanPoint[]padding?:number— Default0.unit?:LengthUnit— Default"mm".
- Returns:
ContainmentResult
Examples
Areas into circles, then pushed apart until nothing overlaps (both)
ts
const circles: Circle[] = seeds.map((s) => ({
id: s.id,
x: s.x,
z: s.z,
radius: radiusFromArea(s.area)
}));
const crowdingBefore = overlapSeverityRatio(circles, padding);
const separated = separateCircles(circles, padding);
return { crowdingBefore, ...separated };Keep the circles inside the site ring (both)
ts
return containCircles(circles, siteRing, padding);Related
pack.bubbleLayout— the force layout that calls thisgeom.polygon— the ring predicates containment uses