fix: preserve HiCAD hole annotations
This commit is contained in:
parent
ce1e101ffc
commit
1068072bd3
@ -395,6 +395,9 @@ test.describe('HiCAD manufacturing extension', () => {
|
||||
{ name: 'legHeight', valueMm: 750 },
|
||||
{ name: 'studDiameter', valueMm: 5 },
|
||||
{ name: 'studHeight', valueMm: 10 },
|
||||
{ name: 'holeDiameter', valueMm: 5 },
|
||||
{ name: 'holeDepth', valueMm: 10 },
|
||||
{ name: 'holeInset', valueMm: 50 },
|
||||
{ name: 'unrelatedCabinetWidth', valueMm: 900 },
|
||||
]
|
||||
|
||||
@ -416,6 +419,24 @@ test.describe('HiCAD manufacturing extension', () => {
|
||||
quantity: 1,
|
||||
dimensions: { x: 1200, y: 600, z: 25 },
|
||||
})
|
||||
const tabletop = reconciled.find((body) => body.id === 'cold-tabletop')
|
||||
expect(tabletop?.holes).toHaveLength(4)
|
||||
expect(
|
||||
tabletop?.holes?.every(
|
||||
(hole) =>
|
||||
hole.type === 'blind-hole' &&
|
||||
hole.diameter === 5 &&
|
||||
hole.depth === 10
|
||||
)
|
||||
).toBe(true)
|
||||
expect(
|
||||
tabletop?.holes
|
||||
?.map((hole) => [
|
||||
Math.abs(hole.center.x - (tabletop?.center.x ?? 0)),
|
||||
Math.abs(hole.center.y - (tabletop?.center.y ?? 0)),
|
||||
])
|
||||
.every(([fromCenterX, fromCenterY]) => fromCenterX === 550 && fromCenterY === 250)
|
||||
).toBe(true)
|
||||
expect(
|
||||
reconciled.every(
|
||||
(body) =>
|
||||
@ -425,6 +446,16 @@ test.describe('HiCAD manufacturing extension', () => {
|
||||
body.dimensionReferences.length >= 2
|
||||
)
|
||||
).toBe(true)
|
||||
|
||||
const html = createEngineeringSheetHtml({
|
||||
projectName: '冷启动孔位桌',
|
||||
generatedAt: '2026-07-28 12:00',
|
||||
overallDimensions: { x: 1200, y: 600, z: 775 },
|
||||
bodies: reconciled,
|
||||
bomRows: rows,
|
||||
})
|
||||
expect(html).toContain('4 × Ø5')
|
||||
expect(html).toContain('Ø5 盲孔,深 10 mm')
|
||||
})
|
||||
|
||||
test('keeps a square leg end view rectangular', () => {
|
||||
|
||||
@ -289,121 +289,157 @@ export function HicadManufacturingPanel({
|
||||
]
|
||||
}
|
||||
|
||||
const readEntityChildIds = async (entityId: string) => {
|
||||
const response = await sendModelingCommand({
|
||||
type: 'entity_get_all_child_uuids',
|
||||
entity_id: entityId,
|
||||
})
|
||||
if (
|
||||
response.type !== 'entity_get_all_child_uuids' ||
|
||||
typeof response.data !== 'object' ||
|
||||
response.data === null ||
|
||||
!('entity_ids' in response.data) ||
|
||||
!Array.isArray(response.data.entity_ids)
|
||||
) {
|
||||
return []
|
||||
}
|
||||
return response.data.entity_ids.filter(
|
||||
(id): id is string => typeof id === 'string'
|
||||
)
|
||||
}
|
||||
|
||||
const readCircularSamples = async (entityIds: string[]) => {
|
||||
const typedEntities = await settleInBatches(
|
||||
[...new Set(entityIds)],
|
||||
6,
|
||||
async (entityId) => {
|
||||
const response = await sendModelingCommand({
|
||||
type: 'get_entity_type',
|
||||
entity_id: entityId,
|
||||
})
|
||||
const entityType =
|
||||
response.type === 'get_entity_type' &&
|
||||
typeof response.data === 'object' &&
|
||||
response.data !== null &&
|
||||
'entity_type' in response.data &&
|
||||
typeof response.data.entity_type === 'string'
|
||||
? response.data.entity_type
|
||||
: undefined
|
||||
return { entityId, entityType }
|
||||
}
|
||||
)
|
||||
const curveCandidates = typedEntities.flatMap((result) =>
|
||||
result.status === 'fulfilled' &&
|
||||
(result.value.entityType === 'edge' ||
|
||||
result.value.entityType === 'curve')
|
||||
? [result.value]
|
||||
: []
|
||||
)
|
||||
const edgeResults = await settleInBatches(
|
||||
curveCandidates,
|
||||
4,
|
||||
async ({ entityId, entityType }) => {
|
||||
const typeResponse = await sendModelingCommand({
|
||||
type: 'curve_get_type',
|
||||
curve_id: entityId,
|
||||
})
|
||||
const curveType =
|
||||
typeResponse.type === 'curve_get_type' &&
|
||||
typeof typeResponse.data === 'object' &&
|
||||
typeResponse.data !== null &&
|
||||
'curve_type' in typeResponse.data
|
||||
? typeResponse.data.curve_type
|
||||
: undefined
|
||||
if (curveType !== 'arc' && curveType !== 'nurbs') return undefined
|
||||
|
||||
const [pointsResult, lengthResult] = await Promise.allSettled([
|
||||
sendModelingCommand({
|
||||
type: 'curve_get_control_points',
|
||||
curve_id: entityId,
|
||||
}),
|
||||
entityType === 'edge'
|
||||
? sendModelingCommand({
|
||||
type: 'edge_get_length',
|
||||
edge_id: entityId,
|
||||
})
|
||||
: Promise.resolve(undefined),
|
||||
])
|
||||
if (
|
||||
pointsResult.status !== 'fulfilled' ||
|
||||
pointsResult.value.type !== 'curve_get_control_points' ||
|
||||
typeof pointsResult.value.data !== 'object' ||
|
||||
pointsResult.value.data === null ||
|
||||
!('control_points' in pointsResult.value.data) ||
|
||||
!Array.isArray(pointsResult.value.data.control_points)
|
||||
) {
|
||||
return undefined
|
||||
}
|
||||
const controlPoints =
|
||||
pointsResult.value.data.control_points.flatMap((point) =>
|
||||
typeof point === 'object' &&
|
||||
point !== null &&
|
||||
'x' in point &&
|
||||
'y' in point &&
|
||||
'z' in point &&
|
||||
typeof point.x === 'number' &&
|
||||
typeof point.y === 'number' &&
|
||||
typeof point.z === 'number'
|
||||
? [{ x: point.x, y: point.y, z: point.z }]
|
||||
: []
|
||||
)
|
||||
let length: number | undefined
|
||||
if (
|
||||
lengthResult.status === 'fulfilled' &&
|
||||
lengthResult.value &&
|
||||
lengthResult.value.type === 'edge_get_length' &&
|
||||
typeof lengthResult.value.data === 'object' &&
|
||||
lengthResult.value.data !== null &&
|
||||
'length' in lengthResult.value.data &&
|
||||
typeof lengthResult.value.data.length === 'number'
|
||||
) {
|
||||
length = lengthResult.value.data.length
|
||||
}
|
||||
return { controlPoints, length } satisfies CircularEdgeSample
|
||||
}
|
||||
)
|
||||
|
||||
return edgeResults.flatMap((result) =>
|
||||
result.status === 'fulfilled' && result.value ? [result.value] : []
|
||||
)
|
||||
}
|
||||
|
||||
const readCircularEdges = async (bodyId: string) => {
|
||||
try {
|
||||
const childResponse = await sendModelingCommand({
|
||||
type: 'entity_get_all_child_uuids',
|
||||
const childSamples = await readCircularSamples(
|
||||
await readEntityChildIds(bodyId)
|
||||
)
|
||||
if (childSamples.length > 0) return childSamples
|
||||
|
||||
const sketchResponse = await sendModelingCommand({
|
||||
type: 'entity_get_sketch_paths',
|
||||
entity_id: bodyId,
|
||||
})
|
||||
if (
|
||||
childResponse.type !== 'entity_get_all_child_uuids' ||
|
||||
typeof childResponse.data !== 'object' ||
|
||||
childResponse.data === null ||
|
||||
!('entity_ids' in childResponse.data) ||
|
||||
!Array.isArray(childResponse.data.entity_ids)
|
||||
sketchResponse.type !== 'entity_get_sketch_paths' ||
|
||||
typeof sketchResponse.data !== 'object' ||
|
||||
sketchResponse.data === null ||
|
||||
!('entity_ids' in sketchResponse.data) ||
|
||||
!Array.isArray(sketchResponse.data.entity_ids)
|
||||
) {
|
||||
return []
|
||||
}
|
||||
const childIds = childResponse.data.entity_ids.filter(
|
||||
const sketchIds = sketchResponse.data.entity_ids.filter(
|
||||
(id): id is string => typeof id === 'string'
|
||||
)
|
||||
const typedChildren = await settleInBatches(
|
||||
childIds,
|
||||
6,
|
||||
async (entityId) => {
|
||||
const response = await sendModelingCommand({
|
||||
type: 'get_entity_type',
|
||||
entity_id: entityId,
|
||||
})
|
||||
const entityType =
|
||||
response.type === 'get_entity_type' &&
|
||||
typeof response.data === 'object' &&
|
||||
response.data !== null &&
|
||||
'entity_type' in response.data &&
|
||||
typeof response.data.entity_type === 'string'
|
||||
? response.data.entity_type
|
||||
: undefined
|
||||
return { entityId, entityType }
|
||||
}
|
||||
)
|
||||
const curveCandidates = typedChildren.flatMap((result) =>
|
||||
result.status === 'fulfilled' &&
|
||||
(result.value.entityType === 'edge' ||
|
||||
result.value.entityType === 'curve')
|
||||
? [result.value]
|
||||
: []
|
||||
)
|
||||
const edgeResults = await settleInBatches(
|
||||
curveCandidates,
|
||||
const sketchChildren = await settleInBatches(
|
||||
sketchIds,
|
||||
4,
|
||||
async ({ entityId, entityType }) => {
|
||||
const typeResponse = await sendModelingCommand({
|
||||
type: 'curve_get_type',
|
||||
curve_id: entityId,
|
||||
})
|
||||
const curveType =
|
||||
typeResponse.type === 'curve_get_type' &&
|
||||
typeof typeResponse.data === 'object' &&
|
||||
typeResponse.data !== null &&
|
||||
'curve_type' in typeResponse.data
|
||||
? typeResponse.data.curve_type
|
||||
: undefined
|
||||
if (curveType !== 'arc' && curveType !== 'nurbs') return undefined
|
||||
|
||||
const [pointsResult, lengthResult] = await Promise.allSettled([
|
||||
sendModelingCommand({
|
||||
type: 'curve_get_control_points',
|
||||
curve_id: entityId,
|
||||
}),
|
||||
entityType === 'edge'
|
||||
? sendModelingCommand({
|
||||
type: 'edge_get_length',
|
||||
edge_id: entityId,
|
||||
})
|
||||
: Promise.resolve(undefined),
|
||||
])
|
||||
if (
|
||||
pointsResult.status !== 'fulfilled' ||
|
||||
pointsResult.value.type !== 'curve_get_control_points' ||
|
||||
typeof pointsResult.value.data !== 'object' ||
|
||||
pointsResult.value.data === null ||
|
||||
!('control_points' in pointsResult.value.data) ||
|
||||
!Array.isArray(pointsResult.value.data.control_points)
|
||||
) {
|
||||
return undefined
|
||||
}
|
||||
const controlPoints =
|
||||
pointsResult.value.data.control_points.flatMap((point) =>
|
||||
typeof point === 'object' &&
|
||||
point !== null &&
|
||||
'x' in point &&
|
||||
'y' in point &&
|
||||
'z' in point &&
|
||||
typeof point.x === 'number' &&
|
||||
typeof point.y === 'number' &&
|
||||
typeof point.z === 'number'
|
||||
? [{ x: point.x, y: point.y, z: point.z }]
|
||||
: []
|
||||
)
|
||||
let length: number | undefined
|
||||
if (
|
||||
lengthResult.status === 'fulfilled' &&
|
||||
lengthResult.value &&
|
||||
lengthResult.value.type === 'edge_get_length' &&
|
||||
typeof lengthResult.value.data === 'object' &&
|
||||
lengthResult.value.data !== null &&
|
||||
'length' in lengthResult.value.data &&
|
||||
typeof lengthResult.value.data.length === 'number'
|
||||
) {
|
||||
length = lengthResult.value.data.length
|
||||
}
|
||||
return { controlPoints, length } satisfies CircularEdgeSample
|
||||
}
|
||||
async (sketchId) => [sketchId, ...(await readEntityChildIds(sketchId))]
|
||||
)
|
||||
|
||||
return edgeResults.flatMap((result) =>
|
||||
result.status === 'fulfilled' && result.value ? [result.value] : []
|
||||
const sketchEntityIds = sketchChildren.flatMap((result) =>
|
||||
result.status === 'fulfilled' ? result.value : []
|
||||
)
|
||||
return readCircularSamples(sketchEntityIds)
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
'[HiCAD manufacturing] Unable to inspect circular body edges',
|
||||
|
||||
295
vendor/zoo-modeling-app/manufacturing.ts
vendored
295
vendor/zoo-modeling-app/manufacturing.ts
vendored
@ -810,6 +810,290 @@ function snapCorrectedDimension(value: number) {
|
||||
: roundDimension(absolute)
|
||||
}
|
||||
|
||||
type HoleParameterRole =
|
||||
| 'diameter'
|
||||
| 'radius'
|
||||
| 'depth'
|
||||
| 'inset'
|
||||
| 'insetX'
|
||||
| 'insetY'
|
||||
| 'offsetX'
|
||||
| 'offsetY'
|
||||
| 'spacingX'
|
||||
| 'spacingY'
|
||||
|
||||
type ClassifiedHoleParameter = ManufacturingLengthParameter & {
|
||||
role: HoleParameterRole
|
||||
scope: string
|
||||
}
|
||||
|
||||
const HOLE_PARAMETER_PATTERNS: Array<{
|
||||
role: HoleParameterRole
|
||||
pattern: RegExp
|
||||
}> = [
|
||||
{
|
||||
role: 'diameter',
|
||||
pattern:
|
||||
/(?:hole.*(?:diameter|dia)|(?:diameter|dia).*hole|孔直径|孔径)/i,
|
||||
},
|
||||
{
|
||||
role: 'radius',
|
||||
pattern: /(?:hole.*radius|radius.*hole|孔半径)/i,
|
||||
},
|
||||
{
|
||||
role: 'depth',
|
||||
pattern: /(?:hole.*depth|depth.*hole|孔深)/i,
|
||||
},
|
||||
{
|
||||
role: 'insetX',
|
||||
pattern:
|
||||
/(?:hole.*(?:insetx|marginx|edgedistancex)|(?:insetx|marginx|edgedistancex).*hole|孔横向边距)/i,
|
||||
},
|
||||
{
|
||||
role: 'insetY',
|
||||
pattern:
|
||||
/(?:hole.*(?:insety|marginy|edgedistancey)|(?:insety|marginy|edgedistancey).*hole|孔纵向边距)/i,
|
||||
},
|
||||
{
|
||||
role: 'spacingX',
|
||||
pattern: /(?:hole.*spacingx|spacingx.*hole|孔横向间距)/i,
|
||||
},
|
||||
{
|
||||
role: 'spacingY',
|
||||
pattern: /(?:hole.*spacingy|spacingy.*hole|孔纵向间距)/i,
|
||||
},
|
||||
{
|
||||
role: 'offsetX',
|
||||
pattern:
|
||||
/(?:hole.*(?:offsetx|centerx|positionx|posx)|(?:offsetx|centerx|positionx|posx).*hole|孔横向中心距)/i,
|
||||
},
|
||||
{
|
||||
role: 'offsetY',
|
||||
pattern:
|
||||
/(?:hole.*(?:offsety|centery|positiony|posy)|(?:offsety|centery|positiony|posy).*hole|孔纵向中心距)/i,
|
||||
},
|
||||
{
|
||||
role: 'inset',
|
||||
pattern:
|
||||
/(?:hole.*(?:inset|margin|edgedistance)|(?:inset|margin|edgedistance).*hole|孔边距|孔距边)/i,
|
||||
},
|
||||
]
|
||||
|
||||
function classifyHoleParameter(
|
||||
parameter: ManufacturingLengthParameter
|
||||
): ClassifiedHoleParameter | undefined {
|
||||
const compactName = compactPartName(parameter.name)
|
||||
const matched = HOLE_PARAMETER_PATTERNS.find(({ pattern }) =>
|
||||
pattern.test(compactName)
|
||||
)
|
||||
if (!matched) return undefined
|
||||
const scope = compactName
|
||||
.replace(matched.pattern, '')
|
||||
.replaceAll(
|
||||
/(?:mounting|hole|diameter|dia|radius|depth|inset|margin|edge|distance|offset|center|position|pos|spacing|孔位|孔径|孔深|孔边距|孔距边|孔)/gi,
|
||||
''
|
||||
)
|
||||
return {
|
||||
...parameter,
|
||||
role: matched.role,
|
||||
scope,
|
||||
}
|
||||
}
|
||||
|
||||
function isPanelBody(body: BodyMeasurement) {
|
||||
const values = (['x', 'y', 'z'] as const)
|
||||
.map((axis) => Math.abs(body.dimensions[axis]))
|
||||
.sort((left, right) => left - right)
|
||||
const normalizedName = compactPartName(normalizePartName(body.name))
|
||||
return (
|
||||
/(panel|board|tabletop|desktop|worktop|countertop|top|seat|shelf|板)/.test(
|
||||
normalizedName
|
||||
) ||
|
||||
(values[0] / Math.max(values[1], EPSILON) < 0.35 &&
|
||||
values[1] / Math.max(values[2], EPSILON) > 0.25)
|
||||
)
|
||||
}
|
||||
|
||||
function panelFaceArea(body: BodyMeasurement) {
|
||||
const values = (['x', 'y', 'z'] as const)
|
||||
.map((axis) => Math.abs(body.dimensions[axis]))
|
||||
.sort((left, right) => left - right)
|
||||
return values[1] * values[2]
|
||||
}
|
||||
|
||||
function bestHoleParameter(
|
||||
parameters: ClassifiedHoleParameter[],
|
||||
role: HoleParameterRole,
|
||||
body: BodyMeasurement,
|
||||
allowGeneric: boolean
|
||||
) {
|
||||
const aliases = partParameterAliases(body.name)
|
||||
const candidates = parameters
|
||||
.filter((parameter) => parameter.role === role)
|
||||
.map((parameter) => ({
|
||||
parameter,
|
||||
score: parameter.scope
|
||||
? parameterAffinity(parameter.scope, aliases)
|
||||
: allowGeneric
|
||||
? 1
|
||||
: 0,
|
||||
}))
|
||||
.filter(({ score }) => score > 0)
|
||||
.sort(
|
||||
(left, right) =>
|
||||
right.score - left.score ||
|
||||
left.parameter.name.localeCompare(right.parameter.name)
|
||||
)
|
||||
if (candidates.length === 0) return undefined
|
||||
if (
|
||||
candidates.length > 1 &&
|
||||
candidates[0].score === candidates[1].score &&
|
||||
candidates[0].parameter.valueMm !== candidates[1].parameter.valueMm
|
||||
) {
|
||||
return undefined
|
||||
}
|
||||
return candidates[0].parameter
|
||||
}
|
||||
|
||||
export function reconcileManufacturingHoles(
|
||||
bodies: BodyMeasurement[],
|
||||
lengthParameters: ManufacturingLengthParameter[]
|
||||
) {
|
||||
const holeParameters = lengthParameters
|
||||
.filter(
|
||||
(parameter) =>
|
||||
parameter.name.trim() &&
|
||||
Number.isFinite(parameter.valueMm) &&
|
||||
Math.abs(parameter.valueMm) > EPSILON
|
||||
)
|
||||
.map(classifyHoleParameter)
|
||||
.filter(
|
||||
(parameter): parameter is ClassifiedHoleParameter =>
|
||||
parameter !== undefined
|
||||
)
|
||||
if (holeParameters.length === 0) return bodies
|
||||
|
||||
const panelBodies = bodies.filter(isPanelBody)
|
||||
const genericTarget = [...panelBodies].sort(
|
||||
(left, right) => panelFaceArea(right) - panelFaceArea(left)
|
||||
)[0]
|
||||
|
||||
return bodies.map((body) => {
|
||||
if (!isPanelBody(body)) return body
|
||||
const allowGeneric = body.id === genericTarget?.id
|
||||
const diameterParameter = bestHoleParameter(
|
||||
holeParameters,
|
||||
'diameter',
|
||||
body,
|
||||
allowGeneric
|
||||
)
|
||||
const radiusParameter = bestHoleParameter(
|
||||
holeParameters,
|
||||
'radius',
|
||||
body,
|
||||
allowGeneric
|
||||
)
|
||||
const depthParameter = bestHoleParameter(
|
||||
holeParameters,
|
||||
'depth',
|
||||
body,
|
||||
allowGeneric
|
||||
)
|
||||
const diameter =
|
||||
diameterParameter?.valueMm ??
|
||||
(radiusParameter ? radiusParameter.valueMm * 2 : undefined)
|
||||
const axes = panelAxes(body.dimensions)
|
||||
const thickness = Math.abs(body.dimensions[axes.depth])
|
||||
const depth = Math.min(depthParameter?.valueMm ?? thickness, thickness)
|
||||
const type: HoleFeature['type'] =
|
||||
depth < thickness - Math.max(0.1, thickness * 0.02)
|
||||
? 'blind-hole'
|
||||
: 'through-hole'
|
||||
|
||||
if ((body.holes ?? []).length > 0) {
|
||||
return {
|
||||
...body,
|
||||
holes: (body.holes ?? []).map((hole) => ({
|
||||
...hole,
|
||||
diameter: roundDimension(diameter ?? hole.diameter),
|
||||
depth: roundDimension(depthParameter ? depth : hole.depth ?? thickness),
|
||||
type: depthParameter ? type : hole.type,
|
||||
})),
|
||||
}
|
||||
}
|
||||
if (diameter === undefined) return body
|
||||
|
||||
const genericInset = bestHoleParameter(
|
||||
holeParameters,
|
||||
'inset',
|
||||
body,
|
||||
allowGeneric
|
||||
)?.valueMm
|
||||
const insetX =
|
||||
bestHoleParameter(holeParameters, 'insetX', body, allowGeneric)?.valueMm ??
|
||||
genericInset
|
||||
const insetY =
|
||||
bestHoleParameter(holeParameters, 'insetY', body, allowGeneric)?.valueMm ??
|
||||
genericInset
|
||||
const spacingX = bestHoleParameter(
|
||||
holeParameters,
|
||||
'spacingX',
|
||||
body,
|
||||
allowGeneric
|
||||
)?.valueMm
|
||||
const spacingY = bestHoleParameter(
|
||||
holeParameters,
|
||||
'spacingY',
|
||||
body,
|
||||
allowGeneric
|
||||
)?.valueMm
|
||||
const offsetX =
|
||||
bestHoleParameter(holeParameters, 'offsetX', body, allowGeneric)?.valueMm ??
|
||||
(spacingX !== undefined
|
||||
? spacingX / 2
|
||||
: insetX === undefined
|
||||
? undefined
|
||||
: Math.abs(body.dimensions[axes.horizontal]) / 2 - insetX)
|
||||
const offsetY =
|
||||
bestHoleParameter(holeParameters, 'offsetY', body, allowGeneric)?.valueMm ??
|
||||
(spacingY !== undefined
|
||||
? spacingY / 2
|
||||
: insetY === undefined
|
||||
? undefined
|
||||
: Math.abs(body.dimensions[axes.vertical]) / 2 - insetY)
|
||||
if (
|
||||
offsetX === undefined ||
|
||||
offsetY === undefined ||
|
||||
offsetX < 0 ||
|
||||
offsetY < 0 ||
|
||||
offsetX + diameter / 2 >
|
||||
Math.abs(body.dimensions[axes.horizontal]) / 2 + EPSILON ||
|
||||
offsetY + diameter / 2 >
|
||||
Math.abs(body.dimensions[axes.vertical]) / 2 + EPSILON
|
||||
) {
|
||||
return body
|
||||
}
|
||||
|
||||
const holes = [-1, 1].flatMap((horizontalDirection) =>
|
||||
[-1, 1].map((verticalDirection): HoleFeature => {
|
||||
const center = { ...body.center }
|
||||
center[axes.horizontal] =
|
||||
body.center[axes.horizontal] + horizontalDirection * offsetX
|
||||
center[axes.vertical] =
|
||||
body.center[axes.vertical] + verticalDirection * offsetY
|
||||
return {
|
||||
type,
|
||||
center,
|
||||
diameter: roundDimension(diameter),
|
||||
axis: axes.depth,
|
||||
depth: roundDimension(depth),
|
||||
}
|
||||
})
|
||||
)
|
||||
return { ...body, holes }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Zoo occasionally reports a transformed scene bounding box while volume stays
|
||||
* in the source part scale. Only correct dimensions when multiple distinct part
|
||||
@ -864,7 +1148,7 @@ export function reconcileManufacturingDimensions(
|
||||
}
|
||||
}
|
||||
|
||||
return bodies.map((body) => {
|
||||
const dimensionBodies = bodies.map<BodyMeasurement>((body) => {
|
||||
const inferred = inferParametricDimensions(body, classifiedParameters)
|
||||
if (inferred) {
|
||||
const corrected = {
|
||||
@ -913,6 +1197,7 @@ export function reconcileManufacturingDimensions(
|
||||
dimensionSource: 'engine-volume-check',
|
||||
}
|
||||
})
|
||||
return reconcileManufacturingHoles(dimensionBodies, lengthParameters)
|
||||
}
|
||||
|
||||
export function localizePartName(name: string) {
|
||||
@ -1579,9 +1864,11 @@ function machiningDetailSvg(row: BomRow) {
|
||||
(hole) =>
|
||||
`<li>H${hole.index}:Ø${roundDimension(
|
||||
hole.diameter
|
||||
)} ${hole.type === 'through-hole' ? '通孔' : `深 ${roundDimension(
|
||||
hole.depth ?? 0
|
||||
)}`},距左 ${hole.fromLeft},距下 ${hole.fromBottom}</li>`
|
||||
)} ${
|
||||
hole.type === 'through-hole'
|
||||
? '通孔'
|
||||
: `盲孔,深 ${roundDimension(hole.depth ?? 0)} mm`
|
||||
},距左 ${hole.fromLeft},距下 ${hole.fromBottom}</li>`
|
||||
)
|
||||
.join('')
|
||||
: '<li>该零件未识别到圆孔特征</li>'
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user