brep-io-kernel 1.0.0-ci.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +32 -0
- package/README.md +157 -0
- package/dist-kernel/brep-kernel.js +74699 -0
- package/package.json +58 -0
- package/src/BREP/AssemblyComponent.js +42 -0
- package/src/BREP/BREP.js +43 -0
- package/src/BREP/BetterSolid.js +805 -0
- package/src/BREP/Edge.js +103 -0
- package/src/BREP/Extrude.js +403 -0
- package/src/BREP/Face.js +187 -0
- package/src/BREP/MeshRepairer.js +634 -0
- package/src/BREP/OffsetShellSolid.js +614 -0
- package/src/BREP/PointCloudWrap.js +302 -0
- package/src/BREP/Revolve.js +345 -0
- package/src/BREP/SolidMethods/authoring.js +112 -0
- package/src/BREP/SolidMethods/booleanOps.js +230 -0
- package/src/BREP/SolidMethods/chamfer.js +122 -0
- package/src/BREP/SolidMethods/edgeResolution.js +25 -0
- package/src/BREP/SolidMethods/fillet.js +792 -0
- package/src/BREP/SolidMethods/index.js +72 -0
- package/src/BREP/SolidMethods/io.js +105 -0
- package/src/BREP/SolidMethods/lifecycle.js +103 -0
- package/src/BREP/SolidMethods/manifoldOps.js +375 -0
- package/src/BREP/SolidMethods/meshCleanup.js +2512 -0
- package/src/BREP/SolidMethods/meshQueries.js +264 -0
- package/src/BREP/SolidMethods/metadata.js +106 -0
- package/src/BREP/SolidMethods/metrics.js +51 -0
- package/src/BREP/SolidMethods/transforms.js +361 -0
- package/src/BREP/SolidMethods/visualize.js +508 -0
- package/src/BREP/SolidShared.js +26 -0
- package/src/BREP/Sweep.js +1596 -0
- package/src/BREP/Tube.js +857 -0
- package/src/BREP/Vertex.js +43 -0
- package/src/BREP/applyBooleanOperation.js +704 -0
- package/src/BREP/boundsUtils.js +48 -0
- package/src/BREP/chamfer.js +551 -0
- package/src/BREP/edgePolylineUtils.js +85 -0
- package/src/BREP/fillets/common.js +388 -0
- package/src/BREP/fillets/fillet.js +1422 -0
- package/src/BREP/fillets/filletGeometry.js +15 -0
- package/src/BREP/fillets/inset.js +389 -0
- package/src/BREP/fillets/offsetHelper.js +143 -0
- package/src/BREP/fillets/outset.js +88 -0
- package/src/BREP/helix.js +193 -0
- package/src/BREP/meshToBrep.js +234 -0
- package/src/BREP/primitives.js +279 -0
- package/src/BREP/setupManifold.js +71 -0
- package/src/BREP/threadGeometry.js +1120 -0
- package/src/BREP/triangleUtils.js +8 -0
- package/src/BREP/triangulate.js +608 -0
- package/src/FeatureRegistry.js +183 -0
- package/src/PartHistory.js +1132 -0
- package/src/UI/AccordionWidget.js +292 -0
- package/src/UI/CADmaterials.js +850 -0
- package/src/UI/EnvMonacoEditor.js +522 -0
- package/src/UI/FloatingWindow.js +396 -0
- package/src/UI/HistoryWidget.js +457 -0
- package/src/UI/MainToolbar.js +131 -0
- package/src/UI/ModelLibraryView.js +194 -0
- package/src/UI/OrthoCameraIdle.js +206 -0
- package/src/UI/PluginsWidget.js +280 -0
- package/src/UI/SceneListing.js +606 -0
- package/src/UI/SelectionFilter.js +629 -0
- package/src/UI/ViewCube.js +389 -0
- package/src/UI/assembly/AssemblyConstraintCollectionWidget.js +329 -0
- package/src/UI/assembly/AssemblyConstraintControlsWidget.js +282 -0
- package/src/UI/assembly/AssemblyConstraintsWidget.css +292 -0
- package/src/UI/assembly/AssemblyConstraintsWidget.js +1373 -0
- package/src/UI/assembly/constraintFaceUtils.js +115 -0
- package/src/UI/assembly/constraintHighlightUtils.js +70 -0
- package/src/UI/assembly/constraintLabelUtils.js +31 -0
- package/src/UI/assembly/constraintPointUtils.js +64 -0
- package/src/UI/assembly/constraintSelectionUtils.js +185 -0
- package/src/UI/assembly/constraintStatusUtils.js +142 -0
- package/src/UI/componentSelectorModal.js +240 -0
- package/src/UI/controls/CombinedTransformControls.js +386 -0
- package/src/UI/dialogs.js +351 -0
- package/src/UI/expressionsManager.js +100 -0
- package/src/UI/featureDialogWidgets/booleanField.js +25 -0
- package/src/UI/featureDialogWidgets/booleanOperationField.js +97 -0
- package/src/UI/featureDialogWidgets/buttonField.js +45 -0
- package/src/UI/featureDialogWidgets/componentSelectorField.js +102 -0
- package/src/UI/featureDialogWidgets/defaultField.js +23 -0
- package/src/UI/featureDialogWidgets/fileField.js +66 -0
- package/src/UI/featureDialogWidgets/index.js +34 -0
- package/src/UI/featureDialogWidgets/numberField.js +165 -0
- package/src/UI/featureDialogWidgets/optionsField.js +33 -0
- package/src/UI/featureDialogWidgets/referenceSelectionField.js +208 -0
- package/src/UI/featureDialogWidgets/stringField.js +24 -0
- package/src/UI/featureDialogWidgets/textareaField.js +28 -0
- package/src/UI/featureDialogWidgets/threadDesignationField.js +160 -0
- package/src/UI/featureDialogWidgets/transformField.js +252 -0
- package/src/UI/featureDialogWidgets/utils.js +43 -0
- package/src/UI/featureDialogWidgets/vec3Field.js +133 -0
- package/src/UI/featureDialogs.js +1414 -0
- package/src/UI/fileManagerWidget.js +615 -0
- package/src/UI/history/HistoryCollectionWidget.js +1294 -0
- package/src/UI/history/historyCollectionWidget.css.js +257 -0
- package/src/UI/history/historyDisplayInfo.js +133 -0
- package/src/UI/mobile.js +28 -0
- package/src/UI/objectDump.js +442 -0
- package/src/UI/pmi/AnnotationCollectionWidget.js +120 -0
- package/src/UI/pmi/AnnotationHistory.js +353 -0
- package/src/UI/pmi/AnnotationRegistry.js +90 -0
- package/src/UI/pmi/BaseAnnotation.js +269 -0
- package/src/UI/pmi/LabelOverlay.css +102 -0
- package/src/UI/pmi/LabelOverlay.js +191 -0
- package/src/UI/pmi/PMIMode.js +1550 -0
- package/src/UI/pmi/PMIViewsWidget.js +1098 -0
- package/src/UI/pmi/annUtils.js +729 -0
- package/src/UI/pmi/dimensions/AngleDimensionAnnotation.js +647 -0
- package/src/UI/pmi/dimensions/ExplodeBodyAnnotation.js +507 -0
- package/src/UI/pmi/dimensions/HoleCalloutAnnotation.js +462 -0
- package/src/UI/pmi/dimensions/LeaderAnnotation.js +403 -0
- package/src/UI/pmi/dimensions/LinearDimensionAnnotation.js +532 -0
- package/src/UI/pmi/dimensions/NoteAnnotation.js +110 -0
- package/src/UI/pmi/dimensions/RadialDimensionAnnotation.js +659 -0
- package/src/UI/pmi/pmiStyle.js +44 -0
- package/src/UI/sketcher/SketchMode3D.js +4095 -0
- package/src/UI/sketcher/dimensions.js +674 -0
- package/src/UI/sketcher/glyphs.js +236 -0
- package/src/UI/sketcher/highlights.js +60 -0
- package/src/UI/toolbarButtons/aboutButton.js +5 -0
- package/src/UI/toolbarButtons/exportButton.js +609 -0
- package/src/UI/toolbarButtons/flatPatternButton.js +307 -0
- package/src/UI/toolbarButtons/importButton.js +160 -0
- package/src/UI/toolbarButtons/inspectorToggleButton.js +12 -0
- package/src/UI/toolbarButtons/metadataButton.js +1063 -0
- package/src/UI/toolbarButtons/orientToFaceButton.js +114 -0
- package/src/UI/toolbarButtons/registerDefaultButtons.js +46 -0
- package/src/UI/toolbarButtons/saveButton.js +99 -0
- package/src/UI/toolbarButtons/scriptRunnerButton.js +302 -0
- package/src/UI/toolbarButtons/testsButton.js +26 -0
- package/src/UI/toolbarButtons/undoRedoButtons.js +25 -0
- package/src/UI/toolbarButtons/wireframeToggleButton.js +5 -0
- package/src/UI/toolbarButtons/zoomToFitButton.js +5 -0
- package/src/UI/triangleDebuggerWindow.js +945 -0
- package/src/UI/viewer.js +4228 -0
- package/src/assemblyConstraints/AssemblyConstraintHistory.js +1576 -0
- package/src/assemblyConstraints/AssemblyConstraintRegistry.js +120 -0
- package/src/assemblyConstraints/BaseAssemblyConstraint.js +66 -0
- package/src/assemblyConstraints/constraintExpressionUtils.js +35 -0
- package/src/assemblyConstraints/constraintUtils/parallelAlignment.js +676 -0
- package/src/assemblyConstraints/constraints/AngleConstraint.js +485 -0
- package/src/assemblyConstraints/constraints/CoincidentConstraint.js +194 -0
- package/src/assemblyConstraints/constraints/DistanceConstraint.js +616 -0
- package/src/assemblyConstraints/constraints/FixedConstraint.js +78 -0
- package/src/assemblyConstraints/constraints/ParallelConstraint.js +252 -0
- package/src/assemblyConstraints/constraints/TouchAlignConstraint.js +961 -0
- package/src/core/entities/HistoryCollectionBase.js +72 -0
- package/src/core/entities/ListEntityBase.js +109 -0
- package/src/core/entities/schemaProcesser.js +121 -0
- package/src/exporters/sheetMetalFlatPattern.js +659 -0
- package/src/exporters/sheetMetalUnfold.js +862 -0
- package/src/exporters/step.js +1135 -0
- package/src/exporters/threeMF.js +575 -0
- package/src/features/assemblyComponent/AssemblyComponentFeature.js +780 -0
- package/src/features/boolean/BooleanFeature.js +94 -0
- package/src/features/chamfer/ChamferFeature.js +116 -0
- package/src/features/datium/DatiumFeature.js +80 -0
- package/src/features/edgeFeatureUtils.js +41 -0
- package/src/features/extrude/ExtrudeFeature.js +143 -0
- package/src/features/fillet/FilletFeature.js +197 -0
- package/src/features/helix/HelixFeature.js +405 -0
- package/src/features/hole/HoleFeature.js +1050 -0
- package/src/features/hole/screwClearance.js +86 -0
- package/src/features/hole/threadDesignationCatalog.js +149 -0
- package/src/features/imageHeightSolid/ImageHeightmapSolidFeature.js +463 -0
- package/src/features/imageToFace/ImageToFaceFeature.js +727 -0
- package/src/features/imageToFace/imageEditor.js +1270 -0
- package/src/features/imageToFace/traceUtils.js +971 -0
- package/src/features/import3dModel/Import3dModelFeature.js +151 -0
- package/src/features/loft/LoftFeature.js +605 -0
- package/src/features/mirror/MirrorFeature.js +151 -0
- package/src/features/offsetFace/OffsetFaceFeature.js +370 -0
- package/src/features/offsetShell/OffsetShellFeature.js +89 -0
- package/src/features/overlapCleanup/OverlapCleanupFeature.js +85 -0
- package/src/features/pattern/PatternFeature.js +275 -0
- package/src/features/patternLinear/PatternLinearFeature.js +120 -0
- package/src/features/patternRadial/PatternRadialFeature.js +186 -0
- package/src/features/plane/PlaneFeature.js +154 -0
- package/src/features/primitiveCone/primitiveConeFeature.js +99 -0
- package/src/features/primitiveCube/primitiveCubeFeature.js +70 -0
- package/src/features/primitiveCylinder/primitiveCylinderFeature.js +91 -0
- package/src/features/primitivePyramid/primitivePyramidFeature.js +72 -0
- package/src/features/primitiveSphere/primitiveSphereFeature.js +62 -0
- package/src/features/primitiveTorus/primitiveTorusFeature.js +109 -0
- package/src/features/remesh/RemeshFeature.js +97 -0
- package/src/features/revolve/RevolveFeature.js +111 -0
- package/src/features/selectionUtils.js +118 -0
- package/src/features/sheetMetal/SheetMetalContourFlangeFeature.js +1656 -0
- package/src/features/sheetMetal/SheetMetalCutoutFeature.js +1056 -0
- package/src/features/sheetMetal/SheetMetalFlangeFeature.js +1568 -0
- package/src/features/sheetMetal/SheetMetalHemFeature.js +43 -0
- package/src/features/sheetMetal/SheetMetalObject.js +141 -0
- package/src/features/sheetMetal/SheetMetalTabFeature.js +176 -0
- package/src/features/sheetMetal/UNFOLD_NEUTRAL_REQUIREMENTS.md +153 -0
- package/src/features/sheetMetal/contour-flange-rebuild-spec.md +261 -0
- package/src/features/sheetMetal/profileUtils.js +25 -0
- package/src/features/sheetMetal/sheetMetalCleanup.js +9 -0
- package/src/features/sheetMetal/sheetMetalFaceTypes.js +146 -0
- package/src/features/sheetMetal/sheetMetalMetadata.js +165 -0
- package/src/features/sheetMetal/sheetMetalPipeline.js +169 -0
- package/src/features/sheetMetal/sheetMetalProfileUtils.js +216 -0
- package/src/features/sheetMetal/sheetMetalTabUtils.js +29 -0
- package/src/features/sheetMetal/sheetMetalTree.js +210 -0
- package/src/features/sketch/SketchFeature.js +955 -0
- package/src/features/sketch/sketchSolver2D/ConstraintEngine.js +800 -0
- package/src/features/sketch/sketchSolver2D/constraintDefinitions.js +704 -0
- package/src/features/sketch/sketchSolver2D/mathHelpersMod.js +307 -0
- package/src/features/spline/SplineEditorSession.js +988 -0
- package/src/features/spline/SplineFeature.js +1388 -0
- package/src/features/spline/splineUtils.js +218 -0
- package/src/features/sweep/SweepFeature.js +110 -0
- package/src/features/transform/TransformFeature.js +152 -0
- package/src/features/tube/TubeFeature.js +635 -0
- package/src/fs.proxy.js +625 -0
- package/src/idbStorage.js +254 -0
- package/src/index.js +12 -0
- package/src/main.js +15 -0
- package/src/metadataManager.js +64 -0
- package/src/path.proxy.js +277 -0
- package/src/plugins/ghLoader.worker.js +151 -0
- package/src/plugins/pluginManager.js +286 -0
- package/src/pmi/PMIViewsManager.js +134 -0
- package/src/services/componentLibrary.js +198 -0
- package/src/tests/ConsoleCapture.js +189 -0
- package/src/tests/S7-diagnostics-2025-12-23T18-37-23-570Z.json +630 -0
- package/src/tests/browserTests.js +597 -0
- package/src/tests/debugBoolean.js +225 -0
- package/src/tests/partFiles/badBoolean.json +957 -0
- package/src/tests/partFiles/extrudeTest.json +88 -0
- package/src/tests/partFiles/filletFail.json +58 -0
- package/src/tests/partFiles/import_TEst.part.part.json +646 -0
- package/src/tests/partFiles/sheetMetalHem.BREP.json +734 -0
- package/src/tests/test_boolean_subtract.js +27 -0
- package/src/tests/test_chamfer.js +17 -0
- package/src/tests/test_extrudeFace.js +24 -0
- package/src/tests/test_fillet.js +17 -0
- package/src/tests/test_fillet_nonClosed.js +45 -0
- package/src/tests/test_filletsMoreDifficult.js +46 -0
- package/src/tests/test_history_features_basic.js +149 -0
- package/src/tests/test_hole.js +282 -0
- package/src/tests/test_mirror.js +16 -0
- package/src/tests/test_offsetShellGrouping.js +85 -0
- package/src/tests/test_plane.js +4 -0
- package/src/tests/test_primitiveCone.js +11 -0
- package/src/tests/test_primitiveCube.js +7 -0
- package/src/tests/test_primitiveCylinder.js +8 -0
- package/src/tests/test_primitivePyramid.js +9 -0
- package/src/tests/test_primitiveSphere.js +17 -0
- package/src/tests/test_primitiveTorus.js +21 -0
- package/src/tests/test_pushFace.js +126 -0
- package/src/tests/test_sheetMetalContourFlange.js +125 -0
- package/src/tests/test_sheetMetal_features.js +80 -0
- package/src/tests/test_sketch_openLoop.js +45 -0
- package/src/tests/test_solidMetrics.js +58 -0
- package/src/tests/test_stlLoader.js +1889 -0
- package/src/tests/test_sweepFace.js +55 -0
- package/src/tests/test_tube.js +45 -0
- package/src/tests/test_tube_closedLoop.js +67 -0
- package/src/tests/tests.js +493 -0
- package/src/tools/assemblyConstraintDialogCapturePage.js +56 -0
- package/src/tools/dialogCapturePageFactory.js +227 -0
- package/src/tools/featureDialogCapturePage.js +47 -0
- package/src/tools/pmiAnnotationDialogCapturePage.js +60 -0
- package/src/utils/axisHelpers.js +99 -0
- package/src/utils/deepClone.js +69 -0
- package/src/utils/geometryTolerance.js +37 -0
- package/src/utils/normalizeTypeString.js +8 -0
- package/src/utils/xformMath.js +51 -0
|
@@ -0,0 +1,800 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { constraints } from "./constraintDefinitions.js";
|
|
4
|
+
import { distance, calculateAngle } from "./mathHelpersMod.js";
|
|
5
|
+
|
|
6
|
+
// === Constraint function table ===
|
|
7
|
+
const constraintFunctions = constraints.constraintFunctions;
|
|
8
|
+
|
|
9
|
+
// === Engine that performs numeric solving on a sketch snapshot ===
|
|
10
|
+
class ConstraintEngine {
|
|
11
|
+
constructor(sketchJSON) {
|
|
12
|
+
const sketch = JSON.parse(sketchJSON);
|
|
13
|
+
this.points = sketch.points.map(p => new Point(p.id, p.x, p.y, p.fixed));
|
|
14
|
+
this.geometries = sketch.geometries || [];
|
|
15
|
+
this.constraints = sketch.constraints || [];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
processConstraintsOfType(type) {
|
|
19
|
+
const list = (type === "all")
|
|
20
|
+
? this.constraints
|
|
21
|
+
: this.constraints.filter(c => c.type === type);
|
|
22
|
+
|
|
23
|
+
for (const constraint of list) {
|
|
24
|
+
constraint.status = "";
|
|
25
|
+
const constraintValue = parseFloat(constraint.value);
|
|
26
|
+
const points = constraint.points.map(id => this.points.find(p => p.id === id));
|
|
27
|
+
const before = JSON.stringify(points);
|
|
28
|
+
|
|
29
|
+
if (constraint.previousPointValues !== undefined &&
|
|
30
|
+
constraint.previousPointValues === before &&
|
|
31
|
+
constraint.status === "solved") continue;
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
constraintFunctions[constraint.type](this, constraint, points, constraintValue);
|
|
35
|
+
} catch (e) {
|
|
36
|
+
// Keep solving other constraints; record the error on this constraint
|
|
37
|
+
constraint.error = e?.message || String(e);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const after = JSON.stringify(points);
|
|
41
|
+
if (before === after) {
|
|
42
|
+
constraint.status = "solved";
|
|
43
|
+
constraint.previousPointValues = after;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async tidyDecimalsOfPoints(decimalsPlaces = 4, resetFixed = true) {
|
|
49
|
+
for (const p of this.points) {
|
|
50
|
+
if (resetFixed) p.fixed = false;
|
|
51
|
+
|
|
52
|
+
if (typeof p.x === "string") p.x = parseFloat(p.x);
|
|
53
|
+
if (typeof p.y === "string") p.y = parseFloat(p.y);
|
|
54
|
+
|
|
55
|
+
if (p.x === null || p.x === undefined || Number.isNaN(p.x)) p.x = 0;
|
|
56
|
+
if (p.y === null || p.y === undefined || Number.isNaN(p.y)) p.y = 0;
|
|
57
|
+
|
|
58
|
+
const k = Math.pow(10, decimalsPlaces);
|
|
59
|
+
p.x = Math.round(p.x * k) / k;
|
|
60
|
+
p.y = Math.round(p.y * k) / k;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
solve(iterations = 100) {
|
|
65
|
+
const decimalsPlaces = 6;
|
|
66
|
+
|
|
67
|
+
// Implied constraints for certain geometry types (e.g., arcs, bezier splines)
|
|
68
|
+
let nextTempId =
|
|
69
|
+
Math.max(0, ...this.constraints.map(c => Number.isFinite(+c.id) ? +c.id : 0)) + 1;
|
|
70
|
+
const pushTemp = (type, points) => {
|
|
71
|
+
this.constraints.push({
|
|
72
|
+
id: nextTempId++,
|
|
73
|
+
type,
|
|
74
|
+
points,
|
|
75
|
+
temporary: true,
|
|
76
|
+
labelX: 0,
|
|
77
|
+
labelY: 0
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
this.geometries.forEach(g => {
|
|
82
|
+
if (g.type === "arc") {
|
|
83
|
+
// Insert a temporary equal-chord constraint between (0-1) and (0-2)
|
|
84
|
+
pushTemp("⇌", [g.points[0], g.points[1], g.points[0], g.points[2]]);
|
|
85
|
+
} else if (g.type === "bezier" && Array.isArray(g.points) && g.points.length >= 4) {
|
|
86
|
+
// Keep internal spline anchors colinear with their adjacent control points
|
|
87
|
+
const ids = g.points || [];
|
|
88
|
+
const segCount = Math.floor((ids.length - 1) / 3);
|
|
89
|
+
const lastAnchorIndex = segCount * 3;
|
|
90
|
+
for (let i = 3; i < lastAnchorIndex; i += 3) {
|
|
91
|
+
const prevHandle = ids[i - 1];
|
|
92
|
+
const anchor = ids[i];
|
|
93
|
+
const nextHandle = ids[i + 1];
|
|
94
|
+
if (prevHandle == null || anchor == null || nextHandle == null) continue;
|
|
95
|
+
if (prevHandle === anchor || nextHandle === anchor || prevHandle === nextHandle) continue;
|
|
96
|
+
pushTemp("⏛", [prevHandle, nextHandle, anchor]);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
this.tidyDecimalsOfPoints(decimalsPlaces, true);
|
|
102
|
+
|
|
103
|
+
// Ground first, then everything
|
|
104
|
+
this.processConstraintsOfType("⏚");
|
|
105
|
+
this.processConstraintsOfType("all");
|
|
106
|
+
|
|
107
|
+
const order = [
|
|
108
|
+
"⏛", "━", "│", "⋯",
|
|
109
|
+
"⟺", "⇌", "∠", "⟂", "∥",
|
|
110
|
+
"⇌", "⟺", "⇌", "⟺", "⏛", "━", "│", // repeated passes for convergence
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
let prev = JSON.stringify(this.points);
|
|
114
|
+
let converged = false;
|
|
115
|
+
|
|
116
|
+
for (let i = 0; i < iterations; i++) {
|
|
117
|
+
for (const t of order) {
|
|
118
|
+
this.processConstraintsOfType(t);
|
|
119
|
+
this.processConstraintsOfType("≡"); // keep coincident snapping frequently
|
|
120
|
+
this.processConstraintsOfType("━");
|
|
121
|
+
this.processConstraintsOfType("|");
|
|
122
|
+
this.tidyDecimalsOfPoints(decimalsPlaces, false);
|
|
123
|
+
this.processConstraintsOfType("━");
|
|
124
|
+
this.processConstraintsOfType("|");
|
|
125
|
+
this.tidyDecimalsOfPoints(decimalsPlaces, false);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const cur = JSON.stringify(this.points);
|
|
129
|
+
if (cur === prev) {
|
|
130
|
+
converged = true;
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
prev = cur;
|
|
134
|
+
|
|
135
|
+
// Movement throttling
|
|
136
|
+
const maxMove = 0.5;
|
|
137
|
+
for (let j = 0; j < this.points.length; j++) {
|
|
138
|
+
const p = this.points[j];
|
|
139
|
+
const last = JSON.parse(prev)[j];
|
|
140
|
+
const dx = p.x - last.x;
|
|
141
|
+
const dy = p.y - last.y;
|
|
142
|
+
const d = Math.hypot(dx, dy);
|
|
143
|
+
if (d > maxMove) {
|
|
144
|
+
const s = maxMove / d;
|
|
145
|
+
p.x = last.x + dx * s;
|
|
146
|
+
p.y = last.y + dy * s;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Return a new sketch object mirroring input structure
|
|
152
|
+
const updatedSketch = {
|
|
153
|
+
points: this.points.map(p => ({ id: p.id, x: p.x, y: p.y, fixed: p.fixed })),
|
|
154
|
+
geometries: this.geometries,
|
|
155
|
+
constraints: this.constraints.filter(c => !c.temporary) // drop temporaries
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
return JSON.parse(JSON.stringify(updatedSketch));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
class Point {
|
|
163
|
+
constructor(id, x, y, fixed = false) {
|
|
164
|
+
this.id = id;
|
|
165
|
+
this.x = x;
|
|
166
|
+
this.y = y;
|
|
167
|
+
this.fixed = fixed;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// === Public API class ===
|
|
172
|
+
export class ConstraintSolver {
|
|
173
|
+
/**
|
|
174
|
+
* @param {Object} opts
|
|
175
|
+
* @param {Object} [opts.sketch] initial sketch {points, geometries, constraints}
|
|
176
|
+
* @param {Function} [opts.notifyUser] (message, type) => void
|
|
177
|
+
* @param {Function} [opts.updateCanvas] () => void
|
|
178
|
+
* @param {Function} [opts.getSelectionItems] () => Array<{type:"point"|"geometry", id:number}>
|
|
179
|
+
* @param {Object} [opts.appState] external state to mirror mode/type/requiredSelections
|
|
180
|
+
*/
|
|
181
|
+
constructor(opts = {}) {
|
|
182
|
+
this.hooks = {
|
|
183
|
+
notifyUser: typeof opts.notifyUser === "function" ? opts.notifyUser : (m) => { /* no-op in headless */ },
|
|
184
|
+
updateCanvas: typeof opts.updateCanvas === "function" ? opts.updateCanvas : () => { },
|
|
185
|
+
getSelectionItems: typeof opts.getSelectionItems === "function" ? opts.getSelectionItems : () => []
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
this.appState = opts.appState || { mode: "", type: "", requiredSelections: 0 };
|
|
189
|
+
|
|
190
|
+
this.sketchObject = opts.sketch ? sanitizeSketch(opts.sketch) : {
|
|
191
|
+
points: [{ id: 0, x: 0, y: 0, fixed: true }],
|
|
192
|
+
geometries: [],
|
|
193
|
+
constraints: [{ id: 0, type: "⏚", points: [0] }]
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
this._paused = false;
|
|
197
|
+
this._pauseReason = "";
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ---------- Solver control ----------
|
|
201
|
+
pause(reason = "") {
|
|
202
|
+
this._paused = true;
|
|
203
|
+
this._pauseReason = reason || "";
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
resume() {
|
|
207
|
+
this._paused = false;
|
|
208
|
+
this._pauseReason = "";
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
isPaused() { return !!this._paused; }
|
|
212
|
+
|
|
213
|
+
// ---------- Core solve ----------
|
|
214
|
+
solveSketch(iterations = null) {
|
|
215
|
+
if (this._paused) {
|
|
216
|
+
return this.sketchObject;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const iters = iterations === "full"
|
|
220
|
+
? this.fullSolve()
|
|
221
|
+
: (iterations == null ? this.defaultLoops() : iterations);
|
|
222
|
+
|
|
223
|
+
const engine = new ConstraintEngine(JSON.stringify(this.sketchObject));
|
|
224
|
+
const solved = engine.solve(iters);
|
|
225
|
+
console.log(`Solver completed in ${iters} iterations.`);
|
|
226
|
+
console.log(solved.constraints);
|
|
227
|
+
this.sketchObject = solved;
|
|
228
|
+
return this.sketchObject;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
defaultLoops() { return 1500; }
|
|
232
|
+
fullSolve() { return 2000; }
|
|
233
|
+
|
|
234
|
+
// ---------- Accessors ----------
|
|
235
|
+
getPointById(id) {
|
|
236
|
+
return this.sketchObject.points.find(p => p.id === parseInt(id));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ---------- Edit operations (formerly exported functions) ----------
|
|
240
|
+
removePointById(id) {
|
|
241
|
+
id = parseInt(id);
|
|
242
|
+
if (id === 0) return;
|
|
243
|
+
|
|
244
|
+
this.sketchObject.points = this.sketchObject.points.filter(p => p.id !== id);
|
|
245
|
+
|
|
246
|
+
// Remove geometries referencing the point
|
|
247
|
+
this.sketchObject.geometries = this.sketchObject.geometries.filter(g => !g.points.includes(id));
|
|
248
|
+
|
|
249
|
+
// Remove constraints referencing the point
|
|
250
|
+
this.sketchObject.constraints = this.sketchObject.constraints.filter(c => !c.points.includes(id));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
removeGeometryById(id) {
|
|
254
|
+
id = parseInt(id);
|
|
255
|
+
if (id === 0) return;
|
|
256
|
+
|
|
257
|
+
this.sketchObject.geometries = this.sketchObject.geometries.filter(g => parseInt(g.id) !== id);
|
|
258
|
+
|
|
259
|
+
// If any constraint stores geometryId, prune those as well
|
|
260
|
+
this.sketchObject.constraints = this.sketchObject.constraints.filter(c => c.geometryId !== id);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
removeConstraintById(id) {
|
|
264
|
+
id = parseInt(id);
|
|
265
|
+
this.sketchObject.constraints = this.sketchObject.constraints.filter(c => parseInt(c.id) !== id);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
toggleConstruction() {
|
|
269
|
+
const items = this.hooks.getSelectionItems();
|
|
270
|
+
if (!items || items.length === 0) return;
|
|
271
|
+
|
|
272
|
+
for (const item of items) {
|
|
273
|
+
if (item.type === "geometry") {
|
|
274
|
+
const g = this.sketchObject.geometries.find(x => x.id === parseInt(item.id));
|
|
275
|
+
if (!g) continue;
|
|
276
|
+
if (g.construction === undefined) g.construction = false;
|
|
277
|
+
g.construction = !g.construction;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
this.hooks.updateCanvas(false);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
geometryCreateLine() {
|
|
284
|
+
this.appState.mode = "createGeometry";
|
|
285
|
+
this.appState.type = "line";
|
|
286
|
+
this.appState.requiredSelections = 2;
|
|
287
|
+
this.createGeometry("line");
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
geometryCreateCircle() {
|
|
291
|
+
this.appState.mode = "createGeometry";
|
|
292
|
+
this.appState.type = "circle";
|
|
293
|
+
this.appState.requiredSelections = 2;
|
|
294
|
+
this.createGeometry("circle");
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
geometryCreateArc() {
|
|
298
|
+
this.appState.mode = "createGeometry";
|
|
299
|
+
this.appState.type = "arc";
|
|
300
|
+
this.appState.requiredSelections = 3;
|
|
301
|
+
this.createGeometry("arc");
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Create rectangle from two selected points (opposite corners in UV plane)
|
|
305
|
+
// Produces 4 line geometries, 4 coincident constraints (one per corner), and 3 perpendicular constraints
|
|
306
|
+
geometryCreateRectangle() {
|
|
307
|
+
this.appState.mode = "createGeometry";
|
|
308
|
+
this.appState.type = "rectangle";
|
|
309
|
+
this.appState.requiredSelections = 2;
|
|
310
|
+
|
|
311
|
+
// Collect two points from selection
|
|
312
|
+
const items = this.hooks.getSelectionItems();
|
|
313
|
+
const selPoints = [];
|
|
314
|
+
for (const it of items || []) {
|
|
315
|
+
if (it.type === "point") {
|
|
316
|
+
const p = this.sketchObject.points.find(x => x.id === parseInt(it.id));
|
|
317
|
+
if (p) selPoints.push(p);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
if (selPoints.length !== 2) return false;
|
|
321
|
+
|
|
322
|
+
const pA = selPoints[0]; // one corner
|
|
323
|
+
const pC = selPoints[1]; // opposite corner
|
|
324
|
+
|
|
325
|
+
const u1 = pA.x, v1 = pA.y;
|
|
326
|
+
const u2 = pC.x, v2 = pC.y;
|
|
327
|
+
|
|
328
|
+
const nextPointId = () => Math.max(0, ...this.sketchObject.points.map(p => +p.id || 0)) + 1;
|
|
329
|
+
const nextGeoId = () => Math.max(0, ...this.sketchObject.geometries.map(g => +g.id || 0)) + 1;
|
|
330
|
+
const nextConId = () => Math.max(0, ...this.sketchObject.constraints.map(c => +c.id || 0)) + 1;
|
|
331
|
+
|
|
332
|
+
// Create additional corner points at axis-aligned positions
|
|
333
|
+
// We intentionally duplicate endpoints per edge and tie with coincident constraints.
|
|
334
|
+
// Corners: C1(u1,v1)=pA vs pA_other, C2(u2,v1)=c2a,c2b, C3(u2,v2)=pC vs pC_other, C4(u1,v2)=c4a,c4b
|
|
335
|
+
const pA_other = { id: nextPointId(), x: u1, y: v1, fixed: false };
|
|
336
|
+
const c2a = { id: pA_other.id + 1, x: u2, y: v1, fixed: false };
|
|
337
|
+
const c2b = { id: c2a.id + 1, x: u2, y: v1, fixed: false };
|
|
338
|
+
const pC_other = { id: c2b.id + 1, x: u2, y: v2, fixed: false };
|
|
339
|
+
const c4a = { id: pC_other.id + 1, x: u1, y: v2, fixed: false };
|
|
340
|
+
const c4b = { id: c4a.id + 1, x: u1, y: v2, fixed: false };
|
|
341
|
+
|
|
342
|
+
this.sketchObject.points.push(pA_other, c2a, c2b, pC_other, c4a, c4b);
|
|
343
|
+
|
|
344
|
+
// Build 4 line geometries with distinct endpoints per adjacent edge
|
|
345
|
+
const gIds = [];
|
|
346
|
+
const pushLine = (a, b) => {
|
|
347
|
+
const gid = nextGeoId();
|
|
348
|
+
this.sketchObject.geometries.push({ id: gid, type: "line", points: [a, b], construction: false });
|
|
349
|
+
gIds.push(gid);
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
// L1: bottom (C1 -> C2): pA -> c2a
|
|
353
|
+
pushLine(pA.id, c2a.id);
|
|
354
|
+
// L2: right (C2 -> C3): c2b -> pC
|
|
355
|
+
pushLine(c2b.id, pC.id);
|
|
356
|
+
// L3: top (C3 -> C4): pC_other -> c4a
|
|
357
|
+
pushLine(pC_other.id, c4a.id);
|
|
358
|
+
// L4: left (C4 -> C1): c4b -> pA_other
|
|
359
|
+
pushLine(c4b.id, pA_other.id);
|
|
360
|
+
|
|
361
|
+
// Coincident constraints at all four corners
|
|
362
|
+
const pushCoincident = (p1, p2) => {
|
|
363
|
+
const cid = nextConId();
|
|
364
|
+
this.sketchObject.constraints.push({ id: cid, type: "≡", points: [p1, p2] });
|
|
365
|
+
};
|
|
366
|
+
// C1: pA with pA_other
|
|
367
|
+
pushCoincident(pA.id, pA_other.id);
|
|
368
|
+
// C2: c2a with c2b
|
|
369
|
+
pushCoincident(c2a.id, c2b.id);
|
|
370
|
+
// C3: pC with pC_other
|
|
371
|
+
pushCoincident(pC.id, pC_other.id);
|
|
372
|
+
// C4: c4a with c4b
|
|
373
|
+
pushCoincident(c4a.id, c4b.id);
|
|
374
|
+
|
|
375
|
+
// Perpendicular constraints between adjacent sides (3 of them; the 4th is implied)
|
|
376
|
+
const pushPerp = (a, b, c, d) => {
|
|
377
|
+
const cid = nextConId();
|
|
378
|
+
this.sketchObject.constraints.push({ id: cid, type: "⟂", points: [a, b, c, d] });
|
|
379
|
+
};
|
|
380
|
+
// Use endpoints that define each line direction
|
|
381
|
+
// L1(pA,c2a) ⟂ L2(c2b,pC)
|
|
382
|
+
pushPerp(pA.id, c2a.id, c2b.id, pC.id);
|
|
383
|
+
// L2(c2b,pC) ⟂ L3(pC_other,c4a)
|
|
384
|
+
pushPerp(c2b.id, pC.id, pC_other.id, c4a.id);
|
|
385
|
+
// L3(pC_other,c4a) ⟂ L4(c4b,pA_other)
|
|
386
|
+
pushPerp(pC_other.id, c4a.id, c4b.id, pA_other.id);
|
|
387
|
+
|
|
388
|
+
// Solve and update
|
|
389
|
+
this.sketchObject = this.solveSketch("full");
|
|
390
|
+
this.hooks.updateCanvas();
|
|
391
|
+
this.appState.mode = "";
|
|
392
|
+
this.appState.type = "";
|
|
393
|
+
this.appState.requiredSelections = 0;
|
|
394
|
+
return true;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
createGeometry(type, points = []) {
|
|
398
|
+
// Use selection if not provided
|
|
399
|
+
if (points.length === 0) {
|
|
400
|
+
const items = this.hooks.getSelectionItems();
|
|
401
|
+
if (items && items.length > 0) {
|
|
402
|
+
points = [];
|
|
403
|
+
for (const it of items) {
|
|
404
|
+
if (it.type === "point") {
|
|
405
|
+
const p = this.sketchObject.points.find(x => x.id === parseInt(it.id));
|
|
406
|
+
if (p) points.push(p);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (this.appState.requiredSelections && points.length !== this.appState.requiredSelections) {
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
let pointIds;
|
|
417
|
+
if (points.length > 0 && typeof points[0] === "object") {
|
|
418
|
+
pointIds = points.map(p => p.id);
|
|
419
|
+
} else {
|
|
420
|
+
pointIds = points;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (!pointIds || pointIds.length === 0) return false;
|
|
424
|
+
|
|
425
|
+
const maxId = Math.max(0, ...this.sketchObject.geometries.map(geo => +geo.id || 0)) + 1;
|
|
426
|
+
const newGeometry = {
|
|
427
|
+
id: maxId,
|
|
428
|
+
type,
|
|
429
|
+
points: pointIds,
|
|
430
|
+
construction: false
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
this.sketchObject.geometries.push(newGeometry);
|
|
434
|
+
this.hooks.updateCanvas();
|
|
435
|
+
this.appState.mode = "";
|
|
436
|
+
this.appState.type = "";
|
|
437
|
+
this.appState.requiredSelections = 0;
|
|
438
|
+
return true;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
createConstraint(type, currentlySelected = null) {
|
|
442
|
+
const selected = [];
|
|
443
|
+
let geometryType = null;
|
|
444
|
+
|
|
445
|
+
const items = Array.isArray(currentlySelected) ? currentlySelected : this.hooks.getSelectionItems();
|
|
446
|
+
|
|
447
|
+
for (const item of items) {
|
|
448
|
+
if (item.type === "point") {
|
|
449
|
+
const p = this.sketchObject.points.find(pp => pp.id === parseInt(item.id));
|
|
450
|
+
if (p) selected.push(p);
|
|
451
|
+
}
|
|
452
|
+
if (item.type === "geometry") {
|
|
453
|
+
const g = this.sketchObject.geometries.find(gg => gg.id === parseInt(item.id));
|
|
454
|
+
if (!g) continue;
|
|
455
|
+
for (const pid of g.points) {
|
|
456
|
+
const p = this.sketchObject.points.find(pp => pp.id === pid);
|
|
457
|
+
if (p) selected.push(p);
|
|
458
|
+
}
|
|
459
|
+
if (g.type === "arc") selected.pop(); // center + start; omit end for 3pt cases
|
|
460
|
+
geometryType = g.type;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (selected.length === 0) return;
|
|
465
|
+
|
|
466
|
+
const selectedPointIds = selected.map(p => parseInt(p.id));
|
|
467
|
+
|
|
468
|
+
const newConstraint = {
|
|
469
|
+
id: 0,
|
|
470
|
+
type,
|
|
471
|
+
points: selectedPointIds,
|
|
472
|
+
labelX: 0,
|
|
473
|
+
labelY: 0,
|
|
474
|
+
displayStyle: "",
|
|
475
|
+
value: null,
|
|
476
|
+
valueNeedsSetup: true
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
if (selected.length === 1) {
|
|
480
|
+
if (type === "⏚") return this.createAndPushNewConstraint(newConstraint);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (selected.length === 2) {
|
|
484
|
+
if (type === "━") return this.createAndPushNewConstraint(newConstraint);
|
|
485
|
+
if (type === "│") return this.createAndPushNewConstraint(newConstraint);
|
|
486
|
+
if (type === "≡") return this.createAndPushNewConstraint(newConstraint);
|
|
487
|
+
|
|
488
|
+
if (type === "⟺") {
|
|
489
|
+
if (geometryType === "arc" || geometryType === "circle") newConstraint.displayStyle = "radius";
|
|
490
|
+
return this.createAndPushNewConstraint(newConstraint);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (selected.length === 3) {
|
|
495
|
+
if (type === "⏛") return this.createAndPushNewConstraint(newConstraint);
|
|
496
|
+
if (type === "⋯") {
|
|
497
|
+
// If selection is Point + Line (and Point was first), ensure Point is last (Midpoint)
|
|
498
|
+
const hasGeometry = items.some(i => i.type === "geometry");
|
|
499
|
+
if (hasGeometry && items[0]?.type === "point") {
|
|
500
|
+
newConstraint.points = selectedPointIds.slice().reverse();
|
|
501
|
+
}
|
|
502
|
+
return this.createAndPushNewConstraint(newConstraint);
|
|
503
|
+
}
|
|
504
|
+
if (type === "⇌") return this.createAndPushNewConstraint(newConstraint);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (selected.length === 4 || selected.length === 5) {
|
|
508
|
+
if (type === "⏛") {
|
|
509
|
+
// Allow Point-on-Line from two selected lines: use first line as target,
|
|
510
|
+
// constrain both endpoints of the second line onto it (all points colinear).
|
|
511
|
+
const geometries = items.filter(i => i.type === "geometry");
|
|
512
|
+
if (geometries.length === 2) {
|
|
513
|
+
const g0 = this.sketchObject.geometries.find(gg => gg.id === parseInt(geometries[0].id));
|
|
514
|
+
const g1 = this.sketchObject.geometries.find(gg => gg.id === parseInt(geometries[1].id));
|
|
515
|
+
if (g0?.type === "line" && g1?.type === "line" &&
|
|
516
|
+
Array.isArray(g0.points) && g0.points.length >= 2 &&
|
|
517
|
+
Array.isArray(g1.points) && g1.points.length >= 2) {
|
|
518
|
+
const a = this.sketchObject.points.find(p => p.id === g0.points[0]);
|
|
519
|
+
const b = this.sketchObject.points.find(p => p.id === g0.points[1]);
|
|
520
|
+
const c0 = this.sketchObject.points.find(p => p.id === g1.points[0]);
|
|
521
|
+
const c1 = this.sketchObject.points.find(p => p.id === g1.points[1]);
|
|
522
|
+
if (a && b && c0 && c1) {
|
|
523
|
+
const base = {
|
|
524
|
+
type,
|
|
525
|
+
labelX: 0,
|
|
526
|
+
labelY: 0,
|
|
527
|
+
displayStyle: "",
|
|
528
|
+
value: null,
|
|
529
|
+
valueNeedsSetup: true
|
|
530
|
+
};
|
|
531
|
+
const maxId = Math.max(0, ...this.sketchObject.constraints.map(c => +c.id || 0)) + 1;
|
|
532
|
+
const cA = { ...base, id: maxId, points: [a.id, b.id, c0.id] };
|
|
533
|
+
const cB = { ...base, id: maxId + 1, points: [a.id, b.id, c1.id] };
|
|
534
|
+
this.sketchObject.constraints.push(cA, cB);
|
|
535
|
+
this.sketchObject = this.solveSketch("full");
|
|
536
|
+
this.hooks.updateCanvas();
|
|
537
|
+
this.hooks.notifyUser("Constraint added", "info");
|
|
538
|
+
return true;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
if (type === "⟂") {
|
|
544
|
+
// Check if this is a tangent constraint (line + arc/circle)
|
|
545
|
+
const isTangentConstraint = this.#detectTangentConstraint(items);
|
|
546
|
+
|
|
547
|
+
if (isTangentConstraint) {
|
|
548
|
+
// Handle tangent constraint: choose closest line endpoint to circle center
|
|
549
|
+
const optimizedPoints = this.#optimizePointsForTangent(items, selected);
|
|
550
|
+
if (optimizedPoints) {
|
|
551
|
+
newConstraint.points = optimizedPoints;
|
|
552
|
+
return this.createAndPushNewConstraint(newConstraint);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Only proceed with standard perpendicular if we have exactly 4 points (two lines)
|
|
557
|
+
if (selected.length !== 4) {
|
|
558
|
+
this.hooks.updateCanvas();
|
|
559
|
+
this.hooks.notifyUser(
|
|
560
|
+
`Invalid selection for constraint type ${type}\nwith ${selected.length} points.`,
|
|
561
|
+
"warning"
|
|
562
|
+
);
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Standard perpendicular constraint for two lines
|
|
567
|
+
let line1AngleA = calculateAngle(selected[0], selected[1]);
|
|
568
|
+
let line1AngleB = calculateAngle(selected[1], selected[0]);
|
|
569
|
+
let line2Angle = calculateAngle(selected[2], selected[3]);
|
|
570
|
+
|
|
571
|
+
line1AngleA = (line1AngleA + 180) % 360 - 180;
|
|
572
|
+
line1AngleB = (line1AngleB + 180) % 360 - 180;
|
|
573
|
+
line2Angle = (line2Angle + 180) % 360 - 180;
|
|
574
|
+
|
|
575
|
+
let diffA = line1AngleA - line2Angle;
|
|
576
|
+
let diffB = line1AngleB - line2Angle;
|
|
577
|
+
|
|
578
|
+
// Choose orientation closer to 90°
|
|
579
|
+
if (Math.abs(90 - diffA) > Math.abs(90 - diffB)) {
|
|
580
|
+
[newConstraint.points[0], newConstraint.points[1]] = [newConstraint.points[1], newConstraint.points[0]];
|
|
581
|
+
}
|
|
582
|
+
return this.createAndPushNewConstraint(newConstraint);
|
|
583
|
+
}
|
|
584
|
+
if (type === "∥") return this.createAndPushNewConstraint(newConstraint);
|
|
585
|
+
if (type === "∠") {
|
|
586
|
+
// Do NOT set a value on creation. The solver initializes the
|
|
587
|
+
// constraint to the current angle on first evaluation, and the
|
|
588
|
+
// renderer will display the interior arc by default.
|
|
589
|
+
return this.createAndPushNewConstraint(newConstraint);
|
|
590
|
+
}
|
|
591
|
+
if (type === "⇌") return this.createAndPushNewConstraint(newConstraint);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
this.hooks.updateCanvas();
|
|
595
|
+
this.hooks.notifyUser(
|
|
596
|
+
`Invalid selection for constraint type ${type}\nwith ${selected.length} points.`,
|
|
597
|
+
"warning"
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
createAndPushNewConstraint(constraint) {
|
|
602
|
+
const maxId = Math.max(0, ...this.sketchObject.constraints.map(c => +c.id || 0)) + 1;
|
|
603
|
+
constraint.id = maxId;
|
|
604
|
+
constraint.value = (constraint.value === null || constraint.value === undefined)
|
|
605
|
+
? null
|
|
606
|
+
: parseFloat(Number(constraint.value).toFixed(4));
|
|
607
|
+
|
|
608
|
+
this.sketchObject.constraints.push(constraint);
|
|
609
|
+
this.sketchObject = this.solveSketch("full");
|
|
610
|
+
|
|
611
|
+
this.hooks.updateCanvas();
|
|
612
|
+
this.hooks.notifyUser("Constraint added", "info");
|
|
613
|
+
return true;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// ---------- Coincident simplification & cleanup ----------
|
|
617
|
+
simplifyCoincidentConstraints() {
|
|
618
|
+
const data = this.sketchObject;
|
|
619
|
+
const coincidentGroups = {};
|
|
620
|
+
const pointToGroup = {};
|
|
621
|
+
|
|
622
|
+
data.constraints.forEach(constraint => {
|
|
623
|
+
if (constraint.type === "≡") {
|
|
624
|
+
const [p1, p2] = constraint.points;
|
|
625
|
+
if (!coincidentGroups[p1]) coincidentGroups[p1] = new Set();
|
|
626
|
+
if (!coincidentGroups[p2]) coincidentGroups[p2] = new Set();
|
|
627
|
+
coincidentGroups[p1].add(p2);
|
|
628
|
+
coincidentGroups[p2].add(p1);
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
// Merge overlapping groups
|
|
633
|
+
for (const [point, group] of Object.entries(coincidentGroups)) {
|
|
634
|
+
for (const other of group) {
|
|
635
|
+
if (coincidentGroups[other]) {
|
|
636
|
+
for (const p of coincidentGroups[other]) {
|
|
637
|
+
group.add(p);
|
|
638
|
+
coincidentGroups[p] = group;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
for (const [point, group] of Object.entries(coincidentGroups)) {
|
|
645
|
+
const minId = Math.min(...Array.from(group));
|
|
646
|
+
pointToGroup[point] = minId;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Replace IDs in constraints and geometries
|
|
650
|
+
data.constraints.forEach(c => {
|
|
651
|
+
c.points = c.points.map(p => pointToGroup[p] || p);
|
|
652
|
+
});
|
|
653
|
+
data.geometries.forEach(g => {
|
|
654
|
+
g.points = g.points.map(p => pointToGroup[p] || p);
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
this.discardUnusedPoints();
|
|
658
|
+
|
|
659
|
+
// Remove degenerate coincident constraints (same point twice)
|
|
660
|
+
data.constraints = data.constraints.filter(c => !(c.type === "≡" && c.points[0] === c.points[1]));
|
|
661
|
+
|
|
662
|
+
return this.sketchObject;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
discardUnusedPoints() {
|
|
666
|
+
const data = this.sketchObject;
|
|
667
|
+
const used = new Set();
|
|
668
|
+
data.constraints.forEach(c => c.points.forEach(pid => used.add(pid)));
|
|
669
|
+
data.geometries.forEach(g => g.points.forEach(pid => used.add(pid)));
|
|
670
|
+
data.points = data.points.filter(p => used.has(p.id));
|
|
671
|
+
return this.sketchObject;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// Helper method to detect if this is a tangent constraint (line + arc/circle)
|
|
675
|
+
#detectTangentConstraint(items) {
|
|
676
|
+
if (items.length !== 2) return false;
|
|
677
|
+
|
|
678
|
+
const geometries = items.filter(item => item.type === "geometry");
|
|
679
|
+
if (geometries.length !== 2) return false;
|
|
680
|
+
|
|
681
|
+
const lineGeo = geometries.find(item => {
|
|
682
|
+
const g = this.sketchObject.geometries.find(gg => gg.id === parseInt(item.id));
|
|
683
|
+
return g?.type === "line";
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
const circularGeo = geometries.find(item => {
|
|
687
|
+
const g = this.sketchObject.geometries.find(gg => gg.id === parseInt(item.id));
|
|
688
|
+
return g?.type === "arc" || g?.type === "circle";
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
return lineGeo && circularGeo;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// Helper method to choose optimal points for tangent constraint
|
|
695
|
+
#optimizePointsForTangent(items, selected) {
|
|
696
|
+
const geometries = items.filter(item => item.type === "geometry");
|
|
697
|
+
if (geometries.length !== 2) return null;
|
|
698
|
+
|
|
699
|
+
let lineGeo = null;
|
|
700
|
+
let circularGeo = null;
|
|
701
|
+
let linePoints = [];
|
|
702
|
+
let circularPoints = [];
|
|
703
|
+
|
|
704
|
+
// Identify line and circular geometry
|
|
705
|
+
for (const item of geometries) {
|
|
706
|
+
const g = this.sketchObject.geometries.find(gg => gg.id === parseInt(item.id));
|
|
707
|
+
if (!g) continue;
|
|
708
|
+
|
|
709
|
+
if (g.type === "line") {
|
|
710
|
+
lineGeo = g;
|
|
711
|
+
linePoints = g.points.map(pid => this.sketchObject.points.find(p => p.id === pid));
|
|
712
|
+
} else if (g.type === "arc" || g.type === "circle") {
|
|
713
|
+
circularGeo = g;
|
|
714
|
+
circularPoints = g.points.map(pid => this.sketchObject.points.find(p => p.id === pid));
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
if (!lineGeo || !circularGeo || linePoints.length < 2 || circularPoints.length < 1) {
|
|
719
|
+
return null;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// For circular geometry: [0] = center, [1] = start point, [2] = end point (for arc)
|
|
723
|
+
const center = circularPoints[0];
|
|
724
|
+
const lineStart = linePoints[0];
|
|
725
|
+
const lineEnd = linePoints[1];
|
|
726
|
+
|
|
727
|
+
// Get arc/circle points (excluding center)
|
|
728
|
+
const arcPoints = circularPoints.slice(1); // Remove center to get actual arc points
|
|
729
|
+
|
|
730
|
+
if (arcPoints.length === 0) {
|
|
731
|
+
// Fallback for circles - use center only
|
|
732
|
+
return [lineStart.id, lineEnd.id, center.id, center.id];
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// Calculate distance from each arc point to the line
|
|
736
|
+
let closestArcPoint = arcPoints[0];
|
|
737
|
+
let minDistance = this.#distancePointToLine(arcPoints[0], lineStart, lineEnd);
|
|
738
|
+
|
|
739
|
+
for (let i = 1; i < arcPoints.length; i++) {
|
|
740
|
+
const dist = this.#distancePointToLine(arcPoints[i], lineStart, lineEnd);
|
|
741
|
+
if (dist < minDistance) {
|
|
742
|
+
minDistance = dist;
|
|
743
|
+
closestArcPoint = arcPoints[i];
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// Create perpendicular constraint using the line and the radius to closest arc point
|
|
748
|
+
// Points: [line_start, line_end, center, closest_arc_point]
|
|
749
|
+
return [lineStart.id, lineEnd.id, center.id, closestArcPoint.id];
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// Helper method to calculate distance from point to line
|
|
753
|
+
#distancePointToLine(point, lineStart, lineEnd) {
|
|
754
|
+
// Calculate line direction vector
|
|
755
|
+
let dirX = lineEnd.x - lineStart.x;
|
|
756
|
+
let dirY = lineEnd.y - lineStart.y;
|
|
757
|
+
const mag = Math.sqrt(dirX * dirX + dirY * dirY);
|
|
758
|
+
|
|
759
|
+
if (mag === 0) return distance(point, lineStart); // Degenerate line case
|
|
760
|
+
|
|
761
|
+
dirX /= mag; // Normalize
|
|
762
|
+
dirY /= mag;
|
|
763
|
+
|
|
764
|
+
// Vector from line start to point
|
|
765
|
+
const vecX = point.x - lineStart.x;
|
|
766
|
+
const vecY = point.y - lineStart.y;
|
|
767
|
+
|
|
768
|
+
// Project point onto line
|
|
769
|
+
const dot = vecX * dirX + vecY * dirY;
|
|
770
|
+
const projX = lineStart.x + dot * dirX;
|
|
771
|
+
const projY = lineStart.y + dot * dirY;
|
|
772
|
+
|
|
773
|
+
// Calculate distance from point to its projection on the line
|
|
774
|
+
const distX = point.x - projX;
|
|
775
|
+
const distY = point.y - projY;
|
|
776
|
+
return Math.sqrt(distX * distX + distY * distY);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// ---------- Utilities ----------
|
|
781
|
+
function sanitizeSketch(sketch) {
|
|
782
|
+
const s = {
|
|
783
|
+
points: Array.isArray(sketch.points) ? sketch.points.map(p => ({
|
|
784
|
+
id: +p.id, x: +p.x, y: +p.y, fixed: !!p.fixed
|
|
785
|
+
})) : [],
|
|
786
|
+
geometries: Array.isArray(sketch.geometries) ? sketch.geometries.slice() : [],
|
|
787
|
+
constraints: Array.isArray(sketch.constraints) ? sketch.constraints.slice() : []
|
|
788
|
+
};
|
|
789
|
+
|
|
790
|
+
// Ensure at least an origin and ground if empty
|
|
791
|
+
if (s.points.length === 0) s.points.push({ id: 0, x: 0, y: 0, fixed: true });
|
|
792
|
+
if (!s.constraints.some(c => c.type === "⏚")) {
|
|
793
|
+
s.constraints.push({ id: 0, type: "⏚", points: [0] });
|
|
794
|
+
}
|
|
795
|
+
return s;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// Named exports for convenience (optional for consumers)
|
|
799
|
+
// Consumers should primarily instantiate the default export (ConstraintSolver)
|
|
800
|
+
export { ConstraintEngine };
|