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.
Files changed (271) hide show
  1. package/LICENSE.md +32 -0
  2. package/README.md +157 -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,674 @@
1
+ import * as THREE from 'three';
2
+ import { drawConstraintGlyphs } from './glyphs.js';
3
+
4
+ // Debug switch for dimension label interactions
5
+ // Toggle at runtime via: window.__SKETCH_DIM_DEBUG = true/false
6
+ const DIM_DEBUG = false;
7
+ const dbg = (...args) => { try { if (DIM_DEBUG || window.__SKETCH_DIM_DEBUG) console.log('[DIM]', ...args); } catch { } };
8
+
9
+ // Unified dimension colors
10
+ const DIM_COLOR_DEFAULT = 0x69a8ff; // blue
11
+ const DIM_COLOR_HOVER = 0xffd54a; // yellow
12
+ const DIM_COLOR_SELECTED = 0x6fe26f; // green
13
+
14
+ function clearDims(inst) {
15
+ if (!inst._dimRoot) return;
16
+ const labels = Array.from(inst._dimRoot.querySelectorAll('.dim-label, .glyph-label'));
17
+ labels.forEach((n) => n.parentNode && n.parentNode.removeChild(n));
18
+ if (inst._dimSVG) while (inst._dimSVG.firstChild) inst._dimSVG.removeChild(inst._dimSVG.firstChild);
19
+ if (inst._dim3D) {
20
+ while (inst._dim3D.children.length) {
21
+ const ch = inst._dim3D.children.pop();
22
+ try { ch.geometry?.dispose(); ch.material?.dispose?.(); } catch { }
23
+ }
24
+ }
25
+ }
26
+
27
+ export function renderDimensions(inst) {
28
+ if (!inst._dimRoot || !inst._solver || !inst._lock) return;
29
+ // If a label drag is active, avoid tearing down/rebuilding HTML labels which
30
+ // would drop pointer capture and prematurely end the drag. Only refresh 3D leaders.
31
+ if (inst._suspendDimLabelRebuild) {
32
+ try { _redrawDim3D(inst); } catch { }
33
+ return;
34
+ }
35
+ clearDims(inst);
36
+ // Reset per-frame angle geometry cache used to center labels on arc midpoints
37
+ try { inst._dimAngleGeom = new Map(); } catch {}
38
+ const s = inst._solver.sketchObject;
39
+ const to3 = (u, v) => new THREE.Vector3()
40
+ .copy(inst._lock.basis.origin)
41
+ .addScaledVector(inst._lock.basis.x, u)
42
+ .addScaledVector(inst._lock.basis.y, v);
43
+ const P = (id) => s.points.find((p) => p.id === id);
44
+
45
+ const mk = (c, text, world, planeOffOverride = null, noNudge = false) => {
46
+ const d = document.createElement('div');
47
+ d.className = 'dim-label';
48
+ try { d.dataset.cid = String(c.id); } catch { }
49
+ d.style.position = 'absolute';
50
+ // Center the label on the placement point
51
+ d.style.transform = 'translate(-50%, -50%)';
52
+ d.style.transformOrigin = '50% 50%';
53
+ d.style.padding = '2px 6px';
54
+ d.style.border = '1px solid #364053';
55
+ d.style.borderRadius = '6px';
56
+ d.style.background = 'rgba(20,24,30,.9)';
57
+ d.style.color = '#e6e6e6';
58
+ d.style.font = '12px ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace';
59
+ d.style.pointerEvents = 'auto';
60
+ d.style.userSelect = 'none';
61
+ d.style.webkitUserSelect = 'none';
62
+ d.style.MozUserSelect = 'none';
63
+ d.style.touchAction = 'none';
64
+ d.setAttribute('draggable', 'false');
65
+ d.onselectstart = () => false;
66
+ d.textContent = text;
67
+
68
+ // Selection/hover styling for labels
69
+ const isSel = Array.from(inst._selection || []).some(it => it.type === 'constraint' && it.id === c.id);
70
+ const isHov = inst._hover && inst._hover.type === 'constraint' && inst._hover.id === c.id;
71
+ if (isSel) {
72
+ d.style.border = '1px solid #2f6d2f';
73
+ d.style.background = 'rgba(111,226,111,.16)';
74
+ } else if (isHov) {
75
+ d.style.border = '1px solid #6f5a12';
76
+ d.style.background = 'rgba(255,213,74,.12)';
77
+ }
78
+
79
+ // Centralized event hookup for drag/edit/hover/click
80
+ attachDimLabelEvents(inst, d, c, world);
81
+
82
+ inst._dimRoot.appendChild(d);
83
+ const saved = inst._dimOffsets.get(c.id) || { du: 0, dv: 0 };
84
+ const off = planeOffOverride || saved;
85
+ updateOneDimPosition(inst, d, world, off, noNudge);
86
+ };
87
+
88
+ // Prepare glyph placement avoidance (used by drawConstraintGlyph)
89
+ try {
90
+ const rectForGlyph = inst.viewer.renderer.domElement.getBoundingClientRect();
91
+ const baseGlyph = Math.max(0.1, worldPerPixel(inst.viewer.camera, rectForGlyph.width, rectForGlyph.height) * 14);
92
+ inst._glyphAvoid = {
93
+ placed: [], // array of {u,v}
94
+ minDist: baseGlyph * 0.9,
95
+ step: baseGlyph * 0.3,
96
+ };
97
+ } catch { }
98
+
99
+ const glyphConstraints = [];
100
+ for (const c of s.constraints || []) {
101
+ const sel = Array.from(inst._selection || []).some(it => it.type === 'constraint' && it.id === c.id);
102
+ const hov = inst._hover && inst._hover.type === 'constraint' && inst._hover.id === c.id;
103
+ if (c.type === '⟺') {
104
+ if (c.displayStyle === 'radius' && c.points?.length >= 2) {
105
+ const pc = P(c.points[0]), pr = P(c.points[1]); if (!pc || !pr) continue;
106
+ const col = sel ? DIM_COLOR_SELECTED : (hov ? DIM_COLOR_HOVER : DIM_COLOR_DEFAULT);
107
+ dimRadius3D(inst, pc, pr, c.id, col);
108
+ const v = new THREE.Vector2(pr.x - pc.x, pr.y - pc.y); const L = v.length() || 1; const rx = v.x / L, ry = v.y / L; const nx = -ry, ny = rx;
109
+ const offSaved = inst._dimOffsets.get(c.id) || {};
110
+ const dr = (offSaved.dr !== undefined || offSaved.dp !== undefined)
111
+ ? (Number(offSaved.dr) || 0)
112
+ : ((Number(offSaved.du) || 0) * rx + (Number(offSaved.dv) || 0) * ry);
113
+ const dp = (offSaved.dr !== undefined || offSaved.dp !== undefined)
114
+ ? (Number(offSaved.dp) || 0)
115
+ : ((Number(offSaved.du) || 0) * nx + (Number(offSaved.dv) || 0) * ny);
116
+ const label = to3(pr.x + rx * dr + nx * dp, pr.y + ry * dr + ny * dp);
117
+ const val = Number(c.value) ?? 0;
118
+ const txt = c.displayStyle === 'diameter' ? `⌀${(2 * val).toFixed(3)} Diameter` : `R${val.toFixed(3)} Radius`;
119
+ mk(c, txt, label, { du: 0, dv: 0 });
120
+ } else if (c.points?.length >= 2) {
121
+ const p0 = P(c.points[0]), p1 = P(c.points[1]); if (!p0 || !p1) continue;
122
+ const basis = (() => { const dx = p1.x - p0.x, dy = p1.y - p0.y; const L = Math.hypot(dx, dy) || 1; const tx = dx / L, ty = dy / L; return { tx, ty, nx: -ty, ny: tx }; })();
123
+ const rect = inst.viewer.renderer.domElement.getBoundingClientRect();
124
+ const base = Math.max(0.1, worldPerPixel(inst.viewer.camera, rect.width, rect.height) * 20);
125
+ const offSaved = inst._dimOffsets.get(c.id) || { du: 0, dv: 0 };
126
+ const d = typeof offSaved.d === 'number' ? offSaved.d : (offSaved.du || 0) * basis.nx + (offSaved.dv || 0) * basis.ny;
127
+ // Constrain label to center of the dimension line: lock tangential offset to 0
128
+ const t = 0;
129
+ const col = sel ? DIM_COLOR_SELECTED : (hov ? DIM_COLOR_HOVER : DIM_COLOR_DEFAULT);
130
+ dimDistance3D(inst, p0, p1, c.id, col);
131
+ mk(c, String((Number(c.value) ?? 0).toFixed(3)), to3((p0.x + p1.x) / 2, (p0.y + p1.y) / 2), { du: basis.tx * t + basis.nx * (base + d), dv: basis.ty * t + basis.ny * (base + d) }, true);
132
+ }
133
+ }
134
+ if (c.type === '∠' && c.points?.length >= 4) {
135
+ const p0 = P(c.points[0]), p1 = P(c.points[1]), p2 = P(c.points[2]), p3 = P(c.points[3]); if (!p0 || !p1 || !p2 || !p3) continue;
136
+ const I = intersect(p0, p1, p2, p3);
137
+ const col = sel ? DIM_COLOR_SELECTED : (hov ? DIM_COLOR_HOVER : DIM_COLOR_DEFAULT);
138
+ // Pass current numeric value to renderer so the arc length matches the annotation
139
+ const angleValueDeg = (typeof c.value === 'number' && Number.isFinite(c.value)) ? Number(c.value) : null;
140
+ dimAngle3D(inst, p0, p1, p2, p3, c.id, I, col, angleValueDeg);
141
+ // Center label on arc midpoint if available from the arc construction
142
+ let labelWorld = to3(I.x, I.y);
143
+ try {
144
+ const gmap = inst._dimAngleGeom;
145
+ const gd = gmap && (gmap.get ? gmap.get(c.id) : gmap[c.id]);
146
+ if (gd && Number.isFinite(gd.midU) && Number.isFinite(gd.midV)) {
147
+ labelWorld = to3(gd.midU, gd.midV);
148
+ }
149
+ } catch {}
150
+ mk(c, String(c.value ?? ''), labelWorld, { du: 0, dv: 0 }, true);
151
+ } else {
152
+ // Non-dimension constraints: collect for grouped glyph rendering
153
+ glyphConstraints.push(c);
154
+ }
155
+ }
156
+
157
+ // Render grouped glyphs (non-dimension constraints)
158
+ try { drawConstraintGlyphs(inst, glyphConstraints); } catch { }
159
+ }
160
+
161
+ // Lightweight redraw of only the 3D leaders/arrows without touching HTML labels.
162
+ // Used during drag so pointer capture on the label is not lost.
163
+ function _redrawDim3D(inst, onlyCid = null) {
164
+ try {
165
+ if (!inst || !inst._solver || !inst._lock || !inst._dim3D) return;
166
+ // Clear existing 3D primitives for either all dims or one cid
167
+ for (let i = inst._dim3D.children.length - 1; i >= 0; i--) {
168
+ const ch = inst._dim3D.children[i];
169
+ const isDim = ch && ch.userData && ch.userData.kind === 'dim';
170
+ const match = onlyCid == null || (isDim && ch.userData.cid === onlyCid);
171
+ if (isDim && match) {
172
+ inst._dim3D.remove(ch);
173
+ try { ch.geometry?.dispose(); ch.material?.dispose?.(); } catch { }
174
+ }
175
+ }
176
+ const s = inst._solver.sketchObject || {};
177
+ const P = (id) => (s.points || []).find((p) => p.id === id);
178
+ const selSet = new Set(Array.from(inst._selection || []).filter(it => it.type === 'constraint').map(it => it.id));
179
+ const hovId = (inst._hover && inst._hover.type === 'constraint') ? inst._hover.id : null;
180
+ for (const c of (s.constraints || [])) {
181
+ if (onlyCid != null && c?.id !== onlyCid) continue;
182
+ const sel = selSet.has(c?.id);
183
+ const hov = (hovId === c?.id);
184
+ const col = sel ? DIM_COLOR_SELECTED : (hov ? DIM_COLOR_HOVER : DIM_COLOR_DEFAULT);
185
+ if (!c) continue;
186
+ if (c.type === '⟺') {
187
+ if (c.displayStyle === 'radius' && Array.isArray(c.points) && c.points.length >= 2) {
188
+ const pc = P(c.points[0]); const pr = P(c.points[1]);
189
+ if (pc && pr) dimRadius3D(inst, pc, pr, c.id, col);
190
+ } else if (Array.isArray(c.points) && c.points.length >= 2) {
191
+ const p0 = P(c.points[0]); const p1 = P(c.points[1]);
192
+ if (p0 && p1) dimDistance3D(inst, p0, p1, c.id, col);
193
+ }
194
+ } else if (c.type === '∠' && Array.isArray(c.points) && c.points.length >= 4) {
195
+ const p0 = P(c.points[0]), p1 = P(c.points[1]), p2 = P(c.points[2]), p3 = P(c.points[3]);
196
+ if (!p0 || !p1 || !p2 || !p3) continue;
197
+ const I = intersect(p0, p1, p2, p3);
198
+ const angleValueDeg = (typeof c.value === 'number' && Number.isFinite(c.value)) ? Number(c.value) : null;
199
+ dimAngle3D(inst, p0, p1, p2, p3, c.id, I, col, angleValueDeg);
200
+ }
201
+ }
202
+ } catch { }
203
+ }
204
+
205
+ // Helpers (module-local)
206
+ function updateOneDimPosition(inst, el, world, off, noNudge = false) {
207
+ const du = Number(off?.du) || 0; const dv = Number(off?.dv) || 0;
208
+ const O = inst._lock.basis.origin, X = inst._lock.basis.x, Y = inst._lock.basis.y;
209
+ // Base world position for the label
210
+ let w = world.clone().add(X.clone().multiplyScalar(du)).add(Y.clone().multiplyScalar(dv));
211
+ // Compute plane coords
212
+ try {
213
+ const d = w.clone().sub(O);
214
+ let u = d.dot(X.clone().normalize());
215
+ let v = d.dot(Y.clone().normalize());
216
+ const u0 = u, v0 = v;
217
+ if (!noNudge) {
218
+ // Nudge away from nearby sketch points to avoid overlap
219
+ const pts = (inst._solver && Array.isArray(inst._solver.sketchObject?.points)) ? inst._solver.sketchObject.points : [];
220
+ const rect = inst.viewer.renderer.domElement.getBoundingClientRect();
221
+ const wpp = worldPerPixel(inst.viewer.camera, rect.width, rect.height);
222
+ const handleR = Math.max(0.02, wpp * 8 * 0.5);
223
+ const minDist = handleR * 1.2;
224
+ let iter = 0;
225
+ while (iter++ < 4) {
226
+ let nearest = null, nd = Infinity;
227
+ for (const p of pts) {
228
+ const dd = Math.hypot(u - p.x, v - p.y);
229
+ if (dd < nd) { nd = dd; nearest = p; }
230
+ }
231
+ if (!nearest || nd >= minDist) break;
232
+ const dx = u - nearest.x, dy = v - nearest.y; const L = Math.hypot(dx, dy) || 1e-6;
233
+ const push = (minDist - nd) + (0.15 * minDist);
234
+ u = nearest.x + (dx / L) * (nd + push);
235
+ v = nearest.y + (dy / L) * (nd + push);
236
+ }
237
+ }
238
+ if (!noNudge && inst && inst._debugDragCID != null && String(inst._debugDragCID) === String(el?.dataset?.cid)) {
239
+ const duN = u - u0, dvN = v - v0; const moved = Math.hypot(duN, dvN);
240
+ if (moved > 1e-6) dbg('label-nudged', { cid: el?.dataset?.cid, from: { u: u0, v: v0 }, to: { u, v }, delta: { du: duN, dv: dvN } });
241
+ }
242
+ // Rebuild world position from nudged (u,v)
243
+ w = new THREE.Vector3().copy(O).addScaledVector(X, u).addScaledVector(Y, v);
244
+ } catch { }
245
+ const pt = w.project(inst.viewer.camera);
246
+ const rect2 = inst.viewer.renderer.domElement.getBoundingClientRect();
247
+ const x = (pt.x * 0.5 + 0.5) * rect2.width; const y = (-pt.y * 0.5 + 0.5) * rect2.height;
248
+ el.style.left = `${Math.round(x)}px`; el.style.top = `${Math.round(y)}px`;
249
+ // Only log label placement for the dimension actively being dragged
250
+ if (inst && inst._debugDragCID != null && String(inst._debugDragCID) === String(el?.dataset?.cid)) {
251
+ dbg('label-place', { cid: el?.dataset?.cid, world: { x: w.x, y: w.y, z: w.z }, screen: { x: Math.round(x), y: Math.round(y) }, off: { du, dv }, noNudge });
252
+ }
253
+ }
254
+
255
+ function pointerToPlaneUV(inst, e) {
256
+ const v = inst.viewer; if (!v || !inst._lock) return null;
257
+ const rect = v.renderer.domElement.getBoundingClientRect();
258
+ const ndc = new THREE.Vector2(((e.clientX - rect.left) / rect.width) * 2 - 1, -(((e.clientY - rect.top) / rect.height) * 2 - 1));
259
+ inst._raycaster.setFromCamera(ndc, v.camera);
260
+ const n = inst._lock?.basis?.z?.clone();
261
+ const o = inst._lock?.basis?.origin?.clone();
262
+ if (!n || !o) return null;
263
+ const pl = new THREE.Plane().setFromNormalAndCoplanarPoint(n, o);
264
+ const hit = new THREE.Vector3();
265
+ let ok = inst._raycaster.ray.intersectPlane(pl, hit);
266
+ if (!ok) {
267
+ const invRay = new THREE.Ray(inst._raycaster.ray.origin.clone(), inst._raycaster.ray.direction.clone().negate());
268
+ ok = invRay.intersectPlane(pl, hit);
269
+ }
270
+ if (!ok) return null;
271
+ const bx = inst._lock.basis.x; const by = inst._lock.basis.y;
272
+ const u = hit.clone().sub(o).dot(bx.clone().normalize());
273
+ const v2 = hit.clone().sub(o).dot(by.clone().normalize());
274
+ const out = { u, v: v2 };
275
+ // Only log during active drag to avoid spam
276
+ if (inst && inst._debugDragCID != null) dbg('pointer->uv', { x: e.clientX, y: e.clientY }, out);
277
+ return out;
278
+ }
279
+
280
+ // Centralized event wiring for dimension labels (drag, click, hover, edit)
281
+ function attachDimLabelEvents(inst, el, c, world) {
282
+ // Click: toggle constraint selection (dblclick handled separately)
283
+ el.addEventListener('click', (e) => {
284
+ if (e.detail > 1) return;
285
+ try { inst.toggleSelectConstraint?.(c.id); } catch { }
286
+ e.preventDefault(); e.stopPropagation(); try { e.stopImmediatePropagation(); } catch { }
287
+ dbg('click', { cid: c.id, type: c.type });
288
+ });
289
+
290
+ // Hover reflects in overlays/sidebar
291
+ el.addEventListener('pointerenter', () => { try { inst.hoverConstraintFromLabel?.(c.id); } catch { } });
292
+ el.addEventListener('pointerleave', () => { try { inst.clearHoverFromLabel?.(c.id); } catch { } });
293
+
294
+ // Edit on double click (value expression support preserved)
295
+ el.addEventListener('dblclick', async (e) => {
296
+ e.preventDefault(); e.stopPropagation();
297
+ dbg('dblclick-edit', { cid: c.id, type: c.type, value: c.value, expr: c.valueExpr });
298
+ const solver = inst?._solver || null;
299
+ const canPause = solver && typeof solver.pause === 'function' && typeof solver.resume === 'function' && typeof solver.isPaused === 'function';
300
+ const pausedByPrompt = !!(canPause && !solver.isPaused());
301
+ let resumed = false;
302
+ const resumeSolver = () => {
303
+ if (!resumed && pausedByPrompt) {
304
+ try { solver.resume(); } catch { }
305
+ resumed = true;
306
+ }
307
+ };
308
+ if (pausedByPrompt) {
309
+ try { solver.pause('dim-edit'); } catch { }
310
+ }
311
+ const initial = (typeof c.valueExpr === 'string' && c.valueExpr.length)
312
+ ? c.valueExpr
313
+ : String(c.value ?? '');
314
+ try {
315
+ const v = await prompt('Enter value', initial);
316
+ if (v == null) return;
317
+ const input = String(v?.trim?.() ?? v);
318
+ if (!input.length) return;
319
+ const ph = inst?.viewer?.partHistory;
320
+ const exprSrc = ph?.expressions || '';
321
+ const runExpr = (expressions, equation) => {
322
+ try {
323
+ const fn = `${expressions}; return ${equation} ;`;
324
+ let result = Function(fn)();
325
+ if (typeof result === 'string') {
326
+ const num = Number(result);
327
+ if (!Number.isNaN(num)) return num;
328
+ }
329
+ return result;
330
+ } catch (err) {
331
+ console.log('Expression eval failed:', err?.message || err);
332
+ return null;
333
+ }
334
+ };
335
+ const plainNumberRe = /^\s*[+-]?(?:\d+(?:\.\d+)?|\.\d+)(?:e[+-]?\d+)?\s*$/i;
336
+ let numeric = null;
337
+ if (plainNumberRe.test(input)) {
338
+ numeric = parseFloat(input);
339
+ c.valueExpr = undefined;
340
+ } else {
341
+ numeric = runExpr(exprSrc, input);
342
+ if (numeric == null || !Number.isFinite(numeric)) return;
343
+ c.valueExpr = input;
344
+ }
345
+ c.value = Number(numeric);
346
+ resumeSolver();
347
+ try { solver?.solveSketch('full'); } catch { }
348
+ try { solver?.hooks?.updateCanvas?.(); } catch { }
349
+ } finally {
350
+ resumeSolver();
351
+ }
352
+ });
353
+
354
+ // Drag handling with commit-on-drop
355
+ let dragging = false, moved = false, sx = 0, sy = 0, start = {};
356
+ let sClientX = 0, sClientY = 0;
357
+ let distNx = 0, distNy = 0, distTx = 0, distTy = 0, distStartD = 0, distStartT = 0;
358
+ let radRx = 0, radRy = 0, radNx = 0, radNy = 0, radStartDr = 0, radStartDp = 0;
359
+ let angStartDU = 0, angStartDV = 0, angMidDX = 0, angMidDY = 0, angStartMag = 0;
360
+ let pendingOff = null;
361
+
362
+ el.addEventListener('pointerdown', (e) => {
363
+ dragging = true; moved = false; pendingOff = null;
364
+ try { inst._suspendDimLabelRebuild = true; inst._activeDimLabelDragId = c.id; } catch { }
365
+ try { inst._debugDragCID = c.id; } catch { }
366
+ const uv = pointerToPlaneUV(inst, e);
367
+ sx = uv?.u || 0; sy = uv?.v || 0;
368
+ start = { ...(inst._dimOffsets.get(c.id) || {}) };
369
+ sClientX = e.clientX || 0; sClientY = e.clientY || 0;
370
+ dbg('pointerdown', { cid: c.id, type: c.type, startUV: { u: sx, v: sy }, startOffset: start });
371
+ if (c.type === '⟺' && c.displayStyle === 'radius' && Array.isArray(c.points) && c.points.length >= 2) {
372
+ const sObj = inst._solver.sketchObject;
373
+ const pc = sObj.points.find((p) => p.id === c.points[0]);
374
+ const pr = sObj.points.find((p) => p.id === c.points[1]);
375
+ if (pc && pr) {
376
+ const vx = pr.x - pc.x, vy = pr.y - pc.y; const L = Math.hypot(vx, vy) || 1;
377
+ radRx = vx / L; radRy = vy / L; radNx = -radRy; radNy = radRx;
378
+ radStartDr = Number(start.dr) || 0; radStartDp = Number(start.dp) || 0;
379
+ }
380
+ } else if (c.type === '⟺' && Array.isArray(c.points) && c.points.length >= 2) {
381
+ const sObj = inst._solver.sketchObject;
382
+ const p0 = sObj.points.find((p) => p.id === c.points[0]);
383
+ const p1 = sObj.points.find((p) => p.id === c.points[1]);
384
+ if (p0 && p1) {
385
+ const dx = p1.x - p0.x, dy = p1.y - p0.y; const L = Math.hypot(dx, dy) || 1;
386
+ const tx = dx / L, ty = dy / L; distTx = tx; distTy = ty;
387
+ distNx = -ty; distNy = tx;
388
+ const du0 = Number(start.du) || 0, dv0 = Number(start.dv) || 0;
389
+ distStartD = (typeof start.d === 'number') ? Number(start.d) : (du0 * distNx + dv0 * distNy);
390
+ // Constrain label to center: lock initial tangential offset to 0
391
+ distStartT = 0;
392
+ }
393
+ } else if (c.type === '∠') {
394
+ angStartDU = Number(start.du) || 0; angStartDV = Number(start.dv) || 0;
395
+ angStartMag = Math.hypot(angStartDU, angStartDV);
396
+ // Use current arc midpoint direction if available; fallback to start offset direction
397
+ try {
398
+ const gd = inst._dimAngleGeom && (inst._dimAngleGeom.get ? inst._dimAngleGeom.get(c.id) : inst._dimAngleGeom[c.id]);
399
+ if (gd && Number.isFinite(gd.midU) && Number.isFinite(gd.midV) && Number.isFinite(gd.cx) && Number.isFinite(gd.cy)) {
400
+ const vx = gd.midU - gd.cx, vy = gd.midV - gd.cy; const L = Math.hypot(vx, vy) || 1;
401
+ angMidDX = vx / L; angMidDY = vy / L;
402
+ } else {
403
+ const L = Math.hypot(angStartDU, angStartDV) || 1; angMidDX = (angStartDU / L); angMidDY = (angStartDV / L);
404
+ }
405
+ } catch { const L = Math.hypot(angStartDU, angStartDV) || 1; angMidDX = (angStartDU / L); angMidDY = (angStartDV / L); }
406
+ }
407
+ try { if (inst.viewer?.controls) inst.viewer.controls.enabled = false; } catch { }
408
+ try { el.setPointerCapture(e.pointerId); } catch { }
409
+ });
410
+
411
+ el.addEventListener('pointermove', (e) => {
412
+ if (!dragging) return;
413
+ const uv = pointerToPlaneUV(inst, e); if (!uv) return;
414
+ const pxThreshold = 3;
415
+ const pxDx = Math.abs((e.clientX || 0) - sClientX);
416
+ const pxDy = Math.abs((e.clientY || 0) - sClientY);
417
+ if (!moved && (pxDx + pxDy) < pxThreshold) return;
418
+ moved = true;
419
+ dbg('pointermove', { cid: c.id, type: c.type, uv, pxDx, pxDy });
420
+ const du = uv.u - sx; const dv = uv.v - sy;
421
+ if (c.type === '⟺' && c.displayStyle === 'radius' && Array.isArray(c.points) && c.points.length >= 2) {
422
+ const dr = (Number(radStartDr) || 0) + (du * radRx + dv * radRy);
423
+ const dp = (Number(radStartDp) || 0) + (du * radNx + dv * radNy);
424
+ pendingOff = { dr, dp };
425
+ // live label preview without committing
426
+ const toLabel = { du: radRx * dr + radNx * dp, dv: radRy * dr + radNy * dp };
427
+ updateOneDimPosition(inst, el, world, toLabel, true);
428
+ dbg('preview-radius', { cid: c.id, dr, dp, toLabel });
429
+ try { inst._dimOffsets.set(c.id, toLabel); _redrawDim3D(inst, c.id); } catch { }
430
+ } else if (c.type === '⟺' && Array.isArray(c.points) && c.points.length >= 2) {
431
+ const deltaN = du * distNx + dv * distNy;
432
+ const newD = distStartD + deltaN; const newT = 0;
433
+ pendingOff = { d: newD, t: newT };
434
+ const rect = inst.viewer.renderer.domElement.getBoundingClientRect();
435
+ const base = Math.max(0.1, worldPerPixel(inst.viewer.camera, rect.width, rect.height) * 20);
436
+ const toLabel = { du: distNx * (base + newD), dv: distNy * (base + newD) };
437
+ updateOneDimPosition(inst, el, world, toLabel, true);
438
+ dbg('preview-distance', { cid: c.id, d: newD, t: newT, toLabel });
439
+ try { inst._dimOffsets.set(c.id, { du: toLabel.du, dv: toLabel.dv }); _redrawDim3D(inst, c.id); } catch { }
440
+ } else if (c.type === '∠') {
441
+ // Constrain angle label to arc midpoint: allow only radial changes along midpoint direction
442
+ const deltaRadial = du * angMidDX + dv * angMidDY;
443
+ const m = Math.max(0, angStartMag + deltaRadial);
444
+ const toGeom = { du: angMidDX * m, dv: angMidDY * m };
445
+ pendingOff = { ...toGeom };
446
+ try { inst._dimOffsets.set(c.id, toGeom); _redrawDim3D(inst, c.id); } catch { }
447
+ // Reposition label exactly at new arc midpoint
448
+ try {
449
+ const gd = inst._dimAngleGeom && (inst._dimAngleGeom.get ? inst._dimAngleGeom.get(c.id) : inst._dimAngleGeom[c.id]);
450
+ if (gd) {
451
+ const labelWorld = new THREE.Vector3().copy(inst._lock.basis.origin)
452
+ .addScaledVector(inst._lock.basis.x, gd.midU)
453
+ .addScaledVector(inst._lock.basis.y, gd.midV);
454
+ updateOneDimPosition(inst, el, labelWorld, { du: 0, dv: 0 }, true);
455
+ }
456
+ } catch {}
457
+ dbg('preview-angle', { cid: c.id, toGeom });
458
+ }
459
+ e.preventDefault(); e.stopPropagation(); try { e.stopImmediatePropagation(); } catch { }
460
+ });
461
+
462
+ const computePendingFromEvent = (e) => {
463
+ const uv = pointerToPlaneUV(inst, e); if (!uv) return null;
464
+ if (c.type === '⟺' && c.displayStyle === 'radius' && Array.isArray(c.points) && c.points.length >= 2) {
465
+ const du = uv.u - sx; const dv = uv.v - sy;
466
+ const dr = (Number(radStartDr) || 0) + (du * radRx + dv * radRy);
467
+ const dp = (Number(radStartDp) || 0) + (du * radNx + dv * radNy);
468
+ return { dr, dp };
469
+ } else if (c.type === '⟺' && Array.isArray(c.points) && c.points.length >= 2) {
470
+ const du = uv.u - sx; const dv = uv.v - sy;
471
+ const deltaN = du * distNx + dv * distNy;
472
+ const newD = distStartD + deltaN; const newT = 0;
473
+ return { d: newD, t: newT };
474
+ } else if (c.type === '∠') {
475
+ const du = uv.u - sx; const dv = uv.v - sy;
476
+ const deltaRadial = du * angMidDX + dv * angMidDY;
477
+ const m = Math.max(0, angStartMag + deltaRadial);
478
+ return { du: angMidDX * m, dv: angMidDY * m };
479
+ }
480
+ return null;
481
+ };
482
+
483
+ const commitAndRefresh = () => {
484
+ if (pendingOff) { try { inst._dimOffsets.set(c.id, pendingOff); dbg('commit', { cid: c.id, off: pendingOff }); } catch { } pendingOff = null; }
485
+ try { inst._solver?.hooks?.updateCanvas?.(); } catch { }
486
+ try { renderDimensions(inst); dbg('renderDimensions'); } catch { }
487
+ };
488
+
489
+ el.addEventListener('pointerup', (e) => {
490
+ let hadPending = !!pendingOff;
491
+ dragging = false;
492
+ try { el.releasePointerCapture(e.pointerId); } catch { }
493
+ try { if (inst.viewer?.controls) inst.viewer.controls.enabled = true; } catch { }
494
+ if (!hadPending) { pendingOff = computePendingFromEvent(e); hadPending = !!pendingOff; dbg('pointerup-computed', { cid: c.id, pendingOff }); }
495
+ if (hadPending) {
496
+ commitAndRefresh();
497
+ e.preventDefault(); e.stopPropagation(); try { e.stopImmediatePropagation(); } catch { }
498
+ }
499
+ try { inst._suspendDimLabelRebuild = false; inst._activeDimLabelDragId = null; } catch { }
500
+ try { inst._debugDragCID = null; } catch { }
501
+ });
502
+
503
+ el.addEventListener('pointercancel', (e) => {
504
+ let hadPending = !!pendingOff;
505
+ dragging = false;
506
+ try { el.releasePointerCapture(e.pointerId); } catch { }
507
+ try { if (inst.viewer?.controls) inst.viewer.controls.enabled = true; } catch { }
508
+ if (!hadPending) { pendingOff = computePendingFromEvent(e); hadPending = !!pendingOff; dbg('pointercancel-computed', { cid: c.id, pendingOff }); }
509
+ if (hadPending) {
510
+ commitAndRefresh();
511
+ e.preventDefault(); e.stopPropagation(); try { e.stopImmediatePropagation(); } catch { }
512
+ }
513
+ try { inst._suspendDimLabelRebuild = false; inst._activeDimLabelDragId = null; } catch { }
514
+ try { inst._debugDragCID = null; } catch { }
515
+ });
516
+ }
517
+
518
+ function dimDistance3D(inst, p0, p1, cid, color = 0x67e667) {
519
+ const off = inst._dimOffsets.get(cid) || { du: 0, dv: 0 };
520
+ const X = inst._lock.basis.x, Y = inst._lock.basis.y, O = inst._lock.basis.origin;
521
+ const u0 = p0.x, v0 = p0.y, u1 = p1.x, v1 = p1.y; const dx = u1 - u0, dy = v1 - v0; const L = Math.hypot(dx, dy) || 1; const tx = dx / L, ty = dy / L; const nx = -ty, ny = tx;
522
+ const rect = inst.viewer.renderer.domElement.getBoundingClientRect();
523
+ const base = Math.max(0.1, worldPerPixel(inst.viewer.camera, rect.width, rect.height) * 20);
524
+ const d = typeof off.d === 'number' ? off.d : (off.du || 0) * nx + (off.dv || 0) * ny;
525
+ const ou = nx * (base + d), ov = ny * (base + d);
526
+ const P = (u, v) => new THREE.Vector3().copy(O).addScaledVector(X, u).addScaledVector(Y, v);
527
+ const addLine = (pts, mat) => { const g = new THREE.BufferGeometry().setFromPoints(pts.map(p => P(p.u, p.v))); const ln = new THREE.Line(g, mat); ln.userData = { kind: 'dim', cid }; ln.renderOrder = 10020; inst._dim3D.add(ln); };
528
+ const green = new THREE.LineBasicMaterial({ color, depthTest: false, depthWrite: false, transparent: true });
529
+ addLine([{ u: u0 + ou, v: v0 + ov }, { u: u1 + ou, v: v1 + ov }], green);
530
+ addLine([{ u: u0, v: v0 }, { u: u0 + ou, v: v0 + ov }], green.clone());
531
+ addLine([{ u: u1, v: v1 }, { u: u1 + ou, v: v1 + ov }], green.clone());
532
+ const ah = Math.max(0.06, worldPerPixel(inst.viewer.camera, rect.width, rect.height) * 6);
533
+ const s = 0.6; const arrow = (ux, vy, dir) => { const tip = { u: ux + ou, v: vy + ov }; const ax = dir * tx, ay = dir * ty; const wx = -ay, wy = ax; const A = { u: tip.u + ax * ah + wx * ah * s, v: tip.v + ay * ah + wy * ah * s }; const B = { u: tip.u + ax * ah - wx * ah * s, v: tip.v + ay * ah - wy * ah * s }; addLine([{ u: tip.u, v: tip.v }, A], green.clone()); addLine([{ u: tip.u, v: tip.v }, B], green.clone()); };
534
+ // Opposed arrows pointing towards the measurement span
535
+ arrow(u0, v0, +1); arrow(u1, v1, -1);
536
+ }
537
+
538
+ function dimRadius3D(inst, pc, pr, cid, color = 0x69a8ff) {
539
+ const off = inst._dimOffsets.get(cid) || {};
540
+ const X = inst._lock.basis.x, Y = inst._lock.basis.y, O = inst._lock.basis.origin;
541
+ const P = (u, v) => new THREE.Vector3().copy(O).addScaledVector(X, u).addScaledVector(Y, v);
542
+ const blue = new THREE.LineBasicMaterial({ color, depthTest: false, depthWrite: false, transparent: true });
543
+ const add = (uvs) => { const g = new THREE.BufferGeometry().setFromPoints(uvs.map(q => P(q.u, q.v))); const ln = new THREE.Line(g, blue); ln.userData = { kind: 'dim', cid }; ln.renderOrder = 10020; inst._dim3D.add(ln); };
544
+ const vx = pr.x - pc.x, vy = pr.y - pc.y; const L = Math.hypot(vx, vy) || 1; const rx = vx / L, ry = vy / L; const nx = -ry, ny = rx;
545
+ // Support both {dr,dp} and generic {du,dv}
546
+ let dr = 0, dp = 0;
547
+ if (off && (off.dr !== undefined || off.dp !== undefined)) {
548
+ dr = Number(off.dr) || 0; dp = Number(off.dp) || 0;
549
+ } else {
550
+ const du = Number(off.du) || 0; const dv = Number(off.dv) || 0;
551
+ dr = du * rx + dv * ry; dp = du * nx + dv * ny;
552
+ }
553
+ const elbow = { u: pr.x + rx * dr, v: pr.y + ry * dr }; const dogleg = { u: elbow.u + nx * dp, v: elbow.v + ny * dp };
554
+ add([{ u: pc.x, v: pc.y }, { u: pr.x, v: pr.y }]); add([{ u: pr.x, v: pr.y }, elbow]); add([elbow, dogleg]);
555
+ const ah = 0.06; const s = 0.6; const tip = { u: pr.x, v: pr.y }; const A = { u: tip.u - rx * ah + nx * ah * 0.6, v: tip.v - ry * ah + ny * ah * 0.6 }; const B = { u: tip.u - rx * ah - nx * ah * 0.6, v: tip.v - ry * ah - ny * ah * 0.6 };
556
+ add([tip, A]); add([tip, B]);
557
+ }
558
+
559
+ function dimAngle3D(inst, p0, p1, p2, p3, cid, I, color = 0x69a8ff, valueDeg = null) {
560
+ // Offset for label drag: translates the arc center together with the label
561
+ const off = inst._dimOffsets.get(cid) || { du: 0, dv: 0 };
562
+ const X = inst._lock.basis.x, Y = inst._lock.basis.y, O = inst._lock.basis.origin; const P = (u, v) => new THREE.Vector3().copy(O).addScaledVector(X, u).addScaledVector(Y, v);
563
+
564
+ // Unit direction of both lines
565
+ const d1 = new THREE.Vector2(p1.x - p0.x, p1.y - p0.y);
566
+ const d2 = new THREE.Vector2(p3.x - p2.x, p3.y - p2.y);
567
+ if (d1.lengthSq() < 1e-12 || d2.lengthSq() < 1e-12) return; // degenerate
568
+ d1.normalize(); d2.normalize();
569
+
570
+ // Base orientation from first line; arc direction sign from the raw difference
571
+ let a0 = Math.atan2(d1.y, d1.x), a1 = Math.atan2(d2.y, d2.x);
572
+ let signedDelta = a1 - a0; while (signedDelta <= -Math.PI) signedDelta += 2 * Math.PI; while (signedDelta > Math.PI) signedDelta -= 2 * Math.PI;
573
+ const defaultDeltaDeg = THREE.MathUtils.euclideanModulo(THREE.MathUtils.radToDeg(a1 - a0), 360);
574
+ let targetDeg;
575
+ if (typeof valueDeg === 'number' && Number.isFinite(valueDeg)) {
576
+ const absVal = Math.abs(valueDeg);
577
+ targetDeg = absVal % 360;
578
+ if (targetDeg < 1e-6 && absVal > 0) targetDeg = 360; // allow full-circle measurements
579
+ } else {
580
+ targetDeg = defaultDeltaDeg;
581
+ }
582
+ if (targetDeg < 1e-6) targetDeg = 1e-6; // keep arc visible
583
+
584
+ let dirSign = Math.sign(signedDelta) || 1;
585
+ if (targetDeg > 180 && targetDeg < 360 - 1e-6) dirSign = -dirSign;
586
+ let d = THREE.MathUtils.degToRad(targetDeg) * dirSign;
587
+
588
+ // Clamp to [0, 2π]
589
+ const twoPi = Math.PI * 2; if (Math.abs(d) > twoPi) d = Math.sign(d) * (twoPi - 1e-6);
590
+
591
+ // Screen-scaled radius and arrow size so it stays visible at any zoom
592
+ const rect = inst.viewer.renderer.domElement.getBoundingClientRect();
593
+ const wpp = worldPerPixel(inst.viewer.camera, rect.width, rect.height);
594
+ const baseR = Math.max(0.3, wpp * 24);
595
+ const ah = Math.max(0.06, wpp * 6);
596
+ // Keep arc centered at the lines' intersection; use label offset magnitude to set radius
597
+ const du = Number(off.du) || 0, dv = Number(off.dv) || 0;
598
+ const r = baseR + Math.hypot(du, dv);
599
+ const cx = I.x, cy = I.y;
600
+
601
+ // Choose the arc side so the arc (+ arrows) are on the same side as the label.
602
+ // Compare label direction with the arc bisector; if it's closer to the opposite
603
+ // bisector, flip the start by PI which mirrors the arc side while preserving span.
604
+ let aStart = a0; // base start
605
+ if (du !== 0 || dv !== 0) {
606
+ const labelAng = Math.atan2(dv, du); // direction from center to label offset
607
+ const angNorm = (a) => { const t = 2 * Math.PI; a %= t; return a < 0 ? a + t : a; };
608
+ const angDiff = (a, b) => { let x = angNorm(a - b); if (x > Math.PI) x = 2 * Math.PI - x; return Math.abs(x); };
609
+ const bisector = aStart + d * 0.5;
610
+ const bisectorOpp = bisector + Math.PI;
611
+ if (angDiff(labelAng, bisectorOpp) + 1e-6 < angDiff(labelAng, bisector)) {
612
+ aStart += Math.PI; // flip arc side
613
+ }
614
+ }
615
+
616
+ // Author the arc polyline
617
+ const segs = 48; // smoother arc
618
+ const uvs = []; for (let i = 0; i <= segs; i++) { const t = aStart + d * (i / segs); uvs.push({ u: cx + Math.cos(t) * r, v: cy + Math.sin(t) * r }); }
619
+ const blue = new THREE.LineBasicMaterial({ color, depthTest: false, depthWrite: false, transparent: true });
620
+ const g = new THREE.BufferGeometry().setFromPoints(uvs.map(q => P(q.u, q.v)));
621
+ const ln = new THREE.Line(g, blue); ln.userData = { kind: 'dim', cid }; ln.renderOrder = 10020; inst._dim3D.add(ln);
622
+
623
+ // Persist geometry info for label centering at arc midpoint
624
+ try {
625
+ const midAng = aStart + d * 0.5;
626
+ const midU = cx + Math.cos(midAng) * r;
627
+ const midV = cy + Math.sin(midAng) * r;
628
+ if (!inst._dimAngleGeom) inst._dimAngleGeom = new Map();
629
+ inst._dimAngleGeom.set(cid, { cx, cy, r, aStart, d, midU, midV });
630
+ } catch {}
631
+
632
+ // Arrowheads at both arc ends (tangential). Make them face the arc span
633
+ // so the two arrowheads are oriented towards each other.
634
+ const s = 0.6; const addArrowUV = (t, dir = 1) => {
635
+ const tx = (-Math.sin(t)) * dir, ty = (Math.cos(t)) * dir;
636
+ const wx = -ty, wy = tx;
637
+ const tip = { u: cx + Math.cos(t) * r, v: cy + Math.sin(t) * r };
638
+ const A = { u: tip.u + tx * ah + wx * ah * s, v: tip.v + ty * ah + wy * ah * s };
639
+ const B = { u: tip.u + tx * ah - wx * ah * s, v: tip.v + ty * ah - wy * ah * s };
640
+ const gg1 = new THREE.BufferGeometry().setFromPoints([P(tip.u, tip.v), P(A.u, A.v)]);
641
+ const gg2 = new THREE.BufferGeometry().setFromPoints([P(tip.u, tip.v), P(B.u, B.v)]);
642
+ const la = new THREE.Line(gg1, blue.clone()); const lb = new THREE.Line(gg2, blue.clone());
643
+ la.renderOrder = 10020; lb.renderOrder = 10020;
644
+ inst._dim3D.add(la); inst._dim3D.add(lb);
645
+ };
646
+ // Orient arrowheads to face each other along the drawn arc
647
+ const dirStart = d >= 0 ? +1 : -1;
648
+ const dirEnd = -dirStart;
649
+ addArrowUV(aStart, dirStart);
650
+ addArrowUV(aStart + d, dirEnd);
651
+ }
652
+
653
+ function worldPerPixel(camera, width, height) {
654
+ if (camera && camera.isOrthographicCamera) {
655
+ const zoom = typeof camera.zoom === 'number' && camera.zoom > 0 ? camera.zoom : 1;
656
+ const wppX = (camera.right - camera.left) / (width * zoom);
657
+ const wppY = (camera.top - camera.bottom) / (height * zoom);
658
+ return Math.max(wppX, wppY);
659
+ }
660
+ const dist = camera.position.length();
661
+ const fovRad = (camera.fov * Math.PI) / 180;
662
+ return (2 * Math.tan(fovRad / 2) * dist) / height;
663
+ }
664
+
665
+ // Robust 2D infinite-line intersection (returns point even if segments don't overlap)
666
+ function intersect(A, B, C, D) {
667
+ const r = { x: B.x - A.x, y: B.y - A.y };
668
+ const s = { x: D.x - C.x, y: D.y - C.y };
669
+ const rxs = r.x * s.y - r.y * s.x;
670
+ // Parallel or nearly parallel: fall back to A to avoid NaNs
671
+ if (Math.abs(rxs) < 1e-12) return { x: A.x, y: A.y };
672
+ const t = ((C.x - A.x) * s.y - (C.y - A.y) * s.x) / rxs;
673
+ return { x: A.x + t * r.x, y: A.y + t * r.y };
674
+ }