Skip to content

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 forNot for
Pushing a set of plan circles apart until nothing overlapsDeciding where circles want to be; there are no attraction forces here — see pack.bubbleLayout
Keeping circles inside a ringRectangles 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: string
  • x: number
  • z: number
  • radius: number
  • fixed?: 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: number
    • minRadius?: number
    • unit?: 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 — Default 0.
    • passes?: number — Default 200.
    • 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 — Default 0.
    • passes?: number — Default 200.
    • unit?: LengthUnit — Default "mm".
  • Returns: SeparationResult

overlapSeverityRatio(circles, padding) ​

Σ penetration / minimum distance over every pair. 0 means clean.

  • Parameters:
    • circles: Circle[]
    • padding?: number — Default 0.
  • 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 — Default 0.
  • Returns: number

circleBounds(circles) ​

Axis-aligned plan bounds of a circle set, radii included. Empty input gives a zero box at the origin.

  • Parameters:
  • 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: Circle
    • padding?: number — Default 0.
    • 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 — Default 0.
    • 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 — Default 0.
    • 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);