@bitbybit-dev/base 0.20.2 → 0.20.3
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 +1 -1
- package/lib/api/inputs/base-inputs.d.ts +8 -0
- package/lib/api/inputs/index.d.ts +3 -0
- package/lib/api/inputs/index.js +3 -0
- package/lib/api/inputs/inputs.d.ts +3 -0
- package/lib/api/inputs/inputs.js +3 -0
- package/lib/api/inputs/line-inputs.d.ts +240 -0
- package/lib/api/inputs/line-inputs.js +247 -0
- package/lib/api/inputs/mesh-inputs.d.ts +82 -0
- package/lib/api/inputs/mesh-inputs.js +83 -0
- package/lib/api/inputs/point-inputs.d.ts +153 -0
- package/lib/api/inputs/point-inputs.js +188 -0
- package/lib/api/inputs/polyline-inputs.d.ts +206 -0
- package/lib/api/inputs/polyline-inputs.js +229 -0
- package/lib/api/inputs/text-inputs.d.ts +1 -1
- package/lib/api/inputs/transforms-inputs.d.ts +18 -0
- package/lib/api/inputs/transforms-inputs.js +29 -0
- package/lib/api/inputs/vector-inputs.d.ts +8 -0
- package/lib/api/inputs/vector-inputs.js +8 -0
- package/lib/api/models/index.d.ts +1 -0
- package/lib/api/models/index.js +1 -0
- package/lib/api/models/point/bucket.d.ts +1 -0
- package/lib/api/models/point/bucket.js +1 -0
- package/lib/api/models/point/hex-grid-data.d.ts +8 -0
- package/lib/api/models/point/hex-grid-data.js +2 -0
- package/lib/api/models/point/index.d.ts +1 -0
- package/lib/api/models/point/index.js +1 -0
- package/lib/api/services/dates.js +45 -15
- package/lib/api/services/index.d.ts +3 -0
- package/lib/api/services/index.js +3 -0
- package/lib/api/services/line.d.ts +149 -0
- package/lib/api/services/line.js +320 -0
- package/lib/api/services/lists.d.ts +1 -1
- package/lib/api/services/lists.js +1 -2
- package/lib/api/services/mesh.d.ts +66 -0
- package/lib/api/services/mesh.js +235 -0
- package/lib/api/services/point.d.ts +96 -1
- package/lib/api/services/point.js +540 -1
- package/lib/api/services/polyline.d.ts +149 -0
- package/lib/api/services/polyline.js +444 -0
- package/lib/api/services/transforms.d.ts +26 -1
- package/lib/api/services/transforms.js +66 -3
- package/lib/api/services/vector.d.ts +18 -0
- package/lib/api/services/vector.js +27 -0
- package/lib/api/unit-test-helper.d.ts +20 -0
- package/lib/api/unit-test-helper.js +130 -0
- package/package.json +2 -2
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
* When creating 2D points, z coordinate is simply set to 0 - [x, y, 0].
|
|
5
5
|
*/
|
|
6
6
|
export class Point {
|
|
7
|
-
constructor(geometryHelper, transforms, vector) {
|
|
7
|
+
constructor(geometryHelper, transforms, vector, lists) {
|
|
8
8
|
this.geometryHelper = geometryHelper;
|
|
9
9
|
this.transforms = transforms;
|
|
10
10
|
this.vector = vector;
|
|
11
|
+
this.lists = lists;
|
|
11
12
|
}
|
|
12
13
|
/**
|
|
13
14
|
* Transforms the single point
|
|
@@ -103,6 +104,18 @@ export class Point {
|
|
|
103
104
|
const scaleTransforms = this.transforms.scaleCenterXYZ({ center: inputs.center, scaleXyz: inputs.scaleXyz });
|
|
104
105
|
return this.geometryHelper.transformControlPoints(scaleTransforms, inputs.points);
|
|
105
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* Stretch multiple points by providing center point, direction and uniform scale factor
|
|
109
|
+
* @param inputs Contains points, center point, direction and scale factor
|
|
110
|
+
* @returns Stretched points
|
|
111
|
+
* @group transforms
|
|
112
|
+
* @shortname stretch points dir from center
|
|
113
|
+
* @drawable true
|
|
114
|
+
*/
|
|
115
|
+
stretchPointsDirFromCenter(inputs) {
|
|
116
|
+
const stretchTransforms = this.transforms.stretchDirFromCenter({ center: inputs.center, scale: inputs.scale, direction: inputs.direction });
|
|
117
|
+
return this.geometryHelper.transformControlPoints(stretchTransforms, inputs.points);
|
|
118
|
+
}
|
|
106
119
|
/**
|
|
107
120
|
* Rotate multiple points by providing center point, axis and degrees of rotation
|
|
108
121
|
* @param inputs Contains points, axis, center point and angle of rotation
|
|
@@ -359,6 +372,479 @@ export class Point {
|
|
|
359
372
|
}
|
|
360
373
|
return points;
|
|
361
374
|
}
|
|
375
|
+
/**
|
|
376
|
+
* Creates a pointy-top or flat-top hexagon grid, scaling hexagons to fit specified dimensions exactly.
|
|
377
|
+
* Returns both center points and the vertices of each (potentially scaled) hexagon.
|
|
378
|
+
* Hexagons are ordered column-first, then row-first.
|
|
379
|
+
* @param inputs Information about the desired grid dimensions and hexagon counts.
|
|
380
|
+
* @returns An object containing the array of center points and an array of hexagon vertex arrays.
|
|
381
|
+
* @group create
|
|
382
|
+
* @shortname scaled hex grid to fit
|
|
383
|
+
* @drawable false
|
|
384
|
+
*/
|
|
385
|
+
hexGridScaledToFit(inputs) {
|
|
386
|
+
var _a, _b, _c, _d;
|
|
387
|
+
let width = inputs.width;
|
|
388
|
+
let height = inputs.height;
|
|
389
|
+
let nrHexagonsInHeight = inputs.nrHexagonsInHeight;
|
|
390
|
+
let nrHexagonsInWidth = inputs.nrHexagonsInWidth;
|
|
391
|
+
let extendTop = (_a = inputs.extendTop) !== null && _a !== void 0 ? _a : false;
|
|
392
|
+
let extendBottom = (_b = inputs.extendBottom) !== null && _b !== void 0 ? _b : false;
|
|
393
|
+
let extendLeft = (_c = inputs.extendLeft) !== null && _c !== void 0 ? _c : false;
|
|
394
|
+
let extendRight = (_d = inputs.extendRight) !== null && _d !== void 0 ? _d : false;
|
|
395
|
+
const { flatTop = false, centerGrid = false, pointsOnGround = false } = inputs;
|
|
396
|
+
// we flip the width and height if the hexagons are flat-topped and will then rotate resuls afterwards as default
|
|
397
|
+
// computes pointy-top hexagons
|
|
398
|
+
if (flatTop) {
|
|
399
|
+
const oldWidth = width;
|
|
400
|
+
width = inputs.height;
|
|
401
|
+
height = oldWidth;
|
|
402
|
+
const oldNrHexagonsInWidth = nrHexagonsInWidth;
|
|
403
|
+
nrHexagonsInWidth = nrHexagonsInHeight;
|
|
404
|
+
nrHexagonsInHeight = oldNrHexagonsInWidth;
|
|
405
|
+
const extendTopOld = extendTop;
|
|
406
|
+
const extendBottomOld = extendBottom;
|
|
407
|
+
const extendLeftOld = extendLeft;
|
|
408
|
+
const extendRightOld = extendRight;
|
|
409
|
+
extendTop = extendLeftOld;
|
|
410
|
+
extendBottom = extendRightOld;
|
|
411
|
+
extendLeft = extendBottomOld;
|
|
412
|
+
extendRight = extendTopOld;
|
|
413
|
+
}
|
|
414
|
+
// --- Input Validation ---
|
|
415
|
+
if (width <= 0 || height <= 0 || nrHexagonsInWidth < 1 || nrHexagonsInHeight < 1) {
|
|
416
|
+
console.warn("Hex grid dimensions and counts must be positive.");
|
|
417
|
+
return { centers: [], hexagons: [], shortestDistEdge: undefined, longestDistEdge: undefined, maxFilletRadius: undefined };
|
|
418
|
+
}
|
|
419
|
+
// --- Generate Unscaled Regular Grid Centers (Radius = 1) ---
|
|
420
|
+
// Use the *existing* hexGrid function, ensuring it doesn't center or project yet.
|
|
421
|
+
const BASE_RADIUS = 1.0;
|
|
422
|
+
const unscaledCenters = this.hexGrid({
|
|
423
|
+
radiusHexagon: BASE_RADIUS,
|
|
424
|
+
nrHexagonsX: nrHexagonsInWidth,
|
|
425
|
+
nrHexagonsY: nrHexagonsInHeight,
|
|
426
|
+
orientOnCenter: false,
|
|
427
|
+
pointsOnGround: false // Keep on XY plane for now
|
|
428
|
+
});
|
|
429
|
+
if (unscaledCenters.length === 0) {
|
|
430
|
+
return { centers: [], hexagons: [], shortestDistEdge: undefined, longestDistEdge: undefined, maxFilletRadius: undefined }; // Return empty if base grid failed
|
|
431
|
+
}
|
|
432
|
+
// --- Generate Unscaled Regular Hexagon Vertices (Radius = 1) ---
|
|
433
|
+
const unscaledHexagons = unscaledCenters.map(center => this.getRegularHexagonVertices(center, BASE_RADIUS));
|
|
434
|
+
// --- Determine Dimensions of the Unscaled Grid Bounding Box ---
|
|
435
|
+
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
|
|
436
|
+
for (const hex of unscaledHexagons) {
|
|
437
|
+
for (const vertex of hex) {
|
|
438
|
+
if (vertex[0] < minX)
|
|
439
|
+
minX = vertex[0];
|
|
440
|
+
if (vertex[0] > maxX)
|
|
441
|
+
maxX = vertex[0];
|
|
442
|
+
if (vertex[1] < minY)
|
|
443
|
+
minY = vertex[1];
|
|
444
|
+
if (vertex[1] > maxY)
|
|
445
|
+
maxY = vertex[1];
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
const unscaledWidth = maxX - minX;
|
|
449
|
+
const unscaledHeight = maxY - minY;
|
|
450
|
+
// --- Step 4: Calculate Scaling Factors ---
|
|
451
|
+
// Handle potential zero dimensions if only 1 hex (W/H would be based on hex size)
|
|
452
|
+
const scaleX = (unscaledWidth > 1e-9) ? width / unscaledWidth : 1;
|
|
453
|
+
const scaleY = (unscaledHeight > 1e-9) ? height / unscaledHeight : 1;
|
|
454
|
+
// If unscaled W/H is 0 (e.g., 1x1 grid), scale=1 means the final hex will have
|
|
455
|
+
// width/height derived from its regular R=1 shape, not fitting totalW/H.
|
|
456
|
+
// This might need adjustment if a single hex *must* fill the total W/H.
|
|
457
|
+
// For now, assume nrU/nrV > 1 or accept R=1 size for single hex.
|
|
458
|
+
// --- Scale Centers and Vertices ---
|
|
459
|
+
// Scale relative to the min corner of the unscaled grid (minX, minY)
|
|
460
|
+
let scaledCenters = unscaledCenters.map(p => [
|
|
461
|
+
(p[0] - minX) * scaleX,
|
|
462
|
+
(p[1] - minY) * scaleY,
|
|
463
|
+
0 // Keep Z=0 for now
|
|
464
|
+
]);
|
|
465
|
+
let scaledHexagons = unscaledHexagons.map(hex => hex.map(v => [
|
|
466
|
+
(v[0] - minX) * scaleX,
|
|
467
|
+
(v[1] - minY) * scaleY,
|
|
468
|
+
0 // Keep Z=0 for now
|
|
469
|
+
]));
|
|
470
|
+
let shortestDistEdge = Infinity;
|
|
471
|
+
let longestDistEdge = -Infinity;
|
|
472
|
+
let maxFilletRadius = 0;
|
|
473
|
+
// --- Calculate Shortes/Longest & Extensions ---
|
|
474
|
+
if (scaledHexagons.length !== 0) {
|
|
475
|
+
const firstHex = scaledHexagons[0];
|
|
476
|
+
maxFilletRadius = this.safestPointsMaxFilletHalfLine({
|
|
477
|
+
points: firstHex,
|
|
478
|
+
checkLastWithFirst: true,
|
|
479
|
+
tolerance: 1e-7
|
|
480
|
+
});
|
|
481
|
+
// Calculate the shortest and longest edge distances
|
|
482
|
+
firstHex.forEach((pt, index) => {
|
|
483
|
+
const nextPt = firstHex[(index + 1) % firstHex.length];
|
|
484
|
+
const dist = this.distance({ startPoint: pt, endPoint: nextPt });
|
|
485
|
+
if (dist < shortestDistEdge) {
|
|
486
|
+
shortestDistEdge = dist;
|
|
487
|
+
}
|
|
488
|
+
if (dist > longestDistEdge) {
|
|
489
|
+
longestDistEdge = dist;
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
if (extendTop || extendBottom || extendLeft || extendRight) {
|
|
493
|
+
const pt1Pointy = firstHex[0];
|
|
494
|
+
const pt2Pointy = firstHex[1];
|
|
495
|
+
const cellHeight = pt1Pointy[1] - pt2Pointy[1];
|
|
496
|
+
const cellWidth = pt2Pointy[0] - pt1Pointy[0];
|
|
497
|
+
if (extendTop && !extendBottom) {
|
|
498
|
+
const transform = {
|
|
499
|
+
center: [0, 0, 0],
|
|
500
|
+
direction: [0, 1, 0],
|
|
501
|
+
scale: height / (height - cellHeight),
|
|
502
|
+
};
|
|
503
|
+
scaledHexagons = scaledHexagons.map(hex => {
|
|
504
|
+
transform.points = hex;
|
|
505
|
+
return this.stretchPointsDirFromCenter(transform);
|
|
506
|
+
});
|
|
507
|
+
transform.points = scaledCenters;
|
|
508
|
+
scaledCenters = this.stretchPointsDirFromCenter(transform);
|
|
509
|
+
}
|
|
510
|
+
if (extendBottom && !extendTop) {
|
|
511
|
+
const transform = {
|
|
512
|
+
center: [0, height, 0],
|
|
513
|
+
direction: [0, -1, 0],
|
|
514
|
+
scale: height / (height - cellHeight),
|
|
515
|
+
};
|
|
516
|
+
scaledHexagons = scaledHexagons.map(hex => {
|
|
517
|
+
transform.points = hex;
|
|
518
|
+
return this.stretchPointsDirFromCenter(transform);
|
|
519
|
+
});
|
|
520
|
+
transform.points = scaledCenters;
|
|
521
|
+
scaledCenters = this.stretchPointsDirFromCenter(transform);
|
|
522
|
+
}
|
|
523
|
+
if (extendTop && extendBottom) {
|
|
524
|
+
const transform = {
|
|
525
|
+
center: [0, height / 2, 0],
|
|
526
|
+
direction: [0, 1, 0],
|
|
527
|
+
scale: height / (height - cellHeight * 2),
|
|
528
|
+
};
|
|
529
|
+
scaledHexagons = scaledHexagons.map(hex => {
|
|
530
|
+
transform.points = hex;
|
|
531
|
+
return this.stretchPointsDirFromCenter(transform);
|
|
532
|
+
});
|
|
533
|
+
transform.points = scaledCenters;
|
|
534
|
+
scaledCenters = this.stretchPointsDirFromCenter(transform);
|
|
535
|
+
}
|
|
536
|
+
if (extendLeft && !extendRight) {
|
|
537
|
+
const transform = {
|
|
538
|
+
center: [width, 0, 0],
|
|
539
|
+
direction: [1, 0, 0],
|
|
540
|
+
scale: width / (width - cellWidth),
|
|
541
|
+
};
|
|
542
|
+
scaledHexagons = scaledHexagons.map(hex => {
|
|
543
|
+
transform.points = hex;
|
|
544
|
+
return this.stretchPointsDirFromCenter(transform);
|
|
545
|
+
});
|
|
546
|
+
transform.points = scaledCenters;
|
|
547
|
+
scaledCenters = this.stretchPointsDirFromCenter(transform);
|
|
548
|
+
}
|
|
549
|
+
if (extendRight && !extendLeft) {
|
|
550
|
+
const transform = {
|
|
551
|
+
center: [0, 0, 0],
|
|
552
|
+
direction: [1, 0, 0],
|
|
553
|
+
scale: width / (width - cellWidth),
|
|
554
|
+
};
|
|
555
|
+
scaledHexagons = scaledHexagons.map(hex => {
|
|
556
|
+
transform.points = hex;
|
|
557
|
+
return this.stretchPointsDirFromCenter(transform);
|
|
558
|
+
});
|
|
559
|
+
transform.points = scaledCenters;
|
|
560
|
+
scaledCenters = this.stretchPointsDirFromCenter(transform);
|
|
561
|
+
}
|
|
562
|
+
if (extendLeft && extendRight) {
|
|
563
|
+
const transform = {
|
|
564
|
+
center: [width / 2, 0, 0],
|
|
565
|
+
direction: [1, 0, 0],
|
|
566
|
+
scale: width / (width - cellWidth * 2),
|
|
567
|
+
};
|
|
568
|
+
scaledHexagons = scaledHexagons.map(hex => {
|
|
569
|
+
transform.points = hex;
|
|
570
|
+
return this.stretchPointsDirFromCenter(transform);
|
|
571
|
+
});
|
|
572
|
+
transform.points = scaledCenters;
|
|
573
|
+
scaledCenters = this.stretchPointsDirFromCenter(transform);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
if (flatTop) {
|
|
578
|
+
// width and height are swapped
|
|
579
|
+
scaledCenters = this.rotatePointsCenterAxis({
|
|
580
|
+
points: scaledCenters,
|
|
581
|
+
center: [width / 2, height / 2, 0],
|
|
582
|
+
axis: [0, 0, 1],
|
|
583
|
+
angle: 90
|
|
584
|
+
});
|
|
585
|
+
scaledHexagons = scaledHexagons.map(hex => {
|
|
586
|
+
return this.rotatePointsCenterAxis({
|
|
587
|
+
points: hex,
|
|
588
|
+
center: [width / 2, height / 2, 0],
|
|
589
|
+
axis: [0, 0, 1],
|
|
590
|
+
angle: 90
|
|
591
|
+
});
|
|
592
|
+
});
|
|
593
|
+
// translate to new center
|
|
594
|
+
const vecTranslation = this.vector.sub({
|
|
595
|
+
first: [height / 2, width / 2, 0],
|
|
596
|
+
second: [width / 2, height / 2, 0]
|
|
597
|
+
});
|
|
598
|
+
scaledCenters = this.translatePoints({
|
|
599
|
+
points: scaledCenters,
|
|
600
|
+
translation: vecTranslation
|
|
601
|
+
});
|
|
602
|
+
scaledHexagons = scaledHexagons.map(hex => {
|
|
603
|
+
return this.translatePoints({
|
|
604
|
+
points: hex,
|
|
605
|
+
translation: vecTranslation
|
|
606
|
+
});
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
// --- Apply Optional Centering ---
|
|
610
|
+
// Center the scaled grid (currently starting at [0,0]) around [0,0]
|
|
611
|
+
if (centerGrid) {
|
|
612
|
+
let shiftX = width / 2;
|
|
613
|
+
let shiftY = height / 2;
|
|
614
|
+
if (flatTop) {
|
|
615
|
+
shiftX = height / 2;
|
|
616
|
+
shiftY = width / 2;
|
|
617
|
+
}
|
|
618
|
+
for (let i = 0; i < scaledCenters.length; i++) {
|
|
619
|
+
scaledCenters[i][0] -= shiftX;
|
|
620
|
+
scaledCenters[i][1] -= shiftY;
|
|
621
|
+
}
|
|
622
|
+
for (let i = 0; i < scaledHexagons.length; i++) {
|
|
623
|
+
for (let j = 0; j < scaledHexagons[i].length; j++) {
|
|
624
|
+
scaledHexagons[i][j][0] -= shiftX;
|
|
625
|
+
scaledHexagons[i][j][1] -= shiftY;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
// --- Apply Optional Ground Projection ---
|
|
630
|
+
if (pointsOnGround) {
|
|
631
|
+
for (let i = 0; i < scaledCenters.length; i++) {
|
|
632
|
+
scaledCenters[i] = [scaledCenters[i][0], 0, scaledCenters[i][1]];
|
|
633
|
+
}
|
|
634
|
+
for (let i = 0; i < scaledHexagons.length; i++) {
|
|
635
|
+
for (let j = 0; j < scaledHexagons[i].length; j++) {
|
|
636
|
+
scaledHexagons[i][j] = [scaledHexagons[i][j][0], 0, scaledHexagons[i][j][1]];
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
// We need to adjust orders to be column first and then row first if we choose flat top
|
|
641
|
+
if (flatTop) {
|
|
642
|
+
const grouped = this.lists.groupNth({
|
|
643
|
+
list: scaledHexagons.reverse(),
|
|
644
|
+
nrElements: inputs.nrHexagonsInWidth,
|
|
645
|
+
keepRemainder: true,
|
|
646
|
+
});
|
|
647
|
+
const res = this.lists.flipLists({
|
|
648
|
+
list: grouped
|
|
649
|
+
});
|
|
650
|
+
res.forEach(s => s.reverse());
|
|
651
|
+
scaledHexagons = res.flat();
|
|
652
|
+
const groupedCenters = this.lists.groupNth({
|
|
653
|
+
list: scaledCenters.reverse(),
|
|
654
|
+
nrElements: inputs.nrHexagonsInWidth,
|
|
655
|
+
keepRemainder: true,
|
|
656
|
+
});
|
|
657
|
+
const resCenters = this.lists.flipLists({
|
|
658
|
+
list: groupedCenters
|
|
659
|
+
});
|
|
660
|
+
resCenters.forEach(s => s.reverse());
|
|
661
|
+
scaledCenters = resCenters.flat();
|
|
662
|
+
}
|
|
663
|
+
// --- Return Result ---
|
|
664
|
+
return {
|
|
665
|
+
centers: scaledCenters,
|
|
666
|
+
hexagons: scaledHexagons,
|
|
667
|
+
shortestDistEdge,
|
|
668
|
+
longestDistEdge,
|
|
669
|
+
maxFilletRadius
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Calculates the maximum possible fillet radius at a corner formed by two line segments
|
|
674
|
+
* sharing an endpoint (C), such that the fillet arc is tangent to both segments
|
|
675
|
+
* and lies entirely within them.
|
|
676
|
+
* @param inputs three points and the tolerance
|
|
677
|
+
* @returns the maximum fillet radius
|
|
678
|
+
* @group fillet
|
|
679
|
+
* @shortname max fillet radius
|
|
680
|
+
* @drawable false
|
|
681
|
+
*/
|
|
682
|
+
maxFilletRadius(inputs) {
|
|
683
|
+
const { start: p1, center: p2, end: c, tolerance = 1e-7 } = inputs;
|
|
684
|
+
const v1 = this.vector.sub({ first: p1, second: c });
|
|
685
|
+
const v2 = this.vector.sub({ first: p2, second: c });
|
|
686
|
+
const len1 = this.vector.length({ vector: v1 });
|
|
687
|
+
const len2 = this.vector.length({ vector: v2 });
|
|
688
|
+
if (len1 < tolerance || len2 < tolerance) {
|
|
689
|
+
return 0;
|
|
690
|
+
}
|
|
691
|
+
const normV1 = this.vector.normalized({ vector: v1 });
|
|
692
|
+
const normV2 = this.vector.normalized({ vector: v2 });
|
|
693
|
+
if (!normV1 || !normV2) {
|
|
694
|
+
return 0;
|
|
695
|
+
}
|
|
696
|
+
// Calculate the cosine of the angle between the vectors
|
|
697
|
+
// Clamp to [-1, 1] to avoid potential domain errors with acos due to floating point inaccuracies
|
|
698
|
+
const cosAlpha = Math.max(-1.0, Math.min(1.0, this.vector.dot({ first: normV1, second: normV2 })));
|
|
699
|
+
// Check for collinearity
|
|
700
|
+
// If vectors point in the same direction (angle ~ 0), no fillet
|
|
701
|
+
if (cosAlpha > 1.0 - tolerance) {
|
|
702
|
+
return 0;
|
|
703
|
+
}
|
|
704
|
+
// If vectors point in opposite directions (angle ~ 180 deg), no corner for a fillet
|
|
705
|
+
if (cosAlpha < -1.0 + tolerance) {
|
|
706
|
+
return 0;
|
|
707
|
+
}
|
|
708
|
+
// Calculate the angle alpha (0 < alpha < PI)
|
|
709
|
+
const alpha = Math.acos(cosAlpha);
|
|
710
|
+
// Calculate tan(alpha / 2)
|
|
711
|
+
// alpha/2 is between 0 and PI/2, so tan is positive and non-zero
|
|
712
|
+
const tanHalfAlpha = Math.tan(alpha / 2.0);
|
|
713
|
+
// If tanHalfAlpha is extremely small (alpha near 0, shouldn't happen due to collinearity check), return 0
|
|
714
|
+
if (tanHalfAlpha < tolerance) {
|
|
715
|
+
return 0;
|
|
716
|
+
}
|
|
717
|
+
// The distance 'd' from corner C to the tangent point must be less than or equal to the segment lengths.
|
|
718
|
+
// d = r / tan(alpha/2) <= min(len1, len2)
|
|
719
|
+
// r <= min(len1, len2) * tan(alpha/2)
|
|
720
|
+
const maxRadius = Math.min(len1, len2) * tanHalfAlpha;
|
|
721
|
+
return maxRadius;
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Calculates the maximum possible fillet radius at a corner C, such that the fillet arc
|
|
725
|
+
* is tangent to both segments (P1-C, P2-C) and the tangent points lie within
|
|
726
|
+
* the first half of each segment (measured from C).
|
|
727
|
+
* @param inputs three points and the tolerance
|
|
728
|
+
* @returns the maximum fillet radius
|
|
729
|
+
* @group fillet
|
|
730
|
+
* @shortname max fillet radius half line
|
|
731
|
+
* @drawable false
|
|
732
|
+
*/
|
|
733
|
+
maxFilletRadiusHalfLine(inputs) {
|
|
734
|
+
const { start: p1, center: p2, end: c, tolerance = 1e-7 } = inputs;
|
|
735
|
+
const v1 = this.vector.sub({ first: p1, second: c });
|
|
736
|
+
const v2 = this.vector.sub({ first: p2, second: c });
|
|
737
|
+
const len1 = this.vector.length({ vector: v1 });
|
|
738
|
+
const len2 = this.vector.length({ vector: v2 });
|
|
739
|
+
if (len1 < tolerance || len2 < tolerance) {
|
|
740
|
+
return 0;
|
|
741
|
+
}
|
|
742
|
+
const normV1 = this.vector.normalized({ vector: v1 });
|
|
743
|
+
const normV2 = this.vector.normalized({ vector: v2 });
|
|
744
|
+
if (!normV1 || !normV2) {
|
|
745
|
+
return 0;
|
|
746
|
+
}
|
|
747
|
+
const cosAlpha = Math.max(-1.0, Math.min(1.0, this.vector.dot({ first: normV1, second: normV2 })));
|
|
748
|
+
if (cosAlpha > 1.0 - tolerance || cosAlpha < -1.0 + tolerance) {
|
|
749
|
+
return 0; // Collinear
|
|
750
|
+
}
|
|
751
|
+
const alpha = Math.acos(cosAlpha);
|
|
752
|
+
const tanHalfAlpha = Math.tan(alpha / 2.0);
|
|
753
|
+
if (tanHalfAlpha < tolerance) {
|
|
754
|
+
return 0;
|
|
755
|
+
}
|
|
756
|
+
// The distance 'd' from corner C to the tangent point must be less than or equal
|
|
757
|
+
// to HALF the length of each segment.
|
|
758
|
+
// d = r / tan(alpha/2) <= min(len1 / 2, len2 / 2)
|
|
759
|
+
// r <= min(len1 / 2, len2 / 2) * tan(alpha/2)
|
|
760
|
+
const maxRadius = Math.min(len1 / 2.0, len2 / 2.0) * tanHalfAlpha;
|
|
761
|
+
return maxRadius;
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Calculates the maximum possible fillet radius at each corner of a polyline formed by
|
|
765
|
+
* formed by a series of points. The fillet radius is calculated for each internal
|
|
766
|
+
* corner and optionally for the closing corners if the polyline is closed.
|
|
767
|
+
* @param inputs Points, checkLastWithFirst flag, and tolerance
|
|
768
|
+
* @returns Array of maximum fillet radii for each corner
|
|
769
|
+
* @group fillet
|
|
770
|
+
* @shortname max fillets half line
|
|
771
|
+
* @drawable false
|
|
772
|
+
*/
|
|
773
|
+
maxFilletsHalfLine(inputs) {
|
|
774
|
+
const { points, checkLastWithFirst = false, tolerance = 1e-7 } = inputs;
|
|
775
|
+
const n = points.length;
|
|
776
|
+
const results = [];
|
|
777
|
+
// Need at least 3 points to form a corner
|
|
778
|
+
if (n < 3) {
|
|
779
|
+
return results;
|
|
780
|
+
}
|
|
781
|
+
// 1. Calculate fillets for internal corners (P[1] to P[n-2])
|
|
782
|
+
for (let i = 1; i < n - 1; i++) {
|
|
783
|
+
const p_prev = points[i - 1];
|
|
784
|
+
const p_corner = points[i];
|
|
785
|
+
const p_next = points[i + 1];
|
|
786
|
+
// Map geometric points to the DTO structure used by calculateMaxFilletRadiusHalfLine
|
|
787
|
+
// DTO: { start: P_prev, center: P_next, end: P_corner, tolerance }
|
|
788
|
+
const cornerInput = {
|
|
789
|
+
start: p_prev,
|
|
790
|
+
center: p_next,
|
|
791
|
+
end: p_corner,
|
|
792
|
+
tolerance: tolerance
|
|
793
|
+
};
|
|
794
|
+
results.push(this.maxFilletRadiusHalfLine(cornerInput));
|
|
795
|
+
}
|
|
796
|
+
// 2. Calculate fillets for closing corners if it's a closed polyline
|
|
797
|
+
if (checkLastWithFirst && n >= 3) {
|
|
798
|
+
// Corner at P[0] (formed by P[n-1]-P[0] and P[1]-P[0])
|
|
799
|
+
const p_prev_start = points[n - 1]; // Previous point is the last point
|
|
800
|
+
const p_corner_start = points[0];
|
|
801
|
+
const p_next_start = points[1];
|
|
802
|
+
const startCornerInput = {
|
|
803
|
+
start: p_prev_start,
|
|
804
|
+
center: p_next_start,
|
|
805
|
+
end: p_corner_start,
|
|
806
|
+
tolerance: tolerance
|
|
807
|
+
};
|
|
808
|
+
results.push(this.maxFilletRadiusHalfLine(startCornerInput));
|
|
809
|
+
// Corner at P[n-1] (formed by P[n-2]-P[n-1] and P[0]-P[n-1])
|
|
810
|
+
const p_prev_end = points[n - 2];
|
|
811
|
+
const p_corner_end = points[n - 1];
|
|
812
|
+
const p_next_end = points[0]; // Next point wraps around to the first point
|
|
813
|
+
const endCornerInput = {
|
|
814
|
+
start: p_prev_end,
|
|
815
|
+
center: p_next_end,
|
|
816
|
+
end: p_corner_end,
|
|
817
|
+
tolerance: tolerance
|
|
818
|
+
};
|
|
819
|
+
results.push(this.maxFilletRadiusHalfLine(endCornerInput));
|
|
820
|
+
}
|
|
821
|
+
return results;
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* Calculates the single safest maximum fillet radius that can be applied
|
|
825
|
+
* uniformly to all corners of collection of points, based on the 'half-line' constraint.
|
|
826
|
+
* This is determined by finding the minimum of the maximum possible fillet
|
|
827
|
+
* radii calculated for each individual corner.
|
|
828
|
+
* @param inputs Defines the points, whether it's closed, and an optional tolerance.
|
|
829
|
+
* @returns The smallest value from the results of pointsMaxFilletsHalfLine.
|
|
830
|
+
* Returns 0 if the polyline has fewer than 3 points or if any
|
|
831
|
+
* calculated maximum radius is 0.
|
|
832
|
+
* @group fillet
|
|
833
|
+
* @shortname safest fillet radii points
|
|
834
|
+
* @drawable false
|
|
835
|
+
*/
|
|
836
|
+
safestPointsMaxFilletHalfLine(inputs) {
|
|
837
|
+
const allMaxRadii = this.maxFilletsHalfLine(inputs);
|
|
838
|
+
if (allMaxRadii.length === 0) {
|
|
839
|
+
// No corners, or fewer than 3 points. No fillet possible.
|
|
840
|
+
return 0;
|
|
841
|
+
}
|
|
842
|
+
// Find the minimum radius among all calculated maximums.
|
|
843
|
+
// If any corner calculation resulted in 0, the safest radius is 0.
|
|
844
|
+
const safestRadius = Math.min(...allMaxRadii);
|
|
845
|
+
// Ensure we don't return a negative radius if Math.min had weird input (shouldn't happen here)
|
|
846
|
+
return Math.max(0, safestRadius);
|
|
847
|
+
}
|
|
362
848
|
/**
|
|
363
849
|
* Removes consecutive duplicates from the point array with tolerance
|
|
364
850
|
* @param inputs points, tolerance and check first and last
|
|
@@ -426,4 +912,57 @@ export class Point {
|
|
|
426
912
|
}
|
|
427
913
|
return { index: closestPointIndex + 1, distance, point };
|
|
428
914
|
}
|
|
915
|
+
/**
|
|
916
|
+
* Checks if two points are almost equal
|
|
917
|
+
* @param inputs Two points and the tolerance
|
|
918
|
+
* @returns true if the points are almost equal
|
|
919
|
+
* @group measure
|
|
920
|
+
* @shortname two points almost equal
|
|
921
|
+
* @drawable false
|
|
922
|
+
*/
|
|
923
|
+
twoPointsAlmostEqual(inputs) {
|
|
924
|
+
const p1 = inputs.point1;
|
|
925
|
+
const p2 = inputs.point2;
|
|
926
|
+
const dist = this.distance({ startPoint: p1, endPoint: p2 });
|
|
927
|
+
return dist < inputs.tolerance;
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* Sorts points lexicographically (X, then Y, then Z)
|
|
931
|
+
* @param inputs points
|
|
932
|
+
* @returns sorted points
|
|
933
|
+
* @group sort
|
|
934
|
+
* @shortname sort points
|
|
935
|
+
* @drawable true
|
|
936
|
+
*/
|
|
937
|
+
sortPoints(inputs) {
|
|
938
|
+
return [...inputs.points].sort((a, b) => {
|
|
939
|
+
if (a[0] !== b[0])
|
|
940
|
+
return a[0] - b[0];
|
|
941
|
+
if (a[1] !== b[1])
|
|
942
|
+
return a[1] - b[1];
|
|
943
|
+
return a[2] - b[2];
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* Calculates the 6 vertices of a regular flat-top hexagon.
|
|
948
|
+
* @param center The center point [x, y, z].
|
|
949
|
+
* @param radius The radius (distance from center to vertex).
|
|
950
|
+
* @returns An array of 6 Point3 vertices in counter-clockwise order.
|
|
951
|
+
*/
|
|
952
|
+
getRegularHexagonVertices(center, radius) {
|
|
953
|
+
const vertices = [];
|
|
954
|
+
const cx = center[0];
|
|
955
|
+
const cy = center[1];
|
|
956
|
+
const cz = center[2];
|
|
957
|
+
const angleStep = Math.PI / 3;
|
|
958
|
+
for (let i = 0; i < 6; i++) {
|
|
959
|
+
const angle = angleStep * i;
|
|
960
|
+
vertices.push([
|
|
961
|
+
cx + radius * Math.sin(angle),
|
|
962
|
+
cy + radius * Math.cos(angle),
|
|
963
|
+
cz // Maintain original Z
|
|
964
|
+
]);
|
|
965
|
+
}
|
|
966
|
+
return vertices;
|
|
967
|
+
}
|
|
429
968
|
}
|