@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
package/dist/holding.js
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a holder and a collet become, and the two gates that refuse one.
|
|
3
|
+
*
|
|
4
|
+
* `records.ts` is the cutting-tool half of this package's output. This is the
|
|
5
|
+
* toolholding half, and it is a separate module rather than a third `ToolKind`
|
|
6
|
+
* because the three vocabularies genuinely do not overlap: a tool answers `DC`,
|
|
7
|
+
* a flute count and a workpiece material group; a holder answers a taper, a
|
|
8
|
+
* clamping mode and a gage length; a collet answers a series and a capacity
|
|
9
|
+
* band. `families/index.ts` states the same rule for the config tables —
|
|
10
|
+
*
|
|
11
|
+
* > Separate tables rather than a `kind` on one, because a holder and a collet
|
|
12
|
+
* > are not variants of a thing.
|
|
13
|
+
*
|
|
14
|
+
* — and the records follow it. What they *do* share is identity, the unit rule
|
|
15
|
+
* and the guid space, which is why both live here and not in two more files:
|
|
16
|
+
* `identity.recordGuid` mints a holder and a tool into one namespace per brand,
|
|
17
|
+
* so a consumer building a catalog from both can refuse a collision between
|
|
18
|
+
* them.
|
|
19
|
+
*
|
|
20
|
+
* ## Two copies of a dimension, and only where something compares them
|
|
21
|
+
*
|
|
22
|
+
* A fit-bearing dimension is kept twice — once in the family's native unit for
|
|
23
|
+
* display, once in canonical millimetres for arithmetic. The reason is that fit
|
|
24
|
+
* and filtering are different questions: a 3/8 in shank is 9.525 mm and
|
|
25
|
+
* genuinely seats in a metric 9-10 mm collet, so a fit comparison must convert,
|
|
26
|
+
* while a range *filter* must still refuse to, because "between 9 and 10 mm" is
|
|
27
|
+
* not a question an inch tool answers.
|
|
28
|
+
*
|
|
29
|
+
* Only {@link HolderRecord.bore}, {@link HolderRecord.gaugeLength},
|
|
30
|
+
* {@link ColletRecord.clampMin} and {@link ColletRecord.clampMax} get a twin.
|
|
31
|
+
* Everything else — usable length, body diameter, adjustment range — is
|
|
32
|
+
* displayed and never compared, and a second copy of a number nothing reads is
|
|
33
|
+
* a field to keep in sync for free.
|
|
34
|
+
*
|
|
35
|
+
* **The twin is derived here rather than read from the other unit column**, and
|
|
36
|
+
* that is a deliberate departure from the reference implementation, which read
|
|
37
|
+
* `D1_mm` directly whatever the family's unit was. Vendors publish pairs that
|
|
38
|
+
* disagree — see {@link checkUnitAgreement} — and a record whose `bore` and
|
|
39
|
+
* `boreMm` came from two contradicting cells is one record stating two sizes.
|
|
40
|
+
* Deriving makes `boreMm` exactly `bore` in millimetres by construction, so the
|
|
41
|
+
* pair cannot drift and a test can pin one to the other.
|
|
42
|
+
*
|
|
43
|
+
* ## Which refusal a vendor's row earns
|
|
44
|
+
*
|
|
45
|
+
* The split is **blank versus wrong**, and it decides whether one bad part ends
|
|
46
|
+
* a family:
|
|
47
|
+
*
|
|
48
|
+
* - A cell the vendor left **blank** that a part cannot exist without — no gage
|
|
49
|
+
* length, no bore on a bore-clamping holder, no capacity on a collet — is an
|
|
50
|
+
* `errors.IncompletePartError`. `registry.toHolding` warns and drops the row,
|
|
51
|
+
* the same call `registry.toRecords` makes for a cutting tool, and for the
|
|
52
|
+
* same reason: MariTool leaves `Shank Size` blank on four HSK holders out of
|
|
53
|
+
* 527, and losing five families over four rows is not a trade worth making.
|
|
54
|
+
* - A value that is **present and unreadable**, or two present values that
|
|
55
|
+
* **contradict** — a clamping mode this package has no word for, a holder
|
|
56
|
+
* that names both a bore and a collet series, a collet whose nominal size
|
|
57
|
+
* falls outside its own published capacity — is an `errors.VendorResponseError`
|
|
58
|
+
* and stops the family. Those say the vendor's vocabulary or this package's
|
|
59
|
+
* reading of it has moved, and skipping past one quietly is how a scraper
|
|
60
|
+
* starts publishing a catalog nobody checked.
|
|
61
|
+
*/
|
|
62
|
+
import { dimensionalColumn, UNIT_SUFFIX } from './conventions.js';
|
|
63
|
+
import { IncompletePartError, ScraperConfigError, VendorResponseError } from './errors.js';
|
|
64
|
+
import { BRANDS, productLink, recordGuid } from './identity.js';
|
|
65
|
+
import { convertLength, fractionValue } from './measure.js';
|
|
66
|
+
import { consoleWarn } from './scrape.js';
|
|
67
|
+
/** Every {@link ClampingMode}, for a message that can list what it knows. */
|
|
68
|
+
export const CLAMPING_MODES = ['bore', 'collet', 'shrink', 'hydraulic'];
|
|
69
|
+
/**
|
|
70
|
+
* The clamping modes that grip a shank directly, and therefore publish a bore.
|
|
71
|
+
*
|
|
72
|
+
* One rule over three values rather than three rules: the gate in
|
|
73
|
+
* {@link checkHolder} asks whether a holder takes a shank or a collet, and
|
|
74
|
+
* every mode but `collet` takes a shank.
|
|
75
|
+
*/
|
|
76
|
+
export const BORE_CLAMPINGS = ['bore', 'shrink', 'hydraulic'];
|
|
77
|
+
/** Every {@link ContactMode}, for the same reason {@link CLAMPING_MODES} is a list. */
|
|
78
|
+
export const CONTACT_MODES = ['taper', 'face'];
|
|
79
|
+
/**
|
|
80
|
+
* A per-family constant a toolholding mapper cannot proceed without.
|
|
81
|
+
*
|
|
82
|
+
* `family.fact`'s counterpart for a family with no `id` and no `ToolKind`. The
|
|
83
|
+
* two are separate rather than one widened function because the subject of the
|
|
84
|
+
* message differs: a cutting-tool family is named by its vendor-local id and a
|
|
85
|
+
* toolholding family by the catalog name a human reads.
|
|
86
|
+
*
|
|
87
|
+
* Refusing rather than defaulting, for the reason `family.fact` states: every
|
|
88
|
+
* default is a claim the family never made, and a missing `taper` becoming
|
|
89
|
+
* `''` ships a holder that fits no spindle and raises nothing.
|
|
90
|
+
*/
|
|
91
|
+
export function holdingFact(family, key, value) {
|
|
92
|
+
if (value === undefined) {
|
|
93
|
+
throw new ScraperConfigError(family.catalogName, `a ${family.kind} family must state ${key} as a fact`);
|
|
94
|
+
}
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* A value the vendor left blank on one part, refused as an incomplete part.
|
|
99
|
+
*
|
|
100
|
+
* The toolholding counterpart of `columns.required`, and it throws the same
|
|
101
|
+
* type for the same reason: this is the one refusal `registry.toHolding` skips
|
|
102
|
+
* past, because a single part with an unpublished cell must not end a family's
|
|
103
|
+
* conversion. See `errors.IncompletePartError` for why the others must not be
|
|
104
|
+
* skipped alike.
|
|
105
|
+
*/
|
|
106
|
+
export function published(value, what, label) {
|
|
107
|
+
if (value === null || value === undefined || value === '') {
|
|
108
|
+
throw new IncompletePartError(what, `publishes no ${label}`);
|
|
109
|
+
}
|
|
110
|
+
return value;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Round a converted value to six decimals.
|
|
114
|
+
*
|
|
115
|
+
* **This removes error rather than adding precision.** 9.525 mm is exactly
|
|
116
|
+
* 0.375 in, but `9.525 / 25.4` is `0.37500000000000006` in binary floating
|
|
117
|
+
* point, and that is the number that would land in a catalog and in a
|
|
118
|
+
* prefix-matched size string. Six places is far coarser than the ~1e-14 the
|
|
119
|
+
* error reaches at these magnitudes and far finer than the four decimals a
|
|
120
|
+
* vendor prints, so nothing anybody stated is lost.
|
|
121
|
+
*/
|
|
122
|
+
function round6(value) {
|
|
123
|
+
return Math.round(value * 1e6) / 1e6;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* `value`, stated in `from`, as a number in `to` — a no-op when they agree.
|
|
127
|
+
*
|
|
128
|
+
* The one conversion every toolholding mapper makes, so that the rounding above
|
|
129
|
+
* is applied in one place rather than wherever somebody remembers to.
|
|
130
|
+
*/
|
|
131
|
+
export function asUnit(value, from, to) {
|
|
132
|
+
return from === to ? value : round6(convertLength(value, from, to));
|
|
133
|
+
}
|
|
134
|
+
export function millimeters(value, unit) {
|
|
135
|
+
return value === null ? null : asUnit(value, unit, 'millimeters');
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* One dimension in `unit`, converted from the other system where that is all
|
|
139
|
+
* the vendor published.
|
|
140
|
+
*
|
|
141
|
+
* **The fallback is load-bearing, not defensive.** Kennametal's `D1` is a unit
|
|
142
|
+
* pair on the BT30 hydraulic chucks and metric-only on the HSK63A HP line — an
|
|
143
|
+
* *inch* family with no `D1_in` column at all. A bare suffixed read is correct
|
|
144
|
+
* on the first and yields null on the second, producing a holder with no bore:
|
|
145
|
+
* it matches no tool, raises nothing, and looks like an empty result rather
|
|
146
|
+
* than a bug.
|
|
147
|
+
*
|
|
148
|
+
* The grammar is `measure.fractionValue`'s, which is the package's one reader
|
|
149
|
+
* for a machinist's number and refuses a range rather than summing it.
|
|
150
|
+
*/
|
|
151
|
+
export function dim(row, label, unit) {
|
|
152
|
+
const native = fractionValue(row[dimensionalColumn(label, unit)] ?? '');
|
|
153
|
+
if (native !== null)
|
|
154
|
+
return native;
|
|
155
|
+
const other = unit === 'millimeters' ? 'inches' : 'millimeters';
|
|
156
|
+
const fallback = fractionValue(row[dimensionalColumn(label, other)] ?? '');
|
|
157
|
+
if (fallback === null)
|
|
158
|
+
return null;
|
|
159
|
+
return round6(convertLength(fallback, other, unit));
|
|
160
|
+
}
|
|
161
|
+
/** Half a unit in the last decimal place a cell actually printed. */
|
|
162
|
+
function halfUlp(raw) {
|
|
163
|
+
const fraction = raw.includes('.') ? raw.slice(raw.indexOf('.') + 1) : '';
|
|
164
|
+
return 0.5 * 10 ** -fraction.length;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Report where a vendor's own millimetre and inch cells disagree.
|
|
168
|
+
*
|
|
169
|
+
* **Vendors really do publish contradictory pairs.** Kennametal's `16ERSS0312`
|
|
170
|
+
* states `D1`'s metric cell as `0.3125` — the inch value sitting in the metric
|
|
171
|
+
* column, a factor of 25.4 out — and `25ER130M` publishes `CCCN` as both
|
|
172
|
+
* 12.0 mm and 0.437 in, which is 11.1 mm. Both are in the source HTML.
|
|
173
|
+
*
|
|
174
|
+
* **This reports; it does not gate.** Two disagreeing cells cannot say which
|
|
175
|
+
* one is wrong, so refusing the family would trade a knowable warning for an
|
|
176
|
+
* unusable pipeline, and correcting a cell here would make this package a place
|
|
177
|
+
* tool data is authored by hand. What protects the output instead is that
|
|
178
|
+
* {@link dim} reads the family's *native* column and never the other one, plus
|
|
179
|
+
* {@link checkCollet}'s native-unit test that a nominal size falls inside its
|
|
180
|
+
* own published capacity. Every disagreement found so far sits in the column
|
|
181
|
+
* {@link dim} ignores; escalate this to a gate if one ever lands in a native
|
|
182
|
+
* column.
|
|
183
|
+
*
|
|
184
|
+
* **The tolerance is the vendor's own rounding, not a percentage.** A relative
|
|
185
|
+
* tolerance cannot tell rounding from error at small sizes: `16ER010M`
|
|
186
|
+
* publishes 0.5 mm as `0.02` in, correct to the two decimals it states and
|
|
187
|
+
* 1.6 % off as a ratio. Half a unit in each column's last printed place is
|
|
188
|
+
* exactly the slack the printed precision allows, and it is nowhere near the
|
|
189
|
+
* 7.6 mm a value in the wrong column produces.
|
|
190
|
+
*
|
|
191
|
+
* Returns whether it warned, so a caller can count.
|
|
192
|
+
*/
|
|
193
|
+
export function checkUnitAgreement(row, label, what, warn = consoleWarn) {
|
|
194
|
+
const rawMm = (row[label + UNIT_SUFFIX.millimeters] ?? '').trim();
|
|
195
|
+
const rawIn = (row[label + UNIT_SUFFIX.inches] ?? '').trim();
|
|
196
|
+
const mm = fractionValue(rawMm);
|
|
197
|
+
const inch = fractionValue(rawIn);
|
|
198
|
+
if (mm === null || inch === null)
|
|
199
|
+
return false;
|
|
200
|
+
const slack = halfUlp(rawIn) * convertLength(1, 'inches', 'millimeters') + halfUlp(rawMm);
|
|
201
|
+
const asMm = convertLength(inch, 'inches', 'millimeters');
|
|
202
|
+
if (Math.abs(mm - asMm) <= slack)
|
|
203
|
+
return false;
|
|
204
|
+
warn(` WARNING: ${what}: ${label} disagrees across unit systems — ` +
|
|
205
|
+
`${mm} mm vs ${inch} in (= ${asMm} mm); the native column is used`);
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* One cell as a {@link ContactMode}, refusing a word this package cannot read.
|
|
210
|
+
*
|
|
211
|
+
* A `VendorResponseError` and not an incomplete part: a *blank* contact is the
|
|
212
|
+
* caller's `published` call, and a contact the vendor states in a word nobody
|
|
213
|
+
* has mapped is the vocabulary having moved.
|
|
214
|
+
*/
|
|
215
|
+
export function contactMode(value, what) {
|
|
216
|
+
if (CONTACT_MODES.includes(value))
|
|
217
|
+
return value;
|
|
218
|
+
throw new VendorResponseError(what, `contact is ${JSON.stringify(value)} — it must be one of ` + `${CONTACT_MODES.join(', ')}`);
|
|
219
|
+
}
|
|
220
|
+
/** One cell as a {@link ClampingMode}, on the same terms as {@link contactMode}. */
|
|
221
|
+
export function clampingMode(value, what) {
|
|
222
|
+
if (CLAMPING_MODES.includes(value))
|
|
223
|
+
return value;
|
|
224
|
+
throw new VendorResponseError(what, `clamping is ${JSON.stringify(value)} — it must be one of ` + `${CLAMPING_MODES.join(', ')}`);
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* One cell as a {@link UnitSystem}, on the same terms as {@link contactMode}.
|
|
228
|
+
*
|
|
229
|
+
* For the vendor that states the unit per row rather than per family: REGO-FIX
|
|
230
|
+
* publishes `PG 25 Ø 6.0 mm` and `PG 25 Ø 1/4"` in one product group, so its
|
|
231
|
+
* collet scrape writes the system it read off each designation into a column.
|
|
232
|
+
*/
|
|
233
|
+
export function unitSystem(value, what) {
|
|
234
|
+
if (Object.hasOwn(UNIT_SUFFIX, value))
|
|
235
|
+
return value;
|
|
236
|
+
throw new VendorResponseError(what, `unit is ${JSON.stringify(value)} — it must be one of ` +
|
|
237
|
+
`${Object.keys(UNIT_SUFFIX).sort().join(', ')}`);
|
|
238
|
+
}
|
|
239
|
+
/** How a record names itself in a warning or a refusal. */
|
|
240
|
+
function subject(fields) {
|
|
241
|
+
return `${fields.catalogNumber} (${fields.materialNumber})`;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Vendor HTML is a system boundary, so this validates rather than guarding
|
|
245
|
+
* against a caller mistake.
|
|
246
|
+
*
|
|
247
|
+
* The `clamping` discriminant and the fields it implies must agree. **That is
|
|
248
|
+
* what turns Kennametal's missing-bore case into a failed conversion instead of
|
|
249
|
+
* a holder that quietly fits nothing** — a bore-clamping holder with no bore is
|
|
250
|
+
* the exact shape of that bug, and it raises nothing anywhere else.
|
|
251
|
+
*/
|
|
252
|
+
export function checkHolder(record) {
|
|
253
|
+
const what = subject(record);
|
|
254
|
+
const bore = BORE_CLAMPINGS.includes(record.clamping);
|
|
255
|
+
if (bore) {
|
|
256
|
+
if (record.bore === null) {
|
|
257
|
+
throw new IncompletePartError(what, `publishes no bore, and a ${record.clamping}-clamping holder grips a shank directly`);
|
|
258
|
+
}
|
|
259
|
+
if (record.colletSeries !== null) {
|
|
260
|
+
throw new VendorResponseError(what, `is ${record.clamping}-clamping and also names collet series ` +
|
|
261
|
+
`${JSON.stringify(record.colletSeries)} — a holder grips one way or the other`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
if (record.colletSeries === null) {
|
|
266
|
+
throw new IncompletePartError(what, 'is collet-clamping and names no collet series');
|
|
267
|
+
}
|
|
268
|
+
if (record.bore !== null) {
|
|
269
|
+
throw new VendorResponseError(what, `is collet-clamping and also publishes a bore of ${record.bore} — ` +
|
|
270
|
+
`a holder grips one way or the other`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
// Optional is fine; *malformed* is refused. A consumer renders this as a
|
|
274
|
+
// download button, and a truncated or redirected link is the one failure that
|
|
275
|
+
// looks like a working feature until somebody clicks it. Widened from the
|
|
276
|
+
// reference implementation's `.stp`-only test because MariTool publishes both
|
|
277
|
+
// spellings.
|
|
278
|
+
const url = record.cadModelUrl;
|
|
279
|
+
if (url !== null && !/^https:\/\/.+\.ste?p$/i.test(url)) {
|
|
280
|
+
throw new VendorResponseError(what, `CAD model URL is not an https .stp or .step: ${JSON.stringify(url)}`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* The same boundary rule for a collet.
|
|
285
|
+
*
|
|
286
|
+
* A collet with no capacity would match every shank or none depending on which
|
|
287
|
+
* way a comparison read a null, which is why {@link ColletRecord.clampMin} and
|
|
288
|
+
* {@link ColletRecord.clampMax} are not nullable and the mapper refuses the row
|
|
289
|
+
* before it gets here.
|
|
290
|
+
*/
|
|
291
|
+
export function checkCollet(record) {
|
|
292
|
+
const what = subject(record);
|
|
293
|
+
// Equality is a sealed coolant-through collet clamping one exact size — real,
|
|
294
|
+
// and not a bug. Only an inverted range is impossible.
|
|
295
|
+
if (record.clampMin > record.clampMax) {
|
|
296
|
+
throw new VendorResponseError(what, `capacity is inverted: ${record.clampMin} > ${record.clampMax}`);
|
|
297
|
+
}
|
|
298
|
+
// In the native unit, which is the gate with teeth: these are the values a
|
|
299
|
+
// consumer compares, and the contradictory cells this catalog knows about all
|
|
300
|
+
// sit in the column `dim` ignores.
|
|
301
|
+
if (record.nominal !== null &&
|
|
302
|
+
(record.nominal < record.clampMin || record.nominal > record.clampMax)) {
|
|
303
|
+
throw new VendorResponseError(what, `nominal ${record.nominal} is outside its own capacity ` +
|
|
304
|
+
`${record.clampMin}-${record.clampMax}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Build a {@link HolderRecord}: mint its guid, derive its millimetre twins, and
|
|
309
|
+
* refuse the states that cannot be true.
|
|
310
|
+
*
|
|
311
|
+
* `guid`, `vendor` and `productLink` are not inputs at all — every adapter
|
|
312
|
+
* minting them would be three copies of `recordGuid(brand, materialNumber)` to
|
|
313
|
+
* drift, on the value that is the join key for every downstream consumer. The
|
|
314
|
+
* nullable dimensions stay **required on the type** so a consumer reading a
|
|
315
|
+
* record never handles `undefined`; only the construction is optional, which is
|
|
316
|
+
* the shape `records.toolRecord` already has.
|
|
317
|
+
*
|
|
318
|
+
* The result is frozen: a record is an interchange value, and a mapper that
|
|
319
|
+
* mutated one would be reaching back across the seam this type exists to draw.
|
|
320
|
+
*/
|
|
321
|
+
export function holderRecord(fields) {
|
|
322
|
+
const record = Object.freeze({
|
|
323
|
+
...fields,
|
|
324
|
+
kind: 'holder',
|
|
325
|
+
guid: recordGuid(fields.brand, fields.materialNumber),
|
|
326
|
+
vendor: BRANDS[fields.brand].vendor,
|
|
327
|
+
productLink: productLink(fields.brand, fields.materialNumber),
|
|
328
|
+
colletSeries: fields.colletSeries ?? null,
|
|
329
|
+
bore: fields.bore ?? null,
|
|
330
|
+
boreMm: millimeters(fields.bore ?? null, fields.unit),
|
|
331
|
+
gaugeLengthMm: millimeters(fields.gaugeLength, fields.unit),
|
|
332
|
+
usableLength: fields.usableLength ?? null,
|
|
333
|
+
clampingLength: fields.clampingLength ?? null,
|
|
334
|
+
adjustmentRange: fields.adjustmentRange ?? null,
|
|
335
|
+
bodyDiameter: fields.bodyDiameter ?? null,
|
|
336
|
+
lockNutDiameter: fields.lockNutDiameter ?? null,
|
|
337
|
+
cadModelUrl: fields.cadModelUrl ?? null,
|
|
338
|
+
cadDxfUrl: fields.cadDxfUrl ?? null,
|
|
339
|
+
});
|
|
340
|
+
checkHolder(record);
|
|
341
|
+
return record;
|
|
342
|
+
}
|
|
343
|
+
/** Build a {@link ColletRecord}, on the same terms as {@link holderRecord}. */
|
|
344
|
+
export function colletRecord(fields) {
|
|
345
|
+
const record = Object.freeze({
|
|
346
|
+
...fields,
|
|
347
|
+
kind: 'collet',
|
|
348
|
+
guid: recordGuid(fields.brand, fields.materialNumber),
|
|
349
|
+
vendor: BRANDS[fields.brand].vendor,
|
|
350
|
+
productLink: productLink(fields.brand, fields.materialNumber),
|
|
351
|
+
nominal: fields.nominal ?? null,
|
|
352
|
+
clampMinMm: millimeters(fields.clampMin, fields.unit),
|
|
353
|
+
clampMaxMm: millimeters(fields.clampMax, fields.unit),
|
|
354
|
+
bodyDiameter: fields.bodyDiameter ?? null,
|
|
355
|
+
functionalLength: fields.functionalLength ?? null,
|
|
356
|
+
overallLength: fields.overallLength ?? null,
|
|
357
|
+
});
|
|
358
|
+
checkCollet(record);
|
|
359
|
+
return record;
|
|
360
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -20,27 +20,37 @@
|
|
|
20
20
|
* point, because a backend embedding this wants the data and a maintainer
|
|
21
21
|
* running the CLI wants the file, and only one of those two needs `fs`.
|
|
22
22
|
*
|
|
23
|
-
* ##
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* `
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
23
|
+
* ## Three record types, one guid space
|
|
24
|
+
*
|
|
25
|
+
* `ToolRecord` is the uniform output for **cutting tools**, and `HolderRecord`
|
|
26
|
+
* and `ColletRecord` are the toolholding half — `registry.toRecords` and
|
|
27
|
+
* `registry.toHolding` map one family's scrape onto them through the adapter
|
|
28
|
+
* its brand binds. All three are minted by `identity.recordGuid` in one
|
|
29
|
+
* namespace per brand, which is what lets a consumer build a catalog from
|
|
30
|
+
* holders and tools together and refuse a collision between them.
|
|
31
|
+
*
|
|
32
|
+
* They are three types rather than one because the vocabularies do not overlap:
|
|
33
|
+
* a tool answers `DC`, a flute count and a workpiece material group; a holder
|
|
34
|
+
* answers a taper, a clamping mode and a gage length; a collet answers a series
|
|
35
|
+
* and a capacity band. `holding.ts` states that at length, and `families/index.ts`
|
|
36
|
+
* already drew the same line for the config tables.
|
|
37
|
+
*
|
|
38
|
+
* **A vendor may publish holders, collets, both or neither, and none of it is
|
|
39
|
+
* required.** REGO-FIX ships toolholding and no cutting tools; MariTool ships
|
|
40
|
+
* holders and no collets; Harvey, EMUGE-FRANKEN and Destiny Tool ship neither.
|
|
41
|
+
* A brand with no mapper for a kind still scrapes, still writes a CSV and still
|
|
42
|
+
* checks a receipt — `registry.toHolding` is the only call that refuses, and it
|
|
43
|
+
* names the brand and what that brand does map.
|
|
36
44
|
*/
|
|
37
45
|
export * from './columns.js';
|
|
38
46
|
export * from './conventions.js';
|
|
39
47
|
export * from './errors.js';
|
|
40
48
|
export * from './family.js';
|
|
41
49
|
export * from './fetch.js';
|
|
50
|
+
export * from './holding.js';
|
|
42
51
|
export * from './identity.js';
|
|
43
52
|
export * from './measure.js';
|
|
53
|
+
export * from './profiles.js';
|
|
44
54
|
export * from './provenance.js';
|
|
45
55
|
export * from './records.js';
|
|
46
56
|
export * from './scrape.js';
|
package/dist/index.js
CHANGED
|
@@ -20,27 +20,37 @@
|
|
|
20
20
|
* point, because a backend embedding this wants the data and a maintainer
|
|
21
21
|
* running the CLI wants the file, and only one of those two needs `fs`.
|
|
22
22
|
*
|
|
23
|
-
* ##
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* `
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
23
|
+
* ## Three record types, one guid space
|
|
24
|
+
*
|
|
25
|
+
* `ToolRecord` is the uniform output for **cutting tools**, and `HolderRecord`
|
|
26
|
+
* and `ColletRecord` are the toolholding half — `registry.toRecords` and
|
|
27
|
+
* `registry.toHolding` map one family's scrape onto them through the adapter
|
|
28
|
+
* its brand binds. All three are minted by `identity.recordGuid` in one
|
|
29
|
+
* namespace per brand, which is what lets a consumer build a catalog from
|
|
30
|
+
* holders and tools together and refuse a collision between them.
|
|
31
|
+
*
|
|
32
|
+
* They are three types rather than one because the vocabularies do not overlap:
|
|
33
|
+
* a tool answers `DC`, a flute count and a workpiece material group; a holder
|
|
34
|
+
* answers a taper, a clamping mode and a gage length; a collet answers a series
|
|
35
|
+
* and a capacity band. `holding.ts` states that at length, and `families/index.ts`
|
|
36
|
+
* already drew the same line for the config tables.
|
|
37
|
+
*
|
|
38
|
+
* **A vendor may publish holders, collets, both or neither, and none of it is
|
|
39
|
+
* required.** REGO-FIX ships toolholding and no cutting tools; MariTool ships
|
|
40
|
+
* holders and no collets; Harvey, EMUGE-FRANKEN and Destiny Tool ship neither.
|
|
41
|
+
* A brand with no mapper for a kind still scrapes, still writes a CSV and still
|
|
42
|
+
* checks a receipt — `registry.toHolding` is the only call that refuses, and it
|
|
43
|
+
* names the brand and what that brand does map.
|
|
36
44
|
*/
|
|
37
45
|
export * from './columns.js';
|
|
38
46
|
export * from './conventions.js';
|
|
39
47
|
export * from './errors.js';
|
|
40
48
|
export * from './family.js';
|
|
41
49
|
export * from './fetch.js';
|
|
50
|
+
export * from './holding.js';
|
|
42
51
|
export * from './identity.js';
|
|
43
52
|
export * from './measure.js';
|
|
53
|
+
export * from './profiles.js';
|
|
44
54
|
export * from './provenance.js';
|
|
45
55
|
export * from './records.js';
|
|
46
56
|
export * from './scrape.js';
|
package/dist/measure.d.ts
CHANGED
|
@@ -41,16 +41,22 @@
|
|
|
41
41
|
* writes `1 1/2` and admits no hyphen at all. What it hands here is
|
|
42
42
|
* {@link Measured}, which is the part they have in common.
|
|
43
43
|
*
|
|
44
|
-
* **`MM_PER_INCH`
|
|
45
|
-
* exported
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
44
|
+
* **`MM_PER_INCH` and `convertLength` are `@toolpath/tool-support`'s**, and are
|
|
45
|
+
* re-exported here under the names this package has always published.
|
|
46
|
+
*
|
|
47
|
+
* They were declared and exported twice inside this package alone —
|
|
48
|
+
* `vendors/harvey/value.ts` and `vendors/regofix/scrape.ts` — so
|
|
49
|
+
* `@toolpath/tool-scraper/vendors/harvey` and `.../vendors/regofix` each
|
|
50
|
+
* published their own copy of 25.4. `tests/vendor-boundary.test.ts` refuses that
|
|
51
|
+
* by name and moving the constant up here fixed it *within* this package, while
|
|
52
|
+
* a third copy went on standing in the application downstream. The domain
|
|
53
|
+
* package is where a constant every consumer shares can only be declared once,
|
|
54
|
+
* and `@toolpath/tool-support`'s own boundary test now holds the whole tree to
|
|
55
|
+
* one `25.4`.
|
|
49
56
|
*/
|
|
50
|
-
import type
|
|
57
|
+
import { convertLength, MM_PER_INCH, type UnitSystem } from '@toolpath/tool-support';
|
|
51
58
|
import { type Warn } from './scrape.js';
|
|
52
|
-
|
|
53
|
-
export declare const MM_PER_INCH = 25.4;
|
|
59
|
+
export { convertLength, MM_PER_INCH };
|
|
54
60
|
/**
|
|
55
61
|
* `.250` -> 0.25, `3/4` -> 0.75, `1-1/2` -> 1.5, `2` -> 2.
|
|
56
62
|
*
|
|
@@ -63,8 +69,6 @@ export declare const MM_PER_INCH = 25.4;
|
|
|
63
69
|
* that guessed would be the mistake `conventions.UNIT_SUFFIX` exists to prevent.
|
|
64
70
|
*/
|
|
65
71
|
export declare function fractionValue(token: string): number | null;
|
|
66
|
-
/** `value`, converted from `from` to `to`. A no-op when they agree. */
|
|
67
|
-
export declare function convertLength(value: number, from: UnitSystem, to: UnitSystem): number;
|
|
68
72
|
/**
|
|
69
73
|
* What a value states about itself: a length system, or degrees.
|
|
70
74
|
*
|
package/dist/measure.js
CHANGED
|
@@ -41,15 +41,22 @@
|
|
|
41
41
|
* writes `1 1/2` and admits no hyphen at all. What it hands here is
|
|
42
42
|
* {@link Measured}, which is the part they have in common.
|
|
43
43
|
*
|
|
44
|
-
* **`MM_PER_INCH`
|
|
45
|
-
* exported
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
44
|
+
* **`MM_PER_INCH` and `convertLength` are `@toolpath/tool-support`'s**, and are
|
|
45
|
+
* re-exported here under the names this package has always published.
|
|
46
|
+
*
|
|
47
|
+
* They were declared and exported twice inside this package alone —
|
|
48
|
+
* `vendors/harvey/value.ts` and `vendors/regofix/scrape.ts` — so
|
|
49
|
+
* `@toolpath/tool-scraper/vendors/harvey` and `.../vendors/regofix` each
|
|
50
|
+
* published their own copy of 25.4. `tests/vendor-boundary.test.ts` refuses that
|
|
51
|
+
* by name and moving the constant up here fixed it *within* this package, while
|
|
52
|
+
* a third copy went on standing in the application downstream. The domain
|
|
53
|
+
* package is where a constant every consumer shares can only be declared once,
|
|
54
|
+
* and `@toolpath/tool-support`'s own boundary test now holds the whole tree to
|
|
55
|
+
* one `25.4`.
|
|
49
56
|
*/
|
|
57
|
+
import { convertLength, MM_PER_INCH } from '@toolpath/tool-support';
|
|
50
58
|
import { consoleWarn } from './scrape.js';
|
|
51
|
-
|
|
52
|
-
export const MM_PER_INCH = 25.4;
|
|
59
|
+
export { convertLength, MM_PER_INCH };
|
|
53
60
|
/**
|
|
54
61
|
* A decimal, a simple fraction or a mixed number — and nothing else.
|
|
55
62
|
*
|
|
@@ -80,12 +87,6 @@ export function fractionValue(token) {
|
|
|
80
87
|
const value = (whole === undefined ? 0 : Number(whole)) + part;
|
|
81
88
|
return Number.isFinite(value) ? value : null;
|
|
82
89
|
}
|
|
83
|
-
/** `value`, converted from `from` to `to`. A no-op when they agree. */
|
|
84
|
-
export function convertLength(value, from, to) {
|
|
85
|
-
if (from === to)
|
|
86
|
-
return value;
|
|
87
|
-
return to === 'inches' ? value / MM_PER_INCH : value * MM_PER_INCH;
|
|
88
|
-
}
|
|
89
90
|
/**
|
|
90
91
|
* A read cell as a length in `unit`, or null where it publishes none.
|
|
91
92
|
*
|
|
@@ -27,7 +27,24 @@
|
|
|
27
27
|
* later, optional step.
|
|
28
28
|
*/
|
|
29
29
|
import type { Fetcher } from '../fetch.js';
|
|
30
|
+
import type { BrandName } from '../identity.js';
|
|
30
31
|
import { type ScrapedRow, type Warn } from '../scrape.js';
|
|
32
|
+
/**
|
|
33
|
+
* What one part's mirrored STEP model is called.
|
|
34
|
+
*
|
|
35
|
+
* REGO-FIX's catalog number is the vendor's own title — `BT 30 / PG 25 x 075`
|
|
36
|
+
* — and a separator in it was being honoured as one: `downloadStep` creates
|
|
37
|
+
* the parent directory, so the file landed in a `BT 30 ` subdirectory instead
|
|
38
|
+
* of flat in `outDir`, against what this module promises. The number stays
|
|
39
|
+
* readable, which is the whole reason the file is named for it.
|
|
40
|
+
*
|
|
41
|
+
* **Exported because two things now resolve it**, and two answers to "what is
|
|
42
|
+
* this part's file called" is one too many — the same argument
|
|
43
|
+
* {@link cadCoverage} makes for counting the column here. `node/holder-import.ts`
|
|
44
|
+
* reads back what this writes, and a mirror that flattened a separator while a
|
|
45
|
+
* reader did not would report every REGO-FIX holder as unmirrored.
|
|
46
|
+
*/
|
|
47
|
+
export declare function stepFileName(catalogNumber: string): string;
|
|
31
48
|
/** One STEP file onto disk. Returns the bytes written. */
|
|
32
49
|
export declare function downloadStep(fetcher: Fetcher, url: string, dest: string): Promise<number>;
|
|
33
50
|
/** One mirrored file. */
|
|
@@ -35,6 +52,35 @@ export interface MirroredStep {
|
|
|
35
52
|
catalogNumber: string;
|
|
36
53
|
bytes: number;
|
|
37
54
|
}
|
|
55
|
+
/** How much of one family the vendor publishes a downloadable model for. */
|
|
56
|
+
export interface CadCoverage {
|
|
57
|
+
/** Rows in the CSV. */
|
|
58
|
+
rows: number;
|
|
59
|
+
/** Rows carrying a {@link CAD_COLUMN} a mirror could download. */
|
|
60
|
+
step: number;
|
|
61
|
+
/** Rows carrying a {@link CAD_DXF_COLUMN}. */
|
|
62
|
+
dxf: number;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* What a mirror of these rows would and would not get, without asking for any
|
|
66
|
+
* of it.
|
|
67
|
+
*
|
|
68
|
+
* **This is the number that bounds anything built on measured geometry**, and
|
|
69
|
+
* it is worth knowing before the download rather than after: two of the three
|
|
70
|
+
* toolholding vendors publish a STEP model for every part, and MariTool — which
|
|
71
|
+
* is 527 of the 601 holder rows — publishes one for about two thirds. A
|
|
72
|
+
* pipeline scoped against 601 and run against 431 is a surprise at the end.
|
|
73
|
+
*
|
|
74
|
+
* Counted here rather than in the CLI because it is the same column read the
|
|
75
|
+
* same way {@link mirrorFamilySteps} reads it, and two answers to "does this row
|
|
76
|
+
* have a model" is one too many. Pure, and it makes no requests.
|
|
77
|
+
*
|
|
78
|
+
* **The DXF is counted and is not a fallback.** `conventions.CAD_DXF_COLUMN`
|
|
79
|
+
* says why the two are different columns: a DXF is a drawing whose datum,
|
|
80
|
+
* projection and layer semantics are the vendor's business, and deriving a
|
|
81
|
+
* profile from one is a separate project rather than a way to cover the gap.
|
|
82
|
+
*/
|
|
83
|
+
export declare function cadCoverage(rows: readonly ScrapedRow[]): CadCoverage;
|
|
38
84
|
/**
|
|
39
85
|
* Every STEP model a holder scrape names, into `outDir`, one file per row.
|
|
40
86
|
*
|
|
@@ -42,6 +88,16 @@ export interface MirroredStep {
|
|
|
42
88
|
* filename is what a human reads and `BT30ER16060M` says what the part is
|
|
43
89
|
* where `1258023` does not.
|
|
44
90
|
*
|
|
91
|
+
* **`brand` is what says which column that number is in, and it is an argument
|
|
92
|
+
* rather than a guess.** This read `row['ISO Catalog Number']` until 2026-09-02,
|
|
93
|
+
* which is Kennametal's pair and REGO-FIX's adopted copy of it. MariTool
|
|
94
|
+
* publishes one number per part, under `Material Number`, and no catalog
|
|
95
|
+
* designation at all — `conventions.IDENTITY_DEVIATIONS` records why — so every
|
|
96
|
+
* one of its 357 published models was skipped with a warning saying the row had
|
|
97
|
+
* no catalog number to name it. `conventions.catalogColumn` is the lookup, and
|
|
98
|
+
* it is a lookup rather than a fallback down a list of candidate columns for the
|
|
99
|
+
* reason stated there.
|
|
100
|
+
*
|
|
45
101
|
* **`outDir` is a required argument and never inferred.** These files are a
|
|
46
102
|
* local working copy, they are gitignored, and a default that pointed into a
|
|
47
103
|
* tracked directory would be the one mistake that silently commits ~3 MB of
|
|
@@ -51,5 +107,6 @@ export interface MirroredStep {
|
|
|
51
107
|
* `lightweightStepUrl`'s documented null case arriving here — and a skipped
|
|
52
108
|
* row spends no delay, because the count that matters is downloads and a
|
|
53
109
|
* family that is mostly blank should not sleep its way through the gaps.
|
|
110
|
+
* {@link cadCoverage} is how many of them there will be, before any of it runs.
|
|
54
111
|
*/
|
|
55
|
-
export declare function mirrorFamilySteps(fetcher: Fetcher, rows: readonly ScrapedRow[], outDir: string, delayMs?: number, warn?: Warn): Promise<MirroredStep[]>;
|
|
112
|
+
export declare function mirrorFamilySteps(fetcher: Fetcher, rows: readonly ScrapedRow[], brand: BrandName, outDir: string, delayMs?: number, warn?: Warn): Promise<MirroredStep[]>;
|