@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,191 @@
1
+ /**
2
+ * The interchange dictionary: what a geometry code measures, and whether the
3
+ * number under it converts.
4
+ *
5
+ * Most of these are ISO 13399's own codes with the standard's meanings — the
6
+ * machine-tool industry's interchange vocabulary, which is also why they appear
7
+ * in Fusion's tool JSON. Three are Autodesk's names for measurements ISO codes
8
+ * differently, and {@link GeometryField.iso} records the standard's counterpart
9
+ * rather than quietly renaming them: a consumer recognises the name the scraper
10
+ * emitted, and a reader can still find the standard's.
11
+ *
12
+ * ## Why the dictionary carries a unit kind and not a label
13
+ *
14
+ * The unit kind is the part that changes behaviour: it is what decides whether
15
+ * a stated number converts with {@link UnitSystem} or is the same number in
16
+ * both systems. A flute count and a point angle do not convert, an L/D ratio
17
+ * does not convert, and a length does — and a consumer that converts the wrong
18
+ * one prints a 60-degree drill point as 2.36.
19
+ *
20
+ * Human-facing labels and descriptions are presentational: which words a detail
21
+ * page puts beside `SFDM` is that page's decision and changes with its
22
+ * audience. {@link GeometryField.definition} is not one of those — it is the
23
+ * phrase a diagnostic quotes back at whoever mapped a column to the wrong code,
24
+ * which is why the scraper already carries it.
25
+ *
26
+ * ## An unknown code is not given a meaning
27
+ *
28
+ * {@link geometryField} answers `null` rather than inventing an entry, and
29
+ * {@link isLengthField} answers `false`. A consumer shows an unrecognised code
30
+ * as the vendor's own and does not convert it — the alternative is guessing a
31
+ * unit for a column nobody has checked, and a guessed conversion is a wrong
32
+ * number that looks right.
33
+ */
34
+ import { type UnitSystem } from './units.js';
35
+ /**
36
+ * What kind of quantity a code states.
37
+ *
38
+ * `mm` is the only one that moves with a unit system. `deg` and `count` are the
39
+ * same number in both; `ratio` is dimensionless by construction, and an `LD` of
40
+ * 4 is 4 whichever sheet it came off.
41
+ */
42
+ export type GeometryUnit = 'mm' | 'deg' | 'count' | 'ratio';
43
+ export interface GeometryField {
44
+ readonly unit: GeometryUnit;
45
+ /**
46
+ * What the field measures, phrased so it can be quoted back at whoever
47
+ * mapped a column to the wrong one.
48
+ */
49
+ readonly definition: string;
50
+ /**
51
+ * The ISO 13399 code for this measurement, or `null` where the standard's
52
+ * counterpart has not been pinned. Equal to the key itself on every field
53
+ * that *is* the standard's code.
54
+ */
55
+ readonly iso: string | null;
56
+ }
57
+ /**
58
+ * The codes this domain knows, keyed by the name the scraper emits.
59
+ *
60
+ * **This vocabulary is the scraper's, not this package's invention.** Renaming
61
+ * a field here would put a translation table between two vocabularies, and a
62
+ * translation table is where an `SFDM` becomes a `DC` in one direction and
63
+ * nobody notices.
64
+ */
65
+ export declare const GEOMETRY_FIELDS: {
66
+ readonly DC: {
67
+ readonly unit: "mm";
68
+ readonly definition: "cutting diameter";
69
+ readonly iso: "DC";
70
+ };
71
+ readonly SFDM: {
72
+ readonly unit: "mm";
73
+ readonly definition: "shank diameter — what the holder grips";
74
+ readonly iso: "DMM";
75
+ };
76
+ readonly OAL: {
77
+ readonly unit: "mm";
78
+ readonly definition: "overall length, tip to the end of the shank";
79
+ readonly iso: "OAL";
80
+ };
81
+ readonly LCF: {
82
+ readonly unit: "mm";
83
+ readonly definition: "flute length — the length of the cutting edge";
84
+ readonly iso: "LCF";
85
+ };
86
+ readonly RE: {
87
+ readonly unit: "mm";
88
+ readonly definition: "corner radius; 0 on a square-end tool";
89
+ readonly iso: "RE";
90
+ };
91
+ /**
92
+ * **`unit: 'mm'` is not the whole truth, and a caller converting a `TP` off
93
+ * an inch tap should know it.** A metric tap's pitch is a length; an inch
94
+ * tap's is conventionally threads-per-inch, which is a *reciprocal*, and
95
+ * converting one as a length gives a number that looks like a pitch and is
96
+ * wrong by a factor of its own value. No consumer in this tree converts it —
97
+ * the scraper reads it from a single column already in the tool's own system,
98
+ * and the catalog drops the code on ingest for exactly this reason — so the
99
+ * hazard is recorded rather than resolved. Resolving it means either
100
+ * confirming the inch convention against a real tap table or giving the
101
+ * dictionary a unit kind for "not decidable from the code alone".
102
+ */
103
+ readonly TP: {
104
+ readonly unit: "mm";
105
+ readonly definition: "thread pitch, in the tool’s own unit system";
106
+ readonly iso: "TP";
107
+ };
108
+ readonly NOF: {
109
+ readonly unit: "count";
110
+ readonly definition: "number of flutes";
111
+ readonly iso: "NOF";
112
+ };
113
+ readonly SIG: {
114
+ readonly unit: "deg";
115
+ readonly definition: "point angle, degrees included";
116
+ readonly iso: "SIG";
117
+ };
118
+ /**
119
+ * ISO 13399's clamping length minimum: the shank a manufacturer wants held.
120
+ *
121
+ * No vendor scraped so far publishes it, and the day one does this is where
122
+ * it lands — read in preference to any rule of thumb about how much shank a
123
+ * shop keeps clamped.
124
+ */
125
+ readonly LSCN: {
126
+ readonly unit: "mm";
127
+ readonly definition: "least of the shank the manufacturer wants clamped";
128
+ readonly iso: "LSCN";
129
+ };
130
+ /**
131
+ * Not a vendor's column: how far the tool is set out of the holder.
132
+ *
133
+ * The number the drawing draws and the details table prints, and the one
134
+ * quantity that was computed in four unconnected places and disagreed by a
135
+ * factor of two on an ordinary tool. It is derived, and its provenance says
136
+ * so.
137
+ */
138
+ readonly LBH: {
139
+ readonly unit: "mm";
140
+ readonly definition: "length below holder — how far the tool is set out of the holder nose";
141
+ readonly iso: null;
142
+ };
143
+ /** Length below holder over cutting diameter — the "×D" a shop reads reach in. */
144
+ readonly LD: {
145
+ readonly unit: "ratio";
146
+ readonly definition: "length below holder over cutting diameter — the ×D a shop reads reach in";
147
+ readonly iso: null;
148
+ };
149
+ readonly 'shoulder-length': {
150
+ readonly unit: "mm";
151
+ readonly definition: "usable length below the full shank";
152
+ readonly iso: null;
153
+ };
154
+ readonly 'shoulder-diameter': {
155
+ readonly unit: "mm";
156
+ readonly definition: "diameter at the shoulder — the neck, where necked";
157
+ readonly iso: null;
158
+ };
159
+ };
160
+ /** A code {@link GEOMETRY_FIELDS} knows. */
161
+ export type GeometryCode = keyof typeof GEOMETRY_FIELDS;
162
+ /**
163
+ * A tool's stated geometry: code to value, lengths in millimetres and angles in
164
+ * degrees whatever system the vendor published in.
165
+ *
166
+ * Keyed by `string` and not by {@link GeometryCode}, because a vendor states
167
+ * columns this dictionary has not pinned yet and dropping them on ingest would
168
+ * lose data that is still the vendor's. `undefined` is admitted so that a
169
+ * record with a narrower `Record<string, number>` geometry satisfies this by
170
+ * structure, with no adapter.
171
+ */
172
+ export type Geometry = Readonly<Record<string, number | undefined>>;
173
+ /** What a code measures, or `null` where this dictionary has not pinned it. */
174
+ export declare const geometryField: (code: string) => GeometryField | null;
175
+ /**
176
+ * Whether a value under this code is a length, and so moves with a unit system.
177
+ *
178
+ * The single question the dictionary's unit kind exists to answer. `false` for
179
+ * an unknown code, which is the same refusal {@link geometryField} makes: a
180
+ * code nobody has classified is not converted, because a guessed conversion is
181
+ * worse than an unconverted number a reader can still recognise.
182
+ */
183
+ export declare const isLengthField: (code: string) => boolean;
184
+ /**
185
+ * `value`, stated under `code`, converted between systems — or unchanged where
186
+ * the code does not state a length.
187
+ *
188
+ * The conversion and the decision whether to convert in one call, so a caller
189
+ * cannot get the second one right and the first one wrong.
190
+ */
191
+ export declare const convertGeometry: (code: string, value: number, from: UnitSystem, to: UnitSystem) => number;
@@ -0,0 +1,124 @@
1
+ /**
2
+ * The interchange dictionary: what a geometry code measures, and whether the
3
+ * number under it converts.
4
+ *
5
+ * Most of these are ISO 13399's own codes with the standard's meanings — the
6
+ * machine-tool industry's interchange vocabulary, which is also why they appear
7
+ * in Fusion's tool JSON. Three are Autodesk's names for measurements ISO codes
8
+ * differently, and {@link GeometryField.iso} records the standard's counterpart
9
+ * rather than quietly renaming them: a consumer recognises the name the scraper
10
+ * emitted, and a reader can still find the standard's.
11
+ *
12
+ * ## Why the dictionary carries a unit kind and not a label
13
+ *
14
+ * The unit kind is the part that changes behaviour: it is what decides whether
15
+ * a stated number converts with {@link UnitSystem} or is the same number in
16
+ * both systems. A flute count and a point angle do not convert, an L/D ratio
17
+ * does not convert, and a length does — and a consumer that converts the wrong
18
+ * one prints a 60-degree drill point as 2.36.
19
+ *
20
+ * Human-facing labels and descriptions are presentational: which words a detail
21
+ * page puts beside `SFDM` is that page's decision and changes with its
22
+ * audience. {@link GeometryField.definition} is not one of those — it is the
23
+ * phrase a diagnostic quotes back at whoever mapped a column to the wrong code,
24
+ * which is why the scraper already carries it.
25
+ *
26
+ * ## An unknown code is not given a meaning
27
+ *
28
+ * {@link geometryField} answers `null` rather than inventing an entry, and
29
+ * {@link isLengthField} answers `false`. A consumer shows an unrecognised code
30
+ * as the vendor's own and does not convert it — the alternative is guessing a
31
+ * unit for a column nobody has checked, and a guessed conversion is a wrong
32
+ * number that looks right.
33
+ */
34
+ import { convertLength } from './units.js';
35
+ /**
36
+ * The codes this domain knows, keyed by the name the scraper emits.
37
+ *
38
+ * **This vocabulary is the scraper's, not this package's invention.** Renaming
39
+ * a field here would put a translation table between two vocabularies, and a
40
+ * translation table is where an `SFDM` becomes a `DC` in one direction and
41
+ * nobody notices.
42
+ */
43
+ export const GEOMETRY_FIELDS = {
44
+ DC: { unit: 'mm', definition: 'cutting diameter', iso: 'DC' },
45
+ SFDM: { unit: 'mm', definition: 'shank diameter — what the holder grips', iso: 'DMM' },
46
+ OAL: { unit: 'mm', definition: 'overall length, tip to the end of the shank', iso: 'OAL' },
47
+ LCF: { unit: 'mm', definition: 'flute length — the length of the cutting edge', iso: 'LCF' },
48
+ RE: { unit: 'mm', definition: 'corner radius; 0 on a square-end tool', iso: 'RE' },
49
+ /**
50
+ * **`unit: 'mm'` is not the whole truth, and a caller converting a `TP` off
51
+ * an inch tap should know it.** A metric tap's pitch is a length; an inch
52
+ * tap's is conventionally threads-per-inch, which is a *reciprocal*, and
53
+ * converting one as a length gives a number that looks like a pitch and is
54
+ * wrong by a factor of its own value. No consumer in this tree converts it —
55
+ * the scraper reads it from a single column already in the tool's own system,
56
+ * and the catalog drops the code on ingest for exactly this reason — so the
57
+ * hazard is recorded rather than resolved. Resolving it means either
58
+ * confirming the inch convention against a real tap table or giving the
59
+ * dictionary a unit kind for "not decidable from the code alone".
60
+ */
61
+ TP: { unit: 'mm', definition: 'thread pitch, in the tool’s own unit system', iso: 'TP' },
62
+ NOF: { unit: 'count', definition: 'number of flutes', iso: 'NOF' },
63
+ SIG: { unit: 'deg', definition: 'point angle, degrees included', iso: 'SIG' },
64
+ /**
65
+ * ISO 13399's clamping length minimum: the shank a manufacturer wants held.
66
+ *
67
+ * No vendor scraped so far publishes it, and the day one does this is where
68
+ * it lands — read in preference to any rule of thumb about how much shank a
69
+ * shop keeps clamped.
70
+ */
71
+ LSCN: {
72
+ unit: 'mm',
73
+ definition: 'least of the shank the manufacturer wants clamped',
74
+ iso: 'LSCN',
75
+ },
76
+ /**
77
+ * Not a vendor's column: how far the tool is set out of the holder.
78
+ *
79
+ * The number the drawing draws and the details table prints, and the one
80
+ * quantity that was computed in four unconnected places and disagreed by a
81
+ * factor of two on an ordinary tool. It is derived, and its provenance says
82
+ * so.
83
+ */
84
+ LBH: {
85
+ unit: 'mm',
86
+ definition: 'length below holder — how far the tool is set out of the holder nose',
87
+ iso: null,
88
+ },
89
+ /** Length below holder over cutting diameter — the "×D" a shop reads reach in. */
90
+ LD: {
91
+ unit: 'ratio',
92
+ definition: 'length below holder over cutting diameter — the ×D a shop reads reach in',
93
+ iso: null,
94
+ },
95
+ 'shoulder-length': {
96
+ unit: 'mm',
97
+ definition: 'usable length below the full shank',
98
+ iso: null,
99
+ },
100
+ 'shoulder-diameter': {
101
+ unit: 'mm',
102
+ definition: 'diameter at the shoulder — the neck, where necked',
103
+ iso: null,
104
+ },
105
+ };
106
+ /** What a code measures, or `null` where this dictionary has not pinned it. */
107
+ export const geometryField = (code) => Object.hasOwn(GEOMETRY_FIELDS, code) ? GEOMETRY_FIELDS[code] : null;
108
+ /**
109
+ * Whether a value under this code is a length, and so moves with a unit system.
110
+ *
111
+ * The single question the dictionary's unit kind exists to answer. `false` for
112
+ * an unknown code, which is the same refusal {@link geometryField} makes: a
113
+ * code nobody has classified is not converted, because a guessed conversion is
114
+ * worse than an unconverted number a reader can still recognise.
115
+ */
116
+ export const isLengthField = (code) => geometryField(code)?.unit === 'mm';
117
+ /**
118
+ * `value`, stated under `code`, converted between systems — or unchanged where
119
+ * the code does not state a length.
120
+ *
121
+ * The conversion and the decision whether to convert in one call, so a caller
122
+ * cannot get the second one right and the first one wrong.
123
+ */
124
+ export const convertGeometry = (code, value, from, to) => (isLengthField(code) ? convertLength(value, from, to) : value);
@@ -0,0 +1,266 @@
1
+ /**
2
+ * What holds a tool, and the stack the two make together.
3
+ *
4
+ * ## The holder is a union, not one shape with optional extras
5
+ *
6
+ * A holder arrives in one of two forms and they are alternatives rather than a
7
+ * refinement of one by the other: {@link Holder} is what a vendor *publishes*
8
+ * about it, and {@link HolderProfile} is what its own CAD model *measures*.
9
+ * {@link isHolderProfile} tells them apart. A consumer that has both picks one;
10
+ * a consumer that has neither passes `null` and the tool stands alone.
11
+ *
12
+ * ## What is here and what is not
13
+ *
14
+ * Three shapes in this tree called themselves a holder and no two agreed on
15
+ * which fields exist — sixteen, nine and nineteen. What they *did* agree on is
16
+ * the geometry below, which is also exactly what a drawing and a clearance
17
+ * sweep read. Identity and commerce — a guid, a brand, a catalog number, the
18
+ * vendor's own CAD download — belong to a catalog's record, which extends this.
19
+ *
20
+ * ## How a holder grips is optional, and the absence is load bearing
21
+ *
22
+ * {@link Holder.clamping}, {@link Holder.boreDiameter} and {@link Holder.taper}
23
+ * arrived with the functions that read them, and they are optional because a
24
+ * drawing has never needed them: a consumer that hands over nine numbers to get
25
+ * a picture must not have to invent a clamping mode to do it.
26
+ *
27
+ * **Absent means nobody has said, and nobody-has-said refuses.** A holder that
28
+ * does not state how it clamps takes no tool, offers no grip range and matches
29
+ * no taper. That is the same rule {@link holderTakesTool} applies to a tool
30
+ * with no stated shank, and for the same reason: the unchecked case here is a
31
+ * cutter falling out of a spindle.
32
+ */
33
+ import type { HolderProfile } from './profile.js';
34
+ import type { ProvenanceMap } from './provenance.js';
35
+ import type { Tool } from './tool.js';
36
+ import { type StickoutPolicy, type StickoutRange, type StickoutTool } from './stickout.js';
37
+ import { type ClampingRule } from './clamping.js';
38
+ /**
39
+ * How a holder grips what it holds.
40
+ *
41
+ * `hydraulic` is here rather than folded into `bore` because vendors classify
42
+ * parts as hydraulic outright, and folding it in would be this package
43
+ * reclassifying a family its vendor already named.
44
+ */
45
+ export type Clamping = 'bore' | 'collet' | 'shrink' | 'hydraulic';
46
+ /**
47
+ * What goes in the spindle, as its vendor publishes it.
48
+ *
49
+ * Every field is nullable because a DIN 4000 sheet is not a promise: each is
50
+ * `null` where the vendor states nothing, and nothing is drawn or swept for it
51
+ * then. An absent number is not a zero — a holder with no stated body diameter
52
+ * is not a holder with no body.
53
+ */
54
+ export interface Holder {
55
+ /** The nose, where a holder fouls the part before the tool runs out of reach. */
56
+ readonly noseDiameter: number | null;
57
+ readonly noseLength: number | null;
58
+ /** The body behind the nose, where the vendor states it step by step. */
59
+ readonly bodyDiameter: number | null;
60
+ readonly bodyLength: number | null;
61
+ /** Nose face to flange face, in millimetres. */
62
+ readonly projection: number | null;
63
+ readonly flangeDiameter: number | null;
64
+ /**
65
+ * Spindle face to holder nose, in millimetres.
66
+ *
67
+ * Not the same as reach: what sticks out past the nose is the tool's, and
68
+ * that is the number a feature depth is measured against.
69
+ */
70
+ readonly gaugeLength: number | null;
71
+ /** For a collet holder: which collet series it takes — `ER16`, `PG10`. */
72
+ readonly colletSeries: string | null;
73
+ /**
74
+ * How the holder grips: through a collet, a bore, a shrink fit or hydraulic
75
+ * pressure.
76
+ *
77
+ * A shrink-fit holder and a hydraulic chuck grip the shank directly, the same
78
+ * way a bore does, and they stay apart because the distinction is one a buyer
79
+ * makes — a shrink fit needs an induction heater on the bench and a hydraulic
80
+ * chuck an actuation screw — and because a vendor states which it published.
81
+ */
82
+ readonly clamping?: Clamping;
83
+ /**
84
+ * For a bore, shrink or hydraulic holder: the one shank diameter it takes, in
85
+ * millimetres.
86
+ *
87
+ * **One diameter, not an upper bound.** A shrink-fit holder bored for 12 mm
88
+ * does not hold a 10 mm shank at all, and treating it as a maximum would put
89
+ * a tool in a holder that drops it.
90
+ */
91
+ readonly boreDiameter?: number | null;
92
+ /** The spindle interface — `BT30`. A holder only fits the machine that takes it. */
93
+ readonly taper?: string | null;
94
+ /**
95
+ * How far the seated collet stands proud of the nose face, in millimetres.
96
+ *
97
+ * A powRgrip collet is pressed in and its front protrudes; the tool sees the
98
+ * collet's own diameter for that much before the nose.
99
+ */
100
+ readonly colletProtrusion: number | null;
101
+ readonly provenance?: ProvenanceMap;
102
+ }
103
+ /**
104
+ * Which of the two holder forms this is.
105
+ *
106
+ * On the presence of `points` rather than on a `kind` tag, because a tag would
107
+ * have to be added to {@link Holder} as well and every existing adapter would
108
+ * stop compiling to gain nothing a structural check does not already give.
109
+ */
110
+ export declare const isHolderProfile: (holder: Holder | HolderProfile) => holder is HolderProfile;
111
+ /**
112
+ * What grips the shank inside a collet holder.
113
+ *
114
+ * As with {@link Holder}, this is the gripping and nothing else. `series` has to
115
+ * equal the holder's exactly — a series is a mechanical interface, not a size
116
+ * class, and an `ER16` collet does not go in an `ER20` nose.
117
+ */
118
+ export interface Collet {
119
+ /** `ER16`, `PG10` — must equal the holder's series exactly. */
120
+ readonly series: string;
121
+ /** The shank diameters it grips, in millimetres. */
122
+ readonly clampMin: number;
123
+ readonly clampMax: number;
124
+ /**
125
+ * How much shank the collet actually holds, in millimetres.
126
+ *
127
+ * `null` where the vendor does not publish it, and that absence is load
128
+ * bearing: without it there is no honest maximum stickout, and the answer has
129
+ * to be "nobody has said" rather than an invented grip rule.
130
+ */
131
+ readonly clampLength: number | null;
132
+ readonly provenance?: ProvenanceMap;
133
+ }
134
+ /**
135
+ * A tool, what holds it, and how far it stands out.
136
+ *
137
+ * **The whole reason this package exists is that `stickout` cannot be a bare
138
+ * number a caller worked out on its own.** How far a tool stands out of its
139
+ * holder is a pure function of the tool, the collet and a shop's policy, and
140
+ * before this it had no home: it was computed in four unconnected places, they
141
+ * disagreed by a factor of two on an ordinary tool, and the details table
142
+ * printed one number while the drawing beside it drew another.
143
+ *
144
+ * The arithmetic that resolves it — the clamping rule, the hold share, the
145
+ * collet's cap — is {@link stickoutRange}, and {@link stickoutLimits} is the
146
+ * collet-shaped way into it. A consumer that has already chosen a stickout
147
+ * passes the one it holds; a consumer that has not asks for it here rather than
148
+ * working out a fifth answer of its own.
149
+ */
150
+ export interface Assembly {
151
+ readonly tool: Tool;
152
+ readonly holder: Holder | HolderProfile | null;
153
+ /** Null for a bore or shrink holder, which grips the shank directly, and for no holder at all. */
154
+ readonly collet: Collet | null;
155
+ /**
156
+ * Tool tip to holder nose, in millimetres — the reach of the stack, as set.
157
+ *
158
+ * `null` draws the tool alone and checks nothing against a part: it is "nobody
159
+ * has decided", not zero.
160
+ */
161
+ readonly stickout: number | null;
162
+ }
163
+ /**
164
+ * A collet fits a holder when the holder takes collets of exactly its series.
165
+ *
166
+ * A series is a mechanical interface and not a size class: an `ER16` collet
167
+ * does not go in an `ER20` nose.
168
+ */
169
+ export declare const colletFitsHolder: (collet: Pick<Collet, "series">, holder: Pick<Holder, "clamping" | "colletSeries">) => boolean;
170
+ /** Whether a collet grips a given shank diameter, in millimetres. */
171
+ export declare const gripsShank: (collet: Pick<Collet, "clampMin" | "clampMax">, shank: number) => boolean;
172
+ /**
173
+ * Whether a holder takes this tool's shank, with the collet if it needs one.
174
+ *
175
+ * **A tool whose shank the vendor does not state is refused, not assumed to
176
+ * fit.** This is the one place the domain differs from "what is not stated is
177
+ * not checked", because here the unchecked case is a cutter falling out of a
178
+ * spindle.
179
+ */
180
+ export declare const holderTakesTool: (holder: Pick<Holder, "clamping" | "colletSeries" | "boreDiameter">, collet: Collet | null, tool: Pick<Tool, "geometry">) => boolean;
181
+ /**
182
+ * The furthest a tool can stand out of its holder, in millimetres: overall
183
+ * length less the length that has to stay gripped.
184
+ *
185
+ * `null` when either is unstated. A maximum stickout is exactly the number
186
+ * somebody would use to decide a deep pocket is reachable, and a guessed one is
187
+ * worse than an absent one. A bore or shrink holder's grip length is the
188
+ * holder's rather than a collet's, and this package does not carry it — so
189
+ * those answer `null` too, honestly, until the contract gains it.
190
+ */
191
+ export declare const maxStickout: (tool: Pick<Tool, "geometry">, collet: Pick<Collet, "clampLength"> | null) => number | null;
192
+ export type HoldBand = 'good' | 'medium' | 'bad';
193
+ /**
194
+ * How well the holder has hold of the tool at this stickout, by the share of
195
+ * the overall length left in the holder.
196
+ *
197
+ * At or above `good` is good; between `least` and that is possible but bad;
198
+ * below `least` is not compatible. The thresholds are a shop's, handed in as
199
+ * fractions rather than named here, because they are the same knob
200
+ * {@link StickoutPolicy.heldShare} is and a package must not carry two.
201
+ */
202
+ export declare const holdBand: (tool: Pick<Tool, "geometry">, stickout: number, thresholds: {
203
+ readonly good: number;
204
+ readonly least: number;
205
+ }) => HoldBand | null;
206
+ /**
207
+ * How far this tool may stand out of this holder — the collet-shaped way into
208
+ * {@link stickoutRange}.
209
+ *
210
+ * **The arithmetic is not here.** This was one of the four places that worked
211
+ * out a stickout, and the one that capped at a share of the overall length
212
+ * while the clamping rule capped at a length of shank and neither knew about
213
+ * the other. `stickout.ts` owns the quantity and combines the two knobs in one
214
+ * place; this maps a collet onto the grip length that module asks for, which is
215
+ * all a collet was ever contributing.
216
+ */
217
+ export declare const stickoutLimits: (tool: StickoutTool, collet: Pick<Collet, "clampLength"> | null,
218
+ /** What the holder needs to clear the part, from the sweep: the setup stands out at least this far. */
219
+ required?: number | null, policy?: StickoutPolicy, rule?: ClampingRule) => StickoutRange | null;
220
+ /**
221
+ * The stickout an assembly starts at: the setup length for this tool, held
222
+ * within what the grip allows.
223
+ *
224
+ * A tool whose setup outruns its grip is gripped as short as the grip lets it
225
+ * and no shorter — rather than refused, because the shop is the one who knows
226
+ * whether that is a problem.
227
+ */
228
+ export declare const defaultStickout: (tool: StickoutTool & Pick<Tool, "geometry">, collet: Pick<Collet, "clampLength"> | null) => number | null;
229
+ /**
230
+ * The shank diameters a crib can grip, given what it is asked to hold with.
231
+ *
232
+ * Every rule above reduces to one number — the shank — so "can anything here
233
+ * hold this tool" is a question about a set of diameters. Working that set out
234
+ * once and asking it per tool is what makes holding usable as a filter: asked
235
+ * tool by tool it is holders × collets × tools, which on a real catalog is tens
236
+ * of millions of comparisons per keystroke.
237
+ */
238
+ export interface GripRanges {
239
+ /** Closed intervals a collet grips, in millimetres. */
240
+ readonly spans: ReadonlyArray<readonly [number, number]>;
241
+ /** Exact diameters a bore or shrink holder takes. */
242
+ readonly bores: ReadonlyArray<number>;
243
+ }
244
+ /** `taper` narrows to one spindle interface, `colletSeries` to one collet family; either left out means "any". */
245
+ export declare const gripRanges: (holders: readonly Pick<Holder, "clamping" | "colletSeries" | "boreDiameter" | "taper">[], collets: readonly Collet[], want?: {
246
+ readonly taper?: string | null;
247
+ readonly colletSeries?: string | null;
248
+ }) => GripRanges;
249
+ /**
250
+ * Whether anything in {@link gripRanges} holds this shank, in millimetres.
251
+ *
252
+ * **Through {@link gripsShank} and {@link boreTakesShank}, not a second
253
+ * comparison.** This is the fast filter and `holderTakesTool` is the exact
254
+ * check, and the two must agree: asked strictly, a ⅜" shank that converts to
255
+ * 9.524999999999999 misses a collet whose sheet says 9.525, so the crib reports
256
+ * no holder for a tool the holder plainly takes. That is the failure the
257
+ * tolerance above was introduced for, and it belongs on both paths.
258
+ */
259
+ export declare const gripsAnyShank: (ranges: GripRanges, shank: number) => boolean;
260
+ /**
261
+ * Whether the crib can hold this tool at all.
262
+ *
263
+ * A tool whose shank the vendor does not state is refused, for the same reason
264
+ * {@link holderTakesTool} refuses it.
265
+ */
266
+ export declare const canHold: (ranges: GripRanges, tool: Pick<Tool, "geometry">) => boolean;