From 46f1d97593e6de9ff4e4a0a9d8ee59bbfd74f816 Mon Sep 17 00:00:00 2001 From: 134 Date: Mon, 27 Jul 2026 21:06:08 +0800 Subject: [PATCH] fix: make HiCAD engineering analysis resilient --- .../hicad-manufacturing-extension.test.ts | 12 ++ .../HicadManufacturingPanel.tsx.template | 184 +++++++++++++++--- 2 files changed, 167 insertions(+), 29 deletions(-) diff --git a/tests/routes/hicad-manufacturing-extension.test.ts b/tests/routes/hicad-manufacturing-extension.test.ts index 90c84de1b..d4f850a28 100644 --- a/tests/routes/hicad-manufacturing-extension.test.ts +++ b/tests/routes/hicad-manufacturing-extension.test.ts @@ -76,6 +76,18 @@ test.describe('HiCAD manufacturing extension', () => { ) expect(launcher).toContain("lazy(() =>") expect(launcher).toContain('ManufacturingBoundary') + + const panel = fs.readFileSync( + path.join( + projectRoot, + 'vendor/zoo-modeling-app/HicadManufacturingPanel.tsx.template' + ), + 'utf8' + ) + expect(panel).toContain("type: 'scene_get_entity_ids'") + expect(panel).toContain("filter: ['solid3d']") + expect(panel).toContain("readBoundingBox([])") + expect(panel).toContain('engineFailureMessage(response)') }) test('creates non-zero reversible offsets for every independent body', () => { diff --git a/vendor/zoo-modeling-app/HicadManufacturingPanel.tsx.template b/vendor/zoo-modeling-app/HicadManufacturingPanel.tsx.template index 993a4365d..c334ac39f 100644 --- a/vendor/zoo-modeling-app/HicadManufacturingPanel.tsx.template +++ b/vendor/zoo-modeling-app/HicadManufacturingPanel.tsx.template @@ -86,11 +86,37 @@ function resolveBodyName( function modelingResponseData(response: unknown): ModelingResponse { const normalized = Array.isArray(response) ? response[0] : response if (!isModelingResponse(normalized)) { - throw new Error('HiCAD 引擎未返回有效结果') + throw new Error(engineFailureMessage(response)) } return normalized.resp.data.modeling_response as ModelingResponse } +function engineFailureMessage(response: unknown) { + const responses = Array.isArray(response) ? response : [response] + const messages = responses.flatMap((item) => { + if (item instanceof Error) return [item.message] + if (typeof item !== 'object' || item === null || !('errors' in item)) { + return [] + } + const errors = (item as { errors?: unknown }).errors + if (!Array.isArray(errors)) return [] + return errors.flatMap((error) => { + if (typeof error === 'string') return [error] + if ( + typeof error === 'object' && + error !== null && + 'message' in error && + typeof error.message === 'string' + ) { + return [error.message] + } + return [] + }) + }) + + return messages.find((message) => message.trim()) ?? 'HiCAD 引擎未返回有效结果' +} + function numberValue(data: unknown, key: string) { if (typeof data !== 'object' || data === null) return undefined const value = (data as Record)[key] @@ -175,7 +201,13 @@ export function HicadManufacturingPanel({ return modelingResponseData(normalized) } if (response !== null && response !== undefined) { - return modelingResponseData(response) + const message = engineFailureMessage(response) + console.error('[HiCAD manufacturing] Engine command failed', { + command: cmd.type, + message, + response, + }) + throw new Error(message) } await new Promise((resolve) => setTimeout(resolve, 200)) @@ -184,6 +216,50 @@ export function HicadManufacturingPanel({ throw new Error('HiCAD 引擎连接尚未就绪,请稍后重新分析') } + const getSceneSolidIds = async () => { + const response = await sendModelingCommand({ + type: 'scene_get_entity_ids', + filter: ['solid3d'], + skip: 0, + take: 1000, + }) + if ( + response.type !== 'scene_get_entity_ids' || + typeof response.data !== 'object' || + response.data === null || + !('entity_ids' in response.data) || + !Array.isArray(response.data.entity_ids) + ) { + return [] + } + + return [ + ...new Set( + response.data.entity_ids.flatMap((ids) => + Array.isArray(ids) + ? ids.filter((id): id is string => typeof id === 'string') + : [] + ) + ), + ] + } + + const readBoundingBox = async (entityIds: string[]) => { + const response = await sendModelingCommand({ + type: 'bounding_box', + entity_ids: entityIds, + output_unit: 'mm', + }) + if ( + response.type !== 'bounding_box' || + typeof response.data !== 'object' || + response.data === null + ) { + throw new Error('HiCAD 引擎没有返回尺寸数据') + } + return response.data as BoundingBox + } + const captureSnapshot = async () => { const response = await sendModelingCommand({ type: 'take_snapshot', @@ -239,8 +315,26 @@ export function HicadManufacturingPanel({ await resetExplodeTransforms() } - const bodyEntries = [...getBodiesFromArtifactGraph(kclManager.artifactGraph)] - if (bodyEntries.length === 0) { + const artifactBodies = [ + ...getBodiesFromArtifactGraph( + kclManager.execStateSignal.value.artifactGraph + ), + ] + let sceneBodyIds: string[] = [] + try { + sceneBodyIds = await getSceneSolidIds() + } catch (error) { + console.warn( + '[HiCAD manufacturing] Unable to enumerate scene solids; using artifact graph', + error + ) + } + const bodyIds = + sceneBodyIds.length > 0 + ? sceneBodyIds + : artifactBodies.map(([bodyId]) => bodyId) + + if (bodyIds.length === 0) { throw new Error('当前模型还没有可识别的实体') } @@ -248,22 +342,9 @@ export function HicadManufacturingPanel({ string, unknown > - const measured = await Promise.all( - bodyEntries.map(async ([bodyId], index): Promise => { - const boundingResponse = await sendModelingCommand({ - type: 'bounding_box', - entity_ids: [bodyId], - output_unit: 'mm', - }) - if ( - boundingResponse.type !== 'bounding_box' || - typeof boundingResponse.data !== 'object' || - boundingResponse.data === null - ) { - throw new Error(`无法读取零件 ${index + 1} 的尺寸`) - } - - const boundingBox = boundingResponse.data as BoundingBox + const measurementResults = await Promise.allSettled( + bodyIds.map(async (bodyId, index): Promise => { + const boundingBox = await readBoundingBox([bodyId]) const [volumeResponse, areaResponse] = await Promise.allSettled([ sendModelingCommand({ type: 'volume', @@ -293,18 +374,63 @@ export function HicadManufacturingPanel({ } }) ) + const measured = measurementResults.flatMap((result) => + result.status === 'fulfilled' ? [result.value] : [] + ) + const measurementErrors = measurementResults.flatMap((result) => + result.status === 'rejected' + ? [ + result.reason instanceof Error + ? result.reason.message + : String(result.reason), + ] + : [] + ) + + if (measured.length === 0) { + try { + const overallBoundingBox = await readBoundingBox([]) + measured.push({ + id: 'hicad-scene-aggregate', + name: '模型整体', + center: overallBoundingBox.center, + dimensions: overallBoundingBox.dimensions, + }) + toast( + '当前引擎只返回了整模尺寸,已生成整体工程记录;独立零件 BOM 需模型包含可分离实体。' + ) + } catch (error) { + const details = + measurementErrors[0] || + (error instanceof Error ? error.message : String(error)) + throw new Error(`无法读取模型尺寸:${details}`) + } + } else if (measurementErrors.length > 0) { + toast( + `已识别 ${measured.length} 个实体,另有 ${measurementErrors.length} 个实体暂时无法测量。` + ) + } setBodies(measured) - await engineViewIsometric({ - engineCommandManager: kclManager.engineCommandManager, - padding: 0.22, - }) - await engineStreamZoomToFit({ - engineCommandManager: kclManager.engineCommandManager, - padding: 0.24, - }) + await Promise.allSettled([ + engineViewIsometric({ + engineCommandManager: kclManager.engineCommandManager, + padding: 0.22, + }), + engineStreamZoomToFit({ + engineCommandManager: kclManager.engineCommandManager, + padding: 0.24, + }), + ]) await new Promise((resolve) => setTimeout(resolve, 180)) - await captureSnapshot() + try { + await captureSnapshot() + } catch (error) { + console.warn( + '[HiCAD manufacturing] Snapshot failed after successful analysis', + error + ) + } return measured } catch (error) { const message =