@code3d/core 0.0.1-alpha.0
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 +22 -0
- package/README.md +824 -0
- package/THIRD_PARTY.md +28 -0
- package/bld/library/alignment-geometry.d.ts +50 -0
- package/bld/library/alignment-geometry.d.ts.map +1 -0
- package/bld/library/alignment-geometry.js +322 -0
- package/bld/library/alignment-geometry.js.map +1 -0
- package/bld/library/bound-solver.d.ts +33 -0
- package/bld/library/bound-solver.d.ts.map +1 -0
- package/bld/library/bound-solver.js +252 -0
- package/bld/library/bound-solver.js.map +1 -0
- package/bld/library/extrude.d.ts +8 -0
- package/bld/library/extrude.d.ts.map +1 -0
- package/bld/library/extrude.js +28 -0
- package/bld/library/extrude.js.map +1 -0
- package/bld/library/index.d.ts +7 -0
- package/bld/library/index.d.ts.map +1 -0
- package/bld/library/index.js +3 -0
- package/bld/library/index.js.map +1 -0
- package/bld/library/kernel-artifact-codec.d.ts +3 -0
- package/bld/library/kernel-artifact-codec.d.ts.map +1 -0
- package/bld/library/kernel-artifact-codec.js +148 -0
- package/bld/library/kernel-artifact-codec.js.map +1 -0
- package/bld/library/kernel-cache.d.ts +63 -0
- package/bld/library/kernel-cache.d.ts.map +1 -0
- package/bld/library/kernel-cache.js +245 -0
- package/bld/library/kernel-cache.js.map +1 -0
- package/bld/library/kernel-shapes.d.ts +20 -0
- package/bld/library/kernel-shapes.d.ts.map +1 -0
- package/bld/library/kernel-shapes.js +108 -0
- package/bld/library/kernel-shapes.js.map +1 -0
- package/bld/library/loft.d.ts +7 -0
- package/bld/library/loft.d.ts.map +1 -0
- package/bld/library/loft.js +161 -0
- package/bld/library/loft.js.map +1 -0
- package/bld/library/material.d.ts +12 -0
- package/bld/library/material.d.ts.map +1 -0
- package/bld/library/material.js +176 -0
- package/bld/library/material.js.map +1 -0
- package/bld/library/model-color.d.ts +8 -0
- package/bld/library/model-color.d.ts.map +1 -0
- package/bld/library/model-color.js +36 -0
- package/bld/library/model-color.js.map +1 -0
- package/bld/library/open-cascade-error.d.ts +2 -0
- package/bld/library/open-cascade-error.d.ts.map +1 -0
- package/bld/library/open-cascade-error.js +22 -0
- package/bld/library/open-cascade-error.js.map +1 -0
- package/bld/library/relation-solver.d.ts +29 -0
- package/bld/library/relation-solver.d.ts.map +1 -0
- package/bld/library/relation-solver.js +258 -0
- package/bld/library/relation-solver.js.map +1 -0
- package/bld/library/replicad.d.ts +17 -0
- package/bld/library/replicad.d.ts.map +1 -0
- package/bld/library/replicad.js +24 -0
- package/bld/library/replicad.js.map +1 -0
- package/bld/library/retained-memory.d.ts +7 -0
- package/bld/library/retained-memory.d.ts.map +1 -0
- package/bld/library/retained-memory.js +27 -0
- package/bld/library/retained-memory.js.map +1 -0
- package/bld/library/runtime.d.ts +775 -0
- package/bld/library/runtime.d.ts.map +1 -0
- package/bld/library/runtime.js +2954 -0
- package/bld/library/runtime.js.map +1 -0
- package/bld/library/shell.d.ts +7 -0
- package/bld/library/shell.d.ts.map +1 -0
- package/bld/library/shell.js +221 -0
- package/bld/library/shell.js.map +1 -0
- package/bld/library/sketch-curve-intersections.d.ts +13 -0
- package/bld/library/sketch-curve-intersections.d.ts.map +1 -0
- package/bld/library/sketch-curve-intersections.js +117 -0
- package/bld/library/sketch-curve-intersections.js.map +1 -0
- package/bld/library/sketch-curves.d.ts +31 -0
- package/bld/library/sketch-curves.d.ts.map +1 -0
- package/bld/library/sketch-curves.js +89 -0
- package/bld/library/sketch-curves.js.map +1 -0
- package/bld/library/sketch-drag-rules.d.ts +35 -0
- package/bld/library/sketch-drag-rules.d.ts.map +1 -0
- package/bld/library/sketch-drag-rules.js +522 -0
- package/bld/library/sketch-drag-rules.js.map +1 -0
- package/bld/library/sketch-face.d.ts +4 -0
- package/bld/library/sketch-face.d.ts.map +1 -0
- package/bld/library/sketch-face.js +72 -0
- package/bld/library/sketch-face.js.map +1 -0
- package/bld/library/sketch-incidence.d.ts +26 -0
- package/bld/library/sketch-incidence.d.ts.map +1 -0
- package/bld/library/sketch-incidence.js +114 -0
- package/bld/library/sketch-incidence.js.map +1 -0
- package/bld/library/sketch-precision.d.ts +19 -0
- package/bld/library/sketch-precision.d.ts.map +1 -0
- package/bld/library/sketch-precision.js +55 -0
- package/bld/library/sketch-precision.js.map +1 -0
- package/bld/library/sketch-regions.d.ts +12 -0
- package/bld/library/sketch-regions.d.ts.map +1 -0
- package/bld/library/sketch-regions.js +193 -0
- package/bld/library/sketch-regions.js.map +1 -0
- package/bld/library/sketch-solver.d.ts +97 -0
- package/bld/library/sketch-solver.d.ts.map +1 -0
- package/bld/library/sketch-solver.js +578 -0
- package/bld/library/sketch-solver.js.map +1 -0
- package/bld/library/sketch.d.ts +142 -0
- package/bld/library/sketch.d.ts.map +1 -0
- package/bld/library/sketch.js +650 -0
- package/bld/library/sketch.js.map +1 -0
- package/bld/library/spatial.d.ts +28 -0
- package/bld/library/spatial.d.ts.map +1 -0
- package/bld/library/spatial.js +195 -0
- package/bld/library/spatial.js.map +1 -0
- package/bld/library/three.d.ts +3 -0
- package/bld/library/three.d.ts.map +1 -0
- package/bld/library/three.js +3 -0
- package/bld/library/three.js.map +1 -0
- package/bld/library/topology-id.d.ts +24 -0
- package/bld/library/topology-id.d.ts.map +1 -0
- package/bld/library/topology-id.js +67 -0
- package/bld/library/topology-id.js.map +1 -0
- package/bld/library/topology-inspection.d.ts +64 -0
- package/bld/library/topology-inspection.d.ts.map +1 -0
- package/bld/library/topology-inspection.js +267 -0
- package/bld/library/topology-inspection.js.map +1 -0
- package/bld/library/topology.d.ts +94 -0
- package/bld/library/topology.d.ts.map +1 -0
- package/bld/library/topology.js +455 -0
- package/bld/library/topology.js.map +1 -0
- package/bld/node/index.d.ts +2 -0
- package/bld/node/index.d.ts.map +1 -0
- package/bld/node/index.js +14 -0
- package/bld/node/index.js.map +1 -0
- package/bld/node/replicad.d.ts +3 -0
- package/bld/node/replicad.d.ts.map +1 -0
- package/bld/node/replicad.js +3 -0
- package/bld/node/replicad.js.map +1 -0
- package/bld/tooling/index.d.ts +26 -0
- package/bld/tooling/index.d.ts.map +1 -0
- package/bld/tooling/index.js +19 -0
- package/bld/tooling/index.js.map +1 -0
- package/package.json +53 -0
- package/src/library/alignment-geometry.ts +431 -0
- package/src/library/bound-solver.ts +410 -0
- package/src/library/extrude.ts +38 -0
- package/src/library/index.ts +88 -0
- package/src/library/kernel-artifact-codec.ts +166 -0
- package/src/library/kernel-cache.ts +337 -0
- package/src/library/kernel-shapes.ts +135 -0
- package/src/library/loft.ts +196 -0
- package/src/library/material.ts +228 -0
- package/src/library/model-color.ts +47 -0
- package/src/library/open-cascade-error.ts +26 -0
- package/src/library/relation-solver.ts +479 -0
- package/src/library/replicad.ts +40 -0
- package/src/library/retained-memory.ts +23 -0
- package/src/library/runtime.ts +5146 -0
- package/src/library/shell.ts +281 -0
- package/src/library/sketch-curve-intersections.ts +154 -0
- package/src/library/sketch-curves.ts +161 -0
- package/src/library/sketch-drag-rules.ts +767 -0
- package/src/library/sketch-face.ts +89 -0
- package/src/library/sketch-incidence.ts +204 -0
- package/src/library/sketch-precision.ts +81 -0
- package/src/library/sketch-regions.ts +271 -0
- package/src/library/sketch-solver.ts +973 -0
- package/src/library/sketch.ts +977 -0
- package/src/library/spatial.ts +254 -0
- package/src/library/three.ts +2 -0
- package/src/library/topology-id.ts +81 -0
- package/src/library/topology-inspection.ts +368 -0
- package/src/library/topology.ts +776 -0
- package/src/library/tsconfig.json +11 -0
- package/src/node/index.ts +20 -0
- package/src/node/replicad.ts +3 -0
- package/src/node/tsconfig.json +16 -0
- package/src/tooling/index.ts +160 -0
- package/src/tooling/tsconfig.json +15 -0
|
@@ -0,0 +1,973 @@
|
|
|
1
|
+
import type {GcsSystem, ModuleStatic} from '@salusoft89/planegcs';
|
|
2
|
+
import type {SketchArcDirection, SketchPosition} from './sketch.js';
|
|
3
|
+
import {sketchArcGeometry} from './sketch-curves.js';
|
|
4
|
+
import {pointLineDistance} from './sketch-incidence.js';
|
|
5
|
+
import {SketchPrecision} from './sketch-precision.js';
|
|
6
|
+
|
|
7
|
+
/** Evaluation-local numeric indices, never author entity or constraint IDs. */
|
|
8
|
+
export type SketchSolveConstraint =
|
|
9
|
+
| Readonly<{
|
|
10
|
+
kind: 'parallel';
|
|
11
|
+
points: readonly [number, number, number, number];
|
|
12
|
+
}>
|
|
13
|
+
| Readonly<{
|
|
14
|
+
kind: 'perpendicular';
|
|
15
|
+
points: readonly [number, number, number, number];
|
|
16
|
+
}>
|
|
17
|
+
| Readonly<{
|
|
18
|
+
kind: 'lineAngle';
|
|
19
|
+
points: readonly [number, number, number, number];
|
|
20
|
+
value: number;
|
|
21
|
+
}>
|
|
22
|
+
| Readonly<{
|
|
23
|
+
kind: 'pointOnCircle';
|
|
24
|
+
points: readonly [point: number, center: number];
|
|
25
|
+
curve: 'circle' | 'arc';
|
|
26
|
+
index: number;
|
|
27
|
+
}>
|
|
28
|
+
| Readonly<{
|
|
29
|
+
kind: 'pointOnLine';
|
|
30
|
+
points: readonly [point: number, start: number, end: number];
|
|
31
|
+
}>
|
|
32
|
+
| Readonly<{kind: 'sweep'; index: number; value: number}>
|
|
33
|
+
| Readonly<{
|
|
34
|
+
kind: 'radius';
|
|
35
|
+
curve: 'circle' | 'arc';
|
|
36
|
+
index: number;
|
|
37
|
+
value: number;
|
|
38
|
+
}>
|
|
39
|
+
| Readonly<{kind: 'fixed'; point: number; position: SketchPosition}>
|
|
40
|
+
| Readonly<{kind: 'x' | 'y'; point: number; value: number}>
|
|
41
|
+
| Readonly<{kind: 'midpoint'; points: readonly [number, number, number]}>
|
|
42
|
+
| Readonly<{
|
|
43
|
+
kind: 'horizontal' | 'vertical' | 'coincident';
|
|
44
|
+
points: readonly [number, number];
|
|
45
|
+
}>
|
|
46
|
+
| Readonly<{
|
|
47
|
+
kind: 'length' | 'angle';
|
|
48
|
+
points: readonly [number, number];
|
|
49
|
+
value: number;
|
|
50
|
+
}>;
|
|
51
|
+
|
|
52
|
+
export type SketchSolveProblem = Readonly<{
|
|
53
|
+
points: readonly Readonly<{
|
|
54
|
+
position: SketchPosition;
|
|
55
|
+
locked: readonly [boolean, boolean];
|
|
56
|
+
}>[];
|
|
57
|
+
lines: readonly (readonly [number, number])[];
|
|
58
|
+
circles: readonly Readonly<{
|
|
59
|
+
center: number;
|
|
60
|
+
radius: number;
|
|
61
|
+
locked: boolean;
|
|
62
|
+
}>[];
|
|
63
|
+
arcs: readonly Readonly<{
|
|
64
|
+
center: number;
|
|
65
|
+
radius: number;
|
|
66
|
+
locked: boolean;
|
|
67
|
+
points: readonly [number, number];
|
|
68
|
+
direction: SketchArcDirection;
|
|
69
|
+
}>[];
|
|
70
|
+
constraints: readonly SketchSolveConstraint[];
|
|
71
|
+
}>;
|
|
72
|
+
|
|
73
|
+
export type SketchSolveResult = Readonly<{
|
|
74
|
+
positions: readonly SketchPosition[];
|
|
75
|
+
radii: readonly number[];
|
|
76
|
+
arcRadii: readonly number[];
|
|
77
|
+
degreesOfFreedom: number;
|
|
78
|
+
redundant: readonly number[];
|
|
79
|
+
}>;
|
|
80
|
+
|
|
81
|
+
export class SketchConstraintError extends Error {
|
|
82
|
+
constructor(
|
|
83
|
+
readonly constraints: readonly number[],
|
|
84
|
+
message: string,
|
|
85
|
+
) {
|
|
86
|
+
super(message);
|
|
87
|
+
this.name = 'SketchConstraintError';
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let module: ModuleStatic;
|
|
92
|
+
export function installSketchSolver(instance: ModuleStatic): void {
|
|
93
|
+
module = instance;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export type SketchSolveTarget =
|
|
97
|
+
| Readonly<{kind: 'point'; point: number; position: SketchPosition}>
|
|
98
|
+
| Readonly<{
|
|
99
|
+
kind: 'radius';
|
|
100
|
+
curve: 'circle' | 'arc';
|
|
101
|
+
index: number;
|
|
102
|
+
value: number;
|
|
103
|
+
}>;
|
|
104
|
+
|
|
105
|
+
export type SketchSolveObjective = SketchSolveTarget &
|
|
106
|
+
Readonly<{weight: number}>;
|
|
107
|
+
|
|
108
|
+
/** A fresh native system per solve: no previous solution or native handles escape. */
|
|
109
|
+
export function solveSketchProblem(
|
|
110
|
+
problem: SketchSolveProblem,
|
|
111
|
+
objectives: readonly SketchSolveObjective[] = [],
|
|
112
|
+
preferredGeometry: SketchSolveProblem = problem,
|
|
113
|
+
): SketchSolveResult {
|
|
114
|
+
const authored = preferredGeometry;
|
|
115
|
+
const precision = new SketchPrecision(problem);
|
|
116
|
+
problem = initializeArcEndpoints(problem, precision);
|
|
117
|
+
const {constraints, points} = problem;
|
|
118
|
+
if (!points.length)
|
|
119
|
+
return {
|
|
120
|
+
positions: [],
|
|
121
|
+
radii: [],
|
|
122
|
+
arcRadii: [],
|
|
123
|
+
degreesOfFreedom: 0,
|
|
124
|
+
redundant: [],
|
|
125
|
+
};
|
|
126
|
+
// Unconstrained values and edits do not need a native modeling kernel.
|
|
127
|
+
if (!constraints.length && !problem.arcs.length)
|
|
128
|
+
return {
|
|
129
|
+
positions: points.map((p, index) => {
|
|
130
|
+
const targets = objectives
|
|
131
|
+
.filter(o => o.kind === 'point')
|
|
132
|
+
.filter(o => o.point === index);
|
|
133
|
+
return p.position.map((value, axis) =>
|
|
134
|
+
p.locked[axis]
|
|
135
|
+
? value
|
|
136
|
+
: leastSquaresTarget(
|
|
137
|
+
value,
|
|
138
|
+
targets.map(o => ({
|
|
139
|
+
value: o.position[axis],
|
|
140
|
+
weight: o.weight,
|
|
141
|
+
})),
|
|
142
|
+
),
|
|
143
|
+
) as [number, number];
|
|
144
|
+
}),
|
|
145
|
+
radii: problem.circles.map((circle, index) =>
|
|
146
|
+
circle.locked
|
|
147
|
+
? circle.radius
|
|
148
|
+
: leastSquaresTarget(
|
|
149
|
+
circle.radius,
|
|
150
|
+
objectives
|
|
151
|
+
.filter(o => o.kind === 'radius')
|
|
152
|
+
.filter(o => o.curve === 'circle' && o.index === index),
|
|
153
|
+
),
|
|
154
|
+
),
|
|
155
|
+
arcRadii: [],
|
|
156
|
+
degreesOfFreedom:
|
|
157
|
+
problem.circles.filter(c => !c.locked).length +
|
|
158
|
+
points.reduce(
|
|
159
|
+
(sum, p) => sum + Number(!p.locked[0]) + Number(!p.locked[1]),
|
|
160
|
+
0,
|
|
161
|
+
),
|
|
162
|
+
redundant: [],
|
|
163
|
+
};
|
|
164
|
+
const {origin, scale} = precision;
|
|
165
|
+
const normalized = (p: SketchPosition): SketchPosition => [
|
|
166
|
+
(p[0] - origin[0]) / scale,
|
|
167
|
+
(p[1] - origin[1]) / scale,
|
|
168
|
+
];
|
|
169
|
+
const gcs = new module.GcsSystem();
|
|
170
|
+
const geometries: {delete(): void}[] = [];
|
|
171
|
+
try {
|
|
172
|
+
gcs.set_debug_mode(0);
|
|
173
|
+
gcs.set_max_iterations(100);
|
|
174
|
+
gcs.set_covergence_threshold(precision.convergence);
|
|
175
|
+
const indices = points.map(p =>
|
|
176
|
+
normalized(p.position).map((v, axis) =>
|
|
177
|
+
gcs.push_p_param(v, p.locked[axis]),
|
|
178
|
+
),
|
|
179
|
+
);
|
|
180
|
+
const nativePoints = indices.map(([x, y]) => {
|
|
181
|
+
const point = gcs.make_point(x, y);
|
|
182
|
+
geometries.push(point);
|
|
183
|
+
return point;
|
|
184
|
+
});
|
|
185
|
+
const radiusIndices = problem.circles.map(c =>
|
|
186
|
+
gcs.push_p_param(c.radius / scale, c.locked),
|
|
187
|
+
);
|
|
188
|
+
const nativeCircles = problem.circles.map((circle, i) => {
|
|
189
|
+
const [x, y] = indices[circle.center];
|
|
190
|
+
const geometry = gcs.make_circle(x, y, radiusIndices[i]);
|
|
191
|
+
geometries.push(geometry);
|
|
192
|
+
return geometry;
|
|
193
|
+
});
|
|
194
|
+
const arcRadiusIndices: number[] = [];
|
|
195
|
+
const arcAngleIndices: number[][] = [];
|
|
196
|
+
const nativeArcs = problem.arcs.map(arc => {
|
|
197
|
+
const center = points[arc.center].position;
|
|
198
|
+
const [start, end] = arc.points.map(i => points[i].position);
|
|
199
|
+
const radius = Math.hypot(start[0] - center[0], start[1] - center[1]);
|
|
200
|
+
if (
|
|
201
|
+
radius === 0 ||
|
|
202
|
+
Math.hypot(end[0] - center[0], end[1] - center[1]) === 0 ||
|
|
203
|
+
Math.hypot(end[0] - start[0], end[1] - start[1]) === 0
|
|
204
|
+
)
|
|
205
|
+
throw new Error(
|
|
206
|
+
'Sketch arcs require a nonzero radius and distinct endpoints; use circle for a full circle.',
|
|
207
|
+
);
|
|
208
|
+
const curve = sketchArcGeometry(center, start, end, arc.direction);
|
|
209
|
+
// Unwrap on the authored directed branch, including major arcs. These
|
|
210
|
+
// parameters are solve-local; persisted endpoints reconstruct the branch.
|
|
211
|
+
const angles = [curve.start, curve.start + curve.sweep].map(angle =>
|
|
212
|
+
gcs.push_p_param(angle, false),
|
|
213
|
+
);
|
|
214
|
+
arcAngleIndices.push(angles);
|
|
215
|
+
const radiusIndex = gcs.push_p_param(arc.radius / scale, arc.locked);
|
|
216
|
+
arcRadiusIndices.push(radiusIndex);
|
|
217
|
+
const [cx, cy] = indices[arc.center],
|
|
218
|
+
[sx, sy] = indices[arc.points[0]],
|
|
219
|
+
[ex, ey] = indices[arc.points[1]];
|
|
220
|
+
const geometry = gcs.make_arc(
|
|
221
|
+
cx,
|
|
222
|
+
cy,
|
|
223
|
+
sx,
|
|
224
|
+
sy,
|
|
225
|
+
ex,
|
|
226
|
+
ey,
|
|
227
|
+
angles[0],
|
|
228
|
+
angles[1],
|
|
229
|
+
radiusIndex,
|
|
230
|
+
);
|
|
231
|
+
geometries.push(geometry);
|
|
232
|
+
// Structural equations, not authored constraints or persistent angles.
|
|
233
|
+
gcs.add_constraint_arc_rules(geometry, 0, true, 1);
|
|
234
|
+
return geometry;
|
|
235
|
+
});
|
|
236
|
+
const constant = (value: number) => gcs.push_p_param(value, true);
|
|
237
|
+
const constantTags = new Set<number>();
|
|
238
|
+
const activeTags = new Set<number>();
|
|
239
|
+
// A locked parameter equation has no unknowns. Check it directly instead
|
|
240
|
+
// of passing a zero-Jacobian row to native redundancy analysis: that
|
|
241
|
+
// analysis also solves the mouse objective and can mistake an already
|
|
242
|
+
// satisfied constant row for a conflict when a different axis cannot move.
|
|
243
|
+
const checkConstant = (
|
|
244
|
+
actual: number,
|
|
245
|
+
expected: number,
|
|
246
|
+
tag: number,
|
|
247
|
+
normalization = scale,
|
|
248
|
+
) => {
|
|
249
|
+
if (
|
|
250
|
+
Math.abs(actual - expected) / normalization >
|
|
251
|
+
precision.nativeAcceptance
|
|
252
|
+
)
|
|
253
|
+
throw new SketchConstraintError(
|
|
254
|
+
[tag - 1],
|
|
255
|
+
`Could not satisfy sketch constraints (${tag}). A locked geometry parameter contradicts the constraint.`,
|
|
256
|
+
);
|
|
257
|
+
constantTags.add(tag);
|
|
258
|
+
};
|
|
259
|
+
// ArcRules also determine dimensions from known point coordinates. Such
|
|
260
|
+
// dimensions are constant equations just like a locked circle radius;
|
|
261
|
+
// sending them again to native redundancy analysis can falsely conflict
|
|
262
|
+
// with an unreachable mouse target even though all hard equations hold.
|
|
263
|
+
const knownCoordinate = (
|
|
264
|
+
index: number,
|
|
265
|
+
axis: number,
|
|
266
|
+
): number | undefined => {
|
|
267
|
+
if (points[index].locked[axis]) return points[index].position[axis];
|
|
268
|
+
const fixed = constraints.find(
|
|
269
|
+
c => c.kind === 'fixed' && c.point === index,
|
|
270
|
+
);
|
|
271
|
+
if (fixed?.kind === 'fixed') return fixed.position[axis];
|
|
272
|
+
const coordinate = constraints.find(
|
|
273
|
+
c => c.kind === (axis === 0 ? 'x' : 'y') && c.point === index,
|
|
274
|
+
);
|
|
275
|
+
return coordinate && 'value' in coordinate ? coordinate.value : undefined;
|
|
276
|
+
};
|
|
277
|
+
const knownPosition = (index: number): SketchPosition | undefined => {
|
|
278
|
+
const x = knownCoordinate(index, 0),
|
|
279
|
+
y = knownCoordinate(index, 1);
|
|
280
|
+
return x !== undefined && y !== undefined ? [x, y] : undefined;
|
|
281
|
+
};
|
|
282
|
+
const knownRadius = (
|
|
283
|
+
kind: 'circle' | 'arc',
|
|
284
|
+
index: number,
|
|
285
|
+
): number | undefined => {
|
|
286
|
+
const curve = (kind === 'circle' ? problem.circles : problem.arcs)[index];
|
|
287
|
+
if (curve.locked) return curve.radius;
|
|
288
|
+
if (kind === 'circle') return undefined;
|
|
289
|
+
const arc = problem.arcs[index];
|
|
290
|
+
const center = knownPosition(arc.center);
|
|
291
|
+
const endpoint =
|
|
292
|
+
knownPosition(arc.points[0]) ?? knownPosition(arc.points[1]);
|
|
293
|
+
return center && endpoint
|
|
294
|
+
? Math.hypot(endpoint[0] - center[0], endpoint[1] - center[1])
|
|
295
|
+
: undefined;
|
|
296
|
+
};
|
|
297
|
+
const coordinate = (
|
|
298
|
+
point: number,
|
|
299
|
+
axis: number,
|
|
300
|
+
value: number,
|
|
301
|
+
tag: number,
|
|
302
|
+
weight = 1,
|
|
303
|
+
) => {
|
|
304
|
+
if (points[point].locked[axis]) {
|
|
305
|
+
checkConstant(points[point].position[axis], value, tag);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
activeTags.add(tag);
|
|
309
|
+
const method =
|
|
310
|
+
axis === 0
|
|
311
|
+
? 'add_constraint_coordinate_x'
|
|
312
|
+
: 'add_constraint_coordinate_y';
|
|
313
|
+
gcs[method](
|
|
314
|
+
nativePoints[point],
|
|
315
|
+
constant((value - origin[axis]) / scale),
|
|
316
|
+
tag,
|
|
317
|
+
true,
|
|
318
|
+
weight,
|
|
319
|
+
);
|
|
320
|
+
};
|
|
321
|
+
constraints.forEach((constraint, index) => {
|
|
322
|
+
const tag = index + 1;
|
|
323
|
+
if (constraint.kind === 'sweep') {
|
|
324
|
+
const arc = problem.arcs[constraint.index];
|
|
325
|
+
const center = knownPosition(arc.center),
|
|
326
|
+
a = knownPosition(arc.points[0]),
|
|
327
|
+
b = knownPosition(arc.points[1]);
|
|
328
|
+
if (center && a && b) {
|
|
329
|
+
checkConstant(
|
|
330
|
+
Math.abs(sketchArcGeometry(center, a, b, arc.direction).sweep),
|
|
331
|
+
(constraint.value * Math.PI) / 180,
|
|
332
|
+
tag,
|
|
333
|
+
1,
|
|
334
|
+
);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const [start, end] = arcAngleIndices[constraint.index];
|
|
338
|
+
const sign =
|
|
339
|
+
problem.arcs[constraint.index].direction === 'ccw' ? 1 : -1;
|
|
340
|
+
gcs.add_constraint_difference(
|
|
341
|
+
start,
|
|
342
|
+
end,
|
|
343
|
+
constant((sign * constraint.value * Math.PI) / 180),
|
|
344
|
+
tag,
|
|
345
|
+
true,
|
|
346
|
+
1,
|
|
347
|
+
);
|
|
348
|
+
} else if (constraint.kind === 'radius') {
|
|
349
|
+
const known = knownRadius(constraint.curve, constraint.index);
|
|
350
|
+
if (known !== undefined) checkConstant(known, constraint.value, tag);
|
|
351
|
+
else {
|
|
352
|
+
activeTags.add(tag);
|
|
353
|
+
const args = [
|
|
354
|
+
constant(constraint.value / scale),
|
|
355
|
+
tag,
|
|
356
|
+
true,
|
|
357
|
+
1,
|
|
358
|
+
] as const;
|
|
359
|
+
if (constraint.curve === 'arc')
|
|
360
|
+
gcs.add_constraint_arc_radius(
|
|
361
|
+
nativeArcs[constraint.index],
|
|
362
|
+
...args,
|
|
363
|
+
);
|
|
364
|
+
else
|
|
365
|
+
gcs.add_constraint_circle_radius(
|
|
366
|
+
nativeCircles[constraint.index],
|
|
367
|
+
...args,
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
} else if (constraint.kind === 'fixed') {
|
|
371
|
+
coordinate(constraint.point, 0, constraint.position[0], tag);
|
|
372
|
+
coordinate(constraint.point, 1, constraint.position[1], tag);
|
|
373
|
+
} else if (constraint.kind === 'x' || constraint.kind === 'y') {
|
|
374
|
+
coordinate(
|
|
375
|
+
constraint.point,
|
|
376
|
+
constraint.kind === 'x' ? 0 : 1,
|
|
377
|
+
constraint.value,
|
|
378
|
+
tag,
|
|
379
|
+
);
|
|
380
|
+
} else if (constraint.kind === 'pointOnCircle') {
|
|
381
|
+
const [p, center] = constraint.points;
|
|
382
|
+
const a = knownPosition(p),
|
|
383
|
+
b = knownPosition(center);
|
|
384
|
+
const radius = knownRadius(constraint.curve, constraint.index);
|
|
385
|
+
if (a && b && radius !== undefined) {
|
|
386
|
+
checkConstant(Math.hypot(a[0] - b[0], a[1] - b[1]), radius, tag);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
gcs.add_constraint_p2p_distance(
|
|
390
|
+
nativePoints[p],
|
|
391
|
+
nativePoints[center],
|
|
392
|
+
(constraint.curve === 'circle' ? radiusIndices : arcRadiusIndices)[
|
|
393
|
+
constraint.index
|
|
394
|
+
],
|
|
395
|
+
tag,
|
|
396
|
+
true,
|
|
397
|
+
1,
|
|
398
|
+
);
|
|
399
|
+
} else if (constraint.kind === 'pointOnLine') {
|
|
400
|
+
const [p, a, b] = constraint.points;
|
|
401
|
+
const known = constraint.points.map(knownPosition);
|
|
402
|
+
if (known.every(p => p !== undefined)) {
|
|
403
|
+
checkConstant(
|
|
404
|
+
pointLineDistance(known[0], known[1], known[2]),
|
|
405
|
+
0,
|
|
406
|
+
tag,
|
|
407
|
+
);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
gcs.add_constraint_point_on_line_ppp(
|
|
411
|
+
nativePoints[p],
|
|
412
|
+
nativePoints[a],
|
|
413
|
+
nativePoints[b],
|
|
414
|
+
tag,
|
|
415
|
+
true,
|
|
416
|
+
1,
|
|
417
|
+
);
|
|
418
|
+
} else if (constraint.kind === 'midpoint') {
|
|
419
|
+
const [m, a, b] = constraint.points;
|
|
420
|
+
// M - A = B - M, independently on each axis. Shared free difference
|
|
421
|
+
// parameters keep these equations linear, even when A and B coincide.
|
|
422
|
+
// Native point symmetry uses a line/bisector and is singular there.
|
|
423
|
+
for (const axis of [0, 1]) {
|
|
424
|
+
const difference = gcs.push_p_param(
|
|
425
|
+
(points[m].position[axis] - points[a].position[axis]) / scale,
|
|
426
|
+
false,
|
|
427
|
+
);
|
|
428
|
+
// PlaneGCS difference is second - first, not first - second.
|
|
429
|
+
gcs.add_constraint_difference(
|
|
430
|
+
indices[a][axis],
|
|
431
|
+
indices[m][axis],
|
|
432
|
+
difference,
|
|
433
|
+
tag,
|
|
434
|
+
true,
|
|
435
|
+
1,
|
|
436
|
+
);
|
|
437
|
+
gcs.add_constraint_difference(
|
|
438
|
+
indices[m][axis],
|
|
439
|
+
indices[b][axis],
|
|
440
|
+
difference,
|
|
441
|
+
tag,
|
|
442
|
+
true,
|
|
443
|
+
1,
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
} else if (
|
|
447
|
+
constraint.kind === 'parallel' ||
|
|
448
|
+
constraint.kind === 'perpendicular' ||
|
|
449
|
+
constraint.kind === 'lineAngle'
|
|
450
|
+
) {
|
|
451
|
+
if (constraint.points.every(i => knownPosition(i))) {
|
|
452
|
+
checkConstant(
|
|
453
|
+
residual(
|
|
454
|
+
constraint,
|
|
455
|
+
points.map((p, i) => knownPosition(i) ?? p.position),
|
|
456
|
+
[],
|
|
457
|
+
[],
|
|
458
|
+
[],
|
|
459
|
+
scale,
|
|
460
|
+
),
|
|
461
|
+
0,
|
|
462
|
+
tag,
|
|
463
|
+
1,
|
|
464
|
+
);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
const [a, b, c, d] = constraint.points.map(i => nativePoints[i]);
|
|
468
|
+
if (constraint.kind === 'parallel') {
|
|
469
|
+
const [ia, ib, ic, id] = constraint.points.map(i => indices[i]);
|
|
470
|
+
const first = gcs.make_line(ia[0], ia[1], ib[0], ib[1]);
|
|
471
|
+
const second = gcs.make_line(ic[0], ic[1], id[0], id[1]);
|
|
472
|
+
geometries.push(first, second);
|
|
473
|
+
gcs.add_constraint_parallel(first, second, tag, true, 1);
|
|
474
|
+
} else if (constraint.kind === 'perpendicular') {
|
|
475
|
+
gcs.add_constraint_perpendicular_pppp(a, b, c, d, tag, true, 1);
|
|
476
|
+
} else {
|
|
477
|
+
gcs.add_constraint_l2l_angle_pppp(
|
|
478
|
+
a,
|
|
479
|
+
b,
|
|
480
|
+
c,
|
|
481
|
+
d,
|
|
482
|
+
constant((constraint.value * Math.PI) / 180),
|
|
483
|
+
tag,
|
|
484
|
+
true,
|
|
485
|
+
1,
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
} else if ('points' in constraint) {
|
|
489
|
+
const [a, b] = constraint.points.map(i => nativePoints[i]);
|
|
490
|
+
switch (constraint.kind) {
|
|
491
|
+
case 'horizontal':
|
|
492
|
+
gcs.add_constraint_horizontal_pp(a, b, tag, true, 1);
|
|
493
|
+
break;
|
|
494
|
+
case 'vertical':
|
|
495
|
+
gcs.add_constraint_vertical_pp(a, b, tag, true, 1);
|
|
496
|
+
break;
|
|
497
|
+
case 'coincident':
|
|
498
|
+
gcs.add_constraint_p2p_coincident(a, b, tag, true, 1);
|
|
499
|
+
break;
|
|
500
|
+
case 'length':
|
|
501
|
+
gcs.add_constraint_p2p_distance(
|
|
502
|
+
a,
|
|
503
|
+
b,
|
|
504
|
+
constant(constraint.value / scale),
|
|
505
|
+
tag,
|
|
506
|
+
true,
|
|
507
|
+
1,
|
|
508
|
+
);
|
|
509
|
+
break;
|
|
510
|
+
case 'angle':
|
|
511
|
+
gcs.add_constraint_p2p_angle(
|
|
512
|
+
a,
|
|
513
|
+
b,
|
|
514
|
+
constant((constraint.value * Math.PI) / 180),
|
|
515
|
+
tag,
|
|
516
|
+
true,
|
|
517
|
+
1,
|
|
518
|
+
);
|
|
519
|
+
break;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
// ArcRules introduce an underdetermined radius/angle system. PlaneGCS
|
|
524
|
+
// sorts its parameters by native address; DogLeg's FullPivLU step then
|
|
525
|
+
// chooses free variables based on allocation history. BFGS initializes
|
|
526
|
+
// arcs without that basic-solution pivot choice or new reference locks.
|
|
527
|
+
// Keep the existing non-arc seed policy: changing it can create new
|
|
528
|
+
// point-on-line incidences before a later drag begins.
|
|
529
|
+
const hardAlgorithm = problem.arcs.length ? 0 : 2;
|
|
530
|
+
const solve = (algorithm = hardAlgorithm) => {
|
|
531
|
+
const status = gcs.solve_system(algorithm);
|
|
532
|
+
const conflicting = constraintIndices(gcs, 'get_conflicting');
|
|
533
|
+
if (status > 1 || conflicting.length)
|
|
534
|
+
throw new SketchConstraintError(
|
|
535
|
+
conflicting,
|
|
536
|
+
`Could not satisfy sketch constraints${conflicting.length ? ` (${conflicting.map(i => i + 1).join(', ')})` : ''}. The constraints may conflict or need a different current geometry.`,
|
|
537
|
+
);
|
|
538
|
+
gcs.apply_solution();
|
|
539
|
+
};
|
|
540
|
+
const redundant: number[] = [];
|
|
541
|
+
if (objectives.length) {
|
|
542
|
+
// Diagnose the actual model before adding soft objectives. PlaneGCS 1.2
|
|
543
|
+
// otherwise includes negative tags in its redundancy consistency solve.
|
|
544
|
+
// Remove only constraints proved fully redundant, retaining every author
|
|
545
|
+
// equation for the independent final residual check below.
|
|
546
|
+
for (;;) {
|
|
547
|
+
solve();
|
|
548
|
+
const found = constraintIndices(gcs, 'get_redundant');
|
|
549
|
+
if (!found.length) break;
|
|
550
|
+
redundant.push(...found);
|
|
551
|
+
for (const index of found) gcs.clear_by_id(index + 1);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
for (const objective of objectives) {
|
|
555
|
+
// Negative tags are PlaneGCS soft objectives; they neither change DOF nor
|
|
556
|
+
// weaken persistent constraints. The gesture is not part of the model.
|
|
557
|
+
if (objective.kind === 'radius') {
|
|
558
|
+
if (knownRadius(objective.curve, objective.index) === undefined) {
|
|
559
|
+
const args = [
|
|
560
|
+
constant(objective.value / scale),
|
|
561
|
+
-1,
|
|
562
|
+
true,
|
|
563
|
+
objective.weight,
|
|
564
|
+
] as const;
|
|
565
|
+
if (objective.curve === 'arc')
|
|
566
|
+
gcs.add_constraint_arc_radius(nativeArcs[objective.index], ...args);
|
|
567
|
+
else
|
|
568
|
+
gcs.add_constraint_circle_radius(
|
|
569
|
+
nativeCircles[objective.index],
|
|
570
|
+
...args,
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
} else
|
|
574
|
+
for (const axis of [0, 1])
|
|
575
|
+
if (knownCoordinate(objective.point, axis) === undefined)
|
|
576
|
+
coordinate(
|
|
577
|
+
objective.point,
|
|
578
|
+
axis,
|
|
579
|
+
objective.position[axis],
|
|
580
|
+
-1,
|
|
581
|
+
objective.weight,
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
// BFGS also accepts a least-squares optimum in soft-only subsystems left
|
|
585
|
+
// by equal-coordinate reduction; their residuals need not all vanish.
|
|
586
|
+
solve(objectives.length ? 0 : hardAlgorithm);
|
|
587
|
+
const positions = indices.map(([x, y], index): SketchPosition => [
|
|
588
|
+
points[index].locked[0]
|
|
589
|
+
? points[index].position[0]
|
|
590
|
+
: gcs.get_p_param(x) * scale + origin[0],
|
|
591
|
+
points[index].locked[1]
|
|
592
|
+
? points[index].position[1]
|
|
593
|
+
: gcs.get_p_param(y) * scale + origin[1],
|
|
594
|
+
]);
|
|
595
|
+
const radii = radiusIndices.map((index, i) =>
|
|
596
|
+
problem.circles[i].locked
|
|
597
|
+
? problem.circles[i].radius
|
|
598
|
+
: gcs.get_p_param(index) * scale,
|
|
599
|
+
);
|
|
600
|
+
const arcRadii = arcRadiusIndices.map((index, i) =>
|
|
601
|
+
problem.arcs[i].locked
|
|
602
|
+
? problem.arcs[i].radius
|
|
603
|
+
: gcs.get_p_param(index) * scale,
|
|
604
|
+
);
|
|
605
|
+
if (
|
|
606
|
+
!positions.every(p => p.every(Number.isFinite)) ||
|
|
607
|
+
!radii.every(Number.isFinite) ||
|
|
608
|
+
!arcRadii.every(Number.isFinite)
|
|
609
|
+
)
|
|
610
|
+
throw new SketchConstraintError(
|
|
611
|
+
[],
|
|
612
|
+
'The sketch solver returned non-finite geometry parameters.',
|
|
613
|
+
);
|
|
614
|
+
// PlaneGCS can return Converged after removing redundant equations even
|
|
615
|
+
// when the applied solution satisfies every authored constraint. Neither
|
|
616
|
+
// Success nor Converged alone is our acceptance criterion: verify all hard
|
|
617
|
+
// equations independently, in normalized geometry units.
|
|
618
|
+
const unsatisfied = constraints.flatMap((c, i) =>
|
|
619
|
+
residual(c, positions, radii, arcRadii, problem.arcs, scale) <=
|
|
620
|
+
precision.nativeAcceptance
|
|
621
|
+
? []
|
|
622
|
+
: [i],
|
|
623
|
+
);
|
|
624
|
+
for (const [i, arc] of problem.arcs.entries()) {
|
|
625
|
+
const center = positions[arc.center];
|
|
626
|
+
if (
|
|
627
|
+
arcRadii[i] <= 0 ||
|
|
628
|
+
arc.points.some(
|
|
629
|
+
point =>
|
|
630
|
+
Math.abs(
|
|
631
|
+
Math.hypot(
|
|
632
|
+
positions[point][0] - center[0],
|
|
633
|
+
positions[point][1] - center[1],
|
|
634
|
+
) - arcRadii[i],
|
|
635
|
+
) /
|
|
636
|
+
scale >
|
|
637
|
+
precision.nativeAcceptance,
|
|
638
|
+
)
|
|
639
|
+
)
|
|
640
|
+
throw new SketchConstraintError(
|
|
641
|
+
[],
|
|
642
|
+
'The sketch solver did not satisfy the arc geometry.',
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
if (unsatisfied.length)
|
|
646
|
+
throw new SketchConstraintError(
|
|
647
|
+
unsatisfied,
|
|
648
|
+
`Sketch constraints ${unsatisfied.map(i => i + 1).join(', ')} did not converge to a valid solution.`,
|
|
649
|
+
);
|
|
650
|
+
return {
|
|
651
|
+
...cleanSolution(
|
|
652
|
+
problem,
|
|
653
|
+
{positions, radii, arcRadii},
|
|
654
|
+
authored,
|
|
655
|
+
objectives,
|
|
656
|
+
precision,
|
|
657
|
+
),
|
|
658
|
+
degreesOfFreedom: gcs.dof(),
|
|
659
|
+
redundant: [
|
|
660
|
+
...new Set([
|
|
661
|
+
...constraintIndices(gcs, 'get_redundant'),
|
|
662
|
+
...redundant,
|
|
663
|
+
...[...constantTags]
|
|
664
|
+
.filter(tag => !activeTags.has(tag))
|
|
665
|
+
.map(tag => tag - 1),
|
|
666
|
+
]),
|
|
667
|
+
].sort((a, b) => a - b),
|
|
668
|
+
};
|
|
669
|
+
} finally {
|
|
670
|
+
for (const geometry of geometries) geometry.delete();
|
|
671
|
+
gcs.clear_data();
|
|
672
|
+
gcs.delete();
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
type Geometry = Pick<SketchSolveResult, 'positions' | 'radii' | 'arcRadii'>;
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Remove numerical tails only within local feature scale, then independently
|
|
680
|
+
* check the complete hard system. Exact input/gesture values take precedence;
|
|
681
|
+
* close points never acquire shared identity through this numeric operation.
|
|
682
|
+
*/
|
|
683
|
+
function cleanSolution(
|
|
684
|
+
problem: SketchSolveProblem,
|
|
685
|
+
result: Geometry,
|
|
686
|
+
authored: SketchSolveProblem,
|
|
687
|
+
objectives: readonly SketchSolveObjective[],
|
|
688
|
+
precision: SketchPrecision,
|
|
689
|
+
): Geometry {
|
|
690
|
+
const {pointScales} = precision;
|
|
691
|
+
const coordinate = (index: number, axis: number, value: number) => {
|
|
692
|
+
if (problem.points[index].locked[axis]) return value;
|
|
693
|
+
const fixed = problem.constraints.find(
|
|
694
|
+
c =>
|
|
695
|
+
(c.kind === 'fixed' || c.kind === (axis === 0 ? 'x' : 'y')) &&
|
|
696
|
+
c.point === index,
|
|
697
|
+
);
|
|
698
|
+
if (fixed?.kind === 'fixed') return fixed.position[axis];
|
|
699
|
+
if (fixed?.kind === 'x' || fixed?.kind === 'y') return fixed.value;
|
|
700
|
+
return precision.clean(value, pointScales[index], [
|
|
701
|
+
...objectives
|
|
702
|
+
.filter(o => o.kind === 'point')
|
|
703
|
+
.filter(o => o.point === index)
|
|
704
|
+
.map(o => o.position[axis]),
|
|
705
|
+
authored.points[index].position[axis],
|
|
706
|
+
]);
|
|
707
|
+
};
|
|
708
|
+
const radius = (curve: 'circle' | 'arc', index: number, value: number) => {
|
|
709
|
+
const entities = curve === 'circle' ? problem.circles : problem.arcs;
|
|
710
|
+
if (entities[index].locked) return value;
|
|
711
|
+
const dimension = problem.constraints.find(
|
|
712
|
+
c => c.kind === 'radius' && c.curve === curve && c.index === index,
|
|
713
|
+
);
|
|
714
|
+
if (dimension?.kind === 'radius') return dimension.value;
|
|
715
|
+
return precision.clean(value, value, [
|
|
716
|
+
(curve === 'circle' ? authored.circles : authored.arcs)[index].radius,
|
|
717
|
+
]);
|
|
718
|
+
};
|
|
719
|
+
const candidate: Geometry = {
|
|
720
|
+
positions: result.positions.map(
|
|
721
|
+
(p, i) =>
|
|
722
|
+
p.map((value, axis) => coordinate(i, axis, value)) as [number, number],
|
|
723
|
+
),
|
|
724
|
+
radii: result.radii.map((value, i) => radius('circle', i, value)),
|
|
725
|
+
arcRadii: result.arcRadii.map((value, i) => radius('arc', i, value)),
|
|
726
|
+
};
|
|
727
|
+
// A stricter threshold than native solve acceptance prevents cleanup from
|
|
728
|
+
// consuming its tolerance budget. Use each equation's own geometry scale.
|
|
729
|
+
const valid =
|
|
730
|
+
problem.constraints.every(c => {
|
|
731
|
+
const scale =
|
|
732
|
+
c.kind === 'length' || c.kind === 'radius'
|
|
733
|
+
? c.value
|
|
734
|
+
: c.kind === 'sweep'
|
|
735
|
+
? problem.arcs[c.index].radius
|
|
736
|
+
: 'point' in c
|
|
737
|
+
? pointScales[c.point]
|
|
738
|
+
: Math.min(...c.points.map(i => pointScales[i]));
|
|
739
|
+
return (
|
|
740
|
+
residual(
|
|
741
|
+
c,
|
|
742
|
+
candidate.positions,
|
|
743
|
+
candidate.radii,
|
|
744
|
+
candidate.arcRadii,
|
|
745
|
+
problem.arcs,
|
|
746
|
+
scale,
|
|
747
|
+
) <= precision.relative
|
|
748
|
+
);
|
|
749
|
+
}) &&
|
|
750
|
+
problem.arcs.every(
|
|
751
|
+
(arc, i) =>
|
|
752
|
+
candidate.positions[arc.points[0]].some(
|
|
753
|
+
(v, axis) => v !== candidate.positions[arc.points[1]][axis],
|
|
754
|
+
) &&
|
|
755
|
+
arc.points.every(
|
|
756
|
+
index =>
|
|
757
|
+
Math.abs(
|
|
758
|
+
Math.hypot(
|
|
759
|
+
...candidate.positions[index].map(
|
|
760
|
+
(v, axis) => v - candidate.positions[arc.center][axis],
|
|
761
|
+
),
|
|
762
|
+
) - candidate.arcRadii[i],
|
|
763
|
+
) <= precision.tolerance(arc.radius, candidate.arcRadii[i]),
|
|
764
|
+
),
|
|
765
|
+
) &&
|
|
766
|
+
problem.lines.every(([a, b]) =>
|
|
767
|
+
candidate.positions[a].some(
|
|
768
|
+
(v, axis) => v !== candidate.positions[b][axis],
|
|
769
|
+
),
|
|
770
|
+
);
|
|
771
|
+
return valid ? candidate : result;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* Radius is current geometry, not a fixed dimension. Seed free endpoint axes
|
|
776
|
+
* along their authored directions before solving structural and authored
|
|
777
|
+
* equations together. Shared endpoints average simultaneous proposals, the
|
|
778
|
+
* least-squares starting position, rather than giving the last arc ownership.
|
|
779
|
+
* No initialization objective or lock enters the solver or model DOF.
|
|
780
|
+
*/
|
|
781
|
+
function initializeArcEndpoints(
|
|
782
|
+
problem: SketchSolveProblem,
|
|
783
|
+
precision: SketchPrecision,
|
|
784
|
+
): SketchSolveProblem {
|
|
785
|
+
if (!problem.arcs.length) return problem;
|
|
786
|
+
// An explicit radius already determines this scalar. Start there rather than
|
|
787
|
+
// projecting satisfied endpoints to conflicting data and asking an
|
|
788
|
+
// underconstrained solve to shrink them again (which can translate the arc).
|
|
789
|
+
// Keep locked values and all equations: contradictory dimensions still fail.
|
|
790
|
+
const arcs = problem.arcs.map((arc, index) => {
|
|
791
|
+
const dimension = problem.constraints.find(
|
|
792
|
+
c => c.kind === 'radius' && c.curve === 'arc' && c.index === index,
|
|
793
|
+
);
|
|
794
|
+
return !arc.locked && dimension?.kind === 'radius'
|
|
795
|
+
? {...arc, radius: dimension.value}
|
|
796
|
+
: arc;
|
|
797
|
+
});
|
|
798
|
+
const proposals = problem.points.map(() => [] as SketchPosition[]);
|
|
799
|
+
for (const arc of arcs) {
|
|
800
|
+
const center = problem.points[arc.center].position;
|
|
801
|
+
for (const index of arc.points) {
|
|
802
|
+
const point = problem.points[index].position;
|
|
803
|
+
const dx = point[0] - center[0],
|
|
804
|
+
dy = point[1] - center[1];
|
|
805
|
+
const length = Math.hypot(dx, dy);
|
|
806
|
+
if (!length)
|
|
807
|
+
throw new Error(
|
|
808
|
+
'Sketch arcs require a nonzero radius and distinct endpoints; use circle for a full circle.',
|
|
809
|
+
);
|
|
810
|
+
// Already valid current geometry is a seed, not an instruction to
|
|
811
|
+
// re-project rounded solver output on every forward compilation.
|
|
812
|
+
if (Math.abs(length - arc.radius) <= precision.tolerance(arc.radius))
|
|
813
|
+
continue;
|
|
814
|
+
proposals[index].push([
|
|
815
|
+
center[0] + (dx / length) * arc.radius,
|
|
816
|
+
center[1] + (dy / length) * arc.radius,
|
|
817
|
+
]);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
return {
|
|
821
|
+
...problem,
|
|
822
|
+
arcs,
|
|
823
|
+
points: problem.points.map((point, index) => {
|
|
824
|
+
const coordinate = (axis: 0 | 1) => {
|
|
825
|
+
const value = point.position[axis];
|
|
826
|
+
const targets = proposals[index];
|
|
827
|
+
if (
|
|
828
|
+
!targets.length ||
|
|
829
|
+
point.locked[axis] ||
|
|
830
|
+
problem.constraints.some(
|
|
831
|
+
c =>
|
|
832
|
+
(c.kind === 'fixed' || c.kind === 'x' || c.kind === 'y') &&
|
|
833
|
+
c.point === index &&
|
|
834
|
+
(c.kind === 'fixed' || c.kind === (axis === 0 ? 'x' : 'y')),
|
|
835
|
+
)
|
|
836
|
+
)
|
|
837
|
+
return value;
|
|
838
|
+
return targets.reduce(
|
|
839
|
+
(sum, p) => sum + (p[axis] - value) / targets.length,
|
|
840
|
+
value,
|
|
841
|
+
);
|
|
842
|
+
};
|
|
843
|
+
return {...point, position: [coordinate(0), coordinate(1)]};
|
|
844
|
+
}),
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
function residual(
|
|
849
|
+
c: SketchSolveConstraint,
|
|
850
|
+
positions: readonly SketchPosition[],
|
|
851
|
+
radii: readonly number[],
|
|
852
|
+
arcRadii: readonly number[],
|
|
853
|
+
arcs: SketchSolveProblem['arcs'],
|
|
854
|
+
scale: number,
|
|
855
|
+
): number {
|
|
856
|
+
switch (c.kind) {
|
|
857
|
+
case 'parallel':
|
|
858
|
+
case 'perpendicular':
|
|
859
|
+
case 'lineAngle': {
|
|
860
|
+
const [a, b, d, e] = c.points.map(i => positions[i]);
|
|
861
|
+
const x1 = b[0] - a[0],
|
|
862
|
+
y1 = b[1] - a[1];
|
|
863
|
+
const x2 = e[0] - d[0],
|
|
864
|
+
y2 = e[1] - d[1];
|
|
865
|
+
const length = Math.hypot(x1, y1) * Math.hypot(x2, y2);
|
|
866
|
+
if (!length) return Infinity;
|
|
867
|
+
if (c.kind === 'parallel') return Math.abs(x1 * y2 - y1 * x2) / length;
|
|
868
|
+
if (c.kind === 'perpendicular')
|
|
869
|
+
return Math.abs(x1 * x2 + y1 * y2) / length;
|
|
870
|
+
const difference =
|
|
871
|
+
Math.atan2(y2, x2) - Math.atan2(y1, x1) - (c.value * Math.PI) / 180;
|
|
872
|
+
return Math.abs(Math.atan2(Math.sin(difference), Math.cos(difference)));
|
|
873
|
+
}
|
|
874
|
+
case 'pointOnCircle': {
|
|
875
|
+
const [p, center] = c.points.map(i => positions[i]);
|
|
876
|
+
const radius = (c.curve === 'circle' ? radii : arcRadii)[c.index];
|
|
877
|
+
return (
|
|
878
|
+
Math.abs(Math.hypot(p[0] - center[0], p[1] - center[1]) - radius) /
|
|
879
|
+
scale
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
case 'pointOnLine': {
|
|
883
|
+
const [p, a, b] = c.points.map(i => positions[i]);
|
|
884
|
+
return pointLineDistance(p, a, b) / scale;
|
|
885
|
+
}
|
|
886
|
+
case 'sweep': {
|
|
887
|
+
const arc = arcs[c.index];
|
|
888
|
+
const curve = sketchArcGeometry(
|
|
889
|
+
positions[arc.center],
|
|
890
|
+
positions[arc.points[0]],
|
|
891
|
+
positions[arc.points[1]],
|
|
892
|
+
arc.direction,
|
|
893
|
+
);
|
|
894
|
+
return Math.abs(Math.abs(curve.sweep) - (c.value * Math.PI) / 180);
|
|
895
|
+
}
|
|
896
|
+
case 'radius':
|
|
897
|
+
return (
|
|
898
|
+
Math.abs((c.curve === 'circle' ? radii : arcRadii)[c.index] - c.value) /
|
|
899
|
+
scale
|
|
900
|
+
);
|
|
901
|
+
case 'fixed':
|
|
902
|
+
return Math.hypot(
|
|
903
|
+
...positions[c.point].map((v, i) => (v - c.position[i]) / scale),
|
|
904
|
+
);
|
|
905
|
+
case 'x':
|
|
906
|
+
case 'y':
|
|
907
|
+
return (
|
|
908
|
+
Math.abs(positions[c.point][c.kind === 'x' ? 0 : 1] - c.value) / scale
|
|
909
|
+
);
|
|
910
|
+
case 'midpoint': {
|
|
911
|
+
const [m, a, b] = c.points.map(i => positions[i]);
|
|
912
|
+
return Math.hypot(
|
|
913
|
+
...m.map(
|
|
914
|
+
(v, axis) => ((v - a[axis]) / scale + (v - b[axis]) / scale) / 2,
|
|
915
|
+
),
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
default: {
|
|
919
|
+
const [a, b] = c.points.map(i => positions[i]);
|
|
920
|
+
const dx = (b[0] - a[0]) / scale,
|
|
921
|
+
dy = (b[1] - a[1]) / scale;
|
|
922
|
+
switch (c.kind) {
|
|
923
|
+
case 'horizontal':
|
|
924
|
+
return Math.abs(dy);
|
|
925
|
+
case 'vertical':
|
|
926
|
+
return Math.abs(dx);
|
|
927
|
+
case 'coincident':
|
|
928
|
+
return Math.hypot(dx, dy);
|
|
929
|
+
case 'length':
|
|
930
|
+
return Math.abs(Math.hypot(dx, dy) - c.value / scale);
|
|
931
|
+
case 'angle': {
|
|
932
|
+
const difference = Math.atan2(dy, dx) - (c.value * Math.PI) / 180;
|
|
933
|
+
return Math.abs(
|
|
934
|
+
Math.atan2(Math.sin(difference), Math.cos(difference)),
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/** Native scale weights multiply residuals, so their squares weight the mean. */
|
|
943
|
+
function leastSquaresTarget(
|
|
944
|
+
current: number,
|
|
945
|
+
targets: readonly Readonly<{value: number; weight: number}>[],
|
|
946
|
+
): number {
|
|
947
|
+
if (!targets.length) return current;
|
|
948
|
+
const total = targets.reduce((sum, target) => sum + target.weight ** 2, 0);
|
|
949
|
+
return (
|
|
950
|
+
targets.reduce(
|
|
951
|
+
(sum, target) => sum + target.value * target.weight ** 2,
|
|
952
|
+
0,
|
|
953
|
+
) / total
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
function constraintIndices(
|
|
958
|
+
gcs: GcsSystem,
|
|
959
|
+
method: 'get_conflicting' | 'get_redundant',
|
|
960
|
+
): number[] {
|
|
961
|
+
const vector = gcs[method]();
|
|
962
|
+
try {
|
|
963
|
+
return [
|
|
964
|
+
...new Set(
|
|
965
|
+
Array.from({length: vector.size()}, (_, i) => vector.get(i) - 1).filter(
|
|
966
|
+
i => i >= 0,
|
|
967
|
+
),
|
|
968
|
+
),
|
|
969
|
+
];
|
|
970
|
+
} finally {
|
|
971
|
+
vector.delete();
|
|
972
|
+
}
|
|
973
|
+
}
|