@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Toolpath
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# Toolpath Tool Support
|
|
2
|
+
|
|
3
|
+
`@toolpath/tool-support` is the cutting-tool domain: what a tool, a holder, a
|
|
4
|
+
collet and an assembly _are_, and the arithmetic that follows from them.
|
|
5
|
+
|
|
6
|
+
**It depends on nothing.** No runtime dependencies, no peer dependencies, no
|
|
7
|
+
React, no DOM, no `fs`, no Toolpath SDK. Every other package that speaks about
|
|
8
|
+
cutting tools depends on it and it depends on none of them, which is what lets a
|
|
9
|
+
Node ingest script, a server route and a React renderer share one answer instead
|
|
10
|
+
of deriving three.
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
@toolpath/tool-support depends on nothing
|
|
14
|
+
↑ ↑ ↑
|
|
15
|
+
@toolpath/tool-scraper @toolpath/tool-drawing your application
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
npm install @toolpath/tool-support
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Why it exists
|
|
25
|
+
|
|
26
|
+
`@toolpath/tool-scraper` produces tool data and `@toolpath/tool-drawing`
|
|
27
|
+
consumes it, and every application in between re-derived what a tool assembly
|
|
28
|
+
is. The same fact ended up declared three times over: two names for one
|
|
29
|
+
millimetres-per-inch constant, three vocabularies for one unit axis, three
|
|
30
|
+
provenance types, two `PROFILES_VERSION`s — one of them imported under an alias
|
|
31
|
+
specifically so it could be compared against the other — and three shapes called
|
|
32
|
+
"holder" of which no two agreed on which fields exist.
|
|
33
|
+
|
|
34
|
+
That is not a tidiness complaint. **How far a tool stands out of its holder was
|
|
35
|
+
computed in four unconnected places and disagreed by a factor of two on an
|
|
36
|
+
ordinary tool.** A details table printed one number and the drawing beside it
|
|
37
|
+
drew another, so the dimension line ran past the holder nose and into the holder
|
|
38
|
+
body. It was fixed inside one application, which means the next consumer of the
|
|
39
|
+
same two packages reproduces it from scratch. That quantity is a pure function of
|
|
40
|
+
the tool, the collet and a shop's policy, and it had no home.
|
|
41
|
+
|
|
42
|
+
## What it holds
|
|
43
|
+
|
|
44
|
+
### The vocabulary
|
|
45
|
+
|
|
46
|
+
| Export | What it is |
|
|
47
|
+
| ---------------------------------------------------------------------- | ----------------------------------------------------------------- |
|
|
48
|
+
| `UnitSystem`, `UNIT_SYSTEMS`, `MM_PER_INCH` | How a vendor publishes a family, and the one constant between two |
|
|
49
|
+
| `convertLength`, `decimalsFor`, `UNIT_ABBREVIATION` | Converting, rounding and spelling it |
|
|
50
|
+
| `Provenance`, `PROVENANCE`, `ProvenanceMap` | Where a stated number came from |
|
|
51
|
+
| `GEOMETRY_FIELDS`, `geometryField`, `isLengthField`, `convertGeometry` | The ISO 13399 dictionary, and whether a code's value converts |
|
|
52
|
+
| `TOOL_FORMS`, `ToolForm`, `MILLING_FORMS`, `isToolForm` | What a tool is, in a CAM library's words |
|
|
53
|
+
|
|
54
|
+
Every length is in millimetres and every angle in degrees, whatever system the
|
|
55
|
+
vendor published in — that is what lets an inch tool and a metric tool compare.
|
|
56
|
+
`UnitSystem` is a fact _about the tool_, never the unit a stored value is in.
|
|
57
|
+
|
|
58
|
+
### The contracts
|
|
59
|
+
|
|
60
|
+
| Export | What it is |
|
|
61
|
+
| -------------------------------------------------- | -------------------------------------------- |
|
|
62
|
+
| `Tool`, `Geometry` | A cutting tool, as the arithmetic needs one |
|
|
63
|
+
| `Holder`, `HolderProfile`, `isHolderProfile` | The holder union: published, or measured |
|
|
64
|
+
| `Collet`, `Assembly` | What grips the shank, and the stack it makes |
|
|
65
|
+
| `PROFILES_VERSION`, `ProfilePoint`, `ProfileDatum` | The measured silhouette |
|
|
66
|
+
| `ReachCurve`, `FeatureDemand` | What a feature demands of a tool |
|
|
67
|
+
|
|
68
|
+
### The arithmetic
|
|
69
|
+
|
|
70
|
+
| Export | What it answers |
|
|
71
|
+
| --------------------------------------------------------------------------------- | ------------------------------------------------------- |
|
|
72
|
+
| `stickoutRange`, `minStickout`, `setupStickout`, `stickoutCeiling` | How far the tool stands out — **the one answer** |
|
|
73
|
+
| `DEFAULT_STICKOUT_POLICY`, `HELD_SHARE`, `StickoutPolicy`, `StickoutLimit` | The floor, the step and the share a shop keeps held |
|
|
74
|
+
| `clampWanted`, `clampShortfall`, `heldDiameter`, `headLength`, `DEFAULT_CLAMPING` | What the clamping rule keeps in the holder |
|
|
75
|
+
| `holderTakesTool`, `colletFitsHolder`, `gripsShank`, `maxStickout`, `holdBand` | Whether this holder takes this tool |
|
|
76
|
+
| `gripRanges`, `gripsAnyShank`, `canHold` | Whether a whole crib can hold it, as one set of spans |
|
|
77
|
+
| `stickoutLimits`, `defaultStickout` | The collet-shaped way into the range above |
|
|
78
|
+
| `hasNeck`, `shankOf`, `heightAt`, `belowGageLine` | The four that used to be written twice |
|
|
79
|
+
| `fitAgainst`, `fitTools`, `DRILLING_FORMS` | Whether a cutter cuts a feature |
|
|
80
|
+
| `clearance`, `toolSilhouette`, `holderSilhouette`, `toolCollisions` | Whether the stack clears the material around it |
|
|
81
|
+
| `assemblyAgainst`, `NOT_MODELLED` | Whether there is a way to _hold_ it that reaches |
|
|
82
|
+
| `sectionOutline`, `materialProfile`, `ASSEMBLY_PARTS`, `SILHOUETTE_PARTS` | The feature in section, and what an assembly is made of |
|
|
83
|
+
|
|
84
|
+
## Two kinds of holder
|
|
85
|
+
|
|
86
|
+
A holder arrives in one of two forms, and they are **alternatives rather than a
|
|
87
|
+
refinement of one by the other**:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import { isHolderProfile, type Holder, type HolderProfile } from '@toolpath/tool-support'
|
|
91
|
+
|
|
92
|
+
const published: Holder = {
|
|
93
|
+
noseDiameter: 27,
|
|
94
|
+
noseLength: 12,
|
|
95
|
+
bodyDiameter: 42,
|
|
96
|
+
bodyLength: 20,
|
|
97
|
+
projection: 60,
|
|
98
|
+
flangeDiameter: 46,
|
|
99
|
+
gaugeLength: 60,
|
|
100
|
+
colletSeries: 'ER16',
|
|
101
|
+
colletProtrusion: 2.5,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const measured: HolderProfile = {
|
|
105
|
+
points: [
|
|
106
|
+
[-30, 8],
|
|
107
|
+
[0, 23],
|
|
108
|
+
[48, 21],
|
|
109
|
+
[60, 13.5],
|
|
110
|
+
],
|
|
111
|
+
datum: 'gage-line',
|
|
112
|
+
colletSeries: 'ER16',
|
|
113
|
+
colletProtrusion: 2.5,
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
isHolderProfile(measured) // true — read `points`
|
|
117
|
+
isHolderProfile(published) // false — read the vendor's own numbers
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
`Holder` is a handful of numbers off a DIN 4000 sheet, and a drawing built from
|
|
121
|
+
them is a stylised holder. `HolderProfile` is the envelope measured off the
|
|
122
|
+
vendor's STEP model — a hundred-odd vertices carrying the V-flange groove and the
|
|
123
|
+
thread relief a machinist actually looks for. Reducing it to a nose and a body
|
|
124
|
+
throws away the only reason to measure, so the two are a union and
|
|
125
|
+
`isHolderProfile` tells them apart. A consumer with neither passes `null`.
|
|
126
|
+
|
|
127
|
+
## No classes, deliberately
|
|
128
|
+
|
|
129
|
+
Everything here is a readonly interface or a pure function over one. Two reasons
|
|
130
|
+
beyond consistency:
|
|
131
|
+
|
|
132
|
+
- **A class loses structural typing at a package boundary.** A catalog's own
|
|
133
|
+
record simply _is_ a `Tool`, with no adapter — the adapter stays a choice
|
|
134
|
+
rather than becoming a requirement.
|
|
135
|
+
- **`instanceof` breaks across duplicate installs.** This tree has been bitten by
|
|
136
|
+
that once already, and `@toolpath/tool-scraper` carries a packaging test about
|
|
137
|
+
it.
|
|
138
|
+
|
|
139
|
+
Where several quantities have to agree, one function answers them together
|
|
140
|
+
rather than one apiece: `stickoutRange` hands back the minimum, the setup length
|
|
141
|
+
and the ceiling in a single record, along with which of the three caps set it. A
|
|
142
|
+
caller cannot take one of those and forget the others, which is precisely how
|
|
143
|
+
the defect above happened — four callers each worked out the piece they wanted.
|
|
144
|
+
|
|
145
|
+
## An unrecognised code is not given a meaning
|
|
146
|
+
|
|
147
|
+
`geometryField` answers `null` and `isLengthField` answers `false` for a code the
|
|
148
|
+
dictionary has not pinned. Show it as the vendor's own and do not convert it: a
|
|
149
|
+
guessed unit is a wrong number that looks right, and a 118-degree drill point
|
|
150
|
+
converted as a length is a plausible-looking 2.36.
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
import { convertGeometry, geometryField } from '@toolpath/tool-support'
|
|
154
|
+
|
|
155
|
+
convertGeometry('DC', 6, 'millimeters', 'inches') // 0.2362…
|
|
156
|
+
convertGeometry('SIG', 118, 'millimeters', 'inches') // 118 — an angle does not convert
|
|
157
|
+
convertGeometry('WOC', 6, 'millimeters', 'inches') // 6 — unpinned, so not converted
|
|
158
|
+
geometryField('WOC') // null
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## The stickout, which is why this exists
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
import { stickoutRange } from '@toolpath/tool-support'
|
|
165
|
+
|
|
166
|
+
const range = stickoutRange(
|
|
167
|
+
{ unitSystem: 'inches', geometry: { DC: 25.4, OAL: 127, LCF: 31.75, SFDM: 25.4 } },
|
|
168
|
+
{ grip: 27.5 },
|
|
169
|
+
)
|
|
170
|
+
// range.setup — what a machinist sets it up at. This is `geometry.LBH`.
|
|
171
|
+
// range.max — the furthest it could ever stand out.
|
|
172
|
+
// range.limitedBy — 'clamp' | 'hold' | 'collet': which cap decided, so a
|
|
173
|
+
// control can say why rather than showing a bare number.
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Every other stickout is this call with more arguments, and `min ≤ setup ≤ max`
|
|
177
|
+
holds by construction — so a drawn stickout can never exceed the length a table
|
|
178
|
+
prints beside it. That invariant is a test, not a sentence in this file.
|
|
179
|
+
|
|
180
|
+
## Status
|
|
181
|
+
|
|
182
|
+
`0.x`: in use, and the surface still moves. A minor is the breaking channel
|
|
183
|
+
while the major is `0` — `^0.1.0` does not accept `0.2.0` — so nothing arrives
|
|
184
|
+
in a consumer's build without them asking for it.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type SweptAssembly } from './clearance.js';
|
|
2
|
+
import type { FeatureDemand } from './demand.js';
|
|
3
|
+
import { type FitFailure } from './fit.js';
|
|
4
|
+
/**
|
|
5
|
+
* Whether the whole stack reaches, not just the cutter.
|
|
6
|
+
*
|
|
7
|
+
* `fit.ts` answers "could this cutter cut this feature". This answers the
|
|
8
|
+
* question a shop actually acts on: **is there a way to hold it that reaches**.
|
|
9
|
+
* The two are different often enough to matter — a 3 mm end mill with 20 mm of
|
|
10
|
+
* flute clears a 15 mm pocket on its own, and fails the moment the only collet
|
|
11
|
+
* that grips a 3 mm shank leaves 12 mm standing out of the holder.
|
|
12
|
+
*
|
|
13
|
+
* This is deliberately thin, and everything it does not model is named in
|
|
14
|
+
* {@link NOT_MODELLED} rather than left for somebody to discover.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* One way of holding a tool, and whether it cuts what was asked.
|
|
18
|
+
*
|
|
19
|
+
* Generic in the assembly so a catalog's own richer record — with its guid, its
|
|
20
|
+
* holder's catalog number and its published maximum stickout — comes back
|
|
21
|
+
* intact rather than as a projection of itself.
|
|
22
|
+
*/
|
|
23
|
+
export interface AssemblyFit<A extends SweptAssembly = SweptAssembly> {
|
|
24
|
+
readonly assembly: A;
|
|
25
|
+
readonly fits: boolean;
|
|
26
|
+
readonly failures: readonly FitFailure[];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* What an assembly check does **not** answer yet, stated so nobody reads a
|
|
30
|
+
* pass as more than it is.
|
|
31
|
+
*
|
|
32
|
+
* - **Holder collision, on a report without a reach curve.** From Engine API
|
|
33
|
+
* 1.0.4 every datasheet carries one and `clearance.ts` sweeps the nose and
|
|
34
|
+
* the shank over it; an older report is not checked rather than guessed. The
|
|
35
|
+
* silhouette swept is the catalog's — nose diameter, shank, neck — not a
|
|
36
|
+
* holder's CAD.
|
|
37
|
+
* - **Deflection.** Reach is geometry; whether a stack at that reach can take
|
|
38
|
+
* a cut is rigidity, and this package has no force model.
|
|
39
|
+
* - **A bore holder's grip length**, which is why those assemblies use the
|
|
40
|
+
* whole tool as their stickout and are an upper bound rather than a fact.
|
|
41
|
+
* - **Reach, on an assembly whose collet publishes no grip length.** REGO-FIX's
|
|
42
|
+
* powRgrip collets do not, so those assemblies carry no stickout and their
|
|
43
|
+
* reach goes unchecked rather than guessed.
|
|
44
|
+
*/
|
|
45
|
+
export declare const NOT_MODELLED: readonly ["holder collision without a reach curve", "deflection", "bore holder grip", "reach without a published collet grip"];
|
|
46
|
+
/**
|
|
47
|
+
* Whether one assembly clears one feature.
|
|
48
|
+
*
|
|
49
|
+
* The cutter's own checks run first and unchanged — an assembly cannot rescue a
|
|
50
|
+
* tool that is too wide. What it adds is reach: the stickout has to clear the
|
|
51
|
+
* whole distance from the part top to the bottom of the feature, because the
|
|
52
|
+
* holder nose cannot go below the top of the part.
|
|
53
|
+
*/
|
|
54
|
+
export declare const assemblyAgainst: (assembly: SweptAssembly, demand: FeatureDemand) => FitFailure[];
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { clearance, describeCollision } from './clearance.js';
|
|
2
|
+
import { fitAgainst } from './fit.js';
|
|
3
|
+
/**
|
|
4
|
+
* What an assembly check does **not** answer yet, stated so nobody reads a
|
|
5
|
+
* pass as more than it is.
|
|
6
|
+
*
|
|
7
|
+
* - **Holder collision, on a report without a reach curve.** From Engine API
|
|
8
|
+
* 1.0.4 every datasheet carries one and `clearance.ts` sweeps the nose and
|
|
9
|
+
* the shank over it; an older report is not checked rather than guessed. The
|
|
10
|
+
* silhouette swept is the catalog's — nose diameter, shank, neck — not a
|
|
11
|
+
* holder's CAD.
|
|
12
|
+
* - **Deflection.** Reach is geometry; whether a stack at that reach can take
|
|
13
|
+
* a cut is rigidity, and this package has no force model.
|
|
14
|
+
* - **A bore holder's grip length**, which is why those assemblies use the
|
|
15
|
+
* whole tool as their stickout and are an upper bound rather than a fact.
|
|
16
|
+
* - **Reach, on an assembly whose collet publishes no grip length.** REGO-FIX's
|
|
17
|
+
* powRgrip collets do not, so those assemblies carry no stickout and their
|
|
18
|
+
* reach goes unchecked rather than guessed.
|
|
19
|
+
*/
|
|
20
|
+
export const NOT_MODELLED = [
|
|
21
|
+
'holder collision without a reach curve',
|
|
22
|
+
'deflection',
|
|
23
|
+
'bore holder grip',
|
|
24
|
+
'reach without a published collet grip',
|
|
25
|
+
];
|
|
26
|
+
/**
|
|
27
|
+
* Whether one assembly clears one feature.
|
|
28
|
+
*
|
|
29
|
+
* The cutter's own checks run first and unchanged — an assembly cannot rescue a
|
|
30
|
+
* tool that is too wide. What it adds is reach: the stickout has to clear the
|
|
31
|
+
* whole distance from the part top to the bottom of the feature, because the
|
|
32
|
+
* holder nose cannot go below the top of the part.
|
|
33
|
+
*/
|
|
34
|
+
export const assemblyAgainst = (assembly, demand) => {
|
|
35
|
+
const failures = [...fitAgainst(assembly.tool, demand)];
|
|
36
|
+
// An unstated stickout is not checked, the same rule the cutter checks
|
|
37
|
+
// follow: what nobody has said is not a limit anybody can be held to.
|
|
38
|
+
if (assembly.stickout !== null &&
|
|
39
|
+
demand.reachBelowTop !== undefined &&
|
|
40
|
+
assembly.stickout < demand.reachBelowTop) {
|
|
41
|
+
failures.push({
|
|
42
|
+
featureTag: demand.featureTag,
|
|
43
|
+
reason: `${assembly.stickout.toFixed(1)} mm of stickout does not clear ${demand.reachBelowTop.toFixed(1)} mm below the part top`,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
// The material around the feature, where the report states it: the holder
|
|
47
|
+
// nose and the shank are swept over the reach curve, and each thing they
|
|
48
|
+
// meet is its own reason.
|
|
49
|
+
if (demand.reachCurve) {
|
|
50
|
+
for (const collision of clearance(assembly, demand.reachCurve).collisions) {
|
|
51
|
+
failures.push({ featureTag: demand.featureTag, reason: describeCollision(collision) });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return failures;
|
|
55
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How much shank stays in the holder.
|
|
3
|
+
*
|
|
4
|
+
* **This is a cap, not the length below the holder.** It was written straight
|
|
5
|
+
* into `geometry.LBH` at build time until 2026-09-03, which made it one of four
|
|
6
|
+
* unreconciled answers to "how far does this tool stand out" — see the table in
|
|
7
|
+
* `stickout.ts`. `LBH` is the *setup* length and `stickout.ts` owns it; what
|
|
8
|
+
* lives here is {@link clampWanted}, the length of shank a shop keeps clamped,
|
|
9
|
+
* which `stickoutRange` takes as one of three caps on that setup.
|
|
10
|
+
*
|
|
11
|
+
* **ISO 13399 calls it `LSCN`** — clamping length minimum, stated against the
|
|
12
|
+
* shank diameter `DMM`, which is what a multiple of "D" means here: the holder
|
|
13
|
+
* grips the shank, not the cut. Manufacturers publish it per tool, and the five
|
|
14
|
+
* Seco end mills checked want between 4 and 6 diameters clamped against the 3×D
|
|
15
|
+
* rule of thumb — a difference that is most of a tool's reach. So the rule
|
|
16
|
+
* reads the vendor's own number first and falls back to a multiple of the
|
|
17
|
+
* diameter for every tool that publishes none, which is every tool in the
|
|
18
|
+
* scraped catalog today because no adapter carries the column yet.
|
|
19
|
+
*
|
|
20
|
+
* the clamping cap = OAL − (minimum clamping length × SFDM)
|
|
21
|
+
*
|
|
22
|
+
* **The bury-the-head case is not handled here.** When the subtraction lands at
|
|
23
|
+
* or under the shoulder length, it is `stickoutRange`'s floor and `gripShort`
|
|
24
|
+
* that answer, and they answer for all three caps rather than this one: a tool
|
|
25
|
+
* the rule cannot hold is gripped as short as the grip allows and says so. As a
|
|
26
|
+
* *cap*, the exception was backwards — it raised the ceiling above what the
|
|
27
|
+
* clamping rule allowed.
|
|
28
|
+
*/
|
|
29
|
+
import type { Geometry } from './geometry.js';
|
|
30
|
+
/** What a shop holds: the vendor's number where there is one, else a multiple of the diameter. */
|
|
31
|
+
export interface ClampingRule {
|
|
32
|
+
/** Read the manufacturer's `LSCN` where the tool publishes one. On by default. */
|
|
33
|
+
readonly vendorSpec: boolean;
|
|
34
|
+
/** Diameters to clamp where it does not — the rule of thumb is 3. Zero for none. */
|
|
35
|
+
readonly perDiameter: number;
|
|
36
|
+
}
|
|
37
|
+
/** What a dataset is built with, and what a page starts at. */
|
|
38
|
+
export declare const DEFAULT_CLAMPING: ClampingRule;
|
|
39
|
+
/**
|
|
40
|
+
* The diameter a clamping length is a multiple **of**: the shank.
|
|
41
|
+
*
|
|
42
|
+
* `LSCN` is stated against `DMM`, and the shank is what the holder grips — a
|
|
43
|
+
* keyseat cutter 22 mm across on a ⌀12 shank is clamped on 12. The cut stands
|
|
44
|
+
* in only where a vendor states no shank.
|
|
45
|
+
*/
|
|
46
|
+
export declare const heldDiameter: (geometry: Geometry) => number | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* Where the shank starts, measured from the tip: past the flutes, and past the
|
|
49
|
+
* reduced section under them where a tool has one. A chuck closes on neither.
|
|
50
|
+
*/
|
|
51
|
+
export declare const headLength: (geometry: Geometry) => number;
|
|
52
|
+
/** What this rule asks to keep in the holder, or null where it says nothing. */
|
|
53
|
+
export declare const clampWanted: (geometry: Geometry, rule?: ClampingRule) => number | null;
|
|
54
|
+
/** How much shank the rule asked for and the tool has not got, or null where it fits. */
|
|
55
|
+
export declare const clampShortfall: (geometry: Geometry, rule?: ClampingRule) => number | null;
|
package/dist/clamping.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How much shank stays in the holder.
|
|
3
|
+
*
|
|
4
|
+
* **This is a cap, not the length below the holder.** It was written straight
|
|
5
|
+
* into `geometry.LBH` at build time until 2026-09-03, which made it one of four
|
|
6
|
+
* unreconciled answers to "how far does this tool stand out" — see the table in
|
|
7
|
+
* `stickout.ts`. `LBH` is the *setup* length and `stickout.ts` owns it; what
|
|
8
|
+
* lives here is {@link clampWanted}, the length of shank a shop keeps clamped,
|
|
9
|
+
* which `stickoutRange` takes as one of three caps on that setup.
|
|
10
|
+
*
|
|
11
|
+
* **ISO 13399 calls it `LSCN`** — clamping length minimum, stated against the
|
|
12
|
+
* shank diameter `DMM`, which is what a multiple of "D" means here: the holder
|
|
13
|
+
* grips the shank, not the cut. Manufacturers publish it per tool, and the five
|
|
14
|
+
* Seco end mills checked want between 4 and 6 diameters clamped against the 3×D
|
|
15
|
+
* rule of thumb — a difference that is most of a tool's reach. So the rule
|
|
16
|
+
* reads the vendor's own number first and falls back to a multiple of the
|
|
17
|
+
* diameter for every tool that publishes none, which is every tool in the
|
|
18
|
+
* scraped catalog today because no adapter carries the column yet.
|
|
19
|
+
*
|
|
20
|
+
* the clamping cap = OAL − (minimum clamping length × SFDM)
|
|
21
|
+
*
|
|
22
|
+
* **The bury-the-head case is not handled here.** When the subtraction lands at
|
|
23
|
+
* or under the shoulder length, it is `stickoutRange`'s floor and `gripShort`
|
|
24
|
+
* that answer, and they answer for all three caps rather than this one: a tool
|
|
25
|
+
* the rule cannot hold is gripped as short as the grip allows and says so. As a
|
|
26
|
+
* *cap*, the exception was backwards — it raised the ceiling above what the
|
|
27
|
+
* clamping rule allowed.
|
|
28
|
+
*/
|
|
29
|
+
/** What a dataset is built with, and what a page starts at. */
|
|
30
|
+
export const DEFAULT_CLAMPING = { vendorSpec: true, perDiameter: 3 };
|
|
31
|
+
/**
|
|
32
|
+
* Two decimals, which is finer than any vendor prints and coarser than the
|
|
33
|
+
* float error of a unit conversion.
|
|
34
|
+
*/
|
|
35
|
+
const round = (value) => Math.round(value * 100) / 100;
|
|
36
|
+
/**
|
|
37
|
+
* The diameter a clamping length is a multiple **of**: the shank.
|
|
38
|
+
*
|
|
39
|
+
* `LSCN` is stated against `DMM`, and the shank is what the holder grips — a
|
|
40
|
+
* keyseat cutter 22 mm across on a ⌀12 shank is clamped on 12. The cut stands
|
|
41
|
+
* in only where a vendor states no shank.
|
|
42
|
+
*/
|
|
43
|
+
export const heldDiameter = (geometry) => geometry.SFDM ?? geometry.DC;
|
|
44
|
+
/**
|
|
45
|
+
* Where the shank starts, measured from the tip: past the flutes, and past the
|
|
46
|
+
* reduced section under them where a tool has one. A chuck closes on neither.
|
|
47
|
+
*/
|
|
48
|
+
export const headLength = (geometry) => Math.max(geometry['shoulder-length'] ?? 0, geometry.LCF ?? 0);
|
|
49
|
+
/** What this rule asks to keep in the holder, or null where it says nothing. */
|
|
50
|
+
export const clampWanted = (geometry, rule = DEFAULT_CLAMPING) => {
|
|
51
|
+
const stated = geometry.LSCN;
|
|
52
|
+
if (rule.vendorSpec && stated !== undefined && stated > 0) {
|
|
53
|
+
return round(stated);
|
|
54
|
+
}
|
|
55
|
+
const shank = heldDiameter(geometry);
|
|
56
|
+
if (rule.perDiameter <= 0 || shank === undefined || shank <= 0) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return round(shank * rule.perDiameter);
|
|
60
|
+
};
|
|
61
|
+
/** How much shank the rule asked for and the tool has not got, or null where it fits. */
|
|
62
|
+
export const clampShortfall = (geometry, rule = DEFAULT_CLAMPING) => {
|
|
63
|
+
const wanted = clampWanted(geometry, rule);
|
|
64
|
+
const { OAL } = geometry;
|
|
65
|
+
if (wanted === null || OAL === undefined) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
const shank = Math.max(0, OAL - headLength(geometry));
|
|
69
|
+
return wanted <= shank ? null : round(wanted - shank);
|
|
70
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
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 { type Margins, type Silhouette, type SilhouettePart } from './parts.js';
|
|
31
|
+
import { type ReachCurve } from './reach.js';
|
|
32
|
+
import { type Tool } from './tool.js';
|
|
33
|
+
import type { Collet, Holder } from './holding.js';
|
|
34
|
+
/**
|
|
35
|
+
* The tool's own profile above the flutes, from what the vendor states.
|
|
36
|
+
*
|
|
37
|
+
* - A neck, where a shoulder diameter *and* length are stated: that radius
|
|
38
|
+
* from the end of the flutes to the shoulder.
|
|
39
|
+
* - The shank, from the shoulder (or the end of the flutes, where there is no
|
|
40
|
+
* neck) upward.
|
|
41
|
+
*
|
|
42
|
+
* A neck whose diameter is unstated is taken to be no wider than the cut,
|
|
43
|
+
* which is what a neck is for, and so has nothing to check.
|
|
44
|
+
*/
|
|
45
|
+
export declare const toolSilhouette: (tool: Pick<Tool, "geometry">) => Silhouette[];
|
|
46
|
+
/**
|
|
47
|
+
* The tool's own body against the part, whatever holds it.
|
|
48
|
+
*
|
|
49
|
+
* A shank or neck that stands past the cutting edge meets the wall above the
|
|
50
|
+
* flutes at every stickout — no holder and no pull-out changes where the
|
|
51
|
+
* tool's own steps sit above its tip. Paul's call (2026-08-30): such a tool
|
|
52
|
+
* is not compatible with the feature and is not shown; the answer is longer
|
|
53
|
+
* flutes or a reduced shank. Swept with the same margins as the holder.
|
|
54
|
+
*/
|
|
55
|
+
export declare const toolCollisions: (tool: Pick<Tool, "geometry">, curve: ReachCurve, margins?: Margins) => Collision[];
|
|
56
|
+
/**
|
|
57
|
+
* What the sweep needs of an assembly.
|
|
58
|
+
*
|
|
59
|
+
* **The parametric holder, not the union.** A measured `HolderProfile` is a
|
|
60
|
+
* hundred-odd vertices and sweeping one is a different function that does not
|
|
61
|
+
* exist yet; taking the union here would let a caller hand over a profile and
|
|
62
|
+
* get a verdict computed from nothing. Declared structurally, so a catalog's
|
|
63
|
+
* own richer assembly satisfies it with no adapter.
|
|
64
|
+
*/
|
|
65
|
+
export interface SweptAssembly {
|
|
66
|
+
readonly tool: Pick<Tool, 'geometry' | 'form'>;
|
|
67
|
+
readonly holder: Holder;
|
|
68
|
+
readonly collet?: Collet | null;
|
|
69
|
+
readonly stickout: number | null;
|
|
70
|
+
}
|
|
71
|
+
export interface Collision {
|
|
72
|
+
readonly part: SilhouettePart;
|
|
73
|
+
/** Where that part of the assembly begins, above the tip, in mm. */
|
|
74
|
+
readonly height: number;
|
|
75
|
+
/** How high the material stands at that part's offset from the cut, in mm. */
|
|
76
|
+
readonly needs: number;
|
|
77
|
+
/** That offset, in mm past the cutting edge. */
|
|
78
|
+
readonly offset: number;
|
|
79
|
+
}
|
|
80
|
+
export interface Clearance {
|
|
81
|
+
/** Every checked part of the silhouette clears. */
|
|
82
|
+
readonly clears: boolean;
|
|
83
|
+
readonly collisions: readonly Collision[];
|
|
84
|
+
/**
|
|
85
|
+
* The least the holder nose has to stand off the tip to clear, in mm — or
|
|
86
|
+
* null where the holder's nose is unstated and so unchecked.
|
|
87
|
+
*/
|
|
88
|
+
readonly requiredStickout: number | null;
|
|
89
|
+
/** What was actually swept, so a pass is read as exactly that much. */
|
|
90
|
+
readonly checked: readonly SilhouettePart[];
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The holder's profile above the tool, from the stickout up.
|
|
94
|
+
*
|
|
95
|
+
* - The seated collet, standing proud of the nose face by its protrusion, at
|
|
96
|
+
* the collet's own diameter.
|
|
97
|
+
* - The nose, for its stated length — or, with no length stated, for the
|
|
98
|
+
* gauge length as before, so an older dataset sweeps what it always did.
|
|
99
|
+
* - The body behind the nose, where stated.
|
|
100
|
+
* - The flange, at the projection. Nothing is swept between it and the body:
|
|
101
|
+
* a `Silhouette` is a radius **from a height upward**, so the last stated
|
|
102
|
+
* diameter carries itself up to the flange, which is the layer model of
|
|
103
|
+
* Justin Mimbs' reach-curve note.
|
|
104
|
+
*/
|
|
105
|
+
export declare const holderSilhouette: (assembly: SweptAssembly, stickout: number) => Silhouette[];
|
|
106
|
+
/**
|
|
107
|
+
* Whether one assembly clears one feature's reach curve.
|
|
108
|
+
*
|
|
109
|
+
* The holder is checked at the assembly's stickout, and `requiredStickout`
|
|
110
|
+
* says the least stickout at which every part of the holder would clear — so
|
|
111
|
+
* a stack that fails can be read as "stick it out further" rather than "no".
|
|
112
|
+
*/
|
|
113
|
+
export declare const clearance: (assembly: SweptAssembly, curve: ReachCurve, margins?: Margins) => Clearance;
|
|
114
|
+
/** Why a collision rules an assembly out, in the words a machinist would use. */
|
|
115
|
+
export declare const describeCollision: (collision: Collision) => string;
|