fix: correct HiCAD BOM geometry
This commit is contained in:
parent
5c798c2214
commit
d2123de778
@ -11,6 +11,7 @@ import {
|
||||
getOverallBounds,
|
||||
groupBodiesIntoBom,
|
||||
localizePartName,
|
||||
reconcileManufacturingDimensions,
|
||||
type BodyMeasurement,
|
||||
type CircularEdgeSample,
|
||||
} from '../../vendor/zoo-modeling-app/manufacturing'
|
||||
@ -249,7 +250,118 @@ test.describe('HiCAD manufacturing extension', () => {
|
||||
expect(rows.some((row) => row.name === 'unitBox')).toBe(false)
|
||||
})
|
||||
|
||||
test('reconciles transformed Safari measurements and groups source instances', async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
const transformedBodies: BodyMeasurement[] = [
|
||||
...[
|
||||
['rearRightStudSource', 202.52],
|
||||
['rearLeftStudSource', 188.56],
|
||||
['frontRightStudSource', 165.4],
|
||||
['frontLeftStud', 178.53],
|
||||
].map(
|
||||
([name, volume], index): BodyMeasurement => ({
|
||||
id: `stud-${index}`,
|
||||
name: String(name),
|
||||
center: { x: index % 2 ? 500 : -500, y: index < 2 ? -200 : 200, z: 760 },
|
||||
dimensions: { x: 6, y: 6, z: 10 },
|
||||
volume: Number(volume),
|
||||
})
|
||||
),
|
||||
...[
|
||||
'rearRightLegSource',
|
||||
'rearLeftLegSource',
|
||||
'frontRightLegSource',
|
||||
'frontLeftLeg',
|
||||
].map(
|
||||
(name, index): BodyMeasurement => ({
|
||||
id: `leg-${index}`,
|
||||
name,
|
||||
center: { x: index % 2 ? 500 : -500, y: index < 2 ? -200 : 200, z: 375 },
|
||||
dimensions: { x: 60, y: 60, z: 750 },
|
||||
volume: 1_463_300 + index * 12,
|
||||
})
|
||||
),
|
||||
{
|
||||
id: 'tabletop',
|
||||
name: 'tableTop',
|
||||
center: { x: 0, y: 0, z: 762.5 },
|
||||
dimensions: { x: 1440, y: 720, z: 25.1 },
|
||||
volume: 17_976_779.67,
|
||||
},
|
||||
]
|
||||
|
||||
const reconciled = reconcileManufacturingDimensions(transformedBodies)
|
||||
const rows = groupBodiesIntoBom(reconciled)
|
||||
|
||||
expect(rows).toHaveLength(3)
|
||||
expect(rows.find((row) => row.name === '连接螺柱')).toMatchObject({
|
||||
quantity: 4,
|
||||
dimensions: { x: 5, y: 5, z: 10 },
|
||||
})
|
||||
expect(rows.find((row) => row.name === '支腿')).toMatchObject({
|
||||
quantity: 4,
|
||||
dimensions: { x: 50, y: 50, z: 750 },
|
||||
})
|
||||
expect(rows.find((row) => row.name === '台面板')).toMatchObject({
|
||||
quantity: 1,
|
||||
dimensions: { x: 1200, y: 600, z: 25 },
|
||||
})
|
||||
|
||||
const html = createEngineeringSheetHtml({
|
||||
projectName: '圆腿桌',
|
||||
generatedAt: '2026-07-28 10:00',
|
||||
overallDimensions: { x: 1200, y: 600, z: 775 },
|
||||
bodies: reconciled,
|
||||
bomRows: rows,
|
||||
})
|
||||
expect(html).toContain('data-cross-section="circular"')
|
||||
expect(html).toContain('class="circular-part-profile"')
|
||||
expect(html).toContain('data-profile="circular"')
|
||||
expect(html).toContain('直径 Ø50 mm')
|
||||
expect(html).toContain('Ø50 mm')
|
||||
|
||||
await page.setViewportSize({ width: 1440, height: 1000 })
|
||||
await page.setContent(html)
|
||||
await expect(page.locator('.circular-part-profile')).toHaveCount(2)
|
||||
const legProfile = page
|
||||
.locator('.part-detail[data-cross-section="circular"]')
|
||||
.filter({ hasText: '支腿' })
|
||||
.locator('.circular-part-profile')
|
||||
const profileBounds = await legProfile.boundingBox()
|
||||
expect(profileBounds).not.toBeNull()
|
||||
expect(Math.abs((profileBounds?.width ?? 0) - (profileBounds?.height ?? 0))).toBeLessThan(
|
||||
1
|
||||
)
|
||||
await page.screenshot({
|
||||
fullPage: true,
|
||||
path: testInfo.outputPath('safari-round-leg-engineering-sheet.png'),
|
||||
})
|
||||
})
|
||||
|
||||
test('keeps a square leg end view rectangular', () => {
|
||||
const squareLeg: BodyMeasurement = {
|
||||
id: 'square-leg',
|
||||
name: 'leg',
|
||||
center: { x: 0, y: 0, z: 375 },
|
||||
dimensions: { x: 50, y: 50, z: 750 },
|
||||
volume: 1_875_000,
|
||||
}
|
||||
const html = createEngineeringSheetHtml({
|
||||
projectName: '方腿桌',
|
||||
generatedAt: '2026-07-28 10:00',
|
||||
overallDimensions: { x: 50, y: 50, z: 750 },
|
||||
bodies: [squareLeg],
|
||||
bomRows: groupBodiesIntoBom([squareLeg]),
|
||||
})
|
||||
|
||||
expect(html).toContain('data-cross-section="rectangular"')
|
||||
expect(html).not.toContain('class="circular-part-profile"')
|
||||
})
|
||||
|
||||
test('localizes the hardware validation assembly for a Chinese BOM', () => {
|
||||
expect(localizePartName('seatBody')).toBe('座板')
|
||||
expect(localizePartName('tableBody')).toBe('桌体')
|
||||
expect(localizePartName('mountingPanel')).toBe('安装基板')
|
||||
expect(localizePartName('connectorPlate')).toBe('连接片')
|
||||
expect(localizePartName('connectorBolt4')).toBe('连接螺栓')
|
||||
|
||||
@ -15,6 +15,7 @@ import {
|
||||
filterManufacturingBodies,
|
||||
getOverallBounds,
|
||||
groupBodiesIntoBom,
|
||||
reconcileManufacturingDimensions,
|
||||
type BodyMeasurement,
|
||||
type BomRow,
|
||||
type CircularEdgeSample,
|
||||
@ -533,7 +534,9 @@ export function HicadManufacturingPanel({
|
||||
)
|
||||
}
|
||||
|
||||
const manufacturingBodies = filterManufacturingBodies(measured)
|
||||
const manufacturingBodies = reconcileManufacturingDimensions(
|
||||
filterManufacturingBodies(measured)
|
||||
)
|
||||
if (manufacturingBodies.length < measured.length) {
|
||||
toast(
|
||||
`已忽略 ${measured.length - manufacturingBodies.length} 个建模模板实体。`
|
||||
|
||||
290
vendor/zoo-modeling-app/manufacturing.ts
vendored
290
vendor/zoo-modeling-app/manufacturing.ts
vendored
@ -410,12 +410,23 @@ export function buildExplodeOffsets(
|
||||
|
||||
function normalizePartName(name: string) {
|
||||
const cleaned = name.trim() || '未命名零件'
|
||||
const withoutCopySuffix =
|
||||
cleaned.replace(/(?:[_-]?(?:copy|part))?[_-]?\d+$/i, '').trim() || cleaned
|
||||
const locationLegMatch = withoutCopySuffix.match(
|
||||
/^(?:front|back|rear)(?:left|right)(leg|foot)$/i
|
||||
const withoutInstanceSuffix =
|
||||
cleaned
|
||||
.replace(
|
||||
/(?:[_-]?(?:copy|part|source|solid|instance))[_-]?\d*$/i,
|
||||
''
|
||||
)
|
||||
.replace(/[_-]?\d+$/i, '')
|
||||
.trim() || cleaned
|
||||
const directionalPartMatch = withoutInstanceSuffix.match(
|
||||
/^(?:front|back|rear)(?:left|right)(leg|foot|stud)$/i
|
||||
)
|
||||
return locationLegMatch?.[1] ?? withoutCopySuffix
|
||||
const normalized = directionalPartMatch?.[1] ?? withoutInstanceSuffix
|
||||
|
||||
if (/^(?:connector|mounting)?stud$/i.test(normalized)) {
|
||||
return 'connectorStud'
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
const CHINESE_PART_NAMES: Record<string, string> = {
|
||||
@ -428,6 +439,7 @@ const CHINESE_PART_NAMES: Record<string, string> = {
|
||||
camlock: '三合一偏心轮',
|
||||
connectorbolt: '连接螺栓',
|
||||
connectorplate: '连接片',
|
||||
connectorstud: '连接螺柱',
|
||||
dividerpanel: '隔板',
|
||||
doorpanel: '门板',
|
||||
dowel: '木榫',
|
||||
@ -458,6 +470,136 @@ const CHINESE_PART_NAMES: Record<string, string> = {
|
||||
washer: '垫圈',
|
||||
}
|
||||
|
||||
type DimensionCorrectionCandidate = {
|
||||
axes: Array<keyof Point3d>
|
||||
scale: number
|
||||
}
|
||||
|
||||
function median(values: number[]) {
|
||||
const sorted = [...values].sort((left, right) => left - right)
|
||||
const middle = Math.floor(sorted.length / 2)
|
||||
return sorted.length % 2 === 0
|
||||
? (sorted[middle - 1] + sorted[middle]) / 2
|
||||
: sorted[middle]
|
||||
}
|
||||
|
||||
function correctionGroupKey(body: BodyMeasurement) {
|
||||
return `${compactPartName(normalizePartName(body.name))}::${dimensionsKey(
|
||||
body.dimensions
|
||||
)}`
|
||||
}
|
||||
|
||||
function dimensionCorrectionCandidate(
|
||||
body: BodyMeasurement,
|
||||
representativeVolume: number
|
||||
): DimensionCorrectionCandidate | undefined {
|
||||
const axes = (['x', 'y', 'z'] as Array<keyof Point3d>).sort(
|
||||
(left, right) =>
|
||||
Math.abs(body.dimensions[left]) - Math.abs(body.dimensions[right])
|
||||
)
|
||||
const values = axes.map((axis) => Math.abs(body.dimensions[axis]))
|
||||
const boundingVolume = values[0] * values[1] * values[2]
|
||||
if (!(representativeVolume > EPSILON) || !(boundingVolume > EPSILON)) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const partName = compactPartName(normalizePartName(body.name))
|
||||
const isPanel =
|
||||
/(panel|board|tabletop|top|seat|shelf)$/.test(partName) &&
|
||||
values[0] / values[1] < 0.35
|
||||
const isRoundRod =
|
||||
/(leg|foot|stud|dowel|pin)$/.test(partName) &&
|
||||
values[1] / values[0] < 1.25 &&
|
||||
values[2] / values[1] > 1.4
|
||||
|
||||
if (isPanel) {
|
||||
return {
|
||||
axes: [axes[1], axes[2]],
|
||||
scale: Math.sqrt(representativeVolume / boundingVolume),
|
||||
}
|
||||
}
|
||||
if (isRoundRod) {
|
||||
return {
|
||||
axes: [axes[0], axes[1]],
|
||||
scale: Math.sqrt(
|
||||
representativeVolume / ((Math.PI / 4) * boundingVolume)
|
||||
),
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function snapCorrectedDimension(value: number) {
|
||||
const absolute = Math.abs(value)
|
||||
const step = absolute >= 100 ? 10 : absolute >= 20 ? 1 : 0.5
|
||||
const snapped = Math.round(absolute / step) * step
|
||||
return Math.abs(snapped - absolute) <= Math.max(step * 0.55, absolute * 0.01)
|
||||
? snapped
|
||||
: roundDimension(absolute)
|
||||
}
|
||||
|
||||
/**
|
||||
* Zoo occasionally reports a transformed scene bounding box while volume stays
|
||||
* in the source part scale. Only correct dimensions when multiple distinct part
|
||||
* families independently confirm the same scale, avoiding guesses from one
|
||||
* hollow or filleted part.
|
||||
*/
|
||||
export function reconcileManufacturingDimensions(
|
||||
bodies: BodyMeasurement[]
|
||||
): BodyMeasurement[] {
|
||||
const groups = new Map<string, BodyMeasurement[]>()
|
||||
for (const body of bodies) {
|
||||
const key = correctionGroupKey(body)
|
||||
groups.set(key, [...(groups.get(key) ?? []), body])
|
||||
}
|
||||
|
||||
const candidates = new Map<string, DimensionCorrectionCandidate>()
|
||||
for (const [key, group] of groups) {
|
||||
const volumes = group
|
||||
.map((body) => body.volume)
|
||||
.filter((volume): volume is number => Number.isFinite(volume))
|
||||
if (volumes.length === 0) continue
|
||||
|
||||
const candidate = dimensionCorrectionCandidate(group[0], median(volumes))
|
||||
if (candidate && candidate.scale >= 0.72 && candidate.scale <= 0.92) {
|
||||
candidates.set(key, candidate)
|
||||
}
|
||||
}
|
||||
|
||||
if (candidates.size < 2) return bodies
|
||||
|
||||
const consensusScale = median(
|
||||
[...candidates.values()].map((candidate) => candidate.scale)
|
||||
)
|
||||
const supported = [...candidates.values()].filter(
|
||||
(candidate) => Math.abs(candidate.scale - consensusScale) <= 0.055
|
||||
)
|
||||
if (supported.length < 2) return bodies
|
||||
|
||||
return bodies.map((body) => {
|
||||
const candidate = candidates.get(correctionGroupKey(body))
|
||||
if (
|
||||
!candidate ||
|
||||
Math.abs(candidate.scale - consensusScale) > 0.055
|
||||
) {
|
||||
return body
|
||||
}
|
||||
|
||||
const corrected = { ...body.dimensions }
|
||||
for (const axis of candidate.axes) {
|
||||
corrected[axis] *= consensusScale
|
||||
}
|
||||
return {
|
||||
...body,
|
||||
dimensions: {
|
||||
x: snapCorrectedDimension(corrected.x),
|
||||
y: snapCorrectedDimension(corrected.y),
|
||||
z: snapCorrectedDimension(corrected.z),
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function localizePartName(name: string) {
|
||||
const cleaned = normalizePartName(name.trim() || '未命名零件')
|
||||
if (/[\u3400-\u9fff]/.test(cleaned)) return cleaned
|
||||
@ -881,10 +1023,43 @@ function uniqueRounded(values: number[]) {
|
||||
.sort((left, right) => left - right)
|
||||
}
|
||||
|
||||
function isCircularPrismaticPart(row: BomRow) {
|
||||
const values = [
|
||||
Math.abs(row.dimensions.x),
|
||||
Math.abs(row.dimensions.y),
|
||||
Math.abs(row.dimensions.z),
|
||||
].sort((left, right) => left - right)
|
||||
const [diameterA, diameterB, length] = values
|
||||
if (
|
||||
diameterA <= EPSILON ||
|
||||
diameterB / diameterA > 1.12 ||
|
||||
length / diameterB < 1.4
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
const compactName = compactPartName(row.name)
|
||||
const explicitlyRound =
|
||||
/(圆形|圆柱|圆管|螺柱|木榫|销|round|cylinder|stud|dowel|pin)/i.test(
|
||||
compactName
|
||||
)
|
||||
const possiblyRound =
|
||||
explicitlyRound || /(支腿|leg|foot)/i.test(compactName)
|
||||
if (!possiblyRound) return false
|
||||
if (explicitlyRound && row.volume === undefined) return true
|
||||
if (!(row.volume && row.volume > EPSILON)) return false
|
||||
|
||||
const cylinderVolume =
|
||||
(Math.PI / 4) * diameterA * diameterB * length
|
||||
const fillRatio = row.volume / cylinderVolume
|
||||
return fillRatio >= 0.65 && fillRatio <= 1.15
|
||||
}
|
||||
|
||||
function sideDimensionSvg(row: BomRow) {
|
||||
const axes = panelAxes(row.dimensions)
|
||||
const horizontal = Math.abs(row.dimensions[axes.depth])
|
||||
const vertical = Math.abs(row.dimensions[axes.vertical])
|
||||
const isCircularSection = isCircularPrismaticPart(row)
|
||||
const maxObjectWidth = 112
|
||||
const maxObjectHeight = 145
|
||||
const scale = Math.min(
|
||||
@ -928,41 +1103,30 @@ function sideDimensionSvg(row: BomRow) {
|
||||
y2="${roundDimension(centerY + radius)}"/>`
|
||||
})
|
||||
.join('')
|
||||
|
||||
return `
|
||||
<svg class="part-side-view" viewBox="0 0 250 245" role="img"
|
||||
aria-label="${escapeHtml(row.name)} 侧面尺寸图">
|
||||
<defs>
|
||||
<marker id="side-arrow-${row.itemNumber}" viewBox="0 0 10 10" refX="5" refY="5"
|
||||
markerWidth="5" markerHeight="5" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#111827"/>
|
||||
</marker>
|
||||
</defs>
|
||||
<text x="125" y="18" text-anchor="middle" font-weight="700">侧面图</text>
|
||||
const profileMarkup = isCircularSection
|
||||
? `
|
||||
<circle class="circular-part-profile" data-profile="circular"
|
||||
cx="${roundDimension(objectX + objectWidth / 2)}"
|
||||
cy="${roundDimension(objectY + objectHeight / 2)}"
|
||||
r="${roundDimension(Math.min(objectWidth, objectHeight) / 2)}"
|
||||
fill="#f9fafb" stroke="#111827" stroke-width="1.6"/>
|
||||
<line class="center-line" x1="${roundDimension(objectX - 7)}"
|
||||
y1="${roundDimension(objectY + objectHeight / 2)}"
|
||||
x2="${roundDimension(objectX + objectWidth + 7)}"
|
||||
y2="${roundDimension(objectY + objectHeight / 2)}"/>
|
||||
<line class="center-line" x1="${roundDimension(
|
||||
objectX + objectWidth / 2
|
||||
)}" y1="${roundDimension(objectY - 7)}"
|
||||
x2="${roundDimension(objectX + objectWidth / 2)}"
|
||||
y2="${roundDimension(objectY + objectHeight + 7)}"/>`
|
||||
: `
|
||||
<rect x="${roundDimension(objectX)}" y="${roundDimension(objectY)}"
|
||||
width="${roundDimension(objectWidth)}" height="${roundDimension(
|
||||
objectHeight
|
||||
)}" fill="#f9fafb" stroke="#111827" stroke-width="1.6"/>
|
||||
${hiddenHoleMarkup}
|
||||
<line x1="${roundDimension(objectX)}" y1="${roundDimension(
|
||||
objectY + objectHeight + 22
|
||||
)}" x2="${roundDimension(objectX + objectWidth)}"
|
||||
y2="${roundDimension(objectY + objectHeight + 22)}" stroke="#111827"
|
||||
marker-start="url(#side-arrow-${row.itemNumber})"
|
||||
marker-end="url(#side-arrow-${row.itemNumber})"/>
|
||||
<line x1="${roundDimension(objectX)}" y1="${roundDimension(
|
||||
objectY + objectHeight
|
||||
)}" x2="${roundDimension(objectX)}" y2="${roundDimension(
|
||||
objectY + objectHeight + 30
|
||||
)}" stroke="#6b7280"/>
|
||||
<line x1="${roundDimension(objectX + objectWidth)}" y1="${roundDimension(
|
||||
objectY + objectHeight
|
||||
)}" x2="${roundDimension(objectX + objectWidth)}" y2="${roundDimension(
|
||||
objectY + objectHeight + 30
|
||||
)}" stroke="#6b7280"/>
|
||||
<text x="${roundDimension(objectX + objectWidth / 2)}"
|
||||
y="${roundDimension(objectY + objectHeight + 39)}"
|
||||
text-anchor="middle">${formatDimension(horizontal)}</text>
|
||||
)}" fill="#f9fafb" stroke="#111827" stroke-width="1.6"/>`
|
||||
const verticalDimensionMarkup = isCircularSection
|
||||
? ''
|
||||
: `
|
||||
<line x1="${roundDimension(objectX - 22)}" y1="${roundDimension(objectY)}"
|
||||
x2="${roundDimension(objectX - 22)}" y2="${roundDimension(
|
||||
objectY + objectHeight
|
||||
@ -982,7 +1146,42 @@ function sideDimensionSvg(row: BomRow) {
|
||||
objectX - 37
|
||||
)} ${roundDimension(objectY + objectHeight / 2)})">${formatDimension(
|
||||
vertical
|
||||
)}</text>
|
||||
)}</text>`
|
||||
|
||||
return `
|
||||
<svg class="part-side-view" viewBox="0 0 250 245" role="img"
|
||||
aria-label="${escapeHtml(row.name)} 侧面尺寸图">
|
||||
<defs>
|
||||
<marker id="side-arrow-${row.itemNumber}" viewBox="0 0 10 10" refX="5" refY="5"
|
||||
markerWidth="5" markerHeight="5" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#111827"/>
|
||||
</marker>
|
||||
</defs>
|
||||
<text x="125" y="18" text-anchor="middle" font-weight="700">侧面图</text>
|
||||
${profileMarkup}
|
||||
${hiddenHoleMarkup}
|
||||
<line x1="${roundDimension(objectX)}" y1="${roundDimension(
|
||||
objectY + objectHeight + 22
|
||||
)}" x2="${roundDimension(objectX + objectWidth)}"
|
||||
y2="${roundDimension(objectY + objectHeight + 22)}" stroke="#111827"
|
||||
marker-start="url(#side-arrow-${row.itemNumber})"
|
||||
marker-end="url(#side-arrow-${row.itemNumber})"/>
|
||||
<line x1="${roundDimension(objectX)}" y1="${roundDimension(
|
||||
objectY + objectHeight
|
||||
)}" x2="${roundDimension(objectX)}" y2="${roundDimension(
|
||||
objectY + objectHeight + 30
|
||||
)}" stroke="#6b7280"/>
|
||||
<line x1="${roundDimension(objectX + objectWidth)}" y1="${roundDimension(
|
||||
objectY + objectHeight
|
||||
)}" x2="${roundDimension(objectX + objectWidth)}" y2="${roundDimension(
|
||||
objectY + objectHeight + 30
|
||||
)}" stroke="#6b7280"/>
|
||||
<text x="${roundDimension(objectX + objectWidth / 2)}"
|
||||
y="${roundDimension(objectY + objectHeight + 39)}"
|
||||
text-anchor="middle">${
|
||||
isCircularSection ? 'Ø' : ''
|
||||
}${formatDimension(horizontal)}</text>
|
||||
${verticalDimensionMarkup}
|
||||
</svg>`
|
||||
}
|
||||
|
||||
@ -991,6 +1190,7 @@ function machiningDetailSvg(row: BomRow) {
|
||||
const horizontal = Math.abs(row.dimensions[axes.horizontal])
|
||||
const vertical = Math.abs(row.dimensions[axes.vertical])
|
||||
const depth = Math.abs(row.dimensions[axes.depth])
|
||||
const isCircularSection = isCircularPrismaticPart(row)
|
||||
const objectMaxWidth = 230
|
||||
const objectMaxHeight = 145
|
||||
const scale = Math.min(
|
||||
@ -1078,10 +1278,14 @@ function machiningDetailSvg(row: BomRow) {
|
||||
.join(';')
|
||||
|
||||
return `
|
||||
<article class="part-detail">
|
||||
<article class="part-detail" data-cross-section="${
|
||||
isCircularSection ? 'circular' : 'rectangular'
|
||||
}">
|
||||
<div class="part-detail-heading">
|
||||
<strong>${row.itemNumber}. ${escapeHtml(row.name)}</strong>
|
||||
<span>数量 ${row.quantity} · 厚度 ${formatDimension(depth)}</span>
|
||||
<span>数量 ${row.quantity} · ${
|
||||
isCircularSection ? '直径 Ø' : '厚度 '
|
||||
}${formatDimension(depth)}</span>
|
||||
</div>
|
||||
<div class="part-detail-layout">
|
||||
<svg class="part-front-view" viewBox="0 0 390 245" role="img" aria-label="${escapeHtml(
|
||||
@ -1135,9 +1339,9 @@ function machiningDetailSvg(row: BomRow) {
|
||||
y="${roundDimension(objectY + objectHeight / 2)}" text-anchor="middle"
|
||||
transform="rotate(-90 ${roundDimension(
|
||||
objectX - 37
|
||||
)} ${roundDimension(objectY + objectHeight / 2)})">${formatDimension(
|
||||
vertical
|
||||
)}</text>
|
||||
)} ${roundDimension(objectY + objectHeight / 2)})">${
|
||||
isCircularSection ? 'Ø' : ''
|
||||
}${formatDimension(vertical)}</text>
|
||||
</svg>
|
||||
${sideDimensionSvg(row)}
|
||||
<div class="hole-schedule">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user