brep-io-kernel 1.0.0-ci.9

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.
Files changed (271) hide show
  1. package/LICENSE.md +32 -0
  2. package/README.md +154 -0
  3. package/dist-kernel/brep-kernel.js +74699 -0
  4. package/package.json +58 -0
  5. package/src/BREP/AssemblyComponent.js +42 -0
  6. package/src/BREP/BREP.js +43 -0
  7. package/src/BREP/BetterSolid.js +805 -0
  8. package/src/BREP/Edge.js +103 -0
  9. package/src/BREP/Extrude.js +403 -0
  10. package/src/BREP/Face.js +187 -0
  11. package/src/BREP/MeshRepairer.js +634 -0
  12. package/src/BREP/OffsetShellSolid.js +614 -0
  13. package/src/BREP/PointCloudWrap.js +302 -0
  14. package/src/BREP/Revolve.js +345 -0
  15. package/src/BREP/SolidMethods/authoring.js +112 -0
  16. package/src/BREP/SolidMethods/booleanOps.js +230 -0
  17. package/src/BREP/SolidMethods/chamfer.js +122 -0
  18. package/src/BREP/SolidMethods/edgeResolution.js +25 -0
  19. package/src/BREP/SolidMethods/fillet.js +792 -0
  20. package/src/BREP/SolidMethods/index.js +72 -0
  21. package/src/BREP/SolidMethods/io.js +105 -0
  22. package/src/BREP/SolidMethods/lifecycle.js +103 -0
  23. package/src/BREP/SolidMethods/manifoldOps.js +375 -0
  24. package/src/BREP/SolidMethods/meshCleanup.js +2512 -0
  25. package/src/BREP/SolidMethods/meshQueries.js +264 -0
  26. package/src/BREP/SolidMethods/metadata.js +106 -0
  27. package/src/BREP/SolidMethods/metrics.js +51 -0
  28. package/src/BREP/SolidMethods/transforms.js +361 -0
  29. package/src/BREP/SolidMethods/visualize.js +508 -0
  30. package/src/BREP/SolidShared.js +26 -0
  31. package/src/BREP/Sweep.js +1596 -0
  32. package/src/BREP/Tube.js +857 -0
  33. package/src/BREP/Vertex.js +43 -0
  34. package/src/BREP/applyBooleanOperation.js +704 -0
  35. package/src/BREP/boundsUtils.js +48 -0
  36. package/src/BREP/chamfer.js +551 -0
  37. package/src/BREP/edgePolylineUtils.js +85 -0
  38. package/src/BREP/fillets/common.js +388 -0
  39. package/src/BREP/fillets/fillet.js +1422 -0
  40. package/src/BREP/fillets/filletGeometry.js +15 -0
  41. package/src/BREP/fillets/inset.js +389 -0
  42. package/src/BREP/fillets/offsetHelper.js +143 -0
  43. package/src/BREP/fillets/outset.js +88 -0
  44. package/src/BREP/helix.js +193 -0
  45. package/src/BREP/meshToBrep.js +234 -0
  46. package/src/BREP/primitives.js +279 -0
  47. package/src/BREP/setupManifold.js +71 -0
  48. package/src/BREP/threadGeometry.js +1120 -0
  49. package/src/BREP/triangleUtils.js +8 -0
  50. package/src/BREP/triangulate.js +608 -0
  51. package/src/FeatureRegistry.js +183 -0
  52. package/src/PartHistory.js +1132 -0
  53. package/src/UI/AccordionWidget.js +292 -0
  54. package/src/UI/CADmaterials.js +850 -0
  55. package/src/UI/EnvMonacoEditor.js +522 -0
  56. package/src/UI/FloatingWindow.js +396 -0
  57. package/src/UI/HistoryWidget.js +457 -0
  58. package/src/UI/MainToolbar.js +131 -0
  59. package/src/UI/ModelLibraryView.js +194 -0
  60. package/src/UI/OrthoCameraIdle.js +206 -0
  61. package/src/UI/PluginsWidget.js +280 -0
  62. package/src/UI/SceneListing.js +606 -0
  63. package/src/UI/SelectionFilter.js +629 -0
  64. package/src/UI/ViewCube.js +389 -0
  65. package/src/UI/assembly/AssemblyConstraintCollectionWidget.js +329 -0
  66. package/src/UI/assembly/AssemblyConstraintControlsWidget.js +282 -0
  67. package/src/UI/assembly/AssemblyConstraintsWidget.css +292 -0
  68. package/src/UI/assembly/AssemblyConstraintsWidget.js +1373 -0
  69. package/src/UI/assembly/constraintFaceUtils.js +115 -0
  70. package/src/UI/assembly/constraintHighlightUtils.js +70 -0
  71. package/src/UI/assembly/constraintLabelUtils.js +31 -0
  72. package/src/UI/assembly/constraintPointUtils.js +64 -0
  73. package/src/UI/assembly/constraintSelectionUtils.js +185 -0
  74. package/src/UI/assembly/constraintStatusUtils.js +142 -0
  75. package/src/UI/componentSelectorModal.js +240 -0
  76. package/src/UI/controls/CombinedTransformControls.js +386 -0
  77. package/src/UI/dialogs.js +351 -0
  78. package/src/UI/expressionsManager.js +100 -0
  79. package/src/UI/featureDialogWidgets/booleanField.js +25 -0
  80. package/src/UI/featureDialogWidgets/booleanOperationField.js +97 -0
  81. package/src/UI/featureDialogWidgets/buttonField.js +45 -0
  82. package/src/UI/featureDialogWidgets/componentSelectorField.js +102 -0
  83. package/src/UI/featureDialogWidgets/defaultField.js +23 -0
  84. package/src/UI/featureDialogWidgets/fileField.js +66 -0
  85. package/src/UI/featureDialogWidgets/index.js +34 -0
  86. package/src/UI/featureDialogWidgets/numberField.js +165 -0
  87. package/src/UI/featureDialogWidgets/optionsField.js +33 -0
  88. package/src/UI/featureDialogWidgets/referenceSelectionField.js +208 -0
  89. package/src/UI/featureDialogWidgets/stringField.js +24 -0
  90. package/src/UI/featureDialogWidgets/textareaField.js +28 -0
  91. package/src/UI/featureDialogWidgets/threadDesignationField.js +160 -0
  92. package/src/UI/featureDialogWidgets/transformField.js +252 -0
  93. package/src/UI/featureDialogWidgets/utils.js +43 -0
  94. package/src/UI/featureDialogWidgets/vec3Field.js +133 -0
  95. package/src/UI/featureDialogs.js +1414 -0
  96. package/src/UI/fileManagerWidget.js +615 -0
  97. package/src/UI/history/HistoryCollectionWidget.js +1294 -0
  98. package/src/UI/history/historyCollectionWidget.css.js +257 -0
  99. package/src/UI/history/historyDisplayInfo.js +133 -0
  100. package/src/UI/mobile.js +28 -0
  101. package/src/UI/objectDump.js +442 -0
  102. package/src/UI/pmi/AnnotationCollectionWidget.js +120 -0
  103. package/src/UI/pmi/AnnotationHistory.js +353 -0
  104. package/src/UI/pmi/AnnotationRegistry.js +90 -0
  105. package/src/UI/pmi/BaseAnnotation.js +269 -0
  106. package/src/UI/pmi/LabelOverlay.css +102 -0
  107. package/src/UI/pmi/LabelOverlay.js +191 -0
  108. package/src/UI/pmi/PMIMode.js +1550 -0
  109. package/src/UI/pmi/PMIViewsWidget.js +1098 -0
  110. package/src/UI/pmi/annUtils.js +729 -0
  111. package/src/UI/pmi/dimensions/AngleDimensionAnnotation.js +647 -0
  112. package/src/UI/pmi/dimensions/ExplodeBodyAnnotation.js +507 -0
  113. package/src/UI/pmi/dimensions/HoleCalloutAnnotation.js +462 -0
  114. package/src/UI/pmi/dimensions/LeaderAnnotation.js +403 -0
  115. package/src/UI/pmi/dimensions/LinearDimensionAnnotation.js +532 -0
  116. package/src/UI/pmi/dimensions/NoteAnnotation.js +110 -0
  117. package/src/UI/pmi/dimensions/RadialDimensionAnnotation.js +659 -0
  118. package/src/UI/pmi/pmiStyle.js +44 -0
  119. package/src/UI/sketcher/SketchMode3D.js +4095 -0
  120. package/src/UI/sketcher/dimensions.js +674 -0
  121. package/src/UI/sketcher/glyphs.js +236 -0
  122. package/src/UI/sketcher/highlights.js +60 -0
  123. package/src/UI/toolbarButtons/aboutButton.js +5 -0
  124. package/src/UI/toolbarButtons/exportButton.js +609 -0
  125. package/src/UI/toolbarButtons/flatPatternButton.js +307 -0
  126. package/src/UI/toolbarButtons/importButton.js +160 -0
  127. package/src/UI/toolbarButtons/inspectorToggleButton.js +12 -0
  128. package/src/UI/toolbarButtons/metadataButton.js +1063 -0
  129. package/src/UI/toolbarButtons/orientToFaceButton.js +114 -0
  130. package/src/UI/toolbarButtons/registerDefaultButtons.js +46 -0
  131. package/src/UI/toolbarButtons/saveButton.js +99 -0
  132. package/src/UI/toolbarButtons/scriptRunnerButton.js +302 -0
  133. package/src/UI/toolbarButtons/testsButton.js +26 -0
  134. package/src/UI/toolbarButtons/undoRedoButtons.js +25 -0
  135. package/src/UI/toolbarButtons/wireframeToggleButton.js +5 -0
  136. package/src/UI/toolbarButtons/zoomToFitButton.js +5 -0
  137. package/src/UI/triangleDebuggerWindow.js +945 -0
  138. package/src/UI/viewer.js +4228 -0
  139. package/src/assemblyConstraints/AssemblyConstraintHistory.js +1576 -0
  140. package/src/assemblyConstraints/AssemblyConstraintRegistry.js +120 -0
  141. package/src/assemblyConstraints/BaseAssemblyConstraint.js +66 -0
  142. package/src/assemblyConstraints/constraintExpressionUtils.js +35 -0
  143. package/src/assemblyConstraints/constraintUtils/parallelAlignment.js +676 -0
  144. package/src/assemblyConstraints/constraints/AngleConstraint.js +485 -0
  145. package/src/assemblyConstraints/constraints/CoincidentConstraint.js +194 -0
  146. package/src/assemblyConstraints/constraints/DistanceConstraint.js +616 -0
  147. package/src/assemblyConstraints/constraints/FixedConstraint.js +78 -0
  148. package/src/assemblyConstraints/constraints/ParallelConstraint.js +252 -0
  149. package/src/assemblyConstraints/constraints/TouchAlignConstraint.js +961 -0
  150. package/src/core/entities/HistoryCollectionBase.js +72 -0
  151. package/src/core/entities/ListEntityBase.js +109 -0
  152. package/src/core/entities/schemaProcesser.js +121 -0
  153. package/src/exporters/sheetMetalFlatPattern.js +659 -0
  154. package/src/exporters/sheetMetalUnfold.js +862 -0
  155. package/src/exporters/step.js +1135 -0
  156. package/src/exporters/threeMF.js +575 -0
  157. package/src/features/assemblyComponent/AssemblyComponentFeature.js +780 -0
  158. package/src/features/boolean/BooleanFeature.js +94 -0
  159. package/src/features/chamfer/ChamferFeature.js +116 -0
  160. package/src/features/datium/DatiumFeature.js +80 -0
  161. package/src/features/edgeFeatureUtils.js +41 -0
  162. package/src/features/extrude/ExtrudeFeature.js +143 -0
  163. package/src/features/fillet/FilletFeature.js +197 -0
  164. package/src/features/helix/HelixFeature.js +405 -0
  165. package/src/features/hole/HoleFeature.js +1050 -0
  166. package/src/features/hole/screwClearance.js +86 -0
  167. package/src/features/hole/threadDesignationCatalog.js +149 -0
  168. package/src/features/imageHeightSolid/ImageHeightmapSolidFeature.js +463 -0
  169. package/src/features/imageToFace/ImageToFaceFeature.js +727 -0
  170. package/src/features/imageToFace/imageEditor.js +1270 -0
  171. package/src/features/imageToFace/traceUtils.js +971 -0
  172. package/src/features/import3dModel/Import3dModelFeature.js +151 -0
  173. package/src/features/loft/LoftFeature.js +605 -0
  174. package/src/features/mirror/MirrorFeature.js +151 -0
  175. package/src/features/offsetFace/OffsetFaceFeature.js +370 -0
  176. package/src/features/offsetShell/OffsetShellFeature.js +89 -0
  177. package/src/features/overlapCleanup/OverlapCleanupFeature.js +85 -0
  178. package/src/features/pattern/PatternFeature.js +275 -0
  179. package/src/features/patternLinear/PatternLinearFeature.js +120 -0
  180. package/src/features/patternRadial/PatternRadialFeature.js +186 -0
  181. package/src/features/plane/PlaneFeature.js +154 -0
  182. package/src/features/primitiveCone/primitiveConeFeature.js +99 -0
  183. package/src/features/primitiveCube/primitiveCubeFeature.js +70 -0
  184. package/src/features/primitiveCylinder/primitiveCylinderFeature.js +91 -0
  185. package/src/features/primitivePyramid/primitivePyramidFeature.js +72 -0
  186. package/src/features/primitiveSphere/primitiveSphereFeature.js +62 -0
  187. package/src/features/primitiveTorus/primitiveTorusFeature.js +109 -0
  188. package/src/features/remesh/RemeshFeature.js +97 -0
  189. package/src/features/revolve/RevolveFeature.js +111 -0
  190. package/src/features/selectionUtils.js +118 -0
  191. package/src/features/sheetMetal/SheetMetalContourFlangeFeature.js +1656 -0
  192. package/src/features/sheetMetal/SheetMetalCutoutFeature.js +1056 -0
  193. package/src/features/sheetMetal/SheetMetalFlangeFeature.js +1568 -0
  194. package/src/features/sheetMetal/SheetMetalHemFeature.js +43 -0
  195. package/src/features/sheetMetal/SheetMetalObject.js +141 -0
  196. package/src/features/sheetMetal/SheetMetalTabFeature.js +176 -0
  197. package/src/features/sheetMetal/UNFOLD_NEUTRAL_REQUIREMENTS.md +153 -0
  198. package/src/features/sheetMetal/contour-flange-rebuild-spec.md +261 -0
  199. package/src/features/sheetMetal/profileUtils.js +25 -0
  200. package/src/features/sheetMetal/sheetMetalCleanup.js +9 -0
  201. package/src/features/sheetMetal/sheetMetalFaceTypes.js +146 -0
  202. package/src/features/sheetMetal/sheetMetalMetadata.js +165 -0
  203. package/src/features/sheetMetal/sheetMetalPipeline.js +169 -0
  204. package/src/features/sheetMetal/sheetMetalProfileUtils.js +216 -0
  205. package/src/features/sheetMetal/sheetMetalTabUtils.js +29 -0
  206. package/src/features/sheetMetal/sheetMetalTree.js +210 -0
  207. package/src/features/sketch/SketchFeature.js +955 -0
  208. package/src/features/sketch/sketchSolver2D/ConstraintEngine.js +800 -0
  209. package/src/features/sketch/sketchSolver2D/constraintDefinitions.js +704 -0
  210. package/src/features/sketch/sketchSolver2D/mathHelpersMod.js +307 -0
  211. package/src/features/spline/SplineEditorSession.js +988 -0
  212. package/src/features/spline/SplineFeature.js +1388 -0
  213. package/src/features/spline/splineUtils.js +218 -0
  214. package/src/features/sweep/SweepFeature.js +110 -0
  215. package/src/features/transform/TransformFeature.js +152 -0
  216. package/src/features/tube/TubeFeature.js +635 -0
  217. package/src/fs.proxy.js +625 -0
  218. package/src/idbStorage.js +254 -0
  219. package/src/index.js +12 -0
  220. package/src/main.js +15 -0
  221. package/src/metadataManager.js +64 -0
  222. package/src/path.proxy.js +277 -0
  223. package/src/plugins/ghLoader.worker.js +151 -0
  224. package/src/plugins/pluginManager.js +286 -0
  225. package/src/pmi/PMIViewsManager.js +134 -0
  226. package/src/services/componentLibrary.js +198 -0
  227. package/src/tests/ConsoleCapture.js +189 -0
  228. package/src/tests/S7-diagnostics-2025-12-23T18-37-23-570Z.json +630 -0
  229. package/src/tests/browserTests.js +597 -0
  230. package/src/tests/debugBoolean.js +225 -0
  231. package/src/tests/partFiles/badBoolean.json +957 -0
  232. package/src/tests/partFiles/extrudeTest.json +88 -0
  233. package/src/tests/partFiles/filletFail.json +58 -0
  234. package/src/tests/partFiles/import_TEst.part.part.json +646 -0
  235. package/src/tests/partFiles/sheetMetalHem.BREP.json +734 -0
  236. package/src/tests/test_boolean_subtract.js +27 -0
  237. package/src/tests/test_chamfer.js +17 -0
  238. package/src/tests/test_extrudeFace.js +24 -0
  239. package/src/tests/test_fillet.js +17 -0
  240. package/src/tests/test_fillet_nonClosed.js +45 -0
  241. package/src/tests/test_filletsMoreDifficult.js +46 -0
  242. package/src/tests/test_history_features_basic.js +149 -0
  243. package/src/tests/test_hole.js +282 -0
  244. package/src/tests/test_mirror.js +16 -0
  245. package/src/tests/test_offsetShellGrouping.js +85 -0
  246. package/src/tests/test_plane.js +4 -0
  247. package/src/tests/test_primitiveCone.js +11 -0
  248. package/src/tests/test_primitiveCube.js +7 -0
  249. package/src/tests/test_primitiveCylinder.js +8 -0
  250. package/src/tests/test_primitivePyramid.js +9 -0
  251. package/src/tests/test_primitiveSphere.js +17 -0
  252. package/src/tests/test_primitiveTorus.js +21 -0
  253. package/src/tests/test_pushFace.js +126 -0
  254. package/src/tests/test_sheetMetalContourFlange.js +125 -0
  255. package/src/tests/test_sheetMetal_features.js +80 -0
  256. package/src/tests/test_sketch_openLoop.js +45 -0
  257. package/src/tests/test_solidMetrics.js +58 -0
  258. package/src/tests/test_stlLoader.js +1889 -0
  259. package/src/tests/test_sweepFace.js +55 -0
  260. package/src/tests/test_tube.js +45 -0
  261. package/src/tests/test_tube_closedLoop.js +67 -0
  262. package/src/tests/tests.js +493 -0
  263. package/src/tools/assemblyConstraintDialogCapturePage.js +56 -0
  264. package/src/tools/dialogCapturePageFactory.js +227 -0
  265. package/src/tools/featureDialogCapturePage.js +47 -0
  266. package/src/tools/pmiAnnotationDialogCapturePage.js +60 -0
  267. package/src/utils/axisHelpers.js +99 -0
  268. package/src/utils/deepClone.js +69 -0
  269. package/src/utils/geometryTolerance.js +37 -0
  270. package/src/utils/normalizeTypeString.js +8 -0
  271. package/src/utils/xformMath.js +51 -0
@@ -0,0 +1,48 @@
1
+ export function computeBoundsFromVertices(verts) {
2
+ if (!verts || typeof verts.length !== 'number' || verts.length < 3) return null;
3
+ let minX = Infinity, minY = Infinity, minZ = Infinity;
4
+ let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
5
+ for (let i = 0; i + 2 < verts.length; i += 3) {
6
+ const x = verts[i + 0];
7
+ const y = verts[i + 1];
8
+ const z = verts[i + 2];
9
+ if (x < minX) minX = x; if (x > maxX) maxX = x;
10
+ if (y < minY) minY = y; if (y > maxY) maxY = y;
11
+ if (z < minZ) minZ = z; if (z > maxZ) maxZ = z;
12
+ }
13
+ if (!Number.isFinite(minX) || !Number.isFinite(maxX)) return null;
14
+ const dx = maxX - minX;
15
+ const dy = maxY - minY;
16
+ const dz = maxZ - minZ;
17
+ return {
18
+ min: [minX, minY, minZ],
19
+ max: [maxX, maxY, maxZ],
20
+ size: [dx, dy, dz],
21
+ diag: Math.hypot(dx, dy, dz) || 0,
22
+ };
23
+ }
24
+
25
+ export function computeBoundsFromPoints(points) {
26
+ if (!Array.isArray(points) || points.length === 0) return null;
27
+ let minX = Infinity, minY = Infinity, minZ = Infinity;
28
+ let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
29
+ let saw = false;
30
+ for (const p of points) {
31
+ if (!Array.isArray(p) || p.length < 3) continue;
32
+ const x = p[0], y = p[1], z = p[2];
33
+ if (x < minX) minX = x; if (x > maxX) maxX = x;
34
+ if (y < minY) minY = y; if (y > maxY) maxY = y;
35
+ if (z < minZ) minZ = z; if (z > maxZ) maxZ = z;
36
+ saw = true;
37
+ }
38
+ if (!saw) return null;
39
+ const dx = maxX - minX;
40
+ const dy = maxY - minY;
41
+ const dz = maxZ - minZ;
42
+ return {
43
+ min: [minX, minY, minZ],
44
+ max: [maxX, maxY, maxZ],
45
+ size: [dx, dy, dz],
46
+ diag: Math.hypot(dx, dy, dz) || 0,
47
+ };
48
+ }
@@ -0,0 +1,551 @@
1
+ import { Solid } from "./BetterSolid.js";
2
+ import * as THREE from 'three';
3
+ import { averageFaceNormalObjectSpace, localFaceNormalAtPoint } from './fillets/inset.js';
4
+
5
+ // Planar chamfer wedge builder along an input edge shared by two faces.
6
+ // Builds a closed solid consisting of:
7
+ // - A ruled "bevel" surface between two offset rails (one on each face)
8
+ // - Two side strips that lie exactly on the original faces (edge → offset rail)
9
+ // - End caps at first/last sections for open edges
10
+ export class ChamferSolid extends Solid {
11
+ /**
12
+ * @param {object} opts
13
+ * @param {any} opts.edgeToChamfer Target edge (must belong to a Solid)
14
+ * @param {number} [opts.distance=1] Chamfer distance (> 0)
15
+ * @param {number} [opts.sampleCount=50] Sample count along the edge when resampling
16
+ * @param {boolean} [opts.snapSeamToEdge=true] Snap seam to the source edge instead of resampling
17
+ * @param {number} [opts.sideStripSubdiv=8] Subdivisions along side strips
18
+ * @param {number} [opts.seamInsetScale=1e-3] Inset scale for seam stabilization
19
+ * @param {'INSET'|'OUTSET'|string} [opts.direction='INSET'] Boolean behavior (subtract vs union)
20
+ * @param {number} [opts.inflate=0] Tool inflation (negated for OUTSET)
21
+ * @param {boolean} [opts.flipSide=false] Flip side selection
22
+ * @param {boolean} [opts.debug=false] Enable debug aids
23
+ * @param {number} [opts.debugStride=12] Sampling stride for debug output
24
+ */
25
+ constructor({ edgeToChamfer, distance = 1, sampleCount = 50, snapSeamToEdge = true, sideStripSubdiv = 8, seamInsetScale = 1e-3, direction = 'INSET', inflate = 0, flipSide = false, debug = false, debugStride = 12 }) {
26
+ super();
27
+ this.edgeToChamfer = edgeToChamfer;
28
+ this.distance = Math.max(1e-9, distance);
29
+ this.sampleCount = Math.max(8, (sampleCount | 0));
30
+ this.snapSeamToEdge = !!snapSeamToEdge;
31
+ this.sideStripSubdiv = Math.max(1, (sideStripSubdiv | 0));
32
+ this.seamInsetScale = Number.isFinite(seamInsetScale) ? seamInsetScale : 1e-3;
33
+ this.direction = (direction || 'INSET').toUpperCase(); // 'INSET' | 'OUTSET'
34
+ this.inflate = Number.isFinite(inflate) ? inflate : 0;
35
+ this.flipSide = !!flipSide;
36
+ this.debug = !!debug;
37
+ this.debugStride = Math.max(1, (debugStride | 0));
38
+ this._debugObjects = [];
39
+ this.operationTargetSolid = null;
40
+ this.generate();
41
+ }
42
+
43
+ generate() {
44
+ if (this.edgeToChamfer && this.edgeToChamfer.parent) {
45
+ this.operationTargetSolid = this.edgeToChamfer.parent;
46
+ } else {
47
+ throw new Error("Edge must be part of a solid");
48
+ }
49
+
50
+ // Clear prior debug helpers
51
+ if (this._debugObjects?.length) {
52
+ const scene = this.operationTargetSolid?.parent;
53
+ if (scene) {
54
+ for (const o of this._debugObjects) scene.remove(o);
55
+ }
56
+ this._debugObjects.length = 0;
57
+ }
58
+
59
+ const solid = this.operationTargetSolid;
60
+ const faceA = this.edgeToChamfer.faces?.[0];
61
+ const faceB = this.edgeToChamfer.faces?.[1];
62
+ if (!faceA || !faceB) throw new Error('ChamferSolid: edge must have two adjacent faces.');
63
+
64
+ const polyLocal = this.edgeToChamfer.userData?.polylineLocal;
65
+ if (!Array.isArray(polyLocal) || polyLocal.length < 2) throw new Error('ChamferSolid: edge polyline missing.');
66
+
67
+ const nAavg = averageFaceNormalObjectSpace(solid, faceA.name);
68
+ const nBavg = averageFaceNormalObjectSpace(solid, faceB.name);
69
+
70
+ const isClosed = !!(this.edgeToChamfer.closedLoop || this.edgeToChamfer.userData?.closedLoop);
71
+ let samples;
72
+ if (this.snapSeamToEdge) {
73
+ const src = polyLocal.slice();
74
+ if (isClosed && src.length > 2) {
75
+ const a = src[0], b = src[src.length - 1];
76
+ if (a && b && a[0] === b[0] && a[1] === b[1] && a[2] === b[2]) src.pop();
77
+ }
78
+ samples = src;
79
+ } else {
80
+ samples = resamplePolyline3(polyLocal, this.sampleCount, isClosed);
81
+ }
82
+
83
+ const railP = [];
84
+ const railA = []; // on faceA (offset inward/outward per face)
85
+ const railB = []; // on faceB (offset inward/outward per face)
86
+ const normalsA = [];
87
+ const normalsB = [];
88
+ const tangents = [];
89
+
90
+ // Decide a global offset sign sSign ∈ {+1,-1} so the bevel consistently
91
+ // goes INSET (toward inward) or OUTSET (toward outward) along the edge.
92
+ // Evaluate at mid sample using local face normals.
93
+ const midIdx = (samples.length / 2) | 0;
94
+ const pm = arrToV(samples[midIdx]);
95
+ const pmPrev = arrToV(samples[Math.max(0, midIdx - 1)]);
96
+ const pmNext = arrToV(samples[Math.min(samples.length - 1, midIdx + 1)]);
97
+ const tm = new THREE.Vector3().subVectors(pmNext, pmPrev).normalize();
98
+ const nAm = localFaceNormalAtPoint(solid, faceA.name, pm) || nAavg;
99
+ const nBm = localFaceNormalAtPoint(solid, faceB.name, pm) || nBavg;
100
+ const vAm = nAm.clone().cross(tm).normalize();
101
+ const vBm = nBm.clone().cross(tm).normalize();
102
+ const outwardAvgMid = nAm.clone().add(nBm);
103
+ if (outwardAvgMid.lengthSq() > 0) outwardAvgMid.normalize();
104
+ const want = (this.direction === 'OUTSET') ? +1 : -1; // desired sign of dot(offsetDir, outwardAvg)
105
+ const sVAm = signNonZero(vAm.dot(outwardAvgMid));
106
+ const sVBm = signNonZero(vBm.dot(outwardAvgMid));
107
+ const sAglobal = want * sVAm; // ensures dot(sA*vAm, outwardAvg) has desired sign
108
+ const sBglobal = want * sVBm; // ensures dot(sB*vBm, outwardAvg) has desired sign
109
+ const sFlip = this.flipSide ? -1 : 1;
110
+ const sA = sAglobal * sFlip;
111
+ const sB = sBglobal * sFlip;
112
+
113
+ // Build offset rails with the chosen global sign
114
+ for (let i = 0; i < samples.length; i++) {
115
+ const p = arrToV(samples[i]);
116
+ const pPrev = isClosed
117
+ ? arrToV(samples[(i - 1 + samples.length) % samples.length])
118
+ : arrToV(samples[Math.max(0, i - 1)]);
119
+ const pNext = isClosed
120
+ ? arrToV(samples[(i + 1) % samples.length])
121
+ : arrToV(samples[Math.min(samples.length - 1, i + 1)]);
122
+ const t = new THREE.Vector3().subVectors(pNext, pPrev);
123
+ if (t.lengthSq() < 1e-14) continue;
124
+ t.normalize();
125
+
126
+ const nA = (localFaceNormalAtPoint(solid, faceA.name, p) || nAavg).clone();
127
+ const nB = (localFaceNormalAtPoint(solid, faceB.name, p) || nBavg).clone();
128
+ let vA3 = nA.clone().cross(t);
129
+ let vB3 = nB.clone().cross(t);
130
+ if (vA3.lengthSq() < 1e-12 || vB3.lengthSq() < 1e-12) continue;
131
+ vA3.normalize(); vB3.normalize();
132
+
133
+ const Ai = p.clone().addScaledVector(vA3, sA * this.distance);
134
+ const Bi = p.clone().addScaledVector(vB3, sB * this.distance);
135
+ railP.push(p.clone());
136
+ railA.push(Ai);
137
+ railB.push(Bi);
138
+ normalsA.push(nA.normalize());
139
+ normalsB.push(nB.normalize());
140
+ tangents.push(t.clone());
141
+
142
+ if (this.debug && (i % this.debugStride === 0)) {
143
+ const scene = this.operationTargetSolid?.parent;
144
+ if (scene) {
145
+ const addLine = (from, to, color) => {
146
+ const g = new THREE.BufferGeometry().setFromPoints([from, to]);
147
+ const m = new THREE.LineBasicMaterial({ color });
148
+ const L = new THREE.Line(g, m);
149
+ L.renderOrder = 10;
150
+ scene.add(L);
151
+ this._debugObjects.push(L);
152
+ };
153
+ const Ls = Math.max(0.4 * this.distance, 1e-3);
154
+ addLine(p, p.clone().addScaledVector(vA3, Ls * sA), 0x00ffff);
155
+ addLine(p, p.clone().addScaledVector(vB3, Ls * sB), 0xffff00);
156
+ addLine(Ai, Bi, 0xff00ff);
157
+ }
158
+ }
159
+ }
160
+
161
+ reorderChamferRailSamples({
162
+ railP,
163
+ railA,
164
+ railB,
165
+ normalsA,
166
+ normalsB,
167
+ tangents,
168
+ isClosed,
169
+ });
170
+
171
+ const closeLoop = !!isClosed;
172
+ const baseName = `CHAMFER_${faceA.name}|${faceB.name}`;
173
+ let railPused = railP;
174
+ let railAused = railA;
175
+ let railBused = railB;
176
+ if (Math.abs(this.inflate) > 1e-12) {
177
+ const inflated = inflateChamferRails({
178
+ railP,
179
+ railA,
180
+ railB,
181
+ normalsA,
182
+ normalsB,
183
+ tangents,
184
+ inflate: this.inflate,
185
+ });
186
+ if (inflated) {
187
+ railPused = inflated.railP;
188
+ railAused = inflated.railA;
189
+ railBused = inflated.railB;
190
+ }
191
+ }
192
+ resolveChamferSelfIntersections([railPused, railAused, railBused], closeLoop);
193
+
194
+ // Build a closed triangular prism and tag faces: _SIDE_A, _SIDE_B, _BEVEL, _CAP0, _CAP1
195
+ buildChamferPrismNamed(this, baseName, railPused, railAused, railBused, closeLoop);
196
+ // use pushFace to push end caps out by a tiny amount to avoid z-fighting with original faces
197
+ const tinyPush = 0.0001;
198
+ this.pushFace(`${baseName}_CAP0`, tinyPush);
199
+ this.pushFace(`${baseName}_CAP1`, tinyPush);
200
+
201
+ }
202
+ }
203
+
204
+ // ---------- Helpers (mostly adapted from fillet.js minimal subset) ----------
205
+
206
+ function arrToV(a) { return new THREE.Vector3(a[0], a[1], a[2]); }
207
+ function vToArr(v) { return [v.x, v.y, v.z]; }
208
+
209
+ function resamplePolyline3(src, n, close) {
210
+ if (!Array.isArray(src) || src.length < 2) return src;
211
+ const list = src.map(arrToV);
212
+ if (close) list.push(list[0].clone());
213
+ const totalLen = polylineLength(list);
214
+ const out = [];
215
+ for (let i = 0; i < n; i++) {
216
+ const t = i / (n - 1);
217
+ const d = t * totalLen;
218
+ const p = pointAtArcLength(list, d);
219
+ out.push([p.x, p.y, p.z]);
220
+ }
221
+ return out;
222
+ }
223
+
224
+ function signNonZero(x) { return (x >= 0) ? +1 : -1; }
225
+
226
+ function polylineLength(pts) {
227
+ let L = 0;
228
+ for (let i = 1; i < pts.length; i++) L += pts[i].distanceTo(pts[i - 1]);
229
+ return L;
230
+ }
231
+
232
+ function resolveChamferSelfIntersections(railGroup, isClosed) {
233
+ if (isClosed || !Array.isArray(railGroup) || railGroup.length === 0) return;
234
+ const baseLen = railGroup[0]?.length || 0;
235
+ if (baseLen < 4) return;
236
+ for (const rail of railGroup) {
237
+ if (!Array.isArray(rail) || rail.length !== baseLen) return;
238
+ }
239
+ const maxIterations = Math.min(4096, baseLen * baseLen * railGroup.length);
240
+ for (let iter = 0; iter < maxIterations; iter++) {
241
+ let best = null;
242
+ for (let r = 0; r < railGroup.length; r++) {
243
+ const hit = nextRailSelfIntersection(railGroup[r]);
244
+ if (!hit) continue;
245
+ if (!best || hit.i < best.i || (hit.i === best.i && hit.j < best.j)) {
246
+ best = { ...hit };
247
+ }
248
+ }
249
+ if (!best) break;
250
+ collapseRailsAtIntersection(railGroup, best);
251
+ }
252
+ }
253
+
254
+ function nextRailSelfIntersection(points) {
255
+ const projection = projectPolylineToPlane(points);
256
+ if (!projection) return null;
257
+ const coords = projection.planar;
258
+ const n = coords.length;
259
+ if (n < 4) return null;
260
+ for (let i = 0; i < n - 3; i++) {
261
+ const a0 = coords[i];
262
+ const a1 = coords[i + 1];
263
+ for (let j = i + 2; j < n - 1; j++) {
264
+ if (j === i + 1) continue;
265
+ const b0 = coords[j];
266
+ const b1 = coords[j + 1];
267
+ const hit = segmentIntersection2D(a0, a1, b0, b1);
268
+ if (hit) return { i, j, t: clamp01(hit.t), u: clamp01(hit.u) };
269
+ }
270
+ }
271
+ return null;
272
+ }
273
+
274
+ function collapseRailsAtIntersection(railGroup, { i, j, t, u }) {
275
+ if (!(j > i + 1)) return;
276
+ const removeCount = j - i;
277
+ for (const arr of railGroup) {
278
+ if (!Array.isArray(arr) || arr.length <= j) return;
279
+ }
280
+ for (const arr of railGroup) {
281
+ const merged = averagePointOnSegments(arr, i, t, j, u);
282
+ arr.splice(i + 1, removeCount, merged);
283
+ }
284
+ }
285
+
286
+ function averagePointOnSegments(arr, i, t, j, u) {
287
+ const a0 = arr[i];
288
+ const a1 = arr[i + 1];
289
+ const b0 = arr[j];
290
+ const b1 = arr[j + 1];
291
+ if (!a0 || !a1 || !b0 || !b1) return a0 ? a0.clone() : new THREE.Vector3();
292
+ const pA = a0.clone().lerp(a1, t);
293
+ const pB = b0.clone().lerp(b1, u);
294
+ return pA.add(pB).multiplyScalar(0.5);
295
+ }
296
+
297
+ function segmentIntersection2D(a1, a2, b1, b2, tol = 1e-12) {
298
+ const r = { x: a2.x - a1.x, y: a2.y - a1.y };
299
+ const s = { x: b2.x - b1.x, y: b2.y - b1.y };
300
+ const denom = r.x * s.y - r.y * s.x;
301
+ if (Math.abs(denom) < tol) return null;
302
+ const dx = b1.x - a1.x;
303
+ const dy = b1.y - a1.y;
304
+ const t = (dx * s.y - dy * s.x) / denom;
305
+ const u = (dx * r.y - dy * r.x) / denom;
306
+ if (t >= -tol && t <= 1 + tol && u >= -tol && u <= 1 + tol) {
307
+ return { t, u };
308
+ }
309
+ return null;
310
+ }
311
+
312
+ function clamp01(v) {
313
+ return Math.max(0, Math.min(1, v));
314
+ }
315
+
316
+ function projectPolylineToPlane(points) {
317
+ if (!Array.isArray(points) || points.length < 2) return null;
318
+ const origin = points[0].clone();
319
+ let axisU = null;
320
+ for (let i = 1; i < points.length; i++) {
321
+ const v = points[i].clone().sub(origin);
322
+ if (v.lengthSq() > 1e-12) { axisU = v.normalize(); break; }
323
+ }
324
+ if (!axisU) return null;
325
+ const normal = new THREE.Vector3();
326
+ const tmp1 = new THREE.Vector3();
327
+ const tmp2 = new THREE.Vector3();
328
+ for (let i = 0; i < points.length - 2; i++) {
329
+ tmp1.subVectors(points[i + 1], points[i]);
330
+ tmp2.subVectors(points[i + 2], points[i + 1]);
331
+ const cross = new THREE.Vector3().crossVectors(tmp1, tmp2);
332
+ if (cross.lengthSq() > 1e-16) normal.add(cross);
333
+ }
334
+ if (normal.lengthSq() < 1e-16) {
335
+ const fallback = Math.abs(axisU.x) < 0.9 ? new THREE.Vector3(1, 0, 0) : new THREE.Vector3(0, 1, 0);
336
+ normal.crossVectors(axisU, fallback);
337
+ if (normal.lengthSq() < 1e-16) normal.set(0, 0, 1);
338
+ }
339
+ normal.normalize();
340
+ const axisV = new THREE.Vector3().crossVectors(normal, axisU);
341
+ if (axisV.lengthSq() < 1e-16) return null;
342
+ axisV.normalize();
343
+ const planar = points.map((p) => {
344
+ const rel = p.clone().sub(origin);
345
+ return {
346
+ x: rel.dot(axisU),
347
+ y: rel.dot(axisV),
348
+ };
349
+ });
350
+ return { origin, axisU, axisV, planar };
351
+ }
352
+
353
+ function reorderChamferRailSamples({ railP, railA, railB, normalsA, normalsB, tangents, isClosed }) {
354
+ const order = computeChamferRailOrder(railP, isClosed);
355
+ if (!order) return;
356
+ const apply = (arr) => {
357
+ if (!Array.isArray(arr) || arr.length !== order.length) return;
358
+ const re = new Array(order.length);
359
+ for (let i = 0; i < order.length; i++) re[i] = arr[order[i]];
360
+ arr.length = 0;
361
+ for (const item of re) arr.push(item);
362
+ };
363
+ apply(railP);
364
+ apply(railA);
365
+ apply(railB);
366
+ apply(normalsA);
367
+ apply(normalsB);
368
+ apply(tangents);
369
+ }
370
+
371
+ function computeChamferRailOrder(points, isClosed) {
372
+ if (isClosed || !Array.isArray(points) || points.length < 3) return null;
373
+ const first = points[0];
374
+ const last = points[points.length - 1];
375
+ if (!first || !last) return null;
376
+ if (first.distanceTo(last) < 1e-9) return null;
377
+
378
+ const n = points.length;
379
+ const used = new Array(n).fill(false);
380
+ const order = [];
381
+ const pushIdx = (idx) => {
382
+ order.push(idx);
383
+ used[idx] = true;
384
+ };
385
+ pushIdx(0);
386
+ used[n - 1] = true; // keep final endpoint reserved
387
+ while (order.length < n - 1) {
388
+ const curr = points[order[order.length - 1]];
389
+ if (!curr) break;
390
+ let best = -1;
391
+ let bestDist = Infinity;
392
+ for (let i = 1; i < n - 1; i++) {
393
+ if (used[i]) continue;
394
+ const candidate = points[i];
395
+ if (!candidate) continue;
396
+ const dist = curr.distanceTo(candidate);
397
+ if (dist < bestDist) {
398
+ bestDist = dist;
399
+ best = i;
400
+ }
401
+ }
402
+ if (best === -1) break;
403
+ pushIdx(best);
404
+ }
405
+ order.push(n - 1);
406
+ if (order.length !== n) return null;
407
+
408
+ let changed = false;
409
+ for (let i = 0; i < n; i++) {
410
+ if (order[i] !== i) { changed = true; break; }
411
+ }
412
+ if (!changed) return null;
413
+
414
+ const originalLength = polylineLength(points);
415
+ const reorderedLength = polylineLengthFromOrder(points, order);
416
+ const tolerance = Math.max(1e-6, originalLength * 1e-4);
417
+ if (!(reorderedLength + tolerance < originalLength)) return null;
418
+ return order;
419
+ }
420
+
421
+ function polylineLengthFromOrder(points, order) {
422
+ if (!Array.isArray(points) || !Array.isArray(order) || order.length < 2) return 0;
423
+ let L = 0;
424
+ for (let i = 1; i < order.length; i++) {
425
+ const a = points[order[i - 1]];
426
+ const b = points[order[i]];
427
+ if (!a || !b) continue;
428
+ L += a.distanceTo(b);
429
+ }
430
+ return L;
431
+ }
432
+
433
+ function pointAtArcLength(pts, dist) {
434
+ let acc = 0;
435
+ for (let i = 1; i < pts.length; i++) {
436
+ const seg = pts[i].distanceTo(pts[i - 1]);
437
+ if (acc + seg >= dist) {
438
+ const t = (dist - acc) / seg;
439
+ return new THREE.Vector3().lerpVectors(pts[i - 1], pts[i], t);
440
+ }
441
+ acc += seg;
442
+ }
443
+ return pts[pts.length - 1].clone();
444
+ }
445
+
446
+ function inflateChamferRails({ railP, railA, railB, normalsA, normalsB, tangents, inflate }) {
447
+ if (!Number.isFinite(inflate) || inflate === 0) return null;
448
+ const count = Math.min(
449
+ railP.length,
450
+ railA.length,
451
+ railB.length,
452
+ normalsA.length,
453
+ normalsB.length,
454
+ tangents.length
455
+ );
456
+ if (count < 2) return null;
457
+ const outP = new Array(count);
458
+ const outA = new Array(count);
459
+ const outB = new Array(count);
460
+ const ab = new THREE.Vector3();
461
+ const bevelNormal = new THREE.Vector3();
462
+ const tangent = new THREE.Vector3();
463
+ for (let i = 0; i < count; i++) {
464
+ const P = railP[i];
465
+ const A = railA[i];
466
+ const B = railB[i];
467
+ const nA = normalsA[i];
468
+ const nB = normalsB[i];
469
+ outP[i] = shiftEdgePoint(P, nA, nB, inflate);
470
+
471
+ if (!A || !B) {
472
+ outA[i] = A ? A.clone() : new THREE.Vector3();
473
+ outB[i] = B ? B.clone() : new THREE.Vector3();
474
+ continue;
475
+ }
476
+
477
+ const t = tangents[i];
478
+ if (!t || t.lengthSq() < 1e-14) {
479
+ outA[i] = A.clone();
480
+ outB[i] = B.clone();
481
+ continue;
482
+ }
483
+ tangent.copy(t).normalize();
484
+ ab.copy(B).sub(A);
485
+ if (ab.lengthSq() < 1e-18) {
486
+ outA[i] = A.clone();
487
+ outB[i] = B.clone();
488
+ continue;
489
+ }
490
+ bevelNormal.crossVectors(ab, tangent);
491
+ const len = bevelNormal.length();
492
+ if (len < 1e-18) {
493
+ outA[i] = A.clone();
494
+ outB[i] = B.clone();
495
+ continue;
496
+ }
497
+ bevelNormal.multiplyScalar(1 / len);
498
+ outA[i] = translatePointWithinPlane(A, nA, bevelNormal, inflate);
499
+ outB[i] = translatePointWithinPlane(B, nB, bevelNormal, inflate);
500
+ }
501
+ return { railP: outP, railA: outA, railB: outB };
502
+ }
503
+
504
+ function shiftEdgePoint(point, normalA, normalB, inflate) {
505
+ if (!point) return new THREE.Vector3();
506
+ if (!normalA || !normalB) return point.clone();
507
+ const nA = normalA.clone().normalize();
508
+ const nB = normalB.clone().normalize();
509
+ const sum = nA.clone().add(nB);
510
+ const denom = 1 + nA.dot(nB);
511
+ if (Math.abs(denom) < 1e-9 || sum.lengthSq() < 1e-18) return point.clone();
512
+ return point.clone().addScaledVector(sum, inflate / denom);
513
+ }
514
+
515
+ function translatePointWithinPlane(point, faceNormal, planeNormal, inflate) {
516
+ if (!point) return new THREE.Vector3();
517
+ if (!faceNormal || !planeNormal) return point.clone();
518
+ const n = faceNormal.clone().normalize();
519
+ const plane = planeNormal.clone().normalize();
520
+ const dir = n.sub(plane.clone().multiplyScalar(plane.dot(n)));
521
+ const lenSq = dir.lengthSq();
522
+ if (lenSq < 1e-18) return point.clone();
523
+ return point.clone().addScaledVector(dir, inflate / lenSq);
524
+ }
525
+
526
+ // Triangular prism with named faces for selective inflation: SIDE_A, SIDE_B, BEVEL, CAPs
527
+ function buildChamferPrismNamed(solid, baseName, railP, railA, railB, closeLoop) {
528
+ const n = Math.min(railP.length, railA.length, railB.length);
529
+ if (n < 2) return;
530
+ const namePA = `${baseName}_SIDE_A`;
531
+ const namePB = `${baseName}_SIDE_B`;
532
+ const nameAB = `${baseName}_BEVEL`;
533
+ const link = (nm, a0, a1, b0, b1) => {
534
+ solid.addTriangle(nm, vToArr(a0), vToArr(b0), vToArr(b1));
535
+ solid.addTriangle(nm, vToArr(a0), vToArr(b1), vToArr(a1));
536
+ };
537
+ for (let i = 0; i < n - 1; i++) {
538
+ link(namePA, railP[i], railP[i+1], railA[i], railA[i+1]); // P-A side
539
+ link(namePB, railP[i], railP[i+1], railB[i], railB[i+1]); // P-B side
540
+ link(nameAB, railA[i], railA[i+1], railB[i], railB[i+1]); // bevel
541
+ }
542
+ if (closeLoop) {
543
+ const i = n - 1, j = 0;
544
+ link(namePA, railP[i], railP[j], railA[i], railA[j]);
545
+ link(namePB, railP[i], railP[j], railB[i], railB[j]);
546
+ link(nameAB, railA[i], railA[j], railB[i], railB[j]);
547
+ } else {
548
+ solid.addTriangle(`${baseName}_CAP0`, vToArr(railP[0]), vToArr(railA[0]), vToArr(railB[0]));
549
+ solid.addTriangle(`${baseName}_CAP1`, vToArr(railP[n-1]), vToArr(railB[n-1]), vToArr(railA[n-1]));
550
+ }
551
+ }
@@ -0,0 +1,85 @@
1
+ import * as THREE from 'three';
2
+
3
+ export function dedupeConsecutivePoints(points, eps = 0) {
4
+ if (!Array.isArray(points) || points.length === 0) return [];
5
+ const out = [points[0]];
6
+ const useEps = Number.isFinite(eps) && eps > 0;
7
+ const epsSq = useEps ? eps * eps : 0;
8
+ for (let i = 1; i < points.length; i++) {
9
+ const curr = points[i];
10
+ const prev = out[out.length - 1];
11
+ if (!Array.isArray(curr) || !Array.isArray(prev)) {
12
+ out.push(curr);
13
+ continue;
14
+ }
15
+ if (useEps) {
16
+ const dx = (curr[0] || 0) - (prev[0] || 0);
17
+ const dy = (curr[1] || 0) - (prev[1] || 0);
18
+ const dz = (curr[2] || 0) - (prev[2] || 0);
19
+ if ((dx * dx + dy * dy + dz * dz) <= epsSq) continue;
20
+ } else if (curr[0] === prev[0] && curr[1] === prev[1] && curr[2] === prev[2]) {
21
+ continue;
22
+ }
23
+ out.push(curr);
24
+ }
25
+ return out;
26
+ }
27
+
28
+ export function getEdgePolylineWorld(edgeObj, { dedupe = true, eps = 0 } = {}) {
29
+ const pts = [];
30
+ if (!edgeObj) return pts;
31
+ const cached = edgeObj?.userData?.polylineLocal;
32
+ const isWorld = !!(edgeObj?.userData?.polylineWorld);
33
+ const v = new THREE.Vector3();
34
+ if (Array.isArray(cached) && cached.length >= 2) {
35
+ if (isWorld) {
36
+ for (const p of cached) pts.push([p[0], p[1], p[2]]);
37
+ } else {
38
+ for (const p of cached) {
39
+ v.set(p[0], p[1], p[2]).applyMatrix4(edgeObj.matrixWorld);
40
+ pts.push([v.x, v.y, v.z]);
41
+ }
42
+ }
43
+ } else {
44
+ const posAttr = edgeObj?.geometry?.getAttribute?.('position');
45
+ if (posAttr && posAttr.itemSize === 3 && posAttr.count >= 2) {
46
+ for (let i = 0; i < posAttr.count; i++) {
47
+ v.set(posAttr.getX(i), posAttr.getY(i), posAttr.getZ(i)).applyMatrix4(edgeObj.matrixWorld);
48
+ pts.push([v.x, v.y, v.z]);
49
+ }
50
+ } else {
51
+ const aStart = edgeObj?.geometry?.attributes?.instanceStart;
52
+ const aEnd = edgeObj?.geometry?.attributes?.instanceEnd;
53
+ if (aStart && aEnd && aStart.itemSize === 3 && aEnd.itemSize === 3 && aStart.count === aEnd.count && aStart.count >= 1) {
54
+ v.set(aStart.getX(0), aStart.getY(0), aStart.getZ(0)).applyMatrix4(edgeObj.matrixWorld);
55
+ pts.push([v.x, v.y, v.z]);
56
+ for (let i = 0; i < aEnd.count; i++) {
57
+ v.set(aEnd.getX(i), aEnd.getY(i), aEnd.getZ(i)).applyMatrix4(edgeObj.matrixWorld);
58
+ pts.push([v.x, v.y, v.z]);
59
+ }
60
+ }
61
+ }
62
+ }
63
+ return dedupe ? dedupeConsecutivePoints(pts, eps) : pts;
64
+ }
65
+
66
+ export function getEdgeLineEndpointsWorld(edgeObj, eps = 1e-12) {
67
+ const pts = getEdgePolylineWorld(edgeObj, { dedupe: false });
68
+ if (!Array.isArray(pts) || pts.length < 2) return null;
69
+ const first = pts[0];
70
+ if (!Array.isArray(first)) return null;
71
+ let second = null;
72
+ for (let i = 1; i < pts.length; i++) {
73
+ const p = pts[i];
74
+ if (!Array.isArray(p)) continue;
75
+ if (Math.abs(p[0] - first[0]) > eps || Math.abs(p[1] - first[1]) > eps || Math.abs(p[2] - first[2]) > eps) {
76
+ second = p;
77
+ break;
78
+ }
79
+ }
80
+ if (!second) return null;
81
+ return {
82
+ start: new THREE.Vector3(first[0], first[1], first[2]),
83
+ end: new THREE.Vector3(second[0], second[1], second[2]),
84
+ };
85
+ }