@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,25 @@
1
+ /**
2
+ * Where a stated fact came from.
3
+ *
4
+ * Carried per constant by the scraper and kept all the way through, for one
5
+ * reason: a number a shop cannot trace is a number they have to take on faith.
6
+ * A derived or assumed value has to be visibly not the vendor's.
7
+ *
8
+ * The scraper calls this `FactSource` and the drawing calls it `Provenance`;
9
+ * they are the same three strings, and this is the one declaration. `Provenance`
10
+ * is the name kept because it is what the values describe — the vendor's
11
+ * `vendor-stated` is not a *source* in the sense the scraper's transport means
12
+ * one.
13
+ */
14
+ /** Every {@link Provenance}, in decreasing order of how much a shop can lean on it. */
15
+ export declare const PROVENANCE: readonly ["vendor-stated", "derived", "assumed"];
16
+ export type Provenance = (typeof PROVENANCE)[number];
17
+ /**
18
+ * Which values are the vendor's and which this pipeline decided, keyed the same
19
+ * way the record they describe is.
20
+ *
21
+ * A key with no entry is not a claim of `vendor-stated`: it is nobody having
22
+ * said. Defaulting an absent provenance to the vendor's is how a derived number
23
+ * ends up presented as published.
24
+ */
25
+ export type ProvenanceMap = Readonly<Record<string, Provenance>>;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Where a stated fact came from.
3
+ *
4
+ * Carried per constant by the scraper and kept all the way through, for one
5
+ * reason: a number a shop cannot trace is a number they have to take on faith.
6
+ * A derived or assumed value has to be visibly not the vendor's.
7
+ *
8
+ * The scraper calls this `FactSource` and the drawing calls it `Provenance`;
9
+ * they are the same three strings, and this is the one declaration. `Provenance`
10
+ * is the name kept because it is what the values describe — the vendor's
11
+ * `vendor-stated` is not a *source* in the sense the scraper's transport means
12
+ * one.
13
+ */
14
+ /** Every {@link Provenance}, in decreasing order of how much a shop can lean on it. */
15
+ export const PROVENANCE = ['vendor-stated', 'derived', 'assumed'];
@@ -0,0 +1,46 @@
1
+ /**
2
+ * The worst-case material around a feature, and what a stack has to get past.
3
+ *
4
+ * **Declared structurally, on purpose.** The shape is exactly what the Toolpath
5
+ * part contracts call a `ReachCurve`, and naming it here rather than importing
6
+ * it is what keeps this package free of the API's schema: a `ReachCurve` off a
7
+ * report satisfies this by structure, with no adapter, and a consumer that only
8
+ * draws a tool pulls in no OpenAPI contract to do it.
9
+ *
10
+ * That seam is what makes the whole package possible. Reading a curve *off a
11
+ * report* is an adapter and stays with whoever holds the report; reading a
12
+ * curve is arithmetic and belongs here.
13
+ */
14
+ /**
15
+ * Material heights by distance out from the cut, as a staircase.
16
+ *
17
+ * Read as "material within `horizontalOffset[i]` of the cut rises to
18
+ * `verticalOffset[i]`". Both are in millimetres; the offsets run outward from
19
+ * the cutting edge and the heights up from the bottom of the feature.
20
+ *
21
+ * The rise comes at the **start** of each run, so everything out to a knot is
22
+ * already as tall as that knot says. Anything that reads this curve has to
23
+ * agree about that, or a drawn staircase and a clearance verdict describe two
24
+ * different pockets.
25
+ */
26
+ export interface ReachCurve {
27
+ readonly horizontalOffset: readonly number[];
28
+ readonly verticalOffset: readonly number[];
29
+ }
30
+ /**
31
+ * The tallest material within `offset` mm of the cut, above the feature's
32
+ * bottom.
33
+ *
34
+ * **The twin that had nowhere else to go.** One copy decided the verdict — the
35
+ * clearance sweep, with a dozen callers that never draw anything — and the
36
+ * other was in the drawing package, because the gaps an overlay dimensions
37
+ * cannot be measured without it. Neither could depend on the other: putting the
38
+ * verdict behind a rendering package is the thing the whole split exists to
39
+ * avoid, and a drawing may not depend on a catalog's data package. So there
40
+ * were two, and the note beside each said they must agree exactly.
41
+ *
42
+ * The rise comes at the **start** of each run: for an offset between knots the
43
+ * material could be anywhere out to the next knot, so the next knot's height is
44
+ * the bound. Past the last knot the curve clamps.
45
+ */
46
+ export declare const heightAt: (curve: ReachCurve, offset: number) => number;
package/dist/reach.js ADDED
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The worst-case material around a feature, and what a stack has to get past.
3
+ *
4
+ * **Declared structurally, on purpose.** The shape is exactly what the Toolpath
5
+ * part contracts call a `ReachCurve`, and naming it here rather than importing
6
+ * it is what keeps this package free of the API's schema: a `ReachCurve` off a
7
+ * report satisfies this by structure, with no adapter, and a consumer that only
8
+ * draws a tool pulls in no OpenAPI contract to do it.
9
+ *
10
+ * That seam is what makes the whole package possible. Reading a curve *off a
11
+ * report* is an adapter and stays with whoever holds the report; reading a
12
+ * curve is arithmetic and belongs here.
13
+ */
14
+ /**
15
+ * The tallest material within `offset` mm of the cut, above the feature's
16
+ * bottom.
17
+ *
18
+ * **The twin that had nowhere else to go.** One copy decided the verdict — the
19
+ * clearance sweep, with a dozen callers that never draw anything — and the
20
+ * other was in the drawing package, because the gaps an overlay dimensions
21
+ * cannot be measured without it. Neither could depend on the other: putting the
22
+ * verdict behind a rendering package is the thing the whole split exists to
23
+ * avoid, and a drawing may not depend on a catalog's data package. So there
24
+ * were two, and the note beside each said they must agree exactly.
25
+ *
26
+ * The rise comes at the **start** of each run: for an offset between knots the
27
+ * material could be anywhere out to the next knot, so the next knot's height is
28
+ * the bound. Past the last knot the curve clamps.
29
+ */
30
+ export const heightAt = (curve, offset) => {
31
+ for (let index = 0; index < curve.horizontalOffset.length; index += 1) {
32
+ if ((curve.horizontalOffset[index] ?? 0) >= offset) {
33
+ return curve.verticalOffset[index] ?? 0;
34
+ }
35
+ }
36
+ return curve.verticalOffset[curve.verticalOffset.length - 1] ?? 0;
37
+ };
@@ -0,0 +1,67 @@
1
+ import { type ReachCurve } from './reach.js';
2
+ /**
3
+ * The feature in section, with the tool in it.
4
+ *
5
+ * A cross-section through the feature at its **worst-case place** — the
6
+ * tightest spot the tool has to reach — drawn the way a machinist sketches a
7
+ * setup: the tool's tip on the feature's bottom, its cutting edge against the
8
+ * wall, the wall up to the feature's top, and the part above and beyond it
9
+ * standing as tall as the reach curve says it can. A drill sits on the hole's
10
+ * axis instead, because a drill does not hug a wall.
11
+ *
12
+ * Everything is from the datasheet: the depth from `zMax − zMin`, the width
13
+ * from the tightest clearance (`cd.ignore.min`) or a hole's diameter, the
14
+ * fillet from `filletRadius`, a hole's bottom from `fullConeDeg`, the top of
15
+ * the part from the highest feature cut the same way, and the material
16
+ * beyond the wall from the reach curve — the same staircase the sweep walks,
17
+ * so what the drawing shows the holder clearing is what the check cleared.
18
+ *
19
+ * Coordinates are millimetres in the drawing's own frame: `x` across from the
20
+ * tool's axis, `z` up from the feature's bottom.
21
+ */
22
+ export type SectionKind = 'pocket' | 'hole' | 'wall' | 'face';
23
+ export interface FeatureSection {
24
+ readonly kind: SectionKind;
25
+ /** The feature's own height, `zMax − zMin`. */
26
+ readonly depth: number;
27
+ /** False for a through feature: nothing is drawn under the bottom. */
28
+ readonly hasFloor: boolean;
29
+ /** Across the tightest place, or a hole's diameter; null where the datasheet does not say. */
30
+ readonly width: number | null;
31
+ /** The floor fillet, 0 for a sharp corner. */
32
+ readonly filletRadius: number;
33
+ /** A hole's bottom cone, full angle; 180 or null is flat. */
34
+ readonly coneDeg: number | null;
35
+ /** The top of the part above the feature's bottom, which is what the holder must clear. */
36
+ readonly topAbove: number;
37
+ readonly curve: ReachCurve | null;
38
+ }
39
+ export interface SectionPoint {
40
+ readonly x: number;
41
+ readonly z: number;
42
+ }
43
+ export interface Section {
44
+ /** Closed polygons of material, to hatch. */
45
+ readonly material: ReadonlyArray<ReadonlyArray<SectionPoint>>;
46
+ /** The surface the tip sits on, across, or null for a through feature. */
47
+ readonly floor: {
48
+ readonly from: number;
49
+ readonly to: number;
50
+ } | null;
51
+ readonly leftWall: number | null;
52
+ readonly rightWall: number | null;
53
+ readonly extent: {
54
+ readonly left: number;
55
+ readonly right: number;
56
+ readonly top: number;
57
+ readonly bottom: number;
58
+ };
59
+ }
60
+ /** Stock drawn under a floor, mm: enough to read as solid, not a claim about thickness. */
61
+ export declare const FLOOR_BAND = 6;
62
+ /** Part drawn beyond the last thing the datasheet states, mm. */
63
+ export declare const REACH = 12;
64
+ export declare const sectionOutline: (section: FeatureSection, tool: {
65
+ readonly diameter: number;
66
+ readonly form: string;
67
+ }) => Section;
@@ -0,0 +1,185 @@
1
+ import { heightAt } from './reach.js';
2
+ /** Forms that sit on a hole's axis rather than against its wall. */
3
+ const ON_AXIS = new Set([
4
+ 'drill',
5
+ 'spot drill',
6
+ 'center drill',
7
+ 'reamer',
8
+ 'counter sink',
9
+ 'counter bore',
10
+ 'boring bar',
11
+ 'tap right hand',
12
+ 'tap left hand',
13
+ 'thread mill',
14
+ ]);
15
+ /** Stock drawn under a floor, mm: enough to read as solid, not a claim about thickness. */
16
+ export const FLOOR_BAND = 6;
17
+ /** Part drawn beyond the last thing the datasheet states, mm. */
18
+ export const REACH = 12;
19
+ const exact = (value) => Math.round(value * 1e6) / 1e6;
20
+ /** A quarter arc of a floor fillet, from the floor tangent up to the wall tangent. */
21
+ const filletArc = (corner, radius, side, steps = 6) => Array.from({ length: steps + 1 }, (_, index) => {
22
+ // From straight down (on the floor, one radius in from the wall) round to
23
+ // straight across (on the wall, one radius up).
24
+ const angle = (-90 + (90 * index) / steps) * (Math.PI / 180);
25
+ const centre = { x: corner.x + side * radius, z: corner.z + radius };
26
+ return {
27
+ x: exact(centre.x - side * radius * Math.cos(angle)),
28
+ z: exact(centre.z + radius * Math.sin(angle)),
29
+ };
30
+ });
31
+ /**
32
+ * The material beyond one wall, as a staircase from the tool's edge outward.
33
+ *
34
+ * `fromEdge` is how far the wall stands from the tool's cutting edge; the
35
+ * curve's offsets are from that edge. Heights are the sweep's own reading —
36
+ * every offset up to a knot is as tall as the knot says — so the staircase
37
+ * rises at the start of each run, exactly as `materialProfile` draws it.
38
+ * Returns (distance beyond the wall, height) pairs, ending `reach` past the
39
+ * last knot.
40
+ */
41
+ const staircase = (curve, fromEdge, floorAt, topAbove, reach) => {
42
+ if (!curve || curve.horizontalOffset.length === 0) {
43
+ return [
44
+ { s: 0, z: Math.max(floorAt, topAbove) },
45
+ { s: reach, z: Math.max(floorAt, topAbove) },
46
+ ];
47
+ }
48
+ const out = [];
49
+ let from = fromEdge;
50
+ curve.horizontalOffset.forEach((offset, index) => {
51
+ if (offset < fromEdge) {
52
+ return;
53
+ }
54
+ const height = Math.max(floorAt, curve.verticalOffset[index] ?? 0);
55
+ out.push({ s: from - fromEdge, z: height }, { s: offset - fromEdge, z: height });
56
+ from = offset;
57
+ });
58
+ const last = out[out.length - 1];
59
+ const height = last ? last.z : Math.max(floorAt, heightAt(curve, fromEdge));
60
+ if (!last) {
61
+ out.push({ s: 0, z: height });
62
+ }
63
+ out.push({ s: (last?.s ?? 0) + reach, z: height });
64
+ return out;
65
+ };
66
+ /**
67
+ * One wall and the material behind it, as a closed polygon.
68
+ *
69
+ * `side` is −1 for the wall on the tool's left, +1 on its right. From the
70
+ * floor tangent of the fillet, round the fillet, up the wall to the feature's
71
+ * top, then outward along the staircase, then down to the bottom of the
72
+ * drawing and back under the fillet.
73
+ */
74
+ const wallPolygon = (wallX, side, section, toolEdge, bottom, floorAt) => {
75
+ const r = Math.max(0, Math.min(section.filletRadius, section.depth));
76
+ const corner = { x: wallX, z: floorAt };
77
+ const points = [];
78
+ if (r > 0) {
79
+ points.push(...filletArc(corner, r, side === -1 ? 1 : -1, 6));
80
+ }
81
+ else {
82
+ points.push(corner);
83
+ }
84
+ points.push({ x: wallX, z: floorAt + section.depth });
85
+ const fromEdge = Math.abs(wallX - toolEdge);
86
+ for (const step of staircase(section.curve, fromEdge, floorAt + section.depth, section.topAbove, REACH)) {
87
+ points.push({ x: exact(wallX + side * step.s), z: exact(step.z) });
88
+ }
89
+ const far = points[points.length - 1].x;
90
+ points.push({ x: far, z: bottom });
91
+ points.push({ x: exact(wallX + side * -1 * r), z: bottom });
92
+ return points;
93
+ };
94
+ export const sectionOutline = (section, tool) => {
95
+ const R = tool.diameter / 2;
96
+ const bottom = section.hasFloor ? -FLOOR_BAND : 0;
97
+ const material = [];
98
+ if (section.kind === 'face') {
99
+ // The feature is the floor: stock under it across the whole drawing, and
100
+ // whatever stands around it either side, from the reach curve.
101
+ const walls = [-1, 1].map((side) => {
102
+ const rise = staircase(section.curve, 0, 0, section.topAbove, REACH);
103
+ const points = rise.map((step) => ({ x: exact(side * (R + step.s)), z: exact(step.z) }));
104
+ return { side, points, far: points[points.length - 1].x };
105
+ });
106
+ const left = walls[0].far;
107
+ const right = walls[1].far;
108
+ material.push([
109
+ { x: left, z: 0 },
110
+ { x: right, z: 0 },
111
+ { x: right, z: bottom },
112
+ { x: left, z: bottom },
113
+ ]);
114
+ for (const wall of walls) {
115
+ if (wall.points.some((point) => point.z > 0)) {
116
+ material.push([{ x: exact(wall.side * R), z: 0 }, ...wall.points, { x: wall.far, z: 0 }]);
117
+ }
118
+ }
119
+ return {
120
+ material,
121
+ floor: { from: left, to: right },
122
+ leftWall: null,
123
+ rightWall: null,
124
+ extent: {
125
+ left,
126
+ right,
127
+ top: Math.max(0, ...walls.flatMap((w) => w.points.map((p) => p.z))),
128
+ bottom,
129
+ },
130
+ };
131
+ }
132
+ const onAxis = section.kind === 'hole' && ON_AXIS.has(tool.form) && section.width !== null;
133
+ const leftWall = onAxis ? -(section.width ?? tool.diameter) / 2 : -R;
134
+ const width = section.width === null ? null : Math.max(section.width, tool.diameter);
135
+ const rightWall = section.kind === 'wall' || width === null ? null : onAxis ? width / 2 : leftWall + width;
136
+ // A drilled bottom: the walls stand on the cone's rim, the tip in its apex.
137
+ const coneRise = section.kind === 'hole' &&
138
+ section.hasFloor &&
139
+ section.coneDeg !== null &&
140
+ section.coneDeg < 180 &&
141
+ section.filletRadius <= 0
142
+ ? Math.abs(leftWall) / Math.tan(((section.coneDeg / 2) * Math.PI) / 180)
143
+ : 0;
144
+ const left = wallPolygon(leftWall, -1, section, -R, bottom, coneRise);
145
+ material.push(left);
146
+ const leftFar = left.find((p) => p.z === bottom).x;
147
+ let rightFar = leftWall + Math.max(width ?? 0, tool.diameter) + REACH * 2;
148
+ if (rightWall !== null) {
149
+ const right = wallPolygon(rightWall, 1, section, R, bottom, coneRise);
150
+ material.push(right);
151
+ rightFar = right.find((p) => p.z === bottom).x;
152
+ }
153
+ let floor = null;
154
+ if (section.hasFloor) {
155
+ const r = Math.max(0, Math.min(section.filletRadius, section.depth));
156
+ const from = leftWall + r;
157
+ const to = rightWall === null ? rightFar : rightWall - r;
158
+ if (coneRise > 0 && rightWall !== null) {
159
+ material.push([
160
+ { x: leftWall, z: exact(coneRise) },
161
+ { x: 0, z: 0 },
162
+ { x: rightWall, z: exact(coneRise) },
163
+ { x: rightWall, z: bottom },
164
+ { x: leftWall, z: bottom },
165
+ ]);
166
+ }
167
+ else {
168
+ material.push([
169
+ { x: from, z: 0 },
170
+ { x: to, z: 0 },
171
+ { x: to, z: bottom },
172
+ { x: from, z: bottom },
173
+ ]);
174
+ }
175
+ floor = { from, to };
176
+ }
177
+ const top = Math.max(...material.flatMap((polygon) => polygon.map((p) => p.z)));
178
+ return {
179
+ material,
180
+ floor,
181
+ leftWall,
182
+ rightWall,
183
+ extent: { left: leftFar, right: rightFar, top, bottom },
184
+ };
185
+ };
@@ -0,0 +1,187 @@
1
+ /**
2
+ * How far a tool stands out of whatever holds it — the one answer.
3
+ *
4
+ * **There used to be four.** The same question was worked out in four
5
+ * unconnected places, and they disagreed by a factor of two on an ordinary
6
+ * tool:
7
+ *
8
+ * | where | what it computed | on a ⌀1 in end mill, `OAL` 5, `LCF` 1.25, `SFDM` 1 |
9
+ * | --------------------------------- | ----------------------------- | -------------------------------------------------- |
10
+ * | the clamping rule → `geometry.LBH` | `OAL − 3×SFDM` | 2.000 in |
11
+ * | the holding module's `max` | `OAL − OAL×heldShare` | 3.333 in |
12
+ * | its `default` → the drawing | flutes, floored and stepped | 1.250 in |
13
+ * | the hole-mode reach check | read `geometry.LBH` as a ceiling | 2.000 in |
14
+ *
15
+ * The details table printed the first and the drawing beside it drew the third,
16
+ * so a dimension line for `LBH` ran up past the holder nose and into the holder
17
+ * body. Neither of the first two consulted the other, and the two knobs behind
18
+ * them — a minimum clamping length (a length of **shank**) and a good hold (a
19
+ * share of the **overall length**) — were combined nowhere.
20
+ *
21
+ * **That bug is the single strongest argument for this package existing.** It
22
+ * was fixed inside one application, which left the next consumer of a tool
23
+ * catalog and a tool drawing to reproduce it from scratch: the quantity is a
24
+ * pure function of the tool, the collet and a shop's policy, and it had no home
25
+ * until this one.
26
+ *
27
+ * So this module owns the quantity outright and every other number is this same
28
+ * function with more arguments:
29
+ *
30
+ * ```
31
+ * geometry.LBH ≡ stickoutRange(tool).setup — no holder, no feature
32
+ * Assembly.stickout ≡ stickoutRange(tool, { grip, required }).setup
33
+ * the ceiling ≡ stickoutRange(tool, …).max
34
+ * ```
35
+ *
36
+ * `min ≤ setup ≤ max` holds by construction, so a drawn stickout can never
37
+ * exceed the length a table prints beside it. That invariant is a test rather
38
+ * than this sentence.
39
+ *
40
+ * **`LBH` is the setup length, not the ceiling.** The "below holder" column
41
+ * answers what a machinist would set the tool up at; the most it *could* stand
42
+ * out is {@link StickoutRange.max}, which is checked and reported but is not
43
+ * the column. What makes that reading workable is that the floor and the step
44
+ * reach it — {@link DEFAULT_STICKOUT_POLICY} carries `least` and `step`, and an
45
+ * earlier default carried zero for both and so produced the bare flute length.
46
+ */
47
+ import { type ClampingRule } from './clamping.js';
48
+ import type { Geometry } from './geometry.js';
49
+ import type { UnitSystem } from './units.js';
50
+ /**
51
+ * What this module needs of a tool: its geometry, and which unit system its
52
+ * step is counted in.
53
+ *
54
+ * Deliberately narrower than a whole {@link Tool} so a build can ask before a
55
+ * tool is finished being built, and so nothing here can reach for a catalog
56
+ * number or a vendor.
57
+ */
58
+ export interface StickoutTool {
59
+ readonly geometry: Geometry;
60
+ readonly unitSystem: UnitSystem;
61
+ }
62
+ /**
63
+ * The share of a tool's overall length a holder must always have hold of.
64
+ *
65
+ * A third. **A shop's figure, not a vendor's** — no vendor in the scraped
66
+ * catalog publishes a minimum engagement — which is why it is named here and
67
+ * every control that shows it should say whose it is. Deliberately a share of
68
+ * the length and not a multiple of the shank diameter: how much of a tool a
69
+ * collet needs is about the tool's leverage, not its shank. That other reading
70
+ * is {@link ClampingRule}, and both are honoured — see {@link StickoutLimit}.
71
+ */
72
+ export declare const HELD_SHARE: number;
73
+ /**
74
+ * How the setup stickout is set.
75
+ *
76
+ * Nobody sets a tool up 6 mm out, so the stickout stands out at least `least`
77
+ * (half an inch) where the tool's length allows, and lands on a round number —
78
+ * the `step` for the tool's unit system (an eighth of an inch, or 3 mm) nearest
79
+ * what the holder needs, never under it.
80
+ */
81
+ export interface StickoutPolicy {
82
+ /** Share of the overall length that must stay in the holder. */
83
+ readonly heldShare: number;
84
+ /** The shortest stickout worth setting up, mm; zero for none. */
85
+ readonly least: number;
86
+ /**
87
+ * The increment the setup lands on, mm, by the tool's unit system; zero for
88
+ * none.
89
+ *
90
+ * Keyed by {@link UnitSystem} rather than by a pair of its own, so a policy
91
+ * cannot name a system this package does not have.
92
+ */
93
+ readonly step: Readonly<Record<UnitSystem, number>>;
94
+ }
95
+ /** What a dataset is built with, and what a page starts at. */
96
+ export declare const DEFAULT_STICKOUT_POLICY: StickoutPolicy;
97
+ /**
98
+ * Which rule set the ceiling, so a control can say why rather than showing a
99
+ * number nobody can trace.
100
+ *
101
+ * `clamp` is the shop's clamping length, or the vendor's own `LSCN`; `hold` is
102
+ * {@link HELD_SHARE}; `collet` is the collet's published grip. They used to be
103
+ * a ceiling each in a different file; here they are three caps and the tightest
104
+ * wins.
105
+ */
106
+ export type StickoutLimit = 'clamp' | 'hold' | 'collet';
107
+ export interface StickoutRange {
108
+ /** Shortest, mm: the flutes out of the collet, or the neck where there is one. */
109
+ readonly min: number;
110
+ /**
111
+ * The length to set the tool up at, mm: the least that works for this
112
+ * feature, floored and stepped by the policy, held under {@link max}.
113
+ *
114
+ * This is `geometry.LBH` when asked with no holder and no feature, and an
115
+ * assembly's stickout when asked with both.
116
+ */
117
+ readonly setup: number;
118
+ /**
119
+ * Longest, mm: the tightest of the three caps, and never under {@link min} —
120
+ * a tool that cannot meet the rule at any depth is gripped as short as the
121
+ * grip allows and {@link gripShort} says so. Null where the tool states no
122
+ * overall length, which is an unbounded range rather than a bound of nothing.
123
+ */
124
+ readonly max: number | null;
125
+ /** Which cap {@link max} came from, or null where nothing capped it. */
126
+ readonly limitedBy: StickoutLimit | null;
127
+ /** The parallel shank behind {@link min}, mm: all a holder can ever grip. */
128
+ readonly grip: number | null;
129
+ /** How much of the tool the tightest cap asks to keep in the holder, mm. */
130
+ readonly wantedGrip: number | null;
131
+ /**
132
+ * True when the rule cannot be met at any depth: the range collapses onto
133
+ * {@link min}, and a control should say why rather than refuse.
134
+ */
135
+ readonly gripShort: boolean;
136
+ }
137
+ export interface StickoutRequest {
138
+ /**
139
+ * How much shank the holder actually grips, mm — a collet's published grip
140
+ * length. Null where the vendor does not publish one, which REGO-FIX's
141
+ * powRgrip line does not, and null for a bore or shrink holder, whose grip
142
+ * this package does not carry.
143
+ *
144
+ * **A length rather than a {@link Collet}**, so this module depends on
145
+ * nothing in `holding.ts` and the two cannot form a cycle. `stickoutLimits`
146
+ * there is the collet-shaped way in.
147
+ */
148
+ readonly grip?: number | null;
149
+ /** What the holder needs to clear the part, mm, from the sweep. */
150
+ readonly required?: number | null;
151
+ /** What the shop keeps clamped. */
152
+ readonly rule?: ClampingRule;
153
+ /** The floor, step and hold share. */
154
+ readonly policy?: StickoutPolicy;
155
+ }
156
+ /**
157
+ * The least a tool can stand out: its flutes, or its neck where it has one.
158
+ *
159
+ * The collet face sits at the end of the flutes, and a stated neck — which a
160
+ * collet must not close on — pushes it back to the shoulder. A tool that states
161
+ * no flute length has no known head, so it has no known stickout at all and
162
+ * this answers `null`; it carries no `LBH` either, rather than one derived from
163
+ * `OAL` and `SFDM` alone.
164
+ */
165
+ export declare const minStickout: (tool: StickoutTool) => number | null;
166
+ /**
167
+ * Every stickout this tool has, in one answer.
168
+ *
169
+ * `null` only when the tool states no flute length, because then nothing about
170
+ * where it stands out of a holder can be worked out at all.
171
+ */
172
+ export declare const stickoutRange: (tool: StickoutTool, request?: StickoutRequest) => StickoutRange | null;
173
+ /**
174
+ * What this tool would be set up at on its own: no holder chosen and no feature
175
+ * to reach. This is `geometry.LBH`, and a build writes it with exactly this
176
+ * call.
177
+ */
178
+ export declare const setupStickout: (tool: StickoutTool, rule?: ClampingRule, policy?: StickoutPolicy) => number | null;
179
+ /**
180
+ * The furthest this tool can ever stand out of a holder, mm.
181
+ *
182
+ * **A reach check's number, not `LBH`.** A tap that will not reach the bottom of
183
+ * a hole at its setup length may reach it pulled further out, and asking `LBH` —
184
+ * which is the setup — would refuse it. Anything asking "could this tool get
185
+ * down there at all" asks this.
186
+ */
187
+ export declare const stickoutCeiling: (tool: StickoutTool, rule?: ClampingRule, policy?: StickoutPolicy) => number | null;