Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .changeset/tool-support-holder-gauge-length.md
Original file line number Diff line number Diff line change
@@ -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.
98 changes: 75 additions & 23 deletions packages/tool-support/src/export/fusion/holder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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(
Expand All @@ -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',
)
}
}

Expand Down
100 changes: 98 additions & 2 deletions packages/tool-support/tests/export-fusion.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 } }],
Expand Down
Loading