diff --git a/tests/routes/hicad-manufacturing-extension.test.ts b/tests/routes/hicad-manufacturing-extension.test.ts
index 50f30e080..184ea3bf5 100644
--- a/tests/routes/hicad-manufacturing-extension.test.ts
+++ b/tests/routes/hicad-manufacturing-extension.test.ts
@@ -168,6 +168,9 @@ test.describe('HiCAD manufacturing extension', () => {
),
'utf8'
)
+ expect(launcher).toContain("document.querySelectorAll('canvas')")
+ expect(launcher).toContain('left: Math.round(canvas.rect.left + 16)')
+ expect(launcher).toContain('className="fixed z-20')
expect(launcher).toContain("lazy(() =>")
expect(launcher).toContain('ManufacturingBoundary')
@@ -280,6 +283,9 @@ test.describe('HiCAD manufacturing extension', () => {
expect(html).toContain('横向中心孔距 320 mm')
expect(html).toContain('纵向中心孔距 200 mm')
expect(html.match(/class="part-detail"/g)).toHaveLength(4)
+ expect(html).toContain('data-generated-exploded-view="true"')
+ expect(html.match(/class="explode-part"/g)).toHaveLength(10)
+ expect(html).not.toContain('暂无爆炸图快照')
await page.setViewportSize({ width: 1600, height: 900 })
await page.setContent(html)
diff --git a/vendor/zoo-modeling-app/HicadManufacturingLauncher.tsx.template b/vendor/zoo-modeling-app/HicadManufacturingLauncher.tsx.template
index 0cc43429c..2c6dc0ba0 100644
--- a/vendor/zoo-modeling-app/HicadManufacturingLauncher.tsx.template
+++ b/vendor/zoo-modeling-app/HicadManufacturingLauncher.tsx.template
@@ -2,6 +2,7 @@ import {
Component,
lazy,
Suspense,
+ useEffect,
useState,
type ErrorInfo,
type ReactNode,
@@ -63,6 +64,52 @@ export function HicadManufacturingLauncher({
projectName: string
}) {
const [active, setActive] = useState(false)
+ const [triggerPosition, setTriggerPosition] = useState({
+ left: 16,
+ bottom: 48,
+ })
+
+ useEffect(() => {
+ const updateTriggerPosition = () => {
+ const canvas = [...document.querySelectorAll('canvas')]
+ .map((element) => ({
+ element,
+ rect: element.getBoundingClientRect(),
+ }))
+ .filter(({ rect }) => rect.width > 280 && rect.height > 220)
+ .sort(
+ (left, right) =>
+ right.rect.width * right.rect.height -
+ left.rect.width * left.rect.height
+ )[0]
+
+ if (!canvas) return
+ setTriggerPosition({
+ left: Math.round(canvas.rect.left + 16),
+ bottom: Math.round(
+ Math.max(window.innerHeight - canvas.rect.bottom + 16, 48)
+ ),
+ })
+ }
+
+ const observer = new ResizeObserver(updateTriggerPosition)
+ const observeCanvases = () =>
+ document.querySelectorAll('canvas').forEach((canvas) => observer.observe(canvas))
+ const mutationObserver = new MutationObserver(() => {
+ observeCanvases()
+ updateTriggerPosition()
+ })
+ observeCanvases()
+ mutationObserver.observe(document.body, { childList: true, subtree: true })
+ window.addEventListener('resize', updateTriggerPosition)
+ updateTriggerPosition()
+
+ return () => {
+ observer.disconnect()
+ mutationObserver.disconnect()
+ window.removeEventListener('resize', updateTriggerPosition)
+ }
+ }, [])
return (
<>
@@ -70,7 +117,8 @@ export function HicadManufacturingLauncher({
type="button"
data-testid="hicad-manufacturing-trigger"
onClick={() => setActive(true)}
- className="absolute right-4 top-14 z-20 h-9 border border-chalkboard-30 bg-chalkboard-10 px-3 text-sm font-medium shadow-sm hover:bg-chalkboard-20 dark:border-chalkboard-70 dark:bg-chalkboard-90 dark:hover:bg-chalkboard-80"
+ style={triggerPosition}
+ className="fixed z-20 h-9 border border-chalkboard-30 bg-chalkboard-10 px-3 text-sm font-medium shadow-sm hover:bg-chalkboard-20 dark:border-chalkboard-70 dark:bg-chalkboard-90 dark:hover:bg-chalkboard-80"
>
工程出图
diff --git a/vendor/zoo-modeling-app/manufacturing.ts b/vendor/zoo-modeling-app/manufacturing.ts
index 4fbd35482..7947f5a6a 100644
--- a/vendor/zoo-modeling-app/manufacturing.ts
+++ b/vendor/zoo-modeling-app/manufacturing.ts
@@ -593,6 +593,167 @@ function escapeHtml(value: string) {
.replaceAll("'", ''')
}
+function createExplodedAssemblySvg(
+ bodies: BodyMeasurement[],
+ bomRows: BomRow[]
+) {
+ const parts = filterManufacturingBodies(bodies)
+ const offsets = new Map(
+ buildExplodeOffsets(parts, 0.42).map(({ id, offset }) => [id, offset])
+ )
+ const itemNumberByBodyId = new Map(
+ bomRows.flatMap((row) =>
+ row.bodyIds.map((bodyId) => [bodyId, row.itemNumber] as const)
+ )
+ )
+ const boxes = parts.map((body, index) => {
+ const offset = offsets.get(body.id) ?? { x: 0, y: 0, z: 0 }
+ const center = {
+ x: body.center.x + offset.x,
+ y: body.center.y + offset.y,
+ z: body.center.z + offset.z,
+ }
+ const half = {
+ x: Math.max(Math.abs(body.dimensions.x) / 2, 0.5),
+ y: Math.max(Math.abs(body.dimensions.y) / 2, 0.5),
+ z: Math.max(Math.abs(body.dimensions.z) / 2, 0.5),
+ }
+ const corner = (x: number, y: number, z: number) => ({
+ x: center.x + half.x * x,
+ y: center.y + half.y * y,
+ z: center.z + half.z * z,
+ })
+ return {
+ body,
+ center,
+ itemNumber: itemNumberByBodyId.get(body.id) ?? index + 1,
+ corners: {
+ a: corner(-1, -1, -1),
+ b: corner(1, -1, -1),
+ c: corner(1, 1, -1),
+ e: corner(-1, -1, 1),
+ f: corner(1, -1, 1),
+ g: corner(1, 1, 1),
+ h: corner(-1, 1, 1),
+ },
+ }
+ })
+ const project = (point: Point3d) => ({
+ x: (point.x - point.y) * 0.866,
+ y: (point.x + point.y) * 0.42 - point.z,
+ })
+ const projectedCorners = boxes.flatMap(({ corners }) =>
+ Object.values(corners).map(project)
+ )
+ const minX = Math.min(...projectedCorners.map((point) => point.x), 0)
+ const maxX = Math.max(...projectedCorners.map((point) => point.x), 1)
+ const minY = Math.min(...projectedCorners.map((point) => point.y), 0)
+ const maxY = Math.max(...projectedCorners.map((point) => point.y), 1)
+ const width = 760
+ const height = 310
+ const drawingLeft = 24
+ const drawingTop = 18
+ const drawingWidth = 535
+ const drawingHeight = 274
+ const scale = Math.min(
+ drawingWidth / Math.max(maxX - minX, EPSILON),
+ drawingHeight / Math.max(maxY - minY, EPSILON)
+ )
+ const screenPoint = (point: Point3d) => {
+ const projected = project(point)
+ return {
+ x: drawingLeft + (projected.x - minX) * scale,
+ y: drawingTop + (projected.y - minY) * scale,
+ }
+ }
+ const points = (values: Point3d[]) =>
+ values
+ .map(screenPoint)
+ .map((point) => `${roundDimension(point.x)},${roundDimension(point.y)}`)
+ .join(' ')
+ const palettes = [
+ ['#d6c0a0', '#b79b77', '#947856'],
+ ['#d1d5db', '#9ca3af', '#6b7280'],
+ ['#bfdbfe', '#93c5fd', '#60a5fa'],
+ ['#ddd6fe', '#c4b5fd', '#a78bfa'],
+ ['#bbf7d0', '#86efac', '#4ade80'],
+ ]
+ const partMarkup = [...boxes]
+ .sort((left, right) => project(left.center).y - project(right.center).y)
+ .map(({ body, center, corners, itemNumber }) => {
+ const palette = palettes[(itemNumber - 1) % palettes.length]
+ const calloutStart = screenPoint(center)
+ const calloutEnd = {
+ x: Math.min(calloutStart.x + 20, drawingLeft + drawingWidth - 12),
+ y: Math.max(calloutStart.y - 18, drawingTop + 12),
+ }
+ return `
+ `
- : '