@toolpath/tool-support 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,209 @@
1
+ /**
2
+ * Whether a tool and what holds it clear the material around a feature.
3
+ *
4
+ * The Engine does the three-dimensional work and hands over a **reach curve**:
5
+ * for each distance out from the wall of the cut, how tall the material within
6
+ * that distance stands above the feature's bottom, worst case over the whole
7
+ * feature. An assembly is a solid of revolution, so it is a profile too —
8
+ * radius by height above the tip — and the check is one comparison per step of
9
+ * that profile: anything standing `d` past the cutting edge must sit at least
10
+ * `heightAt(d)` above the bottom. No sweep, no CAD, a loop over a few numbers.
11
+ *
12
+ * **The profile is what the catalog states, and says so.** A tool is flutes, an
13
+ * optional neck, and a shank. A holder is its nose, then the body behind it
14
+ * where the vendor states one, then the flange at its projection. The seated
15
+ * collet's protrusion below the nose is swept too. A pass here is a pass for
16
+ * exactly the silhouette {@link Clearance.checked} lists.
17
+ *
18
+ * **The curve is conservative.** It is the worst case over the whole feature,
19
+ * so an assembly that fails might clear most of the toolpath — a long slot with
20
+ * one tall wall at one end fails for its whole length. Pass means safe; fail
21
+ * means "somewhere along it".
22
+ *
23
+ * ## Why it is here and not in the drawing package
24
+ *
25
+ * This decision has a dozen callers that never draw anything, and putting it
26
+ * behind a rendering package is the thing the whole split exists to avoid. The
27
+ * *lines* an overlay draws from a verdict are `@toolpath/tool-drawing`'s; the
28
+ * verdict is this.
29
+ */
30
+ import { NO_MARGINS } from './parts.js';
31
+ import { heightAt } from './reach.js';
32
+ import { hasNeck } from './tool.js';
33
+ /**
34
+ * The tool's own profile above the flutes, from what the vendor states.
35
+ *
36
+ * - A neck, where a shoulder diameter *and* length are stated: that radius
37
+ * from the end of the flutes to the shoulder.
38
+ * - The shank, from the shoulder (or the end of the flutes, where there is no
39
+ * neck) upward.
40
+ *
41
+ * A neck whose diameter is unstated is taken to be no wider than the cut,
42
+ * which is what a neck is for, and so has nothing to check.
43
+ */
44
+ export const toolSilhouette = (tool) => {
45
+ const { DC, LCF, SFDM } = tool.geometry;
46
+ const neckDiameter = tool.geometry['shoulder-diameter'];
47
+ const shoulder = tool.geometry['shoulder-length'];
48
+ if (DC === undefined || LCF === undefined) {
49
+ return [];
50
+ }
51
+ const steps = [];
52
+ if (neckDiameter !== undefined && shoulder !== undefined && shoulder > LCF) {
53
+ // A relief narrower than the shank is a neck; one as wide as the shank is
54
+ // plain shank, whatever the vendor's column calls it.
55
+ steps.push({
56
+ part: hasNeck(tool) ? 'neck' : 'shank',
57
+ radius: neckDiameter / 2,
58
+ fromHeight: LCF,
59
+ });
60
+ }
61
+ if (SFDM !== undefined) {
62
+ steps.push({ part: 'shank', radius: SFDM / 2, fromHeight: shoulder ?? LCF });
63
+ }
64
+ return steps;
65
+ };
66
+ /**
67
+ * The tool's own body against the part, whatever holds it.
68
+ *
69
+ * A shank or neck that stands past the cutting edge meets the wall above the
70
+ * flutes at every stickout — no holder and no pull-out changes where the
71
+ * tool's own steps sit above its tip. Paul's call (2026-08-30): such a tool
72
+ * is not compatible with the feature and is not shown; the answer is longer
73
+ * flutes or a reduced shank. Swept with the same margins as the holder.
74
+ */
75
+ export const toolCollisions = (tool, curve, margins = NO_MARGINS) => {
76
+ const DC = tool.geometry.DC;
77
+ if (DC === undefined) {
78
+ return [];
79
+ }
80
+ return collisionsIn(toolSilhouette(tool), curve, DC / 2, margins);
81
+ };
82
+ /** Millimetres a part may be short of clearing and still clear: float noise, not geometry. */
83
+ const CLEARANCE_TOLERANCE = 1e-6;
84
+ const collisionsIn = (steps, curve, cuttingRadius, margins) => steps.flatMap((step) => {
85
+ // At the cut's own radius the step is the wall, which the flutes cut; only
86
+ // what stands past the edge — by more than the room wanted — can meet material.
87
+ const offset = step.radius + margins.radial - cuttingRadius;
88
+ if (offset <= 0) {
89
+ return [];
90
+ }
91
+ const needs = heightAt(curve, offset) + margins.axial;
92
+ // A hair of tolerance: a stack stood out to exactly what it needs lands a
93
+ // femtometre short after the arithmetic, and reported a collet colliding
94
+ // at the stickout this same sweep had just asked for (2026-08-30).
95
+ return step.fromHeight + CLEARANCE_TOLERANCE < needs
96
+ ? [{ part: step.part, height: step.fromHeight, needs, offset }]
97
+ : [];
98
+ });
99
+ /** The collet's own diameter is its series size: a PG 6 collet is 6 mm across. */
100
+ const colletDiameter = (series) => {
101
+ const digits = /(\d+(?:\.\d+)?)/.exec(series ?? '');
102
+ return digits ? Number(digits[1]) : null;
103
+ };
104
+ /**
105
+ * The holder's profile above the tool, from the stickout up.
106
+ *
107
+ * - The seated collet, standing proud of the nose face by its protrusion, at
108
+ * the collet's own diameter.
109
+ * - The nose, for its stated length — or, with no length stated, for the
110
+ * gauge length as before, so an older dataset sweeps what it always did.
111
+ * - The body behind the nose, where stated.
112
+ * - The flange, at the projection. Nothing is swept between it and the body:
113
+ * a `Silhouette` is a radius **from a height upward**, so the last stated
114
+ * diameter carries itself up to the flange, which is the layer model of
115
+ * Justin Mimbs' reach-curve note.
116
+ */
117
+ export const holderSilhouette = (assembly, stickout) => {
118
+ const { holder } = assembly;
119
+ const steps = [];
120
+ if (holder.noseDiameter === null) {
121
+ return steps;
122
+ }
123
+ const collet = colletDiameter(holder.colletSeries);
124
+ if (holder.colletProtrusion !== null && collet !== null) {
125
+ steps.push({
126
+ part: 'collet',
127
+ radius: collet / 2,
128
+ fromHeight: stickout - holder.colletProtrusion,
129
+ });
130
+ }
131
+ steps.push({ part: 'nose', radius: holder.noseDiameter / 2, fromHeight: stickout });
132
+ // Where the nose ends. With no stated nose length the gauge length stands in,
133
+ // so a dataset that predates the column sweeps what it always did.
134
+ const behindTheNose = stickout + (holder.noseLength ?? holder.gaugeLength ?? 0);
135
+ if (holder.bodyDiameter !== null && holder.bodyLength !== null) {
136
+ steps.push({ part: 'body', radius: holder.bodyDiameter / 2, fromHeight: behindTheNose });
137
+ }
138
+ if (holder.projection !== null && holder.flangeDiameter !== null) {
139
+ /**
140
+ * **Cylinders, not cones** (Paul, 2026-08-31).
141
+ *
142
+ * The shape between the last stated diameter and the flange used to be
143
+ * swept as a cone in six steps — a shape no vendor publishes, and on a
144
+ * PG 10 × 062 a 34 mm flare from ⌀18 to ⌀46 that turned tools down for
145
+ * metal that is not there. Justin Mimbs' reach-curve note models a holder
146
+ * as layers: each swept at its widest, no credit for a taper, and the
147
+ * last diameter carried upward — which is what a `Silhouette` already
148
+ * means, so the carry needs no step of its own. The flange is its own
149
+ * layer, at its own height.
150
+ *
151
+ * It is the less conservative reading of an unstated shape, and
152
+ * deliberately so: what the vendor states is what is swept.
153
+ */
154
+ steps.push({
155
+ part: 'flange',
156
+ radius: holder.flangeDiameter / 2,
157
+ fromHeight: stickout + holder.projection,
158
+ });
159
+ }
160
+ return steps;
161
+ };
162
+ /**
163
+ * Whether one assembly clears one feature's reach curve.
164
+ *
165
+ * The holder is checked at the assembly's stickout, and `requiredStickout`
166
+ * says the least stickout at which every part of the holder would clear — so
167
+ * a stack that fails can be read as "stick it out further" rather than "no".
168
+ */
169
+ export const clearance = (assembly, curve, margins = NO_MARGINS) => {
170
+ const { tool, holder, stickout } = assembly;
171
+ const DC = tool.geometry.DC;
172
+ if (DC === undefined) {
173
+ return { clears: true, collisions: [], requiredStickout: null, checked: [] };
174
+ }
175
+ const cuttingRadius = DC / 2;
176
+ const steps = toolSilhouette(tool);
177
+ let requiredStickout = null;
178
+ if (holder.noseDiameter !== null) {
179
+ // What the holder needs is the same whatever the stickout: each of its
180
+ // parts sits a fixed height above the nose face, and has to sit above the
181
+ // material at its own offset. The least stickout is the largest shortfall.
182
+ const atZero = holderSilhouette(assembly, 0);
183
+ requiredStickout = atZero.reduce((most, step) => {
184
+ const offset = step.radius + margins.radial - cuttingRadius;
185
+ if (offset <= 0) {
186
+ return most;
187
+ }
188
+ return Math.max(most, heightAt(curve, offset) + margins.axial - step.fromHeight);
189
+ }, 0);
190
+ if (stickout !== null) {
191
+ steps.push(...holderSilhouette(assembly, stickout));
192
+ }
193
+ }
194
+ const collisions = collisionsIn(steps, curve, cuttingRadius, margins);
195
+ const checked = [...new Set(steps.map((step) => step.part))];
196
+ return { clears: collisions.length === 0, collisions, requiredStickout, checked };
197
+ };
198
+ /** Why a collision rules an assembly out, in the words a machinist would use. */
199
+ export const describeCollision = (collision) => {
200
+ const what = {
201
+ neck: 'the neck',
202
+ shank: 'the shank',
203
+ collet: 'the collet',
204
+ nose: 'the holder nose',
205
+ body: 'the holder body',
206
+ flange: 'the flange',
207
+ }[collision.part];
208
+ return `${what} at ${collision.height.toFixed(1)} mm collides with material ${collision.needs.toFixed(1)} mm tall, ${collision.offset.toFixed(1)} mm out from the cut`;
209
+ };
@@ -0,0 +1,51 @@
1
+ /**
2
+ * What a feature demands of a tool, in millimetres.
3
+ *
4
+ * ## The seam that keeps the part schema out of this package
5
+ *
6
+ * Reading these numbers off a Toolpath datasheet needs the Engine's part
7
+ * schema; *checking a tool against them* needs only tool vocabulary. Those two
8
+ * halves used to sit in one file, and splitting them at exactly this type is
9
+ * what lets the checking travel: every field below is tool language, and no
10
+ * part feature, machining direction or datasheet appears in it.
11
+ *
12
+ * So the adapter — feature to demand — stays with whoever holds the report, and
13
+ * fitting comes here. A consumer with no Toolpath report at all can still state
14
+ * a demand by hand and ask whether a tool meets it.
15
+ *
16
+ * ## Every field is optional, and that is the contract
17
+ *
18
+ * The kernel states different measurements for different feature kinds, and **a
19
+ * demand nobody stated must not silently become a demand of zero**. What is not
20
+ * stated is not checked, and not claimed: that is the difference between a shop
21
+ * trusting a tool list and a shop checking every row of it by hand.
22
+ */
23
+ import type { ReachCurve } from './reach.js';
24
+ export interface FeatureDemand {
25
+ /** The feature this came from, so a result can say which selection excluded a tool. */
26
+ readonly featureTag: string;
27
+ /** The widest cutter that still reaches the tightest corner. */
28
+ readonly maxToolDiameter?: number;
29
+ /** Stated separately for a hole: the widest drill, and the widest endmill. */
30
+ readonly maxDrillDiameter?: number;
31
+ readonly maxEndmillDiameter?: number;
32
+ /** A hole's bore. Nothing wider than this goes in it. */
33
+ readonly holeDiameter?: number;
34
+ /** How deep the cut reaches, which the flutes have to cover. */
35
+ readonly depth?: number;
36
+ /**
37
+ * How far below the top of the part the feature bottoms out, in millimetres.
38
+ *
39
+ * Depth is the feature; this is the *reach* — what the whole stack has to
40
+ * clear before it cuts anything.
41
+ */
42
+ readonly reachBelowTop?: number;
43
+ /** The floor fillet: a corner radius larger than this cannot finish the floor. */
44
+ readonly floorRadius?: number;
45
+ /**
46
+ * How tall the material stands, by distance out from the cut — what a holder
47
+ * and a shank are swept against. A demand without it is simply not checked
48
+ * for collisions.
49
+ */
50
+ readonly reachCurve?: ReachCurve;
51
+ }
package/dist/demand.js ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ * What a feature demands of a tool, in millimetres.
3
+ *
4
+ * ## The seam that keeps the part schema out of this package
5
+ *
6
+ * Reading these numbers off a Toolpath datasheet needs the Engine's part
7
+ * schema; *checking a tool against them* needs only tool vocabulary. Those two
8
+ * halves used to sit in one file, and splitting them at exactly this type is
9
+ * what lets the checking travel: every field below is tool language, and no
10
+ * part feature, machining direction or datasheet appears in it.
11
+ *
12
+ * So the adapter — feature to demand — stays with whoever holds the report, and
13
+ * fitting comes here. A consumer with no Toolpath report at all can still state
14
+ * a demand by hand and ask whether a tool meets it.
15
+ *
16
+ * ## Every field is optional, and that is the contract
17
+ *
18
+ * The kernel states different measurements for different feature kinds, and **a
19
+ * demand nobody stated must not silently become a demand of zero**. What is not
20
+ * stated is not checked, and not claimed: that is the difference between a shop
21
+ * trusting a tool list and a shop checking every row of it by hand.
22
+ */
23
+ export {};
package/dist/fit.d.ts ADDED
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Whether a tool can cut a feature.
3
+ *
4
+ * ## The seam that lets this travel at all
5
+ *
6
+ * Reading a feature's demands off a Toolpath datasheet needs the Engine's part
7
+ * schema. *Checking a tool against those demands* needs only tool vocabulary.
8
+ * Those two halves sat in one file, and splitting them at {@link FeatureDemand}
9
+ * is what lets the checking live here: every field of a demand is tool
10
+ * language, and no part feature, machining direction or datasheet appears in
11
+ * it.
12
+ *
13
+ * So the adapter — feature to demand — stays with whoever holds the report, and
14
+ * this package inherits no OpenAPI contract for it. A consumer with no Toolpath
15
+ * report at all can state a demand by hand and ask the same question.
16
+ *
17
+ * ## What is not stated is not claimed
18
+ *
19
+ * **A demand the datasheet does not state is not checked.** The alternative —
20
+ * treating an absent measurement as zero, or as no limit — is the difference
21
+ * between a shop trusting a tool list and a shop checking every row of it by
22
+ * hand.
23
+ *
24
+ * This is the opposite of the rule `holderTakesTool` follows, and deliberately:
25
+ * there, an unchecked case is a cutter falling out of a spindle, so silence
26
+ * refuses. Here an unchecked case is a tool offered that a machinist will look
27
+ * at anyway, so silence passes.
28
+ */
29
+ import type { FeatureDemand } from './demand.js';
30
+ import type { ToolForm } from './forms.js';
31
+ import type { Tool } from './tool.js';
32
+ /**
33
+ * The forms that go into a hole bore-first, and so are bounded by the bore
34
+ * rather than by what can helix down it.
35
+ *
36
+ * Stated over {@link ToolForm} rather than over a coarser tool type, which is a
37
+ * refinement: the coarse vocabulary this replaced had one word, `drill`, for
38
+ * what a CAM library calls a drill, a centre drill and a spot drill, so a
39
+ * stated spot drill could not be recognised as going in bore-first. All three
40
+ * do.
41
+ */
42
+ export declare const DRILLING_FORMS: ReadonlySet<ToolForm>;
43
+ /** Why a tool cannot cut a feature, in the words a machinist would use. */
44
+ export interface FitFailure {
45
+ readonly featureTag: string;
46
+ readonly reason: string;
47
+ }
48
+ export interface ToolFit<T> {
49
+ readonly tool: T;
50
+ readonly fits: boolean;
51
+ /** Empty when the tool fits. One entry per feature that ruled it out. */
52
+ readonly failures: readonly FitFailure[];
53
+ }
54
+ /** Whether one tool can cut one feature, and why not where it cannot. */
55
+ export declare const fitAgainst: (tool: Pick<Tool, "geometry" | "form">, demand: FeatureDemand) => FitFailure[];
56
+ /**
57
+ * Which tools cut **every** demand given.
58
+ *
59
+ * The intersection is the point of the exercise: one setup wants one tool for
60
+ * as much of the part as possible, and a tool that clears four of five features
61
+ * is not an answer — but knowing which feature ruled it out is, which is why a
62
+ * near miss keeps its failures instead of vanishing.
63
+ *
64
+ * With no demands every tool fits, because nothing has been asked of them yet.
65
+ *
66
+ * Generic in the tool, so a caller gets its own records back rather than a
67
+ * projection of them.
68
+ */
69
+ export declare const fitTools: <T extends Pick<Tool, "geometry" | "form">>(tools: readonly T[], demands: readonly FeatureDemand[]) => ToolFit<T>[];
package/dist/fit.js ADDED
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Whether a tool can cut a feature.
3
+ *
4
+ * ## The seam that lets this travel at all
5
+ *
6
+ * Reading a feature's demands off a Toolpath datasheet needs the Engine's part
7
+ * schema. *Checking a tool against those demands* needs only tool vocabulary.
8
+ * Those two halves sat in one file, and splitting them at {@link FeatureDemand}
9
+ * is what lets the checking live here: every field of a demand is tool
10
+ * language, and no part feature, machining direction or datasheet appears in
11
+ * it.
12
+ *
13
+ * So the adapter — feature to demand — stays with whoever holds the report, and
14
+ * this package inherits no OpenAPI contract for it. A consumer with no Toolpath
15
+ * report at all can state a demand by hand and ask the same question.
16
+ *
17
+ * ## What is not stated is not claimed
18
+ *
19
+ * **A demand the datasheet does not state is not checked.** The alternative —
20
+ * treating an absent measurement as zero, or as no limit — is the difference
21
+ * between a shop trusting a tool list and a shop checking every row of it by
22
+ * hand.
23
+ *
24
+ * This is the opposite of the rule `holderTakesTool` follows, and deliberately:
25
+ * there, an unchecked case is a cutter falling out of a spindle, so silence
26
+ * refuses. Here an unchecked case is a tool offered that a machinist will look
27
+ * at anyway, so silence passes.
28
+ */
29
+ /**
30
+ * The forms that go into a hole bore-first, and so are bounded by the bore
31
+ * rather than by what can helix down it.
32
+ *
33
+ * Stated over {@link ToolForm} rather than over a coarser tool type, which is a
34
+ * refinement: the coarse vocabulary this replaced had one word, `drill`, for
35
+ * what a CAM library calls a drill, a centre drill and a spot drill, so a
36
+ * stated spot drill could not be recognised as going in bore-first. All three
37
+ * do.
38
+ */
39
+ export const DRILLING_FORMS = new Set([
40
+ 'drill',
41
+ 'center drill',
42
+ 'spot drill',
43
+ 'reamer',
44
+ ]);
45
+ /** Whether one tool can cut one feature, and why not where it cannot. */
46
+ export const fitAgainst = (tool, demand) => {
47
+ const failures = [];
48
+ const say = (reason) => failures.push({ featureTag: demand.featureTag, reason });
49
+ const diameter = tool.geometry.DC;
50
+ const fluteLength = tool.geometry.LCF;
51
+ const cornerRadius = tool.geometry.RE;
52
+ const drilling = DRILLING_FORMS.has(tool.form);
53
+ // A hole states its own limits, and which one applies depends on how the tool
54
+ // goes in: a drill is bounded by the bore, an endmill by what can helix in it.
55
+ const widest = drilling
56
+ ? (demand.maxDrillDiameter ?? demand.holeDiameter ?? demand.maxToolDiameter)
57
+ : (demand.maxEndmillDiameter ?? demand.maxToolDiameter);
58
+ if (diameter !== undefined && widest !== undefined && diameter > widest) {
59
+ say(`⌀${diameter} mm is wider than the ${widest} mm this feature admits`);
60
+ }
61
+ if (fluteLength !== undefined && demand.depth !== undefined && fluteLength < demand.depth) {
62
+ say(`${fluteLength} mm of flute does not reach ${demand.depth} mm deep`);
63
+ }
64
+ // A corner radius larger than the floor fillet leaves material the floor does
65
+ // not have room for. A sharp tool in a filleted corner is fine — it just
66
+ // leaves the fillet to something else.
67
+ if (cornerRadius !== undefined &&
68
+ demand.floorRadius !== undefined &&
69
+ cornerRadius > demand.floorRadius) {
70
+ say(`a ${cornerRadius} mm corner does not fit a ${demand.floorRadius} mm floor fillet`);
71
+ }
72
+ return failures;
73
+ };
74
+ /**
75
+ * Which tools cut **every** demand given.
76
+ *
77
+ * The intersection is the point of the exercise: one setup wants one tool for
78
+ * as much of the part as possible, and a tool that clears four of five features
79
+ * is not an answer — but knowing which feature ruled it out is, which is why a
80
+ * near miss keeps its failures instead of vanishing.
81
+ *
82
+ * With no demands every tool fits, because nothing has been asked of them yet.
83
+ *
84
+ * Generic in the tool, so a caller gets its own records back rather than a
85
+ * projection of them.
86
+ */
87
+ export const fitTools = (tools, demands) => tools.map((tool) => {
88
+ const failures = demands.flatMap((demand) => fitAgainst(tool, demand));
89
+ return { tool, fits: failures.length === 0, failures };
90
+ });
@@ -0,0 +1,130 @@
1
+ /**
2
+ * What a tool *is*, in the words a CAM library uses.
3
+ *
4
+ * A scrape hands over a coarse kind — `endmill`, `drill`, `tap` — and that is
5
+ * the right seam for it: it is what a vendor's family table says. But a shop
6
+ * choosing a tool for a filleted pocket is not choosing "an endmill", it is
7
+ * choosing a bull nose, and the difference is one number the vendor did state:
8
+ * the corner radius. So the finer name is derived where a dataset is built, and
9
+ * carried on every tool as its `form`.
10
+ *
11
+ * The vocabulary is Fusion's own library, so a tool exported there lands on the
12
+ * type it already has. This list is the single source: icons draw from it,
13
+ * filter panels offer it, and a drawing decides from it whether it has an
14
+ * honest picture to draw.
15
+ */
16
+ /** One form, and the group a control offers it under. */
17
+ export interface ToolFormEntry {
18
+ readonly value: string;
19
+ readonly label: string;
20
+ readonly group: 'Milling' | 'Hole making';
21
+ }
22
+ export declare const TOOL_FORMS: readonly [{
23
+ readonly value: "ball end mill";
24
+ readonly label: "Ball end mill";
25
+ readonly group: "Milling";
26
+ }, {
27
+ readonly value: "bull nose end mill";
28
+ readonly label: "Bull nose end mill";
29
+ readonly group: "Milling";
30
+ }, {
31
+ readonly value: "flat end mill";
32
+ readonly label: "Flat end mill";
33
+ readonly group: "Milling";
34
+ }, {
35
+ readonly value: "face mill";
36
+ readonly label: "Face mill";
37
+ readonly group: "Milling";
38
+ }, {
39
+ readonly value: "tapered mill";
40
+ readonly label: "Tapered mill";
41
+ readonly group: "Milling";
42
+ }, {
43
+ readonly value: "radius mill";
44
+ readonly label: "Radius mill";
45
+ readonly group: "Milling";
46
+ }, {
47
+ readonly value: "chamfer mill";
48
+ readonly label: "Engrave/chamfer mill";
49
+ readonly group: "Milling";
50
+ }, {
51
+ readonly value: "dovetail mill";
52
+ readonly label: "Dovetail mill";
53
+ readonly group: "Milling";
54
+ }, {
55
+ readonly value: "lollipop mill";
56
+ readonly label: "Lollipop mill";
57
+ readonly group: "Milling";
58
+ }, {
59
+ readonly value: "slot mill";
60
+ readonly label: "Slot mill";
61
+ readonly group: "Milling";
62
+ }, {
63
+ readonly value: "thread mill";
64
+ readonly label: "Thread mill";
65
+ readonly group: "Milling";
66
+ }, {
67
+ readonly value: "circle segment barrel";
68
+ readonly label: "Circle segment barrel";
69
+ readonly group: "Milling";
70
+ }, {
71
+ readonly value: "circle segment lens";
72
+ readonly label: "Circle segment lens";
73
+ readonly group: "Milling";
74
+ }, {
75
+ readonly value: "circle segment oval";
76
+ readonly label: "Circle segment oval";
77
+ readonly group: "Milling";
78
+ }, {
79
+ readonly value: "circle segment taper";
80
+ readonly label: "Circle segment taper";
81
+ readonly group: "Milling";
82
+ }, {
83
+ readonly value: "boring bar";
84
+ readonly label: "Boring bar";
85
+ readonly group: "Hole making";
86
+ }, {
87
+ readonly value: "counter bore";
88
+ readonly label: "Counter bore";
89
+ readonly group: "Hole making";
90
+ }, {
91
+ readonly value: "drill";
92
+ readonly label: "Drill";
93
+ readonly group: "Hole making";
94
+ }, {
95
+ readonly value: "center drill";
96
+ readonly label: "Center drill";
97
+ readonly group: "Hole making";
98
+ }, {
99
+ readonly value: "spot drill";
100
+ readonly label: "Spot drill";
101
+ readonly group: "Hole making";
102
+ }, {
103
+ readonly value: "reamer";
104
+ readonly label: "Reamer";
105
+ readonly group: "Hole making";
106
+ }, {
107
+ readonly value: "counter sink";
108
+ readonly label: "Counter sink";
109
+ readonly group: "Hole making";
110
+ }, {
111
+ readonly value: "tap left hand";
112
+ readonly label: "Tap left hand";
113
+ readonly group: "Hole making";
114
+ }, {
115
+ readonly value: "tap right hand";
116
+ readonly label: "Tap right hand";
117
+ readonly group: "Hole making";
118
+ }];
119
+ /**
120
+ * A form, or `other`.
121
+ *
122
+ * `other` is in the union rather than beside it because it is a real answer: a
123
+ * vendor publishes tools this vocabulary has no word for, and a dataset that
124
+ * had to pick the nearest wrong form would be worse than one that says it does
125
+ * not know. What draws or suggests from a form has to handle it.
126
+ */
127
+ export type ToolForm = (typeof TOOL_FORMS)[number]['value'] | 'other';
128
+ /** The forms that mill — the ones a flute-count suggestion makes sense for. */
129
+ export declare const MILLING_FORMS: ReadonlySet<ToolForm>;
130
+ export declare const isToolForm: (value: string) => value is ToolForm;
package/dist/forms.js ADDED
@@ -0,0 +1,44 @@
1
+ /**
2
+ * What a tool *is*, in the words a CAM library uses.
3
+ *
4
+ * A scrape hands over a coarse kind — `endmill`, `drill`, `tap` — and that is
5
+ * the right seam for it: it is what a vendor's family table says. But a shop
6
+ * choosing a tool for a filleted pocket is not choosing "an endmill", it is
7
+ * choosing a bull nose, and the difference is one number the vendor did state:
8
+ * the corner radius. So the finer name is derived where a dataset is built, and
9
+ * carried on every tool as its `form`.
10
+ *
11
+ * The vocabulary is Fusion's own library, so a tool exported there lands on the
12
+ * type it already has. This list is the single source: icons draw from it,
13
+ * filter panels offer it, and a drawing decides from it whether it has an
14
+ * honest picture to draw.
15
+ */
16
+ export const TOOL_FORMS = [
17
+ { value: 'ball end mill', label: 'Ball end mill', group: 'Milling' },
18
+ { value: 'bull nose end mill', label: 'Bull nose end mill', group: 'Milling' },
19
+ { value: 'flat end mill', label: 'Flat end mill', group: 'Milling' },
20
+ { value: 'face mill', label: 'Face mill', group: 'Milling' },
21
+ { value: 'tapered mill', label: 'Tapered mill', group: 'Milling' },
22
+ { value: 'radius mill', label: 'Radius mill', group: 'Milling' },
23
+ { value: 'chamfer mill', label: 'Engrave/chamfer mill', group: 'Milling' },
24
+ { value: 'dovetail mill', label: 'Dovetail mill', group: 'Milling' },
25
+ { value: 'lollipop mill', label: 'Lollipop mill', group: 'Milling' },
26
+ { value: 'slot mill', label: 'Slot mill', group: 'Milling' },
27
+ { value: 'thread mill', label: 'Thread mill', group: 'Milling' },
28
+ { value: 'circle segment barrel', label: 'Circle segment barrel', group: 'Milling' },
29
+ { value: 'circle segment lens', label: 'Circle segment lens', group: 'Milling' },
30
+ { value: 'circle segment oval', label: 'Circle segment oval', group: 'Milling' },
31
+ { value: 'circle segment taper', label: 'Circle segment taper', group: 'Milling' },
32
+ { value: 'boring bar', label: 'Boring bar', group: 'Hole making' },
33
+ { value: 'counter bore', label: 'Counter bore', group: 'Hole making' },
34
+ { value: 'drill', label: 'Drill', group: 'Hole making' },
35
+ { value: 'center drill', label: 'Center drill', group: 'Hole making' },
36
+ { value: 'spot drill', label: 'Spot drill', group: 'Hole making' },
37
+ { value: 'reamer', label: 'Reamer', group: 'Hole making' },
38
+ { value: 'counter sink', label: 'Counter sink', group: 'Hole making' },
39
+ { value: 'tap left hand', label: 'Tap left hand', group: 'Hole making' },
40
+ { value: 'tap right hand', label: 'Tap right hand', group: 'Hole making' },
41
+ ];
42
+ /** The forms that mill — the ones a flute-count suggestion makes sense for. */
43
+ export const MILLING_FORMS = new Set(TOOL_FORMS.filter((form) => form.group === 'Milling').map((form) => form.value));
44
+ export const isToolForm = (value) => value === 'other' || TOOL_FORMS.some((form) => form.value === value);