@toolpath/tool-drawing 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Stage 2 of the drawing: where the millimetres land on the sheet.
3
+ *
4
+ * Pure arithmetic over a content extent and a measured box. No React, no DOM,
5
+ * no SVG — {@link frameFor} returns numbers and two mapping functions, and the
6
+ * renderer does nothing but place things with them.
7
+ *
8
+ * ## The inversion
9
+ *
10
+ * **The frame is content plus margins, and the scale absorbs the panel's
11
+ * shape.** Never the other way round. The component this replaces derived the
12
+ * *frame* from the panel's aspect ratio while pinning the scale to the tool's
13
+ * length, so a ⌀1 drill 58 mm long got a 312 mm-wide sheet and then had
14
+ * `preserveAspectRatio` shrink it to fit — about 85% of the panel rendered
15
+ * empty. Here `scale` is the smaller of the two px-per-mm ratios that fit the
16
+ * content in the box, and the viewBox describes the content and its margins.
17
+ * A wide panel now buys a bigger drawing rather than a wider sheet.
18
+ *
19
+ * ## The contract with the renderer
20
+ *
21
+ * The `<svg>` must carry `preserveAspectRatio="xMidYMid meet"`, which is the
22
+ * default. That is not decoration: `meet` fits the viewBox by the smaller of
23
+ * its own two ratios, and the algebra below chooses `scale` so that the
24
+ * binding ratio is exactly `scale`. The reported number and the rendered
25
+ * number are then the same, which is what lets {@link Frame.fontSize} be
26
+ * trusted. Under `preserveAspectRatio="none"` the drawing would stretch and
27
+ * `scale` would be a lie on one axis.
28
+ *
29
+ * ## Orientation
30
+ *
31
+ * {@link Frame.toX} and {@link Frame.toY} are the **only** things in this
32
+ * package that know which way the tool axis runs. Everything downstream — the
33
+ * silhouette, the joins, the centreline, the dimensions, the clearance overlay
34
+ * — is written once against them and is orientation-agnostic. Orientation
35
+ * being baked into a pair of closures inside the render pass, and assumed by
36
+ * every renderer below them, is the reason the old component could not be
37
+ * patched into shape.
38
+ */
39
+ /** What framing needs from an outline: how far it reaches, in millimetres. */
40
+ interface Extent {
41
+ /** The tallest point drawn, above the tip. */
42
+ readonly height: number;
43
+ /** The widest radius drawn. The drawing is mirrored, so it spans twice this. */
44
+ readonly radius: number;
45
+ }
46
+ /** A measured panel, in CSS pixels. Zero on the server and before first paint. */
47
+ interface Box {
48
+ readonly width: number;
49
+ readonly height: number;
50
+ }
51
+ type Orientation = 'vertical' | 'horizontal';
52
+ interface Frame {
53
+ readonly orientation: Orientation;
54
+ /** Pixels per millimetre, as the drawing will actually render. */
55
+ readonly scale: number;
56
+ /** `minX minY width height`, in millimetres: the content and its margins. */
57
+ readonly viewBox: string;
58
+ /** A point's horizontal place in viewBox coordinates, from radius and height. */
59
+ readonly toX: (r: number, z: number) => number;
60
+ /** A point's vertical place in viewBox coordinates, from radius and height. */
61
+ readonly toY: (r: number, z: number) => number;
62
+ /** Type size in **millimetres**, back-derived from a target in pixels. */
63
+ readonly fontSize: number;
64
+ /**
65
+ * The chrome actually applied, in pixels — what was asked for, or less.
66
+ *
67
+ * The frame reports what it did with the request, the way it reports the
68
+ * scale it settled on. A caller placing something in the margin has to place
69
+ * it against the room the margin really has: ask for more than
70
+ * {@link MOST_OF_A_PANEL} and the request is scaled back rather than
71
+ * granted, and anything drawn at the full request would then hang off the
72
+ * sheet.
73
+ */
74
+ readonly padding: Padding;
75
+ }
76
+ /**
77
+ * Room for chrome around the content, in **pixels**, flank by flank.
78
+ *
79
+ * **Pixels rather than millimetres because it is chrome**: it should not grow
80
+ * because the tool is long, and it has to stay independent of `scale` or the
81
+ * arithmetic closes a loop — more padding shrinks the scale, which changes the
82
+ * type size, which changes the band widths, which changes the padding.
83
+ *
84
+ * **Named by flank rather than by side of the screen.** `minus` is the `-r`
85
+ * flank and `plus` the `+r` flank; which of them is the screen's left, right,
86
+ * top or bottom is `toX`/`toY`'s business and nothing else's.
87
+ *
88
+ * A plain number is the same room on every side, and that is what a drawing
89
+ * with no dimensions on it wants.
90
+ */
91
+ interface Padding {
92
+ /** Outside the `-r` flank. */
93
+ readonly minus: number;
94
+ /** Outside the `+r` flank. */
95
+ readonly plus: number;
96
+ /** Past each end of the tool, along its axis. */
97
+ readonly along: number;
98
+ }
99
+ interface FrameOptions {
100
+ /**
101
+ * Room for chrome around the content, in **pixels** — dimension bands, the
102
+ * arrowheads, the figures.
103
+ *
104
+ * **Widened from a plain number in phase 4, deliberately.** The dimension
105
+ * bands are genuinely asymmetric: with figures on both flanks each side
106
+ * needs as much room as its own bands take, and those differ. Forcing that
107
+ * through one scalar would mean padding both flanks by the wider of the two
108
+ * and throwing away the difference — real drawing area, on the axis where a
109
+ * long thin tool has least of it. A number still means what it always did.
110
+ */
111
+ readonly padding?: number | Partial<Padding>;
112
+ /**
113
+ * The box to frame against before the panel has been measured. A
114
+ * `ResizeObserver` reports nothing on the server or on first paint, and
115
+ * framing against a zero box would paint at the stack's own width and then
116
+ * visibly jump. This is a plain landscape sheet instead.
117
+ */
118
+ readonly defaultBox?: Box;
119
+ }
120
+ /**
121
+ * The type size a drawing in this box is set in, in **pixels**.
122
+ *
123
+ * Exported because the dimension model has to know it before there is a frame:
124
+ * its band widths are measured in type, the frame's padding is the total of
125
+ * those bands, and a frame cannot be built until the padding is known. It
126
+ * depends only on the panel, so there is no circle to close.
127
+ */
128
+ declare const typeSizeFor: (box: Box, options?: FrameOptions) => number;
129
+ /**
130
+ * Which way the tool axis runs in this box: along its long side.
131
+ *
132
+ * Exported for the same reason as {@link typeSizeFor} — the dimension model
133
+ * has to size its bands before there is a frame, and one of its measures
134
+ * depends on which way the type runs relative to the axis. It reads the panel
135
+ * only, so there is no circle to close.
136
+ */
137
+ declare const orientationFor: (box: Box, options?: FrameOptions) => Orientation;
138
+ declare const frameFor: (outline: Extent, box: Box, options?: FrameOptions) => Frame;
139
+
140
+ /**
141
+ * The sheet the tool is drawn on, and the ink it is drawn in.
142
+ *
143
+ * **Hard colours rather than the application's ramp** (Paul, 2026-09-01): a
144
+ * drawing is a drawing, and a "light grey" written as a zinc utility comes out
145
+ * dark under the flipped ramp. So the sheet, the linework and the two shades a
146
+ * tool has — gold flutes, steel body — are stated here.
147
+ *
148
+ * **One set per theme** (Paul, 2026-09-01: "2d tool visualization can't have
149
+ * the white background in dark mode — make it just barely lighter than any of
150
+ * the other backgrounds"). A white sheet in a dark application is a torch. In
151
+ * dark it is a shade above the card it sits on, and the ink turns over with
152
+ * it: light lines on a dark ground rather than dark lines nobody can see.
153
+ *
154
+ * Stated as literals in this package for a second reason as well: it has no
155
+ * Tailwind, no design-token import and no access to the consumer's ramp, so
156
+ * every colour the drawing uses has to be a colour rather than a class name.
157
+ */
158
+ declare const SHEETS: {
159
+ readonly light: {
160
+ readonly ground: "#ffffff";
161
+ readonly ink: "#3f4650";
162
+ readonly centre: "#15181c";
163
+ readonly dimension: "#606a76";
164
+ readonly body: "#c4c8ce";
165
+ readonly flutes: "#e6bf59";
166
+ readonly holder: "#9aa2ad";
167
+ readonly connection: "#78818d";
168
+ };
169
+ readonly dark: {
170
+ readonly ground: "#22252b";
171
+ readonly ink: "#c7cdd6";
172
+ readonly centre: "#e8ebef";
173
+ readonly dimension: "#8d97a4";
174
+ readonly body: "#5b626c";
175
+ readonly flutes: "#c9a44b";
176
+ readonly holder: "#474d57";
177
+ readonly connection: "#3a4048";
178
+ };
179
+ };
180
+ type Theme = keyof typeof SHEETS;
181
+ type Sheet = (typeof SHEETS)[Theme];
182
+
183
+ export { type Box as B, type Extent as E, type Frame as F, type Orientation as O, type Padding as P, type Sheet as S, type Theme as T, type FrameOptions as a, SHEETS as b, frameFor as f, orientationFor as o, typeSizeFor as t };
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@toolpath/tool-drawing",
3
+ "version": "0.1.0",
4
+ "description": "2D elevation drawing of a cutting tool and its holder",
5
+ "license": "MIT",
6
+ "engines": {
7
+ "node": ">=20"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/toolpath/ui-packages.git",
12
+ "directory": "packages/tool-drawing"
13
+ },
14
+ "homepage": "https://developers.toolpath.com",
15
+ "bugs": {
16
+ "url": "https://github.com/toolpath/ui-packages/issues"
17
+ },
18
+ "publishConfig": {
19
+ "access": "public",
20
+ "registry": "https://registry.npmjs.org"
21
+ },
22
+ "type": "module",
23
+ "main": "./dist/index.js",
24
+ "types": "./dist/index.d.ts",
25
+ "exports": {
26
+ ".": {
27
+ "types": "./dist/index.d.ts",
28
+ "import": "./dist/index.js"
29
+ },
30
+ "./geometry": {
31
+ "types": "./dist/geometry/index.d.ts",
32
+ "import": "./dist/geometry/index.js"
33
+ },
34
+ "./clearance": {
35
+ "types": "./dist/clearance/index.d.ts",
36
+ "import": "./dist/clearance/index.js"
37
+ },
38
+ "./package.json": "./package.json"
39
+ },
40
+ "files": [
41
+ "dist",
42
+ "LICENSE",
43
+ "README.md"
44
+ ],
45
+ "sideEffects": false,
46
+ "scripts": {
47
+ "build": "tsup src/index.ts src/geometry/index.ts src/clearance/index.ts --format esm --dts --clean --external react --external react-dom",
48
+ "check-types": "tsc --noEmit",
49
+ "test": "vitest run"
50
+ },
51
+ "peerDependencies": {
52
+ "react": "^19.0.0",
53
+ "react-dom": "^19.0.0"
54
+ },
55
+ "devDependencies": {
56
+ "@testing-library/react": "^16.3.2",
57
+ "@types/node": "24.10.1",
58
+ "@types/react": "^19.0.0",
59
+ "@types/react-dom": "^19.0.0",
60
+ "jsdom": "^30.0.1",
61
+ "react": "19.2.0",
62
+ "react-dom": "19.2.0",
63
+ "tsup": "8.5.1",
64
+ "typescript": "5.9.3",
65
+ "vitest": "4.1.10"
66
+ }
67
+ }