From 2150544390706ee2dc307784a8ade3c4cc6dd1d6 Mon Sep 17 00:00:00 2001 From: Aymeric Rabot Date: Fri, 21 Aug 2026 18:00:35 -0400 Subject: [PATCH 1/2] fix(editor): simplify print export --- .../sidebar/panels/settings-panel/index.tsx | 4 +- .../print-export-button.test.ts | 93 ++++ .../settings-panel/print-export-button.tsx | 97 ++++ .../settings-panel/print-export-card.test.ts | 233 --------- .../settings-panel/print-export-card.tsx | 475 ------------------ 5 files changed, 192 insertions(+), 710 deletions(-) create mode 100644 packages/editor/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts create mode 100644 packages/editor/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx delete mode 100644 packages/editor/src/components/ui/sidebar/panels/settings-panel/print-export-card.test.ts delete mode 100644 packages/editor/src/components/ui/sidebar/panels/settings-panel/print-export-card.tsx diff --git a/packages/editor/src/components/ui/sidebar/panels/settings-panel/index.tsx b/packages/editor/src/components/ui/sidebar/panels/settings-panel/index.tsx index c46e91ad3..e1984c861 100644 --- a/packages/editor/src/components/ui/sidebar/panels/settings-panel/index.tsx +++ b/packages/editor/src/components/ui/sidebar/panels/settings-panel/index.tsx @@ -29,7 +29,7 @@ import useFloorplanMode from './../../../../../store/use-floorplan-mode' import { AudioSettingsDialog } from './audio-settings-dialog' import { KeyboardShortcutsDialog } from './keyboard-shortcuts-dialog' import { LoadBuildDialog, type PendingImport } from './load-build-dialog' -import { PrintExportCard } from './print-export-card' +import { PrintExportButton } from './print-export-button' type SceneNode = Record & { id?: unknown @@ -410,7 +410,7 @@ export function SettingsPanel({ Export OBJ - +
diff --git a/packages/editor/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts b/packages/editor/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts new file mode 100644 index 000000000..9cc9ebdff --- /dev/null +++ b/packages/editor/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts @@ -0,0 +1,93 @@ +import { describe, expect, test } from 'bun:test' +import type { ModelExport, ModelExportOptions } from '../../../../../lib/model-export' +import type { PrintExportReport } from '../../../../../lib/print-export' +import { preparePrintExport } from './print-export-button' + +const report: PrintExportReport = { + kind: 'print-export-report', + version: 2, + format: '3mf', + scale: 100, + units: 'millimeter', + orientation: 'z-up', + status: 'pass', + bounds: { + min: { x: -25, y: -15, z: 0 }, + max: { x: 25, y: 15, z: 20 }, + width: 50, + depth: 30, + height: 20, + }, + triangleCount: 12, + invalidTriangleCount: 0, + degenerateTriangleCount: 0, + boundaryEdgeCount: 0, + nonManifoldEdgeCount: 0, + connectedComponentCount: 1, + solidComponentCount: 1, + invertedWinding: false, + volumeMm3: 30_000, + diagnostics: [], +} + +describe('simple 3D print export', () => { + test('uses one fixed safe print profile', async () => { + const calls: { format?: string; options?: ModelExportOptions }[] = [] + const artifact = { blob: new Blob(['3mf']), filename: 'house.3mf', metadata: report } + const modelExport: ModelExport = async (format, options) => { + calls.push({ format, options }) + return artifact + } + + const prepared = await preparePrintExport(modelExport, true) + + expect(calls).toEqual([ + { + format: 'print-3mf', + options: { + onlyVisible: true, + download: false, + printScale: 100, + printScope: 'levels', + printContent: 'structure', + printBase: 'none', + }, + }, + ]) + expect(prepared).toEqual({ artifact, report }) + }) + + test('blocks the download when preflight finds invalid geometry', async () => { + const blockedReport: PrintExportReport = { + ...report, + status: 'blocked', + diagnostics: [ + { + severity: 'error', + code: 'open_boundary', + message: 'One wall has an open edge.', + }, + ], + } + const modelExport: ModelExport = async () => ({ + blob: new Blob(['3mf']), + filename: 'house.3mf', + metadata: blockedReport, + }) + + await expect(preparePrintExport(modelExport, true)).rejects.toThrow( + 'One wall has an open edge.', + ) + }) + + test('rejects an exporter response without print metadata', async () => { + const modelExport: ModelExport = async () => ({ + blob: new Blob(['3mf']), + filename: 'house.3mf', + }) + + await expect(preparePrintExport(modelExport, false)).rejects.toThrow( + 'did not return a valid file', + ) + }) +}) diff --git a/packages/editor/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx b/packages/editor/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx new file mode 100644 index 000000000..aa4b6d0d6 --- /dev/null +++ b/packages/editor/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx @@ -0,0 +1,97 @@ +import { AlertTriangle, Printer } from 'lucide-react' +import { useState } from 'react' +import { Button } from '../../../../../components/ui/primitives/button' +import { + isPrintLevelBundleReport, + type PrintLevelBundleReport, +} from '../../../../../lib/level-print-export' +import type { ModelExport, ModelExportArtifact } from '../../../../../lib/model-export' +import { + isPrintExportReport, + type PrintExportReport, +} from '../../../../../lib/print-export' +import useEditor from '../../../../../store/use-editor' + +type PreparedPrintExport = { + artifact: ModelExportArtifact + report: PrintExportReport | PrintLevelBundleReport +} + +function downloadArtifact(artifact: ModelExportArtifact) { + const url = URL.createObjectURL(artifact.blob) + const link = document.createElement('a') + link.href = url + link.download = artifact.filename + link.click() + URL.revokeObjectURL(url) +} + +export async function preparePrintExport( + modelExport: ModelExport, + onlyVisible: boolean, +): Promise { + const artifact = await modelExport('print-3mf', { + onlyVisible, + download: false, + printScale: 100, + printScope: 'levels', + printContent: 'structure', + printBase: 'none', + }) + + if ( + !artifact || + (!isPrintExportReport(artifact.metadata) && !isPrintLevelBundleReport(artifact.metadata)) + ) { + throw new Error('The 3D print exporter did not return a valid file.') + } + + if (artifact.metadata.status === 'blocked') { + const diagnostic = artifact.metadata.diagnostics.find((item) => item.severity === 'error') + throw new Error(diagnostic?.message ?? 'This project cannot be exported as printable parts.') + } + + return { artifact, report: artifact.metadata } +} + +export function PrintExportButton({ onlyVisible }: { onlyVisible: boolean }) { + const modelExport = useEditor((state) => state.modelExport) + const [isExporting, setIsExporting] = useState(false) + const [error, setError] = useState(null) + + const handleExport = async () => { + if (!modelExport) return + + setIsExporting(true) + setError(null) + try { + const prepared = await preparePrintExport(modelExport, onlyVisible) + downloadArtifact(prepared.artifact) + } catch (reason) { + setError(reason instanceof Error ? reason.message : '3D print export failed.') + } finally { + setIsExporting(false) + } + } + + return ( + <> + + {error && ( +
+ + {error} +
+ )} + + ) +} diff --git a/packages/editor/src/components/ui/sidebar/panels/settings-panel/print-export-card.test.ts b/packages/editor/src/components/ui/sidebar/panels/settings-panel/print-export-card.test.ts deleted file mode 100644 index 9998c2309..000000000 --- a/packages/editor/src/components/ui/sidebar/panels/settings-panel/print-export-card.test.ts +++ /dev/null @@ -1,233 +0,0 @@ -import { describe, expect, test } from 'bun:test' -import type { PrintLevelBundleReport } from '../../../../../lib/level-print-export' -import type { ModelExport, ModelExportOptions } from '../../../../../lib/model-export' -import type { PrintExportReport } from '../../../../../lib/print-export' -import { preparePrintExport } from './print-export-card' - -const report: PrintExportReport = { - kind: 'print-export-report', - version: 2, - format: '3mf', - scale: 50, - units: 'millimeter', - orientation: 'z-up', - status: 'pass', - bounds: { - min: { x: -50, y: -30, z: 0 }, - max: { x: 50, y: 30, z: 40 }, - width: 100, - depth: 60, - height: 40, - }, - triangleCount: 12, - invalidTriangleCount: 0, - degenerateTriangleCount: 0, - boundaryEdgeCount: 0, - nonManifoldEdgeCount: 0, - connectedComponentCount: 1, - solidComponentCount: 1, - invertedWinding: false, - volumeMm3: 240_000, - diagnostics: [], -} - -const stlReport: PrintExportReport = { ...report, format: 'stl' } - -const levelReport: PrintLevelBundleReport = { - kind: 'print-level-export-report', - version: 2, - format: 'stl', - scale: 50, - units: 'millimeter', - orientation: 'z-up', - status: 'pass', - partCount: 1, - parts: [ - { - kind: 'level', - levelId: 'level_ground', - label: 'Ground', - objectName: '01 Ground', - filename: '01_ground.stl', - sourceBaseMeters: 0, - report: stlReport, - }, - ], - excludedNodeIds: [], - diagnostics: [], -} - -describe('print export card contract', () => { - test('prepares a visible-only scaled artifact without downloading immediately', async () => { - const calls: { format?: string; options?: ModelExportOptions }[] = [] - const artifact = { blob: new Blob(['3mf']), filename: 'house.3mf', metadata: report } - const modelExport: ModelExport = async (format, options) => { - calls.push({ format, options }) - return artifact - } - - const prepared = await preparePrintExport( - modelExport, - true, - '50', - 'whole', - '3mf', - 'structure', - 'none', - '2', - '2', - '1.8', - ) - - expect(calls).toEqual([ - { - format: 'print-3mf', - options: { - onlyVisible: true, - download: false, - printScale: 50, - printScope: 'whole', - printContent: 'structure', - printBase: 'none', - printMinimumFeatureMm: 1.8, - }, - }, - ]) - expect(prepared).toEqual({ artifact, report }) - }) - - test('rejects invalid scale input before invoking the exporter', async () => { - let invoked = false - const modelExport: ModelExport = async () => { - invoked = true - return null - } - - await expect( - preparePrintExport( - modelExport, - true, - '0', - 'levels', - '3mf', - 'structure', - 'none', - '2', - '2', - '', - ), - ).rejects.toThrow( - 'Enter a positive scale denominator', - ) - expect(invoked).toBe(false) - }) - - test('accepts the per-level archive report contract', async () => { - const calls: { format?: string; options?: ModelExportOptions }[] = [] - const artifact = { blob: new Blob(['zip']), filename: 'levels.zip', metadata: levelReport } - const modelExport: ModelExport = async (format, options) => { - calls.push({ format, options }) - return artifact - } - - const prepared = await preparePrintExport( - modelExport, - true, - '50', - 'levels', - 'stl', - 'everything', - 'plinth', - '3', - '2.5', - '', - ) - - expect(calls).toEqual([ - { - format: 'print-stl', - options: { - onlyVisible: true, - download: false, - printScale: 50, - printScope: 'levels', - printContent: 'everything', - printBase: 'plinth', - printPlinthMarginMm: 3, - printPlinthThicknessMm: 2.5, - }, - }, - ]) - expect(prepared).toEqual({ artifact, report: levelReport }) - }) - - test('rejects invalid plinth dimensions before invoking the exporter', async () => { - let invoked = false - const modelExport: ModelExport = async () => { - invoked = true - return null - } - - await expect( - preparePrintExport( - modelExport, - true, - '50', - 'levels', - '3mf', - 'structure', - 'plinth', - '-1', - '2', - '', - ), - ).rejects.toThrow('non-negative plinth margin') - expect(invoked).toBe(false) - }) - - test('rejects an invalid custom minimum feature target before invoking the exporter', async () => { - let invoked = false - const modelExport: ModelExport = async () => { - invoked = true - return null - } - - await expect( - preparePrintExport( - modelExport, - true, - '50', - 'levels', - '3mf', - 'structure', - 'none', - '2', - '2', - '0', - ), - ).rejects.toThrow('positive minimum feature target') - expect(invoked).toBe(false) - }) - - test('rejects an artifact without the print preflight contract', async () => { - const modelExport: ModelExport = async () => ({ - blob: new Blob(['stl']), - filename: 'house.stl', - }) - - await expect( - preparePrintExport( - modelExport, - false, - '100', - 'whole', - '3mf', - 'structure', - 'none', - '2', - '2', - '', - ), - ).rejects.toThrow('did not return a preflight report') - }) -}) diff --git a/packages/editor/src/components/ui/sidebar/panels/settings-panel/print-export-card.tsx b/packages/editor/src/components/ui/sidebar/panels/settings-panel/print-export-card.tsx deleted file mode 100644 index 38cac0337..000000000 --- a/packages/editor/src/components/ui/sidebar/panels/settings-panel/print-export-card.tsx +++ /dev/null @@ -1,475 +0,0 @@ -import { useScene } from '@pascal-app/core' -import { AlertTriangle, CheckCircle2, Download, Printer, XCircle } from 'lucide-react' -import { useEffect, useId, useRef, useState } from 'react' -import { Button } from '../../../../../components/ui/primitives/button' -import { - isPrintLevelBundleReport, - type PrintBaseMode, - type PrintLevelBundleReport, -} from '../../../../../lib/level-print-export' -import type { PrintContentScope } from '../../../../../lib/print-content-scope' -import type { ModelExport, ModelExportArtifact } from '../../../../../lib/model-export' -import { - isPrintExportReport, - type PrintArtifactFormat, - type PrintExportReport, -} from '../../../../../lib/print-export' -import useEditor from '../../../../../store/use-editor' - -export type PreparedPrintExport = { - artifact: ModelExportArtifact - report: PrintExportReport | PrintLevelBundleReport -} - -function formatMillimeters(value: number): string { - if (value >= 100) return value.toFixed(1) - if (value >= 10) return value.toFixed(2) - return value.toFixed(3) -} - -function downloadArtifact(artifact: ModelExportArtifact) { - const url = URL.createObjectURL(artifact.blob) - const link = document.createElement('a') - link.href = url - link.download = artifact.filename - link.click() - URL.revokeObjectURL(url) -} - -export async function preparePrintExport( - modelExport: ModelExport, - onlyVisible: boolean, - scaleInput: string, - scope: 'whole' | 'levels', - format: PrintArtifactFormat, - content: PrintContentScope, - base: PrintBaseMode, - plinthMarginInput: string, - plinthThicknessInput: string, - minimumFeatureInput: string, -): Promise { - const scale = Number(scaleInput) - if (!Number.isFinite(scale) || scale <= 0) { - throw new RangeError('Enter a positive scale denominator, such as 25, 50, or 100.') - } - - let minimumFeatureMm: number | undefined - if (content === 'structure' && minimumFeatureInput.trim() !== '') { - minimumFeatureMm = Number(minimumFeatureInput) - if (!Number.isFinite(minimumFeatureMm) || minimumFeatureMm <= 0) { - throw new RangeError('Enter a positive minimum feature target in millimeters.') - } - } - - let plinthMarginMm: number | undefined - let plinthThicknessMm: number | undefined - if (base === 'plinth') { - if (scope !== 'levels') { - throw new RangeError('A plinth is available only for per-level print packages.') - } - plinthMarginMm = Number(plinthMarginInput) - plinthThicknessMm = Number(plinthThicknessInput) - if (!Number.isFinite(plinthMarginMm) || plinthMarginMm < 0) { - throw new RangeError('Enter a non-negative plinth margin in millimeters.') - } - if (!Number.isFinite(plinthThicknessMm) || plinthThicknessMm <= 0) { - throw new RangeError('Enter a positive plinth thickness in millimeters.') - } - } - - const artifact = await modelExport(format === '3mf' ? 'print-3mf' : 'print-stl', { - onlyVisible, - download: false, - printScale: scale, - printScope: scope, - printContent: content, - printBase: base, - ...(minimumFeatureMm === undefined ? {} : { printMinimumFeatureMm: minimumFeatureMm }), - ...(plinthMarginMm === undefined ? {} : { printPlinthMarginMm: plinthMarginMm }), - ...(plinthThicknessMm === undefined ? {} : { printPlinthThicknessMm: plinthThicknessMm }), - }) - if ( - !artifact || - (!isPrintExportReport(artifact.metadata) && !isPrintLevelBundleReport(artifact.metadata)) - ) { - throw new Error('The print exporter did not return a preflight report.') - } - return { artifact, report: artifact.metadata } -} - -export function PrintExportCard({ onlyVisible }: { onlyVisible: boolean }) { - const scaleInputId = useId() - const minimumFeatureInputId = useId() - const nodes = useScene((state) => state.nodes) - const modelExport = useEditor((state) => state.modelExport) - const generationRef = useRef(0) - const [printScale, setPrintScale] = useState('100') - const [scope, setScope] = useState<'whole' | 'levels'>('levels') - const [format, setFormat] = useState('3mf') - const [content, setContent] = useState('structure') - const [base, setBase] = useState('none') - const [plinthMargin, setPlinthMargin] = useState('2') - const [plinthThickness, setPlinthThickness] = useState('2') - const [minimumFeature, setMinimumFeature] = useState('') - const [isPreparing, setIsPreparing] = useState(false) - const [prepared, setPrepared] = useState(null) - const [error, setError] = useState(null) - - useEffect(() => { - generationRef.current += 1 - setIsPreparing(false) - setPrepared(null) - setError(null) - }, [ - nodes, - onlyVisible, - printScale, - scope, - format, - content, - base, - plinthMargin, - plinthThickness, - minimumFeature, - modelExport, - ]) - - useEffect( - () => () => { - generationRef.current += 1 - }, - [], - ) - - const handlePrepare = async () => { - if (!modelExport) { - setError('The 3D exporter is still loading.') - return - } - - const generation = generationRef.current + 1 - generationRef.current = generation - setIsPreparing(true) - setPrepared(null) - setError(null) - try { - const next = await preparePrintExport( - modelExport, - onlyVisible, - printScale, - scope, - format, - content, - scope === 'levels' ? base : 'none', - plinthMargin, - plinthThickness, - minimumFeature, - ) - if (generation === generationRef.current) setPrepared(next) - } catch (reason) { - if (generation === generationRef.current) { - setError(reason instanceof Error ? reason.message : 'Print export failed.') - } - } finally { - if (generation === generationRef.current) setIsPreparing(false) - } - } - - return ( -
-
- -
-
Print files
-
- Experimental millimeter, Z-up export normalized to the print bed -
-
-
- - - - - - - - - - - - {scope === 'levels' && base === 'plinth' && ( -
- - -
- )} - - - - - - {error && ( -
- - {error} -
- )} - - {prepared && ( -
-
- {prepared.report.status === 'blocked' ? ( - - ) : prepared.report.status === 'warning' ? ( - - ) : ( - - )} - - {prepared.report.status === 'blocked' - ? 'Basic preflight blocked this download.' - : prepared.report.status === 'warning' - ? 'Prepared with printability warnings.' - : 'Basic surface checks passed.'} - -
- - {isPrintLevelBundleReport(prepared.report) ? ( -
-
- Print parts - {prepared.report.partCount} -
- {prepared.report.parts.map((part) => ( -
-
- {part.label} - - {part.report.bounds - ? `${formatMillimeters(part.report.bounds.width)} × ${formatMillimeters( - part.report.bounds.depth, - )} × ${formatMillimeters(part.report.bounds.height)} mm` - : 'No geometry'} - -
-
- {part.report.triangleCount.toLocaleString()} triangles ·{' '} - {part.report.status === 'pass' ? 'basic checks passed' : part.report.status} -
-
- Solid components ·{' '} - {part.report.solidComponentCount?.toLocaleString() ?? 'Not checked'} · Surface - shells · {part.report.connectedComponentCount?.toLocaleString() ?? 'Not checked'} -
- {part.report.minimumFeatureThicknessMm !== undefined && ( -
- Minimum known feature ·{' '} - {part.report.minimumFeatureThicknessMm === null - ? 'Not measured' - : `${formatMillimeters(part.report.minimumFeatureThicknessMm)} mm`} -
- )} -
- ))} -
- ) : ( -
-
Physical size
-
- {prepared.report.bounds - ? `${formatMillimeters(prepared.report.bounds.width)} × ${formatMillimeters( - prepared.report.bounds.depth, - )} × ${formatMillimeters(prepared.report.bounds.height)} mm` - : '—'} -
-
Triangles
-
- {prepared.report.triangleCount.toLocaleString()} -
-
Boundary edges
-
- {prepared.report.boundaryEdgeCount?.toLocaleString() ?? 'Not checked'} -
-
Non-manifold edges
-
- {prepared.report.nonManifoldEdgeCount?.toLocaleString() ?? 'Not checked'} -
-
Solid components
-
- {prepared.report.solidComponentCount?.toLocaleString() ?? 'Not checked'} -
-
Surface shells
-
- {prepared.report.connectedComponentCount?.toLocaleString() ?? 'Not checked'} -
-
Face winding
-
- {prepared.report.invertedWinding === null - ? 'Not checked' - : prepared.report.invertedWinding - ? 'Inverted' - : 'Outward'} -
- {prepared.report.minimumFeatureThicknessMm !== undefined && ( - <> -
Minimum known feature
-
- {prepared.report.minimumFeatureThicknessMm === null - ? 'Not measured' - : `${formatMillimeters(prepared.report.minimumFeatureThicknessMm)} mm`} -
- - )} -
- )} - -
    - {prepared.report.diagnostics.map((diagnostic, index) => ( -
  • · {diagnostic.message}
  • - ))} -
- - -
- )} -
- ) -} From d0d407492c3ff3623e77c8fd31677b0d21b25070 Mon Sep 17 00:00:00 2001 From: Aymeric Rabot Date: Fri, 21 Aug 2026 18:06:33 -0400 Subject: [PATCH 2/2] fix(editor): surface level print errors --- .../print-export-button.test.ts | 47 +++++++++++++++++++ .../settings-panel/print-export-button.tsx | 16 ++++++- 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/packages/editor/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts b/packages/editor/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts index 9cc9ebdff..26fedea1b 100644 --- a/packages/editor/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts +++ b/packages/editor/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from 'bun:test' +import type { PrintLevelBundleReport } from '../../../../../lib/level-print-export' import type { ModelExport, ModelExportOptions } from '../../../../../lib/model-export' import type { PrintExportReport } from '../../../../../lib/print-export' import { preparePrintExport } from './print-export-button' @@ -80,6 +81,52 @@ describe('simple 3D print export', () => { ) }) + test('shows a per-level preflight error when the bundle has no top-level error', async () => { + const blockedPartReport: PrintExportReport = { + ...report, + status: 'blocked', + diagnostics: [ + { + severity: 'error', + code: 'open_boundary', + message: 'The upper level has an open edge.', + }, + ], + } + const blockedBundleReport: PrintLevelBundleReport = { + kind: 'print-level-export-report', + version: 2, + format: '3mf', + scale: 100, + units: 'millimeter', + orientation: 'z-up', + status: 'blocked', + partCount: 1, + parts: [ + { + kind: 'level', + levelId: 'upper-level', + label: 'Upper level', + objectName: 'Upper level', + filename: null, + sourceBaseMeters: 3, + report: blockedPartReport, + }, + ], + excludedNodeIds: [], + diagnostics: [], + } + const modelExport: ModelExport = async () => ({ + blob: new Blob(['3mf']), + filename: 'house.zip', + metadata: blockedBundleReport, + }) + + await expect(preparePrintExport(modelExport, true)).rejects.toThrow( + 'The upper level has an open edge.', + ) + }) + test('rejects an exporter response without print metadata', async () => { const modelExport: ModelExport = async () => ({ blob: new Blob(['3mf']), diff --git a/packages/editor/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx b/packages/editor/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx index aa4b6d0d6..b7298edb5 100644 --- a/packages/editor/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +++ b/packages/editor/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx @@ -26,6 +26,16 @@ function downloadArtifact(artifact: ModelExportArtifact) { URL.revokeObjectURL(url) } +function firstBlockingMessage(report: PrintExportReport | PrintLevelBundleReport) { + const bundleDiagnostic = report.diagnostics.find((item) => item.severity === 'error') + if (bundleDiagnostic || !isPrintLevelBundleReport(report)) return bundleDiagnostic?.message + + for (const part of report.parts) { + const partDiagnostic = part.report.diagnostics.find((item) => item.severity === 'error') + if (partDiagnostic) return partDiagnostic.message + } +} + export async function preparePrintExport( modelExport: ModelExport, onlyVisible: boolean, @@ -47,8 +57,10 @@ export async function preparePrintExport( } if (artifact.metadata.status === 'blocked') { - const diagnostic = artifact.metadata.diagnostics.find((item) => item.severity === 'error') - throw new Error(diagnostic?.message ?? 'This project cannot be exported as printable parts.') + throw new Error( + firstBlockingMessage(artifact.metadata) ?? + 'This project cannot be exported as printable parts.', + ) } return { artifact, report: artifact.metadata }