@invisra/draft
v0.1.1
Published
A TypeScript library for generating engineering-style SVG drawing sheets.
Maintainers
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/draftUsing an AI coding agent?
llms.txtis 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 atnode_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/fromMMfromunits.tsat 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.
TextElementcarries its own counter-flip, so text you place at a Y-up position renders upright. - Geometry:
Pathis a Canvas2D-style builder (moveTo/lineTo/arc/close) overLineSegment/ArcSegmentprimitives. 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_SIZEScovers ANSI A–E, ISO 216 A0–A5, and US architectural ARCH A–E1.Sheettakes apaperSize+orientationand exposesdrawingArea(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-offPaperSize—PaperSizeis a plain data shape, so any object matching it works aspaperSizeregardless of where it came from. - Border:
Sheet'sborderStyledefaults 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/Oskipped to avoid confusion with1/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:
TitleBlockrenders 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).sizefalls back to theSheet'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.ISO7200TitleBlockis 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,GridTitleBlockis 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 anyTitleBlockLike({ heightMM, render(ctx) }), so a custom title block needs no special registration. - Line styles:
DrawingElementtakes alineStyle—"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 explicitstrokeobject 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 —orientationis"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).offsetis signed: positive is 90° counterclockwise from the measurement axis (up for horizontal, left for vertical).RadialDimension/DiameterDimension(center, radius, { angleDeg })draw anR/⌀leader from the circle's surface, andCallout(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 atvertexcounterclockwise from the ray towardp1to the ray towardp2(any point along each ray, e.g. an edge endpoint — the actual distance fromvertexdoesn'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, orderp1/p2accordingly, 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; passtextto override it.chainDimension(points, options)andbaselineDimension(points, options)are thin helpers overLinearDimensionfor 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 plainLinearDimension[]you loop over and.add()individually — same pattern ascenterMark(), no wrapper class.chainAngularDimension(vertex, rays, options)andbaselineAngularDimension(vertex, rays, options)are the same two patterns forAngularDimension: chain measures each consecutive ray pair (rays[0]→rays[1], rays[1]→rays[2], ...) sharing oneradius; 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.raysare points along each ray (as withAngularDimensionitself, distance fromvertexdoesn't matter) — order them counterclockwise, same CCW-sweep caveat as a bareAngularDimension. - Tolerances:
LinearDimension,AngularDimension,RadialDimension, andDiameterDimensionall taketolerance— a plain number for a symmetric±X, or{ plus, minus }for asymmetric+X/-Y. OnLinearDimension/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/DiameterDimensiononly 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 takebasic(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) andreference(shown parenthesized, e.g."(40.00)", for convenience/traceability rather than inspection). Both only apply to the plain single-line display, nottoleranceDisplay: "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)andiso2768AngularTolerance(shorterSideMM, class)look up the exact ± value from ISO 2768-1's tables (f/m/c/vclasses), andiso2768StraightnessFlatnessTolerance/iso2768PerpendicularityTolerance/iso2768SymmetryTolerance/iso2768CircularRunoutTolerancedo 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"withlinearClass: falsewhen the general dimensional tolerances don't apply) — pass it straight intoTitleBlockFields.generalTolerance. - Hatching:
hatch(boundary, options)fills a region with parallel section-lining, returning oneDrawingElementper line — same "array, caller adds" pattern ascenterMark/chainDimension.boundaryis aPath(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 viaPath.flatten(angleStepDeg?), also usable standalone anywhere you need a Path reduced to straight-edged polygon vertices.hatch()also takesphaseMM(perpendicular offset of the line family, for interleaving multiple passes) anddasharray/linecap, whichhatchPattern()builds on:hatchPattern(boundary, pattern, options?)runshatch()once perHatchLineFamilyin aHatchPattern(angle/spacing/phase/dash per family) and concatenates the results.ANSI31–ANSI38(also asANSI_HATCH_PATTERNS) are the classic US mechanical-drafting material-symbol patterns — iron/steel/bronze/plastic/fire brick/marble/lead/aluminum — converted directly from AutoCAD'sacad.pat(angle, spacing, and dash lengths preserved exactly, inches→mm viatoMM), not approximated from memory. Two honest simplifications: (1) a dash-dot "dot" is a real 0-length dasharray entry inacad.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 onlinecap: "round"to still draw a dot; (2)ANSI36/ANSI38dropacad.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. Ascaleoption scales every family together, like AutoCAD's HPSCALE. Renderable:Sheet.add()accepts anything with atoSVG(): stringmethod —DrawingElement,TextElement, and all the dimension classes satisfy it structurally, so new primitives don't require touchingSheet.- Layers:
new Layer({ name, visible? })groups content into a named SVG<g>;Layeris itselfRenderable, so it nests (layer.add(otherLayer)) and adds to aSheetlike anything else — no dedicated Sheet API needed.visible: falsehides 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 whateverlineStyle/strokeit 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.concentricityandsymmetrywere 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 aFeatureControlFramethe wayRadialDimensiondoes; 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 fromDatumFeatureSymbol, 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 —Pathhas no compound-subpath support, so a true X needs two, the same reasoncenterMark()returns an array);datumTargetLine/datumTargetAreabuild 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 —anchoris 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) andnote(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 asFeatureControlFrame). - Multi-sheet sets:
DrawingSetsolves the ordering problem plainSheet[]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) => Sheetrather than a readySheet;set.toSVGs()resolves{ index, total, sheetLabel }for each and builds them in order, sosheetLabel("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 viacolumns, since it only makes sense paired with aborderStyle: "zoned"sheet) built on the same grid primitives as the title blocks.anchoris the table's top-left corner; pass entries newest-first, since they render top to bottom. Distinct from a title block's singleREVfield — 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 —boundaryis any closed polygon (rectangle()or a plainPoint[]), built as a single continuousPath(chained.arc()calls, not multiplemoveTo()s, sincePathonly 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 withRevisionSymbol(center, letter, { shape?, sizeMM?, ... }): a circled letter by default, since ASME Y14.35's text specifies enclosing the revision letter in a circle, orshape: "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 viacolumns), built on the same grid primitives asRevisionTable, for assembly drawings. Same "caller controls order" contract asRevisionTable: 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 reverseentriesyourself if you want that convention.ItemBalloon(touchPoint, itemNumber, { angleDeg, terminus? })cross-references a part in the assembly view to itsBOMTablerow — a circled number on the same elbow-leader geometry asCallout/WeldSymbol, with the circle sitting at the end of the leader's shoulder.terminusis"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 DEEPcounterbore 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, sodepthSymbol(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 sinceHoleCalloutassumes a round hole).renderElbowLeader(shared byRadialDimension/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.pointstakes 2+ points, so offset/stepped sections (more than one plane) work the same way as a straight one. Both end arrows point inviewDirectionDeg— 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 withhatch()for the resulting section view itself and a plain boldTextElementfor 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 asCuttingPlaneLine: only the marker, not the detail view itself or its own title (a plain boldTextElementreading "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 ownp1-p2midpoint.cylindricalBreakLine(p1, p2, options)is the equivalent "S-break" for a round shaft/tube —p1/p2are the two points spanning the diameter at the break, connected by two mirrored semicircular arcs (radius = 1/4 thep1-p2distance) 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 fullp1-p2length; the minor diameter is a second, inset line pair, met by a true 45-degree chamfer relief at whichever end(s)chamferspecifies (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 ofexternal/internal, since an end view looks straight into the open end either way. Callers supply the diameters directly (no thread-pitch/designation calculations, same asHoleCallout'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 asCallout/HoleCallout(arrow leg, then a horizontal reference line), with weld-type symbols drawn below the line forarrowSide(the surface the arrow touches) and/or above it forotherSide. 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.sizerenders nearest the arrow end of the reference line andlengthPitchnearest 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 exposedPath). EachDrawingElementbecomes a DXFPOLYLINE, withPath.arc()segments preserved as true arcs via per-vertex bulge values (pathToPolyline,dxf/polylineConversion.ts) — not tessellated the wayhatch()'s scanline fill needsPath.flatten()to be. AlineStylemaps to both a same-named DXF layer and linetype (VISIBLE/HIDDEN/CENTER/PHANTOM), with theHIDDEN/CENTER/PHANTOMlinetypes built from the exact same dash-array values asLINE_STYLES, so the DXF dash rhythm matches the SVG output. Pass{ element, layer, linetype?, colorIndex? }instead of a bareDrawingElementto override the inferred layer (e.g. routing hatch lines, which carry nolineStyle, onto their own"HATCH"layer). Output is deliberately old-format DXF (R12/AC1009, classicPOLYLINE/VERTEX/SEQENDrather than the newerLWPOLYLINE) — 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 throughezdxf(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 markupsheet.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) — avisible: falselayer starts in the OCG "OFF" state, the same intent as its SVGdisplay:nonerendering 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'spdftoppm, Ghostscript) during development, including confirming a hiddenLayergenuinely toggles visible again when its OCG is turned on, not just coincidentally absent. - Regression testing:
test/regression.test.tssnapshot-tests the raw SVG/DXF/PDF text output of the full worked example (examples/basic-sheet.ts, refactored to exportbuildBasicSheetExample()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 changeAPI 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) —
datumTargetAreaonly 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) —
revisionClouddivides 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.
