brep-io-kernel 1.0.97 → 1.0.99

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.
@@ -5,12 +5,20 @@ import { distance, calculateAngle } from "./mathHelpersMod.js";
5
5
 
6
6
  // === Constraint function table ===
7
7
  const constraintFunctions = constraints.constraintFunctions;
8
+ let globalDistanceSolveCycleId = 0;
8
9
 
9
10
  // === Engine that performs numeric solving on a sketch snapshot ===
10
11
  class ConstraintEngine {
11
12
  constructor(sketchJSON) {
12
13
  const sketch = JSON.parse(sketchJSON);
13
- this.points = sketch.points.map(p => new Point(p.id, p.x, p.y, p.fixed));
14
+ this.points = sketch.points.map(p => new Point(
15
+ p.id,
16
+ p.x,
17
+ p.y,
18
+ p.fixed,
19
+ p.construction,
20
+ p.externalReference
21
+ ));
14
22
  this.geometries = sketch.geometries || [];
15
23
  this.constraints = sketch.constraints || [];
16
24
  }
@@ -63,6 +71,17 @@ class ConstraintEngine {
63
71
 
64
72
  solve(iterations = 100) {
65
73
  const decimalsPlaces = 6;
74
+ this._distanceSolveCycleId = ++globalDistanceSolveCycleId;
75
+ const hasPendingDistanceTargetSlides = () => {
76
+ const slideTol = Number.isFinite(constraints?.tolerance) ? Number(constraints.tolerance) : 1e-8;
77
+ return this.constraints.some((c) => (
78
+ c?.type === "⟺" &&
79
+ c?._distanceThrottleActive === true &&
80
+ Number.isFinite(c?._distanceRequestedTarget) &&
81
+ Number.isFinite(c?._distanceAppliedTarget) &&
82
+ Math.abs(c._distanceRequestedTarget - c._distanceAppliedTarget) > slideTol
83
+ ));
84
+ };
66
85
 
67
86
  // Implied constraints for certain geometry types (e.g., arcs, bezier splines)
68
87
  let nextTempId =
@@ -101,6 +120,7 @@ class ConstraintEngine {
101
120
  this.tidyDecimalsOfPoints(decimalsPlaces, true);
102
121
 
103
122
  // Ground first, then everything
123
+ this._distanceSolvePassToken = `${this._distanceSolveCycleId}:pre`;
104
124
  this.processConstraintsOfType("⏚");
105
125
  this.processConstraintsOfType("all");
106
126
 
@@ -114,6 +134,7 @@ class ConstraintEngine {
114
134
  let converged = false;
115
135
 
116
136
  for (let i = 0; i < iterations; i++) {
137
+ this._distanceSolvePassToken = `${this._distanceSolveCycleId}:${i}`;
117
138
  for (const t of order) {
118
139
  this.processConstraintsOfType(t);
119
140
  this.processConstraintsOfType("≡"); // keep coincident snapping frequently
@@ -126,7 +147,7 @@ class ConstraintEngine {
126
147
  }
127
148
 
128
149
  const cur = JSON.stringify(this.points);
129
- if (cur === prev) {
150
+ if (cur === prev && !hasPendingDistanceTargetSlides()) {
130
151
  converged = true;
131
152
  break;
132
153
  }
@@ -150,7 +171,14 @@ class ConstraintEngine {
150
171
 
151
172
  // Return a new sketch object mirroring input structure
152
173
  const updatedSketch = {
153
- points: this.points.map(p => ({ id: p.id, x: p.x, y: p.y, fixed: p.fixed })),
174
+ points: this.points.map(p => ({
175
+ id: p.id,
176
+ x: p.x,
177
+ y: p.y,
178
+ fixed: p.fixed,
179
+ construction: p.construction === true,
180
+ externalReference: p.externalReference === true
181
+ })),
154
182
  geometries: this.geometries,
155
183
  constraints: this.constraints.filter(c => !c.temporary) // drop temporaries
156
184
  };
@@ -160,11 +188,13 @@ class ConstraintEngine {
160
188
  }
161
189
 
162
190
  class Point {
163
- constructor(id, x, y, fixed = false) {
191
+ constructor(id, x, y, fixed = false, construction = false, externalReference = false) {
164
192
  this.id = id;
165
193
  this.x = x;
166
194
  this.y = y;
167
195
  this.fixed = fixed;
196
+ this.construction = construction === true;
197
+ this.externalReference = externalReference === true;
168
198
  }
169
199
  }
170
200
 
@@ -188,7 +218,7 @@ export class ConstraintSolver {
188
218
  this.appState = opts.appState || { mode: "", type: "", requiredSelections: 0 };
189
219
 
190
220
  this.sketchObject = opts.sketch ? sanitizeSketch(opts.sketch) : {
191
- points: [{ id: 0, x: 0, y: 0, fixed: true }],
221
+ points: [{ id: 0, x: 0, y: 0, fixed: true, construction: true, externalReference: false }],
192
222
  geometries: [],
193
223
  constraints: [{ id: 0, type: "⏚", points: [0] }]
194
224
  };
@@ -892,14 +922,19 @@ export class ConstraintSolver {
892
922
  function sanitizeSketch(sketch) {
893
923
  const s = {
894
924
  points: Array.isArray(sketch.points) ? sketch.points.map(p => ({
895
- id: +p.id, x: +p.x, y: +p.y, fixed: !!p.fixed
925
+ id: +p.id,
926
+ x: +p.x,
927
+ y: +p.y,
928
+ fixed: !!p.fixed,
929
+ construction: (typeof p?.construction === "boolean") ? p.construction : (+p?.id === 0),
930
+ externalReference: !!p?.externalReference
896
931
  })) : [],
897
932
  geometries: Array.isArray(sketch.geometries) ? sketch.geometries.slice() : [],
898
933
  constraints: Array.isArray(sketch.constraints) ? sketch.constraints.slice() : []
899
934
  };
900
935
 
901
936
  // Ensure at least an origin and ground if empty
902
- if (s.points.length === 0) s.points.push({ id: 0, x: 0, y: 0, fixed: true });
937
+ if (s.points.length === 0) s.points.push({ id: 0, x: 0, y: 0, fixed: true, construction: true, externalReference: false });
903
938
  if (!s.constraints.some(c => c.type === "⏚")) {
904
939
  s.constraints.push({ id: 0, type: "⏚", points: [0] });
905
940
  }
@@ -1,6 +1,9 @@
1
1
  "use strict";
2
2
  import { calculateAngle, rotatePoint, distance, roundToDecimals } from "./mathHelpersMod.js";
3
3
  let tolerance = 0.00001;
4
+ let distanceSlideThresholdRatio = 0.10;
5
+ let distanceSlideStepRatio = 0.10;
6
+ let distanceSlideMinStep = 0.001;
4
7
  const constraintFunctions = [];
5
8
 
6
9
  const normalizeAngle = (angle) => ((angle % 360) + 360) % 360;
@@ -9,6 +12,88 @@ const shortestAngleDelta = (target, current) => {
9
12
  return (delta > 180) ? delta - 360 : delta;
10
13
  };
11
14
 
15
+ function relativeDeltaRatio(a, b, floor = tolerance) {
16
+ const denom = Math.max(Math.abs(a), Math.abs(b), floor);
17
+ return Math.abs(a - b) / denom;
18
+ }
19
+
20
+ function resolveDistanceTargetForSolvePass(solverObject, constraint, requestedTarget) {
21
+ if (constraint?.type !== "⟺" || !Number.isFinite(requestedTarget)) {
22
+ return requestedTarget;
23
+ }
24
+
25
+ const previousRequested = Number.isFinite(constraint._distanceRequestedTarget)
26
+ ? constraint._distanceRequestedTarget
27
+ : null;
28
+
29
+ let appliedTarget = Number.isFinite(constraint._distanceAppliedTarget)
30
+ ? constraint._distanceAppliedTarget
31
+ : (previousRequested ?? requestedTarget);
32
+
33
+ const targetChanged = previousRequested !== null &&
34
+ Math.abs(requestedTarget - previousRequested) > tolerance;
35
+
36
+ if (previousRequested === null) {
37
+ constraint._distanceThrottleActive = false;
38
+ appliedTarget = requestedTarget;
39
+ } else if (targetChanged) {
40
+ const rel = relativeDeltaRatio(requestedTarget, previousRequested, tolerance);
41
+ if (rel > distanceSlideThresholdRatio) {
42
+ constraint._distanceThrottleActive = true;
43
+ } else {
44
+ constraint._distanceThrottleActive = false;
45
+ appliedTarget = requestedTarget;
46
+ }
47
+ }
48
+
49
+ constraint._distanceRequestedTarget = requestedTarget;
50
+
51
+ if (!constraint._distanceThrottleActive) {
52
+ constraint._distanceAppliedTarget = requestedTarget;
53
+ return requestedTarget;
54
+ }
55
+
56
+ const solvePassToken = (typeof solverObject?._distanceSolvePassToken === "string")
57
+ ? solverObject._distanceSolvePassToken
58
+ : null;
59
+ const lastAppliedPassToken = (typeof constraint._distanceLastAppliedPassToken === "string")
60
+ ? constraint._distanceLastAppliedPassToken
61
+ : null;
62
+
63
+ if (solvePassToken !== null && lastAppliedPassToken === solvePassToken) {
64
+ return Number.isFinite(constraint._distanceAppliedTarget)
65
+ ? constraint._distanceAppliedTarget
66
+ : appliedTarget;
67
+ }
68
+
69
+ const delta = requestedTarget - appliedTarget;
70
+ const absDelta = Math.abs(delta);
71
+ if (absDelta <= tolerance) {
72
+ constraint._distanceThrottleActive = false;
73
+ constraint._distanceAppliedTarget = requestedTarget;
74
+ if (solvePassToken !== null) constraint._distanceLastAppliedPassToken = solvePassToken;
75
+ return requestedTarget;
76
+ }
77
+
78
+ // Scale slide speed by the remaining gap so large drops (e.g. 1000 -> 1)
79
+ // can still settle within a single solve evaluation.
80
+ const maxStep = Math.max(
81
+ distanceSlideMinStep,
82
+ absDelta * distanceSlideStepRatio
83
+ );
84
+ const step = Math.min(absDelta, maxStep);
85
+ appliedTarget += Math.sign(delta) * step;
86
+
87
+ if (Math.abs(requestedTarget - appliedTarget) <= tolerance) {
88
+ appliedTarget = requestedTarget;
89
+ constraint._distanceThrottleActive = false;
90
+ }
91
+
92
+ constraint._distanceAppliedTarget = appliedTarget;
93
+ if (solvePassToken !== null) constraint._distanceLastAppliedPassToken = solvePassToken;
94
+ return appliedTarget;
95
+ }
96
+
12
97
 
13
98
  (constraintFunctions["━"] = function (solverObject, constraint, points, constraintValue) {
14
99
  // Horizontal constraint
@@ -74,8 +159,15 @@ const shortestAngleDelta = (target, current) => {
74
159
  if (isNaN(constraintValue) | constraintValue == undefined | constraintValue == null) {
75
160
  targetDistance = currentDistance;
76
161
  constraint.value = currentDistance;
162
+ if (constraint?.type === "⟺") {
163
+ constraint._distanceRequestedTarget = currentDistance;
164
+ constraint._distanceAppliedTarget = currentDistance;
165
+ constraint._distanceThrottleActive = false;
166
+ constraint._distanceLastAppliedPassToken = null;
167
+ }
77
168
  }
78
169
 
170
+ targetDistance = resolveDistanceTargetForSolvePass(solverObject, constraint, targetDistance);
79
171
 
80
172
 
81
173
  let diff = roundToDecimals(Math.abs(targetDistance) - currentDistance, 4);
@@ -691,6 +783,18 @@ const shortestAngleDelta = (target, current) => {
691
783
  export const constraints = {
692
784
  get tolerance() { return tolerance; },
693
785
  set tolerance(value) { tolerance = value; },
786
+ get distanceSlideThresholdRatio() { return distanceSlideThresholdRatio; },
787
+ set distanceSlideThresholdRatio(value) {
788
+ if (Number.isFinite(value) && value >= 0) distanceSlideThresholdRatio = Number(value);
789
+ },
790
+ get distanceSlideStepRatio() { return distanceSlideStepRatio; },
791
+ set distanceSlideStepRatio(value) {
792
+ if (Number.isFinite(value) && value >= 0) distanceSlideStepRatio = Number(value);
793
+ },
794
+ get distanceSlideMinStep() { return distanceSlideMinStep; },
795
+ set distanceSlideMinStep(value) {
796
+ if (Number.isFinite(value) && value >= 0) distanceSlideMinStep = Number(value);
797
+ },
694
798
  constraintFunctions,
695
799
  };
696
800
 
@@ -0,0 +1,46 @@
1
+ # Sketch Solver Topology Fixtures
2
+
3
+ Drop `.json` files in this folder to add sketch-solver topology regression tests automatically.
4
+
5
+ Each file becomes its own test in `pnpm test` via `registerSketchSolverTopologyFixtureTests(...)`.
6
+
7
+ ## Fixture schema
8
+
9
+ ```json
10
+ {
11
+ "name": "rect_width_height_fixture",
12
+ "sketch": { "points": [], "geometries": [], "constraints": [] },
13
+ "sourcePartFile": "src/tests/partFiles/your_case.BREP.json",
14
+ "sourceFeatureId": "S1",
15
+ "initialSolvePasses": 4,
16
+ "edits": [
17
+ { "constraintId": 5, "value": 110 },
18
+ { "expressionValues": { "x": 1200, "y": 2500, "z": 300 } }
19
+ ],
20
+ "expect": {
21
+ "topologyUnchanged": true,
22
+ "distances": [
23
+ { "a": 0, "b": 1, "value": 110, "tol": 0.06 }
24
+ ],
25
+ "anchors": [
26
+ { "pointId": 0, "x": 0, "y": 0, "tol": 0.01 }
27
+ ],
28
+ "coincidentPairs": [
29
+ { "a": 1, "b": 2, "tol": 0.01 }
30
+ ],
31
+ "orientationLoops": [
32
+ { "pointIds": [0, 1, 2, 3], "preserveSign": true, "minAbsArea": 1.0 }
33
+ ]
34
+ }
35
+ }
36
+ ```
37
+
38
+ ## Notes
39
+
40
+ - Provide either `sketch` directly, or `sourcePartFile` (optional `sourceFeatureId`) to pull a sketch from an existing part file.
41
+ - `edits` are applied in order; solver runs after each edit.
42
+ - `constraintId/value` edits set one dimension directly.
43
+ - `expressionValues` edits evaluate every `constraint.valueExpr` using provided variables (useful for `x/y/z` expression-driven sketches).
44
+ - If `expect.topologyUnchanged` is omitted, it defaults to `true`.
45
+ - `tol` defaults to `0.01` when omitted.
46
+ - A fixture fails fast with a clear message if IDs are invalid or references break.
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "coincident_chain_fixture",
3
+ "sketch": {
4
+ "points": [
5
+ { "id": 0, "x": 0, "y": 0, "fixed": true },
6
+ { "id": 1, "x": 40, "y": 0, "fixed": false },
7
+ { "id": 2, "x": 40, "y": 0, "fixed": false },
8
+ { "id": 3, "x": 40, "y": 30, "fixed": false },
9
+ { "id": 4, "x": 40, "y": 30, "fixed": false },
10
+ { "id": 5, "x": 80, "y": 30, "fixed": false }
11
+ ],
12
+ "geometries": [
13
+ { "id": 200, "type": "line", "points": [0, 1], "construction": false },
14
+ { "id": 201, "type": "line", "points": [2, 3], "construction": false },
15
+ { "id": 202, "type": "line", "points": [4, 5], "construction": false }
16
+ ],
17
+ "constraints": [
18
+ { "id": 0, "type": "⏚", "points": [0] },
19
+ { "id": 10, "type": "≡", "points": [1, 2] },
20
+ { "id": 11, "type": "≡", "points": [3, 4] },
21
+ { "id": 12, "type": "━", "points": [0, 1] },
22
+ { "id": 13, "type": "│", "points": [2, 3] },
23
+ { "id": 14, "type": "━", "points": [4, 5] },
24
+ { "id": 15, "type": "⟺", "points": [0, 1], "value": 40 },
25
+ { "id": 16, "type": "⟺", "points": [2, 3], "value": 30 },
26
+ { "id": 17, "type": "⟺", "points": [4, 5], "value": 40 }
27
+ ]
28
+ },
29
+ "edits": [
30
+ { "constraintId": 16, "value": 75 },
31
+ { "constraintId": 15, "value": 60 }
32
+ ],
33
+ "expect": {
34
+ "topologyUnchanged": true,
35
+ "distances": [
36
+ { "a": 0, "b": 1, "value": 60, "tol": 0.1 },
37
+ { "a": 2, "b": 3, "value": 75, "tol": 0.1 },
38
+ { "a": 4, "b": 5, "value": 40, "tol": 0.1 }
39
+ ],
40
+ "anchors": [
41
+ { "pointId": 0, "x": 0, "y": 0, "tol": 0.02 }
42
+ ],
43
+ "coincidentPairs": [
44
+ { "a": 1, "b": 2, "tol": 0.02 },
45
+ { "a": 3, "b": 4, "tol": 0.02 }
46
+ ]
47
+ }
48
+ }
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "rect_width_height_fixture",
3
+ "sketch": {
4
+ "points": [
5
+ { "id": 0, "x": 0, "y": 0, "fixed": true },
6
+ { "id": 1, "x": 40, "y": 0, "fixed": false },
7
+ { "id": 2, "x": 40, "y": 20, "fixed": false },
8
+ { "id": 3, "x": 0, "y": 20, "fixed": false }
9
+ ],
10
+ "geometries": [
11
+ { "id": 100, "type": "line", "points": [0, 1], "construction": false },
12
+ { "id": 101, "type": "line", "points": [1, 2], "construction": false },
13
+ { "id": 102, "type": "line", "points": [2, 3], "construction": false },
14
+ { "id": 103, "type": "line", "points": [3, 0], "construction": false }
15
+ ],
16
+ "constraints": [
17
+ { "id": 0, "type": "⏚", "points": [0] },
18
+ { "id": 1, "type": "━", "points": [0, 1] },
19
+ { "id": 2, "type": "━", "points": [2, 3] },
20
+ { "id": 3, "type": "│", "points": [1, 2] },
21
+ { "id": 4, "type": "│", "points": [3, 0] },
22
+ { "id": 5, "type": "⟺", "points": [0, 1], "value": 40 },
23
+ { "id": 6, "type": "⟺", "points": [1, 2], "value": 20 }
24
+ ]
25
+ },
26
+ "edits": [
27
+ { "constraintId": 5, "value": 110 },
28
+ { "constraintId": 6, "value": 45 }
29
+ ],
30
+ "expect": {
31
+ "topologyUnchanged": true,
32
+ "distances": [
33
+ { "a": 0, "b": 1, "value": 110, "tol": 0.08 },
34
+ { "a": 1, "b": 2, "value": 45, "tol": 0.08 }
35
+ ],
36
+ "anchors": [
37
+ { "pointId": 0, "x": 0, "y": 0, "tol": 0.02 }
38
+ ],
39
+ "orientationLoops": [
40
+ { "pointIds": [0, 1, 2, 3], "preserveSign": true, "minAbsArea": 1.0 }
41
+ ]
42
+ }
43
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "sketch_throttel_expression_sequence_fixture",
3
+ "sourcePartFile": "src/tests/partFiles/sketch_throttel_testing.BREP.json",
4
+ "sourceFeatureId": "S1",
5
+ "initialSolvePasses": 4,
6
+ "edits": [
7
+ { "expressionValues": { "x": 10, "y": 20, "z": 10 } },
8
+ { "expressionValues": { "x": 1200, "y": 2500, "z": 300 } }
9
+ ],
10
+ "expect": {
11
+ "topologyUnchanged": true,
12
+ "distances": [
13
+ { "a": 7, "b": 2, "value": 1200, "tol": 0.2 },
14
+ { "a": 5, "b": 6, "value": 2500, "tol": 0.2 }
15
+ ],
16
+ "anchors": [
17
+ { "pointId": 0, "x": 0, "y": 0, "tol": 0.02 }
18
+ ],
19
+ "coincidentPairs": [
20
+ { "a": 1, "b": 2, "tol": 0.02 },
21
+ { "a": 3, "b": 4, "tol": 0.02 },
22
+ { "a": 6, "b": 7, "tol": 0.02 }
23
+ ]
24
+ }
25
+ }