@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.
- package/LICENSE +21 -0
- package/README.md +184 -0
- package/dist/assembly-fit.d.ts +54 -0
- package/dist/assembly-fit.js +55 -0
- package/dist/clamping.d.ts +55 -0
- package/dist/clamping.js +70 -0
- package/dist/clearance.d.ts +115 -0
- package/dist/clearance.js +209 -0
- package/dist/demand.d.ts +51 -0
- package/dist/demand.js +23 -0
- package/dist/fit.d.ts +69 -0
- package/dist/fit.js +90 -0
- package/dist/forms.d.ts +130 -0
- package/dist/forms.js +44 -0
- package/dist/geometry.d.ts +191 -0
- package/dist/geometry.js +124 -0
- package/dist/holding.d.ts +266 -0
- package/dist/holding.js +203 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.js +62 -0
- package/dist/material.d.ts +29 -0
- package/dist/material.js +30 -0
- package/dist/parts.d.ts +56 -0
- package/dist/parts.js +38 -0
- package/dist/profile.d.ts +104 -0
- package/dist/profile.js +68 -0
- package/dist/provenance.d.ts +25 -0
- package/dist/provenance.js +15 -0
- package/dist/reach.d.ts +46 -0
- package/dist/reach.js +37 -0
- package/dist/section.d.ts +67 -0
- package/dist/section.js +185 -0
- package/dist/stickout.d.ts +187 -0
- package/dist/stickout.js +161 -0
- package/dist/tool.d.ts +80 -0
- package/dist/tool.js +89 -0
- package/dist/units.d.ts +73 -0
- package/dist/units.js +82 -0
- package/package.json +48 -0
|
@@ -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
|
+
};
|
package/dist/demand.d.ts
ADDED
|
@@ -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
|
+
});
|
package/dist/forms.d.ts
ADDED
|
@@ -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);
|