1
import Sketch from "./sketches/Sketch";
2
import { DEG2RAD, RAD2DEG } from "./constants.js";
3
import { localGC } from "./register.js";
4
import { getOC } from "./oclib.js";
5
import { assembleWire } from "./shapeHelpers";
6
import { Edge, Face, Wire } from "./shapes";
8
convertSvgEllipseParams,
12
} from "./sketcherlib";
16
} from "replicad-opencascadejs";
17
import { chamferCurves, Curve2D, filletCurves } from "./lib2d";
35
import Blueprint from "./blueprints/Blueprint";
44
export class BaseSketcher2d {
45
protected pointer: Point2D;
46
protected firstPoint: Point2D;
47
protected pendingCurves: Curve2D[];
48
protected _nextCorner: { radius: number; mode: "fillet" | "chamfer" } | null;
50
constructor(origin: Point2D = [0, 0]) {
51
this.pointer = origin;
52
this.firstPoint = origin;
53
this._nextCorner = null;
55
this.pendingCurves = [];
58
protected _convertToUV([x, y]: Point2D): Point2D {
62
protected _convertFromUV([u, v]: Point2D): Point2D {
66
movePointerTo(point: Point2D): this {
67
if (this.pendingCurves.length)
69
"You can only move the pointer if there is no curve defined"
73
this.firstPoint = point;
77
protected saveCurve(curve: Curve2D) {
78
if (!this._nextCorner) {
79
this.pendingCurves.push(curve);
83
const previousCurve = this.pendingCurves.pop();
84
if (!previousCurve) throw new Error("bug in the custom corner algorithm");
87
this._nextCorner.mode === "chamfer" ? chamferCurves : filletCurves;
89
this.pendingCurves.push(
90
...makeCorner(previousCurve, curve, this._nextCorner.radius)
92
this._nextCorner = null;
95
lineTo(point: Point2D): this {
96
const curve = make2dSegmentCurve(
97
this._convertToUV(this.pointer),
98
this._convertToUV(point)
100
this.pointer = point;
101
this.saveCurve(curve);
105
line(xDist: number, yDist: number): this {
106
return this.lineTo([this.pointer[0] + xDist, this.pointer[1] + yDist]);
109
vLine(distance: number): this {
110
return this.line(0, distance);
113
hLine(distance: number): this {
114
return this.line(distance, 0);
117
vLineTo(yPos: number): this {
118
return this.lineTo([this.pointer[0], yPos]);
121
hLineTo(xPos: number): this {
122
return this.lineTo([xPos, this.pointer[1]]);
125
polarLineTo([r, theta]: Point2D): this {
126
const angleInRads = theta * DEG2RAD;
127
const point = polarToCartesian(r, angleInRads);
128
return this.lineTo(point);
131
polarLine(distance: number, angle: number): this {
132
const angleInRads = angle * DEG2RAD;
133
const [x, y] = polarToCartesian(distance, angleInRads);
134
return this.line(x, y);
137
tangentLine(distance: number): this {
138
const previousCurve = this.pendingCurves.length
139
? this.pendingCurves[this.pendingCurves.length - 1]
143
throw new Error("You need a previous curve to sketch a tangent line");
145
const direction = normalize2d(
146
this._convertFromUV(previousCurve.tangentAt(1))
148
return this.line(direction[0] * distance, direction[1] * distance);
151
threePointsArcTo(end: Point2D, midPoint: Point2D): this {
154
this._convertToUV(this.pointer),
155
this._convertToUV(midPoint),
156
this._convertToUV(end)
169
const [x0, y0] = this.pointer;
170
return this.threePointsArcTo(
171
[x0 + xDist, y0 + yDist],
172
[x0 + viaXDist, y0 + viaYDist]
176
sagittaArcTo(end: Point2D, sagitta: number): this {
177
const [x0, y0] = this.pointer;
178
const [x1, y1] = end;
180
const midPoint = [(x0 + x1) / 2, (y0 + y1) / 2];
182
// perpendicular vector of B - A
183
const sagDir = [-(y1 - y0), x1 - x0];
184
const sagDirLen = Math.sqrt(sagDir[0] ** 2 + sagDir[1] ** 2);
186
const sagPoint: Point2D = [
187
midPoint[0] + (sagDir[0] / sagDirLen) * sagitta,
188
midPoint[1] + (sagDir[1] / sagDirLen) * sagitta,
193
this._convertToUV(this.pointer),
194
this._convertToUV(sagPoint),
195
this._convertToUV(end)
203
sagittaArc(xDist: number, yDist: number, sagitta: number): this {
204
return this.sagittaArcTo(
205
[xDist + this.pointer[0], yDist + this.pointer[1]],
210
vSagittaArc(distance: number, sagitta: number): this {
211
return this.sagittaArc(0, distance, sagitta);
214
hSagittaArc(distance: number, sagitta: number): this {
215
return this.sagittaArc(distance, 0, sagitta);
218
bulgeArcTo(end: Point2D, bulge: number): this {
219
if (!bulge) return this.lineTo(end);
220
const halfChord = distance2d(this.pointer, end) / 2;
221
const bulgeAsSagitta = -bulge * halfChord;
223
return this.sagittaArcTo(end, bulgeAsSagitta);
226
bulgeArc(xDist: number, yDist: number, bulge: number): this {
227
return this.bulgeArcTo(
228
[xDist + this.pointer[0], yDist + this.pointer[1]],
233
vBulgeArc(distance: number, bulge: number): this {
234
return this.bulgeArc(0, distance, bulge);
237
hBulgeArc(distance: number, bulge: number): this {
238
return this.bulgeArc(distance, 0, bulge);
241
tangentArcTo(end: Point2D): this {
242
const previousCurve = this.pendingCurves.length
243
? this.pendingCurves[this.pendingCurves.length - 1]
247
throw new Error("You need a previous curve to sketch a tangent arc");
251
this._convertToUV(this.pointer),
252
previousCurve.tangentAt(1),
253
this._convertToUV(end)
261
tangentArc(xDist: number, yDist: number): this {
262
const [x0, y0] = this.pointer;
263
return this.tangentArcTo([xDist + x0, yDist + y0]);
268
horizontalRadius: number,
269
verticalRadius: number,
274
let rotationAngle = rotation;
275
let majorRadius = horizontalRadius;
276
let minorRadius = verticalRadius;
278
if (horizontalRadius < verticalRadius) {
279
rotationAngle = rotation + 90;
280
majorRadius = verticalRadius;
281
minorRadius = horizontalRadius;
283
const radRotationAngle = rotationAngle * DEG2RAD;
286
* The complicated part in this function comes from the scaling that we do
287
* between standardised units and UV. We need to:
288
* - stretch the length of the radiuses and take into account the angle they
289
* make with the X direction
290
* - modify the angle (as the scaling is not homogenous in the two directions
291
* the angle can change.
294
const convertAxis = (ax: Point2D) => distance2d(this._convertToUV(ax));
295
const r1 = convertAxis(polarToCartesian(majorRadius, radRotationAngle));
296
const r2 = convertAxis(
297
polarToCartesian(minorRadius, radRotationAngle + Math.PI / 2)
300
const xDir = normalize2d(
301
this._convertToUV(rotate2d([1, 0], radRotationAngle))
303
const [, newRotationAngle] = cartesianToPolar(xDir);
305
const { cx, cy, startAngle, endAngle, clockwise, rx, ry } =
306
convertSvgEllipseParams(
307
this._convertToUV(this.pointer),
308
this._convertToUV(end),
316
const arc = make2dEllipseArc(
319
clockwise ? startAngle : endAngle,
320
clockwise ? endAngle : startAngle,
337
horizontalRadius: number,
338
verticalRadius: number,
343
const [x0, y0] = this.pointer;
344
return this.ellipseTo(
345
[xDist + x0, yDist + y0],
354
halfEllipseTo(end: Point2D, minorRadius: number, sweep = false): this {
355
const angle = polarAngle2d(end, this.pointer);
356
const distance = distance2d(end, this.pointer);
358
return this.ellipseTo(
374
const [x0, y0] = this.pointer;
375
return this.halfEllipseTo([x0 + xDist, y0 + yDist], minorRadius, sweep);
378
bezierCurveTo(end: Point2D, controlPoints: Point2D | Point2D[]): this {
380
if (controlPoints.length === 2 && !Array.isArray(controlPoints[0])) {
381
cp = [controlPoints as Point2D];
383
cp = controlPoints as Point2D[];
388
this._convertToUV(this.pointer),
389
cp.map((point) => this._convertToUV(point)),
390
this._convertToUV(end)
398
quadraticBezierCurveTo(end: Point2D, controlPoint: Point2D): this {
399
return this.bezierCurveTo(end, [controlPoint]);
404
startControlPoint: Point2D,
405
endControlPoint: Point2D
407
return this.bezierCurveTo(end, [startControlPoint, endControlPoint]);
410
smoothSplineTo(end: Point2D, config?: SplineConfig): this {
411
const { endTangent, startTangent, startFactor, endFactor } =
412
defaultsSplineConfig(config);
414
const previousCurve = this.pendingCurves.length
415
? this.pendingCurves[this.pendingCurves.length - 1]
418
const defaultDistance = distance2d(this.pointer, end) * 0.25;
420
let startPoleDirection: Point2D;
422
startPoleDirection = startTangent;
423
} else if (!previousCurve) {
424
startPoleDirection = [1, 0];
426
startPoleDirection = this._convertFromUV(previousCurve.tangentAt(1));
429
startPoleDirection = normalize2d(startPoleDirection);
430
const startControl: Point2D = [
431
this.pointer[0] + startPoleDirection[0] * startFactor * defaultDistance,
432
this.pointer[1] + startPoleDirection[1] * startFactor * defaultDistance,
435
let endPoleDirection: Point2D;
436
if (endTangent === "symmetric") {
437
endPoleDirection = [-startPoleDirection[0], -startPoleDirection[1]];
439
endPoleDirection = endTangent;
442
endPoleDirection = normalize2d(endPoleDirection);
443
const endControl: Point2D = [
444
end[0] - endPoleDirection[0] * endFactor * defaultDistance,
445
end[1] - endPoleDirection[1] * endFactor * defaultDistance,
448
return this.cubicBezierCurveTo(end, startControl, endControl);
454
splineConfig?: SplineConfig
456
return this.smoothSplineTo(
457
[xDist + this.pointer[0], yDist + this.pointer[1]],
463
* Changes the corner between the previous and next segments.
465
customCorner(radius: number, mode: "fillet" | "chamfer" = "fillet") {
466
if (!this.pendingCurves.length)
467
throw new Error("You need a curve defined to fillet the angle");
469
this._nextCorner = { mode, radius };
473
protected _customCornerLastWithFirst(
475
mode: "fillet" | "chamfer" = "fillet"
479
const previousCurve = this.pendingCurves.pop();
480
const curve = this.pendingCurves.shift();
482
if (!previousCurve || !curve)
483
throw new Error("Not enough curves to close and fillet");
485
const makeCorner = mode === "chamfer" ? chamferCurves : filletCurves;
487
this.pendingCurves.push(...makeCorner(previousCurve, curve, radius));
490
protected _closeSketch(): void {
491
if (!samePoint(this.pointer, this.firstPoint)) {
492
this.lineTo(this.firstPoint);
496
protected _closeWithMirror() {
497
if (samePoint(this.pointer, this.firstPoint))
499
"Cannot close with a mirror when the sketch is already closed"
501
const startToEndVector: Point2D = [
502
this.pointer[0] - this.firstPoint[0],
503
this.pointer[1] - this.firstPoint[1],
506
const mirrorAxis = axis2d(
507
this._convertToUV(this.pointer),
508
this._convertToUV(startToEndVector)
511
const mirroredCurves = this.pendingCurves.map(
513
new Curve2D(c.innerCurve.Mirrored_2(mirrorAxis) as Handle_Geom2d_Curve)
515
mirroredCurves.reverse();
516
mirroredCurves.map((c) => c.reverse());
517
this.pendingCurves.push(...mirroredCurves);
518
this.pointer = this.firstPoint;
523
* The FaceSketcher allows you to sketch on a face that is not planar, for
524
* instance the sides of a cylinder.
526
* The coordinates passed to the methods corresponds to normalised distances on
527
* this surface, between 0 and 1 in both direction.
529
* Note that if you are drawing on a closed surface (typically a revolution
530
* surface or a cylinder), the first parameters represents the angle and can be
531
* smaller than 0 or bigger than 1.
533
* @category Sketching
535
export default class FaceSketcher
536
extends BaseSketcher2d
537
implements GenericSketcher<Sketch>
539
protected face: Face;
540
protected _bounds: UVBounds;
542
constructor(face: Face, origin: Point2D = [0, 0]) {
544
this.face = face.clone();
545
this._bounds = face.UVBounds;
548
protected _convertToUV([x, y]: Point2D): Point2D {
549
const { uMin, uMax, vMin, vMax } = this._bounds;
550
return [uMin + x * (uMax - uMin), vMin + y * (vMax - vMin)];
553
protected _convertFromUV([u, v]: Point2D): Point2D {
554
const { uMin, uMax, vMin, vMax } = this._bounds;
555
return [(u - uMin) / (uMax - uMin), (v - vMin) / (vMax - vMin)];
558
_adaptSurface(): Handle_Geom_Surface {
560
// CHECK THIS: return new oc.BRep_Tool.Surface_2(this.face.wrapped)
561
return oc.BRep_Tool.Surface_2(this.face.wrapped);
567
protected buildWire(): Wire {
568
const [r, gc] = localGC();
571
const geomSurf = r(this._adaptSurface());
573
const edges = this.pendingCurves.map((curve) => {
576
r(new oc.BRepBuilderAPI_MakeEdge_30(curve.wrapped, geomSurf)).Edge()
580
const wire = assembleWire(edges);
581
oc.BRepLib.BuildCurves3d_2(wire.wrapped);
588
const [r, gc] = localGC();
590
const wire = this.buildWire();
591
const sketch = new Sketch(wire);
593
const face = r(sketch.clone().face());
594
sketch.defaultOrigin = r(face.pointOnSurface(0.5, 0.5));
595
sketch.defaultDirection = r(r(face.normalAt()).multiply(-1));
597
const startPoint = r(wire.startPoint);
598
sketch.defaultOrigin = startPoint;
599
sketch.defaultDirection = r(this.face.normalAt(startPoint));
601
sketch.baseFace = this.face;
611
closeWithMirror(): Sketch {
612
this._closeWithMirror();
617
* Stop drawing, make sure the sketch is closed (by adding a straight line to
618
* from the last point to the first), add a fillet between the last and the
619
* first segments and returns the sketch.
621
closeWithCustomCorner(
623
mode: "fillet" | "chamfer" = "fillet"
626
this._customCornerLastWithFirst(radius, mode);
632
export class BlueprintSketcher
633
extends BaseSketcher2d
634
implements GenericSketcher<Blueprint>
636
constructor(origin: Point2D = [0, 0]) {
638
this.pointer = origin;
639
this.firstPoint = origin;
641
this.pendingCurves = [];
645
return new Blueprint(this.pendingCurves);
653
closeWithMirror(): Blueprint {
654
this._closeWithMirror();
659
* Stop drawing, make sure the sketch is closed (by adding a straight line to
660
* from the last point to the first), add a fillet between the last and the
661
* first segments and returns the sketch.
664
closeWithCustomCorner(
666
mode: "fillet" | "chamfer" = "fillet"
669
this._customCornerLastWithFirst(radius, mode);