Skip to content

geom.polygon ​

The plan-polygon primitives every pack module shares: area and winding, centroid, bounding boxes, containment, distance, segment and ring intersection, ring simplicity, convex hull, minimum-width strip (OBB), shared-edge length, transforms, convex-window clipping, SAT separation and hole bridging.

Plan coordinates are x/z in the caller's length unit; Y is up and never appears here. Rings are open (the closing point is implied, never repeated) and a counter-clockwise ring has positive signed area. Ring-first argument order (f(ring, p)) is the convention throughout.

Units: the algorithms compute natively on the caller's numbers — nothing the caller wrote is converted. The module's own tolerances (and the defaults of caller-settable tolerance parameters) are defined in millimetres and scaled with constMm to the optional trailing unit parameter (default "mm") that every function using one accepts. Pure arithmetic (area, centroid, rotation, hashing…) takes no unit.

It also carries the seeded-jitter primitives {@link stableHash}, {@link hashUnitVector} and {@link hashUnitRatio}. They are not geometry, but every pure module needs a deterministic stand-in for a random source and the geom layer is capped at four files — see the "deterministic jitter" section at the bottom.

Pure module: no host, no DOM, no clock, no RNG. Inputs and outputs are plain data (structuredClone-safe).

ts
// plugin worker
import { signedRingArea, ringArea, polygonArea, ringOrientation, orientRing, ensureCounterClockwise } from "@snaptrude/plugin-toolkit/geom-polygon";

// agent run_worker
modules.geom.polygon.signedRingArea(...)
modules.geom.polygon.ringArea(...)
modules.geom.polygon.polygonArea(...)

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
Ring area, winding, centroid, bounding box and minimum-width stripSet operations on regions — see geom.polygonBoolean
Containment and intersection tests: point in ring, ring in ring, segment crossing, SAT separationDistance-along-a-line questions — see geom.polylineFrame
Ring hygiene and transforms: dedupe, clean, simplify, clip, rotate, bridge holesAnything in three dimensions; every ring is x/z in plan
A deterministic stand-in for a random source (stableHash, hashUnitVector, hashUnitRatio)

Inputs and outputs ​

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

Polygon ​

A region: one outer ring plus any number of holes. Nested holes are not supported.

  • ring: PlanPoint[] — Outer ring, at least three points.
  • holes?: PlanPoint[][] — Voids inside the outer ring, wound either way.

PlanBox ​

Axis-aligned plan box, corner-and-size form.

  • x0: number
  • z0: number
  • x1: number
  • z1: number
  • width: number
  • depth: number

Bounds ​

Axis-aligned plan box, min/max form. The same information as PlanBox under the names the footprint modules report.

  • minX: number
  • minZ: number
  • maxX: number
  • maxZ: number
  • width: number
  • depth: number

RingObb ​

Minimum-width oriented bounding box of a ring.

  • width: number — Narrow dimension: the width of the thinnest strip that contains the ring.
  • length: number — Long dimension, measured along that strip.
  • angleDeg: number — Direction of length, degrees CCW from +x.
  • aspectRatio: number — length / width, or Infinity for a degenerate ring.

Functions ​

signedRingArea(ring) ​

Shoelace area. Positive counter-clockwise, negative clockwise.

  • Parameters:
    • ring: PlanPoint[]
  • Returns: number

ringArea(ring) ​

Unsigned area. A ring bridged with hole slits reports its net area.

  • Parameters:
    • ring: PlanPoint[]
  • Returns: number

polygonArea(poly) ​

Outer ring area minus its holes. Never negative.

  • Parameters:
  • Returns: number

ringOrientation(ring, eps, unit) ​

1 counter-clockwise, -1 clockwise, 0 degenerate (area at or below eps, default 1e-6 mm² scaled to unit²).

Tolerances are defined in mm and scaled to unit; pass the unit your coordinates are in.

  • Parameters:
    • ring: PlanPoint[]
    • eps?: number
    • unit?: LengthUnit — Default "mm".
  • Returns: 1 | -1 | 0

orientRing(ring, ccw) ​

The same ring wound the way asked for. Never mutates the input.

  • Parameters:
    • ring: PlanPoint[]
    • ccw: boolean
  • Returns: PlanPoint[]

ensureCounterClockwise(ring) ​

Reverses the ring when it reads clockwise. Never mutates the input.

  • Parameters:
    • ring: PlanPoint[]
  • Returns: PlanPoint[]

ensureClockwise(ring) ​

Reverses the ring when it reads counter-clockwise (hole orientation).

  • Parameters:
    • ring: PlanPoint[]
  • Returns: PlanPoint[]

ringCentroid(ring) ​

Area centroid. Falls back to the vertex average for a degenerate ring (a slit, a spur, a repeated point), which is the only meaningful answer there.

  • Parameters:
    • ring: PlanPoint[]
  • Returns: PlanPoint

ringVertexCentroid(ring) ​

Plain average of the ring's vertices — NOT the area centroid. Denser runs of vertices pull it towards them, which is exactly what a "push away from the edge" fallback direction wants. An empty ring gives the origin.

  • Parameters:
    • ring: PlanPoint[]
  • Returns: PlanPoint

ringBbox(ring) ​

Axis-aligned bounds. An empty ring gives an all-zero box.

  • Parameters:
    • ring: PlanPoint[]
  • Returns: PlanBox

planBox(x0, z0, x1, z1) ​

Builds a PlanBox and fills in its derived dimensions.

  • Parameters:
    • x0: number
    • z0: number
    • x1: number
    • z1: number
  • Returns: PlanBox

boundsOfRing(ring) ​

The same extent as ringBbox in min/max form. An empty ring gives an inverted (infinite) box rather than a zero one — the callers that use this shape always have points.

  • Parameters:
    • ring: PlanPoint[]
  • Returns: Bounds

ringEdgeAnglesDeg(ring) ​

Edge directions of a ring, degrees in [0, 180), edge i from vertex i.

  • Parameters:
    • ring: PlanPoint[]
  • Returns: number[]

longestEdgeAngleDeg(ring) ​

Direction of the ring's longest edge, degrees CCW from +x, folded into [0, 90) because a rectangle's axes repeat every quarter turn.

  • Parameters:
    • ring: PlanPoint[]
  • Returns: number

Effect: this is the frame pack.rooms subdivides in, so rotating the boundary rotates every wall with it.


pointSegmentDistance(p, a, b) ​

Distance from p to the segment a–b.

  • Parameters:
    • p: PlanPoint
    • a: PlanPoint
    • b: PlanPoint
  • Returns: number

distanceToRing(ring, p) ​

Shortest distance from a point to a ring's edges. Zero on the boundary.

  • Parameters:
    • ring: PlanPoint[]
    • p: PlanPoint
  • Returns: number

nearestPointOnRing(ring, p) ​

Nearest point on the ring's boundary, and its distance.

  • Parameters:
    • ring: PlanPoint[]
    • p: PlanPoint
  • Returns: { point: PlanPoint; distance: number }

pointOnRing(ring, p, tol, unit) ​

True when p lies on the ring's outline within tol (default 1e-6 mm scaled to unit).

Tolerances are defined in mm and scaled to unit; pass the unit your coordinates are in.

  • Parameters:
    • ring: PlanPoint[]
    • p: PlanPoint
    • tol?: number
    • unit?: LengthUnit — Default "mm".
  • Returns: boolean

pointInRing(ring, p, tol, unit) ​

Even-odd containment with an explicit boundary rule: a point on the outline (within tol, default 1e-6 mm scaled to unit) counts as inside.

Tolerances are defined in mm and scaled to unit; pass the unit your coordinates are in.

  • Parameters:
    • ring: PlanPoint[]
    • p: PlanPoint
    • tol?: number
    • unit?: LengthUnit — Default "mm".
  • Returns: boolean

pointInRingParity(ring, p) ​

Even-odd containment, bare crossing count. A point exactly on an edge is not guaranteed either way; no tolerance, no boundary scan.

  • Parameters:
    • ring: PlanPoint[]
    • p: PlanPoint
  • Returns: boolean

pointStrictlyInRing(ring, p, tol, unit) ​

Inside and not on the outline.

Tolerances are defined in mm and scaled to unit; pass the unit your coordinates are in.

  • Parameters:
    • ring: PlanPoint[]
    • p: PlanPoint
    • tol?: number
    • unit?: LengthUnit — Default "mm".
  • Returns: boolean

ringWindingNumber(ring, p) ​

Signed winding number of a ring about p. Zero means outside under the non-zero rule.

  • Parameters:
    • ring: PlanPoint[]
    • p: PlanPoint
  • Returns: number

pointInRingNonZero(ring, p) ​

Non-zero containment. A point on the boundary is undefined — sample off it.

  • Parameters:
    • ring: PlanPoint[]
    • p: PlanPoint
  • Returns: boolean

pointInPolygon(poly, p) ​

Inside the outer ring and outside every hole, under the even-odd parity rule.

  • Parameters:
  • Returns: boolean

segmentsCross(p1, p2, p3, p4) ​

True when the two open segments cross at a single interior point.

  • Parameters:
    • p1: PlanPoint
    • p2: PlanPoint
    • p3: PlanPoint
    • p4: PlanPoint
  • Returns: boolean

segmentsCrossOrOverlap(a, b, c, d, unit) ​

True when the two segments cross at an interior point OR run along each other for more than a shared endpoint.

Tolerances are defined in mm and scaled to unit; pass the unit your coordinates are in.

  • Parameters:
    • a: PlanPoint
    • b: PlanPoint
    • c: PlanPoint
    • d: PlanPoint
    • unit?: LengthUnit — Default "mm".
  • Returns: boolean

pointOnSegmentInterior(a, b, p, unit) ​

True when p lies strictly between a and b on the segment a–b.

Tolerances are defined in mm and scaled to unit; pass the unit your coordinates are in.

  • Parameters:
    • a: PlanPoint
    • b: PlanPoint
    • p: PlanPoint
    • unit?: LengthUnit — Default "mm".
  • Returns: boolean

segmentsIntersect(a, b, c, d, tol, unit) ​

Closed-segment intersection: touching counts. Used by the simplicity test, where a touch is already a defect. tol defaults to 1e-6 mm scaled to unit.

Tolerances are defined in mm and scaled to unit; pass the unit your coordinates are in.

  • Parameters:
    • a: PlanPoint
    • b: PlanPoint
    • c: PlanPoint
    • d: PlanPoint
    • tol?: number
    • unit?: LengthUnit — Default "mm".
  • Returns: boolean

ringsCross(a, b) ​

True when any edge of a properly crosses any edge of b.

  • Parameters:
    • a: PlanPoint[]
    • b: PlanPoint[]
  • Returns: boolean

ringSelfIntersects(ring, unit) ​

True when non-adjacent edges of the ring cross or overlap, or a far vertex lands in the middle of an edge — a bowtie, a fold-back or a pinch.

Tolerances are defined in mm and scaled to unit; pass the unit your coordinates are in.

  • Parameters:
    • ring: PlanPoint[]
    • unit?: LengthUnit — Default "mm".
  • Returns: boolean

Effect: this is the validity gate the polygon booleans and the setback inset both run. It is stricter than ringsCross applied to a ring against itself, which ignores collinear folds.


isSimpleRing(ring, tol, unit) ​

A ring is simple when it has area, no zero-length edge, no edge pair that crosses or touches, and no neighbour pair that doubles back over itself. tol defaults to 1e-6 mm scaled to unit.

Tolerances are defined in mm and scaled to unit; pass the unit your coordinates are in.

  • Parameters:
    • ring: PlanPoint[]
    • tol?: number
    • unit?: LengthUnit — Default "mm".
  • Returns: boolean

Effect: this is the gate pack.rooms refuses a boundary on, and it is stricter than ringSelfIntersects: it works to tol rather than exactly, and a mere touch is already a defect. A hole-bridged ring is deliberately not simple by this test — bridge only after validating.


ringContainsRing(outer, inner, tol, unit) ​

True when inner lies strictly inside outer and touches none of its edges.

Tolerances are defined in mm and scaled to unit; pass the unit your coordinates are in.

  • Parameters:
    • outer: PlanPoint[]
    • inner: PlanPoint[]
    • tol?: number
    • unit?: LengthUnit — Default "mm".
  • Returns: boolean

ringWithinRing(inner, outerRing) ​

True when inner lies inside outerRing, flush seating allowed.

  • Parameters:
    • inner: PlanPoint[]
    • outerRing: PlanPoint[]
  • Returns: boolean

Effect: a vertex exactly ON the outer ring is not reliably inside — shrink inner first (that is what ringInsidePolygon does) when a flush seat has to read as inside. Contrast ringContainsRing, which refuses any contact at all.


ringsOverlap(a, b, tol, unit) ​

True when the two rings share interior area.

Tolerances are defined in mm and scaled to unit; pass the unit your coordinates are in.

  • Parameters:
    • a: PlanPoint[]
    • b: PlanPoint[]
    • tol?: number
    • unit?: LengthUnit — Default "mm".
  • Returns: boolean

Effect: rings that merely touch — two footprints abutting on a blind face — do not overlap, because each ring is pulled tol (default 1e-6 mm scaled to unit) towards its own centroid before the containment half of the test.


ringInsidePolygon(inner, poly, shrink, unit) ​

True when inner lies inside poly and clear of its holes.

Tolerances are defined in mm and scaled to unit; pass the unit your coordinates are in.

  • Parameters:
    • inner: PlanPoint[]
    • poly: Polygon
    • shrink?: number — Default 0.
    • unit?: LengthUnit — Default "mm".
  • Returns: boolean

Effect: shrink pulls inner towards its own centroid first, so a footprint seated flush on a setback line reads as inside while a genuine poke-out still fails. Use a millimetre or two, not a metre.


convexSeparation(a, b) ​

Separating-axis clear distance between two CONVEX rings. Positive is the clear gap, negative the penetration depth, zero exactly touching.

  • Parameters:
    • a: PlanPoint[]
    • b: PlanPoint[]
  • Returns: number

Effect: non-convex rings report the gap of their convex shadow, so the answer is conservative (never larger than the true clear distance).


clipRingByHalfPlane(ring, origin, normal) ​

Sutherland–Hodgman clip against one half plane: keeps the side the normal points away from, i.e. every point with (p − origin) · normal <= 0.

  • Parameters:
    • ring: PlanPoint[]
    • origin: PlanPoint
    • normal: PlanVector
  • Returns: PlanPoint[]

Effect: the subject ring may be concave or hole-bridged — only the clip window has to be convex, which a half plane always is. When the true intersection would be several disconnected pieces the result is one ring joined by zero-width bridges along the clip line: the area stays exact, the outline degrades.


clipRingByBox(ring, box) ​

Clips a ring to an axis-aligned box by four half planes.

  • Parameters:
  • Returns: PlanPoint[]

dedupeRing(ring, tol, unit) ​

Drop consecutive duplicate points (and a repeated closing point). tol defaults to 1e-6 mm scaled to unit.

Tolerances are defined in mm and scaled to unit; pass the unit your coordinates are in.

  • Parameters:
    • ring: PlanPoint[]
    • tol?: number
    • unit?: LengthUnit — Default "mm".
  • Returns: PlanPoint[]

cleanRing(ring, perpTol, unit) ​

Drop repeated vertices, an explicitly repeated closing vertex, and vertices that sit on the line through their neighbours. Ring identity is preserved. perpTol defaults to 1e-4 mm scaled to unit.

Tolerances are defined in mm and scaled to unit; pass the unit your coordinates are in.

  • Parameters:
    • ring: PlanPoint[]
    • perpTol?: number
    • unit?: LengthUnit — Default "mm".
  • Returns: PlanPoint[]

Effect: unlike simplifyRing the collinear pass repeats until nothing changes, so a run of collinear vertices collapses to its ends. This is the normalisation every boolean operand goes through.


canonicaliseRing(ring) ​

Canonical vertex order: the ring starts at its lexicographically smallest vertex. Winding is untouched. Keeps fixtures stable when a tracer starts somewhere else.

  • Parameters:
    • ring: PlanPoint[]
  • Returns: PlanPoint[]

simplifyRing(ring, tol, unit) ​

Drops consecutive duplicates and vertices that sit on the straight line between their neighbours. Clipping leaves both behind. tol defaults to 0.01 mm scaled to unit.

Tolerances are defined in mm and scaled to unit; pass the unit your coordinates are in.

  • Parameters:
    • ring: PlanPoint[]
    • tol?: number
    • unit?: LengthUnit — Default "mm".
  • Returns: PlanPoint[]

convexHull(pts) ​

Andrew's monotone chain. Returns the hull counter-clockwise, no repeats.

  • Parameters:
    • pts: PlanPoint[]
  • Returns: PlanPoint[]

ringObb(ring) ​

Minimum-width oriented box, by rotating calipers over the convex hull: the thinnest strip that contains the ring, and the ring's extent along it.

  • Parameters:
    • ring: PlanPoint[]
  • Returns: RingObb

Effect: width is what pack.rooms calls a cell's width, and aspectRatio is what its proportion score reads. For a concave cell the strip is an over-estimate of the usable width — it is the width of the hull.


sharedEdgeLength(a, b, tol, unit) ​

Total length over which the two rings run along the same line within tol (default 1 mm scaled to unit). This is the "is there room for a door between these two cells" measure: two cells that only meet at a corner share zero length.

Tolerances are defined in mm and scaled to unit; pass the unit your coordinates are in.

  • Parameters:
    • a: PlanPoint[]
    • b: PlanPoint[]
    • tol?: number
    • unit?: LengthUnit — Default "mm".
  • Returns: number

rotatePoint(p, deg) ​

Rotate a plan point about the origin. Positive angles turn +x towards +z.

  • Parameters:
    • p: PlanPoint
    • deg: number
  • Returns: PlanPoint

rotateRing(ring, deg) ​

Rotate every point of a ring about the plan origin.

  • Parameters:
    • ring: PlanPoint[]
    • deg: number
  • Returns: PlanPoint[]

rotatePolygon(poly, deg) ​

Rotate a polygon and its holes about the origin.


rectRing(centre, width, depth, rotationDeg) ​

A rectangle centred on centre, width along its own x axis and depth along its own z axis, rotated counter-clockwise by rotationDeg. Wound counter-clockwise, starting at the local min corner.

  • Parameters:
    • centre: PlanPoint
    • width: number
    • depth: number
    • rotationDeg: number
  • Returns: PlanPoint[]

placeRing(ring, rotationDeg, offset) ​

Rotate a ring about the origin, then translate it. Used to seat a plate.

  • Parameters:
    • ring: PlanPoint[]
    • rotationDeg: number
    • offset: PlanPoint
  • Returns: PlanPoint[]

shrinkRing(ring, by) ​

Move every vertex by towards the ring's area centroid. Zero or less is a copy.

  • Parameters:
    • ring: PlanPoint[]
    • by: number
  • Returns: PlanPoint[]

edgeDistances(edgeCount, setback) ​

Per-edge distances from a uniform number or a per-edge list.

  • Parameters:
    • edgeCount: number
    • setback: number | number[] | undefined
  • Returns: number[]

bridgeHolesIntoRing(outer, holes) ​

Folds every hole into the outer ring with a zero-width slit, giving one ring whose signed area is the net plannable area and whose even-odd interior excludes the holes. Holes are taken largest first so the result is independent of input order.

  • Parameters:
    • outer: PlanPoint[]
    • holes: PlanPoint[][]
  • Returns: PlanPoint[]

Effect: downstream clipping, area and containment need no hole argument at all. The cost is that cell rings inherit the slits as zero-width spurs — real area, degenerate outline.


stableHash(value) ​

FNV-1a over a string. Same string, same number, forever.

  • Parameters:
    • value: string
  • Returns: number

hashUnitVector(key) ​

A unit vector derived from a string. Used where there is no direction to be had (coincident centres).

  • Parameters:
    • key: string
  • Returns: PlanVector

hashUnitRatio(key) ​

A number in [0, 1) derived from a string. Deterministic jitter without an RNG.

  • Parameters:
    • key: string
  • Returns: number

Examples ​

Area, centroid and the narrowest dimension of a plate (both)

ts
const obb = ringObb(ring);
return {
  area: ringArea(ring),
  centroid: ringCentroid(ring),
  minWidth: obb.width,
  aspectRatio: obb.aspectRatio
};

Is there room for a door between these two cells? (both)

ts
return sharedEdgeLength(a, b) >= doorWidth;