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,704 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
import { calculateAngle, rotatePoint, distance, roundToDecimals } from "./mathHelpersMod.js";
|
|
3
|
+
let tolerance = 0.00001;
|
|
4
|
+
const constraintFunctions = [];
|
|
5
|
+
|
|
6
|
+
const normalizeAngle = (angle) => ((angle % 360) + 360) % 360;
|
|
7
|
+
const shortestAngleDelta = (target, current) => {
|
|
8
|
+
const delta = normalizeAngle(target - current);
|
|
9
|
+
return (delta > 180) ? delta - 360 : delta;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
(constraintFunctions["━"] = function (solverObject, constraint, points, constraintValue) {
|
|
14
|
+
// Horizontal constraint
|
|
15
|
+
// test if the points are already on the same horizontal line with a tolerance
|
|
16
|
+
if (Math.abs(points[0].y - points[1].y) < tolerance) {
|
|
17
|
+
constraint.error = null;
|
|
18
|
+
} else {
|
|
19
|
+
constraint.error = `Horizontal constraint not satisfied
|
|
20
|
+
${points[0].y} != ${points[1].y}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!points[0].fixed && !points[1].fixed) {
|
|
24
|
+
const avgY = (points[0].y + points[1].y) / 2;
|
|
25
|
+
points[0].y = avgY;
|
|
26
|
+
points[1].y = avgY;
|
|
27
|
+
} else if (!points[0].fixed) {
|
|
28
|
+
points[0].y = points[1].y;
|
|
29
|
+
} else if (!points[1].fixed) {
|
|
30
|
+
points[1].y = points[0].y;
|
|
31
|
+
}
|
|
32
|
+
}).hints = {
|
|
33
|
+
commandTooltip: "Horizontal Constraint",
|
|
34
|
+
pointsRequired: 2,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
(constraintFunctions["│"] = function (solverObject, constraint, points, constraintValue) {
|
|
40
|
+
// Vertical constraint
|
|
41
|
+
// test if the points are already on the same vertical line with a tolerance
|
|
42
|
+
if (Math.abs(points[0].x - points[1].x) < tolerance * 2) {
|
|
43
|
+
constraint.error = null;
|
|
44
|
+
} else {
|
|
45
|
+
constraint.error = `Vertical constraint not satisfied
|
|
46
|
+
${points[0].x} != ${points[1].x}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!points[0].fixed && !points[1].fixed) {
|
|
50
|
+
const avgX = (points[0].x + points[1].x) / 2;
|
|
51
|
+
points[0].x = avgX;
|
|
52
|
+
points[1].x = avgX;
|
|
53
|
+
} else if (!points[0].fixed) {
|
|
54
|
+
points[0].x = points[1].x;
|
|
55
|
+
} else if (!points[1].fixed) {
|
|
56
|
+
points[1].x = points[0].x;
|
|
57
|
+
}
|
|
58
|
+
}).hints = {
|
|
59
|
+
commandTooltip: "Vertical Constraint",
|
|
60
|
+
pointsRequired: 2,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
(constraintFunctions["⟺"] = function (solverObject, constraint, points, constraintValue) {
|
|
65
|
+
// Distance constraint with movement limiting
|
|
66
|
+
const [pointA, pointB] = points;
|
|
67
|
+
let targetDistance = constraintValue;
|
|
68
|
+
let dx = pointB.x - pointA.x;
|
|
69
|
+
let dy = pointB.y - pointA.y;
|
|
70
|
+
let currentDistance = distance(pointA, pointB);
|
|
71
|
+
|
|
72
|
+
//console.log(constraintValue);
|
|
73
|
+
|
|
74
|
+
if (isNaN(constraintValue) | constraintValue == undefined | constraintValue == null) {
|
|
75
|
+
targetDistance = currentDistance;
|
|
76
|
+
constraint.value = currentDistance;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
let diff = roundToDecimals(Math.abs(targetDistance) - currentDistance, 4);
|
|
82
|
+
//console.log(diff);
|
|
83
|
+
if (Math.abs(diff) === 0) {
|
|
84
|
+
constraint.error = null;
|
|
85
|
+
return;
|
|
86
|
+
} else {
|
|
87
|
+
constraint.error = `Distance constraint not satisfied
|
|
88
|
+
${targetDistance} != ${currentDistance}`;
|
|
89
|
+
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (currentDistance === 0) {
|
|
93
|
+
currentDistance = 1; // Avoid division by zero
|
|
94
|
+
dx = 1;
|
|
95
|
+
dy = 1;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const ratio = diff / currentDistance;
|
|
99
|
+
|
|
100
|
+
let offsetX = dx * ratio * 0.5;
|
|
101
|
+
let offsetY = dy * ratio * 0.5;
|
|
102
|
+
|
|
103
|
+
const direction = targetDistance >= 0 ? 1 : -1;
|
|
104
|
+
|
|
105
|
+
// Limiting the movement
|
|
106
|
+
const maxMove = 1;
|
|
107
|
+
const moveDistance = Math.sqrt(offsetX * offsetX + offsetY * offsetY) || tolerance;
|
|
108
|
+
if (moveDistance > maxMove) {
|
|
109
|
+
const scale = maxMove / moveDistance;
|
|
110
|
+
offsetX *= scale;
|
|
111
|
+
offsetY *= scale;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!pointA.fixed && !pointB.fixed) {
|
|
115
|
+
pointA.x -= offsetX * direction;
|
|
116
|
+
pointA.y -= offsetY * direction;
|
|
117
|
+
pointB.x += offsetX * direction;
|
|
118
|
+
pointB.y += offsetY * direction;
|
|
119
|
+
} else if (!pointA.fixed) {
|
|
120
|
+
pointA.x -= offsetX * 2 * direction;
|
|
121
|
+
pointA.y -= offsetY * 2 * direction;
|
|
122
|
+
} else if (!pointB.fixed) {
|
|
123
|
+
pointB.x += offsetX * 2 * direction;
|
|
124
|
+
pointB.y += offsetY * 2 * direction;
|
|
125
|
+
} else {
|
|
126
|
+
return constraint.error = `points ${pointA.id} and ${pointB.id} are both fixed`;
|
|
127
|
+
}
|
|
128
|
+
return;
|
|
129
|
+
}).hints = {
|
|
130
|
+
commandTooltip: "Distance Constraint",
|
|
131
|
+
pointsRequired: 2,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
(constraintFunctions["⇌"] = function (solverObject, constraint, points, constraintValue) {
|
|
138
|
+
// Equal Distance constraint
|
|
139
|
+
const [pointA, pointB, pointC, pointD] = points;
|
|
140
|
+
|
|
141
|
+
// check if either line has a distance constraint applied to it
|
|
142
|
+
// if so, then the line is not moving
|
|
143
|
+
let line1DistanceConstraint = solverObject.constraints.find(c => c.type === "⟺" && c.points.includes(pointA.id) && c.points.includes(pointB.id));
|
|
144
|
+
let line2DistanceConstraint = solverObject.constraints.find(c => c.type === "⟺" && c.points.includes(pointC.id) && c.points.includes(pointD.id));
|
|
145
|
+
|
|
146
|
+
let avgDistance = null;
|
|
147
|
+
let line1moving = false;
|
|
148
|
+
let line2moving = false;
|
|
149
|
+
if (!(line1DistanceConstraint) && !(line2DistanceConstraint)) {
|
|
150
|
+
// Calculate the current distances
|
|
151
|
+
const distanceAB = Math.sqrt(Math.pow(pointB.x - pointA.x, 2) + Math.pow(pointB.y - pointA.y, 2));
|
|
152
|
+
const distanceCD = Math.sqrt(Math.pow(pointD.x - pointC.x, 2) + Math.pow(pointD.y - pointC.y, 2));
|
|
153
|
+
avgDistance = (distanceAB + distanceCD) / 2;
|
|
154
|
+
line1moving = true;
|
|
155
|
+
line2moving = true;
|
|
156
|
+
} else if (line1DistanceConstraint && !line2DistanceConstraint) {
|
|
157
|
+
avgDistance = line1DistanceConstraint.value;
|
|
158
|
+
line2moving = true;
|
|
159
|
+
} else if (line2DistanceConstraint && !line1DistanceConstraint) {
|
|
160
|
+
avgDistance = line2DistanceConstraint.value;
|
|
161
|
+
line1moving = true;
|
|
162
|
+
} else if (line1DistanceConstraint && line2DistanceConstraint) {
|
|
163
|
+
//console.log(constraint, "Both lines have a distance constraint applied to them")
|
|
164
|
+
return constraint.error = "Both lines have a distance constraint applied to them";
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
if (line1moving) {
|
|
169
|
+
let result1 = constraintFunctions["⟺"](solverObject, constraint, [pointA, pointB], avgDistance);
|
|
170
|
+
if (result1) return result1;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (line2moving) {
|
|
174
|
+
let result2 = constraintFunctions["⟺"](solverObject, constraint, [pointC, pointD], avgDistance);
|
|
175
|
+
if (result2) return result2;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
}).hints = {
|
|
179
|
+
commandTooltip: "Equal Distance Constraint",
|
|
180
|
+
pointsRequired: 4,
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
(constraintFunctions["∥"] = function (solverObject, constraint, points, constraintValue) {
|
|
184
|
+
// Parallel constraint
|
|
185
|
+
// check if either line has a vertical or horizontal constraint applied to it
|
|
186
|
+
// if so simply apply the vertical or horizontal constraint to the other line
|
|
187
|
+
let line1VerticalConstraint = solverObject.constraints.find(c => c.type === "│" && c.points.includes(points[0].id) && c.points.includes(points[1].id));
|
|
188
|
+
let line1HorizontalConstraint = solverObject.constraints.find(c => c.type === "━" && c.points.includes(points[0].id) && c.points.includes(points[1].id));
|
|
189
|
+
let line2VerticalConstraint = solverObject.constraints.find(c => c.type === "│" && c.points.includes(points[2].id) && c.points.includes(points[3].id));
|
|
190
|
+
let line2HorizontalConstraint = solverObject.constraints.find(c => c.type === "━" && c.points.includes(points[2].id) && c.points.includes(points[3].id));
|
|
191
|
+
|
|
192
|
+
if (line1VerticalConstraint) {
|
|
193
|
+
if (line2VerticalConstraint) {
|
|
194
|
+
return constraint.error = "Both lines have a vertical constraint applied to them";
|
|
195
|
+
} else if (line2HorizontalConstraint) {
|
|
196
|
+
return constraint.error = "One line has a vertical constraint and the other has a horizontal constraint";
|
|
197
|
+
} else {
|
|
198
|
+
let result = constraintFunctions["│"](solverObject, constraint, [points[2], points[3]], 0);
|
|
199
|
+
if (result) return result;
|
|
200
|
+
}
|
|
201
|
+
} else if (line1HorizontalConstraint) {
|
|
202
|
+
if (line2VerticalConstraint) {
|
|
203
|
+
return constraint.error = "One line has a vertical constraint and the other has a horizontal constraint";
|
|
204
|
+
} else if (line2HorizontalConstraint) {
|
|
205
|
+
return constraint.error = "Both lines have a horizontal constraint applied to them";
|
|
206
|
+
} else {
|
|
207
|
+
let result = constraintFunctions["━"](solverObject, constraint, [points[2], points[3]], 0);
|
|
208
|
+
if (result) return result;
|
|
209
|
+
}
|
|
210
|
+
} else if (line2VerticalConstraint) {
|
|
211
|
+
let result = constraintFunctions["│"](solverObject, constraint, [points[0], points[1]], 0);
|
|
212
|
+
if (result) return result;
|
|
213
|
+
} else if (line2HorizontalConstraint) {
|
|
214
|
+
let result = constraintFunctions["━"](solverObject, constraint, [points[0], points[1]], 0);
|
|
215
|
+
if (result) return result;
|
|
216
|
+
} else {
|
|
217
|
+
// test angle between the lines
|
|
218
|
+
|
|
219
|
+
let line1Angle = calculateAngle(points[0], points[1]);
|
|
220
|
+
let line2Angle = calculateAngle(points[2], points[3]);
|
|
221
|
+
|
|
222
|
+
let angleDifference = (line1Angle - line2Angle);
|
|
223
|
+
angleDifference = (angleDifference + 360) % 360;
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
let newSetAngle = 0;
|
|
228
|
+
if (angleDifference > 90) newSetAngle = 180;
|
|
229
|
+
if (angleDifference > 180) newSetAngle = 180;
|
|
230
|
+
if (angleDifference > 270) newSetAngle = 360;
|
|
231
|
+
|
|
232
|
+
//console.log(angleDifference, newSetAngle);
|
|
233
|
+
return constraintFunctions["∠"](solverObject, constraint, points, newSetAngle)
|
|
234
|
+
}
|
|
235
|
+
}).hints = {
|
|
236
|
+
commandTooltip: "Parallel Constraint",
|
|
237
|
+
pointsRequired: 4,
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
(constraintFunctions["⟂"] = function (solverObject, constraint, points, constraintValue) {
|
|
242
|
+
// Perpendicular constraint
|
|
243
|
+
// check if either line has a vertical or horizontal constraint applied to it
|
|
244
|
+
// if so simply apply the vertical or horizontal constraint to the other line
|
|
245
|
+
let line1VerticalConstraint = solverObject.constraints.find(c => c.type === "│" && c.points.includes(points[0].id) && c.points.includes(points[1].id));
|
|
246
|
+
let line1HorizontalConstraint = solverObject.constraints.find(c => c.type === "━" && c.points.includes(points[0].id) && c.points.includes(points[1].id));
|
|
247
|
+
let line2VerticalConstraint = solverObject.constraints.find(c => c.type === "│" && c.points.includes(points[2].id) && c.points.includes(points[3].id));
|
|
248
|
+
let line2HorizontalConstraint = solverObject.constraints.find(c => c.type === "━" && c.points.includes(points[2].id) && c.points.includes(points[3].id));
|
|
249
|
+
|
|
250
|
+
if (line1VerticalConstraint) {
|
|
251
|
+
if (line2VerticalConstraint) {
|
|
252
|
+
return constraint.error = "Both lines have a vertical constraint applied to them";
|
|
253
|
+
} else if (line2HorizontalConstraint) {
|
|
254
|
+
return constraint.error = "One line has a vertical constraint and the other has a horizontal constraint";
|
|
255
|
+
} else {
|
|
256
|
+
let result = constraintFunctions["━"](solverObject, constraint, [points[2], points[3]], 0);
|
|
257
|
+
if (result) return result;
|
|
258
|
+
}
|
|
259
|
+
} else if (line1HorizontalConstraint) {
|
|
260
|
+
if (line2VerticalConstraint) {
|
|
261
|
+
return constraint.error = "One line has a vertical constraint and the other has a horizontal constraint";
|
|
262
|
+
} else if (line2HorizontalConstraint) {
|
|
263
|
+
return constraint.error = "Both lines have a horizontal constraint applied to them";
|
|
264
|
+
} else {
|
|
265
|
+
let result = constraintFunctions["│"](solverObject, constraint, [points[2], points[3]], 0);
|
|
266
|
+
if (result) return result;
|
|
267
|
+
}
|
|
268
|
+
} else if (line2VerticalConstraint) {
|
|
269
|
+
let result = constraintFunctions["━"](solverObject, constraint, [points[0], points[1]], 0);
|
|
270
|
+
if (result) return result;
|
|
271
|
+
} else if (line2HorizontalConstraint) {
|
|
272
|
+
let result = constraintFunctions["│"](solverObject, constraint, [points[0], points[1]], 0);
|
|
273
|
+
if (result) return result;
|
|
274
|
+
} else {
|
|
275
|
+
|
|
276
|
+
let p1, p2, p3, p4;
|
|
277
|
+
|
|
278
|
+
[p1, p2, p3, p4] = points;
|
|
279
|
+
|
|
280
|
+
let line1Angle = calculateAngle(p1, p2);
|
|
281
|
+
let line2Angle = calculateAngle(p3, p4);
|
|
282
|
+
let differenceBetweenAngles = line1Angle - line2Angle;
|
|
283
|
+
|
|
284
|
+
differenceBetweenAngles = (differenceBetweenAngles + 360) % 360;
|
|
285
|
+
|
|
286
|
+
let newTargetAngle;
|
|
287
|
+
|
|
288
|
+
if (differenceBetweenAngles <= 180) {
|
|
289
|
+
newTargetAngle = 90;
|
|
290
|
+
} else {
|
|
291
|
+
newTargetAngle = 270;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
//console.log("current values", differenceBetweenAngles, newTargetAngle)
|
|
295
|
+
|
|
296
|
+
return constraintFunctions["∠"](solverObject, constraint, points, newTargetAngle);
|
|
297
|
+
}
|
|
298
|
+
}).hints = {
|
|
299
|
+
commandTooltip: "Perpendicular Constraint",
|
|
300
|
+
pointsRequired: 4,
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
(constraintFunctions["∠"] = function (solverObject, constraint, points, constraintValue) {
|
|
305
|
+
// Angle constraint
|
|
306
|
+
const [p1, p2, p3, p4] = points;
|
|
307
|
+
|
|
308
|
+
const line1Angle = calculateAngle(p1, p2);
|
|
309
|
+
const line2Angle = calculateAngle(p3, p4);
|
|
310
|
+
const differenceBetweenAngles = line1Angle - line2Angle;
|
|
311
|
+
|
|
312
|
+
if (constraint.value == null) {
|
|
313
|
+
// Seed with the current measured angle (normalize into [0, 360))
|
|
314
|
+
constraint.value = roundToDecimals(normalizeAngle(differenceBetweenAngles), 4);
|
|
315
|
+
// return; // Don't return, allow solving to happen immediately (e.g. if constraintValue provided)
|
|
316
|
+
} else if (constraint.value < 0) {
|
|
317
|
+
constraint.value = Math.abs(constraint.value);
|
|
318
|
+
constraint.points = [constraint.points[2], constraint.points[3], constraint.points[1], constraint.points[0]];
|
|
319
|
+
return;
|
|
320
|
+
} else if (constraint.value > 360) {
|
|
321
|
+
constraint.value = normalizeAngle(constraint.value);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const currentAngle = normalizeAngle(differenceBetweenAngles);
|
|
326
|
+
let desiredAngle = Number.isFinite(constraintValue) ? constraintValue : parseFloat(constraint.value);
|
|
327
|
+
if (!Number.isFinite(desiredAngle)) desiredAngle = currentAngle;
|
|
328
|
+
const targetAngle = normalizeAngle(desiredAngle);
|
|
329
|
+
|
|
330
|
+
const deltaRaw = shortestAngleDelta(targetAngle, currentAngle);
|
|
331
|
+
|
|
332
|
+
if (Math.abs(deltaRaw) < tolerance) {
|
|
333
|
+
constraint.error = null;
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (Math.abs(deltaRaw) > tolerance) {
|
|
338
|
+
constraint.error = `Angle constraint not satisfied
|
|
339
|
+
${targetAngle} != ${currentAngle}
|
|
340
|
+
Diff: ${Math.abs(deltaRaw).toFixed(4)}
|
|
341
|
+
`;
|
|
342
|
+
} else {
|
|
343
|
+
constraint.error = null;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
let line1Moving = !(p1.fixed && p2.fixed);
|
|
347
|
+
let line2Moving = !(p3.fixed && p4.fixed);
|
|
348
|
+
|
|
349
|
+
// Lines that already have horizontal/vertical constraints should stay put here.
|
|
350
|
+
if (participateInConstraint(solverObject, "━", [p1, p2])) line1Moving = false;
|
|
351
|
+
if (participateInConstraint(solverObject, "━", [p3, p4])) line2Moving = false;
|
|
352
|
+
if (participateInConstraint(solverObject, "│", [p1, p2])) line1Moving = false;
|
|
353
|
+
if (participateInConstraint(solverObject, "│", [p3, p4])) line2Moving = false;
|
|
354
|
+
|
|
355
|
+
if (!line1Moving && !line2Moving) return;
|
|
356
|
+
|
|
357
|
+
const maxStep = 1.5;
|
|
358
|
+
let delta = deltaRaw;
|
|
359
|
+
if (Math.abs(delta) > maxStep) delta = Math.sign(delta) * maxStep;
|
|
360
|
+
|
|
361
|
+
let rotationLine1 = 0;
|
|
362
|
+
let rotationLine2 = 0;
|
|
363
|
+
|
|
364
|
+
if (line1Moving && line2Moving) {
|
|
365
|
+
rotationLine1 = delta / 2;
|
|
366
|
+
rotationLine2 = -delta / 2;
|
|
367
|
+
} else if (line1Moving) {
|
|
368
|
+
rotationLine1 = delta;
|
|
369
|
+
} else if (line2Moving) {
|
|
370
|
+
rotationLine2 = -delta;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (line1Moving && rotationLine1) {
|
|
374
|
+
if (p1.fixed) {
|
|
375
|
+
rotatePoint(p1, p2, rotationLine1);
|
|
376
|
+
} else if (p2.fixed) {
|
|
377
|
+
rotatePoint(p2, p1, rotationLine1);
|
|
378
|
+
} else {
|
|
379
|
+
// Rotate around midpoint to decouple rotation from translation/length changes
|
|
380
|
+
const midX = (p1.x + p2.x) / 2;
|
|
381
|
+
const midY = (p1.y + p2.y) / 2;
|
|
382
|
+
const center = { x: midX, y: midY };
|
|
383
|
+
rotatePoint(center, p1, rotationLine1);
|
|
384
|
+
rotatePoint(center, p2, rotationLine1);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (line2Moving && rotationLine2) {
|
|
389
|
+
if (p3.fixed) {
|
|
390
|
+
rotatePoint(p3, p4, rotationLine2);
|
|
391
|
+
} else if (p4.fixed) {
|
|
392
|
+
rotatePoint(p4, p3, rotationLine2);
|
|
393
|
+
} else {
|
|
394
|
+
// Rotate around midpoint
|
|
395
|
+
const midX = (p3.x + p4.x) / 2;
|
|
396
|
+
const midY = (p3.y + p4.y) / 2;
|
|
397
|
+
const center = { x: midX, y: midY };
|
|
398
|
+
rotatePoint(center, p3, rotationLine2);
|
|
399
|
+
rotatePoint(center, p4, rotationLine2);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return;
|
|
404
|
+
}).hints = {
|
|
405
|
+
commandTooltip: "Angle Constraint",
|
|
406
|
+
pointsRequired: 4,
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
(constraintFunctions["≡"] = function (solverObject, constraint, points, constraintValue) {
|
|
411
|
+
// Coincident constraint
|
|
412
|
+
const [point1, point2] = points;
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
if (point1.fixed && point2.fixed) {
|
|
416
|
+
if (participateInConstraint(solverObject, "⏚", [points[0]]) && participateInConstraint(solverObject, "⏚", [points[1]])) {
|
|
417
|
+
constraint.error = "Both points are fixed";
|
|
418
|
+
}
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (point1.x === point2.x && point1.y === point2.y) {
|
|
423
|
+
// console.log("points are coincident");
|
|
424
|
+
constraint.error = null;
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
if (!point1.fixed && !point2.fixed) {
|
|
428
|
+
// If both points are not fixed, average their coordinates
|
|
429
|
+
const avgX = (point1.x + point2.x) / 2;
|
|
430
|
+
const avgY = (point1.y + point2.y) / 2;
|
|
431
|
+
point1.x = avgX;
|
|
432
|
+
point1.y = avgY;
|
|
433
|
+
point2.x = avgX;
|
|
434
|
+
point2.y = avgY;
|
|
435
|
+
} else if (!point1.fixed) {
|
|
436
|
+
point1.x = point2.x;
|
|
437
|
+
point1.y = point2.y;
|
|
438
|
+
point1.fixed = true;
|
|
439
|
+
} else if (!point2.fixed) {
|
|
440
|
+
point2.x = point1.x;
|
|
441
|
+
point2.y = point1.y;
|
|
442
|
+
point2.fixed = true;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
}
|
|
446
|
+
if (point1.fixed || point2.fixed) {
|
|
447
|
+
point1.fixed = true;
|
|
448
|
+
point2.fixed = true;
|
|
449
|
+
}
|
|
450
|
+
}).hints = {
|
|
451
|
+
commandTooltip: "Coincident Constraint",
|
|
452
|
+
pointsRequired: 2,
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
(constraintFunctions["⏛"] = function (solverObject, constraint, points, constraintValue) {
|
|
458
|
+
const [pointA, pointB, pointC] = points; // Line AB, Point C
|
|
459
|
+
|
|
460
|
+
// Vector AB
|
|
461
|
+
const dx = pointB.x - pointA.x;
|
|
462
|
+
const dy = pointB.y - pointA.y;
|
|
463
|
+
const lenSq = dx * dx + dy * dy;
|
|
464
|
+
|
|
465
|
+
// Handle degenerate line case (A ~= B)
|
|
466
|
+
if (lenSq < tolerance) {
|
|
467
|
+
// Treat as coincident C to A
|
|
468
|
+
const dist = distance(pointA, pointC);
|
|
469
|
+
if (dist > tolerance) {
|
|
470
|
+
constraint.error = `Point on Line: Line is degenerate (points too close) and Point C is not coincident.`;
|
|
471
|
+
// Simple coincident push
|
|
472
|
+
if (!pointC.fixed) {
|
|
473
|
+
pointC.x = pointA.x;
|
|
474
|
+
pointC.y = pointA.y;
|
|
475
|
+
} else if (!pointA.fixed) {
|
|
476
|
+
pointA.x = pointC.x;
|
|
477
|
+
pointA.y = pointC.y;
|
|
478
|
+
// Sync B to A since they are "coincident" in this check
|
|
479
|
+
if (!pointB.fixed) {
|
|
480
|
+
pointB.x = pointC.x;
|
|
481
|
+
pointB.y = pointC.y
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
} else {
|
|
485
|
+
constraint.error = null;
|
|
486
|
+
}
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Project C onto line AB
|
|
491
|
+
// t = Dot(AC, AB) / |AB|^2
|
|
492
|
+
const t = ((pointC.x - pointA.x) * dx + (pointC.y - pointA.y) * dy) / lenSq;
|
|
493
|
+
|
|
494
|
+
// Closest Point on Line
|
|
495
|
+
const projX = pointA.x + t * dx;
|
|
496
|
+
const projY = pointA.y + t * dy;
|
|
497
|
+
|
|
498
|
+
// Error Vector (C -> Proj)
|
|
499
|
+
// We want C to be at Proj. So Error = C - Proj.
|
|
500
|
+
const errX = pointC.x - projX;
|
|
501
|
+
const errY = pointC.y - projY;
|
|
502
|
+
const errDist = Math.sqrt(errX * errX + errY * errY);
|
|
503
|
+
|
|
504
|
+
if (errDist < tolerance) {
|
|
505
|
+
constraint.error = null;
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
constraint.error = `Point on Line not satisfied. Dist: ${errDist.toFixed(4)}`;
|
|
510
|
+
|
|
511
|
+
// Gradients / Distribution
|
|
512
|
+
// To minimize Error^2 = (Cx - Ax - t*dx)^2 + ...
|
|
513
|
+
// Standard iterative geometric projection:
|
|
514
|
+
// Move C towards Proj.
|
|
515
|
+
// Move Line towards C.
|
|
516
|
+
|
|
517
|
+
// Weighting:
|
|
518
|
+
// If all movable, we distribute the move.
|
|
519
|
+
// C moves by -Error.
|
|
520
|
+
// A moves by +Error * (1-t).
|
|
521
|
+
// B moves by +Error * t.
|
|
522
|
+
// (This effectively rotates/moves the line based on the lever arm t)
|
|
523
|
+
|
|
524
|
+
let wA = !pointA.fixed ? 1 : 0;
|
|
525
|
+
let wB = !pointB.fixed ? 1 : 0;
|
|
526
|
+
let wC = !pointC.fixed ? 1 : 0;
|
|
527
|
+
|
|
528
|
+
// Normalize weights?
|
|
529
|
+
// Actually, we can just apply the delta directly with a damping/learning rate or just full Newton step geometry.
|
|
530
|
+
// Geometric projection is usually stable with full steps if not conflicting.
|
|
531
|
+
// However, since we have 3 points sharing the error correction:
|
|
532
|
+
// If we move C full step, error is 0. If we move A/B full step, error is 0.
|
|
533
|
+
// We should split it.
|
|
534
|
+
|
|
535
|
+
// Simplified: Just assume roughly equal contribution capability?
|
|
536
|
+
// Let's use specific "Position Based Dynamics" style constraints.
|
|
537
|
+
// Inverse Masses: wA, wB, wC.
|
|
538
|
+
// Jacobian J for C is N (normal). J for A is -(1-t)N. J for B is -tN.
|
|
539
|
+
// Lambda = -Constraint / sum(w * J^2).
|
|
540
|
+
// J^2 roughly 1 for C. (1-t)^2 for A. t^2 for B.
|
|
541
|
+
|
|
542
|
+
let denom = 0;
|
|
543
|
+
if (!pointC.fixed) denom += 1;
|
|
544
|
+
if (!pointA.fixed) denom += (1 - t) * (1 - t);
|
|
545
|
+
if (!pointB.fixed) denom += t * t;
|
|
546
|
+
|
|
547
|
+
if (denom === 0) return; // All fixed
|
|
548
|
+
|
|
549
|
+
// Relaxation factor (can use 1.0 for direct projection, but 0.8 helps stability)
|
|
550
|
+
const k = 1.0;
|
|
551
|
+
|
|
552
|
+
// Vector to correct error: (-errX, -errY)
|
|
553
|
+
// C contributes: 1 * deltaC = -err
|
|
554
|
+
// A contributes: -(1-t) * deltaA = -err
|
|
555
|
+
// B contributes: -t * deltaB = -err
|
|
556
|
+
|
|
557
|
+
// Common scalar lambda
|
|
558
|
+
// We want sum(changes) to cancel error.
|
|
559
|
+
// Actually, let's just use the direct formulas from PBD for Point-Segment distance.
|
|
560
|
+
// corrC = - (w_c / sum) * error
|
|
561
|
+
// corrA = + (w_a * (1-t) / sum) * error
|
|
562
|
+
// corrB = + (w_a * t / sum) * error
|
|
563
|
+
|
|
564
|
+
// In our case error vector E = (errX, errY) = C - Proj.
|
|
565
|
+
// We want to displace points so C' becomes Proj'.
|
|
566
|
+
|
|
567
|
+
const factor = k / denom;
|
|
568
|
+
|
|
569
|
+
// Parallel expansion force (from analytical gradient of distance metric)
|
|
570
|
+
// Helps avoid line collapse by encouraging length increase to reduce angular error.
|
|
571
|
+
// Magnitude ~ Error / Length.
|
|
572
|
+
// Only apply if we are moving the line points.
|
|
573
|
+
let expansionX = 0;
|
|
574
|
+
let expansionY = 0;
|
|
575
|
+
if ((!pointA.fixed || !pointB.fixed) && lenSq > tolerance) {
|
|
576
|
+
// Direction B-A
|
|
577
|
+
const len = Math.sqrt(lenSq);
|
|
578
|
+
const ux = dx / len;
|
|
579
|
+
const uy = dy / len;
|
|
580
|
+
|
|
581
|
+
// Force magnitude: errDist / len.
|
|
582
|
+
// We dampen it slightly to avoid over-expansion instabilities.
|
|
583
|
+
const expForce = (errDist / len) * 0.1 * factor;
|
|
584
|
+
|
|
585
|
+
expansionX = ux * expForce;
|
|
586
|
+
expansionY = uy * expForce;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
if (!pointC.fixed) {
|
|
590
|
+
pointC.x -= errX * factor;
|
|
591
|
+
pointC.y -= errY * factor;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (!pointA.fixed) {
|
|
595
|
+
pointA.x += errX * (1 - t) * factor;
|
|
596
|
+
pointA.y += errY * (1 - t) * factor;
|
|
597
|
+
|
|
598
|
+
// Push A away from B (negative dir)
|
|
599
|
+
pointA.x -= expansionX;
|
|
600
|
+
pointA.y -= expansionY;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (!pointB.fixed) {
|
|
604
|
+
pointB.x += errX * t * factor;
|
|
605
|
+
pointB.y += errY * t * factor;
|
|
606
|
+
|
|
607
|
+
// Push B away from A (positive dir)
|
|
608
|
+
pointB.x += expansionX;
|
|
609
|
+
pointB.y += expansionY;
|
|
610
|
+
}
|
|
611
|
+
}).hints = {
|
|
612
|
+
commandTooltip: "Point on Line Constraint",
|
|
613
|
+
pointsRequired: 3,
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
// Midpoint constraint with bidirectional update
|
|
617
|
+
(constraintFunctions["⋯"] = function (solverObject, constraint, points, constraintValue) {
|
|
618
|
+
// Gracefully change the name of the constraint to upgrade from old files if needed
|
|
619
|
+
if (constraint.type === "⋱") constraint.type = "⋯";
|
|
620
|
+
|
|
621
|
+
const [pointA, pointB, pointC] = points; // C is the midpoint of A and B
|
|
622
|
+
|
|
623
|
+
// Constraint equation: 2*C - A - B = 0
|
|
624
|
+
// We treat this as a vector equation and project the error.
|
|
625
|
+
|
|
626
|
+
// Calculate current residual (error)
|
|
627
|
+
const rx = 2 * pointC.x - pointA.x - pointB.x;
|
|
628
|
+
const ry = 2 * pointC.y - pointA.y - pointB.y;
|
|
629
|
+
|
|
630
|
+
// Check satisfaction
|
|
631
|
+
if (Math.abs(rx) < tolerance && Math.abs(ry) < tolerance) {
|
|
632
|
+
constraint.error = null;
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
constraint.error = `Midpoint constraint not satisfied. Error: ${Math.hypot(rx, ry).toFixed(4)}`;
|
|
637
|
+
|
|
638
|
+
// Gradients of internal function f = 2C - A - B
|
|
639
|
+
// grad(C) = 2, grad(A) = -1, grad(B) = -1
|
|
640
|
+
// We assume equal weights for "movability" but respect fixed status.
|
|
641
|
+
|
|
642
|
+
let denom = 0;
|
|
643
|
+
if (!pointA.fixed) denom += 1; // (-1)^2
|
|
644
|
+
if (!pointB.fixed) denom += 1; // (-1)^2
|
|
645
|
+
if (!pointC.fixed) denom += 4; // (2)^2
|
|
646
|
+
|
|
647
|
+
if (denom === 0) {
|
|
648
|
+
constraint.error = "All points fixed in Midpoint constraint";
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Lagrange multiplier step (Newton step for linear constraint)
|
|
653
|
+
// alpha = - Error / sum(grad^2)
|
|
654
|
+
const alphaX = -rx / denom;
|
|
655
|
+
const alphaY = -ry / denom;
|
|
656
|
+
|
|
657
|
+
// Update points
|
|
658
|
+
// New Pos = Old Pos + alpha * grad
|
|
659
|
+
if (!pointA.fixed) {
|
|
660
|
+
pointA.x += alphaX * (-1);
|
|
661
|
+
pointA.y += alphaY * (-1);
|
|
662
|
+
}
|
|
663
|
+
if (!pointB.fixed) {
|
|
664
|
+
pointB.x += alphaX * (-1);
|
|
665
|
+
pointB.y += alphaY * (-1);
|
|
666
|
+
}
|
|
667
|
+
if (!pointC.fixed) {
|
|
668
|
+
pointC.x += alphaX * (2);
|
|
669
|
+
pointC.y += alphaY * (2);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
}).hints = {
|
|
673
|
+
commandTooltip: "Midpoint Constraint",
|
|
674
|
+
pointsRequired: 3,
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
//gracefully change the name of the constraint
|
|
678
|
+
//constraintFunctions["⋱"] = constraintFunctions["⋯"];
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
(constraintFunctions["⏚"] = function (solverObject, constraint, points, constraintValue) {
|
|
683
|
+
// Fixed constraint
|
|
684
|
+
points[0].fixed = true;
|
|
685
|
+
}).hints = {
|
|
686
|
+
commandTooltip: "Fix Point",
|
|
687
|
+
pointsRequired: 1,
|
|
688
|
+
};
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
export const constraints = {
|
|
692
|
+
tolerance,
|
|
693
|
+
constraintFunctions,
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
function participateInConstraint(solverObject, constraintType, points) {
|
|
701
|
+
return solverObject.constraints.some(c => {
|
|
702
|
+
return c.type === constraintType && points.every(point => c.points.includes(point.id));
|
|
703
|
+
});
|
|
704
|
+
}
|