brep-io-kernel 1.0.0-ci.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +32 -0
- package/README.md +157 -0
- package/dist-kernel/brep-kernel.js +74699 -0
- package/package.json +58 -0
- package/src/BREP/AssemblyComponent.js +42 -0
- package/src/BREP/BREP.js +43 -0
- package/src/BREP/BetterSolid.js +805 -0
- package/src/BREP/Edge.js +103 -0
- package/src/BREP/Extrude.js +403 -0
- package/src/BREP/Face.js +187 -0
- package/src/BREP/MeshRepairer.js +634 -0
- package/src/BREP/OffsetShellSolid.js +614 -0
- package/src/BREP/PointCloudWrap.js +302 -0
- package/src/BREP/Revolve.js +345 -0
- package/src/BREP/SolidMethods/authoring.js +112 -0
- package/src/BREP/SolidMethods/booleanOps.js +230 -0
- package/src/BREP/SolidMethods/chamfer.js +122 -0
- package/src/BREP/SolidMethods/edgeResolution.js +25 -0
- package/src/BREP/SolidMethods/fillet.js +792 -0
- package/src/BREP/SolidMethods/index.js +72 -0
- package/src/BREP/SolidMethods/io.js +105 -0
- package/src/BREP/SolidMethods/lifecycle.js +103 -0
- package/src/BREP/SolidMethods/manifoldOps.js +375 -0
- package/src/BREP/SolidMethods/meshCleanup.js +2512 -0
- package/src/BREP/SolidMethods/meshQueries.js +264 -0
- package/src/BREP/SolidMethods/metadata.js +106 -0
- package/src/BREP/SolidMethods/metrics.js +51 -0
- package/src/BREP/SolidMethods/transforms.js +361 -0
- package/src/BREP/SolidMethods/visualize.js +508 -0
- package/src/BREP/SolidShared.js +26 -0
- package/src/BREP/Sweep.js +1596 -0
- package/src/BREP/Tube.js +857 -0
- package/src/BREP/Vertex.js +43 -0
- package/src/BREP/applyBooleanOperation.js +704 -0
- package/src/BREP/boundsUtils.js +48 -0
- package/src/BREP/chamfer.js +551 -0
- package/src/BREP/edgePolylineUtils.js +85 -0
- package/src/BREP/fillets/common.js +388 -0
- package/src/BREP/fillets/fillet.js +1422 -0
- package/src/BREP/fillets/filletGeometry.js +15 -0
- package/src/BREP/fillets/inset.js +389 -0
- package/src/BREP/fillets/offsetHelper.js +143 -0
- package/src/BREP/fillets/outset.js +88 -0
- package/src/BREP/helix.js +193 -0
- package/src/BREP/meshToBrep.js +234 -0
- package/src/BREP/primitives.js +279 -0
- package/src/BREP/setupManifold.js +71 -0
- package/src/BREP/threadGeometry.js +1120 -0
- package/src/BREP/triangleUtils.js +8 -0
- package/src/BREP/triangulate.js +608 -0
- package/src/FeatureRegistry.js +183 -0
- package/src/PartHistory.js +1132 -0
- package/src/UI/AccordionWidget.js +292 -0
- package/src/UI/CADmaterials.js +850 -0
- package/src/UI/EnvMonacoEditor.js +522 -0
- package/src/UI/FloatingWindow.js +396 -0
- package/src/UI/HistoryWidget.js +457 -0
- package/src/UI/MainToolbar.js +131 -0
- package/src/UI/ModelLibraryView.js +194 -0
- package/src/UI/OrthoCameraIdle.js +206 -0
- package/src/UI/PluginsWidget.js +280 -0
- package/src/UI/SceneListing.js +606 -0
- package/src/UI/SelectionFilter.js +629 -0
- package/src/UI/ViewCube.js +389 -0
- package/src/UI/assembly/AssemblyConstraintCollectionWidget.js +329 -0
- package/src/UI/assembly/AssemblyConstraintControlsWidget.js +282 -0
- package/src/UI/assembly/AssemblyConstraintsWidget.css +292 -0
- package/src/UI/assembly/AssemblyConstraintsWidget.js +1373 -0
- package/src/UI/assembly/constraintFaceUtils.js +115 -0
- package/src/UI/assembly/constraintHighlightUtils.js +70 -0
- package/src/UI/assembly/constraintLabelUtils.js +31 -0
- package/src/UI/assembly/constraintPointUtils.js +64 -0
- package/src/UI/assembly/constraintSelectionUtils.js +185 -0
- package/src/UI/assembly/constraintStatusUtils.js +142 -0
- package/src/UI/componentSelectorModal.js +240 -0
- package/src/UI/controls/CombinedTransformControls.js +386 -0
- package/src/UI/dialogs.js +351 -0
- package/src/UI/expressionsManager.js +100 -0
- package/src/UI/featureDialogWidgets/booleanField.js +25 -0
- package/src/UI/featureDialogWidgets/booleanOperationField.js +97 -0
- package/src/UI/featureDialogWidgets/buttonField.js +45 -0
- package/src/UI/featureDialogWidgets/componentSelectorField.js +102 -0
- package/src/UI/featureDialogWidgets/defaultField.js +23 -0
- package/src/UI/featureDialogWidgets/fileField.js +66 -0
- package/src/UI/featureDialogWidgets/index.js +34 -0
- package/src/UI/featureDialogWidgets/numberField.js +165 -0
- package/src/UI/featureDialogWidgets/optionsField.js +33 -0
- package/src/UI/featureDialogWidgets/referenceSelectionField.js +208 -0
- package/src/UI/featureDialogWidgets/stringField.js +24 -0
- package/src/UI/featureDialogWidgets/textareaField.js +28 -0
- package/src/UI/featureDialogWidgets/threadDesignationField.js +160 -0
- package/src/UI/featureDialogWidgets/transformField.js +252 -0
- package/src/UI/featureDialogWidgets/utils.js +43 -0
- package/src/UI/featureDialogWidgets/vec3Field.js +133 -0
- package/src/UI/featureDialogs.js +1414 -0
- package/src/UI/fileManagerWidget.js +615 -0
- package/src/UI/history/HistoryCollectionWidget.js +1294 -0
- package/src/UI/history/historyCollectionWidget.css.js +257 -0
- package/src/UI/history/historyDisplayInfo.js +133 -0
- package/src/UI/mobile.js +28 -0
- package/src/UI/objectDump.js +442 -0
- package/src/UI/pmi/AnnotationCollectionWidget.js +120 -0
- package/src/UI/pmi/AnnotationHistory.js +353 -0
- package/src/UI/pmi/AnnotationRegistry.js +90 -0
- package/src/UI/pmi/BaseAnnotation.js +269 -0
- package/src/UI/pmi/LabelOverlay.css +102 -0
- package/src/UI/pmi/LabelOverlay.js +191 -0
- package/src/UI/pmi/PMIMode.js +1550 -0
- package/src/UI/pmi/PMIViewsWidget.js +1098 -0
- package/src/UI/pmi/annUtils.js +729 -0
- package/src/UI/pmi/dimensions/AngleDimensionAnnotation.js +647 -0
- package/src/UI/pmi/dimensions/ExplodeBodyAnnotation.js +507 -0
- package/src/UI/pmi/dimensions/HoleCalloutAnnotation.js +462 -0
- package/src/UI/pmi/dimensions/LeaderAnnotation.js +403 -0
- package/src/UI/pmi/dimensions/LinearDimensionAnnotation.js +532 -0
- package/src/UI/pmi/dimensions/NoteAnnotation.js +110 -0
- package/src/UI/pmi/dimensions/RadialDimensionAnnotation.js +659 -0
- package/src/UI/pmi/pmiStyle.js +44 -0
- package/src/UI/sketcher/SketchMode3D.js +4095 -0
- package/src/UI/sketcher/dimensions.js +674 -0
- package/src/UI/sketcher/glyphs.js +236 -0
- package/src/UI/sketcher/highlights.js +60 -0
- package/src/UI/toolbarButtons/aboutButton.js +5 -0
- package/src/UI/toolbarButtons/exportButton.js +609 -0
- package/src/UI/toolbarButtons/flatPatternButton.js +307 -0
- package/src/UI/toolbarButtons/importButton.js +160 -0
- package/src/UI/toolbarButtons/inspectorToggleButton.js +12 -0
- package/src/UI/toolbarButtons/metadataButton.js +1063 -0
- package/src/UI/toolbarButtons/orientToFaceButton.js +114 -0
- package/src/UI/toolbarButtons/registerDefaultButtons.js +46 -0
- package/src/UI/toolbarButtons/saveButton.js +99 -0
- package/src/UI/toolbarButtons/scriptRunnerButton.js +302 -0
- package/src/UI/toolbarButtons/testsButton.js +26 -0
- package/src/UI/toolbarButtons/undoRedoButtons.js +25 -0
- package/src/UI/toolbarButtons/wireframeToggleButton.js +5 -0
- package/src/UI/toolbarButtons/zoomToFitButton.js +5 -0
- package/src/UI/triangleDebuggerWindow.js +945 -0
- package/src/UI/viewer.js +4228 -0
- package/src/assemblyConstraints/AssemblyConstraintHistory.js +1576 -0
- package/src/assemblyConstraints/AssemblyConstraintRegistry.js +120 -0
- package/src/assemblyConstraints/BaseAssemblyConstraint.js +66 -0
- package/src/assemblyConstraints/constraintExpressionUtils.js +35 -0
- package/src/assemblyConstraints/constraintUtils/parallelAlignment.js +676 -0
- package/src/assemblyConstraints/constraints/AngleConstraint.js +485 -0
- package/src/assemblyConstraints/constraints/CoincidentConstraint.js +194 -0
- package/src/assemblyConstraints/constraints/DistanceConstraint.js +616 -0
- package/src/assemblyConstraints/constraints/FixedConstraint.js +78 -0
- package/src/assemblyConstraints/constraints/ParallelConstraint.js +252 -0
- package/src/assemblyConstraints/constraints/TouchAlignConstraint.js +961 -0
- package/src/core/entities/HistoryCollectionBase.js +72 -0
- package/src/core/entities/ListEntityBase.js +109 -0
- package/src/core/entities/schemaProcesser.js +121 -0
- package/src/exporters/sheetMetalFlatPattern.js +659 -0
- package/src/exporters/sheetMetalUnfold.js +862 -0
- package/src/exporters/step.js +1135 -0
- package/src/exporters/threeMF.js +575 -0
- package/src/features/assemblyComponent/AssemblyComponentFeature.js +780 -0
- package/src/features/boolean/BooleanFeature.js +94 -0
- package/src/features/chamfer/ChamferFeature.js +116 -0
- package/src/features/datium/DatiumFeature.js +80 -0
- package/src/features/edgeFeatureUtils.js +41 -0
- package/src/features/extrude/ExtrudeFeature.js +143 -0
- package/src/features/fillet/FilletFeature.js +197 -0
- package/src/features/helix/HelixFeature.js +405 -0
- package/src/features/hole/HoleFeature.js +1050 -0
- package/src/features/hole/screwClearance.js +86 -0
- package/src/features/hole/threadDesignationCatalog.js +149 -0
- package/src/features/imageHeightSolid/ImageHeightmapSolidFeature.js +463 -0
- package/src/features/imageToFace/ImageToFaceFeature.js +727 -0
- package/src/features/imageToFace/imageEditor.js +1270 -0
- package/src/features/imageToFace/traceUtils.js +971 -0
- package/src/features/import3dModel/Import3dModelFeature.js +151 -0
- package/src/features/loft/LoftFeature.js +605 -0
- package/src/features/mirror/MirrorFeature.js +151 -0
- package/src/features/offsetFace/OffsetFaceFeature.js +370 -0
- package/src/features/offsetShell/OffsetShellFeature.js +89 -0
- package/src/features/overlapCleanup/OverlapCleanupFeature.js +85 -0
- package/src/features/pattern/PatternFeature.js +275 -0
- package/src/features/patternLinear/PatternLinearFeature.js +120 -0
- package/src/features/patternRadial/PatternRadialFeature.js +186 -0
- package/src/features/plane/PlaneFeature.js +154 -0
- package/src/features/primitiveCone/primitiveConeFeature.js +99 -0
- package/src/features/primitiveCube/primitiveCubeFeature.js +70 -0
- package/src/features/primitiveCylinder/primitiveCylinderFeature.js +91 -0
- package/src/features/primitivePyramid/primitivePyramidFeature.js +72 -0
- package/src/features/primitiveSphere/primitiveSphereFeature.js +62 -0
- package/src/features/primitiveTorus/primitiveTorusFeature.js +109 -0
- package/src/features/remesh/RemeshFeature.js +97 -0
- package/src/features/revolve/RevolveFeature.js +111 -0
- package/src/features/selectionUtils.js +118 -0
- package/src/features/sheetMetal/SheetMetalContourFlangeFeature.js +1656 -0
- package/src/features/sheetMetal/SheetMetalCutoutFeature.js +1056 -0
- package/src/features/sheetMetal/SheetMetalFlangeFeature.js +1568 -0
- package/src/features/sheetMetal/SheetMetalHemFeature.js +43 -0
- package/src/features/sheetMetal/SheetMetalObject.js +141 -0
- package/src/features/sheetMetal/SheetMetalTabFeature.js +176 -0
- package/src/features/sheetMetal/UNFOLD_NEUTRAL_REQUIREMENTS.md +153 -0
- package/src/features/sheetMetal/contour-flange-rebuild-spec.md +261 -0
- package/src/features/sheetMetal/profileUtils.js +25 -0
- package/src/features/sheetMetal/sheetMetalCleanup.js +9 -0
- package/src/features/sheetMetal/sheetMetalFaceTypes.js +146 -0
- package/src/features/sheetMetal/sheetMetalMetadata.js +165 -0
- package/src/features/sheetMetal/sheetMetalPipeline.js +169 -0
- package/src/features/sheetMetal/sheetMetalProfileUtils.js +216 -0
- package/src/features/sheetMetal/sheetMetalTabUtils.js +29 -0
- package/src/features/sheetMetal/sheetMetalTree.js +210 -0
- package/src/features/sketch/SketchFeature.js +955 -0
- package/src/features/sketch/sketchSolver2D/ConstraintEngine.js +800 -0
- package/src/features/sketch/sketchSolver2D/constraintDefinitions.js +704 -0
- package/src/features/sketch/sketchSolver2D/mathHelpersMod.js +307 -0
- package/src/features/spline/SplineEditorSession.js +988 -0
- package/src/features/spline/SplineFeature.js +1388 -0
- package/src/features/spline/splineUtils.js +218 -0
- package/src/features/sweep/SweepFeature.js +110 -0
- package/src/features/transform/TransformFeature.js +152 -0
- package/src/features/tube/TubeFeature.js +635 -0
- package/src/fs.proxy.js +625 -0
- package/src/idbStorage.js +254 -0
- package/src/index.js +12 -0
- package/src/main.js +15 -0
- package/src/metadataManager.js +64 -0
- package/src/path.proxy.js +277 -0
- package/src/plugins/ghLoader.worker.js +151 -0
- package/src/plugins/pluginManager.js +286 -0
- package/src/pmi/PMIViewsManager.js +134 -0
- package/src/services/componentLibrary.js +198 -0
- package/src/tests/ConsoleCapture.js +189 -0
- package/src/tests/S7-diagnostics-2025-12-23T18-37-23-570Z.json +630 -0
- package/src/tests/browserTests.js +597 -0
- package/src/tests/debugBoolean.js +225 -0
- package/src/tests/partFiles/badBoolean.json +957 -0
- package/src/tests/partFiles/extrudeTest.json +88 -0
- package/src/tests/partFiles/filletFail.json +58 -0
- package/src/tests/partFiles/import_TEst.part.part.json +646 -0
- package/src/tests/partFiles/sheetMetalHem.BREP.json +734 -0
- package/src/tests/test_boolean_subtract.js +27 -0
- package/src/tests/test_chamfer.js +17 -0
- package/src/tests/test_extrudeFace.js +24 -0
- package/src/tests/test_fillet.js +17 -0
- package/src/tests/test_fillet_nonClosed.js +45 -0
- package/src/tests/test_filletsMoreDifficult.js +46 -0
- package/src/tests/test_history_features_basic.js +149 -0
- package/src/tests/test_hole.js +282 -0
- package/src/tests/test_mirror.js +16 -0
- package/src/tests/test_offsetShellGrouping.js +85 -0
- package/src/tests/test_plane.js +4 -0
- package/src/tests/test_primitiveCone.js +11 -0
- package/src/tests/test_primitiveCube.js +7 -0
- package/src/tests/test_primitiveCylinder.js +8 -0
- package/src/tests/test_primitivePyramid.js +9 -0
- package/src/tests/test_primitiveSphere.js +17 -0
- package/src/tests/test_primitiveTorus.js +21 -0
- package/src/tests/test_pushFace.js +126 -0
- package/src/tests/test_sheetMetalContourFlange.js +125 -0
- package/src/tests/test_sheetMetal_features.js +80 -0
- package/src/tests/test_sketch_openLoop.js +45 -0
- package/src/tests/test_solidMetrics.js +58 -0
- package/src/tests/test_stlLoader.js +1889 -0
- package/src/tests/test_sweepFace.js +55 -0
- package/src/tests/test_tube.js +45 -0
- package/src/tests/test_tube_closedLoop.js +67 -0
- package/src/tests/tests.js +493 -0
- package/src/tools/assemblyConstraintDialogCapturePage.js +56 -0
- package/src/tools/dialogCapturePageFactory.js +227 -0
- package/src/tools/featureDialogCapturePage.js +47 -0
- package/src/tools/pmiAnnotationDialogCapturePage.js +60 -0
- package/src/utils/axisHelpers.js +99 -0
- package/src/utils/deepClone.js +69 -0
- package/src/utils/geometryTolerance.js +37 -0
- package/src/utils/normalizeTypeString.js +8 -0
- package/src/utils/xformMath.js +51 -0
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
// Basic 3MF exporter using JSZip
|
|
2
|
+
// - Packages a minimal 3MF container with a single model file
|
|
3
|
+
// - Supports exporting one or multiple SOLID objects from the scene
|
|
4
|
+
// - Uses current manifold mesh data: vertProperties (float triples) and triVerts (index triples)
|
|
5
|
+
|
|
6
|
+
import JSZip from 'jszip';
|
|
7
|
+
|
|
8
|
+
function _parseDataUrl(dataUrl) {
|
|
9
|
+
try {
|
|
10
|
+
if (typeof dataUrl !== 'string') return null;
|
|
11
|
+
if (!dataUrl.startsWith('data:')) return null;
|
|
12
|
+
const comma = dataUrl.indexOf(',');
|
|
13
|
+
if (comma < 0) return null;
|
|
14
|
+
const header = dataUrl.slice(5, comma); // after 'data:' up to comma
|
|
15
|
+
const payload = dataUrl.slice(comma + 1);
|
|
16
|
+
const isBase64 = /;base64/i.test(header);
|
|
17
|
+
const mime = header.split(';')[0] || 'application/octet-stream';
|
|
18
|
+
const ext = (mime === 'image/png') ? 'png' : (mime === 'image/jpeg' ? 'jpg' : 'bin');
|
|
19
|
+
let bytes;
|
|
20
|
+
if (isBase64) {
|
|
21
|
+
const bin = atob(payload);
|
|
22
|
+
const u8 = new Uint8Array(bin.length);
|
|
23
|
+
for (let i = 0; i < bin.length; i++) u8[i] = bin.charCodeAt(i);
|
|
24
|
+
bytes = u8;
|
|
25
|
+
} else {
|
|
26
|
+
// URI-encoded data
|
|
27
|
+
const str = decodeURIComponent(payload);
|
|
28
|
+
const u8 = new Uint8Array(str.length);
|
|
29
|
+
for (let i = 0; i < str.length; i++) u8[i] = str.charCodeAt(i) & 0xFF;
|
|
30
|
+
bytes = u8;
|
|
31
|
+
}
|
|
32
|
+
return { bytes, mime, ext };
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function xmlEsc(s) {
|
|
39
|
+
return String(s ?? '').replace(/&/g, '&')
|
|
40
|
+
.replace(/</g, '<')
|
|
41
|
+
.replace(/>/g, '>')
|
|
42
|
+
.replace(/\"/g, '"')
|
|
43
|
+
.replace(/'/g, ''');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function numStr(n, precision = 6) {
|
|
47
|
+
if (!Number.isFinite(n)) return '0';
|
|
48
|
+
const s = n.toFixed(precision);
|
|
49
|
+
// Trim trailing zeros and optional dot for compactness
|
|
50
|
+
return s.replace(/\.0+$/,'').replace(/(\.[0-9]*?)0+$/,'$1');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function _isIdentityMatrixElements(e) {
|
|
54
|
+
if (!e || e.length !== 16) return true;
|
|
55
|
+
return e[0] === 1 && e[4] === 0 && e[8] === 0 && e[12] === 0
|
|
56
|
+
&& e[1] === 0 && e[5] === 1 && e[9] === 0 && e[13] === 0
|
|
57
|
+
&& e[2] === 0 && e[6] === 0 && e[10] === 1 && e[14] === 0
|
|
58
|
+
&& e[3] === 0 && e[7] === 0 && e[11] === 0 && e[15] === 1;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function _clampByte(n) {
|
|
62
|
+
return Math.max(0, Math.min(255, Math.round(n)));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function _rgbToHex(r, g, b) {
|
|
66
|
+
return `#${_clampByte(r).toString(16).padStart(2, '0')}${_clampByte(g).toString(16).padStart(2, '0')}${_clampByte(b).toString(16).padStart(2, '0')}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function _parseRgbComponent(raw) {
|
|
70
|
+
const s = String(raw ?? '').trim();
|
|
71
|
+
if (!s) return NaN;
|
|
72
|
+
if (s.endsWith('%')) {
|
|
73
|
+
const num = parseFloat(s.slice(0, -1));
|
|
74
|
+
if (!Number.isFinite(num)) return NaN;
|
|
75
|
+
return (num / 100) * 255;
|
|
76
|
+
}
|
|
77
|
+
const num = parseFloat(s);
|
|
78
|
+
if (!Number.isFinite(num)) return NaN;
|
|
79
|
+
return num;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function _parseHue(raw) {
|
|
83
|
+
const s = String(raw ?? '').trim().toLowerCase();
|
|
84
|
+
if (!s) return NaN;
|
|
85
|
+
if (s.endsWith('turn')) {
|
|
86
|
+
const num = parseFloat(s.slice(0, -4));
|
|
87
|
+
if (!Number.isFinite(num)) return NaN;
|
|
88
|
+
return num * 360;
|
|
89
|
+
}
|
|
90
|
+
if (s.endsWith('rad')) {
|
|
91
|
+
const num = parseFloat(s.slice(0, -3));
|
|
92
|
+
if (!Number.isFinite(num)) return NaN;
|
|
93
|
+
return (num * 180) / Math.PI;
|
|
94
|
+
}
|
|
95
|
+
if (s.endsWith('deg')) {
|
|
96
|
+
const num = parseFloat(s.slice(0, -3));
|
|
97
|
+
if (!Number.isFinite(num)) return NaN;
|
|
98
|
+
return num;
|
|
99
|
+
}
|
|
100
|
+
const num = parseFloat(s);
|
|
101
|
+
if (!Number.isFinite(num)) return NaN;
|
|
102
|
+
return num;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function _parsePercent(raw) {
|
|
106
|
+
const s = String(raw ?? '').trim();
|
|
107
|
+
if (!s) return NaN;
|
|
108
|
+
if (s.endsWith('%')) {
|
|
109
|
+
const num = parseFloat(s.slice(0, -1));
|
|
110
|
+
if (!Number.isFinite(num)) return NaN;
|
|
111
|
+
return num / 100;
|
|
112
|
+
}
|
|
113
|
+
const num = parseFloat(s);
|
|
114
|
+
if (!Number.isFinite(num)) return NaN;
|
|
115
|
+
return num > 1 ? num / 100 : num;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function _hslToRgb(h, s, l) {
|
|
119
|
+
const hue = ((h % 360) + 360) % 360;
|
|
120
|
+
const c = (1 - Math.abs(2 * l - 1)) * s;
|
|
121
|
+
const x = c * (1 - Math.abs((hue / 60) % 2 - 1));
|
|
122
|
+
const m = l - c / 2;
|
|
123
|
+
let r = 0; let g = 0; let b = 0;
|
|
124
|
+
if (hue < 60) { r = c; g = x; b = 0; }
|
|
125
|
+
else if (hue < 120) { r = x; g = c; b = 0; }
|
|
126
|
+
else if (hue < 180) { r = 0; g = c; b = x; }
|
|
127
|
+
else if (hue < 240) { r = 0; g = x; b = c; }
|
|
128
|
+
else if (hue < 300) { r = x; g = 0; b = c; }
|
|
129
|
+
else { r = c; g = 0; b = x; }
|
|
130
|
+
return { r: r + m, g: g + m, b: b + m };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function _parseColorToHex(value) {
|
|
134
|
+
if (value == null) return null;
|
|
135
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
136
|
+
const n = Math.max(0, Math.min(0xffffff, Math.round(value)));
|
|
137
|
+
return `#${n.toString(16).padStart(6, '0')}`;
|
|
138
|
+
}
|
|
139
|
+
if (typeof value === 'string') {
|
|
140
|
+
const v = value.trim();
|
|
141
|
+
if (!v) return null;
|
|
142
|
+
const hexMatch = v.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i);
|
|
143
|
+
if (hexMatch) {
|
|
144
|
+
const h = hexMatch[1];
|
|
145
|
+
if (h.length === 3) {
|
|
146
|
+
return `#${h[0]}${h[0]}${h[1]}${h[1]}${h[2]}${h[2]}`.toLowerCase();
|
|
147
|
+
}
|
|
148
|
+
return `#${h.toLowerCase()}`;
|
|
149
|
+
}
|
|
150
|
+
const hex0xMatch = v.match(/^0x([0-9a-f]{6})$/i);
|
|
151
|
+
if (hex0xMatch) return `#${hex0xMatch[1].toLowerCase()}`;
|
|
152
|
+
const rgbMatch = v.match(/^rgba?\((.+)\)$/i);
|
|
153
|
+
if (rgbMatch) {
|
|
154
|
+
const inner = rgbMatch[1].replace('/', ' ');
|
|
155
|
+
const parts = inner.split(/[, ]+/).map(p => p.trim()).filter(Boolean);
|
|
156
|
+
if (parts.length < 3) return null;
|
|
157
|
+
const r = _parseRgbComponent(parts[0]);
|
|
158
|
+
const g = _parseRgbComponent(parts[1]);
|
|
159
|
+
const b = _parseRgbComponent(parts[2]);
|
|
160
|
+
if (![r, g, b].every(Number.isFinite)) return null;
|
|
161
|
+
return _rgbToHex(r, g, b);
|
|
162
|
+
}
|
|
163
|
+
const hslMatch = v.match(/^hsla?\((.+)\)$/i);
|
|
164
|
+
if (hslMatch) {
|
|
165
|
+
const inner = hslMatch[1].replace('/', ' ');
|
|
166
|
+
const parts = inner.split(/[, ]+/).map(p => p.trim()).filter(Boolean);
|
|
167
|
+
if (parts.length < 3) return null;
|
|
168
|
+
const h = _parseHue(parts[0]);
|
|
169
|
+
const s = _parsePercent(parts[1]);
|
|
170
|
+
const l = _parsePercent(parts[2]);
|
|
171
|
+
if (![h, s, l].every(Number.isFinite)) return null;
|
|
172
|
+
const rgb = _hslToRgb(h, s, l);
|
|
173
|
+
return _rgbToHex(rgb.r * 255, rgb.g * 255, rgb.b * 255);
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
if (Array.isArray(value) && value.length >= 3) {
|
|
178
|
+
const r = Number(value[0]);
|
|
179
|
+
const g = Number(value[1]);
|
|
180
|
+
const b = Number(value[2]);
|
|
181
|
+
if (![r, g, b].every(Number.isFinite)) return null;
|
|
182
|
+
const max = Math.max(r, g, b);
|
|
183
|
+
return max <= 1 ? _rgbToHex(r * 255, g * 255, b * 255) : _rgbToHex(r, g, b);
|
|
184
|
+
}
|
|
185
|
+
if (typeof value === 'object') {
|
|
186
|
+
const r = Number(value.r);
|
|
187
|
+
const g = Number(value.g);
|
|
188
|
+
const b = Number(value.b);
|
|
189
|
+
if ([r, g, b].every(Number.isFinite)) {
|
|
190
|
+
const max = Math.max(r, g, b);
|
|
191
|
+
return max <= 1 ? _rgbToHex(r * 255, g * 255, b * 255) : _rgbToHex(r, g, b);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function _pickColorValue(meta, keys) {
|
|
198
|
+
if (!meta || typeof meta !== 'object') return null;
|
|
199
|
+
for (const key of keys) {
|
|
200
|
+
if (!Object.prototype.hasOwnProperty.call(meta, key)) continue;
|
|
201
|
+
const raw = meta[key];
|
|
202
|
+
if (raw == null) continue;
|
|
203
|
+
if (typeof raw === 'string' && raw.trim() === '') continue;
|
|
204
|
+
return raw;
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function _resolveColorHex(meta, keys) {
|
|
210
|
+
const raw = _pickColorValue(meta, keys);
|
|
211
|
+
return _parseColorToHex(raw);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Build the core 3MF model XML for one or more solids.
|
|
216
|
+
* @param {Array} solids Array of SOLID-like objects that expose getMesh() and name.
|
|
217
|
+
* @param {{unit?: 'millimeter'|'inch'|'foot'|'meter'|'centimeter'|'micron', precision?: number, scale?: number, metadataManager?: any, useMetadataColors?: boolean, includeFaceTags?: boolean, applyWorldTransform?: boolean}} opts
|
|
218
|
+
* @returns {string}
|
|
219
|
+
*/
|
|
220
|
+
export function build3MFModelXML(solids, opts = {}) {
|
|
221
|
+
const unit = opts.unit || 'millimeter';
|
|
222
|
+
const precision = Number.isFinite(opts.precision) ? opts.precision : 6;
|
|
223
|
+
const scale = Number.isFinite(opts.scale) ? opts.scale : 1.0;
|
|
224
|
+
const modelMetadata = opts.modelMetadata && typeof opts.modelMetadata === 'object' ? opts.modelMetadata : null;
|
|
225
|
+
const includeFaceTags = opts.includeFaceTags !== false; // default on
|
|
226
|
+
const useMetadataColors = opts.useMetadataColors !== false;
|
|
227
|
+
const applyWorldTransform = opts.applyWorldTransform !== false;
|
|
228
|
+
|
|
229
|
+
const lines = [];
|
|
230
|
+
lines.push('<?xml version="1.0" encoding="UTF-8"?>');
|
|
231
|
+
lines.push('<model xml:lang="en-US" unit="' + xmlEsc(unit) + '" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">');
|
|
232
|
+
if (modelMetadata) {
|
|
233
|
+
for (const k of Object.keys(modelMetadata)) {
|
|
234
|
+
const v = modelMetadata[k];
|
|
235
|
+
lines.push(` <metadata name="${xmlEsc(k)}">${xmlEsc(v)}</metadata>`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
lines.push(' <resources>');
|
|
239
|
+
|
|
240
|
+
// Resource/object id allocator (unique across the model)
|
|
241
|
+
let nextId = 1;
|
|
242
|
+
const buildItems = [];
|
|
243
|
+
|
|
244
|
+
let solidIdx = 0;
|
|
245
|
+
for (const s of (solids || [])) {
|
|
246
|
+
if (!s || typeof s.getMesh !== 'function') continue;
|
|
247
|
+
const mesh = s.getMesh();
|
|
248
|
+
try {
|
|
249
|
+
if (!mesh || !mesh.vertProperties || !mesh.triVerts) continue;
|
|
250
|
+
const rawName = s.name || `solid_${solidIdx + 1}`;
|
|
251
|
+
const name = xmlEsc(rawName);
|
|
252
|
+
const vp = mesh.vertProperties; // Float32Array
|
|
253
|
+
const tv = mesh.triVerts; // Uint32Array
|
|
254
|
+
const tCount = (tv.length / 3) | 0;
|
|
255
|
+
const metadataManager = opts.metadataManager && typeof opts.metadataManager.getMetadata === 'function' ? opts.metadataManager : null;
|
|
256
|
+
const idToFaceName = s && s._idToFaceName instanceof Map ? s._idToFaceName : null;
|
|
257
|
+
let worldMatrixElements = null;
|
|
258
|
+
if (applyWorldTransform) {
|
|
259
|
+
try {
|
|
260
|
+
if (typeof s.updateWorldMatrix === 'function') {
|
|
261
|
+
s.updateWorldMatrix(true, false);
|
|
262
|
+
} else if (typeof s.updateMatrixWorld === 'function') {
|
|
263
|
+
s.updateMatrixWorld(true);
|
|
264
|
+
}
|
|
265
|
+
} catch { /* best-effort only */ }
|
|
266
|
+
const wm = s && s.matrixWorld;
|
|
267
|
+
if (wm && wm.elements && wm.elements.length === 16 && !_isIdentityMatrixElements(wm.elements)) {
|
|
268
|
+
worldMatrixElements = wm.elements;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Optional: build per-object BaseMaterials for metadata colors or face tags.
|
|
273
|
+
let matPid = null; // resource id for this object's material group
|
|
274
|
+
let objectPidAttr = '';
|
|
275
|
+
let objectPindexAttr = '';
|
|
276
|
+
let faceColorById = null;
|
|
277
|
+
let faceMatIndexById = null;
|
|
278
|
+
let solidMatIndex = null;
|
|
279
|
+
let useFaceTagsFallback = false;
|
|
280
|
+
|
|
281
|
+
const faceIDs = (mesh.faceID && mesh.faceID.length === tCount) ? mesh.faceID : null;
|
|
282
|
+
if (useMetadataColors) {
|
|
283
|
+
let solidColorHex = null;
|
|
284
|
+
try {
|
|
285
|
+
const solidMeta = (metadataManager && s?.name)
|
|
286
|
+
? metadataManager.getMetadata(s.name)
|
|
287
|
+
: null;
|
|
288
|
+
solidColorHex = _resolveColorHex(solidMeta, ['solidColor', 'color'])
|
|
289
|
+
|| _resolveColorHex(s?.userData?.metadata || null, ['solidColor', 'color']);
|
|
290
|
+
} catch { solidColorHex = null; }
|
|
291
|
+
|
|
292
|
+
if (faceIDs && idToFaceName) {
|
|
293
|
+
faceColorById = new Map();
|
|
294
|
+
const seenFace = new Set();
|
|
295
|
+
for (let t = 0; t < faceIDs.length; t++) {
|
|
296
|
+
const fid = faceIDs[t] >>> 0;
|
|
297
|
+
if (seenFace.has(fid)) continue;
|
|
298
|
+
seenFace.add(fid);
|
|
299
|
+
const faceName = idToFaceName.get(fid) || `FACE_${fid}`;
|
|
300
|
+
let faceMeta = null;
|
|
301
|
+
try { faceMeta = typeof s.getFaceMetadata === 'function' ? s.getFaceMetadata(faceName) : null; } catch { faceMeta = null; }
|
|
302
|
+
let faceHex = null;
|
|
303
|
+
if (metadataManager) {
|
|
304
|
+
try { faceHex = _resolveColorHex(metadataManager.getMetadata(faceName), ['faceColor', 'color']); } catch { faceHex = null; }
|
|
305
|
+
}
|
|
306
|
+
if (!faceHex) faceHex = _resolveColorHex(faceMeta, ['faceColor', 'color']);
|
|
307
|
+
if (faceHex) faceColorById.set(fid, faceHex);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const hasFaceColors = faceColorById && faceColorById.size > 0;
|
|
312
|
+
const hasSolidColor = !!solidColorHex;
|
|
313
|
+
const hasMetadataColors = hasSolidColor || hasFaceColors;
|
|
314
|
+
|
|
315
|
+
if (hasMetadataColors) {
|
|
316
|
+
const materials = [];
|
|
317
|
+
const colorToIndex = new Map();
|
|
318
|
+
const addMaterial = (hex, label) => {
|
|
319
|
+
if (!hex) return null;
|
|
320
|
+
if (!colorToIndex.has(hex)) {
|
|
321
|
+
colorToIndex.set(hex, materials.length);
|
|
322
|
+
materials.push({ color: hex, name: label || '' });
|
|
323
|
+
}
|
|
324
|
+
return colorToIndex.get(hex);
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
if (hasSolidColor) solidMatIndex = addMaterial(solidColorHex, `${rawName}_SOLID`);
|
|
328
|
+
if (hasFaceColors) {
|
|
329
|
+
faceMatIndexById = new Map();
|
|
330
|
+
for (const [fid, hex] of faceColorById.entries()) {
|
|
331
|
+
const faceName = idToFaceName ? (idToFaceName.get(fid) || `FACE_${fid}`) : `FACE_${fid}`;
|
|
332
|
+
const idx = addMaterial(hex, faceName);
|
|
333
|
+
faceMatIndexById.set(fid, idx);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (materials.length > 0) {
|
|
338
|
+
matPid = nextId++;
|
|
339
|
+
lines.push(` <basematerials id="${matPid}">`);
|
|
340
|
+
for (const entry of materials) {
|
|
341
|
+
const nm = entry.name ? xmlEsc(entry.name) : '';
|
|
342
|
+
const nameAttr = nm ? ` name="${nm}"` : '';
|
|
343
|
+
lines.push(` <base${nameAttr} displaycolor="${entry.color}"/>`);
|
|
344
|
+
}
|
|
345
|
+
lines.push(' </basematerials>');
|
|
346
|
+
if (solidMatIndex != null) {
|
|
347
|
+
objectPidAttr = ` pid="${matPid}"`;
|
|
348
|
+
objectPindexAttr = ` pindex="${solidMatIndex}"`;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
} else if (includeFaceTags && faceIDs) {
|
|
352
|
+
useFaceTagsFallback = true;
|
|
353
|
+
}
|
|
354
|
+
} else if (includeFaceTags && faceIDs) {
|
|
355
|
+
useFaceTagsFallback = true;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
let faceIndexOf = null;
|
|
359
|
+
if (useFaceTagsFallback && faceIDs) {
|
|
360
|
+
// Gather unique face IDs present on this mesh
|
|
361
|
+
const uniqueIds = [];
|
|
362
|
+
const seen = new Set();
|
|
363
|
+
for (let t = 0; t < faceIDs.length; t++) {
|
|
364
|
+
const fid = faceIDs[t] >>> 0;
|
|
365
|
+
if (!seen.has(fid)) { seen.add(fid); uniqueIds.push(fid); }
|
|
366
|
+
}
|
|
367
|
+
if (uniqueIds.length > 0) {
|
|
368
|
+
// Map each ID to a readable name if available on the Solid, else fallback
|
|
369
|
+
const idToName = new Map();
|
|
370
|
+
for (let i = 0; i < uniqueIds.length; i++) {
|
|
371
|
+
const fid = uniqueIds[i];
|
|
372
|
+
const nm = (idToFaceName && idToFaceName.get(fid)) || `FACE_${fid}`;
|
|
373
|
+
idToName.set(fid, String(nm));
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Assign contiguous indices in encounter order
|
|
377
|
+
const idToMatIdx = new Map();
|
|
378
|
+
for (let i = 0; i < uniqueIds.length; i++) idToMatIdx.set(uniqueIds[i], i);
|
|
379
|
+
faceIndexOf = (fid) => idToMatIdx.get(fid) ?? 0;
|
|
380
|
+
|
|
381
|
+
// Emit basematerials resource
|
|
382
|
+
matPid = nextId++;
|
|
383
|
+
lines.push(` <basematerials id="${matPid}">`);
|
|
384
|
+
for (let i = 0; i < uniqueIds.length; i++) {
|
|
385
|
+
const fid = uniqueIds[i];
|
|
386
|
+
const nm = idToName.get(fid);
|
|
387
|
+
// Deterministic color derived from name hash for readability
|
|
388
|
+
let color = '#808080';
|
|
389
|
+
try {
|
|
390
|
+
let h = 2166136261 >>> 0;
|
|
391
|
+
const sname = nm || '';
|
|
392
|
+
for (let k = 0; k < sname.length; k++) { h ^= sname.charCodeAt(k); h = (h * 16777619) >>> 0; }
|
|
393
|
+
const r = ((h ) & 0xFF);
|
|
394
|
+
const g = ((h >> 8) & 0xFF);
|
|
395
|
+
const b = ((h >> 16) & 0xFF);
|
|
396
|
+
color = `#${r.toString(16).padStart(2,'0')}${g.toString(16).padStart(2,'0')}${b.toString(16).padStart(2,'0')}`;
|
|
397
|
+
} catch {}
|
|
398
|
+
lines.push(` <base name="${xmlEsc(nm)}" displaycolor="${color}"/>`);
|
|
399
|
+
}
|
|
400
|
+
lines.push(' </basematerials>');
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const objId = nextId++;
|
|
405
|
+
lines.push(` <object id="${objId}" type="model" name="${name}"${objectPidAttr}${objectPindexAttr}>`);
|
|
406
|
+
lines.push(' <mesh>');
|
|
407
|
+
|
|
408
|
+
// Vertices
|
|
409
|
+
lines.push(' <vertices>');
|
|
410
|
+
const vCount = (vp.length / 3) | 0;
|
|
411
|
+
for (let i = 0; i < vCount; i++) {
|
|
412
|
+
let x = vp[i * 3 + 0];
|
|
413
|
+
let y = vp[i * 3 + 1];
|
|
414
|
+
let z = vp[i * 3 + 2];
|
|
415
|
+
if (worldMatrixElements) {
|
|
416
|
+
const e = worldMatrixElements;
|
|
417
|
+
const nx = e[0] * x + e[4] * y + e[8] * z + e[12];
|
|
418
|
+
const ny = e[1] * x + e[5] * y + e[9] * z + e[13];
|
|
419
|
+
const nz = e[2] * x + e[6] * y + e[10] * z + e[14];
|
|
420
|
+
x = nx; y = ny; z = nz;
|
|
421
|
+
}
|
|
422
|
+
const xs = numStr(x * scale, precision);
|
|
423
|
+
const ys = numStr(y * scale, precision);
|
|
424
|
+
const zs = numStr(z * scale, precision);
|
|
425
|
+
lines.push(` <vertex x="${xs}" y="${ys}" z="${zs}"/>`);
|
|
426
|
+
}
|
|
427
|
+
lines.push(' </vertices>');
|
|
428
|
+
|
|
429
|
+
// Triangles
|
|
430
|
+
lines.push(' <triangles>');
|
|
431
|
+
if (faceIDs && faceMatIndexById && matPid != null && faceMatIndexById.size > 0) {
|
|
432
|
+
for (let t = 0; t < tCount; t++) {
|
|
433
|
+
const v1 = tv[t * 3 + 0] >>> 0;
|
|
434
|
+
const v2 = tv[t * 3 + 1] >>> 0;
|
|
435
|
+
const v3 = tv[t * 3 + 2] >>> 0;
|
|
436
|
+
const fid = faceIDs[t] >>> 0;
|
|
437
|
+
const idx = faceMatIndexById.get(fid);
|
|
438
|
+
if (idx != null) {
|
|
439
|
+
lines.push(` <triangle v1="${v1}" v2="${v2}" v3="${v3}" pid="${matPid}" p1="${idx}" p2="${idx}" p3="${idx}"/>`);
|
|
440
|
+
} else {
|
|
441
|
+
lines.push(` <triangle v1="${v1}" v2="${v2}" v3="${v3}"/>`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
} else if (matPid != null && faceIndexOf && faceIDs) {
|
|
445
|
+
for (let t = 0; t < tCount; t++) {
|
|
446
|
+
const v1 = tv[t * 3 + 0] >>> 0;
|
|
447
|
+
const v2 = tv[t * 3 + 1] >>> 0;
|
|
448
|
+
const v3 = tv[t * 3 + 2] >>> 0;
|
|
449
|
+
const idx = faceIndexOf(faceIDs[t] >>> 0) >>> 0;
|
|
450
|
+
lines.push(` <triangle v1="${v1}" v2="${v2}" v3="${v3}" pid="${matPid}" p1="${idx}" p2="${idx}" p3="${idx}"/>`);
|
|
451
|
+
}
|
|
452
|
+
} else {
|
|
453
|
+
for (let t = 0; t < tCount; t++) {
|
|
454
|
+
const v1 = tv[t * 3 + 0] >>> 0;
|
|
455
|
+
const v2 = tv[t * 3 + 1] >>> 0;
|
|
456
|
+
const v3 = tv[t * 3 + 2] >>> 0;
|
|
457
|
+
lines.push(` <triangle v1="${v1}" v2="${v2}" v3="${v3}"/>`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
lines.push(' </triangles>');
|
|
461
|
+
|
|
462
|
+
lines.push(' </mesh>');
|
|
463
|
+
lines.push(' </object>');
|
|
464
|
+
buildItems.push(objId);
|
|
465
|
+
solidIdx++;
|
|
466
|
+
} finally { try { if (mesh && typeof mesh.delete === 'function') mesh.delete(); } catch {} }
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
lines.push(' </resources>');
|
|
470
|
+
lines.push(' <build>');
|
|
471
|
+
for (const id of buildItems) {
|
|
472
|
+
lines.push(` <item objectid="${id}"/>`);
|
|
473
|
+
}
|
|
474
|
+
lines.push(' </build>');
|
|
475
|
+
lines.push('</model>');
|
|
476
|
+
|
|
477
|
+
return lines.join('\n');
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function contentTypesXML() {
|
|
481
|
+
return [
|
|
482
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
483
|
+
'<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">',
|
|
484
|
+
' <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>',
|
|
485
|
+
' <Default Extension="model" ContentType="application/vnd.ms-package.3dmanufacturing-3dmodel+xml"/>',
|
|
486
|
+
' <Default Extension="xml" ContentType="application/xml"/>',
|
|
487
|
+
' <Default Extension="png" ContentType="image/png"/>',
|
|
488
|
+
' <Default Extension="jpg" ContentType="image/jpeg"/>',
|
|
489
|
+
' <Default Extension="jpeg" ContentType="image/jpeg"/>',
|
|
490
|
+
' <Default Extension="svg" ContentType="image/svg+xml"/>',
|
|
491
|
+
'</Types>'
|
|
492
|
+
].join('\n');
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function rootRelsXML({ thumbnailPath, viewImages } = {}) {
|
|
496
|
+
const lines = [];
|
|
497
|
+
lines.push('<?xml version="1.0" encoding="UTF-8"?>');
|
|
498
|
+
lines.push('<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">');
|
|
499
|
+
lines.push(' <Relationship Target="/3D/3dmodel.model" Id="rel0" Type="http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel"/>');
|
|
500
|
+
if (thumbnailPath) {
|
|
501
|
+
lines.push(` <Relationship Target="${xmlEsc(thumbnailPath)}" Id="relThumb" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail"/>`);
|
|
502
|
+
}
|
|
503
|
+
if (Array.isArray(viewImages) && viewImages.length) {
|
|
504
|
+
viewImages.forEach((path, idx) => {
|
|
505
|
+
const id = `relView${idx}`;
|
|
506
|
+
const target = path.startsWith('/') ? path : `/${path}`;
|
|
507
|
+
lines.push(` <Relationship Target="${xmlEsc(target)}" Id="${xmlEsc(id)}" Type="http://schemas.microsoft.com/3dmanufacturing/2013/01/other"/>`);
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
lines.push('</Relationships>');
|
|
511
|
+
return lines.join('\n');
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Generate a 3MF zip archive as Uint8Array.
|
|
516
|
+
* @param {Array} solids Array of SOLID-like objects that expose getMesh() and name.
|
|
517
|
+
* @param {{unit?: string, precision?: number, scale?: number, metadataManager?: any, useMetadataColors?: boolean, includeFaceTags?: boolean, applyWorldTransform?: boolean}} opts
|
|
518
|
+
* @returns {Promise<Uint8Array>}
|
|
519
|
+
*/
|
|
520
|
+
export async function generate3MF(solids, opts = {}) {
|
|
521
|
+
const modelXml = build3MFModelXML(solids, opts);
|
|
522
|
+
const zip = new JSZip();
|
|
523
|
+
zip.file('[Content_Types].xml', contentTypesXML());
|
|
524
|
+
zip.folder('3D').file('3dmodel.model', modelXml);
|
|
525
|
+
// Optional thumbnail embedding (PNG/JPEG)
|
|
526
|
+
let thumbPkgRelPath = null;
|
|
527
|
+
if (opts.thumbnail) {
|
|
528
|
+
try {
|
|
529
|
+
let bytes = null;
|
|
530
|
+
let ext = 'png';
|
|
531
|
+
if (typeof opts.thumbnail === 'string') {
|
|
532
|
+
const parsed = _parseDataUrl(opts.thumbnail);
|
|
533
|
+
if (parsed && parsed.bytes) { bytes = parsed.bytes; ext = (parsed.ext || 'png'); }
|
|
534
|
+
} else if (opts.thumbnail instanceof Uint8Array) {
|
|
535
|
+
bytes = opts.thumbnail;
|
|
536
|
+
ext = 'png';
|
|
537
|
+
}
|
|
538
|
+
if (bytes && bytes.length > 0) {
|
|
539
|
+
const fname = `thumbnail.${ext}`;
|
|
540
|
+
const path = `Metadata/${fname}`;
|
|
541
|
+
zip.folder('Metadata').file(fname, bytes);
|
|
542
|
+
// Root/package-level relationship target (absolute from package root)
|
|
543
|
+
thumbPkgRelPath = `/${path}`;
|
|
544
|
+
}
|
|
545
|
+
} catch { /* ignore thumbnail errors */ }
|
|
546
|
+
}
|
|
547
|
+
// Additional attachments (e.g., Metadata/featureHistory.json)
|
|
548
|
+
const extra = opts.additionalFiles && typeof opts.additionalFiles === 'object' ? opts.additionalFiles : null;
|
|
549
|
+
// Root-level relationships (3D model and optional thumbnail)
|
|
550
|
+
const viewRelPaths = [];
|
|
551
|
+
if (extra) {
|
|
552
|
+
for (const p of Object.keys(extra)) {
|
|
553
|
+
const lower = p.toLowerCase();
|
|
554
|
+
if (lower.startsWith('views/') && lower.endsWith('.png')) {
|
|
555
|
+
const clean = p.startsWith('/') ? p : `/${p}`;
|
|
556
|
+
viewRelPaths.push(clean);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
zip.folder('_rels').file('.rels', rootRelsXML({ thumbnailPath: thumbPkgRelPath, viewImages: viewRelPaths }));
|
|
561
|
+
if (extra) {
|
|
562
|
+
for (const p of Object.keys(extra)) {
|
|
563
|
+
const path = String(p).replace(/^\/+/, '');
|
|
564
|
+
const data = extra[p];
|
|
565
|
+
// Detect if binary/Uint8Array; otherwise treat as string
|
|
566
|
+
if (data instanceof Uint8Array) {
|
|
567
|
+
zip.file(path, data);
|
|
568
|
+
} else {
|
|
569
|
+
zip.file(path, String(data));
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
const data = await zip.generateAsync({ type: 'uint8array', compression: 'DEFLATE', compressionOptions: { level: 6 } });
|
|
574
|
+
return data;
|
|
575
|
+
}
|