@toolpath/tool-scraper 2.1.0 → 2.3.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/dist/conventions.d.ts +48 -2
- package/dist/conventions.js +41 -0
- package/dist/family.d.ts +16 -2
- package/dist/holding.d.ts +396 -0
- package/dist/holding.js +360 -0
- package/dist/index.d.ts +23 -13
- package/dist/index.js +23 -13
- package/dist/measure.d.ts +14 -10
- package/dist/measure.js +14 -13
- package/dist/node/cad-mirror.d.ts +58 -1
- package/dist/node/cad-mirror.js +56 -8
- package/dist/node/cli.d.ts +4 -1
- package/dist/node/cli.js +192 -18
- package/dist/node/holder-import.d.ts +223 -0
- package/dist/node/holder-import.js +379 -0
- package/dist/node/index.d.ts +1 -0
- package/dist/node/index.js +1 -0
- package/dist/node/paths.d.ts +16 -0
- package/dist/node/paths.js +20 -0
- package/dist/profiles.d.ts +275 -0
- package/dist/profiles.js +295 -0
- package/dist/provenance.d.ts +9 -1
- package/dist/provenance.js +9 -1
- package/dist/records.d.ts +60 -20
- package/dist/records.js +31 -19
- package/dist/registry.d.ts +61 -5
- package/dist/registry.js +109 -6
- package/dist/vendors/kennametal/holding.d.ts +35 -0
- package/dist/vendors/kennametal/holding.js +112 -0
- package/dist/vendors/kennametal/index.d.ts +1 -0
- package/dist/vendors/kennametal/index.js +1 -0
- package/dist/vendors/maritool/holding.d.ts +79 -0
- package/dist/vendors/maritool/holding.js +164 -0
- package/dist/vendors/maritool/index.d.ts +1 -0
- package/dist/vendors/maritool/index.js +1 -0
- package/dist/vendors/maritool/scrape.d.ts +37 -13
- package/dist/vendors/maritool/scrape.js +53 -14
- package/dist/vendors/regofix/holding.d.ts +35 -0
- package/dist/vendors/regofix/holding.js +108 -0
- package/dist/vendors/regofix/index.d.ts +1 -0
- package/dist/vendors/regofix/index.js +1 -0
- package/dist/vendors/regofix/scrape.js +2 -2
- package/package.json +3 -2
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A measured holder envelope, on the gage line, as the drawing wants it.
|
|
3
|
+
*
|
|
4
|
+
* `holding.ts` mints a holder from what the vendor *publishes*. This is the
|
|
5
|
+
* other half: what the holder's own CAD model *measures*, reduced to the
|
|
6
|
+
* silhouette a 2D elevation draws and cross-checked against the published gage
|
|
7
|
+
* length. The measuring is the Toolpath Engine API's — `node/holder-import.ts`
|
|
8
|
+
* makes the calls — and everything here is pure, so the 0/1/2/N cases are
|
|
9
|
+
* testable against literals rather than against a stack that has to be running.
|
|
10
|
+
*
|
|
11
|
+
* ## What replaced 1,800 lines of Python and a GUI
|
|
12
|
+
*
|
|
13
|
+
* The reference implementation sampled each STEP solid from inside a live
|
|
14
|
+
* Fusion 360 process over an MCP bridge, then did its own z-binned envelope,
|
|
15
|
+
* outward-only Douglas–Peucker, undercut fill and 7:24 taper solve. The API
|
|
16
|
+
* returns all of it — the layer stack, the gauge length, the size class and the
|
|
17
|
+
* taper family — so what is left here is a change of datum and a cross-check.
|
|
18
|
+
*
|
|
19
|
+
* The two agree. Four holders re-measured through the API against the profiles
|
|
20
|
+
* Fusion produced for the same STEP files, at the same 0.05 mm tolerance:
|
|
21
|
+
* `BT30ER16060M` 60 vs 60.000003 (2.8 nm), `BT30HC14100M` 100.355 vs
|
|
22
|
+
* 100.354999, `BT30HPVTT038295` 75 vs 75.000000, `BTKV30ER16100M` 89.4 vs
|
|
23
|
+
* 89.400000, with the total envelope height matching to three decimals on all
|
|
24
|
+
* four. The API decimates finer — about twice the layers at the same tolerance
|
|
25
|
+
* — which costs a drawing segments and changes no dimension.
|
|
26
|
+
*
|
|
27
|
+
* ## Its own document, keyed by guid
|
|
28
|
+
*
|
|
29
|
+
* A profile is ~110 points that only an assembly drawing needs, and a catalog
|
|
30
|
+
* is loaded by every page. So this is a second document that ships beside the
|
|
31
|
+
* records and is read lazily, rather than a field on {@link HolderRecord}.
|
|
32
|
+
* Keyed by guid because that is the one identifier that survives a re-scrape.
|
|
33
|
+
*
|
|
34
|
+
* **Collets get no profile.** They publish no CAD model and are not drawn: a
|
|
35
|
+
* collet sits inside the nut, which the holder's own envelope already includes.
|
|
36
|
+
*/
|
|
37
|
+
import type { ProfileDatum, ProfilePoint } from '@toolpath/tool-support';
|
|
38
|
+
import type { HolderRecord } from './holding.js';
|
|
39
|
+
import type { BrandName } from './identity.js';
|
|
40
|
+
/** Bumped when {@link ProfilesDocument}'s shape changes in a way a consumer must handle. */
|
|
41
|
+
export declare const PROFILES_VERSION = 1;
|
|
42
|
+
/**
|
|
43
|
+
* How far a measured gage length may sit from the vendor's published one before
|
|
44
|
+
* the profile is called incomplete, in millimetres.
|
|
45
|
+
*
|
|
46
|
+
* **Both bounds come from the data rather than from feel.** The largest
|
|
47
|
+
* *explained* deviation across the 53-holder Kennametal corpus is 0.355 mm —
|
|
48
|
+
* the nose lip on `BT30HC14100M`, real material past the face the vendor
|
|
49
|
+
* measures `L1` to, reproduced by the API to 1.5 nm. The smallest *real*
|
|
50
|
+
* shortfall is 10.6 mm — the collet nut the five BTKV30 STEP models omit, whose
|
|
51
|
+
* bodies stop at the threaded nose. That is a 30x gap, and this sits 2.8x above
|
|
52
|
+
* the first and 10.6x below the second. A family that lands in between is a
|
|
53
|
+
* finding to investigate, not a number to widen.
|
|
54
|
+
*/
|
|
55
|
+
export declare const GAUGE_TOLERANCE_MM = 1;
|
|
56
|
+
/** Which interface a {@link MeasuredHolder.sizeClass} belongs to. */
|
|
57
|
+
export type TaperFamily = 'iso7x24' | 'hsk';
|
|
58
|
+
/** Every {@link TaperFamily}, for a message that can list what it knows. */
|
|
59
|
+
export declare const TAPER_FAMILIES: readonly TaperFamily[];
|
|
60
|
+
/**
|
|
61
|
+
* The 7:24 designations this package can read a size out of.
|
|
62
|
+
*
|
|
63
|
+
* A table rather than "letters then digits", for the reason
|
|
64
|
+
* `conventions.IDENTITY_DEVIATIONS` is one: an unknown prefix must be a line
|
|
65
|
+
* somebody added on evidence, not a regex that quietly accepted it. These are
|
|
66
|
+
* the two the scraped catalog states — Kennametal and REGO-FIX declare `BT30`
|
|
67
|
+
* throughout, MariTool states `BT30`, `BT40`, `CAT40` and `CAT50` per part —
|
|
68
|
+
* and a vendor publishing `SK`, `CV` or `ISO` adds its own entry here.
|
|
69
|
+
*
|
|
70
|
+
* **`BTKV` is deliberately absent.** `families/kennametal.ts` records why: a
|
|
71
|
+
* BTKV30 is the same JIS B 6339 cone as a BT30 and differs by seating on the
|
|
72
|
+
* flange face as well, which is `HolderRecord.contact`, so those families
|
|
73
|
+
* declare `BT30` and the distinction stays on the axis that carries it.
|
|
74
|
+
*/
|
|
75
|
+
export declare const TAPER_PREFIXES: readonly string[];
|
|
76
|
+
/** A cone of the measured stack, in millimetres, as the API returns one. */
|
|
77
|
+
export interface HolderLayer {
|
|
78
|
+
/** Layer height. */
|
|
79
|
+
readonly thickness: number;
|
|
80
|
+
/** Diameter at the end nearer the nose. */
|
|
81
|
+
readonly bottomDiameter: number;
|
|
82
|
+
/** Diameter at the end nearer the spindle. */
|
|
83
|
+
readonly topDiameter: number;
|
|
84
|
+
}
|
|
85
|
+
/** The import options a measurement was produced with, echoed by the API. */
|
|
86
|
+
export interface ImportOptions {
|
|
87
|
+
/** Simplification tolerance, in mm of radius. */
|
|
88
|
+
readonly tolerance: number;
|
|
89
|
+
/** Whether enclosed bays — a V-flange groove, a thread relief — were raised to their brims. */
|
|
90
|
+
readonly fillBays: boolean;
|
|
91
|
+
/** Whether the holder was turned end for end after the automatic orientation. */
|
|
92
|
+
readonly flipped: boolean;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* One holder as the API measured it, plus the two things the API cannot know.
|
|
96
|
+
*
|
|
97
|
+
* `brand` and `catalogNumber` are the caller's: the API is handed a STEP file
|
|
98
|
+
* and hands back geometry, and which part that file is came from the filename
|
|
99
|
+
* `node/cad-mirror.ts` wrote it under.
|
|
100
|
+
*/
|
|
101
|
+
export interface MeasuredHolder {
|
|
102
|
+
readonly brand: BrandName;
|
|
103
|
+
readonly catalogNumber: string;
|
|
104
|
+
/** The envelope as a stack of cones, **nose first**. Always complete, taper included. */
|
|
105
|
+
readonly layers: readonly HolderLayer[];
|
|
106
|
+
/**
|
|
107
|
+
* Millimetres from the bottom of the stack to the gauge plane, or null.
|
|
108
|
+
*
|
|
109
|
+
* **Null is not zero.** A straight shank or a Capto carries no cone to place
|
|
110
|
+
* a gauge plane on, and a profile measured off one has no gage-line datum to
|
|
111
|
+
* be stated in.
|
|
112
|
+
*/
|
|
113
|
+
readonly gaugeLength: number | null;
|
|
114
|
+
/** 30/40/50 for a 7:24, 25–160 for an HSK, or null where no taper was found. */
|
|
115
|
+
readonly sizeClass: number | null;
|
|
116
|
+
readonly taperFamily: TaperFamily | null;
|
|
117
|
+
/** What pins the numbers to a run. */
|
|
118
|
+
readonly kernelVersion: string;
|
|
119
|
+
readonly options: ImportOptions;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* One vertex of a silhouette: `[z, r]`, both in millimetres.
|
|
123
|
+
*
|
|
124
|
+
* `@toolpath/tool-support`'s, re-exported under this package's own name.
|
|
125
|
+
*/
|
|
126
|
+
export type { ProfilePoint } from '@toolpath/tool-support';
|
|
127
|
+
/**
|
|
128
|
+
* What `z = 0` means on a profile.
|
|
129
|
+
*
|
|
130
|
+
* `gage-line` is the datum everything downstream assumes — the plane the
|
|
131
|
+
* spindle measures stickout from, with `z` increasing toward the cutting end,
|
|
132
|
+
* so the taper is negative and the nose positive. `nose` is the fallback where
|
|
133
|
+
* the API found no gauge plane to solve, and it is stated rather than silently
|
|
134
|
+
* referenced to an arbitrary end.
|
|
135
|
+
*
|
|
136
|
+
* **Per profile rather than per document**, which is where the reference
|
|
137
|
+
* implementation put it. One `datum` over a batch is only true while every
|
|
138
|
+
* holder in it has a taper, and the first Capto or straight-shank holder makes
|
|
139
|
+
* the document's own header wrong about some of its entries.
|
|
140
|
+
*
|
|
141
|
+
* `@toolpath/tool-support`'s, re-exported under this package's own name. It was
|
|
142
|
+
* declared here, in the drawing package, and in the application between them —
|
|
143
|
+
* three copies of two strings, one of which decides whether a consumer may
|
|
144
|
+
* print a gauge length at all.
|
|
145
|
+
*/
|
|
146
|
+
export type { ProfileDatum } from '@toolpath/tool-support';
|
|
147
|
+
/** One holder's measured silhouette, and how far it agrees with the vendor. */
|
|
148
|
+
export interface HolderProfile {
|
|
149
|
+
readonly catalogNumber: string;
|
|
150
|
+
readonly datum: ProfileDatum;
|
|
151
|
+
/** The silhouette, `z` ascending. Two points share a `z` where the solid steps. */
|
|
152
|
+
readonly points: readonly ProfilePoint[];
|
|
153
|
+
/** Where the API put the gauge plane, in mm from the nose, or null. */
|
|
154
|
+
readonly gaugeLengthSolved: number | null;
|
|
155
|
+
/** The vendor's own `L1`, in mm — `HolderRecord.gaugeLengthMm`. */
|
|
156
|
+
readonly gaugeLengthPublished: number;
|
|
157
|
+
readonly sizeClass: number | null;
|
|
158
|
+
readonly taperFamily: TaperFamily | null;
|
|
159
|
+
/** Whether the two gage lengths agree to within {@link GAUGE_TOLERANCE_MM}. */
|
|
160
|
+
readonly complete: boolean;
|
|
161
|
+
/**
|
|
162
|
+
* How far the model falls short of the published gage length, when it does.
|
|
163
|
+
*
|
|
164
|
+
* Present only on an incomplete profile, and only the amount. *Why* a
|
|
165
|
+
* vendor's model stops early is a fact about that family and belongs in the
|
|
166
|
+
* family's own notes rather than repeated on five records as a string a UI
|
|
167
|
+
* would be tempted to print verbatim.
|
|
168
|
+
*/
|
|
169
|
+
readonly shortfallMm?: number;
|
|
170
|
+
}
|
|
171
|
+
/** Every measured holder of a run, keyed by the guid its record was minted under. */
|
|
172
|
+
export interface ProfilesDocument {
|
|
173
|
+
readonly profilesVersion: number;
|
|
174
|
+
/**
|
|
175
|
+
* Always millimetres.
|
|
176
|
+
*
|
|
177
|
+
* The API measures in mm whatever the family's unit is, and nothing here
|
|
178
|
+
* converts: a shape measures what it measures, and an inch holder's profile
|
|
179
|
+
* is the same solid as a metric one's. `HolderRecord.gaugeLength` is what
|
|
180
|
+
* carries the vendor's own unit, for display.
|
|
181
|
+
*/
|
|
182
|
+
readonly unit: 'millimeters';
|
|
183
|
+
readonly kernelVersion: string;
|
|
184
|
+
readonly options: ImportOptions;
|
|
185
|
+
readonly holderCount: number;
|
|
186
|
+
readonly holders: Readonly<Record<string, HolderProfile>>;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* A taper designation as a size and an interface — `BT30` -> 30 / iso7x24.
|
|
190
|
+
*
|
|
191
|
+
* The other half of {@link checkProfile}'s agreement gate: the API measures a
|
|
192
|
+
* `sizeClass` and a `taperFamily` off the solid and cannot tell BT40 from CAT40
|
|
193
|
+
* from ISO40, so what it *can* be held to is the size and the family, and the
|
|
194
|
+
* vendor's own designation is where those come from.
|
|
195
|
+
*
|
|
196
|
+
* **Throws on a designation it does not know**, rather than returning null and
|
|
197
|
+
* letting the check be skipped. A prefix nobody has written down is this
|
|
198
|
+
* package's vocabulary being short, which is a `ScraperConfigError` — and
|
|
199
|
+
* silently not checking a family is how a mirrored STEP file goes unnoticed.
|
|
200
|
+
*/
|
|
201
|
+
export declare function taperDesignation(taper: string): {
|
|
202
|
+
sizeClass: number;
|
|
203
|
+
family: TaperFamily;
|
|
204
|
+
};
|
|
205
|
+
/**
|
|
206
|
+
* The measured stack as a silhouette on the gage line.
|
|
207
|
+
*
|
|
208
|
+
* `layers` is nose-first and datumed on nothing; a drawing wants `z` ascending
|
|
209
|
+
* from the spindle end with the gage line at zero. The conversion is one line
|
|
210
|
+
* of arithmetic and one decision:
|
|
211
|
+
*
|
|
212
|
+
* ```
|
|
213
|
+
* zFromNose = 0, then the running sum of thickness
|
|
214
|
+
* z = gaugeLength - zFromNose // nose positive, spindle end negative
|
|
215
|
+
* r = diameter / 2
|
|
216
|
+
* ```
|
|
217
|
+
*
|
|
218
|
+
* On `BT30ER16060M` that puts the nose at `z = +60` and the top of the taper at
|
|
219
|
+
* `60 - 108.4 = -48.4`, which is exactly the range Fusion measured, with the
|
|
220
|
+
* ER16 nut at `r = 16.0` — the scraped `lockNutDiameter` of 32 — at both ends.
|
|
221
|
+
*
|
|
222
|
+
* **The decision is the step faces.** Consecutive layers usually meet at one
|
|
223
|
+
* diameter, and two of `BT30ER16060M`'s 112 do not: the solid jumps vertically
|
|
224
|
+
* there. Emitting one point per boundary would draw those two jumps as slopes
|
|
225
|
+
* across the neighbouring layers and quietly shave material off the envelope,
|
|
226
|
+
* so a boundary whose diameters disagree gets **two points at the same `z`**.
|
|
227
|
+
* That is why {@link checkProfile} requires `z` to be non-decreasing rather
|
|
228
|
+
* than strictly increasing.
|
|
229
|
+
*
|
|
230
|
+
* With `gaugeLength` null there is no gauge plane to datum on and the nose is
|
|
231
|
+
* used instead, which is what {@link HolderProfile.datum} states. An empty
|
|
232
|
+
* stack yields no points rather than throwing — {@link checkProfile} is where a
|
|
233
|
+
* profile too short to draw is refused, and it says so with the part's name.
|
|
234
|
+
*/
|
|
235
|
+
export declare function layersToProfile(layers: readonly HolderLayer[], gaugeLength: number | null): ProfilePoint[];
|
|
236
|
+
/**
|
|
237
|
+
* Boundary validation for a measurement, the role `holding.ts`'s gates play for
|
|
238
|
+
* a scraped row.
|
|
239
|
+
*
|
|
240
|
+
* These come from a third-party CAD model rather than a vendor table, so the
|
|
241
|
+
* failures worth catching are the ones that **render as a plausible picture**:
|
|
242
|
+
* a silhouette that runs backwards, a negative radius, a datum outside the
|
|
243
|
+
* part, or a model that is not the holder the row says it is.
|
|
244
|
+
*
|
|
245
|
+
* `declaredTaper` is {@link HolderRecord.taper}. The reference implementation
|
|
246
|
+
* gated on `sizeClass == 30` instead, which was correct when the catalog was
|
|
247
|
+
* BT30 throughout and is not now — MariTool ships CAT40, CAT50, BT40 and nine
|
|
248
|
+
* HSK sizes. Agreement with what the vendor declares catches the same thing
|
|
249
|
+
* and travels: a CAT40 row whose model measures as an HSK is a mirrored file or
|
|
250
|
+
* a mis-scraped row. Geometry cannot tell BT40 from CAT40, so only the size and
|
|
251
|
+
* the family are checked.
|
|
252
|
+
*/
|
|
253
|
+
export declare function checkProfile(profile: HolderProfile, declaredTaper: string): void;
|
|
254
|
+
/**
|
|
255
|
+
* Measured holders plus the records they belong to -> the profiles document.
|
|
256
|
+
*
|
|
257
|
+
* **The join is `(brand, catalogNumber)`, and both halves matter.** The catalog
|
|
258
|
+
* number is what a mirrored STEP file is named for, so it is what a measurement
|
|
259
|
+
* carries back; `HolderRecord.catalogNumber` is that same cell, because every
|
|
260
|
+
* mapper reads it from `conventions.catalogColumn(brand)` — Kennametal's
|
|
261
|
+
* `ISO Catalog Number`, MariTool's `Material Number`, which is why the two
|
|
262
|
+
* identity fields are not interchangeable here. The brand is in the key because
|
|
263
|
+
* `node/paths.ts#stepDir` is per vendor: two vendors' catalog numbers live in
|
|
264
|
+
* two directories and have never had to be distinct from each other.
|
|
265
|
+
*
|
|
266
|
+
* **A measured holder matching no record raises.** It means a family was
|
|
267
|
+
* measured and then renamed or dropped, and silently omitting it looks exactly
|
|
268
|
+
* like a holder the vendor publishes no model for — the one thing this document
|
|
269
|
+
* must not be ambiguous about.
|
|
270
|
+
*
|
|
271
|
+
* **Every measurement must agree on the kernel and the options.** A document
|
|
272
|
+
* states one of each, and a batch that spanned a kernel upgrade or two
|
|
273
|
+
* `fillBays` settings would state one and contain both.
|
|
274
|
+
*/
|
|
275
|
+
export declare function buildProfiles(measured: readonly MeasuredHolder[], holders: readonly HolderRecord[]): ProfilesDocument;
|
package/dist/profiles.js
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A measured holder envelope, on the gage line, as the drawing wants it.
|
|
3
|
+
*
|
|
4
|
+
* `holding.ts` mints a holder from what the vendor *publishes*. This is the
|
|
5
|
+
* other half: what the holder's own CAD model *measures*, reduced to the
|
|
6
|
+
* silhouette a 2D elevation draws and cross-checked against the published gage
|
|
7
|
+
* length. The measuring is the Toolpath Engine API's — `node/holder-import.ts`
|
|
8
|
+
* makes the calls — and everything here is pure, so the 0/1/2/N cases are
|
|
9
|
+
* testable against literals rather than against a stack that has to be running.
|
|
10
|
+
*
|
|
11
|
+
* ## What replaced 1,800 lines of Python and a GUI
|
|
12
|
+
*
|
|
13
|
+
* The reference implementation sampled each STEP solid from inside a live
|
|
14
|
+
* Fusion 360 process over an MCP bridge, then did its own z-binned envelope,
|
|
15
|
+
* outward-only Douglas–Peucker, undercut fill and 7:24 taper solve. The API
|
|
16
|
+
* returns all of it — the layer stack, the gauge length, the size class and the
|
|
17
|
+
* taper family — so what is left here is a change of datum and a cross-check.
|
|
18
|
+
*
|
|
19
|
+
* The two agree. Four holders re-measured through the API against the profiles
|
|
20
|
+
* Fusion produced for the same STEP files, at the same 0.05 mm tolerance:
|
|
21
|
+
* `BT30ER16060M` 60 vs 60.000003 (2.8 nm), `BT30HC14100M` 100.355 vs
|
|
22
|
+
* 100.354999, `BT30HPVTT038295` 75 vs 75.000000, `BTKV30ER16100M` 89.4 vs
|
|
23
|
+
* 89.400000, with the total envelope height matching to three decimals on all
|
|
24
|
+
* four. The API decimates finer — about twice the layers at the same tolerance
|
|
25
|
+
* — which costs a drawing segments and changes no dimension.
|
|
26
|
+
*
|
|
27
|
+
* ## Its own document, keyed by guid
|
|
28
|
+
*
|
|
29
|
+
* A profile is ~110 points that only an assembly drawing needs, and a catalog
|
|
30
|
+
* is loaded by every page. So this is a second document that ships beside the
|
|
31
|
+
* records and is read lazily, rather than a field on {@link HolderRecord}.
|
|
32
|
+
* Keyed by guid because that is the one identifier that survives a re-scrape.
|
|
33
|
+
*
|
|
34
|
+
* **Collets get no profile.** They publish no CAD model and are not drawn: a
|
|
35
|
+
* collet sits inside the nut, which the holder's own envelope already includes.
|
|
36
|
+
*/
|
|
37
|
+
import { ScraperConfigError, VendorResponseError } from './errors.js';
|
|
38
|
+
/** Bumped when {@link ProfilesDocument}'s shape changes in a way a consumer must handle. */
|
|
39
|
+
export const PROFILES_VERSION = 1;
|
|
40
|
+
/**
|
|
41
|
+
* How far a measured gage length may sit from the vendor's published one before
|
|
42
|
+
* the profile is called incomplete, in millimetres.
|
|
43
|
+
*
|
|
44
|
+
* **Both bounds come from the data rather than from feel.** The largest
|
|
45
|
+
* *explained* deviation across the 53-holder Kennametal corpus is 0.355 mm —
|
|
46
|
+
* the nose lip on `BT30HC14100M`, real material past the face the vendor
|
|
47
|
+
* measures `L1` to, reproduced by the API to 1.5 nm. The smallest *real*
|
|
48
|
+
* shortfall is 10.6 mm — the collet nut the five BTKV30 STEP models omit, whose
|
|
49
|
+
* bodies stop at the threaded nose. That is a 30x gap, and this sits 2.8x above
|
|
50
|
+
* the first and 10.6x below the second. A family that lands in between is a
|
|
51
|
+
* finding to investigate, not a number to widen.
|
|
52
|
+
*/
|
|
53
|
+
export const GAUGE_TOLERANCE_MM = 1.0;
|
|
54
|
+
/** Every {@link TaperFamily}, for a message that can list what it knows. */
|
|
55
|
+
export const TAPER_FAMILIES = ['iso7x24', 'hsk'];
|
|
56
|
+
/**
|
|
57
|
+
* The 7:24 designations this package can read a size out of.
|
|
58
|
+
*
|
|
59
|
+
* A table rather than "letters then digits", for the reason
|
|
60
|
+
* `conventions.IDENTITY_DEVIATIONS` is one: an unknown prefix must be a line
|
|
61
|
+
* somebody added on evidence, not a regex that quietly accepted it. These are
|
|
62
|
+
* the two the scraped catalog states — Kennametal and REGO-FIX declare `BT30`
|
|
63
|
+
* throughout, MariTool states `BT30`, `BT40`, `CAT40` and `CAT50` per part —
|
|
64
|
+
* and a vendor publishing `SK`, `CV` or `ISO` adds its own entry here.
|
|
65
|
+
*
|
|
66
|
+
* **`BTKV` is deliberately absent.** `families/kennametal.ts` records why: a
|
|
67
|
+
* BTKV30 is the same JIS B 6339 cone as a BT30 and differs by seating on the
|
|
68
|
+
* flange face as well, which is `HolderRecord.contact`, so those families
|
|
69
|
+
* declare `BT30` and the distinction stays on the axis that carries it.
|
|
70
|
+
*/
|
|
71
|
+
export const TAPER_PREFIXES = ['BT', 'CAT'];
|
|
72
|
+
/** `HSK63A` -> 63, `HSK100A` -> 100. The form letter is optional; the size is not. */
|
|
73
|
+
const HSK_DESIGNATION = /^HSK(\d+)[A-Z]?$/;
|
|
74
|
+
/**
|
|
75
|
+
* A taper designation as a size and an interface — `BT30` -> 30 / iso7x24.
|
|
76
|
+
*
|
|
77
|
+
* The other half of {@link checkProfile}'s agreement gate: the API measures a
|
|
78
|
+
* `sizeClass` and a `taperFamily` off the solid and cannot tell BT40 from CAT40
|
|
79
|
+
* from ISO40, so what it *can* be held to is the size and the family, and the
|
|
80
|
+
* vendor's own designation is where those come from.
|
|
81
|
+
*
|
|
82
|
+
* **Throws on a designation it does not know**, rather than returning null and
|
|
83
|
+
* letting the check be skipped. A prefix nobody has written down is this
|
|
84
|
+
* package's vocabulary being short, which is a `ScraperConfigError` — and
|
|
85
|
+
* silently not checking a family is how a mirrored STEP file goes unnoticed.
|
|
86
|
+
*/
|
|
87
|
+
export function taperDesignation(taper) {
|
|
88
|
+
const hsk = HSK_DESIGNATION.exec(taper);
|
|
89
|
+
if (hsk !== null)
|
|
90
|
+
return { sizeClass: Number(hsk[1]), family: 'hsk' };
|
|
91
|
+
for (const prefix of TAPER_PREFIXES) {
|
|
92
|
+
if (!taper.startsWith(prefix))
|
|
93
|
+
continue;
|
|
94
|
+
const size = taper.slice(prefix.length);
|
|
95
|
+
if (/^\d+$/.test(size))
|
|
96
|
+
return { sizeClass: Number(size), family: 'iso7x24' };
|
|
97
|
+
}
|
|
98
|
+
throw new ScraperConfigError(taper, `is not a taper designation this package can read a size out of ` +
|
|
99
|
+
`(7:24 prefixes: ${TAPER_PREFIXES.join(', ')}; HSK as HSK<size><form>) — ` +
|
|
100
|
+
`add the prefix to TAPER_PREFIXES once it is clear what interface it names`);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The measured stack as a silhouette on the gage line.
|
|
104
|
+
*
|
|
105
|
+
* `layers` is nose-first and datumed on nothing; a drawing wants `z` ascending
|
|
106
|
+
* from the spindle end with the gage line at zero. The conversion is one line
|
|
107
|
+
* of arithmetic and one decision:
|
|
108
|
+
*
|
|
109
|
+
* ```
|
|
110
|
+
* zFromNose = 0, then the running sum of thickness
|
|
111
|
+
* z = gaugeLength - zFromNose // nose positive, spindle end negative
|
|
112
|
+
* r = diameter / 2
|
|
113
|
+
* ```
|
|
114
|
+
*
|
|
115
|
+
* On `BT30ER16060M` that puts the nose at `z = +60` and the top of the taper at
|
|
116
|
+
* `60 - 108.4 = -48.4`, which is exactly the range Fusion measured, with the
|
|
117
|
+
* ER16 nut at `r = 16.0` — the scraped `lockNutDiameter` of 32 — at both ends.
|
|
118
|
+
*
|
|
119
|
+
* **The decision is the step faces.** Consecutive layers usually meet at one
|
|
120
|
+
* diameter, and two of `BT30ER16060M`'s 112 do not: the solid jumps vertically
|
|
121
|
+
* there. Emitting one point per boundary would draw those two jumps as slopes
|
|
122
|
+
* across the neighbouring layers and quietly shave material off the envelope,
|
|
123
|
+
* so a boundary whose diameters disagree gets **two points at the same `z`**.
|
|
124
|
+
* That is why {@link checkProfile} requires `z` to be non-decreasing rather
|
|
125
|
+
* than strictly increasing.
|
|
126
|
+
*
|
|
127
|
+
* With `gaugeLength` null there is no gauge plane to datum on and the nose is
|
|
128
|
+
* used instead, which is what {@link HolderProfile.datum} states. An empty
|
|
129
|
+
* stack yields no points rather than throwing — {@link checkProfile} is where a
|
|
130
|
+
* profile too short to draw is refused, and it says so with the part's name.
|
|
131
|
+
*/
|
|
132
|
+
export function layersToProfile(layers, gaugeLength) {
|
|
133
|
+
const first = layers[0];
|
|
134
|
+
if (first === undefined)
|
|
135
|
+
return [];
|
|
136
|
+
let z = gaugeLength ?? 0;
|
|
137
|
+
const points = [[z, first.bottomDiameter / 2]];
|
|
138
|
+
for (const layer of layers) {
|
|
139
|
+
const bottom = layer.bottomDiameter / 2;
|
|
140
|
+
// Exact, not toleranced: the stack is one solid's decimation, so a
|
|
141
|
+
// boundary either repeats a diameter bit for bit or is a real step face.
|
|
142
|
+
if (bottom !== points[points.length - 1][1])
|
|
143
|
+
points.push([z, bottom]);
|
|
144
|
+
z -= layer.thickness;
|
|
145
|
+
points.push([z, layer.topDiameter / 2]);
|
|
146
|
+
}
|
|
147
|
+
return points.reverse();
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Boundary validation for a measurement, the role `holding.ts`'s gates play for
|
|
151
|
+
* a scraped row.
|
|
152
|
+
*
|
|
153
|
+
* These come from a third-party CAD model rather than a vendor table, so the
|
|
154
|
+
* failures worth catching are the ones that **render as a plausible picture**:
|
|
155
|
+
* a silhouette that runs backwards, a negative radius, a datum outside the
|
|
156
|
+
* part, or a model that is not the holder the row says it is.
|
|
157
|
+
*
|
|
158
|
+
* `declaredTaper` is {@link HolderRecord.taper}. The reference implementation
|
|
159
|
+
* gated on `sizeClass == 30` instead, which was correct when the catalog was
|
|
160
|
+
* BT30 throughout and is not now — MariTool ships CAT40, CAT50, BT40 and nine
|
|
161
|
+
* HSK sizes. Agreement with what the vendor declares catches the same thing
|
|
162
|
+
* and travels: a CAT40 row whose model measures as an HSK is a mirrored file or
|
|
163
|
+
* a mis-scraped row. Geometry cannot tell BT40 from CAT40, so only the size and
|
|
164
|
+
* the family are checked.
|
|
165
|
+
*/
|
|
166
|
+
export function checkProfile(profile, declaredTaper) {
|
|
167
|
+
const what = profile.catalogNumber;
|
|
168
|
+
const { points } = profile;
|
|
169
|
+
if (points.length < 2) {
|
|
170
|
+
throw new VendorResponseError(what, 'a profile needs at least two points');
|
|
171
|
+
}
|
|
172
|
+
let previous = -Infinity;
|
|
173
|
+
for (const [z, r] of points) {
|
|
174
|
+
if (z < previous)
|
|
175
|
+
throw new VendorResponseError(what, `profile z is not ascending at ${z}`);
|
|
176
|
+
if (r < 0)
|
|
177
|
+
throw new VendorResponseError(what, `negative radius ${r} at z ${z}`);
|
|
178
|
+
previous = z;
|
|
179
|
+
}
|
|
180
|
+
const low = points[0][0];
|
|
181
|
+
const high = points[points.length - 1][0];
|
|
182
|
+
if (profile.datum === 'gage-line' && !(low < 0 && 0 < high)) {
|
|
183
|
+
throw new VendorResponseError(what, `the gage line at z=0 is outside the profile (${low} .. ${high}) — ` +
|
|
184
|
+
`the datum was not applied`);
|
|
185
|
+
}
|
|
186
|
+
const declared = taperDesignation(declaredTaper);
|
|
187
|
+
if (profile.sizeClass !== declared.sizeClass || profile.taperFamily !== declared.family) {
|
|
188
|
+
throw new VendorResponseError(what, `the row declares ${declaredTaper} (size ${declared.sizeClass}, ${declared.family}) ` +
|
|
189
|
+
`and its model measures size ${profile.sizeClass} / ${profile.taperFamily} — ` +
|
|
190
|
+
`the wrong STEP file was mirrored, or the row's taper is wrong`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/** `brand` and a catalog number as one map key. */
|
|
194
|
+
function partKey(brand, catalogNumber) {
|
|
195
|
+
return `${brand}${catalogNumber}`;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Measured holders plus the records they belong to -> the profiles document.
|
|
199
|
+
*
|
|
200
|
+
* **The join is `(brand, catalogNumber)`, and both halves matter.** The catalog
|
|
201
|
+
* number is what a mirrored STEP file is named for, so it is what a measurement
|
|
202
|
+
* carries back; `HolderRecord.catalogNumber` is that same cell, because every
|
|
203
|
+
* mapper reads it from `conventions.catalogColumn(brand)` — Kennametal's
|
|
204
|
+
* `ISO Catalog Number`, MariTool's `Material Number`, which is why the two
|
|
205
|
+
* identity fields are not interchangeable here. The brand is in the key because
|
|
206
|
+
* `node/paths.ts#stepDir` is per vendor: two vendors' catalog numbers live in
|
|
207
|
+
* two directories and have never had to be distinct from each other.
|
|
208
|
+
*
|
|
209
|
+
* **A measured holder matching no record raises.** It means a family was
|
|
210
|
+
* measured and then renamed or dropped, and silently omitting it looks exactly
|
|
211
|
+
* like a holder the vendor publishes no model for — the one thing this document
|
|
212
|
+
* must not be ambiguous about.
|
|
213
|
+
*
|
|
214
|
+
* **Every measurement must agree on the kernel and the options.** A document
|
|
215
|
+
* states one of each, and a batch that spanned a kernel upgrade or two
|
|
216
|
+
* `fillBays` settings would state one and contain both.
|
|
217
|
+
*/
|
|
218
|
+
export function buildProfiles(measured, holders) {
|
|
219
|
+
const head = measured[0];
|
|
220
|
+
if (head === undefined) {
|
|
221
|
+
throw new ScraperConfigError('profiles', 'no measured holders — a profiles document covering nothing is not a ' +
|
|
222
|
+
'result, and writing one would look exactly like a run that worked');
|
|
223
|
+
}
|
|
224
|
+
const byPart = new Map();
|
|
225
|
+
for (const holder of holders) {
|
|
226
|
+
const key = partKey(holder.brand, holder.catalogNumber);
|
|
227
|
+
const clash = byPart.get(key);
|
|
228
|
+
if (clash !== undefined) {
|
|
229
|
+
throw new VendorResponseError(holder.catalogNumber, `${holder.brand} publishes it twice (${clash.materialNumber} and ` +
|
|
230
|
+
`${holder.materialNumber}) — the catalog number is what a mirrored ` +
|
|
231
|
+
`STEP file is named for, so it cannot identify two holders`);
|
|
232
|
+
}
|
|
233
|
+
byPart.set(key, holder);
|
|
234
|
+
}
|
|
235
|
+
const entries = new Map();
|
|
236
|
+
const inOrder = [...measured].sort((a, b) => a.catalogNumber.localeCompare(b.catalogNumber));
|
|
237
|
+
for (const record of inOrder) {
|
|
238
|
+
const holder = byPart.get(partKey(record.brand, record.catalogNumber));
|
|
239
|
+
if (holder === undefined) {
|
|
240
|
+
throw new VendorResponseError(record.catalogNumber, `was measured and matches no scraped ${record.brand} holder`);
|
|
241
|
+
}
|
|
242
|
+
checkRun(head, record);
|
|
243
|
+
const solved = record.gaugeLength;
|
|
244
|
+
const published = holder.gaugeLengthMm;
|
|
245
|
+
const shortfall = solved === null ? null : published - solved;
|
|
246
|
+
const complete = shortfall !== null && Math.abs(shortfall) <= GAUGE_TOLERANCE_MM;
|
|
247
|
+
const profile = {
|
|
248
|
+
catalogNumber: record.catalogNumber,
|
|
249
|
+
datum: solved === null ? 'nose' : 'gage-line',
|
|
250
|
+
points: layersToProfile(record.layers, solved),
|
|
251
|
+
gaugeLengthSolved: solved,
|
|
252
|
+
gaugeLengthPublished: published,
|
|
253
|
+
sizeClass: record.sizeClass,
|
|
254
|
+
taperFamily: record.taperFamily,
|
|
255
|
+
complete,
|
|
256
|
+
...(complete || shortfall === null ? {} : { shortfallMm: round(shortfall, 4) }),
|
|
257
|
+
};
|
|
258
|
+
checkProfile(profile, holder.taper);
|
|
259
|
+
// `index.ts` promises one guid space across holders and tools, and this
|
|
260
|
+
// document is keyed by it: two profiles under one guid would be one
|
|
261
|
+
// silently overwriting the other rather than the collision being refused.
|
|
262
|
+
const taken = entries.get(holder.guid);
|
|
263
|
+
if (taken !== undefined) {
|
|
264
|
+
throw new VendorResponseError(holder.guid, `is the guid of both ${taken.catalogNumber} and ${record.catalogNumber} — ` +
|
|
265
|
+
`two holders cannot share one identity`);
|
|
266
|
+
}
|
|
267
|
+
entries.set(holder.guid, profile);
|
|
268
|
+
}
|
|
269
|
+
return {
|
|
270
|
+
profilesVersion: PROFILES_VERSION,
|
|
271
|
+
unit: 'millimeters',
|
|
272
|
+
kernelVersion: head.kernelVersion,
|
|
273
|
+
options: head.options,
|
|
274
|
+
holderCount: entries.size,
|
|
275
|
+
holders: Object.fromEntries(entries),
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
/** Every measurement in one document came out of one run of one kernel. */
|
|
279
|
+
function checkRun(head, record) {
|
|
280
|
+
if (record.kernelVersion !== head.kernelVersion) {
|
|
281
|
+
throw new VendorResponseError(record.catalogNumber, `was measured by kernel ${record.kernelVersion} and ${head.catalogNumber} ` +
|
|
282
|
+
`by ${head.kernelVersion} — one document states one kernel version`);
|
|
283
|
+
}
|
|
284
|
+
for (const key of ['tolerance', 'fillBays', 'flipped']) {
|
|
285
|
+
if (record.options[key] !== head.options[key]) {
|
|
286
|
+
throw new VendorResponseError(record.catalogNumber, `was imported with ${key}=${record.options[key]} and ${head.catalogNumber} ` +
|
|
287
|
+
`with ${key}=${head.options[key]} — one document states one set of options`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
/** Decimal places, so a shortfall is a number and not a float artefact. */
|
|
292
|
+
function round(value, places) {
|
|
293
|
+
const scale = 10 ** places;
|
|
294
|
+
return Math.round(value * scale) / scale;
|
|
295
|
+
}
|
package/dist/provenance.d.ts
CHANGED
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
* is shaped `YYYY-MM-DD` — and `cite: ''` would otherwise satisfy the
|
|
45
45
|
* compiler while citing nothing.
|
|
46
46
|
*/
|
|
47
|
+
import { type Provenance } from '@toolpath/tool-support';
|
|
47
48
|
/** What a per-family constant can be. */
|
|
48
49
|
export type FactValue = string | number | boolean;
|
|
49
50
|
/**
|
|
@@ -53,9 +54,16 @@ export type FactValue = string | number | boolean;
|
|
|
53
54
|
* before the guesses and finishes on the ones only a person stands behind —
|
|
54
55
|
* `derived` first, then `assumed`. (`vendor-stated` leads the order but is
|
|
55
56
|
* filtered out of that document entirely; see {@link assumptions}.)
|
|
57
|
+
*
|
|
58
|
+
* **`@toolpath/tool-support`'s `PROVENANCE`**, re-exported under this package's
|
|
59
|
+
* own name. The order is load-bearing here and is the shared list's, so the
|
|
60
|
+
* ordering that document is read in cannot drift from the vocabulary a drawing
|
|
61
|
+
* marks a derived dimension by. Three identical declarations of these three
|
|
62
|
+
* strings stood before it — this one, the drawing's `Provenance`, and the
|
|
63
|
+
* application's.
|
|
56
64
|
*/
|
|
57
65
|
export declare const SOURCES: readonly ["vendor-stated", "derived", "assumed"];
|
|
58
|
-
export type FactSource =
|
|
66
|
+
export type FactSource = Provenance;
|
|
59
67
|
/**
|
|
60
68
|
* One per-family constant, with its provenance.
|
|
61
69
|
*
|
package/dist/provenance.js
CHANGED
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
* is shaped `YYYY-MM-DD` — and `cite: ''` would otherwise satisfy the
|
|
45
45
|
* compiler while citing nothing.
|
|
46
46
|
*/
|
|
47
|
+
import { PROVENANCE } from '@toolpath/tool-support';
|
|
47
48
|
import { ScraperConfigError } from './errors.js';
|
|
48
49
|
import { compare } from './order.js';
|
|
49
50
|
/**
|
|
@@ -53,8 +54,15 @@ import { compare } from './order.js';
|
|
|
53
54
|
* before the guesses and finishes on the ones only a person stands behind —
|
|
54
55
|
* `derived` first, then `assumed`. (`vendor-stated` leads the order but is
|
|
55
56
|
* filtered out of that document entirely; see {@link assumptions}.)
|
|
57
|
+
*
|
|
58
|
+
* **`@toolpath/tool-support`'s `PROVENANCE`**, re-exported under this package's
|
|
59
|
+
* own name. The order is load-bearing here and is the shared list's, so the
|
|
60
|
+
* ordering that document is read in cannot drift from the vocabulary a drawing
|
|
61
|
+
* marks a derived dimension by. Three identical declarations of these three
|
|
62
|
+
* strings stood before it — this one, the drawing's `Provenance`, and the
|
|
63
|
+
* application's.
|
|
56
64
|
*/
|
|
57
|
-
export const SOURCES =
|
|
65
|
+
export const SOURCES = PROVENANCE;
|
|
58
66
|
const DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
59
67
|
/** What a note has to say, per source kind. */
|
|
60
68
|
const WANTED = {
|