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

track-layout

v0.12.0

Published

Railway track layout model: a Bezier track graph with junctions and elevation, stations and platforms, and save/load.

Downloads

1,890

Readme

track-layout

Railway track layout model for freeform, real-scale Bezier track: a track graph with junctions and elevation, stations and platforms, and save/load.

Extracted from banana, a 2D railway simulator. Built on the @ue-too packages, which are peer dependencies.

Status: 0.x. APIs may change between minor versions.

Install

bun add track-layout @ue-too/board @ue-too/curve @ue-too/math

Example

import { TrackGraph } from 'track-layout';

const graph = new TrackGraph();
const east = { x: 1, y: 0 };
const a = graph.createNewEmptyJoint({ x: 0, y: 0 }, east);
const b = graph.createNewEmptyJoint({ x: 100, y: 0 }, east);

// Straight track is a quadratic curve with its midpoint as control point.
graph.setNewSegmentStyle({ trackStyle: 'slab' });
graph.connectJoints(a, b, [{ x: 50, y: 0 }], 1.067);

const saved = JSON.stringify(graph.serialize());

Laying and editing track

track-layout/editing holds the laying engine and the editing tools as state machines: layout, joint direction, duplicate to side and catenary. They need the optional peer @ue-too/being.

import { TrackGraph } from 'track-layout';
import {
    CurveCreationEngine,
    createLayoutStateMachine,
} from 'track-layout/editing';

const graph = new TrackGraph();
// Replace with your camera's window-to-world conversion.
const windowToWorld = (p: { x: number; y: number }) => p;
const engine = new CurveCreationEngine(graph, windowToWorld);
const layout = createLayoutStateMachine(engine);

layout.happens('startLayout');
layout.happens('pointerMove', { x: 0, y: 0 });
layout.happens('leftPointerUp', { x: 0, y: 0 });
layout.happens('pointerMove', { x: 100, y: 0 });
layout.happens('leftPointerUp', { x: 100, y: 0 }); // lays a segment

Placing stations

track-layout/station-placement holds the station placement tools as state machines: island stations, single-spine platforms and dual-spine platform pairs. Like the editing tools, they need @ue-too/being.

  • Previews. Each engine draws its preview through an interface you implement with your renderer: StationPlacementPreview, SingleSpinePlacementPreview or DualSpinePlacementPreview.
  • Commits go to the managers. Draw new stations and platforms from StationManager.onStationAdded and TrackAlignedPlatformManager.onPlatformAdded, and remove them on onStationRemoved and onPlatformRemoved. The spine tools also link each new platform to its station, and move a station onto its first platform; TrackAlignedPlatformManager.onChange signals that, not a StationManager event.
  • Hints. The spine tools report each step through onHint, with keys from SINGLE_SPINE_HINT_KEYS and DUAL_SPINE_HINT_KEYS.
import {
    ELEVATION,
    StationManager,
    TrackAlignedPlatformManager,
    TrackGraph,
} from 'track-layout';
import {
    SingleSpinePlacementEngine,
    type SingleSpinePlacementPreview,
    createSingleSpinePlacementStateMachine,
} from 'track-layout/station-placement';

const graph = new TrackGraph();
const stations = new StationManager();
const platforms = new TrackAlignedPlatformManager();
// Replace with your renderer's preview and your camera's conversion.
const preview: SingleSpinePlacementPreview = {
    showTrackHighlight() {},
    showPlacementPreview() {},
    hidePreview() {},
};
const windowToWorld = (p: { x: number; y: number }) => p;

platforms.onPlatformAdded(id => console.log('draw', platforms.getPlatform(id)));

const engine = new SingleSpinePlacementEngine(
    graph,
    windowToWorld,
    stations,
    platforms,
    preview,
    key => console.log(key) // e.g. 'hintPickStart'
);
const tool = createSingleSpinePlacementStateMachine(engine);
const stationId = stations.createStation({
    name: 'Central',
    position: { x: 50, y: 10 },
    elevation: ELEVATION.GROUND,
    platforms: [],
    trackSegments: [],
    joints: [],
    trackAlignedPlatforms: [],
});
tool.happens('startPlacement', { stationId });
// Then pointerMove / leftPointerUp: pick the spine's start and end on a
// track, add outer vertices, and click the start anchor to create it.

Drawing a layout with Pixi

track-layout/pixi holds Pixi 8 renderers for track, stations, track-aligned platforms and joint-direction indicators. It needs the optional peer pixi.js, 8.20.1 or a later 8.x, but not @ue-too/being.

  • Layer host. The renderers draw into a LayerHost, which orders content by elevation band. WorldRenderSystem is the default one: add its container to your stage under the camera transform. Draw your own elevation-ordered content (trains, buildings) into the same host so it interleaves with track.
  • Track. TrackRenderSystem draws the track the graph already holds when it is built, then track as the graph adds and removes it. Its options are all optional:
    • textureRenderer, such as { renderer: app.renderer }. Without it detailed track is only the zoomed-out line, and that is hidden from zoom level 5 up: zoomed-in track (ballast, rails, beds, shadows, tunnels and cuttings) isn't drawn, and neither are detailed stations or platforms (catenary poles still are). Apps that draw detailed track for people should always pass one.
    • terrain, anything with getHeight(x, y). Without it the ground is flat at height 0, so track below ground level is drawn in a tunnel (given a textureRenderer).
    • curveCreation, duplicateToSide and catenaryLayout: the engines from track-layout/editing, whose previews and highlights it draws.
  • Stations and platforms. StationRenderSystem and TrackAlignedPlatformRenderSystem draw the stations and platforms their managers already hold when they are built, then draw and remove them as the managers create and destroy them. Both renderers also implement the station placement previews.
  • Render styles. Set a renderer's renderStyle to draw with lines instead of textures. It redraws everything when the style changes.
    • TrackRenderSystem: 'detailed' (the default: textured track when zoomed in, a line when zoomed out), 'centerline' (a line along the middle of each segment) or 'rails' (a line along each rail, the segment's gauge apart). The line styles show at every zoom level and draw the previews the same way. They leave out ballast, beds, shadows, catenary masts and tunnel walls and cuttings. They draw a bridge (parapets and wings) where track crosses at least 3 m over other track, which is cut beneath it, and underground track as lighter, broken lines with a portal where it reaches the surface. A deck or gap near a joint carries on across it, onto every segment there (all the branches at a junction). Tracks side by side over the same track, at the same level and with no more than 2 m between their parapets, share one bridge: only the outer parapets are drawn, and the gap beneath runs unbroken. Highlights and snap dots are drawn as in 'detailed'.
      • Each segment's lineStyle ({ preset?, pattern?, color?, width? }, saved with the layout) changes how the line styles draw it. Presets are tunnel (drawn and treated as underground), bridge (parapets along the segment), planned (dashed) and disused (dotted, grey); set fields win over the preset. Patterns and widths are in screen pixels.

        graph.setSegmentStyle(n, {
            lineStyle: { preset: 'planned', color: 0x2266cc },
        });
      • bridgeGapClearance (option and property): how far, in metres, a gap reaches past the upper track's parapets; 0.5 by default, from 0 to 25.

    • StationRenderSystem and TrackAlignedPlatformRenderSystem: 'detailed' (the default) or 'outline', which outlines each platform. The two platforms that make up an island are outlined separately, so a line runs down its middle.
    • Lines are one pixel wide at any zoom unless a lineStyle sets a width, and the line styles need no textureRenderer.
  • Joint directions. new JointDirectionRenderSystem(host, trackGraph, preferenceMap, camera) draws its indicators while the joint-direction tool is shown: call show() when the tool activates and hide() when it deactivates.
  • Each renderer's cleanup() removes what it drew and stops listening (dispose() for JointDirectionRenderSystem). StationRenderSystem's cleanup() leaves its placement preview, so call hidePreview() first if one is showing.
import { DefaultBoardCamera } from '@ue-too/board';
import { Application } from 'pixi.js';
import {
    StationManager,
    TrackAlignedPlatformManager,
    TrackGraph,
} from 'track-layout';
import { CurveCreationEngine } from 'track-layout/editing';
import {
    StationRenderSystem,
    TrackAlignedPlatformRenderSystem,
    TrackRenderSystem,
    WorldRenderSystem,
} from 'track-layout/pixi';

const app = new Application();
await app.init();
const camera = new DefaultBoardCamera();
const graph = new TrackGraph();
const stations = new StationManager();
const platforms = new TrackAlignedPlatformManager();
// Replace with your camera's window-to-world conversion.
const windowToWorld = (p: { x: number; y: number }) => p;

const host = new WorldRenderSystem();
app.stage.addChild(host.container); // apply the camera's transform to it
const textureRenderer = { renderer: app.renderer };

new TrackRenderSystem(host, graph.trackCurveManager, camera, {
    textureRenderer,
    curveCreation: new CurveCreationEngine(graph, windowToWorld),
});
new StationRenderSystem(host, stations, graph, textureRenderer);
new TrackAlignedPlatformRenderSystem(
    host,
    platforms,
    stations,
    graph,
    textureRenderer
);

Development

bun install
bun test
bun run typecheck
bun run build
bun run pack:local   # writes .pack/track-layout-local.tgz for trying in an app

Releasing

Releases are published from GitHub Actions: run the Release workflow on main (or a version/* branch for a patch to an older line).

  • Version bump: auto reads the conventional commits since the last v* tag (bun scripts/next-version.ts); patch, minor and major force one.
  • Dry run: bumps and packs in the runner, then stops without publishing or pushing. It can run on any branch.
  • A release commits chore(release): track-layout x.y.z, tags vx.y.z, publishes to npm with provenance, opens a GitHub release, and, from main, creates a version/x.y.z branch.

npm trusts the workflow through trusted publishing, so no npm token is stored.