1
import { Plane, PlaneName, Point, Vector } from "./geom";
2
import { makePlane } from "./geomHelpers";
3
import { localGC } from "./register";
4
import { DEG2RAD, RAD2DEG } from "./constants";
5
import { distance2d, polarAngle2d, polarToCartesian, Point2D } from "./lib2d";
13
} from "./shapeHelpers.js";
16
convertSvgEllipseParams,
20
} from "./sketcherlib.js";
21
import { CurveLike, Edge, Wire } from "./shapes.js";
22
import { Handle_Geom_BezierCurve } from "replicad-opencascadejs";
23
import Sketch from "./sketches/Sketch.js";
26
* The FaceSketcher allows you to sketch on a plane.
30
export default class Sketcher implements GenericSketcher<Sketch> {
31
protected plane: Plane;
32
protected pointer: Vector;
33
protected firstPoint: Vector;
34
protected pendingEdges: Edge[];
35
protected _mirrorWire: boolean;
38
* The sketcher can be defined by a plane, or a simple plane definition,
39
* with either a point of origin, or the position on the normal axis from
40
* the coordinates origin
42
constructor(plane: Plane);
43
constructor(plane?: PlaneName, origin?: Point | number);
44
constructor(plane?: Plane | PlaneName, origin?: Point) {
46
plane instanceof Plane ? makePlane(plane) : makePlane(plane, origin);
48
this.pointer = new Vector(this.plane.origin);
49
this.firstPoint = new Vector(this.plane.origin);
51
this.pendingEdges = [];
52
this._mirrorWire = false;
59
protected _updatePointer(newPointer: Vector): void {
60
this.pointer = newPointer;
63
movePointerTo([x, y]: Point2D): this {
64
if (this.pendingEdges.length)
66
"You can only move the pointer if there is no edge defined"
68
this._updatePointer(this.plane.toWorldCoords([x, y]));
69
this.firstPoint = new Vector(this.pointer);
73
lineTo([x, y]: Point2D): this {
74
const endPoint = this.plane.toWorldCoords([x, y]);
75
this.pendingEdges.push(makeLine(this.pointer, endPoint));
76
this._updatePointer(endPoint);
80
line(xDist: number, yDist: number): this {
81
const pointer = this.plane.toLocalCoords(this.pointer);
82
return this.lineTo([xDist + pointer.x, yDist + pointer.y]);
85
vLine(distance: number): this {
86
return this.line(0, distance);
89
hLine(distance: number): this {
90
return this.line(distance, 0);
93
vLineTo(yPos: number): this {
94
const pointer = this.plane.toLocalCoords(this.pointer);
95
return this.lineTo([pointer.x, yPos]);
98
hLineTo(xPos: number): this {
99
const pointer = this.plane.toLocalCoords(this.pointer);
100
return this.lineTo([xPos, pointer.y]);
103
polarLine(distance: number, angle: number): this {
104
const angleInRads = angle * DEG2RAD;
105
const [x, y] = polarToCartesian(distance, angleInRads);
106
return this.line(x, y);
109
polarLineTo([r, theta]: [number, number]): this {
110
const angleInRads = theta * DEG2RAD;
111
const point = polarToCartesian(r, angleInRads);
112
return this.lineTo(point);
115
tangentLine(distance: number): this {
116
const [r, gc] = localGC();
117
const previousEdge = this.pendingEdges.length
118
? this.pendingEdges[this.pendingEdges.length - 1]
122
throw new Error("you need a previous edge to create a tangent line");
124
const tangent = r(previousEdge.tangentAt(1));
125
const endPoint = r(tangent.normalized().multiply(distance)).add(
129
this.pendingEdges.push(makeLine(this.pointer, endPoint));
130
this._updatePointer(endPoint);
135
threePointsArcTo(end: Point2D, innerPoint: Point2D): this {
136
const gpoint1 = this.plane.toWorldCoords(innerPoint);
137
const gpoint2 = this.plane.toWorldCoords(end);
139
this.pendingEdges.push(makeThreePointArc(this.pointer, gpoint1, gpoint2));
141
this._updatePointer(gpoint2);
151
const pointer = this.plane.toLocalCoords(this.pointer);
152
return this.threePointsArcTo(
153
[pointer.x + xDist, pointer.y + yDist],
154
[pointer.x + viaXDist, pointer.y + viaYDist]
158
tangentArcTo(end: Point2D): this {
159
const endPoint = this.plane.toWorldCoords(end);
160
const previousEdge = this.pendingEdges[this.pendingEdges.length - 1];
162
this.pendingEdges.push(
163
makeTangentArc(previousEdge.endPoint, previousEdge.tangentAt(1), endPoint)
166
this._updatePointer(endPoint);
170
tangentArc(xDist: number, yDist: number): this {
171
const pointer = this.plane.toLocalCoords(this.pointer);
172
return this.tangentArcTo([xDist + pointer.x, yDist + pointer.y]);
175
sagittaArcTo(end: Point2D, sagitta: number): this {
176
const startPoint = this.pointer;
177
const endPoint = this.plane.toWorldCoords(end);
179
let p = endPoint.add(startPoint);
180
const midPoint = p.multiply(0.5);
182
p = endPoint.sub(startPoint);
183
const sagDirection = p.cross(this.plane.zDir).normalized();
185
const sagVector = sagDirection.multiply(sagitta);
187
const sagPoint = midPoint.add(sagVector);
189
this.pendingEdges.push(makeThreePointArc(this.pointer, sagPoint, endPoint));
190
this._updatePointer(endPoint);
195
sagittaArc(xDist: number, yDist: number, sagitta: number): this {
196
const pointer = this.plane.toLocalCoords(this.pointer);
197
return this.sagittaArcTo([xDist + pointer.x, yDist + pointer.y], sagitta);
200
vSagittaArc(distance: number, sagitta: number): this {
201
return this.sagittaArc(0, distance, sagitta);
204
hSagittaArc(distance: number, sagitta: number): this {
205
return this.sagittaArc(distance, 0, sagitta);
208
bulgeArcTo(end: Point2D, bulge: number): this {
209
if (!bulge) return this.lineTo(end);
210
const pointer = this.plane.toLocalCoords(this.pointer);
211
const halfChord = distance2d([pointer.x, pointer.y], end) / 2;
212
const bulgeAsSagitta = -bulge * halfChord;
214
return this.sagittaArcTo(end, bulgeAsSagitta);
217
bulgeArc(xDist: number, yDist: number, bulge: number): this {
218
const pointer = this.plane.toLocalCoords(this.pointer);
219
return this.bulgeArcTo([xDist + pointer.x, yDist + this.pointer.y], bulge);
222
vBulgeArc(distance: number, bulge: number): this {
223
return this.bulgeArc(0, distance, bulge);
226
hBulgeArc(distance: number, bulge: number): this {
227
return this.bulgeArc(distance, 0, bulge);
232
horizontalRadius: number,
233
verticalRadius: number,
238
const [r, gc] = localGC();
239
const start = this.plane.toLocalCoords(this.pointer);
241
let rotationAngle = rotation;
242
let majorRadius = horizontalRadius;
243
let minorRadius = verticalRadius;
245
if (horizontalRadius < verticalRadius) {
246
rotationAngle = rotation + 90;
247
majorRadius = verticalRadius;
248
minorRadius = horizontalRadius;
251
const { cx, cy, rx, ry, startAngle, endAngle, clockwise } =
252
convertSvgEllipseParams(
257
rotationAngle * DEG2RAD,
263
new Vector(this.plane.xDir).rotate(
270
const arc = makeEllipseArc(
273
clockwise ? startAngle : endAngle,
274
clockwise ? endAngle : startAngle,
275
r(this.plane.toWorldCoords([cx, cy])),
281
// This does not work, we may need to hack a bit more within
283
arc.wrapped.Reverse();
286
this.pendingEdges.push(arc);
287
this._updatePointer(this.plane.toWorldCoords(end));
295
horizontalRadius: number,
296
verticalRadius: number,
301
const pointer = this.plane.toLocalCoords(this.pointer);
302
return this.ellipseTo(
303
[xDist + pointer.x, yDist + pointer.y],
312
halfEllipseTo(end: Point2D, verticalRadius: number, sweep = false): this {
313
const pointer = this.plane.toLocalCoords(this.pointer);
314
const start: Point2D = [pointer.x, pointer.y];
316
const angle = polarAngle2d(end, start);
317
const distance = distance2d(end, start);
319
return this.ellipseTo(
332
verticalRadius: number,
335
const pointer = this.plane.toLocalCoords(this.pointer);
336
return this.halfEllipseTo(
337
[xDist + pointer.x, yDist + pointer.y],
343
bezierCurveTo(end: Point2D, controlPoints: Point2D | Point2D[]): this {
345
if (controlPoints.length === 2 && !Array.isArray(controlPoints[0])) {
346
cp = [controlPoints as Point2D];
348
cp = controlPoints as Point2D[];
351
const inWorldPoints = cp.map((p) => this.plane.toWorldCoords(p));
352
const endPoint = this.plane.toWorldCoords(end);
354
this.pendingEdges.push(
355
makeBezierCurve([this.pointer, ...inWorldPoints, endPoint])
358
this._updatePointer(endPoint);
362
quadraticBezierCurveTo(end: Point2D, controlPoint: Point2D): this {
363
return this.bezierCurveTo(end, [controlPoint]);
368
startControlPoint: Point2D,
369
endControlPoint: Point2D
371
return this.bezierCurveTo(end, [startControlPoint, endControlPoint]);
374
smoothSplineTo(end: Point2D, config?: SplineConfig): this {
375
const [r, gc] = localGC();
376
const { endTangent, startTangent, startFactor, endFactor } =
377
defaultsSplineConfig(config);
379
const endPoint = this.plane.toWorldCoords(end);
380
const previousEdge = this.pendingEdges.length
381
? this.pendingEdges[this.pendingEdges.length - 1]
384
const defaultDistance = r(endPoint.sub(this.pointer)).Length * 0.25;
386
let startPoleDirection: Point;
388
startPoleDirection = this.plane.toWorldCoords(startTangent);
389
} else if (!previousEdge) {
390
startPoleDirection = this.plane.toWorldCoords([1, 0]);
391
} else if (previousEdge.geomType === "BEZIER_CURVE") {
393
r(previousEdge.curve).wrapped as CurveLike & {
394
Bezier: () => Handle_Geom_BezierCurve;
399
const previousPole = r(new Vector(rawCurve.Pole(rawCurve.NbPoles() - 1)));
401
startPoleDirection = r(this.pointer.sub(previousPole));
403
startPoleDirection = r(previousEdge.tangentAt(1));
406
const poleDistance = r(
407
startPoleDirection.normalized().multiply(startFactor * defaultDistance)
409
const startControl = r(this.pointer.add(poleDistance));
411
let endPoleDirection: Point;
412
if (endTangent === "symmetric") {
413
endPoleDirection = r(startPoleDirection.multiply(-1));
415
endPoleDirection = r(this.plane.toWorldCoords(endTangent));
418
const endPoleDistance = r(
419
endPoleDirection.normalized().multiply(endFactor * defaultDistance)
421
const endControl = r(endPoint.sub(endPoleDistance));
423
this.pendingEdges.push(
424
makeBezierCurve([this.pointer, startControl, endControl, endPoint])
427
this._updatePointer(endPoint);
435
splineConfig: SplineConfig = {}
437
const pointer = this.plane.toLocalCoords(this.pointer);
438
return this.smoothSplineTo(
439
[xDist + pointer.x, yDist + pointer.y],
444
protected _mirrorWireOnStartEnd(wire: Wire): Wire {
445
const startToEndVector = this.pointer.sub(this.firstPoint).normalize();
446
const normal = startToEndVector.cross(this.plane.zDir);
448
const mirroredWire = wire.clone().mirror(normal, this.pointer);
450
const combinedWire = assembleWire([wire, mirroredWire]);
455
protected buildWire(): Wire {
456
if (!this.pendingEdges.length)
457
throw new Error("No lines to convert into a wire");
459
let wire = assembleWire(this.pendingEdges);
461
if (this._mirrorWire) {
462
wire = this._mirrorWireOnStartEnd(wire);
468
protected _closeSketch(): void {
469
if (!this.pointer.equals(this.firstPoint) && !this._mirrorWire) {
470
const endpoint = this.plane.toLocalCoords(this.firstPoint);
471
this.lineTo([endpoint.x, endpoint.y]);
476
const sketch = new Sketch(this.buildWire(), {
477
defaultOrigin: this.plane.origin,
478
defaultDirection: this.plane.zDir,
488
closeWithMirror(): Sketch {
489
this._mirrorWire = true;