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

@toolpath/tool-support

v0.6.0

Published

The cutting-tool domain: what a tool, holder, collet and assembly are

Downloads

1,273

Readme

Toolpath Tool Support

@toolpath/tool-support is the cutting-tool domain: what a tool, a holder, a collet and an assembly are, and the arithmetic that follows from them.

It depends on nothing. No runtime dependencies, no peer dependencies, no React, no DOM, no fs, no Toolpath SDK. Every other package that speaks about cutting tools depends on it and it depends on none of them, which is what lets a Node ingest script, a server route and a React renderer share one answer instead of deriving three.

                    @toolpath/tool-support        depends on nothing
                     ↑          ↑          ↑
    @toolpath/tool-scraper   @toolpath/tool-drawing   your application

Install

npm install @toolpath/tool-support

Why it exists

@toolpath/tool-scraper produces tool data and @toolpath/tool-drawing consumes it, and every application in between re-derived what a tool assembly is. The same fact ended up declared three times over: two names for one millimetres-per-inch constant, three vocabularies for one unit axis, three provenance types, two PROFILES_VERSIONs — one of them imported under an alias specifically so it could be compared against the other — and three shapes called "holder" of which no two agreed on which fields exist.

That is not a tidiness complaint. How far a tool stands out of its holder was computed in four unconnected places and disagreed by a factor of two on an ordinary tool. A details table printed one number and the drawing beside it drew another, so the dimension line ran past the holder nose and into the holder body. It was fixed inside one application, which means the next consumer of the same two packages reproduces it from scratch. That quantity is a pure function of the tool, the collet and a shop's policy, and it had no home.

What it holds

The vocabulary

| Export | What it is | | ---------------------------------------------------------------------- | ----------------------------------------------------------------- | | UnitSystem, UNIT_SYSTEMS, MM_PER_INCH | How a vendor publishes a family, and the one constant between two | | convertLength, decimalsFor, UNIT_ABBREVIATION | Converting, rounding and spelling it | | Provenance, PROVENANCE, ProvenanceMap | Where a stated number came from | | GEOMETRY_FIELDS, geometryField, isLengthField, convertGeometry | The ISO 13399 dictionary, and whether a code's value converts | | TOOL_FORMS, ToolForm, MILLING_FORMS, isToolForm | What a tool is, in a CAM library's words |

Every length is in millimetres and every angle in degrees, whatever system the vendor published in — that is what lets an inch tool and a metric tool compare. UnitSystem is a fact about the tool, never the unit a stored value is in.

The contracts

| Export | What it is | | -------------------------------------------------- | -------------------------------------------- | | Tool, Geometry | A cutting tool, as the arithmetic needs one | | Holder, HolderProfile, isHolderProfile | The holder union: published, or measured | | Collet, Assembly | What grips the shank, and the stack it makes | | PROFILES_VERSION, ProfilePoint, ProfileDatum | The measured silhouette | | ReachCurve, FeatureDemand | What a feature demands of a tool |

The arithmetic

| Export | What it answers | | --------------------------------------------------------------------------------- | ------------------------------------------------------- | | stickoutRange, minStickout, setupStickout, stickoutCeiling | How far the tool stands out — the one answer | | DEFAULT_STICKOUT_POLICY, HELD_SHARE, StickoutPolicy, StickoutLimit | The floor, the step and the share a shop keeps held | | clampWanted, clampShortfall, heldDiameter, headLength, DEFAULT_CLAMPING | What the clamping rule keeps in the holder | | holderTakesTool, colletFitsHolder, gripsShank, maxStickout, holdBand | Whether this holder takes this tool | | gripRanges, gripsAnyShank, canHold | Whether a whole crib can hold it, as one set of spans | | stickoutLimits, defaultStickout | The collet-shaped way into the range above | | hasNeck, shankOf, heightAt, belowGageLine | The four that used to be written twice | | fitAgainst, fitTools, DRILLING_FORMS | Whether a cutter cuts a feature | | clearance, toolSilhouette, holderSilhouette, toolCollisions | Whether the stack clears the material around it | | assemblyAgainst, NOT_MODELLED | Whether there is a way to hold it that reaches | | sectionOutline, materialProfile, ASSEMBLY_PARTS, SILHOUETTE_PARTS | The feature in section, and what an assembly is made of |

Two kinds of holder

A holder arrives in one of two forms, and they are alternatives rather than a refinement of one by the other:

import { isHolderProfile, type Holder, type HolderProfile } from '@toolpath/tool-support'

const published: Holder = {
  noseDiameter: 27,
  noseLength: 12,
  bodyDiameter: 42,
  bodyLength: 20,
  projection: 60,
  flangeDiameter: 46,
  gaugeLength: 60,
  colletSeries: 'ER16',
  colletProtrusion: 2.5,
}

const measured: HolderProfile = {
  points: [
    [-30, 8],
    [0, 23],
    [48, 21],
    [60, 13.5],
  ],
  datum: 'gage-line',
  colletSeries: 'ER16',
  colletProtrusion: 2.5,
}

isHolderProfile(measured) // true — read `points`
isHolderProfile(published) // false — read the vendor's own numbers

Holder is a handful of numbers off a DIN 4000 sheet, and a drawing built from them is a stylised holder. HolderProfile is the envelope measured off the vendor's STEP model — a hundred-odd vertices carrying the V-flange groove and the thread relief a machinist actually looks for. Reducing it to a nose and a body throws away the only reason to measure, so the two are a union and isHolderProfile tells them apart. A consumer with neither passes null.

No classes, deliberately

Everything here is a readonly interface or a pure function over one. Two reasons beyond consistency:

  • A class loses structural typing at a package boundary. A catalog's own record simply is a Tool, with no adapter — the adapter stays a choice rather than becoming a requirement.
  • instanceof breaks across duplicate installs. This tree has been bitten by that once already, and @toolpath/tool-scraper carries a packaging test about it.

Where several quantities have to agree, one function answers them together rather than one apiece: stickoutRange hands back the minimum, the setup length and the ceiling in a single record, along with which of the three caps set it. A caller cannot take one of those and forget the others, which is precisely how the defect above happened — four callers each worked out the piece they wanted.

An unrecognised code is not given a meaning

geometryField answers null and isLengthField answers false for a code the dictionary has not pinned. Show it as the vendor's own and do not convert it: a guessed unit is a wrong number that looks right, and a 118-degree drill point converted as a length is a plausible-looking 2.36.

import { convertGeometry, geometryField } from '@toolpath/tool-support'

convertGeometry('DC', 6, 'millimeters', 'inches') // 0.2362…
convertGeometry('SIG', 118, 'millimeters', 'inches') // 118 — an angle does not convert
convertGeometry('WOC', 6, 'millimeters', 'inches') // 6 — unpinned, so not converted
geometryField('WOC') // null

The stickout, which is why this exists

import { stickoutRange } from '@toolpath/tool-support'

const range = stickoutRange(
  { unitSystem: 'inches', geometry: { DC: 25.4, OAL: 127, LCF: 31.75, SFDM: 25.4 } },
  { grip: 27.5 },
)
// range.setup      — what a machinist sets it up at. This is `geometry.LBH`.
// range.max        — the furthest it could ever stand out.
// range.limitedBy  — 'clamp' | 'hold' | 'collet': which cap decided, so a
//                    control can say why rather than showing a bare number.

Every other stickout is this call with more arguments, and min ≤ setup ≤ max holds by construction — so a drawn stickout can never exceed the length a table prints beside it. That invariant is a test, not a sentence in this file.

Exporting to a CAM system

@toolpath/tool-support/export/fusion writes an Autodesk Fusion tool library. It is the first of several exporters, so what every exporter needs — the input's identity half, and the account of what could not be carried across — lives in @toolpath/tool-support/export and one subpath per format holds the rest.

import { fusionLibrary, fusionLibraryJson } from '@toolpath/tool-support/export/fusion'

const { document, notes } = fusionLibrary({
  tools: [{ tool, assembly: { stickout: 24, holder } }],
})
writeFileSync('shop.tools', fusionLibraryJson(document))

tool is a CatalogTool: a Tool plus the guid, vendor, catalog number and unit system a catalog carries. Every field is spelled the way @toolpath/tool-scraper's ToolRecord spells it, so a scraped record is an input with no adapter — asserted by assignment in the tests, so a name that drifts stops compiling.

notes is the half that matters on real data. A catalog always contains tools a format cannot hold, so a batch is never refused for one bad record: the tool is left out and the note says why. Each note is skipped (no record written), dropped (a fact the format has no word for), filled (a value this package supplied) or coerced (a stated value mapped to something weaker).

What it will and will not supply is the one rule worth knowing. Fusion requires geometry a vendor does not always publish — a bare tool states neither how far it stands out nor its assembly gauge length — so the default fills conventions (a tool is right-handed, a thread has one start) and readings of the tool's own dimensions (LB from setupStickout; a plain shank's shoulder is its shank). It never invents a measurement: a bull nose with no stated corner radius is skipped rather than exported as a flat end mill. fill: 'none' supplies nothing and reports every gap.

Feeds and speeds

Presets are carried, not computed — what a tool's feeds and speeds should be is a machining model this package does not have. A caller passes them and the exporter checks them against the type they are going on:

fusionLibrary({ tools: [{ tool, presets }] })

That check is worth more than it sounds, because Autodesk states a preset's shape per tool type and the five shapes are nothing like each other. A milling preset requires seventeen fields; a tap's requires six and models nine in total, having no feedrate at all. A drill states a feed per revolution and no cutting feedrate, while a spot drill — which applications commonly generate through the same code as a drill — additionally requires five feedrates a drill does not model. A preset short of what its type demands is dropped with a note naming every missing field, and a field the type does not model is dropped with a note of its own. The tool still exports either way: presets: [] is legal, and a tool with no feeds is more use than a tool that was left out.

Presets are not unit-converted. Which unit each field uses differs per field — v_c is metres or feet per minute, v_f millimetres or inches per minute, f_z a length per tooth — so they must arrive stated in the same unit system as the tool. This is the one place in the exporter where a caller can be wrong and nothing will notice.

The rules each type must satisfy are Autodesk's own published JSON Schema, reduced into fusion/digest.json at the repository root. pnpm fusion:check-upstream asks Autodesk weekly whether it has moved, and a test fails if the table this package reads drifts from it.

Status

0.x: in use, and the surface still moves. A minor is the breaking channel while the major is 0 — ^0.1.0 does not accept 0.2.0 — so nothing arrives in a consumer's build without them asking for it.