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,961 @@
1
+ import * as THREE from 'three';
2
+ import { BaseAssemblyConstraint } from '../BaseAssemblyConstraint.js';
3
+ import { solveParallelAlignment, resolveParallelSelection } from '../constraintUtils/parallelAlignment.js';
4
+ import { objectRepresentativePoint } from '../../UI/pmi/annUtils.js';
5
+
6
+ const DEFAULT_TOUCH_TOLERANCE = 1e-6;
7
+ const MAX_POINT_ROTATION_STEP = THREE.MathUtils.degToRad(5);
8
+
9
+ const inputParamsSchema = {
10
+ id: {
11
+ type: 'string',
12
+ default_value: null,
13
+ hint: 'Unique identifier for the constraint.',
14
+ },
15
+ elements: {
16
+ type: 'reference_selection',
17
+ label: 'Elements',
18
+ hint: 'Select two faces, edges, or vertices.',
19
+ selectionFilter: ['FACE', 'EDGE', 'VERTEX'],
20
+ multiple: true,
21
+ minSelections: 2,
22
+ maxSelections: 2,
23
+ },
24
+ reverse: {
25
+ type: 'boolean',
26
+ label: 'Reverse',
27
+ default_value: false,
28
+ hint: 'Flip the stored orientation preference.',
29
+ },
30
+ };
31
+
32
+ export class TouchAlignConstraint extends BaseAssemblyConstraint {
33
+ static shortName = '⪥';
34
+ static longName = '⪥ Touch Align Constraint';
35
+ static constraintType = 'touch_align';
36
+ static aliases = ['touch', 'touch_align', 'touch-align', 'TALN'];
37
+ static inputParamsSchema = inputParamsSchema;
38
+
39
+ constructor(partHistory) {
40
+ super(partHistory);
41
+ this._debugHelpers = [];
42
+ }
43
+
44
+ clearDebugArrows(context = {}) {
45
+ const scene = context.scene || null;
46
+ if (!scene) return;
47
+ this.#clearNormalDebug(scene);
48
+ }
49
+
50
+ async run(context = {}) {
51
+ const pd = this.persistentData = this.persistentData || {};
52
+ const [selA, selB] = selectionPair(this.inputParams);
53
+
54
+ if ((context.iteration ?? 0) === 0) {
55
+ this.#clearNormalDebug(context.scene || null);
56
+ }
57
+
58
+ if (!selA || !selB) {
59
+ pd.status = 'incomplete';
60
+ pd.message = 'Select two references to define the constraint.';
61
+ pd.satisfied = false;
62
+ pd.lastAppliedMoves = [];
63
+ pd.lastAppliedRotations = [];
64
+ return { ok: false, status: 'incomplete', satisfied: false, applied: false, message: pd.message };
65
+ }
66
+
67
+ const objectA = context.resolveObject?.(selA) || null;
68
+ const objectB = context.resolveObject?.(selB) || null;
69
+ const kindA = normalizeSelectionKind(selectionKindFrom(objectA, selA));
70
+ const kindB = normalizeSelectionKind(selectionKindFrom(objectB, selB));
71
+
72
+ if (kindA === 'FACE' && kindB === 'FACE') {
73
+ return this.faceToFace(context, selA, selB);
74
+ }
75
+
76
+ if (kindA === 'EDGE' && kindB === 'EDGE') {
77
+ return this.edgeToEdge(context, selA, selB);
78
+ }
79
+
80
+ if (kindA === 'POINT' && kindB === 'POINT') {
81
+ return this.pointToPoint(context, selA, selB);
82
+ }
83
+
84
+ const message = 'Touch Align requires two selections of the same type. Face-to-face alignment is currently supported.';
85
+ pd.status = 'unsupported-selection';
86
+ pd.message = message;
87
+ pd.satisfied = false;
88
+ pd.error = null;
89
+ pd.errorDeg = null;
90
+ pd.lastAppliedMoves = [];
91
+ pd.lastAppliedRotations = [];
92
+ if (pd.exception) delete pd.exception;
93
+ return { ok: false, status: 'unsupported-selection', satisfied: false, applied: false, message };
94
+ }
95
+
96
+ async solve(context = {}) {
97
+ return this.run(context);
98
+ }
99
+
100
+ #effectiveOppose(context, selectionA, selectionB) {
101
+ const base = this.#preferredOppose(context, selectionA, selectionB);
102
+ const reverseToggle = !!this.inputParams.reverse;
103
+ return reverseToggle ? !base : base;
104
+ }
105
+
106
+ #preferredOppose(context, selectionA, selectionB) {
107
+ const pd = this.persistentData = this.persistentData || {};
108
+ if (typeof pd.preferredOppose !== 'boolean') {
109
+ const infoA = resolveParallelSelection(this, context, selectionA, 'elements[0]');
110
+ const infoB = resolveParallelSelection(this, context, selectionB, 'elements[1]');
111
+ const dirA = infoA?.direction?.clone()?.normalize();
112
+ const dirB = infoB?.direction?.clone()?.normalize();
113
+ if (!dirA || !dirB || dirA.lengthSq() === 0 || dirB.lengthSq() === 0) {
114
+ throw new Error('TouchAlignConstraint: Unable to resolve directions for orientation preference.');
115
+ }
116
+ const dot = THREE.MathUtils.clamp(dirA.dot(dirB), -1, 1);
117
+ pd.preferredOppose = dot < 0;
118
+ pd.lastOrientationDot = dot;
119
+ }
120
+ pd.isNewConstraint = false;
121
+ return !!pd.preferredOppose;
122
+ }
123
+
124
+ async faceToFace(context, selA, selB) {
125
+ const pd = this.persistentData = this.persistentData || {};
126
+
127
+ const opposeNormals = this.#effectiveOppose(context, selA, selB);
128
+
129
+ const parallelResult = solveParallelAlignment({
130
+ constraint: this,
131
+ context,
132
+ selectionA: selA,
133
+ selectionB: selB,
134
+ opposeNormals,
135
+ selectionLabelA: 'elements[0]',
136
+ selectionLabelB: 'elements[1]',
137
+ });
138
+
139
+ const infoA = parallelResult.infoA || null;
140
+ const infoB = parallelResult.infoB || null;
141
+
142
+ if (context.debugMode && infoA && infoB) {
143
+ this.#updateNormalDebug(context, infoA, infoB);
144
+ }
145
+
146
+ pd.lastAppliedRotations = Array.isArray(parallelResult.rotations) ? parallelResult.rotations : [];
147
+
148
+ if (!parallelResult.ok) {
149
+ pd.status = parallelResult.status;
150
+ pd.message = parallelResult.message || '';
151
+ pd.satisfied = false;
152
+ pd.error = parallelResult.error ?? null;
153
+ pd.errorDeg = parallelResult.angleDeg ?? null;
154
+ pd.exception = parallelResult.exception || null;
155
+ return parallelResult;
156
+ }
157
+
158
+ if (!parallelResult.satisfied) {
159
+ pd.status = parallelResult.status;
160
+ pd.message = parallelResult.message || 'Aligning surface orientations…';
161
+ pd.satisfied = false;
162
+ pd.error = parallelResult.angle ?? null;
163
+ pd.errorDeg = parallelResult.angleDeg ?? null;
164
+ pd.lastAppliedMoves = [];
165
+ if (pd.exception) delete pd.exception;
166
+ return {
167
+ ...parallelResult,
168
+ stage: 'orientation',
169
+ };
170
+ }
171
+
172
+ if (!infoA || !infoB || !infoA.origin || !infoB.origin || !infoA.direction) {
173
+ const message = 'Unable to resolve contact data after alignment.';
174
+ pd.status = 'invalid-selection';
175
+ pd.message = message;
176
+ pd.satisfied = false;
177
+ return { ok: false, status: 'invalid-selection', satisfied: false, applied: false, message };
178
+ }
179
+
180
+ const fixedA = context.isComponentFixed?.(infoA.component);
181
+ const fixedB = context.isComponentFixed?.(infoB.component);
182
+ const translationGain = context.translationGain ?? 1;
183
+ const tolerance = Math.max(Math.abs(context.tolerance ?? DEFAULT_TOUCH_TOLERANCE), 1e-8);
184
+
185
+ const dirA = infoA.direction.clone().normalize();
186
+ const delta = new THREE.Vector3().subVectors(infoB.origin, infoA.origin);
187
+ const moves = [];
188
+ let applied = false;
189
+
190
+ const applyMove = (component, vec) => {
191
+ if (!component || !vec || vec.lengthSq() === 0) return false;
192
+ const ok = context.applyTranslation?.(component, vec);
193
+ if (ok) {
194
+ moves.push({ component: component.name || component.uuid, move: vectorToArray(vec) });
195
+ }
196
+ return ok;
197
+ };
198
+
199
+ const separation = delta.dot(dirA);
200
+ const distance = Math.abs(separation);
201
+
202
+ if (distance <= tolerance) {
203
+ const message = 'Faces are touching within tolerance.';
204
+ pd.status = 'satisfied';
205
+ pd.message = message;
206
+ pd.satisfied = true;
207
+ pd.error = distance;
208
+ pd.errorDeg = 0;
209
+ pd.lastAppliedMoves = [];
210
+ return {
211
+ ok: true,
212
+ status: 'satisfied',
213
+ satisfied: true,
214
+ applied: false,
215
+ error: distance,
216
+ message,
217
+ infoA,
218
+ infoB,
219
+ diagnostics: { separation, moves: [] },
220
+ };
221
+ }
222
+
223
+ if (fixedA && fixedB) {
224
+ const message = 'Both components are fixed; unable to translate to touch.';
225
+ pd.status = 'blocked';
226
+ pd.message = message;
227
+ pd.satisfied = false;
228
+ pd.error = distance;
229
+ pd.errorDeg = 0;
230
+ return {
231
+ ok: false,
232
+ status: 'blocked',
233
+ satisfied: false,
234
+ applied: false,
235
+ error: distance,
236
+ message,
237
+ infoA,
238
+ infoB,
239
+ diagnostics: { separation, moves: [] },
240
+ };
241
+ }
242
+
243
+ const correction = -separation * Math.max(0, Math.min(1, translationGain));
244
+ const halfCorrection = correction * 0.5;
245
+
246
+ if (!fixedA && !fixedB) {
247
+ const moveA = dirA.clone().multiplyScalar(-halfCorrection);
248
+ const moveB = dirA.clone().multiplyScalar(halfCorrection);
249
+ applied = applyMove(infoA.component, moveA) || applied;
250
+ applied = applyMove(infoB.component, moveB) || applied;
251
+ } else if (fixedA && !fixedB) {
252
+ const moveB = dirA.clone().multiplyScalar(correction);
253
+ applied = applyMove(infoB.component, moveB) || applied;
254
+ } else if (!fixedA && fixedB) {
255
+ const moveA = dirA.clone().multiplyScalar(-correction);
256
+ applied = applyMove(infoA.component, moveA) || applied;
257
+ }
258
+
259
+ const status = applied ? 'adjusted' : 'pending';
260
+ const message = applied
261
+ ? 'Applied translation to bring faces into contact.'
262
+ : 'Waiting for a movable component to translate.';
263
+
264
+ pd.status = status;
265
+ pd.message = message;
266
+ pd.satisfied = false;
267
+ pd.error = distance;
268
+ pd.errorDeg = 0;
269
+ pd.lastAppliedMoves = moves;
270
+ if (pd.exception) delete pd.exception;
271
+
272
+ return {
273
+ ok: true,
274
+ status,
275
+ satisfied: false,
276
+ applied,
277
+ error: distance,
278
+ message,
279
+ infoA,
280
+ infoB,
281
+ diagnostics: { separation, moves },
282
+ };
283
+ }
284
+
285
+ async edgeToEdge(_context, _selA, _selB) {
286
+ const context = _context || {};
287
+ const pd = this.persistentData = this.persistentData || {};
288
+
289
+ const opposeNormals = this.#effectiveOppose(context, _selA, _selB);
290
+
291
+ const parallelResult = solveParallelAlignment({
292
+ constraint: this,
293
+ context,
294
+ selectionA: _selA,
295
+ selectionB: _selB,
296
+ opposeNormals,
297
+ selectionLabelA: 'elements[0]',
298
+ selectionLabelB: 'elements[1]',
299
+ });
300
+
301
+ const infoA = parallelResult.infoA || null;
302
+ const infoB = parallelResult.infoB || null;
303
+
304
+ if (context.debugMode && infoA && infoB) {
305
+ this.#updateNormalDebug(context, infoA, infoB);
306
+ }
307
+
308
+ pd.lastAppliedRotations = Array.isArray(parallelResult.rotations) ? parallelResult.rotations : [];
309
+
310
+ if (!parallelResult.ok) {
311
+ pd.status = parallelResult.status;
312
+ pd.message = parallelResult.message || '';
313
+ pd.satisfied = false;
314
+ pd.error = parallelResult.error ?? null;
315
+ pd.errorDeg = parallelResult.angleDeg ?? null;
316
+ pd.lastAppliedMoves = [];
317
+ pd.exception = parallelResult.exception || null;
318
+ return parallelResult;
319
+ }
320
+
321
+ if (!parallelResult.satisfied) {
322
+ pd.status = parallelResult.status;
323
+ pd.message = parallelResult.message || 'Aligning edge directions…';
324
+ pd.satisfied = false;
325
+ pd.error = parallelResult.angle ?? null;
326
+ pd.errorDeg = parallelResult.angleDeg ?? null;
327
+ pd.lastAppliedMoves = [];
328
+ if (pd.exception) delete pd.exception;
329
+ return {
330
+ ...parallelResult,
331
+ stage: 'orientation',
332
+ };
333
+ }
334
+
335
+ if (!infoA || !infoB || !infoA.origin || !infoB.origin || !infoA.direction) {
336
+ const message = 'Unable to resolve edge data after alignment.';
337
+ pd.status = 'invalid-selection';
338
+ pd.message = message;
339
+ pd.satisfied = false;
340
+ pd.lastAppliedMoves = [];
341
+ pd.error = null;
342
+ pd.errorDeg = null;
343
+ return { ok: false, status: 'invalid-selection', satisfied: false, applied: false, message };
344
+ }
345
+
346
+ const fixedA = context.isComponentFixed?.(infoA.component);
347
+ const fixedB = context.isComponentFixed?.(infoB.component);
348
+ const translationGain = Math.max(0, Math.min(1, context.translationGain ?? 1));
349
+ const tolerance = Math.max(Math.abs(context.tolerance ?? DEFAULT_TOUCH_TOLERANCE), 1e-8);
350
+
351
+ const dir = infoA.direction.clone().normalize();
352
+ const delta = new THREE.Vector3().subVectors(infoB.origin, infoA.origin);
353
+ const parallelComponent = dir.clone().multiplyScalar(delta.dot(dir));
354
+ const separationVec = delta.clone().sub(parallelComponent);
355
+ const distance = separationVec.length();
356
+
357
+ pd.error = distance;
358
+ pd.errorDeg = null;
359
+
360
+ if (distance <= tolerance) {
361
+ const message = 'Edges are colinear within tolerance.';
362
+ pd.status = 'satisfied';
363
+ pd.message = message;
364
+ pd.satisfied = true;
365
+ pd.lastAppliedMoves = [];
366
+ if (pd.exception) delete pd.exception;
367
+ return {
368
+ ok: true,
369
+ status: 'satisfied',
370
+ satisfied: true,
371
+ applied: false,
372
+ error: distance,
373
+ message,
374
+ infoA,
375
+ infoB,
376
+ diagnostics: {
377
+ separationVector: separationVec.toArray(),
378
+ parallelComponent: parallelComponent.toArray(),
379
+ },
380
+ };
381
+ }
382
+
383
+ if (fixedA && fixedB) {
384
+ const message = 'Both components are fixed; unable to translate to make edges colinear.';
385
+ pd.status = 'blocked';
386
+ pd.message = message;
387
+ pd.satisfied = false;
388
+ pd.lastAppliedMoves = [];
389
+ if (pd.exception) delete pd.exception;
390
+ return {
391
+ ok: false,
392
+ status: 'blocked',
393
+ satisfied: false,
394
+ applied: false,
395
+ error: distance,
396
+ message,
397
+ infoA,
398
+ infoB,
399
+ diagnostics: {
400
+ separationVector: separationVec.toArray(),
401
+ parallelComponent: parallelComponent.toArray(),
402
+ },
403
+ };
404
+ }
405
+
406
+ const correctionVec = separationVec.clone().multiplyScalar(translationGain);
407
+ const moves = [];
408
+ let applied = false;
409
+
410
+ const applyMove = (component, vec) => {
411
+ if (!component || !vec || vec.lengthSq() === 0) return false;
412
+ const ok = context.applyTranslation?.(component, vec);
413
+ if (ok) {
414
+ moves.push({ component: component.name || component.uuid, move: vectorToArray(vec) });
415
+ }
416
+ return ok;
417
+ };
418
+
419
+ if (!fixedA && !fixedB) {
420
+ const moveA = correctionVec.clone().multiplyScalar(0.5);
421
+ const moveB = correctionVec.clone().multiplyScalar(-0.5);
422
+ applied = applyMove(infoA.component, moveA) || applied;
423
+ applied = applyMove(infoB.component, moveB) || applied;
424
+ } else if (fixedA && !fixedB) {
425
+ const moveB = correctionVec.clone().negate();
426
+ applied = applyMove(infoB.component, moveB) || applied;
427
+ } else if (!fixedA && fixedB) {
428
+ const moveA = correctionVec.clone();
429
+ applied = applyMove(infoA.component, moveA) || applied;
430
+ }
431
+
432
+ const status = applied ? 'adjusted' : 'pending';
433
+ const message = applied
434
+ ? 'Applied translation to bring edges onto the same line.'
435
+ : 'Waiting for a movable component to translate.';
436
+
437
+ pd.status = status;
438
+ pd.message = message;
439
+ pd.satisfied = false;
440
+ pd.lastAppliedMoves = moves;
441
+ if (pd.exception) delete pd.exception;
442
+
443
+ return {
444
+ ok: true,
445
+ status,
446
+ satisfied: false,
447
+ applied,
448
+ error: distance,
449
+ message,
450
+ infoA,
451
+ infoB,
452
+ diagnostics: {
453
+ separationVector: separationVec.toArray(),
454
+ parallelComponent: parallelComponent.toArray(),
455
+ moves,
456
+ },
457
+ stage: 'translation',
458
+ };
459
+ }
460
+
461
+ async pointToPoint(_context, _selA, _selB) {
462
+ const context = _context || {};
463
+ const pd = this.persistentData = this.persistentData || {};
464
+
465
+ let infoA = resolvePointSelection(this, context, _selA);
466
+ let infoB = resolvePointSelection(this, context, _selB);
467
+
468
+ pd.pointToPointNextMode = pd.pointToPointNextMode === 'rotate' ? 'rotate' : 'translate';
469
+
470
+ if (!infoA?.component || !infoB?.component) {
471
+ const message = 'Point selections must belong to assembly components.';
472
+ pd.status = 'invalid-selection';
473
+ pd.message = message;
474
+ pd.satisfied = false;
475
+ pd.error = null;
476
+ pd.errorDeg = null;
477
+ pd.lastAppliedMoves = [];
478
+ pd.lastAppliedRotations = [];
479
+ pd.pointToPointNextMode = 'translate';
480
+ if (pd.exception) delete pd.exception;
481
+ return { ok: false, status: 'invalid-selection', satisfied: false, applied: false, message };
482
+ }
483
+
484
+ if (infoA.component === infoB.component) {
485
+ const message = 'Select points from different components for touch alignment.';
486
+ pd.status = 'invalid-selection';
487
+ pd.message = message;
488
+ pd.satisfied = false;
489
+ pd.error = null;
490
+ pd.errorDeg = null;
491
+ pd.lastAppliedMoves = [];
492
+ pd.lastAppliedRotations = [];
493
+ pd.pointToPointNextMode = 'translate';
494
+ if (pd.exception) delete pd.exception;
495
+ return { ok: false, status: 'invalid-selection', satisfied: false, applied: false, message };
496
+ }
497
+
498
+ if (!infoA.point || !infoB.point) {
499
+ const message = 'Unable to resolve world-space positions for the selected points.';
500
+ pd.status = 'invalid-selection';
501
+ pd.message = message;
502
+ pd.satisfied = false;
503
+ pd.error = null;
504
+ pd.errorDeg = null;
505
+ pd.lastAppliedMoves = [];
506
+ pd.lastAppliedRotations = [];
507
+ pd.pointToPointNextMode = 'translate';
508
+ if (pd.exception) delete pd.exception;
509
+ return { ok: false, status: 'invalid-selection', satisfied: false, applied: false, message };
510
+ }
511
+
512
+ const tolerance = Math.max(Math.abs(context.tolerance ?? DEFAULT_TOUCH_TOLERANCE), 1e-8);
513
+ const translationGain = Math.max(0, Math.min(1, context.translationGain ?? 1));
514
+ const rotationGain = Math.max(0, Math.min(1, context.rotationGain ?? 1));
515
+
516
+ const fixedA = context.isComponentFixed?.(infoA.component);
517
+ const fixedB = context.isComponentFixed?.(infoB.component);
518
+
519
+ let delta = new THREE.Vector3().subVectors(infoA.point, infoB.point);
520
+ let distance = delta.length();
521
+
522
+ const refreshInfo = () => {
523
+ const nextA = resolvePointSelection(this, context, _selA);
524
+ const nextB = resolvePointSelection(this, context, _selB);
525
+ if (nextA?.point && nextB?.point) {
526
+ infoA = nextA;
527
+ infoB = nextB;
528
+ delta = new THREE.Vector3().subVectors(infoA.point, infoB.point);
529
+ distance = delta.length();
530
+ }
531
+ };
532
+
533
+ pd.error = distance;
534
+ pd.errorDeg = null;
535
+
536
+ if (distance <= tolerance) {
537
+ const message = 'Points are coincident within tolerance.';
538
+ pd.status = 'satisfied';
539
+ pd.message = message;
540
+ pd.satisfied = true;
541
+ pd.lastAppliedMoves = [];
542
+ pd.lastAppliedRotations = [];
543
+ pd.pointToPointNextMode = 'translate';
544
+ if (pd.exception) delete pd.exception;
545
+ return {
546
+ ok: true,
547
+ status: 'satisfied',
548
+ satisfied: true,
549
+ applied: false,
550
+ error: distance,
551
+ message,
552
+ infoA,
553
+ infoB,
554
+ diagnostics: {
555
+ distance,
556
+ delta: delta.toArray(),
557
+ moves: [],
558
+ rotations: [],
559
+ stage: 'satisfied',
560
+ },
561
+ };
562
+ }
563
+
564
+ const performRotation = () => {
565
+ const rotations = [];
566
+ const moves = [];
567
+ let appliedRotation = false;
568
+
569
+ const applyRotationTowards = (sourceInfo, targetPoint, share) => {
570
+ const component = sourceInfo?.component;
571
+ if (!component || share <= 0 || context.isComponentFixed?.(component)) return false;
572
+ const pivot = resolveComponentMidpoint(this, component);
573
+ if (!pivot) return false;
574
+
575
+ const fromVec = sourceInfo.point.clone().sub(pivot);
576
+ const toVec = targetPoint.clone().sub(pivot);
577
+
578
+ if (fromVec.lengthSq() <= 1e-12 || toVec.lengthSq() <= 1e-12) return false;
579
+
580
+ const nFrom = fromVec.clone().normalize();
581
+ const nTo = toVec.clone().normalize();
582
+ const dot = THREE.MathUtils.clamp(nFrom.dot(nTo), -1, 1);
583
+ const angle = Math.acos(dot);
584
+ if (!Number.isFinite(angle) || angle <= 1e-6) return false;
585
+
586
+ const maxStep = Math.min(angle, MAX_POINT_ROTATION_STEP);
587
+ const stepAngle = maxStep * rotationGain * share;
588
+ if (!Number.isFinite(stepAngle) || stepAngle <= 1e-6) return false;
589
+
590
+ const axis = fromVec.clone().cross(toVec);
591
+ if (axis.lengthSq() <= 1e-12) return false;
592
+ axis.normalize();
593
+
594
+ const quaternion = new THREE.Quaternion().setFromAxisAngle(axis, stepAngle);
595
+
596
+ component.updateMatrixWorld?.(true);
597
+ const pivotWorld = pivot.clone();
598
+ let pivotLocal = null;
599
+ if (typeof component.worldToLocal === 'function' && typeof component.localToWorld === 'function') {
600
+ pivotLocal = component.worldToLocal(pivot.clone());
601
+ }
602
+
603
+ const ok = context.applyRotation?.(component, quaternion);
604
+ if (!ok) return false;
605
+
606
+ appliedRotation = true;
607
+
608
+ const record = {
609
+ component: component.name || component.uuid,
610
+ quaternion: quaternion.toArray(),
611
+ axis: vectorToArray(axis),
612
+ angleRad: stepAngle,
613
+ angleDeg: THREE.MathUtils.radToDeg(stepAngle),
614
+ pivot: vectorToArray(pivotWorld),
615
+ };
616
+ rotations.push(record);
617
+
618
+ if (pivotLocal) {
619
+ component.updateMatrixWorld?.(true);
620
+ const pivotAfter = component.localToWorld(pivotLocal.clone());
621
+ const correction = pivotWorld.clone().sub(pivotAfter);
622
+ if (correction.lengthSq() > 1e-12) {
623
+ const moved = context.applyTranslation?.(component, correction);
624
+ if (moved) {
625
+ moves.push({ component: component.name || component.uuid, move: vectorToArray(correction) });
626
+ }
627
+ }
628
+ }
629
+
630
+ return true;
631
+ };
632
+
633
+ const movableA = !fixedA && infoA.component;
634
+ const movableB = !fixedB && infoB.component;
635
+
636
+ if (!movableA && !movableB) {
637
+ return { handled: false };
638
+ }
639
+
640
+ const shareA = movableA && movableB ? 0.5 : (movableA ? 1 : 0);
641
+ const shareB = movableA && movableB ? 0.5 : (movableB ? 1 : 0);
642
+
643
+ if (shareA > 0) applyRotationTowards(infoA, infoB.point, shareA);
644
+ if (shareB > 0) applyRotationTowards(infoB, infoA.point, shareB);
645
+
646
+ if (!appliedRotation) {
647
+ return { handled: false };
648
+ }
649
+
650
+ refreshInfo();
651
+ pd.error = distance;
652
+ pd.status = 'adjusted';
653
+ pd.message = 'Applied rotation to bring points closer.';
654
+ pd.satisfied = false;
655
+ pd.lastAppliedMoves = moves;
656
+ pd.lastAppliedRotations = rotations;
657
+ pd.pointToPointNextMode = 'translate';
658
+ if (pd.exception) delete pd.exception;
659
+
660
+ return {
661
+ handled: true,
662
+ applied: true,
663
+ result: {
664
+ ok: true,
665
+ status: 'adjusted',
666
+ satisfied: false,
667
+ applied: true,
668
+ error: distance,
669
+ message: pd.message,
670
+ infoA,
671
+ infoB,
672
+ diagnostics: {
673
+ distance,
674
+ delta: delta.toArray(),
675
+ rotations,
676
+ moves,
677
+ stage: 'rotation',
678
+ },
679
+ },
680
+ };
681
+ };
682
+
683
+ const performTranslation = () => {
684
+ const moves = [];
685
+ let appliedTranslation = false;
686
+
687
+ const applyMove = (component, vec) => {
688
+ if (!component || !vec || vec.lengthSq() === 0) return false;
689
+ const ok = context.applyTranslation?.(component, vec);
690
+ if (ok) {
691
+ moves.push({ component: component.name || component.uuid, move: vectorToArray(vec) });
692
+ }
693
+ return ok;
694
+ };
695
+
696
+ if (fixedA && fixedB) {
697
+ const message = 'Both components are fixed; unable to translate points into contact.';
698
+ pd.status = 'blocked';
699
+ pd.message = message;
700
+ pd.satisfied = false;
701
+ pd.lastAppliedMoves = [];
702
+ pd.lastAppliedRotations = [];
703
+ pd.pointToPointNextMode = 'translate';
704
+ if (pd.exception) delete pd.exception;
705
+ return {
706
+ result: {
707
+ ok: false,
708
+ status: 'blocked',
709
+ satisfied: false,
710
+ applied: false,
711
+ error: distance,
712
+ message,
713
+ infoA,
714
+ infoB,
715
+ diagnostics: {
716
+ distance,
717
+ delta: delta.toArray(),
718
+ moves: [],
719
+ rotations: [],
720
+ stage: 'translation',
721
+ },
722
+ },
723
+ applied: false,
724
+ };
725
+ }
726
+
727
+ if (!fixedA && !fixedB) {
728
+ const step = delta.clone().multiplyScalar(0.5 * translationGain);
729
+ if (step.lengthSq() > 0) {
730
+ appliedTranslation = applyMove(infoA.component, step.clone().negate()) || appliedTranslation;
731
+ appliedTranslation = applyMove(infoB.component, step) || appliedTranslation;
732
+ }
733
+ } else if (fixedA && !fixedB) {
734
+ const step = delta.clone().multiplyScalar(translationGain);
735
+ if (step.lengthSq() > 0) {
736
+ appliedTranslation = applyMove(infoB.component, step) || appliedTranslation;
737
+ }
738
+ } else if (!fixedA && fixedB) {
739
+ const step = delta.clone().multiplyScalar(translationGain);
740
+ if (step.lengthSq() > 0) {
741
+ appliedTranslation = applyMove(infoA.component, step.clone().negate()) || appliedTranslation;
742
+ }
743
+ }
744
+
745
+ if (appliedTranslation) {
746
+ refreshInfo();
747
+ pd.error = distance;
748
+ }
749
+
750
+ const status = appliedTranslation ? 'adjusted' : 'pending';
751
+ const message = appliedTranslation
752
+ ? 'Applied translation to align points.'
753
+ : 'Waiting for a movable component to translate.';
754
+
755
+ pd.status = status;
756
+ pd.message = message;
757
+ pd.satisfied = false;
758
+ pd.lastAppliedMoves = moves;
759
+ pd.lastAppliedRotations = [];
760
+ pd.pointToPointNextMode = 'rotate';
761
+ if (pd.exception) delete pd.exception;
762
+
763
+ return {
764
+ result: {
765
+ ok: true,
766
+ status,
767
+ satisfied: false,
768
+ applied: appliedTranslation,
769
+ error: distance,
770
+ message,
771
+ infoA,
772
+ infoB,
773
+ diagnostics: {
774
+ distance,
775
+ delta: delta.toArray(),
776
+ moves,
777
+ rotations: [],
778
+ stage: 'translation',
779
+ },
780
+ },
781
+ applied: appliedTranslation,
782
+ };
783
+ };
784
+
785
+ let finalResult = null;
786
+
787
+ if (pd.pointToPointNextMode === 'rotate') {
788
+ const rotationAttempt = performRotation();
789
+ if (rotationAttempt?.handled) {
790
+ return rotationAttempt.result;
791
+ }
792
+ // rotation skipped; fall through to translation
793
+ }
794
+
795
+ finalResult = performTranslation();
796
+ return finalResult.result;
797
+ }
798
+
799
+ #updateNormalDebug(context, infoA, infoB) {
800
+ if (!context?.debugMode) return;
801
+ const scene = context.scene || null;
802
+ if (!scene) return;
803
+
804
+ const iteration = context.iteration ?? 0;
805
+ const entries = [
806
+ { info: infoA, color: 0xff4d4d, label: 'A' },
807
+ { info: infoB, color: 0x4dff91, label: 'B' },
808
+ ];
809
+
810
+ const constraintId = this.inputParams?.id ?? this.inputParams?.constraintID ?? 'unknown';
811
+ for (const { info, color, label } of entries) {
812
+ if (!info?.direction || !info.origin) continue;
813
+ const dir = info.direction.clone().normalize();
814
+ if (dir.lengthSq() === 0) continue;
815
+
816
+ const origin = info.origin.clone();
817
+ const length = Math.max(this.#estimateHelperLength(info), 10);
818
+ const arrow = new THREE.ArrowHelper(dir, origin, length, color, length * 0.25, length * 0.15);
819
+ arrow.name = `touch-align-normal-${constraintId}-${label}-iter${iteration}`;
820
+ scene.add(arrow);
821
+ this._debugHelpers.push(arrow);
822
+ }
823
+ }
824
+
825
+ #clearNormalDebug(scene) {
826
+ if (!this._debugHelpers) return;
827
+ for (const helper of this._debugHelpers) {
828
+ if (!helper) continue;
829
+ if (scene && helper.parent === scene) {
830
+ scene.remove(helper);
831
+ } else if (helper.parent) {
832
+ helper.parent.remove(helper);
833
+ }
834
+ }
835
+ this._debugHelpers.length = 0;
836
+ }
837
+
838
+ #estimateHelperLength(info) {
839
+ const candidates = [];
840
+ const pushBound = (obj) => {
841
+ if (!obj) return;
842
+ if (obj.geometry?.computeBoundingSphere && !obj.geometry.boundingSphere) {
843
+ try { obj.geometry.computeBoundingSphere(); } catch {}
844
+ }
845
+ const sphere = obj.geometry?.boundingSphere;
846
+ if (sphere?.radius) candidates.push(Math.abs(sphere.radius));
847
+ if (obj.geometry?.computeBoundingBox && !obj.geometry.boundingBox) {
848
+ try { obj.geometry.computeBoundingBox(); } catch {}
849
+ }
850
+ const box = obj.geometry?.boundingBox;
851
+ if (box) candidates.push(box.getSize(new THREE.Vector3()).length() / 2);
852
+ if (typeof obj.getWorldScale === 'function') {
853
+ const scale = obj.getWorldScale(new THREE.Vector3());
854
+ candidates.push(scale.length() * 5);
855
+ }
856
+ };
857
+
858
+ pushBound(info.object);
859
+ if (Array.isArray(info.component?.children)) {
860
+ for (const child of info.component.children) {
861
+ pushBound(child);
862
+ }
863
+ }
864
+
865
+ candidates.push(info.component?.userData?.boundingRadius || 0);
866
+
867
+ const max = candidates.reduce((acc, val) => (Number.isFinite(val) ? Math.max(acc, val) : acc), 0);
868
+ return Number.isFinite(max) && max > 0 ? max : 0;
869
+ }
870
+ }
871
+
872
+
873
+ function selectionPair(params) {
874
+ if (!params || typeof params !== 'object') return [null, null];
875
+ const raw = Array.isArray(params.elements) ? params.elements : [];
876
+ const picks = raw.filter((item) => item != null).slice(0, 2);
877
+ params.elements = picks;
878
+ if (picks.length === 2) return picks;
879
+ if (picks.length === 1) return [picks[0], null];
880
+ return [null, null];
881
+ }
882
+
883
+ function vectorToArray(vec) {
884
+ if (!vec) return [0, 0, 0];
885
+ return [vec.x, vec.y, vec.z];
886
+ }
887
+
888
+ function resolvePointSelection(constraint, context, selection) {
889
+ if (!selection) return null;
890
+ const object = context.resolveObject?.(selection) || null;
891
+ const component = context.resolveComponent?.(selection) || null;
892
+ const point = resolveSelectionPoint(constraint, object, component);
893
+ return {
894
+ selection,
895
+ object,
896
+ component: component || null,
897
+ point,
898
+ };
899
+ }
900
+
901
+ function resolveSelectionPoint(constraint, object, component) {
902
+ if (object) {
903
+ try {
904
+ const rep = objectRepresentativePoint(null, object);
905
+ if (rep && typeof rep.clone === 'function') return rep.clone();
906
+ } catch {}
907
+ const worldPoint = constraint.getWorldPoint(object);
908
+ if (worldPoint) return worldPoint;
909
+ }
910
+ if (component) {
911
+ try {
912
+ const rep = objectRepresentativePoint(null, component);
913
+ if (rep && typeof rep.clone === 'function') return rep.clone();
914
+ } catch {}
915
+ component.updateMatrixWorld?.(true);
916
+ const worldPoint = constraint.getWorldPoint(component);
917
+ if (worldPoint) return worldPoint;
918
+ }
919
+ return null;
920
+ }
921
+
922
+ function resolveComponentMidpoint(constraint, component) {
923
+ if (!component) return null;
924
+ component.updateMatrixWorld?.(true);
925
+ try {
926
+ const rep = objectRepresentativePoint(null, component);
927
+ if (rep && typeof rep.clone === 'function') return rep.clone();
928
+ } catch {}
929
+ const worldPoint = constraint.getWorldPoint(component);
930
+ if (worldPoint) return worldPoint;
931
+ if (typeof component.getWorldPosition === 'function') {
932
+ return component.getWorldPosition(new THREE.Vector3());
933
+ }
934
+ if (component.position) {
935
+ const pos = component.position.clone();
936
+ if (component.parent?.matrixWorld) {
937
+ return pos.applyMatrix4(component.parent.matrixWorld);
938
+ }
939
+ return pos;
940
+ }
941
+ return null;
942
+ }
943
+
944
+ function selectionKindFrom(object, selection) {
945
+ const raw = (selection?.kind
946
+ || object?.userData?.type
947
+ || object?.userData?.brepType
948
+ || object?.type
949
+ || '').toString().toUpperCase();
950
+ if (!raw) return 'UNKNOWN';
951
+ if (raw.includes('FACE')) return 'FACE';
952
+ if (raw.includes('EDGE')) return 'EDGE';
953
+ if (raw.includes('VERTEX') || raw.includes('POINT')) return 'POINT';
954
+ if (raw.includes('COMPONENT')) return 'COMPONENT';
955
+ return raw;
956
+ }
957
+
958
+ function normalizeSelectionKind(kind) {
959
+ if (kind === 'COMPONENT') return 'FACE';
960
+ return kind;
961
+ }