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,727 @@
|
|
|
1
|
+
import { BREP } from "../../BREP/BREP.js";
|
|
2
|
+
const THREE = BREP.THREE;
|
|
3
|
+
import { LineGeometry } from 'three/examples/jsm/Addons.js';
|
|
4
|
+
import { ImageEditorUI } from './imageEditor.js';
|
|
5
|
+
import { traceImageDataToPolylines, applyCurveFit, rdp, assignBreaksToLoops, splitLoopIntoEdges, sanitizeLoopsForExtrude, dropIntersectingLoops } from './traceUtils.js';
|
|
6
|
+
|
|
7
|
+
const renderHiddenField = ({ id, row }) => {
|
|
8
|
+
if (row && row.style) row.style.display = 'none';
|
|
9
|
+
const input = document.createElement('input');
|
|
10
|
+
input.type = 'hidden';
|
|
11
|
+
input.id = id;
|
|
12
|
+
return { inputEl: input, inputRegistered: false, skipDefaultRefresh: true };
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const inputParamsSchema = {
|
|
16
|
+
id: {
|
|
17
|
+
type: "string",
|
|
18
|
+
default_value: null,
|
|
19
|
+
hint: "unique identifier for the image trace feature",
|
|
20
|
+
},
|
|
21
|
+
fileToImport: {
|
|
22
|
+
type: "file",
|
|
23
|
+
default_value: "",
|
|
24
|
+
accept: ".png,image/png",
|
|
25
|
+
hint: "Monochrome PNG data (click to choose a file)",
|
|
26
|
+
},
|
|
27
|
+
editImage: {
|
|
28
|
+
type: "button",
|
|
29
|
+
label: "Edit Image",
|
|
30
|
+
default_value: null,
|
|
31
|
+
hint: "Launch the paint like image editor",
|
|
32
|
+
actionFunction: (ctx) => {
|
|
33
|
+
let { fileToImport } = ctx.feature.inputParams;
|
|
34
|
+
// If no image, start with a blank 300x300 transparent canvas
|
|
35
|
+
if (!fileToImport) {
|
|
36
|
+
try {
|
|
37
|
+
const c = document.createElement('canvas');
|
|
38
|
+
c.width = 300; c.height = 300;
|
|
39
|
+
const ctx2d = c.getContext('2d');
|
|
40
|
+
ctx2d.fillStyle = '#ffffff';
|
|
41
|
+
ctx2d.fillRect(0, 0, c.width, c.height);
|
|
42
|
+
fileToImport = c.toDataURL('image/png');
|
|
43
|
+
} catch (_) { fileToImport = null; }
|
|
44
|
+
}
|
|
45
|
+
const imageEditor = new ImageEditorUI(fileToImport, {
|
|
46
|
+
onSave: (editedImage) => {
|
|
47
|
+
// Update both live feature params and dialog params
|
|
48
|
+
try { ctx.feature.inputParams.fileToImport = editedImage; } catch (_) {}
|
|
49
|
+
try { if (ctx.params) ctx.params.fileToImport = editedImage; } catch (_) {}
|
|
50
|
+
// Trigger recompute akin to onChange
|
|
51
|
+
try {
|
|
52
|
+
if (ctx.partHistory) {
|
|
53
|
+
ctx.partHistory.currentHistoryStepId = ctx.feature.inputParams.featureID;
|
|
54
|
+
if (typeof ctx.partHistory.runHistory === 'function') {
|
|
55
|
+
const runPromise = ctx.partHistory.runHistory();
|
|
56
|
+
if (runPromise && typeof runPromise.then === 'function') {
|
|
57
|
+
runPromise.then(() => ctx.partHistory?.queueHistorySnapshot?.({ debounceMs: 0, reason: 'image-edit' }));
|
|
58
|
+
} else {
|
|
59
|
+
ctx.partHistory?.queueHistorySnapshot?.({ debounceMs: 0, reason: 'image-edit' });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
} catch (_) {}
|
|
64
|
+
},
|
|
65
|
+
onCancel: () => { /* no-op */ }
|
|
66
|
+
}, {
|
|
67
|
+
featureSchema: inputParamsSchema,
|
|
68
|
+
featureParams: ctx && ctx.feature && ctx.feature.inputParams ? ctx.feature.inputParams : (ctx?.params || {}),
|
|
69
|
+
partHistory: ctx && ctx.partHistory ? ctx.partHistory : null,
|
|
70
|
+
viewer: ctx && ctx.viewer ? ctx.viewer : (ctx && ctx.partHistory && ctx.partHistory.viewer ? ctx.partHistory.viewer : null),
|
|
71
|
+
onParamsChange: () => {
|
|
72
|
+
try {
|
|
73
|
+
if (ctx && ctx.partHistory) {
|
|
74
|
+
ctx.partHistory.currentHistoryStepId = ctx.feature?.inputParams?.featureID;
|
|
75
|
+
if (typeof ctx.partHistory.runHistory === 'function') {
|
|
76
|
+
const runPromise = ctx.partHistory.runHistory();
|
|
77
|
+
if (runPromise && typeof runPromise.then === 'function') {
|
|
78
|
+
runPromise.then(() => ctx.partHistory?.queueHistorySnapshot?.({ debounceMs: 0, reason: 'image-edit' }));
|
|
79
|
+
} else {
|
|
80
|
+
ctx.partHistory?.queueHistorySnapshot?.({ debounceMs: 0, reason: 'image-edit' });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
} catch (_) { /* ignore */ }
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
imageEditor.open();
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
threshold: {
|
|
92
|
+
type: "number",
|
|
93
|
+
default_value: 128,
|
|
94
|
+
hint: "Pixel threshold (0-255) to classify foreground vs background",
|
|
95
|
+
},
|
|
96
|
+
invert: {
|
|
97
|
+
type: "boolean",
|
|
98
|
+
default_value: false,
|
|
99
|
+
hint: "Invert classification (swap foreground/background)",
|
|
100
|
+
},
|
|
101
|
+
pixelScale: {
|
|
102
|
+
type: "number",
|
|
103
|
+
default_value: 1,
|
|
104
|
+
hint: "World units per pixel (scale for the traced face)",
|
|
105
|
+
},
|
|
106
|
+
center: {
|
|
107
|
+
type: "boolean",
|
|
108
|
+
default_value: true,
|
|
109
|
+
hint: "Center the traced result around the origin",
|
|
110
|
+
},
|
|
111
|
+
smoothCurves: {
|
|
112
|
+
type: "boolean",
|
|
113
|
+
default_value: true,
|
|
114
|
+
hint: "Fit curved segments (Potrace-like) to smooth the traced outlines",
|
|
115
|
+
},
|
|
116
|
+
curveTolerance: {
|
|
117
|
+
type: "number",
|
|
118
|
+
default_value: 0.75,
|
|
119
|
+
step:0.1,
|
|
120
|
+
hint: "Max deviation (world units) for curve smoothing/flattening; larger = smoother",
|
|
121
|
+
},
|
|
122
|
+
speckleArea: {
|
|
123
|
+
type: "number",
|
|
124
|
+
default_value: 2,
|
|
125
|
+
hint: "Discard tiny traced loops below this pixel-area (turd size)",
|
|
126
|
+
},
|
|
127
|
+
simplifyCollinear: {
|
|
128
|
+
type: "boolean",
|
|
129
|
+
default_value: false,
|
|
130
|
+
hint: "Remove intermediate points on straight segments",
|
|
131
|
+
},
|
|
132
|
+
rdpTolerance: {
|
|
133
|
+
type: "number",
|
|
134
|
+
default_value: 1,
|
|
135
|
+
hint: "Optional Ramer–Douglas–Peucker tolerance in world units (0 to disable)",
|
|
136
|
+
},
|
|
137
|
+
edgeSplitAngle: {
|
|
138
|
+
type: "number",
|
|
139
|
+
default_value: 70,
|
|
140
|
+
step: 1,
|
|
141
|
+
hint: "Corner angle (deg) for splitting traced loops into edge segments",
|
|
142
|
+
},
|
|
143
|
+
edgeMinSpacing: {
|
|
144
|
+
type: "number",
|
|
145
|
+
default_value: 0,
|
|
146
|
+
step: 0.5,
|
|
147
|
+
hint: "Minimum edge length between corner splits (world units)",
|
|
148
|
+
},
|
|
149
|
+
edgeBreakPoints: {
|
|
150
|
+
type: "string",
|
|
151
|
+
default_value: [],
|
|
152
|
+
label: "Edge Break Points",
|
|
153
|
+
hint: "Internal use: manual edge breaks from the image editor",
|
|
154
|
+
renderWidget: renderHiddenField,
|
|
155
|
+
},
|
|
156
|
+
edgeSuppressedBreaks: {
|
|
157
|
+
type: "string",
|
|
158
|
+
default_value: [],
|
|
159
|
+
label: "Edge Suppressed Breaks",
|
|
160
|
+
hint: "Internal use: suppressed auto breaks from the image editor",
|
|
161
|
+
renderWidget: renderHiddenField,
|
|
162
|
+
},
|
|
163
|
+
placementPlane: {
|
|
164
|
+
type: "reference_selection",
|
|
165
|
+
selectionFilter: ["PLANE", "FACE"],
|
|
166
|
+
multiple: false,
|
|
167
|
+
default_value: null,
|
|
168
|
+
hint: "Select a plane or face where the traced image will be placed",
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
export class ImageToFaceFeature {
|
|
173
|
+
static shortName = "IMAGE";
|
|
174
|
+
static longName = "Image to Face";
|
|
175
|
+
static inputParamsSchema = inputParamsSchema;
|
|
176
|
+
|
|
177
|
+
constructor() {
|
|
178
|
+
this.inputParams = {};
|
|
179
|
+
this.persistentData = {};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async run(partHistory) {
|
|
183
|
+
const { fileToImport, threshold, invert, pixelScale, center, smoothCurves, curveTolerance, speckleArea, simplifyCollinear, rdpTolerance, edgeSplitAngle, edgeMinSpacing, edgeBreakPoints, edgeSuppressedBreaks } = this.inputParams;
|
|
184
|
+
|
|
185
|
+
const imageData = await decodeToImageData(fileToImport);
|
|
186
|
+
if (!imageData) {
|
|
187
|
+
console.warn('[IMAGE] No image data decoded');
|
|
188
|
+
return { added: [], removed: [] };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const scale = Number(pixelScale) || 1;
|
|
192
|
+
const traceLoops = traceImageDataToPolylines(imageData, {
|
|
193
|
+
threshold: Number.isFinite(Number(threshold)) ? Number(threshold) : 128,
|
|
194
|
+
mode: "luma+alpha",
|
|
195
|
+
invert: !!invert,
|
|
196
|
+
mergeCollinear: !!simplifyCollinear,
|
|
197
|
+
simplify: (rdpTolerance && Number(rdpTolerance) > 0) ? (Number(rdpTolerance) / Math.max(Math.abs(scale) || 1, 1e-9)) : 0,
|
|
198
|
+
minArea: Number.isFinite(Number(speckleArea)) ? Math.max(0, Number(speckleArea)) : 0,
|
|
199
|
+
});
|
|
200
|
+
const loopsGrid = traceLoops.map((loop) => loop.map((p) => [p.x, p.y]));
|
|
201
|
+
if (!loopsGrid.length) {
|
|
202
|
+
console.warn('[IMAGE] No contours found in image');
|
|
203
|
+
return { added: [], removed: [] };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Convert grid loops (integer node coords in image space, y-down) to world 2D loops (x, y-up)
|
|
207
|
+
const loops2D = loopsGrid.map((pts) => gridToWorld2D(pts, scale));
|
|
208
|
+
|
|
209
|
+
// Optional curve fitting (Potrace-like) then simplification/cleanup
|
|
210
|
+
let workingLoops = loops2D;
|
|
211
|
+
const fallbackLoops = loops2D.map((l) => simplifyLoop(l, { simplifyCollinear: true, rdpTolerance: 0 }));
|
|
212
|
+
if (smoothCurves !== false) {
|
|
213
|
+
workingLoops = applyCurveFit(workingLoops, {
|
|
214
|
+
tolerance: Number.isFinite(Number(curveTolerance)) ? Math.max(0.01, Number(curveTolerance)) : Math.max(0.05, Math.abs(scale) * 0.75),
|
|
215
|
+
cornerThresholdDeg: 70,
|
|
216
|
+
iterations: 3,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
const cleanCollinear = smoothCurves === false;
|
|
220
|
+
let simpLoops = workingLoops.map((l) => simplifyLoop(l, { simplifyCollinear: cleanCollinear, rdpTolerance: 0 }));
|
|
221
|
+
const sanitizeEps = Math.max(1e-6, 1e-6 * Math.max(Math.abs(scale) || 1, 1));
|
|
222
|
+
simpLoops = sanitizeLoopsForExtrude(simpLoops, fallbackLoops, { eps: sanitizeEps });
|
|
223
|
+
const invalidCount = simpLoops.filter((l) => !Array.isArray(l) || l.length < 3).length;
|
|
224
|
+
if (invalidCount) console.warn(`[IMAGE] Dropped ${invalidCount} degenerate or self-intersecting loop(s)`);
|
|
225
|
+
const beforeIntersect = simpLoops.length;
|
|
226
|
+
simpLoops = dropIntersectingLoops(simpLoops, { eps: sanitizeEps });
|
|
227
|
+
const droppedIntersect = beforeIntersect - simpLoops.length;
|
|
228
|
+
if (droppedIntersect) console.warn(`[IMAGE] Dropped ${droppedIntersect} intersecting loop(s) to keep output manifold`);
|
|
229
|
+
simpLoops = simpLoops.filter((l) => Array.isArray(l) && l.length >= 3);
|
|
230
|
+
if (!simpLoops.length) {
|
|
231
|
+
console.warn('[IMAGE] All loops invalid after cleanup; aborting');
|
|
232
|
+
return { added: [], removed: [] };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Optionally center (only if there are any points)
|
|
236
|
+
let centerOffset = { x: 0, y: 0 };
|
|
237
|
+
if (center) {
|
|
238
|
+
const allPts = simpLoops.flat();
|
|
239
|
+
if (allPts.length) {
|
|
240
|
+
const bb = bounds2D(allPts);
|
|
241
|
+
const cx = 0.5 * (bb.minX + bb.maxX);
|
|
242
|
+
const cy = 0.5 * (bb.minY + bb.maxY);
|
|
243
|
+
centerOffset = { x: cx, y: cy };
|
|
244
|
+
simpLoops = simpLoops.map((loop) => loop.map(([x, y]) => [x - cx, y - cy]));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const cornerThresholdDeg = Number.isFinite(Number(edgeSplitAngle))
|
|
249
|
+
? Math.max(1, Math.min(179, Number(edgeSplitAngle)))
|
|
250
|
+
: 70;
|
|
251
|
+
const cornerSpacing = Number.isFinite(Number(edgeMinSpacing))
|
|
252
|
+
? Math.max(0, Number(edgeMinSpacing))
|
|
253
|
+
: 0;
|
|
254
|
+
const minSegLen = Math.max(0.5 * Math.abs(scale || 1), 1e-6);
|
|
255
|
+
|
|
256
|
+
const manualBreaksWorld = normalizeBreakPoints(edgeBreakPoints, {
|
|
257
|
+
scale,
|
|
258
|
+
offsetX: centerOffset.x,
|
|
259
|
+
offsetY: centerOffset.y,
|
|
260
|
+
});
|
|
261
|
+
const suppressedBreaksWorld = normalizeBreakPoints(edgeSuppressedBreaks, {
|
|
262
|
+
scale,
|
|
263
|
+
offsetX: centerOffset.x,
|
|
264
|
+
offsetY: centerOffset.y,
|
|
265
|
+
});
|
|
266
|
+
const initialBreaksByLoop = manualBreaksWorld.length
|
|
267
|
+
? assignBreaksToLoops(simpLoops, manualBreaksWorld)
|
|
268
|
+
: simpLoops.map(() => []);
|
|
269
|
+
const loopsWithBreaks = initialBreaksByLoop.some((arr) => arr.length)
|
|
270
|
+
? simpLoops.map((loop, idx) => {
|
|
271
|
+
const breaks = initialBreaksByLoop[idx] || [];
|
|
272
|
+
if (!breaks.length) return loop;
|
|
273
|
+
const info = splitLoopIntoEdges(loop, {
|
|
274
|
+
angleDeg: cornerThresholdDeg,
|
|
275
|
+
minSegLen,
|
|
276
|
+
cornerSpacing,
|
|
277
|
+
manualBreaks: breaks,
|
|
278
|
+
autoBreaks: false,
|
|
279
|
+
returnDebug: true,
|
|
280
|
+
});
|
|
281
|
+
return Array.isArray(info?.ring) && info.ring.length ? info.ring : loop;
|
|
282
|
+
})
|
|
283
|
+
: simpLoops;
|
|
284
|
+
const breaksByLoop = manualBreaksWorld.length
|
|
285
|
+
? assignBreaksToLoops(loopsWithBreaks, manualBreaksWorld)
|
|
286
|
+
: loopsWithBreaks.map(() => []);
|
|
287
|
+
const suppressedByLoop = suppressedBreaksWorld.length
|
|
288
|
+
? assignBreaksToLoops(loopsWithBreaks, suppressedBreaksWorld)
|
|
289
|
+
: loopsWithBreaks.map(() => []);
|
|
290
|
+
|
|
291
|
+
// Group into outer + holes by nesting parity
|
|
292
|
+
const groups = groupLoopsOuterHoles(loopsWithBreaks);
|
|
293
|
+
|
|
294
|
+
// Determine placement transform from selected plane/face
|
|
295
|
+
const basis = getPlacementBasis(this.inputParams?.placementPlane, partHistory);
|
|
296
|
+
const bO = new THREE.Vector3().fromArray(basis.origin);
|
|
297
|
+
const bX = new THREE.Vector3().fromArray(basis.x);
|
|
298
|
+
const bY = new THREE.Vector3().fromArray(basis.y);
|
|
299
|
+
const bZ = new THREE.Vector3().fromArray(basis.z);
|
|
300
|
+
const m = new THREE.Matrix4().makeBasis(bX, bY, bZ).setPosition(bO);
|
|
301
|
+
// Quantize world coordinates to reduce FP drift and guarantee identical
|
|
302
|
+
// vertices between caps and walls. Use a small absolute grid (~1e-6).
|
|
303
|
+
const Q = 1e-6;
|
|
304
|
+
const q = (n) => Math.abs(n) < Q ? 0 : Math.round(n / Q) * Q;
|
|
305
|
+
const toW = (x, y) => {
|
|
306
|
+
const v = new THREE.Vector3(x, y, 0).applyMatrix4(m);
|
|
307
|
+
return [q(v.x), q(v.y), q(v.z)];
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
// Build triangulated Face and boundary Edges
|
|
311
|
+
const sceneGroup = new THREE.Group();
|
|
312
|
+
const featureId = (this.inputParams?.featureID != null && String(this.inputParams.featureID).length)
|
|
313
|
+
? String(this.inputParams.featureID)
|
|
314
|
+
: 'IMAGE_Sketch';
|
|
315
|
+
const edgeNamePrefix = featureId ? `${featureId}:` : '';
|
|
316
|
+
sceneGroup.name = featureId;
|
|
317
|
+
sceneGroup.type = 'SKETCH';
|
|
318
|
+
sceneGroup.onClick = () => { };
|
|
319
|
+
sceneGroup.userData = sceneGroup.userData || {};
|
|
320
|
+
sceneGroup.userData.sketchBasis = {
|
|
321
|
+
origin: Array.isArray(basis.origin) ? basis.origin.slice() : [0, 0, 0],
|
|
322
|
+
x: Array.isArray(basis.x) ? basis.x.slice() : [1, 0, 0],
|
|
323
|
+
y: Array.isArray(basis.y) ? basis.y.slice() : [0, 1, 0],
|
|
324
|
+
z: Array.isArray(basis.z) ? basis.z.slice() : [0, 0, 1],
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
// Build triangulation using THREE.ShapeUtils
|
|
328
|
+
const triPositions = [];
|
|
329
|
+
const boundaryLoopsWorld = [];
|
|
330
|
+
const profileGroups = [];
|
|
331
|
+
|
|
332
|
+
for (const grp of groups) {
|
|
333
|
+
let contour = grp.outer.slice();
|
|
334
|
+
// Drop duplicate last point if present for triangulation API
|
|
335
|
+
if (contour.length >= 2) {
|
|
336
|
+
const f = contour[0], l = contour[contour.length - 1];
|
|
337
|
+
if (f[0] === l[0] && f[1] === l[1]) contour.pop();
|
|
338
|
+
}
|
|
339
|
+
if (signedArea([...contour, contour[0]]) > 0) contour = contour.reverse(); // ensure CW for outer
|
|
340
|
+
const holes = grp.holes.map((h) => {
|
|
341
|
+
let hh = h.slice();
|
|
342
|
+
if (hh.length >= 2) {
|
|
343
|
+
const f = hh[0], l = hh[hh.length - 1];
|
|
344
|
+
if (f[0] === l[0] && f[1] === l[1]) hh.pop();
|
|
345
|
+
}
|
|
346
|
+
if (signedArea([...hh, hh[0]]) < 0) hh = hh.reverse(); // ensure CCW for holes
|
|
347
|
+
return hh;
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
const contourV2 = contour.map((p) => new THREE.Vector2(p[0], p[1]));
|
|
351
|
+
const holesV2 = holes.map((arr) => arr.map((p) => new THREE.Vector2(p[0], p[1])));
|
|
352
|
+
const tris = THREE.ShapeUtils.triangulateShape(contourV2, holesV2);
|
|
353
|
+
|
|
354
|
+
const allPts = contour.concat(...holes);
|
|
355
|
+
for (const t of tris) {
|
|
356
|
+
const a = allPts[t[0]], b = allPts[t[1]], c = allPts[t[2]];
|
|
357
|
+
triPositions.push(a[0], a[1], 0, b[0], b[1], 0, c[0], c[1], 0);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Boundary loop records for downstream Sweep side construction
|
|
361
|
+
const contourClosed = (contour.length && (contour[0][0] === contour[contour.length - 1][0] && contour[0][1] === contour[contour.length - 1][1])) ? contour : contour.concat([contour[0]]);
|
|
362
|
+
const contourClosedW = contourClosed.map(([x, y]) => toW(x, y));
|
|
363
|
+
boundaryLoopsWorld.push({ pts: contourClosedW, isHole: false });
|
|
364
|
+
const holesClosed = holes.map((h) => (h.length && (h[0][0] === h[h.length - 1][0] && h[0][1] === h[h.length - 1][1])) ? h : h.concat([h[0]]));
|
|
365
|
+
const holesClosedW = holesClosed.map((h) => h.map(([x, y]) => toW(x, y)));
|
|
366
|
+
for (const hw of holesClosedW) boundaryLoopsWorld.push({ pts: hw, isHole: true });
|
|
367
|
+
|
|
368
|
+
// For profileGroups used by Sweep caps, store OPEN loops (no duplicate last point)
|
|
369
|
+
const contourOpen = contourClosed.slice(0, -1);
|
|
370
|
+
const holesOpen = holesClosed.map(h => h.slice(0, -1));
|
|
371
|
+
profileGroups.push({
|
|
372
|
+
contour2D: contourOpen.slice(),
|
|
373
|
+
holes2D: holesOpen.map(h => h.slice()),
|
|
374
|
+
contourW: contourClosedW.slice(0, -1),
|
|
375
|
+
holesW: holesClosedW.map(hw => hw.slice(0, -1))
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (!triPositions.length) {
|
|
380
|
+
console.warn('[IMAGE] Triangulation produced no area');
|
|
381
|
+
return { added: [], removed: [] };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const geom = new THREE.BufferGeometry();
|
|
385
|
+
geom.setAttribute('position', new THREE.Float32BufferAttribute(triPositions, 3));
|
|
386
|
+
// Transform triangles from local plane to world placement
|
|
387
|
+
geom.applyMatrix4(m);
|
|
388
|
+
// Quantize geometry to the same grid as boundary loops/edges.
|
|
389
|
+
const posAttr = geom.getAttribute('position');
|
|
390
|
+
if (posAttr && posAttr.itemSize === 3) {
|
|
391
|
+
for (let i = 0; i < posAttr.count; i++) {
|
|
392
|
+
posAttr.setXYZ(
|
|
393
|
+
i,
|
|
394
|
+
q(posAttr.getX(i)),
|
|
395
|
+
q(posAttr.getY(i)),
|
|
396
|
+
q(posAttr.getZ(i))
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
posAttr.needsUpdate = true;
|
|
400
|
+
}
|
|
401
|
+
geom.computeVertexNormals();
|
|
402
|
+
geom.computeBoundingSphere();
|
|
403
|
+
|
|
404
|
+
const face = new BREP.Face(geom);
|
|
405
|
+
face.type = 'FACE';
|
|
406
|
+
face.name = `${edgeNamePrefix}PROFILE`;
|
|
407
|
+
face.userData.faceName = face.name;
|
|
408
|
+
face.userData.boundaryLoopsWorld = boundaryLoopsWorld;
|
|
409
|
+
face.userData.profileGroups = profileGroups;
|
|
410
|
+
|
|
411
|
+
// Edges from loops, split at corners to enable per-edge sidewalls
|
|
412
|
+
const edges = [];
|
|
413
|
+
let edgeIdx = 0;
|
|
414
|
+
let loopIdx = 0;
|
|
415
|
+
const addEdgeSegmentsFromLoop = (loop2D, isHole, manualBreaks, suppressedBreaks) => {
|
|
416
|
+
if (!loop2D || loop2D.length < 2) return;
|
|
417
|
+
const segments = splitLoopIntoEdges(loop2D, {
|
|
418
|
+
angleDeg: cornerThresholdDeg,
|
|
419
|
+
minSegLen,
|
|
420
|
+
cornerSpacing,
|
|
421
|
+
manualBreaks,
|
|
422
|
+
suppressedBreaks,
|
|
423
|
+
autoBreaks: false
|
|
424
|
+
});
|
|
425
|
+
let segIdx = 0;
|
|
426
|
+
for (const seg of segments) {
|
|
427
|
+
if (!seg || seg.length < 2) continue;
|
|
428
|
+
const positions = [];
|
|
429
|
+
const worldPts = [];
|
|
430
|
+
for (let i = 0; i < seg.length; i++) {
|
|
431
|
+
const p = seg[i];
|
|
432
|
+
const w = toW(p[0], p[1]);
|
|
433
|
+
positions.push(w[0], w[1], w[2]);
|
|
434
|
+
worldPts.push([w[0], w[1], w[2]]);
|
|
435
|
+
}
|
|
436
|
+
if (positions.length < 6) continue;
|
|
437
|
+
const lg = new LineGeometry();
|
|
438
|
+
lg.setPositions(positions);
|
|
439
|
+
try { lg.computeBoundingSphere(); } catch { }
|
|
440
|
+
const e = new BREP.Edge(lg);
|
|
441
|
+
e.type = 'EDGE';
|
|
442
|
+
e.name = `${edgeNamePrefix}L${edgeIdx++}`;
|
|
443
|
+
e.closedLoop = false;
|
|
444
|
+
e.userData = {
|
|
445
|
+
polylineLocal: worldPts,
|
|
446
|
+
polylineWorld: true,
|
|
447
|
+
isHole: !!isHole,
|
|
448
|
+
loopIndex: loopIdx,
|
|
449
|
+
segmentIndex: segIdx++
|
|
450
|
+
};
|
|
451
|
+
edges.push(e);
|
|
452
|
+
}
|
|
453
|
+
loopIdx++;
|
|
454
|
+
};
|
|
455
|
+
// Emit edge segments for outer and hole loops
|
|
456
|
+
for (const grp of groups) {
|
|
457
|
+
const outerClosed = grp.outer[0] && grp.outer[grp.outer.length - 1] && (grp.outer[0][0] === grp.outer[grp.outer.length - 1][0] && grp.outer[0][1] === grp.outer[grp.outer.length - 1][1]) ? grp.outer : grp.outer.concat([grp.outer[0]]);
|
|
458
|
+
const outerBreaks = breaksByLoop[grp.outerIndex] || [];
|
|
459
|
+
const outerSuppressed = suppressedByLoop[grp.outerIndex] || [];
|
|
460
|
+
addEdgeSegmentsFromLoop(outerClosed, false, outerBreaks, outerSuppressed);
|
|
461
|
+
for (let hi = 0; hi < grp.holes.length; hi++) {
|
|
462
|
+
const h = grp.holes[hi];
|
|
463
|
+
const hClosed = h[0] && h[h.length - 1] && (h[0][0] === h[h.length - 1][0] && h[0][1] === h[h.length - 1][1]) ? h : h.concat([h[0]]);
|
|
464
|
+
const holeIndex = grp.holeIndices[hi];
|
|
465
|
+
const holeBreaks = breaksByLoop[holeIndex] || [];
|
|
466
|
+
const holeSuppressed = suppressedByLoop[holeIndex] || [];
|
|
467
|
+
addEdgeSegmentsFromLoop(hClosed, true, holeBreaks, holeSuppressed);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Attach edge references to face for convenience
|
|
472
|
+
try { face.edges = edges.slice(); } catch { }
|
|
473
|
+
|
|
474
|
+
sceneGroup.add(face);
|
|
475
|
+
for (const e of edges) sceneGroup.add(e);
|
|
476
|
+
|
|
477
|
+
return { added: [sceneGroup], removed: [] };
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// --- Helpers -----------------------------------------------------------------
|
|
482
|
+
|
|
483
|
+
async function decodeToImageData(raw) {
|
|
484
|
+
try {
|
|
485
|
+
if (!raw) return null;
|
|
486
|
+
if (raw instanceof ImageData) return raw;
|
|
487
|
+
if (raw instanceof ArrayBuffer) {
|
|
488
|
+
// Attempt to decode as PNG
|
|
489
|
+
try {
|
|
490
|
+
const blob = new Blob([raw], { type: 'image/png' });
|
|
491
|
+
const img = await createImageBitmap(blob);
|
|
492
|
+
const c = document.createElement('canvas');
|
|
493
|
+
c.width = img.width; c.height = img.height;
|
|
494
|
+
const ctx = c.getContext('2d');
|
|
495
|
+
ctx.drawImage(img, 0, 0);
|
|
496
|
+
const id = ctx.getImageData(0, 0, img.width, img.height);
|
|
497
|
+
try { img.close && img.close(); } catch { }
|
|
498
|
+
return id;
|
|
499
|
+
} catch { }
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
if (typeof raw === 'string') {
|
|
503
|
+
if (raw.startsWith('data:')) {
|
|
504
|
+
const img = await createImageBitmap(await (await fetch(raw)).blob());
|
|
505
|
+
const c = document.createElement('canvas');
|
|
506
|
+
c.width = img.width; c.height = img.height;
|
|
507
|
+
const ctx = c.getContext('2d');
|
|
508
|
+
ctx.drawImage(img, 0, 0);
|
|
509
|
+
const id = ctx.getImageData(0, 0, img.width, img.height);
|
|
510
|
+
try { img.close && img.close(); } catch { }
|
|
511
|
+
return id;
|
|
512
|
+
}
|
|
513
|
+
// Try to parse as binary base64 (png)
|
|
514
|
+
try {
|
|
515
|
+
const b64 = raw;
|
|
516
|
+
const binaryStr = (typeof atob === 'function') ? atob(b64) : (typeof Buffer !== 'undefined' ? Buffer.from(b64, 'base64').toString('binary') : '');
|
|
517
|
+
const len = binaryStr.length | 0;
|
|
518
|
+
const bytes = new Uint8Array(len);
|
|
519
|
+
for (let i = 0; i < len; i++) bytes[i] = binaryStr.charCodeAt(i) & 0xff;
|
|
520
|
+
const blob = new Blob([bytes], { type: 'image/png' });
|
|
521
|
+
const img = await createImageBitmap(blob);
|
|
522
|
+
const c = document.createElement('canvas');
|
|
523
|
+
c.width = img.width; c.height = img.height;
|
|
524
|
+
const ctx = c.getContext('2d');
|
|
525
|
+
ctx.drawImage(img, 0, 0);
|
|
526
|
+
const id = ctx.getImageData(0, 0, img.width, img.height);
|
|
527
|
+
try { img.close && img.close(); } catch { }
|
|
528
|
+
return id;
|
|
529
|
+
} catch { }
|
|
530
|
+
return null;
|
|
531
|
+
}
|
|
532
|
+
} catch (e) {
|
|
533
|
+
console.warn('[IMAGE] Failed to decode input as image data', e);
|
|
534
|
+
}
|
|
535
|
+
return null;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function gridToWorld2D(gridLoop, scale = 1) {
|
|
539
|
+
// gridLoop: list of [xNode, yNode], y grows down; map to world with y up, z=0
|
|
540
|
+
const out = [];
|
|
541
|
+
for (let i = 0; i < gridLoop.length; i++) {
|
|
542
|
+
const gx = gridLoop[i][0];
|
|
543
|
+
const gy = gridLoop[i][1];
|
|
544
|
+
out.push([gx * scale, -gy * scale]);
|
|
545
|
+
}
|
|
546
|
+
return out;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function simplifyLoop(loop, { simplifyCollinear = true, rdpTolerance = 0 } = {}) {
|
|
550
|
+
let pts = loop.slice();
|
|
551
|
+
// Ensure closed for area/orientation helpers
|
|
552
|
+
if (pts.length && (pts[0][0] !== pts[pts.length - 1][0] || pts[0][1] !== pts[pts.length - 1][1])) {
|
|
553
|
+
pts.push([pts[0][0], pts[0][1]]);
|
|
554
|
+
}
|
|
555
|
+
if (simplifyCollinear) pts = removeCollinear2D(pts);
|
|
556
|
+
if (rdpTolerance && rdpTolerance > 0) pts = rdp(pts, rdpTolerance);
|
|
557
|
+
// Guarantee closure
|
|
558
|
+
if (pts.length && (pts[0][0] !== pts[pts.length - 1][0] || pts[0][1] !== pts[pts.length - 1][1])) pts.push([pts[0][0], pts[0][1]]);
|
|
559
|
+
return pts;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function removeCollinear2D(loop) {
|
|
563
|
+
if (loop.length < 4) return loop.slice();
|
|
564
|
+
const out = [];
|
|
565
|
+
for (let i = 0; i < loop.length - 1; i++) { // leave duplicate last for closure
|
|
566
|
+
const a = loop[(i + loop.length - 2) % (loop.length - 1)];
|
|
567
|
+
const b = loop[(i + loop.length - 1) % (loop.length - 1)];
|
|
568
|
+
const c = loop[i];
|
|
569
|
+
const abx = b[0] - a[0], aby = b[1] - a[1];
|
|
570
|
+
const bcx = c[0] - b[0], bcy = c[1] - b[1];
|
|
571
|
+
const cross = abx * bcy - aby * bcx;
|
|
572
|
+
if (Math.abs(cross) > 1e-12) out.push(b);
|
|
573
|
+
}
|
|
574
|
+
if (out.length >= 1) {
|
|
575
|
+
out.push([out[0][0], out[0][1]]);
|
|
576
|
+
return out;
|
|
577
|
+
}
|
|
578
|
+
// If fully collinear or degenerate, keep original loop to avoid empty result
|
|
579
|
+
return loop.slice();
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function signedArea(loop) {
|
|
583
|
+
let area = 0;
|
|
584
|
+
for (let i = 0; i < loop.length - 1; i++) {
|
|
585
|
+
const a = loop[i], b = loop[i + 1];
|
|
586
|
+
area += a[0] * b[1] - a[1] * b[0];
|
|
587
|
+
}
|
|
588
|
+
return 0.5 * area;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function bounds2D(pts) {
|
|
592
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
593
|
+
for (const p of pts) {
|
|
594
|
+
if (p[0] < minX) minX = p[0];
|
|
595
|
+
if (p[1] < minY) minY = p[1];
|
|
596
|
+
if (p[0] > maxX) maxX = p[0];
|
|
597
|
+
if (p[1] > maxY) maxY = p[1];
|
|
598
|
+
}
|
|
599
|
+
return { minX, minY, maxX, maxY };
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Point-in-polygon using winding number. Accepts closed or open polygon arrays.
|
|
603
|
+
function pointInPoly(pt, poly) {
|
|
604
|
+
const n = Array.isArray(poly) ? poly.length : 0;
|
|
605
|
+
if (n < 3) return false;
|
|
606
|
+
let ring = poly;
|
|
607
|
+
const first = ring[0], last = ring[ring.length - 1];
|
|
608
|
+
if (first && last && first[0] === last[0] && first[1] === last[1]) ring = ring.slice(0, ring.length - 1);
|
|
609
|
+
const x = pt[0], y = pt[1];
|
|
610
|
+
let wn = 0;
|
|
611
|
+
const isLeft = (ax, ay, bx, by, cx, cy) => (bx - ax) * (cy - ay) - (by - ay) * (cx - ax);
|
|
612
|
+
for (let i = 0; i < ring.length; i++) {
|
|
613
|
+
const a = ring[i];
|
|
614
|
+
const b = ring[(i + 1) % ring.length];
|
|
615
|
+
if ((a[1] <= y) && (b[1] > y) && isLeft(a[0], a[1], b[0], b[1], x, y) > 0) wn++;
|
|
616
|
+
else if ((a[1] > y) && (b[1] <= y) && isLeft(a[0], a[1], b[0], b[1], x, y) < 0) wn--;
|
|
617
|
+
}
|
|
618
|
+
return wn !== 0;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function groupLoopsOuterHoles(loops) {
|
|
622
|
+
// Normalize: ensure each loop is closed and oriented CCW for holes, CW for outers
|
|
623
|
+
const closed = loops.map((l) => {
|
|
624
|
+
const c = l.slice();
|
|
625
|
+
if (c.length && (c[0][0] !== c[c.length - 1][0] || c[0][1] !== c[c.length - 1][1])) c.push([c[0][0], c[0][1]]);
|
|
626
|
+
return c;
|
|
627
|
+
});
|
|
628
|
+
const norm = closed.map((l) => {
|
|
629
|
+
const A = signedArea(l);
|
|
630
|
+
if (A < 0) return l.slice();
|
|
631
|
+
const r = l.slice(); r.reverse(); return r;
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
const reps = norm.map((l) => l[0]);
|
|
635
|
+
const depth = new Array(norm.length).fill(0);
|
|
636
|
+
for (let i = 0; i < norm.length; i++) {
|
|
637
|
+
for (let j = 0; j < norm.length; j++) {
|
|
638
|
+
if (i === j) continue;
|
|
639
|
+
if (pointInPoly(reps[i], norm[j])) depth[i]++;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Even depth -> outer; holes are immediate odd-depth children
|
|
644
|
+
const groups = [];
|
|
645
|
+
for (let i = 0; i < norm.length; i++) if ((depth[i] % 2) === 0) groups.push({ outer: i, holes: [] });
|
|
646
|
+
for (let h = 0; h < norm.length; h++) if ((depth[h] % 2) === 1) {
|
|
647
|
+
let best = -1, bestDepth = Infinity;
|
|
648
|
+
for (let g = 0; g < groups.length; g++) {
|
|
649
|
+
const oi = groups[g].outer;
|
|
650
|
+
if (pointInPoly(reps[h], norm[oi])) {
|
|
651
|
+
if (depth[oi] < bestDepth) { best = g; bestDepth = depth[oi]; }
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
if (best >= 0) groups[best].holes.push(h);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
return groups.map((g) => ({
|
|
658
|
+
outer: norm[g.outer].slice(),
|
|
659
|
+
outerIndex: g.outer,
|
|
660
|
+
holes: g.holes.map((h) => norm[h].slice()),
|
|
661
|
+
holeIndices: g.holes.slice(),
|
|
662
|
+
}));
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function normalizeBreakPoints(raw, { scale = 1, offsetX = 0, offsetY = 0 } = {}) {
|
|
666
|
+
if (!Array.isArray(raw)) return [];
|
|
667
|
+
const out = [];
|
|
668
|
+
for (const bp of raw) {
|
|
669
|
+
let x;
|
|
670
|
+
let y;
|
|
671
|
+
if (Array.isArray(bp)) {
|
|
672
|
+
x = Number(bp[0]);
|
|
673
|
+
y = Number(bp[1]);
|
|
674
|
+
} else if (bp && typeof bp === 'object') {
|
|
675
|
+
x = Number(bp.x ?? bp[0]);
|
|
676
|
+
y = Number(bp.y ?? bp[1]);
|
|
677
|
+
}
|
|
678
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) continue;
|
|
679
|
+
out.push([x * scale - offsetX, -y * scale - offsetY]);
|
|
680
|
+
}
|
|
681
|
+
return out;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function getPlacementBasis(ref, partHistory) {
|
|
685
|
+
// Returns { origin:[x,y,z], x:[x,y,z], y:[x,y,z], z:[x,y,z] }
|
|
686
|
+
const x = new THREE.Vector3(1, 0, 0);
|
|
687
|
+
const y = new THREE.Vector3(0, 1, 0);
|
|
688
|
+
const z = new THREE.Vector3(0, 0, 1);
|
|
689
|
+
const origin = new THREE.Vector3(0, 0, 0);
|
|
690
|
+
|
|
691
|
+
let refObj = null;
|
|
692
|
+
try {
|
|
693
|
+
if (Array.isArray(ref)) refObj = ref[0] || null;
|
|
694
|
+
else if (ref && typeof ref === 'object') refObj = ref;
|
|
695
|
+
else if (ref) refObj = partHistory?.scene?.getObjectByName(ref);
|
|
696
|
+
} catch { }
|
|
697
|
+
|
|
698
|
+
if (refObj) {
|
|
699
|
+
try { refObj.updateWorldMatrix(true, true); } catch { }
|
|
700
|
+
// Origin: geometric center if available else world pos
|
|
701
|
+
try {
|
|
702
|
+
const g = refObj.geometry;
|
|
703
|
+
if (g) {
|
|
704
|
+
const bs = g.boundingSphere || (g.computeBoundingSphere(), g.boundingSphere);
|
|
705
|
+
if (bs) origin.copy(refObj.localToWorld(bs.center.clone()));
|
|
706
|
+
else origin.copy(refObj.getWorldPosition(new THREE.Vector3()));
|
|
707
|
+
} else origin.copy(refObj.getWorldPosition(new THREE.Vector3()));
|
|
708
|
+
} catch { origin.copy(refObj.getWorldPosition(new THREE.Vector3())); }
|
|
709
|
+
|
|
710
|
+
// Orientation: FACE uses average normal; PLANE/others use object z-axis
|
|
711
|
+
let n = null;
|
|
712
|
+
if (refObj.type === 'FACE' && typeof refObj.getAverageNormal === 'function') {
|
|
713
|
+
try { n = refObj.getAverageNormal().normalize(); } catch { n = null; }
|
|
714
|
+
}
|
|
715
|
+
if (!n) {
|
|
716
|
+
try { n = new THREE.Vector3(0, 0, 1).applyQuaternion(refObj.getWorldQuaternion(new THREE.Quaternion())).normalize(); } catch { n = new THREE.Vector3(0, 0, 1); }
|
|
717
|
+
}
|
|
718
|
+
const worldUp = new THREE.Vector3(0, 1, 0);
|
|
719
|
+
const tmp = new THREE.Vector3();
|
|
720
|
+
const zx = Math.abs(n.dot(worldUp)) > 0.9 ? new THREE.Vector3(1, 0, 0) : worldUp;
|
|
721
|
+
x.copy(tmp.crossVectors(zx, n).normalize());
|
|
722
|
+
y.copy(tmp.crossVectors(n, x).normalize());
|
|
723
|
+
z.copy(n);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
return { origin: [origin.x, origin.y, origin.z], x: [x.x, x.y, x.z], y: [y.x, y.y, y.z], z: [z.x, z.y, z.z] };
|
|
727
|
+
}
|