diff --git a/.changeset/tool-support-holder-gauge-length.md b/.changeset/tool-support-holder-gauge-length.md new file mode 100644 index 0000000..bf45a43 --- /dev/null +++ b/.changeset/tool-support-holder-gauge-length.md @@ -0,0 +1,28 @@ +--- +'@toolpath/tool-support': minor +--- + +Derive a Fusion holder's `gaugeLength` from the shape that was exported, on the published arm as +well as the measured one. + +Autodesk defines the holder's gauge length as the height _below the gauge line_, so it is a +reading of the segment stack rather than a fact standing beside it. The measured arm already +worked that way — `belowGageLine` makes the cut and the last vertex is the answer — but a +published holder was written with the vendor's own figure against a stack built from the vendor's +dimensions, and on a V-flange holder those are two different lengths. REGO-FIX publishes `B4`, +nose to gauge line, beside `B3`, nose to the flange face, and `B4 - B3` is 48.4 mm on every BT 30: +the gauge-line-to-flange distance from the vendor's own standards table. `B3` is what +`fromPublished` can draw, because no vendor publishes the shape of the flange above it. So a +BT 30 collet chuck went out declared 98.4 mm below the gauge line and drawn 50 mm long, and +`assemblyGaugeLength` — the stickout plus the holder's gauge length — put the tool 48.4 mm from +the gauge line it was drawn against. + +The stack's own height is now the exported figure, and the vendor's is reported instead: a +`dropped` note on `holder.gaugeLength` naming both numbers where they disagree. Where a vendor +publishes no gauge length the holder no longer goes out without the key — its shape is fully +drawn and its height therefore known — and a `filled` note says the number was read off the +geometry. A `nose`-datumed profile still omits `gaugeLength` entirely, because its silhouette is +the whole holder rather than the part below the gauge line. + +Both fixtures that covered this published a `projection` and a `gaugeLength` that were equal, so +no test could tell the two readings apart. One that does is added. diff --git a/packages/tool-support/src/export/fusion/holder.ts b/packages/tool-support/src/export/fusion/holder.ts index 225c65c..ed57730 100644 --- a/packages/tool-support/src/export/fusion/holder.ts +++ b/packages/tool-support/src/export/fusion/holder.ts @@ -15,6 +15,21 @@ * the whole format with `additionalProperties: false`, so there is nowhere to * annotate one. * + * ## The gauge length is the stack's own height + * + * Because it is defined as the height *below the gauge line*, it is a reading of + * the geometry rather than a fact standing beside it. So both arms derive it + * from what they actually exported, and neither states a figure it did not draw: + * a document claiming one number while drawing another leaves Fusion to + * reconcile the two silently, and a machinist to find out at the spindle. + * + * A vendor's own figure can disagree, and on a V-flange holder it usually does. + * REGO-FIX publishes `B4`, nose to gauge line, beside `B3`, nose to the flange + * face, and `B4 - B3` is 48.4 mm on every BT 30 — the gauge-line-to-flange + * distance in the vendor's own standards table. `B3` is the one + * {@link fromPublished} can draw, because no vendor publishes the shape of the + * flange above it. That difference is reported rather than exported. + * * ## Why the measured arm is the easy one * * A CAT40 model is measured whole, and half of what comes back is the 7:24 cone @@ -137,49 +152,71 @@ const fromProfile = (profile: HolderProfile, unit: UnitSystem): FusionSegment[] return segments } +/** A published holder's stack, and how tall it stands in millimetres. */ +interface PublishedStack { + readonly segments: FusionSegment[] + readonly reach: number +} + /** - * A published holder as segments, nose first. + * A published holder as segments, nose first, and how far they reach. * * The layer model `holderSilhouette` already states — the nose, the body behind * it where the vendor states one, the flange at its projection — read from the * nose rather than from the tool tip. A vendor that publishes no nose diameter * has published no shape, and nothing is drawn for it. + * + * `reach` is accumulated as the steps are placed rather than summed back off + * them: a segment's height has already been converted into the export's unit and + * rounded, so adding those up would put a conversion and six decimal places + * between the stack and the number that is supposed to measure it. */ -const fromPublished = (holder: Holder, unit: UnitSystem): FusionSegment[] => { +const fromPublished = (holder: Holder, unit: UnitSystem): PublishedStack => { const { noseDiameter, noseLength, bodyDiameter, bodyLength, flangeDiameter, projection } = holder - if (noseDiameter === null) return [] + if (noseDiameter === null) return { segments: [], reach: 0 } const mm = (value: number) => exported(convertLength(value, 'millimeters', unit)) - const step = (height: number, diameter: number): FusionSegment | null => - height > 0 - ? { height: mm(height), 'lower-diameter': mm(diameter), 'upper-diameter': mm(diameter) } - : null - const segments: FusionSegment[] = [] - const nose = step(noseLength ?? 0, noseDiameter) - if (nose !== null) segments.push(nose) + let reach = 0 + const step = (height: number, diameter: number): void => { + if (!(height > 0)) return + segments.push({ + height: mm(height), + 'lower-diameter': mm(diameter), + 'upper-diameter': mm(diameter), + }) + reach += height + } + step(noseLength ?? 0, noseDiameter) if (bodyDiameter !== null) { - const body = step(bodyLength ?? 0, bodyDiameter) - if (body !== null) segments.push(body) + step(bodyLength ?? 0, bodyDiameter) } // The flange stands at the holder's projection, so what is left between the // steps already placed and that plane is where it starts. if (flangeDiameter !== null && projection !== null) { const placed = (noseLength ?? 0) + (bodyDiameter !== null ? (bodyLength ?? 0) : 0) - const flange = step(projection - placed, flangeDiameter) - if (flange !== null) segments.push(flange) + step(projection - placed, flangeDiameter) } - return segments + return { segments, reach } } +/** + * Finer than the six places {@link exported} keeps, so only a disagreement a + * vendor actually published reads as one. + */ +const GAUGE_EPSILON = 1e-6 + /** * A holder as Fusion holds one, and an account of what did not travel. * - * `gaugeLength` is written only where it is known: the last vertex's `z` on a - * `gage-line` profile, the vendor's own figure on a published holder, and - * nothing at all on a `nose`-datumed profile. + * `gaugeLength` is written only where the exported shape measures it: the last + * vertex's `z` on a `gage-line` profile, the height of the stack on a published + * holder, and nothing at all on a `nose`-datumed profile, whose silhouette is + * the whole holder — taper, retention knob and all — rather than the part below + * the gauge line. Omitting the key there puts Fusion into manual mode, which is + * the honest answer; a zero would read as a measurement. */ export const fusionHolder = (entry: CatalogHolder): HolderResult => { const { holder, unit, guid } = entry @@ -189,7 +226,9 @@ export const fusionHolder = (entry: CatalogHolder): HolderResult => { } const measured = isHolderProfile(holder) - const segments = measured ? fromProfile(holder, unit) : fromPublished(holder, unit) + const profile = measured ? fromProfile(holder, unit) : null + const published = measured ? null : fromPublished(holder, unit) + const segments = profile ?? published?.segments ?? [] if (segments.length === 0) { note( @@ -215,10 +254,23 @@ export const fusionHolder = (entry: CatalogHolder): HolderResult => { 'Fusion is left to ask for the gauge length rather than shown a guess', ) } - } else { - gaugeLength = holder.gaugeLength - if (gaugeLength === null) { - note('dropped', 'holder.gaugeLength', 'the vendor publishes no gauge length') + } else if (published !== null) { + gaugeLength = published.reach + const stated = holder.gaugeLength + if (stated === null) { + note( + 'filled', + 'holder.gaugeLength', + 'the vendor publishes no gauge length, so this is the height of the shape exported', + ) + } else if (Math.abs(stated - published.reach) > GAUGE_EPSILON) { + note( + 'dropped', + 'holder.gaugeLength', + `the vendor states ${exported(stated)} mm below the gauge line and its published ` + + `dimensions draw ${exported(published.reach)} mm of holder — Fusion is given the ` + + 'height it can measure rather than one it cannot draw', + ) } } diff --git a/packages/tool-support/tests/export-fusion.test.ts b/packages/tool-support/tests/export-fusion.test.ts index 576922d..9d518b0 100644 --- a/packages/tool-support/tests/export-fusion.test.ts +++ b/packages/tool-support/tests/export-fusion.test.ts @@ -121,8 +121,10 @@ describe('a tool with a holder', () => { it('runs the holder from the nose toward the machine', () => { // Autodesk states it on the segment array: ordered from the cutter end to - // the machine tool. So the nose is first, and the heights sum to the gauge - // length because the stack stops at the gauge line. + // the machine tool, so the nose is first. The heights sum to the gauge + // length because the gauge length *is* that sum — this fixture's vendor + // happens to publish the same figure, and `a published holder that does not + // reach its own gauge line` is the case where one does not. const segments = (written.tool as FusionTool).holder?.segments ?? [] expect(segments[0]?.['lower-diameter']).toBe(33) expect(segments.reduce((total, segment) => total + segment.height, 0)).toBe(50) @@ -273,6 +275,100 @@ describe('a measured holder', () => { }) }) +describe('a published holder that does not reach its own gauge line', () => { + /** + * A BT 30 collet chuck, published the way DIN 4000 publishes one. + * + * `projection` is nose to the flange face and `gaugeLength` is nose to the + * gauge line, and on a BT 30 those differ by 48.4 mm — the gauge-line-to- + * flange distance, which is a property of the taper rather than of the part. + * The 48.4 mm above the flange face is real holder, and no vendor publishes + * its shape, so the stack this exporter can draw stops short of the figure + * the vendor states. + */ + const chuck: Holder = { + noseDiameter: 10, + noseLength: 10.55, + bodyDiameter: 12.02, + bodyLength: 9.6, + projection: 50, + flangeDiameter: 46, + gaugeLength: 98.4, + colletSeries: 'PG 6', + colletProtrusion: null, + } + + const held = (holder: Holder): FusionTool => { + const written = fusionTool({ + tool: endMill, + assembly: { + stickout: 24, + holder: { guid: catalogHolder.guid, holder, unit: 'millimeters' }, + }, + }) + return written.tool as FusionTool + } + + it('states the height it drew, not the length it could not draw', () => { + const tool = held(chuck) + const segments = tool.holder?.segments ?? [] + expect(segments.reduce((total, segment) => total + segment.height, 0)).toBe(50) + expect(tool.holder?.gaugeLength).toBe(50) + }) + + it('measures the assembly from the same place it measured the holder', () => { + // A document whose assembly gauge length was built from the vendor's figure + // and whose holder was built from the vendor's dimensions would put the tool + // 48.4 mm from the gauge line it is drawn against. + expect(held(chuck).geometry.assemblyGaugeLength).toBe(74) + }) + + it('reports the difference rather than exporting it', () => { + const written = fusionTool({ + tool: endMill, + assembly: { + stickout: 24, + holder: { guid: catalogHolder.guid, holder: chuck, unit: 'millimeters' }, + }, + }) + const dropped = written.notes.find( + (note) => note.kind === 'dropped' && note.field === 'holder.gaugeLength', + ) + expect(dropped?.message).toContain('98.4') + expect(dropped?.message).toContain('50') + }) + + it('says nothing where the vendor figure and the stack agree', () => { + const reaching: Holder = { ...chuck, gaugeLength: 50 } + const written = fusionTool({ + tool: endMill, + assembly: { + stickout: 24, + holder: { guid: catalogHolder.guid, holder: reaching, unit: 'millimeters' }, + }, + }) + expect(written.notes.filter((note) => note.field === 'holder.gaugeLength')).toHaveLength(0) + }) + + it('still gives Fusion a gauge length where the vendor publishes none', () => { + // This used to leave the key off, which puts Fusion into manual mode for a + // holder whose shape is fully drawn and whose height is therefore known. + const unstated: Holder = { ...chuck, gaugeLength: null } + const written = fusionTool({ + tool: endMill, + assembly: { + stickout: 24, + holder: { guid: catalogHolder.guid, holder: unstated, unit: 'millimeters' }, + }, + }) + expect((written.tool as FusionTool).holder?.gaugeLength).toBe(50) + const filled = written.notes.find( + (note) => note.kind === 'filled' && note.field === 'holder.gaugeLength', + ) + expect(filled).toBeDefined() + }) +}) + describe('the document and its text', () => { const { document, notes } = fusionLibrary({ tools: [{ tool: endMill, assembly: { stickout: 24, holder: catalogHolder } }],