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,485 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import { BaseAssemblyConstraint } from '../BaseAssemblyConstraint.js';
|
|
3
|
+
import { ANGLE_TOLERANCE, MAX_ROTATION_PER_ITERATION, resolveParallelSelection } from '../constraintUtils/parallelAlignment.js';
|
|
4
|
+
import { objectRepresentativePoint, getElementDirection } from '../../UI/pmi/annUtils.js';
|
|
5
|
+
|
|
6
|
+
const DEFAULT_ANGLE_LINEAR_TOLERANCE = 1e-12;
|
|
7
|
+
|
|
8
|
+
const inputParamsSchema = {
|
|
9
|
+
id: {
|
|
10
|
+
type: 'string',
|
|
11
|
+
default_value: null,
|
|
12
|
+
hint: 'Unique identifier for the constraint.',
|
|
13
|
+
},
|
|
14
|
+
elements: {
|
|
15
|
+
type: 'reference_selection',
|
|
16
|
+
label: 'Elements',
|
|
17
|
+
hint: 'Select two faces or edges.',
|
|
18
|
+
selectionFilter: ['FACE', 'EDGE'],
|
|
19
|
+
multiple: true,
|
|
20
|
+
minSelections: 2,
|
|
21
|
+
maxSelections: 2,
|
|
22
|
+
},
|
|
23
|
+
angle: {
|
|
24
|
+
type: 'number',
|
|
25
|
+
label: 'Angle (deg)',
|
|
26
|
+
default_value: 90,
|
|
27
|
+
hint: 'Desired signed angle between Element A and Element B in degrees (-360 to 360).',
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
export class AngleConstraint extends BaseAssemblyConstraint {
|
|
33
|
+
static shortName = '∠';
|
|
34
|
+
static longName = '∠ Angle Constraint';
|
|
35
|
+
static constraintType = 'angle';
|
|
36
|
+
static aliases = ['angle', 'angle_between', 'angular', 'ANGL'];
|
|
37
|
+
static inputParamsSchema = inputParamsSchema;
|
|
38
|
+
|
|
39
|
+
constructor(partHistory) {
|
|
40
|
+
super(partHistory);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async solve(context = {}) {
|
|
44
|
+
const pd = this.persistentData = this.persistentData || {};
|
|
45
|
+
const [selA, selB] = selectionPair(this.inputParams);
|
|
46
|
+
const targetAngleValue = Number(this.inputParams.angle ?? 0);
|
|
47
|
+
const targetAngleDeg = clampAndNormalizeAngleDeg(targetAngleValue);
|
|
48
|
+
const targetAngleRad = THREE.MathUtils.degToRad(targetAngleDeg);
|
|
49
|
+
|
|
50
|
+
if (!selA || !selB) {
|
|
51
|
+
pd.status = 'incomplete';
|
|
52
|
+
pd.message = 'Select two references to define the constraint.';
|
|
53
|
+
pd.satisfied = false;
|
|
54
|
+
pd.error = null;
|
|
55
|
+
pd.errorDeg = null;
|
|
56
|
+
pd.lastAppliedRotations = [];
|
|
57
|
+
pd.lastAppliedMoves = [];
|
|
58
|
+
return { ok: false, status: 'incomplete', satisfied: false, applied: false, message: pd.message };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let infoA;
|
|
62
|
+
let infoB;
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
infoA = this.#resolveSelectionInfo(context, selA, 'elements[0]');
|
|
66
|
+
infoB = this.#resolveSelectionInfo(context, selB, 'elements[1]');
|
|
67
|
+
} catch (error) {
|
|
68
|
+
const message = error?.message || 'Unable to resolve selection references.';
|
|
69
|
+
pd.status = 'invalid-selection';
|
|
70
|
+
pd.message = message;
|
|
71
|
+
pd.satisfied = false;
|
|
72
|
+
pd.error = null;
|
|
73
|
+
pd.errorDeg = null;
|
|
74
|
+
pd.lastAppliedRotations = [];
|
|
75
|
+
pd.lastAppliedMoves = [];
|
|
76
|
+
pd.exception = error;
|
|
77
|
+
return { ok: false, status: 'invalid-selection', satisfied: false, applied: false, message, exception: error };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!infoA || !infoB || !infoA.direction || !infoB.direction) {
|
|
81
|
+
const message = 'Unable to resolve directions for the selected references.';
|
|
82
|
+
pd.status = 'invalid-selection';
|
|
83
|
+
pd.message = message;
|
|
84
|
+
pd.satisfied = false;
|
|
85
|
+
pd.error = null;
|
|
86
|
+
pd.errorDeg = null;
|
|
87
|
+
pd.lastAppliedRotations = [];
|
|
88
|
+
pd.lastAppliedMoves = [];
|
|
89
|
+
return { ok: false, status: 'invalid-selection', satisfied: false, applied: false, message };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!infoA.component || !infoB.component) {
|
|
93
|
+
const message = 'Both selections must belong to assembly components.';
|
|
94
|
+
pd.status = 'invalid-selection';
|
|
95
|
+
pd.message = message;
|
|
96
|
+
pd.satisfied = false;
|
|
97
|
+
pd.error = null;
|
|
98
|
+
pd.errorDeg = null;
|
|
99
|
+
pd.lastAppliedRotations = [];
|
|
100
|
+
pd.lastAppliedMoves = [];
|
|
101
|
+
return { ok: false, status: 'invalid-selection', satisfied: false, applied: false, message };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (infoA.component === infoB.component) {
|
|
105
|
+
const message = 'Select references from two different components.';
|
|
106
|
+
pd.status = 'invalid-selection';
|
|
107
|
+
pd.message = message;
|
|
108
|
+
pd.satisfied = false;
|
|
109
|
+
pd.error = null;
|
|
110
|
+
pd.errorDeg = null;
|
|
111
|
+
pd.lastAppliedRotations = [];
|
|
112
|
+
pd.lastAppliedMoves = [];
|
|
113
|
+
return { ok: false, status: 'invalid-selection', satisfied: false, applied: false, message };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const measurement = this.#measureAngle(infoA, infoB);
|
|
117
|
+
if (!measurement) {
|
|
118
|
+
const message = 'Unable to measure angle between selections.';
|
|
119
|
+
pd.status = 'invalid-selection';
|
|
120
|
+
pd.message = message;
|
|
121
|
+
pd.satisfied = false;
|
|
122
|
+
pd.error = null;
|
|
123
|
+
pd.errorDeg = null;
|
|
124
|
+
pd.lastAppliedRotations = [];
|
|
125
|
+
pd.lastAppliedMoves = [];
|
|
126
|
+
return { ok: false, status: 'invalid-selection', satisfied: false, applied: false, message };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const angle = measurement.angle;
|
|
130
|
+
const angleDeg = measurement.angleDeg;
|
|
131
|
+
const signedAngle = measurement.signedAngle;
|
|
132
|
+
const signedAngleDeg = measurement.signedAngleDeg;
|
|
133
|
+
const error = signedAngle - targetAngleRad;
|
|
134
|
+
|
|
135
|
+
const fixedA = context.isComponentFixed?.(infoA.component);
|
|
136
|
+
const fixedB = context.isComponentFixed?.(infoB.component);
|
|
137
|
+
|
|
138
|
+
const linearTolerance = Math.abs(context.tolerance ?? DEFAULT_ANGLE_LINEAR_TOLERANCE);
|
|
139
|
+
const explicitAngleTol = Number.isFinite(context.angleTolerance) ? Math.abs(context.angleTolerance) : null;
|
|
140
|
+
const angleTolerance = explicitAngleTol && explicitAngleTol > 0
|
|
141
|
+
? Math.max(ANGLE_TOLERANCE, explicitAngleTol)
|
|
142
|
+
: Math.max(ANGLE_TOLERANCE, linearTolerance * 10);
|
|
143
|
+
|
|
144
|
+
pd.error = Math.abs(error);
|
|
145
|
+
pd.errorDeg = Math.abs(THREE.MathUtils.radToDeg(error));
|
|
146
|
+
|
|
147
|
+
if (Math.abs(error) <= angleTolerance) {
|
|
148
|
+
const message = 'Angle satisfied within tolerance.';
|
|
149
|
+
pd.status = 'satisfied';
|
|
150
|
+
pd.message = message;
|
|
151
|
+
pd.satisfied = true;
|
|
152
|
+
pd.lastAppliedRotations = [];
|
|
153
|
+
pd.lastAppliedMoves = [];
|
|
154
|
+
if (pd.exception) delete pd.exception;
|
|
155
|
+
return {
|
|
156
|
+
ok: true,
|
|
157
|
+
status: 'satisfied',
|
|
158
|
+
satisfied: true,
|
|
159
|
+
applied: false,
|
|
160
|
+
angle,
|
|
161
|
+
angleDeg,
|
|
162
|
+
signedAngle,
|
|
163
|
+
signedAngleDeg,
|
|
164
|
+
targetAngle: targetAngleRad,
|
|
165
|
+
error,
|
|
166
|
+
message,
|
|
167
|
+
infoA,
|
|
168
|
+
infoB,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (fixedA && fixedB) {
|
|
173
|
+
const message = 'Both components are fixed; unable to adjust angle.';
|
|
174
|
+
pd.status = 'blocked';
|
|
175
|
+
pd.message = message;
|
|
176
|
+
pd.satisfied = false;
|
|
177
|
+
pd.lastAppliedRotations = [];
|
|
178
|
+
pd.lastAppliedMoves = [];
|
|
179
|
+
if (pd.exception) delete pd.exception;
|
|
180
|
+
return {
|
|
181
|
+
ok: false,
|
|
182
|
+
status: 'blocked',
|
|
183
|
+
satisfied: false,
|
|
184
|
+
applied: false,
|
|
185
|
+
angle,
|
|
186
|
+
angleDeg,
|
|
187
|
+
targetAngle: targetAngleRad,
|
|
188
|
+
error,
|
|
189
|
+
message,
|
|
190
|
+
infoA,
|
|
191
|
+
infoB,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const desiredSignedAngle = targetAngleRad;
|
|
196
|
+
const delta = signedAngle - desiredSignedAngle;
|
|
197
|
+
|
|
198
|
+
const phiACurrent = 0;
|
|
199
|
+
const phiBCurrent = signedAngle;
|
|
200
|
+
let phiATarget;
|
|
201
|
+
let phiBTarget;
|
|
202
|
+
|
|
203
|
+
if (!fixedA && !fixedB) {
|
|
204
|
+
const halfDelta = delta / 2;
|
|
205
|
+
phiATarget = phiACurrent + halfDelta;
|
|
206
|
+
phiBTarget = phiBCurrent - halfDelta;
|
|
207
|
+
} else if (!fixedA && fixedB) {
|
|
208
|
+
phiBTarget = phiBCurrent;
|
|
209
|
+
phiATarget = phiBTarget - desiredSignedAngle;
|
|
210
|
+
} else {
|
|
211
|
+
phiATarget = phiACurrent;
|
|
212
|
+
phiBTarget = phiATarget + desiredSignedAngle;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const rotationGain = Math.max(0, Math.min(1, context.rotationGain ?? 1));
|
|
216
|
+
const rotations = [];
|
|
217
|
+
let applied = false;
|
|
218
|
+
|
|
219
|
+
const applyRotation = (info, currentDir, targetDir, gainMultiplier) => {
|
|
220
|
+
if (!info?.component || !currentDir || !targetDir) return false;
|
|
221
|
+
const quaternion = computeRotationTowards(currentDir, targetDir, rotationGain * gainMultiplier);
|
|
222
|
+
if (!quaternion) return false;
|
|
223
|
+
const ok = context.applyRotation?.(info.component, quaternion);
|
|
224
|
+
if (!ok) return false;
|
|
225
|
+
rotations.push({ component: info.component.name || info.component.uuid, quaternion: quaternion.toArray() });
|
|
226
|
+
info.component.updateMatrixWorld?.(true);
|
|
227
|
+
return true;
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const shareA = (!fixedA && !fixedB) ? 0.5 : (!fixedA ? 1 : 0);
|
|
231
|
+
const shareB = (!fixedA && !fixedB) ? 0.5 : (!fixedB ? 1 : 0);
|
|
232
|
+
|
|
233
|
+
if (shareA > 0) {
|
|
234
|
+
const targetDirA = this.#directionFromAngle(measurement, phiATarget);
|
|
235
|
+
applied = applyRotation(infoA, measurement.dirA, targetDirA, shareA) || applied;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (shareB > 0) {
|
|
239
|
+
const targetDirB = this.#directionFromAngle(measurement, phiBTarget);
|
|
240
|
+
applied = applyRotation(infoB, measurement.dirB, targetDirB, shareB) || applied;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const status = applied ? 'adjusted' : 'pending';
|
|
244
|
+
const message = applied
|
|
245
|
+
? 'Applied rotation to move toward target angle.'
|
|
246
|
+
: 'Waiting for a movable component to rotate.';
|
|
247
|
+
|
|
248
|
+
pd.status = status;
|
|
249
|
+
pd.message = message;
|
|
250
|
+
pd.satisfied = false;
|
|
251
|
+
pd.lastAppliedRotations = rotations;
|
|
252
|
+
pd.lastAppliedMoves = [];
|
|
253
|
+
if (pd.exception) delete pd.exception;
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
ok: true,
|
|
257
|
+
status,
|
|
258
|
+
satisfied: false,
|
|
259
|
+
applied,
|
|
260
|
+
angle,
|
|
261
|
+
angleDeg,
|
|
262
|
+
signedAngle,
|
|
263
|
+
signedAngleDeg,
|
|
264
|
+
targetAngle: targetAngleRad,
|
|
265
|
+
error,
|
|
266
|
+
message,
|
|
267
|
+
infoA,
|
|
268
|
+
infoB,
|
|
269
|
+
rotations,
|
|
270
|
+
diagnostics: {
|
|
271
|
+
angle,
|
|
272
|
+
angleDeg,
|
|
273
|
+
signedAngle,
|
|
274
|
+
signedAngleDeg,
|
|
275
|
+
targetAngle: targetAngleRad,
|
|
276
|
+
error,
|
|
277
|
+
shareA,
|
|
278
|
+
shareB,
|
|
279
|
+
desiredSignedAngle,
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async run(context = {}) {
|
|
285
|
+
return this.solve(context);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
#resolveSelectionInfo(context, selection, label) {
|
|
289
|
+
const object = context.resolveObject?.(selection) || null;
|
|
290
|
+
const component = context.resolveComponent?.(selection) || null;
|
|
291
|
+
const kind = selectionKindFrom(object, selection);
|
|
292
|
+
|
|
293
|
+
if (kind === 'FACE') {
|
|
294
|
+
return resolveParallelSelection(this, context, selection, label);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (kind !== 'EDGE') {
|
|
298
|
+
throw new Error(`AngleConstraint: Unsupported selection for ${label}.`);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const origin = this.#resolveOrigin(object, component) || new THREE.Vector3();
|
|
302
|
+
const direction = this.#resolveEdgeDirection(object, component);
|
|
303
|
+
|
|
304
|
+
if (!direction) {
|
|
305
|
+
throw new Error('AngleConstraint: Unable to resolve edge direction.');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
selection,
|
|
310
|
+
object,
|
|
311
|
+
component: component || null,
|
|
312
|
+
origin,
|
|
313
|
+
direction,
|
|
314
|
+
kind,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
#resolveOrigin(object, component) {
|
|
319
|
+
if (object) {
|
|
320
|
+
try {
|
|
321
|
+
const rep = objectRepresentativePoint(null, object);
|
|
322
|
+
if (rep && typeof rep.clone === 'function') return rep.clone();
|
|
323
|
+
} catch { }
|
|
324
|
+
if (typeof object.getWorldPosition === 'function') {
|
|
325
|
+
return object.getWorldPosition(new THREE.Vector3());
|
|
326
|
+
}
|
|
327
|
+
if (object.isVector3) return object.clone();
|
|
328
|
+
}
|
|
329
|
+
if (component) {
|
|
330
|
+
component.updateMatrixWorld?.(true);
|
|
331
|
+
if (typeof component.getWorldPosition === 'function') {
|
|
332
|
+
return component.getWorldPosition(new THREE.Vector3());
|
|
333
|
+
}
|
|
334
|
+
if (component.position) {
|
|
335
|
+
const pos = component.position.clone();
|
|
336
|
+
component.parent?.updateMatrixWorld?.(true);
|
|
337
|
+
if (component.parent?.matrixWorld) {
|
|
338
|
+
return pos.applyMatrix4(component.parent.matrixWorld.clone());
|
|
339
|
+
}
|
|
340
|
+
return pos;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
#resolveEdgeDirection(object, component) {
|
|
347
|
+
const dir = getElementDirection(null, object);
|
|
348
|
+
if (dir && dir.lengthSq() > 0) return dir.clone().normalize();
|
|
349
|
+
if (component) {
|
|
350
|
+
const compDir = getElementDirection(null, component);
|
|
351
|
+
if (compDir && compDir.lengthSq() > 0) return compDir.clone().normalize();
|
|
352
|
+
}
|
|
353
|
+
const geom = object?.geometry;
|
|
354
|
+
if (geom?.getAttribute) {
|
|
355
|
+
const pos = geom.getAttribute('position');
|
|
356
|
+
if (pos && pos.count >= 2) {
|
|
357
|
+
const a = new THREE.Vector3(pos.getX(0), pos.getY(0), pos.getZ(0));
|
|
358
|
+
const b = new THREE.Vector3(pos.getX(1), pos.getY(1), pos.getZ(1));
|
|
359
|
+
object.updateMatrixWorld?.(true);
|
|
360
|
+
a.applyMatrix4(object.matrixWorld);
|
|
361
|
+
b.applyMatrix4(object.matrixWorld);
|
|
362
|
+
return b.sub(a).normalize();
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
#measureAngle(infoA, infoB) {
|
|
369
|
+
const dirA = normalizeOrNull(infoA?.direction);
|
|
370
|
+
const dirB = normalizeOrNull(infoB?.direction);
|
|
371
|
+
if (!dirA || !dirB) return null;
|
|
372
|
+
|
|
373
|
+
let axis = new THREE.Vector3().crossVectors(dirA, dirB);
|
|
374
|
+
if (axis.lengthSq() <= 1e-12) {
|
|
375
|
+
axis = arbitraryPerpendicular(dirA);
|
|
376
|
+
}
|
|
377
|
+
if (axis.lengthSq() <= 1e-12) return null;
|
|
378
|
+
axis.normalize();
|
|
379
|
+
|
|
380
|
+
let basisU = new THREE.Vector3().crossVectors(axis, dirA);
|
|
381
|
+
if (basisU.lengthSq() <= 1e-12) {
|
|
382
|
+
basisU = arbitraryPerpendicular(dirA);
|
|
383
|
+
}
|
|
384
|
+
if (basisU.lengthSq() <= 1e-12) return null;
|
|
385
|
+
basisU.normalize();
|
|
386
|
+
|
|
387
|
+
const cosVal = THREE.MathUtils.clamp(dirA.dot(dirB), -1, 1);
|
|
388
|
+
const sinVal = THREE.MathUtils.clamp(basisU.dot(dirB), -1, 1);
|
|
389
|
+
const signedAngle = Math.atan2(sinVal, cosVal);
|
|
390
|
+
const angle = Math.acos(cosVal);
|
|
391
|
+
|
|
392
|
+
return {
|
|
393
|
+
angle,
|
|
394
|
+
angleDeg: THREE.MathUtils.radToDeg(angle),
|
|
395
|
+
signedAngle,
|
|
396
|
+
signedAngleDeg: THREE.MathUtils.radToDeg(signedAngle),
|
|
397
|
+
axis,
|
|
398
|
+
basisU,
|
|
399
|
+
dirA,
|
|
400
|
+
dirB,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
#directionFromAngle(measurement, angle) {
|
|
405
|
+
if (!measurement) return null;
|
|
406
|
+
const { dirA, basisU } = measurement;
|
|
407
|
+
if (!dirA || !basisU) return null;
|
|
408
|
+
const cosVal = Math.cos(angle);
|
|
409
|
+
const sinVal = Math.sin(angle);
|
|
410
|
+
const out = dirA.clone().multiplyScalar(cosVal).add(basisU.clone().multiplyScalar(sinVal));
|
|
411
|
+
if (out.lengthSq() === 0) return null;
|
|
412
|
+
return out.normalize();
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
function selectionPair(params) {
|
|
420
|
+
if (!params || typeof params !== 'object') return [null, null];
|
|
421
|
+
const raw = Array.isArray(params.elements) ? params.elements : [];
|
|
422
|
+
const picks = raw.filter((item) => item != null).slice(0, 2);
|
|
423
|
+
params.elements = picks;
|
|
424
|
+
if (picks.length === 2) return picks;
|
|
425
|
+
if (picks.length === 1) return [picks[0], null];
|
|
426
|
+
return [null, null];
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function selectionKindFrom(object, selection) {
|
|
430
|
+
const val = (selection && typeof selection.kind === 'string') ? selection.kind : null;
|
|
431
|
+
const raw = (object?.userData?.type || object?.userData?.brepType || object?.type || val || '')
|
|
432
|
+
.toString()
|
|
433
|
+
.toUpperCase();
|
|
434
|
+
if (!raw) return 'UNKNOWN';
|
|
435
|
+
if (raw.includes('FACE')) return 'FACE';
|
|
436
|
+
if (raw.includes('EDGE')) return 'EDGE';
|
|
437
|
+
if (raw.includes('VERTEX') || raw.includes('POINT')) return 'POINT';
|
|
438
|
+
if (raw.includes('COMPONENT')) return 'COMPONENT';
|
|
439
|
+
return raw;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function normalizeOrNull(vec) {
|
|
443
|
+
if (!vec) return null;
|
|
444
|
+
if (vec.lengthSq() === 0) return null;
|
|
445
|
+
return vec.clone().normalize();
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function arbitraryPerpendicular(dir) {
|
|
449
|
+
if (!dir || dir.lengthSq() === 0) return new THREE.Vector3(0, 0, 1);
|
|
450
|
+
const axis = Math.abs(dir.dot(new THREE.Vector3(0, 0, 1))) < 0.9
|
|
451
|
+
? new THREE.Vector3(0, 0, 1)
|
|
452
|
+
: new THREE.Vector3(0, 1, 0);
|
|
453
|
+
const perp = new THREE.Vector3().crossVectors(dir, axis);
|
|
454
|
+
if (perp.lengthSq() === 0) {
|
|
455
|
+
perp.crossVectors(dir, new THREE.Vector3(1, 0, 0));
|
|
456
|
+
}
|
|
457
|
+
return perp.lengthSq() === 0 ? new THREE.Vector3(1, 0, 0) : perp.normalize();
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function clampAndNormalizeAngleDeg(value) {
|
|
461
|
+
const safeValue = Number.isFinite(value) ? THREE.MathUtils.clamp(value, -360, 360) : 0;
|
|
462
|
+
const wrapped = ((safeValue % 360) + 360) % 360;
|
|
463
|
+
if (wrapped === 180) return safeValue < 0 ? -180 : 180;
|
|
464
|
+
return wrapped > 180 ? wrapped - 360 : wrapped;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function computeRotationTowards(fromDir, toDir, gain = 1) {
|
|
468
|
+
if (!fromDir || !toDir) return null;
|
|
469
|
+
const a = fromDir.clone().normalize();
|
|
470
|
+
const b = toDir.clone().normalize();
|
|
471
|
+
const dot = THREE.MathUtils.clamp(a.dot(b), -1, 1);
|
|
472
|
+
let angle = Math.acos(dot);
|
|
473
|
+
if (!Number.isFinite(angle) || angle <= 1e-6) return null;
|
|
474
|
+
let axis = new THREE.Vector3().crossVectors(a, b);
|
|
475
|
+
if (axis.lengthSq() <= 1e-12) {
|
|
476
|
+
axis = arbitraryPerpendicular(a);
|
|
477
|
+
}
|
|
478
|
+
if (axis.lengthSq() <= 1e-12) return null;
|
|
479
|
+
axis.normalize();
|
|
480
|
+
const clampedGain = Math.max(0, Math.min(1, gain));
|
|
481
|
+
const intendedAngle = angle * clampedGain;
|
|
482
|
+
const appliedAngle = Math.min(intendedAngle, MAX_ROTATION_PER_ITERATION, angle);
|
|
483
|
+
if (appliedAngle <= 1e-6) return null;
|
|
484
|
+
return new THREE.Quaternion().setFromAxisAngle(axis, appliedAngle);
|
|
485
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import { BaseAssemblyConstraint } from '../BaseAssemblyConstraint.js';
|
|
3
|
+
import { objectRepresentativePoint } from '../../UI/pmi/annUtils.js';
|
|
4
|
+
|
|
5
|
+
const DEFAULT_COINCIDENT_TOLERANCE = 1e-6;
|
|
6
|
+
|
|
7
|
+
const inputParamsSchema = {
|
|
8
|
+
id: {
|
|
9
|
+
type: 'string',
|
|
10
|
+
default_value: null,
|
|
11
|
+
hint: 'Unique identifier for the constraint.',
|
|
12
|
+
},
|
|
13
|
+
elements: {
|
|
14
|
+
type: 'reference_selection',
|
|
15
|
+
label: 'Elements',
|
|
16
|
+
hint: 'Select two references (vertex, edge, face, or component).',
|
|
17
|
+
selectionFilter: ['VERTEX', 'EDGE', 'FACE', 'COMPONENT'],
|
|
18
|
+
multiple: true,
|
|
19
|
+
minSelections: 2,
|
|
20
|
+
maxSelections: 2,
|
|
21
|
+
},
|
|
22
|
+
applyImmediately: {
|
|
23
|
+
type: 'boolean',
|
|
24
|
+
label: 'Apply Immediately',
|
|
25
|
+
default_value: false,
|
|
26
|
+
hint: 'Maintained for compatibility; runtime solver applies adjustments iteratively.',
|
|
27
|
+
},
|
|
28
|
+
faceNormalOpposed: {
|
|
29
|
+
type: 'boolean',
|
|
30
|
+
label: 'Oppose Face Normals',
|
|
31
|
+
default_value: false,
|
|
32
|
+
hint: 'Preserved for future expansion.',
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export class CoincidentConstraint extends BaseAssemblyConstraint {
|
|
37
|
+
static shortName = 'COIN';
|
|
38
|
+
static longName = 'Coincident Constraint';
|
|
39
|
+
static constraintType = 'coincident';
|
|
40
|
+
static aliases = ['mate', 'coincident', 'coincident constraint'];
|
|
41
|
+
static inputParamsSchema = inputParamsSchema;
|
|
42
|
+
|
|
43
|
+
async solve(context = {}) {
|
|
44
|
+
const pd = this.persistentData = this.persistentData || {};
|
|
45
|
+
const tolerance = Math.max(Math.abs(context.tolerance ?? DEFAULT_COINCIDENT_TOLERANCE), 1e-8);
|
|
46
|
+
|
|
47
|
+
const [selA, selB] = selectionPair(this.inputParams);
|
|
48
|
+
|
|
49
|
+
if (!selA || !selB) {
|
|
50
|
+
pd.status = 'incomplete';
|
|
51
|
+
pd.message = 'Select two references to define the constraint.';
|
|
52
|
+
pd.satisfied = false;
|
|
53
|
+
return { ok: false, status: 'incomplete', satisfied: false, applied: false, message: pd.message };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const infoA = selectionInfo(this, context, selA);
|
|
57
|
+
const infoB = selectionInfo(this, context, selB);
|
|
58
|
+
|
|
59
|
+
if (!infoA.component || !infoB.component) {
|
|
60
|
+
pd.status = 'invalid-selection';
|
|
61
|
+
pd.message = 'Both selections must belong to assembly components.';
|
|
62
|
+
pd.satisfied = false;
|
|
63
|
+
return { ok: false, status: 'invalid-selection', satisfied: false, applied: false, message: pd.message };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (infoA.component === infoB.component) {
|
|
67
|
+
pd.status = 'invalid-selection';
|
|
68
|
+
pd.message = 'Select references from two different components.';
|
|
69
|
+
pd.satisfied = false;
|
|
70
|
+
return { ok: false, status: 'invalid-selection', satisfied: false, applied: false, message: pd.message };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!infoA.point || !infoB.point) {
|
|
74
|
+
pd.status = 'invalid-selection';
|
|
75
|
+
pd.message = 'Unable to resolve world-space positions for one or both selections.';
|
|
76
|
+
pd.satisfied = false;
|
|
77
|
+
return { ok: false, status: 'invalid-selection', satisfied: false, applied: false, message: pd.message };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const delta = new THREE.Vector3().subVectors(infoA.point, infoB.point);
|
|
81
|
+
const distance = delta.length();
|
|
82
|
+
|
|
83
|
+
const fixedA = context.isComponentFixed?.(infoA.component);
|
|
84
|
+
const fixedB = context.isComponentFixed?.(infoB.component);
|
|
85
|
+
const translationGain = context.translationGain ?? 1;
|
|
86
|
+
|
|
87
|
+
if (distance <= tolerance) {
|
|
88
|
+
pd.status = 'satisfied';
|
|
89
|
+
pd.message = 'Selections are coincident within tolerance.';
|
|
90
|
+
pd.error = distance;
|
|
91
|
+
pd.satisfied = true;
|
|
92
|
+
pd.lastAppliedMoves = [];
|
|
93
|
+
return { ok: true, status: 'satisfied', satisfied: true, applied: false, error: distance, message: pd.message };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (fixedA && fixedB) {
|
|
97
|
+
pd.status = 'blocked';
|
|
98
|
+
pd.message = 'Both components are fixed; unable to adjust positions.';
|
|
99
|
+
pd.error = distance;
|
|
100
|
+
pd.satisfied = false;
|
|
101
|
+
pd.lastAppliedMoves = [];
|
|
102
|
+
return { ok: false, status: 'blocked', satisfied: false, applied: false, error: distance, message: pd.message };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const moves = [];
|
|
106
|
+
let applied = false;
|
|
107
|
+
|
|
108
|
+
const applyMove = (component, moveVector) => {
|
|
109
|
+
if (!component || !moveVector || moveVector.lengthSq() === 0) return false;
|
|
110
|
+
const ok = context.applyTranslation?.(component, moveVector);
|
|
111
|
+
if (ok) {
|
|
112
|
+
moves.push({ component: component.name || component.uuid, move: vectorToArray(moveVector) });
|
|
113
|
+
}
|
|
114
|
+
return ok;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
if (!fixedA && !fixedB) {
|
|
118
|
+
const step = delta.clone().multiplyScalar(0.5 * translationGain);
|
|
119
|
+
if (step.lengthSq() > 0) {
|
|
120
|
+
applied = applyMove(infoA.component, step.clone().multiplyScalar(-1)) || applied;
|
|
121
|
+
applied = applyMove(infoB.component, step) || applied;
|
|
122
|
+
}
|
|
123
|
+
} else if (fixedA && !fixedB) {
|
|
124
|
+
const step = delta.clone().multiplyScalar(translationGain);
|
|
125
|
+
if (step.lengthSq() > 0) applied = applyMove(infoB.component, step) || applied;
|
|
126
|
+
} else if (!fixedA && fixedB) {
|
|
127
|
+
const step = delta.clone().multiplyScalar(translationGain);
|
|
128
|
+
if (step.lengthSq() > 0) applied = applyMove(infoA.component, step.clone().multiplyScalar(-1)) || applied;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const status = applied ? 'adjusted' : 'pending';
|
|
132
|
+
const message = applied ? 'Applied translation to reduce separation.' : 'Waiting for a movable component to adjust.';
|
|
133
|
+
|
|
134
|
+
pd.status = status;
|
|
135
|
+
pd.message = message;
|
|
136
|
+
pd.error = distance;
|
|
137
|
+
pd.satisfied = false;
|
|
138
|
+
if (moves.length) pd.lastAppliedMoves = moves;
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
ok: true,
|
|
142
|
+
status,
|
|
143
|
+
satisfied: false,
|
|
144
|
+
applied,
|
|
145
|
+
error: distance,
|
|
146
|
+
message,
|
|
147
|
+
diagnostics: { distance, moves },
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async run(context = {}) {
|
|
152
|
+
return this.solve(context);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
function selectionPair(params) {
|
|
158
|
+
if (!params || typeof params !== 'object') return [null, null];
|
|
159
|
+
const raw = Array.isArray(params.elements) ? params.elements : [];
|
|
160
|
+
const picks = raw.filter((item) => item != null).slice(0, 2);
|
|
161
|
+
params.elements = picks;
|
|
162
|
+
if (picks.length === 2) return picks;
|
|
163
|
+
if (picks.length === 1) return [picks[0], null];
|
|
164
|
+
return [null, null];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function resolvePoint(constraint, object, component) {
|
|
168
|
+
if (object) {
|
|
169
|
+
try {
|
|
170
|
+
const rep = objectRepresentativePoint(null, object);
|
|
171
|
+
if (rep && typeof rep.clone === 'function') return rep.clone();
|
|
172
|
+
} catch {}
|
|
173
|
+
const worldPoint = constraint.getWorldPoint(object);
|
|
174
|
+
if (worldPoint) return worldPoint;
|
|
175
|
+
}
|
|
176
|
+
if (component) {
|
|
177
|
+
component.updateMatrixWorld?.(true);
|
|
178
|
+
const worldPoint = constraint.getWorldPoint(component);
|
|
179
|
+
if (worldPoint) return worldPoint;
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function selectionInfo(constraint, context, selection) {
|
|
185
|
+
const object = context.resolveObject?.(selection) || null;
|
|
186
|
+
const component = context.resolveComponent?.(selection) || null;
|
|
187
|
+
const point = resolvePoint(constraint, object, component);
|
|
188
|
+
return { object, component, point };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function vectorToArray(vec) {
|
|
192
|
+
if (!vec) return [0, 0, 0];
|
|
193
|
+
return [vec.x, vec.y, vec.z];
|
|
194
|
+
}
|