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

@invisra/draft

v0.1.1

Published

A TypeScript library for generating engineering-style SVG drawing sheets.

Readme

draft

A TypeScript library for generating engineering-style SVG drawing sheets.

draft is a lightweight, dependency-free 2D drafting core: geometry primitives (lines, arcs, polylines), a physical-unit SVG renderer, standard paper sizes (ANSI + ISO), and a title block — the pieces you need to lay out engineering drawing sheets, without a 3D CAD kernel underneath.

Install

npm install @invisra/draft

Using an AI coding agent? llms.txt is a dense API index (every export, one line each, plus worked examples) sized for an agent's context window — it ships in the published package at node_modules/@invisra/draft/llms.txt.

Quick start

import { Sheet, TitleBlock, DrawingElement, roundedRectangle, circle, centerMark, LinearDimension, RadialDimension } from "@invisra/draft";

const sheet = new Sheet({ orientation: "landscape", borderStyle: "zoned" }); // defaults to ANSI A (Letter), "plain" border

sheet.setTitleBlock(
  new TitleBlock({
    title: "BRACKET, MOUNTING",
    drawingNumber: "DRW-1000",
    revision: "A",
    scale: "1:1",
    sheet: "1 OF 1",
    material: "AL 6061-T6",
    finish: "ANODIZE, CLEAR",
    generalTolerance: ["X.XX = ±0.01", "ANGLES = ±0.5°"],
    projection: "third-angle",
    drawnBy: "S. RICHS",
    drawnDate: "2026-07-08",
  }),
);

const area = sheet.drawingArea;
sheet.add(new DrawingElement(roundedRectangle(area.x + 20, area.y + 20, 80, 50, 6)));

const holeCenter = { x: area.x + 30, y: area.y + 30 };
sheet.add(new DrawingElement(circle(holeCenter.x, holeCenter.y, 4)));
for (const mark of centerMark(holeCenter, 4, 2.5)) {
  sheet.add(new DrawingElement(mark, { lineStyle: "centerline" }));
}

sheet.add(new LinearDimension({ x: area.x + 20, y: area.y + 20 }, { x: area.x + 100, y: area.y + 20 }, { offset: -12, orientation: "horizontal" }));
sheet.add(new RadialDimension(holeCenter, 4, { angleDeg: 45 }));

const svg = sheet.toSVG();

See examples/basic-sheet.ts for a complete runnable example (npm run example).

Design

  • Units: all geometry is in millimeters. Sheets rendered from an inch-based paper size (e.g. ANSI/Letter) convert internally; use toMM/fromMM from units.ts at any external boundary that deals in inches.
  • Coordinate system: Y-up, origin at the bottom-left of the sheet — standard drafting convention (+X right, +Y up). The SVG renderer applies a single coordinate flip so you never think in SVG's native Y-down space. TextElement carries its own counter-flip, so text you place at a Y-up position renders upright.
  • Geometry: Path is a Canvas2D-style builder (moveTo/lineTo/arc/close) over LineSegment/ArcSegment primitives. Arcs are stored in CAD form (center, radius, start/end angle, sweep direction) rather than SVG's endpoint form, so sweep direction is unambiguous; SVG arc flags are derived at serialization time.
  • Paper sizes: PAPER_SIZES covers ANSI A–E, ISO 216 A0–A5, and US architectural ARCH A–E1. Sheet takes a paperSize + orientation and exposes drawingArea (the usable region inside the border and above the title block). JIS mechanical drawings use the same ISO A sizes already provided (JIS A ≡ ISO A) — there's no separate JIS export, since the JIS B series is a real but different scale meant for general stationery, not drafting; adding it under a "JIS" paper-size option would be wrong, not just redundant. For anything else, customPaperSize(widthMM, heightMM, label?) builds a one-off PaperSizePaperSize is a plain data shape, so any object matching it works as paperSize regardless of where it came from.
  • Border: Sheet's borderStyle defaults to "plain" (a single frame). "zoned" adds a map-style zone reference grid in the margin — numbered columns left-to-right and lettered rows top-to-bottom (I/O skipped to avoid confusion with 1/0), so a location can be called out like "zone C3" (used by revision callouts). Zone count is derived per sheet from a ~50mm target zone size (zonedBorderOptions.targetZoneSizeMM), matching documented ISO 5457 practice; exact starting corner/direction varies a little across ASME and company templates in practice, so treat this as a sensible default rather than a hard guarantee of one specific standard.
  • Title block: TitleBlock renders the classic engineering "corner block" layout — a signoff grid (drawn/checked/eng approved/mfg approved, each with name + date) on the left, and drawing identification (size, drawing number, revision, title, material, finish, general tolerances, projection-angle symbol, scale, sheet) on the right. Anchored to the bottom-right of the sheet, sized independently of the sheet width (clamped to fit narrow sheets). size falls back to the Sheet's paper size label automatically. The projection symbol ("first-angle" / "third-angle") is the real ISO 128 / ASME Y14.3 pictogram — a truncated cone in profile next to its concentric-circle end view, not just a text label. ISO7200TitleBlock is a second built-in style, based on the ISO 7200:2004 field set (legal owner, identification number, revision index, date of issue, sheet, title, creator, approval person, document type) — a single full-width column, deliberately fewer fields than the ASME block, per the standard's own stated principle of keeping the title block's field count to a minimum. (The standard's example title-block diagram sits behind ISO's paywall beyond the fields/obligations table, so this layout is my own reasonable arrangement of the verified field set, not a pixel-for-pixel reproduction of ISO's Figure 1 — the field names and mandatory/optional status themselves came straight from the standard text.) For anything else, GridTitleBlock is the generic system both of these are built on: pass it your own columns of rows of cells (grid.ts"labeled" cells with a corner label plus a value, or "caption" cells with centered static text) for a fully custom layout. Sheet.setTitleBlock() accepts any TitleBlockLike ({ heightMM, render(ctx) }), so a custom title block needs no special registration.
  • Line styles: DrawingElement takes a lineStyle"visible" (thick, continuous, the default), "hidden" (thin dashed), "centerline" (thin, long dash/short dash), or "phantom" (thin, long dash/short/short) — per ASME Y14.2 / ISO 128 convention. An explicit stroke object still overrides individual fields (e.g. { lineStyle: "centerline", stroke: { color: "red" } }). centerMark(center, radius, overshoot?) draws the cross for a hole or round feature's center; short marks render solid rather than dash-dot since the dash period doesn't fit — expected behavior, not a bug.
  • Dimensioning: LinearDimension(p1, p2, { offset, orientation }) draws extension lines, a dimension line broken around centered text, and arrowheads — orientation is "aligned" (parallel to p1→p2, the default), or "horizontal"/"vertical" to force an axis even when the two points aren't exactly aligned (e.g. the X-distance between holes at different Y). offset is signed: positive is 90° counterclockwise from the measurement axis (up for horizontal, left for vertical). RadialDimension/ DiameterDimension(center, radius, { angleDeg }) draw an R/ leader from the circle's surface, and Callout(point, text, { angleDeg }) is the same elbow-leader mechanic for arbitrary notes (e.g. "4X ⌀8.00 THRU"). All three share one elbow-leader renderer (renderElbowLeader) so label text always stays horizontal regardless of leader angle (unidirectional dimensioning). AngularDimension(vertex, p1, p2, { radius }) measures the angle at vertex counterclockwise from the ray toward p1 to the ray toward p2 (any point along each ray, e.g. an edge endpoint — the actual distance from vertex doesn't matter, only the direction) — draws extension lines out to a dimension arc broken around centered text, with arrowheads tangent to the arc. Since it's measured CCW specifically, order p1/p2 accordingly, or you'll get the reflex angle (e.g. 270° instead of 90°). The measured value auto-formats from the geometry on all four dimension types; pass text to override it. chainDimension(points, options) and baselineDimension(points, options) are thin helpers over LinearDimension for stacking a series: chain dimensions each consecutive pair (p0→p1, p1→p2, ...) sharing one offset (adjacent-feature spacing); baseline dimensions every point from a common datum (points[0]) at automatically increasing offsets (options.stackSpacing, default 8mm) so they don't overlap (position from one reference edge). Both return a plain LinearDimension[] you loop over and .add() individually — same pattern as centerMark(), no wrapper class. chainAngularDimension(vertex, rays, options) and baselineAngularDimension(vertex, rays, options) are the same two patterns for AngularDimension: chain measures each consecutive ray pair (rays[0]→rays[1], rays[1]→rays[2], ...) sharing one radius; baseline measures every ray from a common datum ray (rays[0]) at automatically increasing radii (options.stackSpacing, default 10mm) so the arcs nest without overlapping. rays are points along each ray (as with AngularDimension itself, distance from vertex doesn't matter) — order them counterclockwise, same CCW-sweep caveat as a bare AngularDimension.
  • Tolerances: LinearDimension, AngularDimension, RadialDimension, and DiameterDimension all take tolerance — a plain number for a symmetric ±X, or { plus, minus } for asymmetric +X/-Y. On LinearDimension/AngularDimension, toleranceDisplay: "limits" instead shows the computed upper/lower limits stacked (two lines, nominal omitted) — the dimension line/arc gap sizes itself to fit whichever layout you pick. RadialDimension/DiameterDimension only support the inline ±/+/- form (their leader text is single-line; no stacked limits there). All dimension/tolerance/limit values render at a fixed decimal precision (style.precision, default 2 — "80.00" not "80"), matching real drawing convention where every dimension on a sheet shares one precision regardless of whether a given value happens to be a whole number. All four classes also take basic (ASME Y14.5 basic dimension: a theoretically exact value paired with a GD&T feature control frame elsewhere — drawn boxed, sized to fit the rendered text with no separate geometry code needed) and reference (shown parenthesized, e.g. "(40.00)", for convenience/traceability rather than inspection). Both only apply to the plain single-line display, not toleranceDisplay: "limits" (a basic dimension has no tolerance to compute limits from in the first place).
  • ISO 2768 general tolerances: for dimensions that don't carry an individual tolerance, iso2768LinearTolerance(nominalSizeMM, class) and iso2768AngularTolerance(shorterSideMM, class) look up the exact ± value from ISO 2768-1's tables (f/m/c/v classes), and iso2768StraightnessFlatnessTolerance/iso2768PerpendicularityTolerance/ iso2768SymmetryTolerance/iso2768CircularRunoutTolerance do the same for ISO 2768-2's geometric classes (H/K/L) — all measured directly from the primary standard text. formatAngularTolerance(decimalDegrees) renders an angular value in the standard's own degree/minute notation (e.g. "±0°20′"). iso2768Note({ linearClass?, geometricClass?, envelopeRequirement? }) builds the citation string exactly as ISO 2768-1 clause 5 / ISO 2768-2 clause 6 specify for the title block (e.g. "ISO 2768-mK", or "ISO 2768-K" with linearClass: false when the general dimensional tolerances don't apply) — pass it straight into TitleBlockFields.generalTolerance.
  • Hatching: hatch(boundary, options) fills a region with parallel section-lining, returning one DrawingElement per line — same "array, caller adds" pattern as centerMark/chainDimension. boundary is a Path (or an array of them: outer boundary plus hole boundaries, combined under the even-odd fill rule, so holes are automatically left unhatched — no separate "holes" parameter). Internally a scanline fill rotated to the hatch angle (angleDeg, default 45°); arcs are tessellated first via Path.flatten(angleStepDeg?), also usable standalone anywhere you need a Path reduced to straight-edged polygon vertices. hatch() also takes phaseMM (perpendicular offset of the line family, for interleaving multiple passes) and dasharray/linecap, which hatchPattern() builds on: hatchPattern(boundary, pattern, options?) runs hatch() once per HatchLineFamily in a HatchPattern (angle/spacing/phase/dash per family) and concatenates the results. ANSI31ANSI38 (also as ANSI_HATCH_PATTERNS) are the classic US mechanical-drafting material-symbol patterns — iron/steel/bronze/plastic/fire brick/marble/lead/aluminum — converted directly from AutoCAD's acad.pat (angle, spacing, and dash lengths preserved exactly, inches→mm via toMM), not approximated from memory. Two honest simplifications: (1) a dash-dot "dot" is a real 0-length dasharray entry in acad.pat, but a literal 0 renders as nothing (not a dot) in at least librsvg — verified directly — so a small positive epsilon is used instead, relying on linecap: "round" to still draw a dot; (2) ANSI36/ANSI38 drop acad.pat's per-row dash-phase stagger ("brick coursing"), since each hatch line's dasharray independently restarts at its own clipped start point in this engine — angle, spacing, and dash rhythm are otherwise exact, so both render as a regular grid rather than a staggered one. A scale option scales every family together, like AutoCAD's HPSCALE.
  • Renderable: Sheet.add() accepts anything with a toSVG(): string method — DrawingElement, TextElement, and all the dimension classes satisfy it structurally, so new primitives don't require touching Sheet.
  • Layers: new Layer({ name, visible? }) groups content into a named SVG <g>; Layer is itself Renderable, so it nests (layer.add(otherLayer)) and adds to a Sheet like anything else — no dedicated Sheet API needed. visible: false hides the group (display:none) without removing its content from the output, so downstream tools (Illustrator, Inkscape, a custom web viewer) can toggle it back on; it does not mean "don't render this" the way skipping .add() would. There's no per-layer default styling — content keeps whatever lineStyle/stroke it was given when constructed; a layer is purely an organizational/visibility grouping.
  • GD&T: all 14 ASME Y14.5 geometric characteristic symbols (renderCharacteristicSymbol / GDTCharacteristic) — straightness, flatness, circularity, cylindricity, profile of a line/surface, angularity, perpendicularity, parallelism, position, concentricity, symmetry, and circular/total runout. concentricity and symmetry were deprecated in ASME Y14.5-2018 in favor of position tolerancing, but still appear on legacy drawings, so they're included. The tricky ones (profile line vs. surface, cylindricity's oblique tangent lines, symmetry's three stacked lines, single vs. double runout arrow) were verified against the Unicode Miscellaneous Technical block's official character names/cross-references and targeted research before implementing, not drawn from memory alone. FeatureControlFrame(anchor, characteristic, toleranceValue, options) renders the standard compartmented box — symbol, tolerance (optional prefix and / material-condition modifier), and 0–3 datum reference compartments (each with its own optional modifier). DatumFeatureSymbol(touchPoint, letter, { angleDeg }) is the filled-triangle-plus-leader-plus-boxed-letter that tags a surface as a datum — distinct from a frame's datum reference, which cites an already-tagged datum. Neither attaches a leader automatically to a FeatureControlFrame the way RadialDimension does; position it directly, or compose your own leader from existing primitives. Not yet covered: full GD&T is a large standard — this covers the characteristic symbols and frame structure, not composite tolerancing, projected tolerance zones, or the free-state/statistical-tolerance modifiers.
  • Datum targets: DatumTargetSymbol(touchPoint, letter, targetNumber, { angleDeg, areaSize?, side? }) is ASME Y14.5's other datum mechanism — a circle divided in half by a horizontal line (lower half always "A1"-style letter+number; upper half shows a circular contact area's diameter, left blank for a point/line target), on a leader that's solid for a near-side target or dashed (side: "far") for far-side. Auto-grows to fit whichever half's text is longest rather than overflowing a fixed size. Distinct from DatumFeatureSymbol, which tags an entire feature as a datum rather than a specific point/line/area used to establish one. datumTargetPoint(center) draws the "X" contact-point marker (as two separate line elements — Path has no compound-subpath support, so a true X needs two, the same reason centerMark() returns an array); datumTargetLine/datumTargetArea build the phantom-line/cross-hatched line and area markers on top of it.
  • Surface finish: SurfaceFinishSymbol(anchor, size, options) is the ISO 1302 / ASME Y14.36 surface texture checkmark — anchor is its vertex, resting on the surface/leader being called out. materialRemoval: "required" adds the bar (machining mandatory) and "prohibited" adds the circle (machining disallowed, e.g. an as-cast/as-forged surface); omit for the bare "any process" checkmark. roughnessValue (Ra) and note (production method/treatment, e.g. "MILLED", shown above an extension line from the long leg's tip) attach the two most commonly used pieces of data. ISO 1302 states the leg angle ("approximately 60°") but not a leg-length ratio or exact circle/bar sizing, so those were measured directly from the standard's own figures (a high-resolution scan of ISO 1302:1992 Figures 1–3) rather than estimated — both legs came out at ~60–62° (confirming the spec text) with the long leg ~2.15× the short one. Not covered: the full §6.1 data-position grid (waviness height, sampling length, alternate roughness parameters, machining allowance, direction-of-lay symbols), and no automatic leader (same reasoning as FeatureControlFrame).
  • Multi-sheet sets: DrawingSet solves the ordering problem plain Sheet[] can't — a sheet's title block typically needs "2 OF 5", but you don't know the total until every sheet is added. set.add(factory) takes a (ctx) => Sheet rather than a ready Sheet; set.toSVGs() resolves { index, total, sheetLabel } for each and builds them in order, so sheetLabel ("2 OF 5") is available while constructing each sheet's title block, not after.
  • Revision table: RevisionTable(anchor, entries, options) — a ZONE/REV/DESCRIPTION/DATE/APPROVED table (ZONE is opt-in via columns, since it only makes sense paired with a borderStyle: "zoned" sheet) built on the same grid primitives as the title blocks. anchor is the table's top-left corner; pass entries newest-first, since they render top to bottom. Distinct from a title block's single REV field — this is the full history, and the one place the zoned border's grid references actually get used by something.
  • Revision clouds: revisionCloud(boundary, { arcLengthMM?, bulgeRatio?, strokeWidthMM?, color? }) draws a scalloped boundary of conjoined outward arcs around a changed area — boundary is any closed polygon (rectangle() or a plain Point[]), built as a single continuous Path (chained .arc() calls, not multiple moveTo()s, since Path only supports one subpath). Bump size (arcLengthMM, default 8mm chord length) and bulge height (bulgeRatio, default 0.18 of the chord) aren't values fixed by ASME Y14.35 — even AutoCAD's own REVCLOUD command treats arc length as an approximate, randomized target rather than a spec'd constant — so these are reasonable, visually-verified defaults. Pair it with RevisionSymbol(center, letter, { shape?, sizeMM?, ... }): a circled letter by default, since ASME Y14.35's text specifies enclosing the revision letter in a circle, or shape: "triangle" for the widely-used "delta" convention that's common industry practice but not the literally-standardized shape.
  • Bill of materials: BOMTable(anchor, entries, options) — an ITEM/QTY/PART NUMBER/DESCRIPTION table (MATERIAL is opt-in via columns), built on the same grid primitives as RevisionTable, for assembly drawings. Same "caller controls order" contract as RevisionTable: entries render top to bottom in the order given. Many shops place this table directly above the title block with item 1 in the row nearest the title block (ascending upward) rather than at the top — that specific direction isn't a single universally-cited rule, so reverse entries yourself if you want that convention. ItemBalloon(touchPoint, itemNumber, { angleDeg, terminus? }) cross-references a part in the assembly view to its BOMTable row — a circled number on the same elbow-leader geometry as Callout/WeldSymbol, with the circle sitting at the end of the leader's shoulder. terminus is "dot" (default, a small filled circle) for pointing at a general surface/area, or "arrow" for pointing precisely at an edge/profile — the conventional distinction between the two leader styles.
  • Hole callouts: HoleCallout(center, radius, { angleDeg, thru?, depth?, counterbore?, countersink? }) is a leader that stacks the standard multi-line note — ⌀8.00 THRU, or ⌀8.00 X 5.00 DEEP, plus an optional ⌴⌀14.00 X 3.00 DEEP counterbore line and/or ⌵⌀9.00 X 90° countersink line. // are all legitimate Unicode drafting characters (Miscellaneous Technical block) used inline as text, same treatment as the diameter symbol elsewhere in the library. Depth has no equivalent single character, so depthSymbol(center, size) draws the real ASME glyph (a horizontal line with a downward arrow) — composable standalone for non-circular features (see the pocket-depth callout in the example, which uses plain "DEEP" text instead since HoleCallout assumes a round hole). renderElbowLeader (shared by RadialDimension/DiameterDimension/ Callout/HoleCallout) now also accepts a string array for stacked lines, not just a single string.
  • Section views: CuttingPlaneLine(points, { viewDirectionDeg, label? }) draws the ASME Y14.2/Y14.3 cutting-plane symbol — a thick (0.6mm) line with the same long-short-short dash rhythm as the "phantom" line style (but thick, not thin), a perpendicular arrow leg at each end, and an optional bold label. points takes 2+ points, so offset/stepped sections (more than one plane) work the same way as a straight one. Both end arrows point in viewDirectionDeg — the direction of sight through the cut, i.e. toward the material that ends up shown in the section, not toward the viewer standing at the cutting plane (easy to get backwards; verified against multiple independent sources before implementing, same as the projection symbol earlier). Pairs with hatch() for the resulting section view itself and a plain bold TextElement for the "SECTION A-A" label — this class is only the marker on the source view, not an automatic section generator.
  • Detail views: DetailViewCallout(center, radius, { angleDeg, label, text? }) is the ASME Y14.3 detail-view marker — a phantom-line circle around the area of a source view that's shown enlarged elsewhere, with an elbow leader (arrow touching the circle) to a "DETAIL X" label. Same scope as CuttingPlaneLine: only the marker, not the detail view itself or its own title (a plain bold TextElement reading "DETAIL A" plus a "SCALE 2:1" line, same pattern as a section's "SECTION A-A").
  • Break lines: zigzagBreakLine(p1, p2, options) is the ASME Y14.3 conventional "long break" — an otherwise-straight line with a single zigzag jog centered on its midpoint, for shortening a long uniform flat member (a bar, plate, extrusion) without changing scale. Call it once per edge being broken (top and bottom of a rectangular bar); both calls' jogs line up automatically since each centers on its own p1-p2 midpoint. cylindricalBreakLine(p1, p2, options) is the equivalent "S-break" for a round shaft/tube — p1/p2 are the two points spanning the diameter at the break, connected by two mirrored semicircular arcs (radius = 1/4 the p1-p2 distance) forming an exact S-curve, called once per break since it already spans both outer edges. Neither the straight zigzag's exact proportions nor the S-break's curve are numerically dimensioned in the standard (only "reveal the characteristic shape of the cross section" is specified) — both are the widely-used conventional construction, verified by rendering rather than a primary-source measurement. The freehand "short break" (thick wavy line) style isn't covered — it's inherently a hand-sketching convention, a poor fit for a precise programmatic generator.
  • Thread representation: threadSideView(p1, p2, { majorDiameter, minorDiameter, kind, chamfer? }) is the ASME Y14.6 simplified thread symbol, longitudinal view — the major diameter runs the full p1-p2 length; the minor diameter is a second, inset line pair, met by a true 45-degree chamfer relief at whichever end(s) chamfer specifies (chamfer depth = half the major/minor difference, which is exactly what makes the angle 45°). kind: "external" (a shaft) draws both diameters visible (major thick, minor thin) since they're genuinely on the surface you're looking at; "internal" (a tapped hole) draws both hidden, same reasoning as any other unsectioned hole. threadEndView(center, majorDiameter, minorDiameter) is the circular view: a full major-diameter circle plus a broken minor-diameter circle — the gap is what visually distinguishes it from a plain concentric-circle counterbore/countersink symbol; both visible regardless of external/internal, since an end view looks straight into the open end either way. Callers supply the diameters directly (no thread-pitch/designation calculations, same as HoleCallout's plain text callouts).
  • Weld symbols: WeldSymbol(jointPoint, { angleDeg, arrowSide?, otherSide?, allAround?, fieldWeld?, tailNote? }) is an AWS A2.4 welding symbol — built on the same elbow-leader geometry as Callout/HoleCallout (arrow leg, then a horizontal reference line), with weld-type symbols drawn below the line for arrowSide (the surface the arrow touches) and/or above it for otherSide. Covers the four most common weld types — "fillet" (right triangle), "square-groove", "v-groove", and "bevel-groove" — plus the weld-all-around circle and field-weld flag supplementary symbols at the arrow/reference-line junction, and a tailed free-text note. AWS A2.4 isn't freely previewable the way the ISO standards used elsewhere in this library are, so these shapes are the widely-corroborated conventional construction (cross-checked across multiple independent welding-education sources) rather than a primary-source pixel measurement. size renders nearest the arrow end of the reference line and lengthPitch nearest the tail end, which matches most real drawings but isn't strictly AWS's absolute left/right reading-order rule (see the class doc comment). Not covered: U-groove, J-groove, flare-V/flare-bevel, plug/slot, spot, seam, stud, surfacing, edge, and backing welds, contour symbols, and multiple/staggered reference lines.
  • No kernel dependency: unlike CAD libraries built on OpenCascade/WASM, every primitive here is plain TypeScript — no WASM, no async init, no registered kernel adapter.
  • DXF export: exportDXF(elements, options?) writes geometry + layers only — not text, dimensions, GD&T, or title blocks (those are SVG-only annotation classes with no exposed Path). Each DrawingElement becomes a DXF POLYLINE, with Path.arc() segments preserved as true arcs via per-vertex bulge values (pathToPolyline, dxf/polylineConversion.ts) — not tessellated the way hatch()'s scanline fill needs Path.flatten() to be. A lineStyle maps to both a same-named DXF layer and linetype (VISIBLE/HIDDEN/CENTER/PHANTOM), with the HIDDEN/CENTER/ PHANTOM linetypes built from the exact same dash-array values as LINE_STYLES, so the DXF dash rhythm matches the SVG output. Pass { element, layer, linetype?, colorIndex? } instead of a bare DrawingElement to override the inferred layer (e.g. routing hatch lines, which carry no lineStyle, onto their own "HATCH" layer). Output is deliberately old-format DXF (R12/AC1009, classic POLYLINE/VERTEX/ SEQEND rather than the newer LWPOLYLINE) — R12 needs no entity handles or subclass markers, keeping the writer a plain, dependency-free group-code serializer while still supporting per-vertex bulge; verified by round-tripping generated files through ezdxf (Python) during development.
  • PDF export: exportPDF(sheet, options?) renders the whole sheet — border, title block, dimensions, GD&T, hatching, everything, not a geometry-only subset — because unlike DXF it works by parsing the exact SVG markup sheet.toSVG() already produces (a small hand-rolled parser for exactly the four element shapes this library's SVG generator ever emits — svg/g/path/text — not a general SVG parser) and converting that into PDF content-stream operators, arcs included (SVG arcs become cubic Bezier curves via the standard endpoint-to-center recovery + kappa approximation, since PDF has no native arc operator). Layers become real PDF Optional Content Groups, toggleable in a viewer's layers panel (e.g. Acrobat) — a visible: false layer starts in the OCG "OFF" state, the same intent as its SVG display:none rendering but genuinely interactive here. Two deliberate, documented simplifications, both to keep the exporter dependency-free (no bundled font files, no image/zlib libraries): text renders in standard (non-embedded) Helvetica/Helvetica-Bold, so only WinAnsi/Latin-1 characters are guaranteed to render — the handful of Unicode drafting symbols this library uses as literal text (//; everything else, like GD&T characteristic symbols and the circled M/L modifiers, is already vector geometry) are substituted with their historical pre-Unicode equivalents (Ø, CBORE , CSK ) rather than risking missing glyphs; and color parsing covers hex plus a bounded set of CSS named colors, falling back to black for anything else (e.g. rgb()/hsl() functions). Output is plain-ASCII and byte-deterministic (no timestamps, no random /ID), verified against real PDF tooling (pypdf, poppler's pdftoppm, Ghostscript) during development, including confirming a hidden Layer genuinely toggles visible again when its OCG is turned on, not just coincidentally absent.
  • Regression testing: test/regression.test.ts snapshot-tests the raw SVG/DXF/PDF text output of the full worked example (examples/basic-sheet.ts, refactored to export buildBasicSheetExample() so both the CLI demo and the test suite build the same scenario) plus a couple of scenarios it doesn't otherwise exercise (ISO7200TitleBlock, CuttingPlaneLine). Deliberately text snapshots, not pixel diffs: different SVG renderers draw identical markup slightly differently (confirmed earlier — librsvg vs. ImageMagick disagreed on a real drawing), which would make a pixel baseline an unreliable, machine-dependent regression signal; a text diff of the generated markup has no such flakiness and adds no dependencies. This catches unintended changes to already-verified output — it doesn't replace the manual librsvg-render-and-inspect verification (see git history) used to confirm a new feature is correct in the first place.

Scripts

npm run build       # bundle to dist/ (ESM + CJS + .d.ts)
npm test            # run the vitest suite
npm run typecheck   # tsc --noEmit
npm run lint        # eslint
npm run example     # generate examples/basic-sheet.{svg,dxf,pdf} + basic-sheet-detail.{svg,pdf}
npm run example:hatches # generate examples/material-hatches.svg (ANSI31-38 pattern gallery)
npm run docs        # generate a static API reference site (docs/, from TSDoc comments)
npm run docs:watch  # regenerate docs/ on every source change

API documentation for every exported class, function, and type is generated straight from the source's TSDoc comments via TypeDoc, the same way Sphinx builds docs from docstrings in Python. Run npm run docs and open docs/index.html; it isn't checked into the repo or published to npm (see .gitignore), so it's always generated fresh from the current source.

Known gaps

Everything originally on the roadmap (dimensioning + tolerances, hatching, layers, GD&T, configurable title blocks, multi-sheet sets, revision history) has landed. What's still explicitly out of scope, called out where relevant above:

  • Composite tolerancing, projected tolerance zones, and free-state/statistical modifiers in GD&T — the characteristic symbols and frame structure are covered, not the full standard
  • ISO 1302's full surface-finish data-position grid (waviness, sampling length, non-Ra parameters, machining allowance, direction-of-lay symbols) and its arbitrary-rotation display rule — only the roughness value, a free-text note, and an always-upright symbol are covered
  • AWS A2.4 weld symbols beyond the 4 basic types covered (fillet, square/V/bevel groove) — U-groove, J-groove, flare types, plug/slot, spot, seam, stud, surfacing, edge, backing welds, contour symbols, and multiple reference lines
  • The freehand "short break" (thick wavy line) convention — only the long (zigzag) and cylindrical (S-curve) break lines are covered
  • Non-circular datum target areas (an arbitrary outline, not just a circular contact area) — datumTargetArea only covers the circular case
  • Freehand/randomized revision-cloud bump sizing (real hand-drawn or AutoCAD-style clouds vary each arc's chord length within a range) — revisionCloud divides each edge into evenly-sized bumps instead
  • ISO 2768-1 Table 2 (tolerances for broken edges/external radii/chamfer heights) — a differently-grouped, coarser table than the general linear tolerances covered by iso2768LinearTolerance
  • ASME Y14.6 schematic (individual crest-line) thread representation, and computing major/minor diameter from a thread designation (e.g. "1/4-20 UNC") — only the simplified representation is covered, and diameters are supplied directly, not derived from a thread-pitch table
  • DXF export covers geometry + layers only — no text, dimensions, GD&T, or title blocks as DXF entities (SVG remains the only output for those)
  • PDF export renders the whole sheet, but text uses standard (non-embedded) fonts — only WinAnsi/Latin-1 characters are guaranteed to render correctly; the library's own Unicode drafting symbols are substituted for ASCII-safe equivalents (see the Design section), but arbitrary Unicode passed in custom text is not

License

MIT © Invisra — see LICENSE. See CHANGELOG.md for release history.