RepliCAD

Форк
0
/
Sketcher2d.ts 
673 строки · 16.9 Кб
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";
7
import {
8
  convertSvgEllipseParams,
9
  defaultsSplineConfig,
10
  SplineConfig,
11
  GenericSketcher,
12
} from "./sketcherlib";
13
import {
14
  Handle_Geom2d_Curve,
15
  Handle_Geom_Surface,
16
} from "replicad-opencascadejs";
17
import { chamferCurves, Curve2D, filletCurves } from "./lib2d";
18

19
import {
20
  normalize2d,
21
  polarAngle2d,
22
  samePoint,
23
  distance2d,
24
  axis2d,
25
  rotate2d,
26
  polarToCartesian,
27
  cartesianToPolar,
28
  make2dSegmentCurve,
29
  make2dTangentArc,
30
  make2dThreePointArc,
31
  make2dBezierCurve,
32
  make2dEllipseArc,
33
  Point2D,
34
} from "./lib2d";
35
import Blueprint from "./blueprints/Blueprint";
36

37
type UVBounds = {
38
  uMin: number;
39
  uMax: number;
40
  vMin: number;
41
  vMax: number;
42
};
43

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;
49

50
  constructor(origin: Point2D = [0, 0]) {
51
    this.pointer = origin;
52
    this.firstPoint = origin;
53
    this._nextCorner = null;
54

55
    this.pendingCurves = [];
56
  }
57

58
  protected _convertToUV([x, y]: Point2D): Point2D {
59
    return [x, y];
60
  }
61

62
  protected _convertFromUV([u, v]: Point2D): Point2D {
63
    return [u, v];
64
  }
65

66
  movePointerTo(point: Point2D): this {
67
    if (this.pendingCurves.length)
68
      throw new Error(
69
        "You can only move the pointer if there is no curve defined"
70
      );
71

72
    this.pointer = point;
73
    this.firstPoint = point;
74
    return this;
75
  }
76

77
  protected saveCurve(curve: Curve2D) {
78
    if (!this._nextCorner) {
79
      this.pendingCurves.push(curve);
80
      return;
81
    }
82

83
    const previousCurve = this.pendingCurves.pop();
84
    if (!previousCurve) throw new Error("bug in the custom corner algorithm");
85

86
    const makeCorner =
87
      this._nextCorner.mode === "chamfer" ? chamferCurves : filletCurves;
88

89
    this.pendingCurves.push(
90
      ...makeCorner(previousCurve, curve, this._nextCorner.radius)
91
    );
92
    this._nextCorner = null;
93
  }
94

95
  lineTo(point: Point2D): this {
96
    const curve = make2dSegmentCurve(
97
      this._convertToUV(this.pointer),
98
      this._convertToUV(point)
99
    );
100
    this.pointer = point;
101
    this.saveCurve(curve);
102
    return this;
103
  }
104

105
  line(xDist: number, yDist: number): this {
106
    return this.lineTo([this.pointer[0] + xDist, this.pointer[1] + yDist]);
107
  }
108

109
  vLine(distance: number): this {
110
    return this.line(0, distance);
111
  }
112

113
  hLine(distance: number): this {
114
    return this.line(distance, 0);
115
  }
116

117
  vLineTo(yPos: number): this {
118
    return this.lineTo([this.pointer[0], yPos]);
119
  }
120

121
  hLineTo(xPos: number): this {
122
    return this.lineTo([xPos, this.pointer[1]]);
123
  }
124

125
  polarLineTo([r, theta]: Point2D): this {
126
    const angleInRads = theta * DEG2RAD;
127
    const point = polarToCartesian(r, angleInRads);
128
    return this.lineTo(point);
129
  }
130

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);
135
  }
136

137
  tangentLine(distance: number): this {
138
    const previousCurve = this.pendingCurves.length
139
      ? this.pendingCurves[this.pendingCurves.length - 1]
140
      : null;
141

142
    if (!previousCurve)
143
      throw new Error("You need a previous curve to sketch a tangent line");
144

145
    const direction = normalize2d(
146
      this._convertFromUV(previousCurve.tangentAt(1))
147
    );
148
    return this.line(direction[0] * distance, direction[1] * distance);
149
  }
150

151
  threePointsArcTo(end: Point2D, midPoint: Point2D): this {
152
    this.saveCurve(
153
      make2dThreePointArc(
154
        this._convertToUV(this.pointer),
155
        this._convertToUV(midPoint),
156
        this._convertToUV(end)
157
      )
158
    );
159
    this.pointer = end;
160
    return this;
161
  }
162

163
  threePointsArc(
164
    xDist: number,
165
    yDist: number,
166
    viaXDist: number,
167
    viaYDist: number
168
  ): this {
169
    const [x0, y0] = this.pointer;
170
    return this.threePointsArcTo(
171
      [x0 + xDist, y0 + yDist],
172
      [x0 + viaXDist, y0 + viaYDist]
173
    );
174
  }
175

176
  sagittaArcTo(end: Point2D, sagitta: number): this {
177
    const [x0, y0] = this.pointer;
178
    const [x1, y1] = end;
179

180
    const midPoint = [(x0 + x1) / 2, (y0 + y1) / 2];
181

182
    // perpendicular vector of B - A
183
    const sagDir = [-(y1 - y0), x1 - x0];
184
    const sagDirLen = Math.sqrt(sagDir[0] ** 2 + sagDir[1] ** 2);
185

186
    const sagPoint: Point2D = [
187
      midPoint[0] + (sagDir[0] / sagDirLen) * sagitta,
188
      midPoint[1] + (sagDir[1] / sagDirLen) * sagitta,
189
    ];
190

191
    this.saveCurve(
192
      make2dThreePointArc(
193
        this._convertToUV(this.pointer),
194
        this._convertToUV(sagPoint),
195
        this._convertToUV(end)
196
      )
197
    );
198
    this.pointer = end;
199

200
    return this;
201
  }
202

203
  sagittaArc(xDist: number, yDist: number, sagitta: number): this {
204
    return this.sagittaArcTo(
205
      [xDist + this.pointer[0], yDist + this.pointer[1]],
206
      sagitta
207
    );
208
  }
209

210
  vSagittaArc(distance: number, sagitta: number): this {
211
    return this.sagittaArc(0, distance, sagitta);
212
  }
213

214
  hSagittaArc(distance: number, sagitta: number): this {
215
    return this.sagittaArc(distance, 0, sagitta);
216
  }
217

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;
222

223
    return this.sagittaArcTo(end, bulgeAsSagitta);
224
  }
225

226
  bulgeArc(xDist: number, yDist: number, bulge: number): this {
227
    return this.bulgeArcTo(
228
      [xDist + this.pointer[0], yDist + this.pointer[1]],
229
      bulge
230
    );
231
  }
232

233
  vBulgeArc(distance: number, bulge: number): this {
234
    return this.bulgeArc(0, distance, bulge);
235
  }
236

237
  hBulgeArc(distance: number, bulge: number): this {
238
    return this.bulgeArc(distance, 0, bulge);
239
  }
240

241
  tangentArcTo(end: Point2D): this {
242
    const previousCurve = this.pendingCurves.length
243
      ? this.pendingCurves[this.pendingCurves.length - 1]
244
      : null;
245

246
    if (!previousCurve)
247
      throw new Error("You need a previous curve to sketch a tangent arc");
248

249
    this.saveCurve(
250
      make2dTangentArc(
251
        this._convertToUV(this.pointer),
252
        previousCurve.tangentAt(1),
253
        this._convertToUV(end)
254
      )
255
    );
256

257
    this.pointer = end;
258
    return this;
259
  }
260

261
  tangentArc(xDist: number, yDist: number): this {
262
    const [x0, y0] = this.pointer;
263
    return this.tangentArcTo([xDist + x0, yDist + y0]);
264
  }
265

266
  ellipseTo(
267
    end: Point2D,
268
    horizontalRadius: number,
269
    verticalRadius: number,
270
    rotation = 0,
271
    longAxis = false,
272
    sweep = false
273
  ): this {
274
    let rotationAngle = rotation;
275
    let majorRadius = horizontalRadius;
276
    let minorRadius = verticalRadius;
277

278
    if (horizontalRadius < verticalRadius) {
279
      rotationAngle = rotation + 90;
280
      majorRadius = verticalRadius;
281
      minorRadius = horizontalRadius;
282
    }
283
    const radRotationAngle = rotationAngle * DEG2RAD;
284

285
    /*
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.
292
     */
293

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)
298
    );
299

300
    const xDir = normalize2d(
301
      this._convertToUV(rotate2d([1, 0], radRotationAngle))
302
    );
303
    const [, newRotationAngle] = cartesianToPolar(xDir);
304

305
    const { cx, cy, startAngle, endAngle, clockwise, rx, ry } =
306
      convertSvgEllipseParams(
307
        this._convertToUV(this.pointer),
308
        this._convertToUV(end),
309
        r1,
310
        r2,
311
        newRotationAngle,
312
        longAxis,
313
        sweep
314
      );
315

316
    const arc = make2dEllipseArc(
317
      rx,
318
      ry,
319
      clockwise ? startAngle : endAngle,
320
      clockwise ? endAngle : startAngle,
321
      [cx, cy],
322
      xDir
323
    );
324

325
    if (!clockwise) {
326
      arc.reverse();
327
    }
328

329
    this.saveCurve(arc);
330
    this.pointer = end;
331
    return this;
332
  }
333

334
  ellipse(
335
    xDist: number,
336
    yDist: number,
337
    horizontalRadius: number,
338
    verticalRadius: number,
339
    rotation = 0,
340
    longAxis = false,
341
    sweep = false
342
  ): this {
343
    const [x0, y0] = this.pointer;
344
    return this.ellipseTo(
345
      [xDist + x0, yDist + y0],
346
      horizontalRadius,
347
      verticalRadius,
348
      rotation,
349
      longAxis,
350
      sweep
351
    );
352
  }
353

354
  halfEllipseTo(end: Point2D, minorRadius: number, sweep = false): this {
355
    const angle = polarAngle2d(end, this.pointer);
356
    const distance = distance2d(end, this.pointer);
357

358
    return this.ellipseTo(
359
      end,
360
      distance / 2,
361
      minorRadius,
362
      angle * RAD2DEG,
363
      true,
364
      sweep
365
    );
366
  }
367

368
  halfEllipse(
369
    xDist: number,
370
    yDist: number,
371
    minorRadius: number,
372
    sweep = false
373
  ): this {
374
    const [x0, y0] = this.pointer;
375
    return this.halfEllipseTo([x0 + xDist, y0 + yDist], minorRadius, sweep);
376
  }
377

378
  bezierCurveTo(end: Point2D, controlPoints: Point2D | Point2D[]): this {
379
    let cp: Point2D[];
380
    if (controlPoints.length === 2 && !Array.isArray(controlPoints[0])) {
381
      cp = [controlPoints as Point2D];
382
    } else {
383
      cp = controlPoints as Point2D[];
384
    }
385

386
    this.saveCurve(
387
      make2dBezierCurve(
388
        this._convertToUV(this.pointer),
389
        cp.map((point) => this._convertToUV(point)),
390
        this._convertToUV(end)
391
      )
392
    );
393

394
    this.pointer = end;
395
    return this;
396
  }
397

398
  quadraticBezierCurveTo(end: Point2D, controlPoint: Point2D): this {
399
    return this.bezierCurveTo(end, [controlPoint]);
400
  }
401

402
  cubicBezierCurveTo(
403
    end: Point2D,
404
    startControlPoint: Point2D,
405
    endControlPoint: Point2D
406
  ): this {
407
    return this.bezierCurveTo(end, [startControlPoint, endControlPoint]);
408
  }
409

410
  smoothSplineTo(end: Point2D, config?: SplineConfig): this {
411
    const { endTangent, startTangent, startFactor, endFactor } =
412
      defaultsSplineConfig(config);
413

414
    const previousCurve = this.pendingCurves.length
415
      ? this.pendingCurves[this.pendingCurves.length - 1]
416
      : null;
417

418
    const defaultDistance = distance2d(this.pointer, end) * 0.25;
419

420
    let startPoleDirection: Point2D;
421
    if (startTangent) {
422
      startPoleDirection = startTangent;
423
    } else if (!previousCurve) {
424
      startPoleDirection = [1, 0];
425
    } else {
426
      startPoleDirection = this._convertFromUV(previousCurve.tangentAt(1));
427
    }
428

429
    startPoleDirection = normalize2d(startPoleDirection);
430
    const startControl: Point2D = [
431
      this.pointer[0] + startPoleDirection[0] * startFactor * defaultDistance,
432
      this.pointer[1] + startPoleDirection[1] * startFactor * defaultDistance,
433
    ];
434

435
    let endPoleDirection: Point2D;
436
    if (endTangent === "symmetric") {
437
      endPoleDirection = [-startPoleDirection[0], -startPoleDirection[1]];
438
    } else {
439
      endPoleDirection = endTangent;
440
    }
441

442
    endPoleDirection = normalize2d(endPoleDirection);
443
    const endControl: Point2D = [
444
      end[0] - endPoleDirection[0] * endFactor * defaultDistance,
445
      end[1] - endPoleDirection[1] * endFactor * defaultDistance,
446
    ];
447

448
    return this.cubicBezierCurveTo(end, startControl, endControl);
449
  }
450

451
  smoothSpline(
452
    xDist: number,
453
    yDist: number,
454
    splineConfig?: SplineConfig
455
  ): this {
456
    return this.smoothSplineTo(
457
      [xDist + this.pointer[0], yDist + this.pointer[1]],
458
      splineConfig
459
    );
460
  }
461

462
  /**
463
   * Changes the corner between the previous and next segments.
464
   */
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");
468

469
    this._nextCorner = { mode, radius };
470
    return this;
471
  }
472

473
  protected _customCornerLastWithFirst(
474
    radius: number,
475
    mode: "fillet" | "chamfer" = "fillet"
476
  ) {
477
    if (!radius) return;
478

479
    const previousCurve = this.pendingCurves.pop();
480
    const curve = this.pendingCurves.shift();
481

482
    if (!previousCurve || !curve)
483
      throw new Error("Not enough curves to close and fillet");
484

485
    const makeCorner = mode === "chamfer" ? chamferCurves : filletCurves;
486

487
    this.pendingCurves.push(...makeCorner(previousCurve, curve, radius));
488
  }
489

490
  protected _closeSketch(): void {
491
    if (!samePoint(this.pointer, this.firstPoint)) {
492
      this.lineTo(this.firstPoint);
493
    }
494
  }
495

496
  protected _closeWithMirror() {
497
    if (samePoint(this.pointer, this.firstPoint))
498
      throw new Error(
499
        "Cannot close with a mirror when the sketch is already closed"
500
      );
501
    const startToEndVector: Point2D = [
502
      this.pointer[0] - this.firstPoint[0],
503
      this.pointer[1] - this.firstPoint[1],
504
    ];
505

506
    const mirrorAxis = axis2d(
507
      this._convertToUV(this.pointer),
508
      this._convertToUV(startToEndVector)
509
    );
510

511
    const mirroredCurves = this.pendingCurves.map(
512
      (c) =>
513
        new Curve2D(c.innerCurve.Mirrored_2(mirrorAxis) as Handle_Geom2d_Curve)
514
    );
515
    mirroredCurves.reverse();
516
    mirroredCurves.map((c) => c.reverse());
517
    this.pendingCurves.push(...mirroredCurves);
518
    this.pointer = this.firstPoint;
519
  }
520
}
521

522
/**
523
 * The FaceSketcher allows you to sketch on a face that is not planar, for
524
 * instance the sides of a cylinder.
525
 *
526
 * The coordinates passed to the methods corresponds to normalised distances on
527
 * this surface, between 0 and 1 in both direction.
528
 *
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.
532
 *
533
 * @category Sketching
534
 */
535
export default class FaceSketcher
536
  extends BaseSketcher2d
537
  implements GenericSketcher<Sketch>
538
{
539
  protected face: Face;
540
  protected _bounds: UVBounds;
541

542
  constructor(face: Face, origin: Point2D = [0, 0]) {
543
    super(origin);
544
    this.face = face.clone();
545
    this._bounds = face.UVBounds;
546
  }
547

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)];
551
  }
552

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)];
556
  }
557

558
  _adaptSurface(): Handle_Geom_Surface {
559
    const oc = getOC();
560
    // CHECK THIS: return new oc.BRep_Tool.Surface_2(this.face.wrapped)
561
    return oc.BRep_Tool.Surface_2(this.face.wrapped);
562
  }
563

564
  /**
565
   * @ignore
566
   */
567
  protected buildWire(): Wire {
568
    const [r, gc] = localGC();
569
    const oc = getOC();
570

571
    const geomSurf = r(this._adaptSurface());
572

573
    const edges = this.pendingCurves.map((curve) => {
574
      return r(
575
        new Edge(
576
          r(new oc.BRepBuilderAPI_MakeEdge_30(curve.wrapped, geomSurf)).Edge()
577
        )
578
      );
579
    });
580
    const wire = assembleWire(edges);
581
    oc.BRepLib.BuildCurves3d_2(wire.wrapped);
582

583
    gc();
584
    return wire;
585
  }
586

587
  done(): Sketch {
588
    const [r, gc] = localGC();
589

590
    const wire = this.buildWire();
591
    const sketch = new Sketch(wire);
592
    if (wire.isClosed) {
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));
596
    } else {
597
      const startPoint = r(wire.startPoint);
598
      sketch.defaultOrigin = startPoint;
599
      sketch.defaultDirection = r(this.face.normalAt(startPoint));
600
    }
601
    sketch.baseFace = this.face;
602
    gc();
603
    return sketch;
604
  }
605

606
  close(): Sketch {
607
    this._closeSketch();
608
    return this.done();
609
  }
610

611
  closeWithMirror(): Sketch {
612
    this._closeWithMirror();
613
    return this.close();
614
  }
615

616
  /**
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.
620
   */
621
  closeWithCustomCorner(
622
    radius: number,
623
    mode: "fillet" | "chamfer" = "fillet"
624
  ): Sketch {
625
    this._closeSketch();
626
    this._customCornerLastWithFirst(radius, mode);
627

628
    return this.done();
629
  }
630
}
631

632
export class BlueprintSketcher
633
  extends BaseSketcher2d
634
  implements GenericSketcher<Blueprint>
635
{
636
  constructor(origin: Point2D = [0, 0]) {
637
    super();
638
    this.pointer = origin;
639
    this.firstPoint = origin;
640

641
    this.pendingCurves = [];
642
  }
643

644
  done(): Blueprint {
645
    return new Blueprint(this.pendingCurves);
646
  }
647

648
  close(): Blueprint {
649
    this._closeSketch();
650
    return this.done();
651
  }
652

653
  closeWithMirror(): Blueprint {
654
    this._closeWithMirror();
655
    return this.close();
656
  }
657

658
  /**
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.
662
   */
663

664
  closeWithCustomCorner(
665
    radius: number,
666
    mode: "fillet" | "chamfer" = "fillet"
667
  ): Blueprint {
668
    this._closeSketch();
669
    this._customCornerLastWithFirst(radius, mode);
670

671
    return this.done();
672
  }
673
}
674

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.