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,971 @@
|
|
|
1
|
+
// Shared clean-room trace + smoothing utilities for ImageToFace
|
|
2
|
+
|
|
3
|
+
export function traceImageDataToPolylines(imageData, options = {}) {
|
|
4
|
+
const opt = {
|
|
5
|
+
threshold: 128,
|
|
6
|
+
mode: "luma", // "alpha" | "luma" | "luma+alpha"
|
|
7
|
+
invert: false,
|
|
8
|
+
minArea: 0,
|
|
9
|
+
mergeCollinear: true,
|
|
10
|
+
simplify: 0,
|
|
11
|
+
includeOrientation: false,
|
|
12
|
+
...options,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const w = imageData?.width | 0;
|
|
16
|
+
const h = imageData?.height | 0;
|
|
17
|
+
if (!w || !h) return [];
|
|
18
|
+
|
|
19
|
+
const mask = binarize(imageData, w, h, opt);
|
|
20
|
+
const edges = buildBoundaryEdges(mask, w, h);
|
|
21
|
+
const loops = stitchEdgesToLoops(edges);
|
|
22
|
+
|
|
23
|
+
const out = [];
|
|
24
|
+
for (const loop of loops) {
|
|
25
|
+
let poly = loop;
|
|
26
|
+
|
|
27
|
+
if (opt.mergeCollinear) poly = removeCollinear(poly);
|
|
28
|
+
const area = polygonArea(poly);
|
|
29
|
+
|
|
30
|
+
if (Math.abs(area) < opt.minArea) continue;
|
|
31
|
+
|
|
32
|
+
if (opt.simplify > 0 && poly.length >= 4) {
|
|
33
|
+
poly = rdpClosed(poly, opt.simplify);
|
|
34
|
+
if (opt.mergeCollinear) poly = removeCollinear(poly);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (poly.length >= 3) {
|
|
38
|
+
out.push(opt.includeOrientation ? { polyline: poly, area } : poly);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function binarize(imageData, w, h, opt) {
|
|
46
|
+
const src = imageData.data;
|
|
47
|
+
const mask = new Uint8Array(w * h);
|
|
48
|
+
|
|
49
|
+
for (let i = 0, p = 0; i < src.length; i += 4, p++) {
|
|
50
|
+
const r = src[i + 0];
|
|
51
|
+
const g = src[i + 1];
|
|
52
|
+
const b = src[i + 2];
|
|
53
|
+
const a = src[i + 3];
|
|
54
|
+
|
|
55
|
+
const luma = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
56
|
+
|
|
57
|
+
let fg;
|
|
58
|
+
if (opt.mode === "alpha") {
|
|
59
|
+
fg = a >= opt.threshold;
|
|
60
|
+
} else if (opt.mode === "luma+alpha") {
|
|
61
|
+
fg = a > 0 && luma < opt.threshold;
|
|
62
|
+
} else {
|
|
63
|
+
fg = luma < opt.threshold;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (opt.invert) fg = !fg;
|
|
67
|
+
mask[p] = fg ? 1 : 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return mask;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function at(mask, w, h, x, y) {
|
|
74
|
+
if (x < 0 || y < 0 || x >= w || y >= h) return 0;
|
|
75
|
+
return mask[y * w + x];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function buildBoundaryEdges(mask, w, h) {
|
|
79
|
+
const edges = [];
|
|
80
|
+
|
|
81
|
+
for (let y = 0; y < h; y++) {
|
|
82
|
+
for (let x = 0; x < w; x++) {
|
|
83
|
+
if (!at(mask, w, h, x, y)) continue;
|
|
84
|
+
|
|
85
|
+
if (!at(mask, w, h, x, y - 1)) edges.push({ sx: x, sy: y, ex: x + 1, ey: y, dir: 0 });
|
|
86
|
+
if (!at(mask, w, h, x + 1, y)) edges.push({ sx: x + 1, sy: y, ex: x + 1, ey: y + 1, dir: 1 });
|
|
87
|
+
if (!at(mask, w, h, x, y + 1)) edges.push({ sx: x + 1, sy: y + 1, ex: x, ey: y + 1, dir: 2 });
|
|
88
|
+
if (!at(mask, w, h, x - 1, y)) edges.push({ sx: x, sy: y + 1, ex: x, ey: y, dir: 3 });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return edges;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function stitchEdgesToLoops(edges) {
|
|
96
|
+
const startMap = new Map();
|
|
97
|
+
const visited = new Uint8Array(edges.length);
|
|
98
|
+
|
|
99
|
+
for (let i = 0; i < edges.length; i++) {
|
|
100
|
+
const e = edges[i];
|
|
101
|
+
const k = vkey(e.sx, e.sy);
|
|
102
|
+
let arr = startMap.get(k);
|
|
103
|
+
if (!arr) startMap.set(k, (arr = []));
|
|
104
|
+
arr.push(i);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const loops = [];
|
|
108
|
+
|
|
109
|
+
for (let i = 0; i < edges.length; i++) {
|
|
110
|
+
if (visited[i]) continue;
|
|
111
|
+
|
|
112
|
+
const loop = [];
|
|
113
|
+
let currEdge = edges[i];
|
|
114
|
+
visited[i] = 1;
|
|
115
|
+
|
|
116
|
+
const startX = currEdge.sx;
|
|
117
|
+
const startY = currEdge.sy;
|
|
118
|
+
|
|
119
|
+
loop.push({ x: startX, y: startY });
|
|
120
|
+
|
|
121
|
+
let cx = currEdge.ex;
|
|
122
|
+
let cy = currEdge.ey;
|
|
123
|
+
let dir = currEdge.dir;
|
|
124
|
+
const maxSteps = edges.length + 10;
|
|
125
|
+
|
|
126
|
+
for (let steps = 0; steps < maxSteps; steps++) {
|
|
127
|
+
if (cx === startX && cy === startY) break;
|
|
128
|
+
|
|
129
|
+
loop.push({ x: cx, y: cy });
|
|
130
|
+
|
|
131
|
+
const nextIndex = pickNextEdge(startMap, edges, visited, cx, cy, dir);
|
|
132
|
+
if (nextIndex < 0) {
|
|
133
|
+
loop.length = 0;
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const ne = edges[nextIndex];
|
|
138
|
+
visited[nextIndex] = 1;
|
|
139
|
+
|
|
140
|
+
cx = ne.ex;
|
|
141
|
+
cy = ne.ey;
|
|
142
|
+
dir = ne.dir;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (loop.length >= 3 && (loop[0].x !== loop[loop.length - 1].x || loop[0].y !== loop[loop.length - 1].y)) {
|
|
146
|
+
loops.push(loop);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return loops;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function pickNextEdge(startMap, edges, visited, vx, vy, prevDir) {
|
|
154
|
+
const k = vkey(vx, vy);
|
|
155
|
+
const candidates = startMap.get(k);
|
|
156
|
+
if (!candidates || candidates.length === 0) return -1;
|
|
157
|
+
|
|
158
|
+
const preferred = [
|
|
159
|
+
(prevDir + 1) & 3,
|
|
160
|
+
prevDir,
|
|
161
|
+
(prevDir + 3) & 3,
|
|
162
|
+
(prevDir + 2) & 3,
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
let bestIdx = -1;
|
|
166
|
+
let bestRank = 999;
|
|
167
|
+
|
|
168
|
+
for (const ei of candidates) {
|
|
169
|
+
if (visited[ei]) continue;
|
|
170
|
+
const d = edges[ei].dir;
|
|
171
|
+
const rank = preferred.indexOf(d);
|
|
172
|
+
if (rank >= 0 && rank < bestRank) {
|
|
173
|
+
bestRank = rank;
|
|
174
|
+
bestIdx = ei;
|
|
175
|
+
if (bestRank === 0) break;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return bestIdx;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function vkey(x, y) {
|
|
183
|
+
return `${x},${y}`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function removeCollinear(poly) {
|
|
187
|
+
if (poly.length < 4) return poly;
|
|
188
|
+
|
|
189
|
+
const out = [];
|
|
190
|
+
const n = poly.length;
|
|
191
|
+
|
|
192
|
+
for (let i = 0; i < n; i++) {
|
|
193
|
+
const a = poly[(i - 1 + n) % n];
|
|
194
|
+
const b = poly[i];
|
|
195
|
+
const c = poly[(i + 1) % n];
|
|
196
|
+
|
|
197
|
+
const abx = b.x - a.x, aby = b.y - a.y;
|
|
198
|
+
const bcx = c.x - b.x, bcy = c.y - b.y;
|
|
199
|
+
|
|
200
|
+
const cross = abx * bcy - aby * bcx;
|
|
201
|
+
if (cross !== 0) {
|
|
202
|
+
out.push(b);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if ((abx === 0 && aby === 0) || (bcx === 0 && bcy === 0)) out.push(b);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return out.length >= 3 ? out : poly;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function polygonArea(poly) {
|
|
213
|
+
let a = 0;
|
|
214
|
+
const n = poly.length;
|
|
215
|
+
for (let i = 0; i < n; i++) {
|
|
216
|
+
const p = poly[i];
|
|
217
|
+
const q = poly[(i + 1) % n];
|
|
218
|
+
a += p.x * q.y - q.x * p.y;
|
|
219
|
+
}
|
|
220
|
+
return a / 2;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function rdpClosed(poly, eps) {
|
|
224
|
+
if (poly.length < 4) return poly;
|
|
225
|
+
|
|
226
|
+
const centroid = poly.reduce((acc, p) => ({ x: acc.x + p.x, y: acc.y + p.y }), { x: 0, y: 0 });
|
|
227
|
+
centroid.x /= poly.length;
|
|
228
|
+
centroid.y /= poly.length;
|
|
229
|
+
|
|
230
|
+
let split = 0;
|
|
231
|
+
let best = -1;
|
|
232
|
+
for (let i = 0; i < poly.length; i++) {
|
|
233
|
+
const dx = poly[i].x - centroid.x;
|
|
234
|
+
const dy = poly[i].y - centroid.y;
|
|
235
|
+
const d2 = dx * dx + dy * dy;
|
|
236
|
+
if (d2 > best) {
|
|
237
|
+
best = d2;
|
|
238
|
+
split = i;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const open = poly.slice(split).concat(poly.slice(0, split + 1));
|
|
243
|
+
const simplified = rdpOpen(open, eps);
|
|
244
|
+
|
|
245
|
+
simplified.pop();
|
|
246
|
+
|
|
247
|
+
const rotated = simplified.slice(-split).concat(simplified.slice(0, -split));
|
|
248
|
+
return rotated;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function rdpOpen(points, eps) {
|
|
252
|
+
if (points.length <= 2) return points;
|
|
253
|
+
|
|
254
|
+
const keep = new Uint8Array(points.length);
|
|
255
|
+
keep[0] = 1;
|
|
256
|
+
keep[points.length - 1] = 1;
|
|
257
|
+
|
|
258
|
+
const stack = [[0, points.length - 1]];
|
|
259
|
+
const eps2 = eps * eps;
|
|
260
|
+
|
|
261
|
+
while (stack.length) {
|
|
262
|
+
const [a, b] = stack.pop();
|
|
263
|
+
let maxDist2 = -1;
|
|
264
|
+
let idx = -1;
|
|
265
|
+
|
|
266
|
+
const p1 = points[a];
|
|
267
|
+
const p2 = points[b];
|
|
268
|
+
|
|
269
|
+
for (let i = a + 1; i < b; i++) {
|
|
270
|
+
const d2 = pointToSegmentDist2(points[i], p1, p2);
|
|
271
|
+
if (d2 > maxDist2) {
|
|
272
|
+
maxDist2 = d2;
|
|
273
|
+
idx = i;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (maxDist2 > eps2 && idx !== -1) {
|
|
278
|
+
keep[idx] = 1;
|
|
279
|
+
stack.push([a, idx], [idx, b]);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const out = [];
|
|
284
|
+
for (let i = 0; i < points.length; i++) if (keep[i]) out.push(points[i]);
|
|
285
|
+
return out;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function pointToSegmentDist2(p, a, b) {
|
|
289
|
+
const abx = b.x - a.x;
|
|
290
|
+
const aby = b.y - a.y;
|
|
291
|
+
const apx = p.x - a.x;
|
|
292
|
+
const apy = p.y - a.y;
|
|
293
|
+
|
|
294
|
+
const abLen2 = abx * abx + aby * aby;
|
|
295
|
+
if (abLen2 === 0) return apx * apx + apy * apy;
|
|
296
|
+
|
|
297
|
+
let t = (apx * abx + apy * aby) / abLen2;
|
|
298
|
+
t = Math.max(0, Math.min(1, t));
|
|
299
|
+
|
|
300
|
+
const cx = a.x + t * abx;
|
|
301
|
+
const cy = a.y + t * aby;
|
|
302
|
+
|
|
303
|
+
const dx = p.x - cx;
|
|
304
|
+
const dy = p.y - cy;
|
|
305
|
+
return dx * dx + dy * dy;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function pointToSegmentDist2XY(px, py, ax, ay, bx, by) {
|
|
309
|
+
const abx = bx - ax;
|
|
310
|
+
const aby = by - ay;
|
|
311
|
+
const apx = px - ax;
|
|
312
|
+
const apy = py - ay;
|
|
313
|
+
|
|
314
|
+
const abLen2 = abx * abx + aby * aby;
|
|
315
|
+
if (abLen2 === 0) return apx * apx + apy * apy;
|
|
316
|
+
|
|
317
|
+
let t = (apx * abx + apy * aby) / abLen2;
|
|
318
|
+
t = Math.max(0, Math.min(1, t));
|
|
319
|
+
|
|
320
|
+
const cx = ax + t * abx;
|
|
321
|
+
const cy = ay + t * aby;
|
|
322
|
+
|
|
323
|
+
const dx = px - cx;
|
|
324
|
+
const dy = py - cy;
|
|
325
|
+
return dx * dx + dy * dy;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export function rdp(points, epsilon) {
|
|
329
|
+
if (points.length <= 3) return points.slice();
|
|
330
|
+
const open = points.slice(0, points.length - 1);
|
|
331
|
+
const simplified = rdpRecursive(open, epsilon);
|
|
332
|
+
if (!simplified.length) return points.slice();
|
|
333
|
+
simplified.push([simplified[0][0], simplified[0][1]]);
|
|
334
|
+
return simplified;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function rdpRecursive(points, epsilon) {
|
|
338
|
+
if (points.length < 3) return points.slice();
|
|
339
|
+
const p0 = points[0];
|
|
340
|
+
const pN = points[points.length - 1];
|
|
341
|
+
let index = -1; let dmax = 0;
|
|
342
|
+
for (let i = 1; i < points.length - 1; i++) {
|
|
343
|
+
const d = pointLineDist(points[i], p0, pN);
|
|
344
|
+
if (d > dmax) { index = i; dmax = d; }
|
|
345
|
+
}
|
|
346
|
+
if (dmax > epsilon) {
|
|
347
|
+
const left = rdpRecursive(points.slice(0, index + 1), epsilon);
|
|
348
|
+
const right = rdpRecursive(points.slice(index), epsilon);
|
|
349
|
+
return left.slice(0, left.length - 1).concat(right);
|
|
350
|
+
} else {
|
|
351
|
+
return [p0, pN];
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function pointLineDist(p, a, b) {
|
|
356
|
+
const x = p[0], y = p[1];
|
|
357
|
+
const x1 = a[0], y1 = a[1];
|
|
358
|
+
const x2 = b[0], y2 = b[1];
|
|
359
|
+
const A = x - x1; const B = y - y1; const C = x2 - x1; const D = y2 - y1;
|
|
360
|
+
const dot = A * C + B * D;
|
|
361
|
+
const len2 = C * C + D * D;
|
|
362
|
+
const t = len2 > 0 ? Math.max(0, Math.min(1, dot / len2)) : 0;
|
|
363
|
+
const px = x1 + t * C; const py = y1 + t * D;
|
|
364
|
+
const dx = x - px; const dy = y - py;
|
|
365
|
+
return Math.hypot(dx, dy);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export function applyCurveFit(loops, { tolerance = 0.75, cornerThresholdDeg = 70, iterations = 3 } = {}) {
|
|
369
|
+
const tol = Math.max(1e-4, tolerance);
|
|
370
|
+
const angThresh = Math.max(0, Math.min(180, cornerThresholdDeg)) * (Math.PI / 180);
|
|
371
|
+
|
|
372
|
+
const fitLoop = (loop) => {
|
|
373
|
+
if (!Array.isArray(loop) || loop.length < 3) return loop.slice();
|
|
374
|
+
const ring = (loop[0][0] === loop[loop.length - 1][0] && loop[0][1] === loop[loop.length - 1][1]) ? loop.slice(0, -1) : loop.slice();
|
|
375
|
+
if (ring.length < 3) return loop.slice();
|
|
376
|
+
|
|
377
|
+
const corners = findCorners(ring, angThresh);
|
|
378
|
+
let smoothed;
|
|
379
|
+
if (corners.length === 0) {
|
|
380
|
+
smoothed = chaikinClosed(ring, iterations);
|
|
381
|
+
} else {
|
|
382
|
+
smoothed = smoothWithAnchors(ring, corners, iterations);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
let closed = smoothed.slice();
|
|
386
|
+
if (closed[0][0] !== closed[closed.length - 1][0] || closed[0][1] !== closed[closed.length - 1][1]) {
|
|
387
|
+
closed.push([closed[0][0], closed[0][1]]);
|
|
388
|
+
}
|
|
389
|
+
closed = rdp(closed, tol);
|
|
390
|
+
if (closed[0][0] !== closed[closed.length - 1][0] || closed[0][1] !== closed[closed.length - 1][1]) {
|
|
391
|
+
closed.push([closed[0][0], closed[0][1]]);
|
|
392
|
+
}
|
|
393
|
+
return closed;
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
return loops.map((l) => fitLoop(l));
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function cleanLoop2D(loop, eps) {
|
|
400
|
+
if (!Array.isArray(loop)) return [];
|
|
401
|
+
const out = [];
|
|
402
|
+
const n = loop.length;
|
|
403
|
+
for (let i = 0; i < n; i++) {
|
|
404
|
+
const p = loop[i];
|
|
405
|
+
if (!Array.isArray(p) || p.length < 2) continue;
|
|
406
|
+
const x = Number(p[0]);
|
|
407
|
+
const y = Number(p[1]);
|
|
408
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) continue;
|
|
409
|
+
if (!out.length) {
|
|
410
|
+
out.push([x, y]);
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
const prev = out[out.length - 1];
|
|
414
|
+
const dx = x - prev[0];
|
|
415
|
+
const dy = y - prev[1];
|
|
416
|
+
if ((dx * dx + dy * dy) <= eps * eps) continue;
|
|
417
|
+
out.push([x, y]);
|
|
418
|
+
}
|
|
419
|
+
if (out.length >= 2) {
|
|
420
|
+
const first = out[0];
|
|
421
|
+
const last = out[out.length - 1];
|
|
422
|
+
const dx = first[0] - last[0];
|
|
423
|
+
const dy = first[1] - last[1];
|
|
424
|
+
if ((dx * dx + dy * dy) <= eps * eps) out.pop();
|
|
425
|
+
}
|
|
426
|
+
return out;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function loopSelfIntersects(loop, eps) {
|
|
430
|
+
const ring = cleanLoop2D(loop, eps);
|
|
431
|
+
const n = ring.length;
|
|
432
|
+
if (n < 4) return false;
|
|
433
|
+
const orient = (ax, ay, bx, by, cx, cy) => (bx - ax) * (cy - ay) - (by - ay) * (cx - ax);
|
|
434
|
+
const onSeg = (ax, ay, bx, by, cx, cy) =>
|
|
435
|
+
cx >= Math.min(ax, bx) - eps && cx <= Math.max(ax, bx) + eps
|
|
436
|
+
&& cy >= Math.min(ay, by) - eps && cy <= Math.max(ay, by) + eps;
|
|
437
|
+
const segsIntersect = (a, b, c, d) => {
|
|
438
|
+
const o1 = orient(a[0], a[1], b[0], b[1], c[0], c[1]);
|
|
439
|
+
const o2 = orient(a[0], a[1], b[0], b[1], d[0], d[1]);
|
|
440
|
+
const o3 = orient(c[0], c[1], d[0], d[1], a[0], a[1]);
|
|
441
|
+
const o4 = orient(c[0], c[1], d[0], d[1], b[0], b[1]);
|
|
442
|
+
if (Math.abs(o1) <= eps && onSeg(a[0], a[1], b[0], b[1], c[0], c[1])) return true;
|
|
443
|
+
if (Math.abs(o2) <= eps && onSeg(a[0], a[1], b[0], b[1], d[0], d[1])) return true;
|
|
444
|
+
if (Math.abs(o3) <= eps && onSeg(c[0], c[1], d[0], d[1], a[0], a[1])) return true;
|
|
445
|
+
if (Math.abs(o4) <= eps && onSeg(c[0], c[1], d[0], d[1], b[0], b[1])) return true;
|
|
446
|
+
return (o1 * o2 < -eps) && (o3 * o4 < -eps);
|
|
447
|
+
};
|
|
448
|
+
for (let i = 0; i < n; i++) {
|
|
449
|
+
const a = ring[i];
|
|
450
|
+
const b = ring[(i + 1) % n];
|
|
451
|
+
for (let j = i + 1; j < n; j++) {
|
|
452
|
+
const isAdjacent = (j === i) || (j === i + 1) || (i === 0 && j === n - 1);
|
|
453
|
+
if (isAdjacent) continue;
|
|
454
|
+
const c = ring[j];
|
|
455
|
+
const d = ring[(j + 1) % n];
|
|
456
|
+
if (segsIntersect(a, b, c, d)) return true;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function loopAreaAbs(loop) {
|
|
463
|
+
const ring = cleanLoop2D(loop, 1e-12);
|
|
464
|
+
const n = ring.length;
|
|
465
|
+
if (n < 3) return 0;
|
|
466
|
+
let a = 0;
|
|
467
|
+
for (let i = 0; i < n; i++) {
|
|
468
|
+
const p = ring[i];
|
|
469
|
+
const q = ring[(i + 1) % n];
|
|
470
|
+
a += p[0] * q[1] - q[0] * p[1];
|
|
471
|
+
}
|
|
472
|
+
return Math.abs(a * 0.5);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function loopsIntersect2D(loopA, loopB, eps) {
|
|
476
|
+
const a = cleanLoop2D(loopA, eps);
|
|
477
|
+
const b = cleanLoop2D(loopB, eps);
|
|
478
|
+
if (a.length < 2 || b.length < 2) return false;
|
|
479
|
+
const orient = (ax, ay, bx, by, cx, cy) => (bx - ax) * (cy - ay) - (by - ay) * (cx - ax);
|
|
480
|
+
const onSeg = (ax, ay, bx, by, cx, cy) =>
|
|
481
|
+
cx >= Math.min(ax, bx) - eps && cx <= Math.max(ax, bx) + eps
|
|
482
|
+
&& cy >= Math.min(ay, by) - eps && cy <= Math.max(ay, by) + eps;
|
|
483
|
+
const segsIntersect = (p1, p2, p3, p4) => {
|
|
484
|
+
const o1 = orient(p1[0], p1[1], p2[0], p2[1], p3[0], p3[1]);
|
|
485
|
+
const o2 = orient(p1[0], p1[1], p2[0], p2[1], p4[0], p4[1]);
|
|
486
|
+
const o3 = orient(p3[0], p3[1], p4[0], p4[1], p1[0], p1[1]);
|
|
487
|
+
const o4 = orient(p3[0], p3[1], p4[0], p4[1], p2[0], p2[1]);
|
|
488
|
+
if (Math.abs(o1) <= eps && onSeg(p1[0], p1[1], p2[0], p2[1], p3[0], p3[1])) return true;
|
|
489
|
+
if (Math.abs(o2) <= eps && onSeg(p1[0], p1[1], p2[0], p2[1], p4[0], p4[1])) return true;
|
|
490
|
+
if (Math.abs(o3) <= eps && onSeg(p3[0], p3[1], p4[0], p4[1], p1[0], p1[1])) return true;
|
|
491
|
+
if (Math.abs(o4) <= eps && onSeg(p3[0], p3[1], p4[0], p4[1], p2[0], p2[1])) return true;
|
|
492
|
+
return (o1 * o2 < -eps) && (o3 * o4 < -eps);
|
|
493
|
+
};
|
|
494
|
+
const na = a.length;
|
|
495
|
+
const nb = b.length;
|
|
496
|
+
for (let i = 0; i < na; i++) {
|
|
497
|
+
const a0 = a[i];
|
|
498
|
+
const a1 = a[(i + 1) % na];
|
|
499
|
+
for (let j = 0; j < nb; j++) {
|
|
500
|
+
const b0 = b[j];
|
|
501
|
+
const b1 = b[(j + 1) % nb];
|
|
502
|
+
if (segsIntersect(a0, a1, b0, b1)) return true;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
return false;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
export function sanitizeLoopsForExtrude(loops, fallbackLoops, { eps = 1e-6 } = {}) {
|
|
509
|
+
const base = Array.isArray(loops) ? loops : [];
|
|
510
|
+
const fallback = Array.isArray(fallbackLoops) ? fallbackLoops : [];
|
|
511
|
+
const out = [];
|
|
512
|
+
for (let i = 0; i < base.length; i++) {
|
|
513
|
+
let loop = cleanLoop2D(base[i], eps);
|
|
514
|
+
if (loop.length < 3) { out.push(loop); continue; }
|
|
515
|
+
if (loopSelfIntersects(loop, eps)) {
|
|
516
|
+
const fb = cleanLoop2D(fallback[i] || loop, eps);
|
|
517
|
+
if (fb.length >= 3 && !loopSelfIntersects(fb, eps)) loop = fb;
|
|
518
|
+
else loop = [];
|
|
519
|
+
}
|
|
520
|
+
out.push(loop);
|
|
521
|
+
}
|
|
522
|
+
return out;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
export function dropIntersectingLoops(loops, { eps = 1e-6 } = {}) {
|
|
526
|
+
const list = Array.isArray(loops) ? loops : [];
|
|
527
|
+
const n = list.length;
|
|
528
|
+
if (n < 2) return list.slice();
|
|
529
|
+
const areas = list.map((l) => loopAreaAbs(l));
|
|
530
|
+
const drop = new Set();
|
|
531
|
+
for (let i = 0; i < n; i++) {
|
|
532
|
+
if (drop.has(i)) continue;
|
|
533
|
+
for (let j = i + 1; j < n; j++) {
|
|
534
|
+
if (drop.has(j)) continue;
|
|
535
|
+
if (!loopsIntersect2D(list[i], list[j], eps)) continue;
|
|
536
|
+
if (areas[i] <= areas[j]) drop.add(i);
|
|
537
|
+
else drop.add(j);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
return list.filter((_, idx) => !drop.has(idx));
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
export function assignBreaksToLoops(loops, breaks, { snapDist = Infinity } = {}) {
|
|
544
|
+
const out = Array.isArray(loops) ? loops.map(() => []) : [];
|
|
545
|
+
if (!Array.isArray(loops) || !Array.isArray(breaks) || !breaks.length) return out;
|
|
546
|
+
const snap2 = Number.isFinite(snapDist) ? snapDist * snapDist : Infinity;
|
|
547
|
+
|
|
548
|
+
for (const bp of breaks) {
|
|
549
|
+
const px = Array.isArray(bp) ? Number(bp[0]) : NaN;
|
|
550
|
+
const py = Array.isArray(bp) ? Number(bp[1]) : NaN;
|
|
551
|
+
if (!Number.isFinite(px) || !Number.isFinite(py)) continue;
|
|
552
|
+
|
|
553
|
+
let bestLoop = -1;
|
|
554
|
+
let bestDist2 = Infinity;
|
|
555
|
+
|
|
556
|
+
for (let li = 0; li < loops.length; li++) {
|
|
557
|
+
const loop = loops[li];
|
|
558
|
+
if (!Array.isArray(loop) || loop.length < 2) continue;
|
|
559
|
+
const ring = loop.length > 1
|
|
560
|
+
&& loop[0][0] === loop[loop.length - 1][0]
|
|
561
|
+
&& loop[0][1] === loop[loop.length - 1][1]
|
|
562
|
+
? loop.slice(0, -1)
|
|
563
|
+
: loop;
|
|
564
|
+
const n = ring.length;
|
|
565
|
+
if (n < 2) continue;
|
|
566
|
+
for (let i = 0; i < n; i++) {
|
|
567
|
+
const a = ring[i];
|
|
568
|
+
const b = ring[(i + 1) % n];
|
|
569
|
+
const d2 = pointToSegmentDist2XY(px, py, a[0], a[1], b[0], b[1]);
|
|
570
|
+
if (d2 < bestDist2) {
|
|
571
|
+
bestDist2 = d2;
|
|
572
|
+
bestLoop = li;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (bestLoop >= 0 && bestDist2 <= snap2) {
|
|
578
|
+
out[bestLoop].push([px, py]);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
return out;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function findCorners(ring, angThresh) {
|
|
586
|
+
const n = ring.length;
|
|
587
|
+
if (n < 3) return [];
|
|
588
|
+
const window = Math.max(2, Math.min(8, Math.floor(n / 40) || 2));
|
|
589
|
+
const straightThresh = 0.85;
|
|
590
|
+
const minSpan = window * 0.75;
|
|
591
|
+
const corners = [];
|
|
592
|
+
|
|
593
|
+
const sampleDir = (startIdx, step) => {
|
|
594
|
+
let sx = 0;
|
|
595
|
+
let sy = 0;
|
|
596
|
+
let total = 0;
|
|
597
|
+
for (let k = 0; k < window; k++) {
|
|
598
|
+
const i0 = (startIdx + k * step + n) % n;
|
|
599
|
+
const i1 = (i0 + step + n) % n;
|
|
600
|
+
const dx = ring[i1][0] - ring[i0][0];
|
|
601
|
+
const dy = ring[i1][1] - ring[i0][1];
|
|
602
|
+
const len = Math.hypot(dx, dy);
|
|
603
|
+
if (!len) continue;
|
|
604
|
+
sx += dx;
|
|
605
|
+
sy += dy;
|
|
606
|
+
total += len;
|
|
607
|
+
}
|
|
608
|
+
const mag = Math.hypot(sx, sy);
|
|
609
|
+
return {
|
|
610
|
+
dir: mag > 1e-9 ? [sx / mag, sy / mag] : [0, 0],
|
|
611
|
+
straightness: total > 0 ? mag / total : 0,
|
|
612
|
+
span: total
|
|
613
|
+
};
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
for (let i = 0; i < n; i++) {
|
|
617
|
+
const prev = sampleDir(i - window, 1);
|
|
618
|
+
const next = sampleDir(i, 1);
|
|
619
|
+
if (prev.span < minSpan || next.span < minSpan) continue;
|
|
620
|
+
if (prev.straightness < straightThresh || next.straightness < straightThresh) continue;
|
|
621
|
+
const dot = prev.dir[0] * next.dir[0] + prev.dir[1] * next.dir[1];
|
|
622
|
+
const ang = Math.acos(Math.max(-1, Math.min(1, dot)));
|
|
623
|
+
if (ang > angThresh) corners.push(i);
|
|
624
|
+
}
|
|
625
|
+
return corners;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function chaikinClosed(points, iterations) {
|
|
629
|
+
let pts = points.slice();
|
|
630
|
+
for (let k = 0; k < iterations; k++) {
|
|
631
|
+
const next = [];
|
|
632
|
+
for (let i = 0; i < pts.length; i++) {
|
|
633
|
+
const a = pts[i];
|
|
634
|
+
const b = pts[(i + 1) % pts.length];
|
|
635
|
+
const q = [0.75 * a[0] + 0.25 * b[0], 0.75 * a[1] + 0.25 * b[1]];
|
|
636
|
+
const r = [0.25 * a[0] + 0.75 * b[0], 0.25 * a[1] + 0.75 * b[1]];
|
|
637
|
+
next.push(q, r);
|
|
638
|
+
}
|
|
639
|
+
pts = next;
|
|
640
|
+
}
|
|
641
|
+
if (pts[0][0] !== pts[pts.length - 1][0] || pts[0][1] !== pts[pts.length - 1][1]) {
|
|
642
|
+
pts.push([pts[0][0], pts[0][1]]);
|
|
643
|
+
}
|
|
644
|
+
return pts;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function chaikinOpen(points, iterations) {
|
|
648
|
+
let pts = points.slice();
|
|
649
|
+
for (let k = 0; k < iterations; k++) {
|
|
650
|
+
const next = [];
|
|
651
|
+
next.push(pts[0]);
|
|
652
|
+
for (let i = 0; i < pts.length - 1; i++) {
|
|
653
|
+
const a = pts[i];
|
|
654
|
+
const b = pts[i + 1];
|
|
655
|
+
const q = [0.75 * a[0] + 0.25 * b[0], 0.75 * a[1] + 0.25 * b[1]];
|
|
656
|
+
const r = [0.25 * a[0] + 0.75 * b[0], 0.25 * a[1] + 0.75 * b[1]];
|
|
657
|
+
next.push(q, r);
|
|
658
|
+
}
|
|
659
|
+
next.push(pts[pts.length - 1]);
|
|
660
|
+
pts = next;
|
|
661
|
+
}
|
|
662
|
+
return pts;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function smoothWithAnchors(ring, corners, iterations) {
|
|
666
|
+
const n = ring.length;
|
|
667
|
+
const out = [];
|
|
668
|
+
const anchors = corners.slice();
|
|
669
|
+
anchors.sort((a, b) => a - b);
|
|
670
|
+
const uniq = [];
|
|
671
|
+
for (const idx of anchors) {
|
|
672
|
+
if (!uniq.length || uniq[uniq.length - 1] !== idx) uniq.push(idx);
|
|
673
|
+
}
|
|
674
|
+
anchors.length = 0; anchors.push(...uniq);
|
|
675
|
+
|
|
676
|
+
for (let ci = 0; ci < anchors.length; ci++) {
|
|
677
|
+
const aIdx = anchors[ci];
|
|
678
|
+
const bIdx = anchors[(ci + 1) % anchors.length];
|
|
679
|
+
const seg = [];
|
|
680
|
+
seg.push(ring[aIdx]);
|
|
681
|
+
let idx = (aIdx + 1) % n;
|
|
682
|
+
while (idx !== bIdx) {
|
|
683
|
+
seg.push(ring[idx]);
|
|
684
|
+
idx = (idx + 1) % n;
|
|
685
|
+
}
|
|
686
|
+
seg.push(ring[bIdx]);
|
|
687
|
+
|
|
688
|
+
const sm = chaikinOpen(seg, iterations);
|
|
689
|
+
if (ci === 0) {
|
|
690
|
+
for (const p of sm) out.push(p);
|
|
691
|
+
} else {
|
|
692
|
+
for (let i = 1; i < sm.length; i++) out.push(sm[i]);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
if (out[0][0] !== out[out.length - 1][0] || out[0][1] !== out[out.length - 1][1]) {
|
|
696
|
+
out.push([out[0][0], out[0][1]]);
|
|
697
|
+
}
|
|
698
|
+
return out;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
export function splitLoopIntoEdges(loop2D, {
|
|
702
|
+
angleDeg = 70,
|
|
703
|
+
minSegLen = 1e-6,
|
|
704
|
+
cornerSpacing = 0,
|
|
705
|
+
manualBreaks = [],
|
|
706
|
+
suppressedBreaks = [],
|
|
707
|
+
autoBreaks = true,
|
|
708
|
+
returnDebug = false,
|
|
709
|
+
} = {}) {
|
|
710
|
+
if (!Array.isArray(loop2D) || loop2D.length < 2) return returnDebug ? { segments: [] } : [];
|
|
711
|
+
let ring = loop2D.slice();
|
|
712
|
+
if (ring.length >= 2 && ring[0][0] === ring[ring.length - 1][0] && ring[0][1] === ring[ring.length - 1][1]) {
|
|
713
|
+
ring.pop();
|
|
714
|
+
}
|
|
715
|
+
let n = ring.length;
|
|
716
|
+
if (n < 2) return returnDebug ? { segments: [] } : [];
|
|
717
|
+
|
|
718
|
+
const manualPts = Array.isArray(manualBreaks) ? manualBreaks : [];
|
|
719
|
+
const manualCornerIndices = [];
|
|
720
|
+
if (manualPts.length) {
|
|
721
|
+
const perSeg = new Map();
|
|
722
|
+
const vertexBreaks = new Set();
|
|
723
|
+
const endpointEps = Math.max(minSegLen * 0.25, 1e-6);
|
|
724
|
+
const closestOnRing = (pt) => {
|
|
725
|
+
const px = pt[0];
|
|
726
|
+
const py = pt[1];
|
|
727
|
+
let best = { dist2: Infinity, segIndex: -1, t: 0, point: null };
|
|
728
|
+
for (let i = 0; i < n; i++) {
|
|
729
|
+
const a = ring[i];
|
|
730
|
+
const b = ring[(i + 1) % n];
|
|
731
|
+
const abx = b[0] - a[0];
|
|
732
|
+
const aby = b[1] - a[1];
|
|
733
|
+
const abLen2 = abx * abx + aby * aby;
|
|
734
|
+
if (abLen2 <= 0) continue;
|
|
735
|
+
let t = ((px - a[0]) * abx + (py - a[1]) * aby) / abLen2;
|
|
736
|
+
t = Math.max(0, Math.min(1, t));
|
|
737
|
+
const cx = a[0] + t * abx;
|
|
738
|
+
const cy = a[1] + t * aby;
|
|
739
|
+
const dx = px - cx;
|
|
740
|
+
const dy = py - cy;
|
|
741
|
+
const d2 = dx * dx + dy * dy;
|
|
742
|
+
if (d2 < best.dist2) {
|
|
743
|
+
best = { dist2: d2, segIndex: i, t, point: [cx, cy] };
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
return best;
|
|
747
|
+
};
|
|
748
|
+
|
|
749
|
+
for (const pt of manualPts) {
|
|
750
|
+
if (!Array.isArray(pt) || pt.length < 2) continue;
|
|
751
|
+
const px = Number(pt[0]);
|
|
752
|
+
const py = Number(pt[1]);
|
|
753
|
+
if (!Number.isFinite(px) || !Number.isFinite(py)) continue;
|
|
754
|
+
const res = closestOnRing([px, py]);
|
|
755
|
+
if (res.segIndex < 0 || !res.point) continue;
|
|
756
|
+
const a = ring[res.segIndex];
|
|
757
|
+
const b = ring[(res.segIndex + 1) % n];
|
|
758
|
+
const da = Math.hypot(res.point[0] - a[0], res.point[1] - a[1]);
|
|
759
|
+
const db = Math.hypot(res.point[0] - b[0], res.point[1] - b[1]);
|
|
760
|
+
if (da <= endpointEps) {
|
|
761
|
+
vertexBreaks.add(res.segIndex);
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
if (db <= endpointEps) {
|
|
765
|
+
vertexBreaks.add((res.segIndex + 1) % n);
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
let arr = perSeg.get(res.segIndex);
|
|
769
|
+
if (!arr) { arr = []; perSeg.set(res.segIndex, arr); }
|
|
770
|
+
arr.push({ t: res.t, point: res.point });
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
if (vertexBreaks.size || perSeg.size) {
|
|
774
|
+
const expanded = [];
|
|
775
|
+
const markManualIndex = (idx) => {
|
|
776
|
+
if (!manualCornerIndices.length || manualCornerIndices[manualCornerIndices.length - 1] !== idx) {
|
|
777
|
+
manualCornerIndices.push(idx);
|
|
778
|
+
}
|
|
779
|
+
};
|
|
780
|
+
for (let i = 0; i < n; i++) {
|
|
781
|
+
const a = ring[i];
|
|
782
|
+
const startIndex = expanded.length;
|
|
783
|
+
expanded.push(a);
|
|
784
|
+
if (vertexBreaks.has(i)) markManualIndex(startIndex);
|
|
785
|
+
const inserts = perSeg.get(i);
|
|
786
|
+
if (inserts && inserts.length) {
|
|
787
|
+
inserts.sort((u, v) => u.t - v.t);
|
|
788
|
+
for (const ins of inserts) {
|
|
789
|
+
const p = ins.point;
|
|
790
|
+
const last = expanded[expanded.length - 1];
|
|
791
|
+
if (!last || last[0] !== p[0] || last[1] !== p[1]) {
|
|
792
|
+
expanded.push(p);
|
|
793
|
+
markManualIndex(expanded.length - 1);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
ring = expanded;
|
|
799
|
+
n = ring.length;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
const angThresh = Math.max(0, Math.min(180, angleDeg)) * (Math.PI / 180);
|
|
804
|
+
let totalLen = 0;
|
|
805
|
+
const cum = new Array(n).fill(0);
|
|
806
|
+
for (let i = 0; i < n; i++) {
|
|
807
|
+
const a = ring[i];
|
|
808
|
+
const b = ring[(i + 1) % n];
|
|
809
|
+
totalLen += Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
810
|
+
if (i + 1 < n) cum[i + 1] = totalLen;
|
|
811
|
+
}
|
|
812
|
+
const avgLen = totalLen > 1e-9 ? (totalLen / n) : minSegLen;
|
|
813
|
+
const spanLen = Math.max(minSegLen, avgLen * 4);
|
|
814
|
+
const minSpan = spanLen * 0.75;
|
|
815
|
+
const straightnessThresh = 0.97;
|
|
816
|
+
const minCornerSpacing = Math.max(spanLen * 1.5, totalLen * 0.015, minSegLen * 2, cornerSpacing || 0);
|
|
817
|
+
|
|
818
|
+
const sampleDir = (startIdx, step) => {
|
|
819
|
+
let sx = 0;
|
|
820
|
+
let sy = 0;
|
|
821
|
+
let acc = 0;
|
|
822
|
+
let idx = startIdx;
|
|
823
|
+
for (let guard = 0; guard < n; guard++) {
|
|
824
|
+
const next = (idx + step + n) % n;
|
|
825
|
+
const dx = ring[next][0] - ring[idx][0];
|
|
826
|
+
const dy = ring[next][1] - ring[idx][1];
|
|
827
|
+
const len = Math.hypot(dx, dy);
|
|
828
|
+
if (len > 0) {
|
|
829
|
+
sx += dx;
|
|
830
|
+
sy += dy;
|
|
831
|
+
acc += len;
|
|
832
|
+
}
|
|
833
|
+
idx = next;
|
|
834
|
+
if (acc >= spanLen) break;
|
|
835
|
+
}
|
|
836
|
+
const mag = Math.hypot(sx, sy);
|
|
837
|
+
const straightness = acc > 0 ? (mag / acc) : 0;
|
|
838
|
+
return {
|
|
839
|
+
dir: mag > 1e-9 ? [sx / mag, sy / mag] : [0, 0],
|
|
840
|
+
span: acc,
|
|
841
|
+
straightness
|
|
842
|
+
};
|
|
843
|
+
};
|
|
844
|
+
|
|
845
|
+
const candidates = [];
|
|
846
|
+
if (autoBreaks !== false) {
|
|
847
|
+
for (let i = 0; i < n; i++) {
|
|
848
|
+
const prev = sampleDir(i, -1);
|
|
849
|
+
const next = sampleDir(i, 1);
|
|
850
|
+
if (prev.span < minSpan || next.span < minSpan) continue;
|
|
851
|
+
if (prev.straightness < straightnessThresh || next.straightness < straightnessThresh) continue;
|
|
852
|
+
const inDir = [-prev.dir[0], -prev.dir[1]];
|
|
853
|
+
const dot = inDir[0] * next.dir[0] + inDir[1] * next.dir[1];
|
|
854
|
+
const ang = Math.acos(Math.max(-1, Math.min(1, dot)));
|
|
855
|
+
if (ang >= angThresh) candidates.push({ idx: i, ang });
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
const arcDist = (a, b) => {
|
|
860
|
+
const da = Math.abs(cum[a] - cum[b]);
|
|
861
|
+
return Math.min(da, totalLen - da);
|
|
862
|
+
};
|
|
863
|
+
const corners = [];
|
|
864
|
+
candidates.sort((a, b) => b.ang - a.ang);
|
|
865
|
+
for (const cand of candidates) {
|
|
866
|
+
let tooClose = false;
|
|
867
|
+
for (const sel of corners) {
|
|
868
|
+
if (arcDist(cand.idx, sel.idx) < minCornerSpacing) { tooClose = true; break; }
|
|
869
|
+
}
|
|
870
|
+
if (!tooClose) corners.push(cand);
|
|
871
|
+
}
|
|
872
|
+
if (corners.length < 2 && candidates.length) {
|
|
873
|
+
corners.length = 0;
|
|
874
|
+
corners.push(...candidates.sort((a, b) => a.idx - b.idx));
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
const suppressedIdx = new Set();
|
|
878
|
+
const suppressedPts = Array.isArray(suppressedBreaks) ? suppressedBreaks : [];
|
|
879
|
+
if (autoBreaks !== false && suppressedPts.length) {
|
|
880
|
+
const snapDist = Math.max(minSegLen * 0.5, 1e-6);
|
|
881
|
+
const snapDist2 = snapDist * snapDist;
|
|
882
|
+
for (const pt of suppressedPts) {
|
|
883
|
+
if (!Array.isArray(pt) || pt.length < 2) continue;
|
|
884
|
+
const px = Number(pt[0]);
|
|
885
|
+
const py = Number(pt[1]);
|
|
886
|
+
if (!Number.isFinite(px) || !Number.isFinite(py)) continue;
|
|
887
|
+
let bestIdx = -1;
|
|
888
|
+
let bestD2 = Infinity;
|
|
889
|
+
for (let i = 0; i < n; i++) {
|
|
890
|
+
const p = ring[i];
|
|
891
|
+
const dx = p[0] - px;
|
|
892
|
+
const dy = p[1] - py;
|
|
893
|
+
const d2 = dx * dx + dy * dy;
|
|
894
|
+
if (d2 < bestD2) {
|
|
895
|
+
bestD2 = d2;
|
|
896
|
+
bestIdx = i;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
if (bestIdx >= 0 && bestD2 <= snapDist2) {
|
|
900
|
+
suppressedIdx.add(bestIdx);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
if (suppressedIdx.size) {
|
|
906
|
+
for (let i = corners.length - 1; i >= 0; i--) {
|
|
907
|
+
if (suppressedIdx.has(corners[i].idx)) corners.splice(i, 1);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
const cornerIdx = [];
|
|
912
|
+
for (const c of corners) cornerIdx.push(c.idx);
|
|
913
|
+
for (const idx of manualCornerIndices) cornerIdx.push(idx);
|
|
914
|
+
if (cornerIdx.length < 2) {
|
|
915
|
+
const loopOut = ring.concat([ring[0]]);
|
|
916
|
+
return returnDebug
|
|
917
|
+
? { segments: [loopOut], corners: [], manualCorners: manualCornerIndices.slice(), ring: ring.slice() }
|
|
918
|
+
: [loopOut];
|
|
919
|
+
}
|
|
920
|
+
cornerIdx.sort((a, b) => a - b);
|
|
921
|
+
const uniq = [];
|
|
922
|
+
for (const idx of cornerIdx) {
|
|
923
|
+
if (!uniq.length || uniq[uniq.length - 1] !== idx) uniq.push(idx);
|
|
924
|
+
}
|
|
925
|
+
if (uniq.length < 2) {
|
|
926
|
+
const loopOut = ring.concat([ring[0]]);
|
|
927
|
+
return returnDebug
|
|
928
|
+
? { segments: [loopOut], corners: [], manualCorners: manualCornerIndices.slice(), ring: ring.slice() }
|
|
929
|
+
: [loopOut];
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
const segments = [];
|
|
933
|
+
const dedupeSeg = (seg) => {
|
|
934
|
+
const out = [];
|
|
935
|
+
let prev = null;
|
|
936
|
+
for (const p of seg) {
|
|
937
|
+
if (!prev || p[0] !== prev[0] || p[1] !== prev[1]) out.push(p);
|
|
938
|
+
prev = p;
|
|
939
|
+
}
|
|
940
|
+
return out;
|
|
941
|
+
};
|
|
942
|
+
for (let i = 0; i < uniq.length; i++) {
|
|
943
|
+
const start = uniq[i];
|
|
944
|
+
const end = uniq[(i + 1) % uniq.length];
|
|
945
|
+
const seg = [];
|
|
946
|
+
let k = start;
|
|
947
|
+
for (let guard = 0; guard <= n; guard++) {
|
|
948
|
+
seg.push(ring[k]);
|
|
949
|
+
if (k === end) break;
|
|
950
|
+
k = (k + 1) % n;
|
|
951
|
+
}
|
|
952
|
+
const cleaned = dedupeSeg(seg);
|
|
953
|
+
if (cleaned.length >= 2) segments.push(cleaned);
|
|
954
|
+
}
|
|
955
|
+
if (!segments.length) {
|
|
956
|
+
const loopOut = ring.concat([ring[0]]);
|
|
957
|
+
return returnDebug
|
|
958
|
+
? { segments: [loopOut], corners: [], manualCorners: manualCornerIndices.slice(), ring: ring.slice() }
|
|
959
|
+
: [loopOut];
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
if (!returnDebug) return segments;
|
|
963
|
+
const manualSet = new Set(manualCornerIndices);
|
|
964
|
+
const autoCorners = uniq.filter((idx) => !manualSet.has(idx));
|
|
965
|
+
return {
|
|
966
|
+
segments,
|
|
967
|
+
corners: autoCorners,
|
|
968
|
+
manualCorners: Array.from(new Set(manualCornerIndices)).sort((a, b) => a - b),
|
|
969
|
+
ring: ring.slice(),
|
|
970
|
+
};
|
|
971
|
+
}
|