@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,89 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getOC,
|
|
3
|
+
makeCircle,
|
|
4
|
+
makeLine,
|
|
5
|
+
makeThreePointArc,
|
|
6
|
+
type Edge,
|
|
7
|
+
type Face,
|
|
8
|
+
type Wire,
|
|
9
|
+
} from 'replicad';
|
|
10
|
+
import {castOwnedShape} from './kernel-shapes.js';
|
|
11
|
+
import {sketchCurvePosition, type SketchCurve} from './sketch-curves.js';
|
|
12
|
+
import type {SketchPosition} from './sketch.js';
|
|
13
|
+
import type {SketchRegion} from './sketch-regions.js';
|
|
14
|
+
|
|
15
|
+
/** A right-handed horizontal sketch plane: (x, y) -> (x, 0, -y), normal +Y. */
|
|
16
|
+
type KernelPoint = [number, number, number];
|
|
17
|
+
const position = (p: SketchPosition): KernelPoint => [p[0], 0, -p[1]];
|
|
18
|
+
|
|
19
|
+
function boundaryEdge(curve: SketchCurve): Edge {
|
|
20
|
+
if (curve.kind === 'line')
|
|
21
|
+
return makeLine(position(curve.points[0]), position(curve.points[1]));
|
|
22
|
+
if (curve.kind === 'circle' || Math.abs(curve.sweep) === 2 * Math.PI)
|
|
23
|
+
return makeCircle(curve.radius, position(curve.center), [
|
|
24
|
+
0,
|
|
25
|
+
curve.kind === 'circle' || curve.sweep > 0 ? 1 : -1,
|
|
26
|
+
0,
|
|
27
|
+
]);
|
|
28
|
+
return makeThreePointArc(
|
|
29
|
+
...([0, 0.5, 1].map(t => position(sketchCurvePosition(curve, t))) as [
|
|
30
|
+
KernelPoint,
|
|
31
|
+
KernelPoint,
|
|
32
|
+
KernelPoint,
|
|
33
|
+
]),
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function boundaryWire(curves: readonly SketchCurve[]): Wire {
|
|
38
|
+
const oc = getOC();
|
|
39
|
+
const builder = new oc.BRepBuilderAPI_MakeWire();
|
|
40
|
+
const edges: Edge[] = [];
|
|
41
|
+
try {
|
|
42
|
+
for (const curve of curves) {
|
|
43
|
+
const edge = boundaryEdge(curve);
|
|
44
|
+
edges.push(edge);
|
|
45
|
+
builder.Add(edge.wrapped);
|
|
46
|
+
}
|
|
47
|
+
if (!builder.IsDone())
|
|
48
|
+
throw new Error('Could not connect the sketch contour into a wire.');
|
|
49
|
+
return castOwnedShape(builder.Wire()) as Wire;
|
|
50
|
+
} finally {
|
|
51
|
+
builder.delete();
|
|
52
|
+
edges.forEach(edge => edge.delete());
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function sketchRegionFace(region: SketchRegion): Face {
|
|
57
|
+
const oc = getOC();
|
|
58
|
+
const wires: Wire[] = [];
|
|
59
|
+
try {
|
|
60
|
+
for (const curves of [region.outer, ...region.holes])
|
|
61
|
+
wires.push(boundaryWire(curves));
|
|
62
|
+
const builder = new oc.BRepBuilderAPI_MakeFace(wires[0].wrapped, true);
|
|
63
|
+
try {
|
|
64
|
+
for (const hole of wires.slice(1)) builder.Add(hole.wrapped);
|
|
65
|
+
if (!builder.IsDone())
|
|
66
|
+
throw new Error('Could not create a planar sketch face.');
|
|
67
|
+
const face = castOwnedShape(builder.Face()) as Face;
|
|
68
|
+
try {
|
|
69
|
+
const analyzer = new oc.BRepCheck_Analyzer(face.wrapped, true, false);
|
|
70
|
+
try {
|
|
71
|
+
if (!analyzer.IsValid())
|
|
72
|
+
throw new Error(
|
|
73
|
+
'The sketch contours do not form a valid planar face.',
|
|
74
|
+
);
|
|
75
|
+
} finally {
|
|
76
|
+
analyzer.delete();
|
|
77
|
+
}
|
|
78
|
+
return face;
|
|
79
|
+
} catch (error) {
|
|
80
|
+
face.delete();
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
} finally {
|
|
84
|
+
builder.delete();
|
|
85
|
+
}
|
|
86
|
+
} finally {
|
|
87
|
+
wires.forEach(wire => wire.delete());
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import type {SketchPosition} from './sketch.js';
|
|
2
|
+
import type {SketchSolveProblem, SketchSolveResult} from './sketch-solver.js';
|
|
3
|
+
import {
|
|
4
|
+
sketchCurveTolerance,
|
|
5
|
+
sketchArcGeometry,
|
|
6
|
+
sketchCurveClosestParameter,
|
|
7
|
+
sketchCurvePosition,
|
|
8
|
+
type SketchCurve,
|
|
9
|
+
} from './sketch-curves.js';
|
|
10
|
+
|
|
11
|
+
export type SketchIncidenceGeometry = Readonly<{
|
|
12
|
+
points: readonly SketchPosition[];
|
|
13
|
+
lines: SketchSolveProblem['lines'];
|
|
14
|
+
circles: readonly Pick<
|
|
15
|
+
SketchSolveProblem['circles'][number],
|
|
16
|
+
'center' | 'radius'
|
|
17
|
+
>[];
|
|
18
|
+
arcs: readonly Omit<SketchSolveProblem['arcs'][number], 'locked'>[];
|
|
19
|
+
}>;
|
|
20
|
+
export type SketchIncidence = Readonly<{
|
|
21
|
+
point: number;
|
|
22
|
+
kind: 'line' | 'circle' | 'arc';
|
|
23
|
+
index: number;
|
|
24
|
+
}>;
|
|
25
|
+
|
|
26
|
+
export function sketchIncidenceGeometry(
|
|
27
|
+
problem: SketchSolveProblem,
|
|
28
|
+
result?: SketchSolveResult,
|
|
29
|
+
): SketchIncidenceGeometry {
|
|
30
|
+
return {
|
|
31
|
+
points: result?.positions ?? problem.points.map(p => p.position),
|
|
32
|
+
lines: problem.lines,
|
|
33
|
+
circles: problem.circles.map((c, i) => ({
|
|
34
|
+
...c,
|
|
35
|
+
radius: result ? result.radii[i] : c.radius,
|
|
36
|
+
})),
|
|
37
|
+
arcs: problem.arcs.map((a, i) => ({
|
|
38
|
+
...a,
|
|
39
|
+
radius: result ? result.arcRadii[i] : a.radius,
|
|
40
|
+
})),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function sketchIncidenceCurve(
|
|
45
|
+
geometry: SketchIncidenceGeometry,
|
|
46
|
+
contact: SketchIncidence,
|
|
47
|
+
): SketchCurve {
|
|
48
|
+
if (contact.kind === 'line')
|
|
49
|
+
return {
|
|
50
|
+
kind: 'line',
|
|
51
|
+
points: geometry.lines[contact.index].map(i => geometry.points[i]) as [
|
|
52
|
+
SketchPosition,
|
|
53
|
+
SketchPosition,
|
|
54
|
+
],
|
|
55
|
+
};
|
|
56
|
+
const curve = (contact.kind === 'circle' ? geometry.circles : geometry.arcs)[
|
|
57
|
+
contact.index
|
|
58
|
+
];
|
|
59
|
+
const center = geometry.points[curve.center];
|
|
60
|
+
if (contact.kind === 'circle')
|
|
61
|
+
return {kind: 'circle', center, radius: curve.radius};
|
|
62
|
+
const arc = geometry.arcs[contact.index];
|
|
63
|
+
return {
|
|
64
|
+
...sketchArcGeometry(
|
|
65
|
+
center,
|
|
66
|
+
geometry.points[arc.points[0]],
|
|
67
|
+
geometry.points[arc.points[1]],
|
|
68
|
+
arc.direction,
|
|
69
|
+
),
|
|
70
|
+
radius: arc.radius,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function sketchIncidencePoints(
|
|
75
|
+
geometry: SketchIncidenceGeometry,
|
|
76
|
+
contact: SketchIncidence,
|
|
77
|
+
): readonly number[] {
|
|
78
|
+
return [
|
|
79
|
+
contact.point,
|
|
80
|
+
...(contact.kind === 'line'
|
|
81
|
+
? geometry.lines[contact.index]
|
|
82
|
+
: contact.kind === 'circle'
|
|
83
|
+
? [geometry.circles[contact.index].center]
|
|
84
|
+
: [
|
|
85
|
+
geometry.arcs[contact.index].center,
|
|
86
|
+
...geometry.arcs[contact.index].points,
|
|
87
|
+
]),
|
|
88
|
+
];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function isPointOnSketchCurve(
|
|
92
|
+
point: SketchPosition,
|
|
93
|
+
curve: SketchCurve,
|
|
94
|
+
): boolean {
|
|
95
|
+
if (curve.kind === 'line')
|
|
96
|
+
return isPointOnSketchSegment(point, ...curve.points);
|
|
97
|
+
const nearest = sketchCurvePosition(
|
|
98
|
+
curve,
|
|
99
|
+
sketchCurveClosestParameter(curve, point),
|
|
100
|
+
);
|
|
101
|
+
return (
|
|
102
|
+
Math.hypot(point[0] - nearest[0], point[1] - nearest[1]) <=
|
|
103
|
+
sketchCurveTolerance(curve)
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Finite boundary violated by a solution on the underlying unbounded curve. */
|
|
108
|
+
export function sketchIncidenceBoundary(
|
|
109
|
+
geometry: SketchIncidenceGeometry,
|
|
110
|
+
contact: SketchIncidence,
|
|
111
|
+
): number | undefined {
|
|
112
|
+
if (contact.kind === 'circle') return;
|
|
113
|
+
const curve = sketchIncidenceCurve(geometry, contact);
|
|
114
|
+
const point = geometry.points[contact.point];
|
|
115
|
+
if (isPointOnSketchCurve(point, curve)) return;
|
|
116
|
+
const endpoints =
|
|
117
|
+
contact.kind === 'line'
|
|
118
|
+
? geometry.lines[contact.index]
|
|
119
|
+
: geometry.arcs[contact.index].points;
|
|
120
|
+
const t = sketchCurveClosestParameter(curve, point);
|
|
121
|
+
if (t === 0 || t === 1) return endpoints[t];
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Recognition is gesture-local and shares finite geometry/tolerance with trimming. */
|
|
126
|
+
export function sketchIncidences(
|
|
127
|
+
geometry: SketchIncidenceGeometry,
|
|
128
|
+
): readonly SketchIncidence[] {
|
|
129
|
+
const seen = new Set<string>();
|
|
130
|
+
return (['line', 'circle', 'arc'] as const).flatMap(kind => {
|
|
131
|
+
const curves =
|
|
132
|
+
kind === 'line'
|
|
133
|
+
? geometry.lines
|
|
134
|
+
: kind === 'circle'
|
|
135
|
+
? geometry.circles
|
|
136
|
+
: geometry.arcs;
|
|
137
|
+
return curves.flatMap((_, index) =>
|
|
138
|
+
geometry.points.flatMap((position, point): SketchIncidence[] => {
|
|
139
|
+
const contact = {point, kind, index};
|
|
140
|
+
if (sketchIncidencePoints(geometry, contact).slice(1).includes(point))
|
|
141
|
+
return [];
|
|
142
|
+
if (
|
|
143
|
+
!isPointOnSketchCurve(
|
|
144
|
+
position,
|
|
145
|
+
sketchIncidenceCurve(geometry, contact),
|
|
146
|
+
)
|
|
147
|
+
)
|
|
148
|
+
return [];
|
|
149
|
+
const key =
|
|
150
|
+
kind === 'line'
|
|
151
|
+
? [point, ...[...geometry.lines[index]].sort((a, b) => a - b)].join(
|
|
152
|
+
':',
|
|
153
|
+
)
|
|
154
|
+
: kind + ':' + point + ':' + index;
|
|
155
|
+
if (seen.has(key)) return [];
|
|
156
|
+
seen.add(key);
|
|
157
|
+
return [contact];
|
|
158
|
+
}),
|
|
159
|
+
);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function lineParameter(
|
|
164
|
+
point: SketchPosition,
|
|
165
|
+
a: SketchPosition,
|
|
166
|
+
b: SketchPosition,
|
|
167
|
+
): number {
|
|
168
|
+
const dx = b[0] - a[0],
|
|
169
|
+
dy = b[1] - a[1];
|
|
170
|
+
const length = Math.hypot(dx, dy);
|
|
171
|
+
return (
|
|
172
|
+
((point[0] - a[0]) * (dx / length) + (point[1] - a[1]) * (dy / length)) /
|
|
173
|
+
length
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function pointLineDistance(
|
|
178
|
+
point: SketchPosition,
|
|
179
|
+
a: SketchPosition,
|
|
180
|
+
b: SketchPosition,
|
|
181
|
+
): number {
|
|
182
|
+
const dx = b[0] - a[0],
|
|
183
|
+
dy = b[1] - a[1];
|
|
184
|
+
const length = Math.hypot(dx, dy);
|
|
185
|
+
return Math.abs(
|
|
186
|
+
(point[0] - a[0]) * (dy / length) - (point[1] - a[1]) * (dx / length),
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function isPointOnSketchSegment(
|
|
191
|
+
point: SketchPosition,
|
|
192
|
+
a: SketchPosition,
|
|
193
|
+
b: SketchPosition,
|
|
194
|
+
): boolean {
|
|
195
|
+
const length = Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
196
|
+
if (!length) return false;
|
|
197
|
+
const tolerance = sketchCurveTolerance({kind: 'line', points: [a, b]});
|
|
198
|
+
const t = lineParameter(point, a, b);
|
|
199
|
+
return (
|
|
200
|
+
t >= -tolerance / length &&
|
|
201
|
+
t <= 1 + tolerance / length &&
|
|
202
|
+
pointLineDistance(point, a, b) <= tolerance
|
|
203
|
+
);
|
|
204
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type {SketchSolveProblem} from './sketch-solver.js';
|
|
2
|
+
|
|
3
|
+
export const sketchRelativePrecision = 1e-9;
|
|
4
|
+
|
|
5
|
+
/** Feature-local numeric budget, independent of screen/grid size or decimal
|
|
6
|
+
* count. Native normalized steps and decimal cleanup use fractions of it.
|
|
7
|
+
* An unrelated large object must not erase a deliberate small feature.
|
|
8
|
+
*/
|
|
9
|
+
export class SketchPrecision {
|
|
10
|
+
readonly relative = sketchRelativePrecision;
|
|
11
|
+
/** Reject invalid native output before the stricter local cleanup check. */
|
|
12
|
+
readonly nativeAcceptance = this.relative * 100;
|
|
13
|
+
readonly pointScales: readonly number[];
|
|
14
|
+
readonly origin: readonly [number, number];
|
|
15
|
+
readonly scale: number;
|
|
16
|
+
readonly convergence: number;
|
|
17
|
+
|
|
18
|
+
constructor(problem: SketchSolveProblem) {
|
|
19
|
+
this.origin = problem.points[0]?.position ?? [0, 0];
|
|
20
|
+
this.pointScales = problem.points.map((point, index) => {
|
|
21
|
+
const sizes = [
|
|
22
|
+
...problem.lines
|
|
23
|
+
.filter(line => line.includes(index))
|
|
24
|
+
.map(([a, b]) =>
|
|
25
|
+
Math.hypot(
|
|
26
|
+
...problem.points[a].position.map(
|
|
27
|
+
(v, axis) => v - problem.points[b].position[axis],
|
|
28
|
+
),
|
|
29
|
+
),
|
|
30
|
+
),
|
|
31
|
+
...problem.circles.filter(c => c.center === index).map(c => c.radius),
|
|
32
|
+
...problem.arcs
|
|
33
|
+
.filter(a => a.center === index || a.points.includes(index))
|
|
34
|
+
.map(a => a.radius),
|
|
35
|
+
].filter(size => size > 0);
|
|
36
|
+
return sizes.length
|
|
37
|
+
? Math.min(...sizes)
|
|
38
|
+
: Math.max(...point.position.map(Math.abs)) || 1;
|
|
39
|
+
});
|
|
40
|
+
this.scale =
|
|
41
|
+
Math.max(
|
|
42
|
+
...problem.points.flatMap(p =>
|
|
43
|
+
p.position.map((v, axis) => Math.abs(v - this.origin[axis])),
|
|
44
|
+
),
|
|
45
|
+
...problem.circles.map(c => c.radius),
|
|
46
|
+
...problem.arcs.map(a => a.radius),
|
|
47
|
+
...problem.constraints.flatMap(c =>
|
|
48
|
+
c.kind === 'length' || c.kind === 'radius' ? [c.value] : [],
|
|
49
|
+
),
|
|
50
|
+
) || 1;
|
|
51
|
+
this.convergence = Math.max(
|
|
52
|
+
Number.EPSILON * 8,
|
|
53
|
+
((Math.min(this.scale, ...this.pointScales) / this.scale) *
|
|
54
|
+
this.relative) /
|
|
55
|
+
10000,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
tolerance(scale: number, value = 0): number {
|
|
60
|
+
return Math.max(
|
|
61
|
+
scale * this.relative,
|
|
62
|
+
Math.abs(value) * Number.EPSILON * 8,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
clean(value: number, scale: number, preferred: readonly number[]): number {
|
|
67
|
+
const tolerance = this.tolerance(scale, value);
|
|
68
|
+
for (const target of preferred)
|
|
69
|
+
if (Math.abs(target - value) <= tolerance) return target;
|
|
70
|
+
const rounding = Math.max(
|
|
71
|
+
(scale * this.relative) / 100,
|
|
72
|
+
Math.abs(value) * Number.EPSILON * 8,
|
|
73
|
+
);
|
|
74
|
+
if (Math.abs(value) <= rounding) return 0;
|
|
75
|
+
for (let digits = 1; digits < 16; digits++) {
|
|
76
|
+
const rounded = Number(value.toPrecision(digits));
|
|
77
|
+
if (Math.abs(rounded - value) <= rounding) return rounded;
|
|
78
|
+
}
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import {
|
|
2
|
+
sketchPointResolver,
|
|
3
|
+
type SketchSnapshot,
|
|
4
|
+
type SketchPosition,
|
|
5
|
+
} from './sketch.js';
|
|
6
|
+
import {
|
|
7
|
+
sketchCurveGeometry,
|
|
8
|
+
sketchCurvePosition,
|
|
9
|
+
sketchCurveTolerance,
|
|
10
|
+
sketchPositiveAngle,
|
|
11
|
+
type SketchCurve,
|
|
12
|
+
} from './sketch-curves.js';
|
|
13
|
+
import {sketchCurveIntersections} from './sketch-curve-intersections.js';
|
|
14
|
+
import {isPointOnSketchCurve} from './sketch-incidence.js';
|
|
15
|
+
|
|
16
|
+
/** Evaluation-local boundaries. Neither array order nor a kernel wire is an author ID. */
|
|
17
|
+
export type SketchRegion = Readonly<{
|
|
18
|
+
outer: readonly SketchCurve[];
|
|
19
|
+
holes: readonly (readonly SketchCurve[])[];
|
|
20
|
+
}>;
|
|
21
|
+
|
|
22
|
+
type Boundary = {
|
|
23
|
+
curve: SketchCurve;
|
|
24
|
+
label: string;
|
|
25
|
+
ends?: readonly [number, number];
|
|
26
|
+
};
|
|
27
|
+
const tau = 2 * Math.PI;
|
|
28
|
+
const distance = (a: SketchPosition, b: SketchPosition) =>
|
|
29
|
+
Math.hypot(a[0] - b[0], a[1] - b[1]);
|
|
30
|
+
|
|
31
|
+
/** Closed, non-intersecting contours; nested contours alternate material and holes. */
|
|
32
|
+
export function sketchRegions(
|
|
33
|
+
layers: readonly SketchSnapshot[],
|
|
34
|
+
): readonly SketchRegion[] {
|
|
35
|
+
const resolve = sketchPointResolver(layers);
|
|
36
|
+
const points = new Map(
|
|
37
|
+
layers.flatMap(layer =>
|
|
38
|
+
layer.entities.flatMap(e =>
|
|
39
|
+
e.kind === 'point'
|
|
40
|
+
? [[JSON.stringify([layer.id, e.id]), e.position] as const]
|
|
41
|
+
: [],
|
|
42
|
+
),
|
|
43
|
+
),
|
|
44
|
+
);
|
|
45
|
+
const boundaries: Boundary[] = layers.flatMap(layer =>
|
|
46
|
+
layer.entities.flatMap(entity => {
|
|
47
|
+
const curve = sketchCurveGeometry(entity, ref => {
|
|
48
|
+
const point = resolve(ref);
|
|
49
|
+
return points.get(JSON.stringify([point.layer, point.id]))!;
|
|
50
|
+
});
|
|
51
|
+
return curve ? [{curve, label: `${layer.id}:${entity.id}`}] : [];
|
|
52
|
+
}),
|
|
53
|
+
);
|
|
54
|
+
const vertices: {
|
|
55
|
+
position: SketchPosition;
|
|
56
|
+
tolerance: number;
|
|
57
|
+
edges: number[];
|
|
58
|
+
}[] = [];
|
|
59
|
+
const vertex = (position: SketchPosition, tolerance: number) => {
|
|
60
|
+
const index = vertices.findIndex(
|
|
61
|
+
v => distance(v.position, position) <= Math.min(v.tolerance, tolerance),
|
|
62
|
+
);
|
|
63
|
+
if (index >= 0) return index;
|
|
64
|
+
vertices.push({position, tolerance, edges: []});
|
|
65
|
+
return vertices.length - 1;
|
|
66
|
+
};
|
|
67
|
+
for (const [i, boundary] of boundaries.entries()) {
|
|
68
|
+
const {curve} = boundary;
|
|
69
|
+
if (curve.kind === 'circle') continue;
|
|
70
|
+
const tolerance = sketchCurveTolerance(curve);
|
|
71
|
+
const ends = [
|
|
72
|
+
vertex(sketchCurvePosition(curve, 0), tolerance),
|
|
73
|
+
vertex(sketchCurvePosition(curve, 1), tolerance),
|
|
74
|
+
] as const;
|
|
75
|
+
if (ends[0] === ends[1])
|
|
76
|
+
throw new Error(`Sketch boundary ${boundary.label} is degenerate.`);
|
|
77
|
+
boundary.ends = ends;
|
|
78
|
+
ends.forEach(v => vertices[v].edges.push(i));
|
|
79
|
+
}
|
|
80
|
+
for (let i = 0; i < boundaries.length; i++) {
|
|
81
|
+
const a = boundaries[i];
|
|
82
|
+
for (const b of boundaries.slice(i + 1)) {
|
|
83
|
+
const contacts = sketchCurveIntersections(a.curve, b.curve);
|
|
84
|
+
if (
|
|
85
|
+
overlap(
|
|
86
|
+
a.curve,
|
|
87
|
+
b.curve,
|
|
88
|
+
contacts.map(c => c.parameters[0]),
|
|
89
|
+
) ||
|
|
90
|
+
contacts.some(c =>
|
|
91
|
+
c.parameters.some((_, j) => {
|
|
92
|
+
const curve = j === 0 ? a.curve : b.curve;
|
|
93
|
+
return (
|
|
94
|
+
curve.kind === 'circle' ||
|
|
95
|
+
(distance(c.position, sketchCurvePosition(curve, 0)) >
|
|
96
|
+
sketchCurveTolerance(curve) &&
|
|
97
|
+
distance(c.position, sketchCurvePosition(curve, 1)) >
|
|
98
|
+
sketchCurveTolerance(curve))
|
|
99
|
+
);
|
|
100
|
+
}),
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
throw new Error(
|
|
104
|
+
`Sketch boundaries ${a.label} and ${b.label} intersect or overlap. Trim them into separate closed contours before creating faces.`,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
for (const v of vertices) {
|
|
109
|
+
if (v.edges.length !== 2)
|
|
110
|
+
throw new Error(
|
|
111
|
+
`Sketch has an ${v.edges.length === 1 ? 'open' : 'ambiguous branching'} contour at [${v.position.join(', ')}]. Faces require closed, non-branching boundaries.`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
const visited = new Set<number>();
|
|
115
|
+
const loops: SketchCurve[][] = [];
|
|
116
|
+
for (const [start, boundary] of boundaries.entries()) {
|
|
117
|
+
if (visited.has(start)) continue;
|
|
118
|
+
if (!boundary.ends) {
|
|
119
|
+
visited.add(start);
|
|
120
|
+
loops.push([boundary.curve]);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const curves: SketchCurve[] = [];
|
|
124
|
+
let edge = start,
|
|
125
|
+
from = boundary.ends[0];
|
|
126
|
+
do {
|
|
127
|
+
const current = boundaries[edge];
|
|
128
|
+
const forward = current.ends![0] === from;
|
|
129
|
+
curves.push(forward ? current.curve : reverseCurve(current.curve));
|
|
130
|
+
visited.add(edge);
|
|
131
|
+
from = current.ends![forward ? 1 : 0];
|
|
132
|
+
edge = vertices[from].edges.find(e => e !== edge)!;
|
|
133
|
+
} while (edge !== start);
|
|
134
|
+
const area = contourArea(curves);
|
|
135
|
+
if (Math.abs(area) <= Math.max(...curves.map(sketchCurveTolerance)) ** 2)
|
|
136
|
+
throw new Error('Sketch contour has no finite area.');
|
|
137
|
+
loops.push(area < 0 ? curves.reverse().map(reverseCurve) : curves);
|
|
138
|
+
}
|
|
139
|
+
const areas = loops.map(loop => Math.abs(contourArea(loop)));
|
|
140
|
+
const parents = loops.map((loop, i) => {
|
|
141
|
+
const p = sketchCurvePosition(loop[0], 0);
|
|
142
|
+
const enclosing = loops.flatMap((other, j) =>
|
|
143
|
+
j !== i && areas[j] > areas[i] && contourContains(other, p) ? [j] : [],
|
|
144
|
+
);
|
|
145
|
+
return enclosing.sort((a, b) => areas[a] - areas[b])[0];
|
|
146
|
+
});
|
|
147
|
+
const depth = (i: number): number =>
|
|
148
|
+
parents[i] === undefined ? 0 : 1 + depth(parents[i]);
|
|
149
|
+
return loops.flatMap((outer, i) =>
|
|
150
|
+
depth(i) % 2
|
|
151
|
+
? []
|
|
152
|
+
: [
|
|
153
|
+
{
|
|
154
|
+
outer,
|
|
155
|
+
holes: loops
|
|
156
|
+
.filter((_, j) => parents[j] === i)
|
|
157
|
+
.map(loop => [...loop].reverse().map(reverseCurve)),
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function reverseCurve(curve: SketchCurve): SketchCurve {
|
|
164
|
+
if (curve.kind === 'line')
|
|
165
|
+
return {...curve, points: [curve.points[1], curve.points[0]]};
|
|
166
|
+
if (curve.kind === 'arc')
|
|
167
|
+
return {...curve, start: curve.start + curve.sweep, sweep: -curve.sweep};
|
|
168
|
+
// A full circular hole needs an explicit clockwise traversal too.
|
|
169
|
+
return {
|
|
170
|
+
kind: 'arc',
|
|
171
|
+
center: curve.center,
|
|
172
|
+
radius: curve.radius,
|
|
173
|
+
start: 0,
|
|
174
|
+
sweep: -tau,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function overlap(
|
|
179
|
+
a: SketchCurve,
|
|
180
|
+
b: SketchCurve,
|
|
181
|
+
intersections: readonly number[],
|
|
182
|
+
): boolean {
|
|
183
|
+
if (a.kind === 'line' || b.kind === 'line') {
|
|
184
|
+
if (a.kind !== 'line' || b.kind !== 'line' || intersections.length < 2)
|
|
185
|
+
return false;
|
|
186
|
+
} else if (
|
|
187
|
+
distance(a.center, b.center) >
|
|
188
|
+
Math.min(sketchCurveTolerance(a), sketchCurveTolerance(b)) ||
|
|
189
|
+
Math.abs(a.radius - b.radius) >
|
|
190
|
+
Math.min(sketchCurveTolerance(a), sketchCurveTolerance(b))
|
|
191
|
+
)
|
|
192
|
+
return false;
|
|
193
|
+
const cuts = [0, ...intersections, 1].sort((a, b) => a - b);
|
|
194
|
+
return cuts
|
|
195
|
+
.slice(1)
|
|
196
|
+
.some(
|
|
197
|
+
(end, i) =>
|
|
198
|
+
end > cuts[i] &&
|
|
199
|
+
isPointOnSketchCurve(sketchCurvePosition(a, (cuts[i] + end) / 2), b),
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Green's theorem, translated near the contour to avoid large-offset cancellation. */
|
|
204
|
+
export function contourArea(curves: readonly SketchCurve[]): number {
|
|
205
|
+
if (!curves.length) return 0;
|
|
206
|
+
const origin = sketchCurvePosition(curves[0], 0);
|
|
207
|
+
return curves.reduce((area, curve) => {
|
|
208
|
+
if (curve.kind === 'line') {
|
|
209
|
+
const [a, b] = curve.points.map(p => [
|
|
210
|
+
p[0] - origin[0],
|
|
211
|
+
p[1] - origin[1],
|
|
212
|
+
]);
|
|
213
|
+
return area + (a[0] * b[1] - b[0] * a[1]) / 2;
|
|
214
|
+
}
|
|
215
|
+
if (curve.kind === 'circle') return area + Math.PI * curve.radius ** 2;
|
|
216
|
+
const [x, y] = [curve.center[0] - origin[0], curve.center[1] - origin[1]];
|
|
217
|
+
const a = curve.start,
|
|
218
|
+
b = a + curve.sweep,
|
|
219
|
+
r = curve.radius;
|
|
220
|
+
return (
|
|
221
|
+
area +
|
|
222
|
+
(r * (x * (Math.sin(b) - Math.sin(a)) - y * (Math.cos(b) - Math.cos(a))) +
|
|
223
|
+
r * r * curve.sweep) /
|
|
224
|
+
2
|
|
225
|
+
);
|
|
226
|
+
}, 0);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Analytic horizontal ray crossings, with half-open, Y-monotone curve pieces. */
|
|
230
|
+
function contourContains(
|
|
231
|
+
curves: readonly SketchCurve[],
|
|
232
|
+
point: SketchPosition,
|
|
233
|
+
): boolean {
|
|
234
|
+
let crossings = 0;
|
|
235
|
+
for (const curve of curves) {
|
|
236
|
+
if (curve.kind === 'line') {
|
|
237
|
+
const [a, b] = curve.points;
|
|
238
|
+
if (
|
|
239
|
+
a[1] > point[1] !== b[1] > point[1] &&
|
|
240
|
+
a[0] + ((point[1] - a[1]) * (b[0] - a[0])) / (b[1] - a[1]) > point[0]
|
|
241
|
+
)
|
|
242
|
+
crossings++;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
const start = curve.kind === 'circle' ? 0 : curve.start;
|
|
246
|
+
const sweep = curve.kind === 'circle' ? tau : curve.sweep;
|
|
247
|
+
const cuts = [
|
|
248
|
+
0,
|
|
249
|
+
1,
|
|
250
|
+
...[Math.PI / 2, (3 * Math.PI) / 2]
|
|
251
|
+
.map(
|
|
252
|
+
a =>
|
|
253
|
+
sketchPositiveAngle(Math.sign(sweep) * (a - start)) /
|
|
254
|
+
Math.abs(sweep),
|
|
255
|
+
)
|
|
256
|
+
.filter(t => t > 0 && t < 1),
|
|
257
|
+
].sort((a, b) => a - b);
|
|
258
|
+
for (let i = 1; i < cuts.length; i++) {
|
|
259
|
+
const a = sketchCurvePosition(curve, cuts[i - 1]),
|
|
260
|
+
b = sketchCurvePosition(curve, cuts[i]);
|
|
261
|
+
if (a[1] > point[1] === b[1] > point[1]) continue;
|
|
262
|
+
const dy = point[1] - curve.center[1];
|
|
263
|
+
const x =
|
|
264
|
+
curve.center[0] +
|
|
265
|
+
Math.sign(Math.cos(start + (sweep * (cuts[i - 1] + cuts[i])) / 2)) *
|
|
266
|
+
Math.sqrt(Math.max(0, (curve.radius - dy) * (curve.radius + dy)));
|
|
267
|
+
if (x > point[0]) crossings++;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return crossings % 2 === 1;
|
|
271
|
+
}
|