npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@proc-geo/core

v0.2.1

Published

Procedural geometry algorithms in pure TypeScript — straight skeleton, random polygon generation, pen-stroke to spline fitting, and D0L L-systems

Downloads

251

Readme

@proc-geo/core

Procedural geometry algorithms in pure TypeScript. No React, no browser APIs, no canvas — just functions over plain { x, y } data, so the same code runs in Node, a worker, or the browser.

npm install @proc-geo/core

Ships ESM + CJS with bundled type declarations. The only runtime dependency is loglevel.

Modules

| Module | What it does | |---------------------|-------------------------------------------------------------------------------------| | Straight skeleton | Computes the straight skeleton of a simple polygon, including reflex/split events. | | Random polygon | Generates random non-self-intersecting polygons from tunable parameters. | | Stroke → spline | Turns raw pointer-capture samples into a fitted cubic Bézier spline. | | D0L system | Deterministic context-free L-systems with turtle interpretation. |

Everything is exported from the package root:

import { runAlgorithmV5, runStrokePipeline, generateRandomPolygon } from '@proc-geo/core';

Straight skeleton

The straight skeleton is the locus traced by a polygon's vertices as its edges move inward at equal speed. It underpins roof generation, straight-line polygon offsetting, and medial-axis approximation.

import { runAlgorithmV5 } from '@proc-geo/core';
import type { Vector2 } from '@proc-geo/core';

// Clockwise winding — see the warning below.
const polygon: Vector2[] = [
  { x: 0, y: 0 },
  { x: 0, y: 120 },
  { x: 200, y: 120 },
  { x: 200, y: 0 },
];

const context = runAlgorithmV5(polygon);
const graph = context.graph; // nodes + edges of the completed skeleton

For this rectangle the result has 6 nodes and 10 edges: the 4 original corners plus the two ridge nodes where the bisectors meet.

Input must be wound clockwise. Counter-clockwise input does not throw — the solver logs Skeleton remains incomplete at warn level and returns a graph containing only the original boundary, with no interior edges. If your winding is not already guaranteed, normalize first:

import { ensureClockwiseSkeleton } from '@proc-geo/core';

const context = runAlgorithmV5(ensureClockwiseSkeleton(polygon));

Note that "clockwise" is measured in the coordinate system you supply. Screen coordinates put y downward, so a polygon that looks clockwise on screen is counter-clockwise numerically. isClockwise is exported if you want to check.

runAlgorithmV5 takes at least three vertices and throws below that. Self-intersecting input is decomposed automatically (decomposePolygon) and the sub-results merged (mergeSkeletonGraphs).

The returned StraightSkeletonSolverContext exposes the graph plus the solver's edge-lookup helpers. Exterior edges (the original polygon boundary) and interior edges (the bisector rays generated during the run) are tracked separately; interior edge IDs begin at graph.numExteriorNodes.

Stepping through a run

For visualization or debugging, runAlgorithmV5Stepped returns snapshots instead of just the final state, and reports failure by return value rather than throwing:

const { snapshots, error } = runAlgorithmV5Stepped(polygon);

Logging

The solver logs through loglevel under the skeleton:* namespaces, defaulting to warn:

import { setSkeletonLogLevel } from '@proc-geo/core';

setSkeletonLogLevel('debug'); // 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'silent'

Random polygon

Generates a random simple polygon by walking edges with randomized lengths and turn angles, retrying until the result is non-self-intersecting.

import { generateRandomPolygon, DEFAULT_PARAMS } from '@proc-geo/core';

const polygon = generateRandomPolygon({
  ...DEFAULT_PARAMS,
  maxEdges: 12,
});

RandomPolygonParams takes edgeLength and angleDelta as { min, max, variance } ranges plus a maxEdges cap. variance blends between a uniform distribution (0) and one peaked at the range's center (1). The optional second and third arguments set the start position and the retry limit (default 10); if every attempt fails, a small triangle is returned rather than throwing.

Pair it with @proc-geo/test-fixtures for 35 named polygons covering known tricky cases.


Stroke → spline

A four-stage pipeline that converts raw pointer samples into a fitted cubic Bézier spline. Each stage is independently configurable and every stage has a pass-through variant, so you can isolate one stage's effect.

import { runStrokePipeline, DEFAULT_STROKE_PIPELINE_CONFIG } from '@proc-geo/core';
import type { StrokePoint } from '@proc-geo/core';

// Captured from pointermove: t is the event timestamp in ms.
const raw: StrokePoint[] = [
  { x: 10, y: 10, t: 0 },
  { x: 24, y: 31, t: 16 },
  // …
];

const result = runStrokePipeline(raw, DEFAULT_STROKE_PIPELINE_CONFIG);
result.fit?.segments;  // CubicBezier[]
result.fit?.maxError;  // worst deviation from the input points

Stages and variants

| Stage | Variants | |-------------------|-----------------------------------------------------------------| | smoothing | pass-through, moving-average, gaussian, one-euro, chaikin | | simplification | pass-through, rdp, resample | | cornerDetection | pass-through, angle-threshold | | fitting | pass-through, schneider, catmull-rom |

Each variant is a discriminated union member carrying its own parameters, so the type checker enforces that { variant: 'gaussian' } supplies sigma and nothing else:

const result = runStrokePipeline(raw, {
  smoothing: { variant: 'one-euro', minCutoff: 1, beta: 0.005 },
  simplification: { variant: 'rdp', epsilon: 2 },
  cornerDetection: { variant: 'angle-threshold', thresholdDeg: 60, span: 4 },
  fitting: { variant: 'schneider', errorTolerance: 4 },
});

SMOOTHING_VARIANT_DEFAULTS and its three siblings provide a sensible starting config per variant — useful for building a UI where changing a dropdown swaps in fresh parameters.

Notes on the individual stages:

  • one-euro is the 1€ filter (Casiez et al. 2012): velocity-adaptive, so it smooths hard when the pen is slow and lags little when it is fast. It reads the t timestamps and, being causal, does not pin the final point. The kernel smoothers (moving-average, gaussian) shrink their window at the ends and so pin both endpoints exactly.
  • schneider is Schneider's classic least-squares fitting with recursive subdivision, adding segments until errorTolerance is met. catmull-rom interpolates every input point exactly (zero error) — pick it when the curve must pass through the samples rather than approximate them.
  • Corner detection feeds hard breakpoints into fitting. Sections between corners are fitted independently, so tangents are one-sided and the curve creases at a corner instead of rounding it off.

Correspondence and morphing

result.correspondence is index-matched to raw: entry i is where raw point i lands on the final curve. This holds even when a stage changes the point count (rdp, resample, chaikin) — the pipeline falls back to arc-length-fraction mapping in that case. It makes animating the cleanup a one-liner:

import { lerpStroke } from '@proc-geo/core';

const midway = lerpStroke(raw, result.correspondence, 0.5);

The intermediate stage outputs (smoothed, simplified, corners) are all returned too, so you can render the pipeline stage by stage.


D0L system

Deterministic, context-free Lindenmayer systems with a turtle-graphics interpreter. An alphabet of user-defined letters rewrites over generations; each letter resolves to a sequence of the six turtle keywords F, +, -, [, ], f.

import {
  compileDolSystem,
  generateDolSystem,
  interpretDolSystem,
} from '@proc-geo/core';

const system = compileDolSystem({
  alphabet: { A: ['F'], B: ['F'] },
  productions: { A: ['B', '[', '+', 'A', ']', '-', 'A'], B: ['B', 'B'] },
  axiom: ['A'],
  turtle: { stepLength: 10, angleDelta: 25, generationScaling: 0.9 },
  maxIterations: 8,
});

const generated = generateDolSystem(system, 5);
const { paths, bounds } = interpretDolSystem(generated, {
  stepLength: 10,
  angleDelta: 25,
  generationScaling: 0.9,
});

compileDolSystem validates the configuration and throws DolSystemValidationError — carrying an errors array of { field, message } — when it fails. Letters missing from productions receive an identity rule.

generateDolSystem(system, generations, maxWordLength?, skipProvenance?) accepts a word-length cap to bound runaway growth. interpretDolSystem returns paths as an array of polylines — a new polyline starts after each ] pop — plus the overall bounds. Each Segment carries the letter it descended from, so output can be styled by which rule produced it.


Related packages

License

MIT © Will Buchanan