@gridspace/raster-path 1.0.6 → 1.0.8
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/build/app.js +187 -12
- package/build/index.html +9 -2
- package/build/raster-path.js +125 -11
- package/build/raster-worker.js +952 -7
- package/package.json +6 -3
- package/src/core/path-radial-v3.js +405 -0
- package/src/core/path-radial.js +0 -1
- package/src/core/path-tracing.js +492 -0
- package/src/core/raster-config.js +37 -4
- package/src/core/raster-path.js +125 -11
- package/src/core/raster-worker.js +51 -0
- package/src/core/workload-calibrate.js +57 -3
- package/src/shaders/radial-rasterize-batched.wgsl +164 -0
- package/src/shaders/radial-rotate-triangles.wgsl +70 -0
- package/src/shaders/tracing-toolpath.wgsl +95 -0
- package/src/test/radial-v3-benchmark.cjs +184 -0
- package/src/test/radial-v3-bucket-test.cjs +154 -0
- package/src/test/tracing-test.cjs +307 -0
- package/src/web/app.js +187 -12
- package/src/web/index.html +9 -2
package/build/app.js
CHANGED
|
@@ -12,6 +12,7 @@ let zFloor = -100;
|
|
|
12
12
|
let xStep = 5;
|
|
13
13
|
let yStep = 5;
|
|
14
14
|
let angleStep = 1.0; // degrees
|
|
15
|
+
let traceStep = 0.5; // mm - sampling resolution for tracing mode
|
|
15
16
|
let toolSize = 2.5; // mm - target tool diameter
|
|
16
17
|
|
|
17
18
|
let modelSTL = null; // ArrayBuffer (current, possibly rotated)
|
|
@@ -61,6 +62,7 @@ function saveParameters() {
|
|
|
61
62
|
localStorage.setItem('raster-xStep', xStep);
|
|
62
63
|
localStorage.setItem('raster-yStep', yStep);
|
|
63
64
|
localStorage.setItem('raster-angleStep', angleStep);
|
|
65
|
+
localStorage.setItem('raster-traceStep', traceStep);
|
|
64
66
|
localStorage.setItem('raster-toolSize', toolSize);
|
|
65
67
|
console.log(`[App] Saved tool size: ${toolSize}mm`);
|
|
66
68
|
|
|
@@ -69,6 +71,12 @@ function saveParameters() {
|
|
|
69
71
|
if (showWrappedCheckbox) {
|
|
70
72
|
localStorage.setItem('raster-showWrapped', showWrappedCheckbox.checked);
|
|
71
73
|
}
|
|
74
|
+
|
|
75
|
+
// Save radial V3 checkbox
|
|
76
|
+
const radialV3Checkbox = document.getElementById('radial-v3');
|
|
77
|
+
if (radialV3Checkbox) {
|
|
78
|
+
localStorage.setItem('raster-radialV3', radialV3Checkbox.checked);
|
|
79
|
+
}
|
|
72
80
|
}
|
|
73
81
|
|
|
74
82
|
function loadParameters() {
|
|
@@ -127,6 +135,12 @@ function loadParameters() {
|
|
|
127
135
|
document.getElementById('angle-step').value = angleStep;
|
|
128
136
|
}
|
|
129
137
|
|
|
138
|
+
const savedTraceStep = localStorage.getItem('raster-traceStep');
|
|
139
|
+
if (savedTraceStep !== null) {
|
|
140
|
+
traceStep = parseFloat(savedTraceStep);
|
|
141
|
+
document.getElementById('trace-step').value = traceStep;
|
|
142
|
+
}
|
|
143
|
+
|
|
130
144
|
const savedToolSize = localStorage.getItem('raster-toolSize');
|
|
131
145
|
if (savedToolSize !== null) {
|
|
132
146
|
toolSize = parseFloat(savedToolSize);
|
|
@@ -145,6 +159,15 @@ function loadParameters() {
|
|
|
145
159
|
showWrappedCheckbox.checked = savedShowWrapped === 'true';
|
|
146
160
|
}
|
|
147
161
|
}
|
|
162
|
+
|
|
163
|
+
// Restore radial V3 checkbox
|
|
164
|
+
const savedRadialV3 = localStorage.getItem('raster-radialV3');
|
|
165
|
+
if (savedRadialV3 !== null) {
|
|
166
|
+
const radialV3Checkbox = document.getElementById('radial-v3');
|
|
167
|
+
if (radialV3Checkbox) {
|
|
168
|
+
radialV3Checkbox.checked = savedRadialV3 === 'true';
|
|
169
|
+
}
|
|
170
|
+
}
|
|
148
171
|
}
|
|
149
172
|
|
|
150
173
|
// ============================================================================
|
|
@@ -513,10 +536,14 @@ async function initRasterPath() {
|
|
|
513
536
|
rasterPath.terminate();
|
|
514
537
|
}
|
|
515
538
|
|
|
539
|
+
const radialV3Checkbox = document.getElementById('radial-v3');
|
|
540
|
+
const useRadialV3 = mode === 'radial' && radialV3Checkbox && radialV3Checkbox.checked;
|
|
541
|
+
|
|
516
542
|
rasterPath = new RasterPath({
|
|
517
543
|
mode: mode,
|
|
518
544
|
resolution: resolution,
|
|
519
545
|
rotationStep: mode === 'radial' ? angleStep : undefined,
|
|
546
|
+
radialV3: useRadialV3,
|
|
520
547
|
batchDivisor: 5,
|
|
521
548
|
debug: true
|
|
522
549
|
});
|
|
@@ -545,8 +572,8 @@ async function rasterizeAll() {
|
|
|
545
572
|
updateInfo(`Tool loaded in ${(t1 - t0).toFixed(0)}ms`);
|
|
546
573
|
}
|
|
547
574
|
|
|
548
|
-
if (mode === 'planar') {
|
|
549
|
-
// Planar mode: rasterize terrain immediately
|
|
575
|
+
if (mode === 'planar' || mode === 'tracing') {
|
|
576
|
+
// Planar/Tracing mode: rasterize terrain immediately
|
|
550
577
|
if (modelTriangles) {
|
|
551
578
|
updateInfo('Rasterizing terrain...');
|
|
552
579
|
const t0 = performance.now();
|
|
@@ -557,7 +584,7 @@ async function rasterizeAll() {
|
|
|
557
584
|
const t1 = performance.now();
|
|
558
585
|
updateInfo(`Terrain rasterized in ${(t1 - t0).toFixed(0)}ms`);
|
|
559
586
|
}
|
|
560
|
-
} else {
|
|
587
|
+
} else if (mode === 'radial') {
|
|
561
588
|
// Radial mode: MUST load tool FIRST
|
|
562
589
|
if (!toolTriangles) {
|
|
563
590
|
updateInfo('Error: Radial mode requires tool to be loaded first');
|
|
@@ -608,30 +635,81 @@ async function generateToolpath() {
|
|
|
608
635
|
updateInfo('Model must be rasterized first');
|
|
609
636
|
return;
|
|
610
637
|
}
|
|
611
|
-
} else {
|
|
638
|
+
} else if (mode === 'radial') {
|
|
612
639
|
// Radial mode: terrain must be loaded (stored internally)
|
|
613
640
|
if (!modelTriangles) {
|
|
614
641
|
updateInfo('Model STL must be loaded');
|
|
615
642
|
return;
|
|
616
643
|
}
|
|
644
|
+
} else if (mode === 'tracing') {
|
|
645
|
+
// Tracing mode: terrain must be rasterized
|
|
646
|
+
if (!modelRasterData) {
|
|
647
|
+
updateInfo('Model must be rasterized first');
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
617
650
|
}
|
|
618
651
|
|
|
619
652
|
try {
|
|
620
653
|
const t0 = performance.now();
|
|
621
654
|
updateInfo('Generating toolpath...');
|
|
622
655
|
|
|
623
|
-
//
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
656
|
+
// Generate trace paths for tracing mode
|
|
657
|
+
let tracePaths = null;
|
|
658
|
+
if (mode === 'tracing') {
|
|
659
|
+
// Get model bounds from raster data
|
|
660
|
+
const bounds = modelRasterData.bounds;
|
|
661
|
+
const minX = bounds.min.x;
|
|
662
|
+
const maxX = bounds.max.x;
|
|
663
|
+
const minY = bounds.min.y;
|
|
664
|
+
const maxY = bounds.max.y;
|
|
665
|
+
|
|
666
|
+
// Create two cross paths: horizontal through center, vertical through center
|
|
667
|
+
const centerY = (minY + maxY) / 2;
|
|
668
|
+
const centerX = (minX + maxX) / 2;
|
|
669
|
+
|
|
670
|
+
tracePaths = [
|
|
671
|
+
new Float32Array([minX, centerY, maxX, centerY]), // Horizontal line
|
|
672
|
+
new Float32Array([centerX, minY, centerX, maxY]) // Vertical line
|
|
673
|
+
];
|
|
674
|
+
|
|
675
|
+
debug.log(`Generated trace paths: H(${minX.toFixed(2)}, ${centerY.toFixed(2)}) to (${maxX.toFixed(2)}, ${centerY.toFixed(2)})`);
|
|
676
|
+
debug.log(` V(${centerX.toFixed(2)}, ${minY.toFixed(2)}) to (${centerX.toFixed(2)}, ${maxY.toFixed(2)})`);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// Unified API - works for all modes!
|
|
680
|
+
const generateParams = {
|
|
627
681
|
zFloor: zFloor
|
|
628
|
-
}
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
if (mode === 'tracing') {
|
|
685
|
+
generateParams.paths = tracePaths;
|
|
686
|
+
generateParams.step = traceStep;
|
|
687
|
+
} else {
|
|
688
|
+
generateParams.xStep = xStep;
|
|
689
|
+
generateParams.yStep = yStep;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
toolpathData = await rasterPath.generateToolpaths(generateParams);
|
|
629
693
|
|
|
630
694
|
const t1 = performance.now();
|
|
631
695
|
|
|
632
696
|
if (mode === 'planar') {
|
|
633
697
|
const numPoints = toolpathData.pathData.length;
|
|
634
698
|
updateInfo(`Toolpath generated: ${numPoints.toLocaleString()} points in ${(t1 - t0).toFixed(0)}ms`);
|
|
699
|
+
} else if (mode === 'tracing') {
|
|
700
|
+
const totalPoints = toolpathData.paths.reduce((sum, path) => sum + path.length / 3, 0);
|
|
701
|
+
debug.log(`[Tracing] Generated ${toolpathData.paths.length} paths with ${totalPoints} total points`);
|
|
702
|
+
|
|
703
|
+
// Log sample Z values from each path
|
|
704
|
+
toolpathData.paths.forEach((path, idx) => {
|
|
705
|
+
const zValues = [];
|
|
706
|
+
for (let i = 2; i < Math.min(path.length, 15); i += 3) {
|
|
707
|
+
zValues.push(path[i].toFixed(2));
|
|
708
|
+
}
|
|
709
|
+
debug.log(`[Tracing] Path ${idx} Z samples:`, zValues.join(', '));
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
updateInfo(`Toolpath generated: ${toolpathData.paths.length} paths, ${totalPoints.toLocaleString()} points in ${(t1 - t0).toFixed(0)}ms`);
|
|
635
713
|
} else {
|
|
636
714
|
// debug.log('[Radial] Toolpaths generated:', toolpathData);
|
|
637
715
|
debug.log(`[Radial] Received ${toolpathData.strips.length} strips from worker, numStrips=${toolpathData.numStrips}`);
|
|
@@ -901,8 +979,8 @@ function displayModelRaster(wrapped) {
|
|
|
901
979
|
const positions = [];
|
|
902
980
|
const colors = [];
|
|
903
981
|
|
|
904
|
-
if (mode === 'planar') {
|
|
905
|
-
// Planar: terrain is dense (Z-only array)
|
|
982
|
+
if (mode === 'planar' || mode === 'tracing') {
|
|
983
|
+
// Planar/Tracing: terrain is dense (Z-only array)
|
|
906
984
|
const { positions: rasterPos, bounds, gridWidth, gridHeight } = modelRasterData;
|
|
907
985
|
const stepSize = resolution;
|
|
908
986
|
|
|
@@ -1057,6 +1135,59 @@ function displayToolpaths(wrapped) {
|
|
|
1057
1135
|
return;
|
|
1058
1136
|
}
|
|
1059
1137
|
|
|
1138
|
+
if (mode === 'tracing') {
|
|
1139
|
+
// Tracing toolpaths - array of XYZ paths
|
|
1140
|
+
const { paths } = toolpathData;
|
|
1141
|
+
|
|
1142
|
+
// Calculate total points
|
|
1143
|
+
let totalPoints = 0;
|
|
1144
|
+
for (const path of paths) {
|
|
1145
|
+
totalPoints += path.length / 3;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
debug.log('[Toolpath Display] Tracing mode:', paths.length, 'paths,', totalPoints, 'total points');
|
|
1149
|
+
|
|
1150
|
+
// Preallocate typed arrays
|
|
1151
|
+
const positions = new Float32Array(totalPoints * 3);
|
|
1152
|
+
const colors = new Float32Array(totalPoints * 3);
|
|
1153
|
+
|
|
1154
|
+
let arrayIdx = 0;
|
|
1155
|
+
for (let pathIdx = 0; pathIdx < paths.length; pathIdx++) {
|
|
1156
|
+
const path = paths[pathIdx];
|
|
1157
|
+
const numPoints = path.length / 3;
|
|
1158
|
+
|
|
1159
|
+
// Use different colors for each path
|
|
1160
|
+
const color = pathIdx === 0 ? [1, 0.4, 0] : [0, 0.8, 1]; // Orange for horizontal, cyan for vertical
|
|
1161
|
+
|
|
1162
|
+
for (let i = 0; i < numPoints; i++) {
|
|
1163
|
+
positions[arrayIdx] = path[i * 3]; // X
|
|
1164
|
+
positions[arrayIdx + 1] = path[i * 3 + 1]; // Y
|
|
1165
|
+
positions[arrayIdx + 2] = path[i * 3 + 2]; // Z
|
|
1166
|
+
|
|
1167
|
+
colors[arrayIdx] = color[0];
|
|
1168
|
+
colors[arrayIdx + 1] = color[1];
|
|
1169
|
+
colors[arrayIdx + 2] = color[2];
|
|
1170
|
+
|
|
1171
|
+
arrayIdx += 3;
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// Create geometry
|
|
1176
|
+
const geometry = new THREE.BufferGeometry();
|
|
1177
|
+
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
|
1178
|
+
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
|
|
1179
|
+
|
|
1180
|
+
const material = new THREE.PointsMaterial({
|
|
1181
|
+
size: resolution * 1.5,
|
|
1182
|
+
vertexColors: true
|
|
1183
|
+
});
|
|
1184
|
+
|
|
1185
|
+
toolpathPoints = new THREE.Points(geometry, material);
|
|
1186
|
+
rotatedGroup.add(toolpathPoints);
|
|
1187
|
+
|
|
1188
|
+
return; // Exit early for tracing mode
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1060
1191
|
if (mode === 'planar') {
|
|
1061
1192
|
// Planar toolpaths
|
|
1062
1193
|
const { pathData, numScanlines, pointsPerLine } = toolpathData;
|
|
@@ -1324,15 +1455,35 @@ function updateButtonStates() {
|
|
|
1324
1455
|
// ============================================================================
|
|
1325
1456
|
|
|
1326
1457
|
function updateModeUI() {
|
|
1327
|
-
// Show/hide
|
|
1458
|
+
// Show/hide mode-specific controls
|
|
1328
1459
|
const wrappedContainer = document.getElementById('wrapped-container').classList;
|
|
1329
1460
|
const angleStepContainer = document.getElementById('angle-step-container').classList;
|
|
1461
|
+
const traceStepContainer = document.getElementById('trace-step-container').classList;
|
|
1462
|
+
const xStepContainer = document.getElementById('x-step-container').classList;
|
|
1463
|
+
const yStepContainer = document.getElementById('y-step-container').classList;
|
|
1464
|
+
const radialV3Container = document.getElementById('radial-v3-container').classList;
|
|
1465
|
+
|
|
1330
1466
|
if (mode === 'radial') {
|
|
1331
1467
|
wrappedContainer.remove('hide');
|
|
1332
1468
|
angleStepContainer.remove('hide');
|
|
1469
|
+
traceStepContainer.add('hide');
|
|
1470
|
+
xStepContainer.remove('hide');
|
|
1471
|
+
yStepContainer.remove('hide');
|
|
1472
|
+
radialV3Container.remove('hide');
|
|
1473
|
+
} else if (mode === 'tracing') {
|
|
1474
|
+
wrappedContainer.add('hide');
|
|
1475
|
+
angleStepContainer.add('hide');
|
|
1476
|
+
traceStepContainer.remove('hide');
|
|
1477
|
+
xStepContainer.add('hide');
|
|
1478
|
+
yStepContainer.add('hide');
|
|
1333
1479
|
} else {
|
|
1480
|
+
// planar
|
|
1334
1481
|
wrappedContainer.add('hide');
|
|
1335
1482
|
angleStepContainer.add('hide');
|
|
1483
|
+
traceStepContainer.add('hide');
|
|
1484
|
+
radialV3Container.add('hide');
|
|
1485
|
+
xStepContainer.remove('hide');
|
|
1486
|
+
yStepContainer.remove('hide');
|
|
1336
1487
|
}
|
|
1337
1488
|
}
|
|
1338
1489
|
|
|
@@ -1446,12 +1597,36 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|
|
1446
1597
|
modelRasterData = null; // Need to re-rasterize with new angle step
|
|
1447
1598
|
toolRasterData = null;
|
|
1448
1599
|
toolpathData = null;
|
|
1600
|
+
initRasterPath(); // Reinit with new angle step
|
|
1449
1601
|
}
|
|
1450
1602
|
saveParameters();
|
|
1451
1603
|
updateInfo(`Angle Step changed to ${angleStep}°`);
|
|
1452
1604
|
updateButtonStates();
|
|
1453
1605
|
});
|
|
1454
1606
|
|
|
1607
|
+
document.getElementById('trace-step').addEventListener('change', (e) => {
|
|
1608
|
+
traceStep = parseFloat(e.target.value);
|
|
1609
|
+
if (mode === 'tracing') {
|
|
1610
|
+
toolpathData = null; // Need to regenerate toolpath
|
|
1611
|
+
}
|
|
1612
|
+
saveParameters();
|
|
1613
|
+
updateInfo(`Trace Step changed to ${traceStep}mm`);
|
|
1614
|
+
updateButtonStates();
|
|
1615
|
+
});
|
|
1616
|
+
|
|
1617
|
+
document.getElementById('radial-v3').addEventListener('change', (e) => {
|
|
1618
|
+
if (mode === 'radial') {
|
|
1619
|
+
modelRasterData = null; // Need to re-rasterize with different algorithm
|
|
1620
|
+
toolRasterData = null;
|
|
1621
|
+
toolpathData = null;
|
|
1622
|
+
initRasterPath(); // Reinit with V3 setting
|
|
1623
|
+
}
|
|
1624
|
+
saveParameters();
|
|
1625
|
+
const v3Status = e.target.checked ? 'V3 (experimental)' : 'V2 (default)';
|
|
1626
|
+
updateInfo(`Radial algorithm: ${v3Status}`);
|
|
1627
|
+
updateButtonStates();
|
|
1628
|
+
});
|
|
1629
|
+
|
|
1455
1630
|
// Tool size change
|
|
1456
1631
|
document.getElementById('tool-size').addEventListener('change', async (e) => {
|
|
1457
1632
|
toolSize = parseFloat(e.target.value);
|
package/build/index.html
CHANGED
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
<div class="mode-toggle">
|
|
54
54
|
<label><input type="radio" name="mode" value="planar" checked> Planar</label>
|
|
55
55
|
<label><input type="radio" name="mode" value="radial"> Radial</label>
|
|
56
|
+
<label><input type="radio" name="mode" value="tracing"> Tracing</label>
|
|
56
57
|
</div>
|
|
57
58
|
</div>
|
|
58
59
|
|
|
@@ -79,15 +80,21 @@
|
|
|
79
80
|
<label>
|
|
80
81
|
Z Floor: <input type="number" id="z-floor" value="-100" step="10" style="width: 70px;">
|
|
81
82
|
</label>
|
|
82
|
-
<label>
|
|
83
|
+
<label id="x-step-container">
|
|
83
84
|
X Step: <input type="number" id="x-step" value="5" min="1" max="50" style="width: 60px;">
|
|
84
85
|
</label>
|
|
85
|
-
<label>
|
|
86
|
+
<label id="y-step-container">
|
|
86
87
|
Y Step: <input type="number" id="y-step" value="5" min="1" max="50" style="width: 60px;">
|
|
87
88
|
</label>
|
|
88
89
|
<label id="angle-step-container" class="hide">
|
|
89
90
|
Angle Step (deg): <input type="number" id="angle-step" value="1" min="0.1" max="10" step="0.1" style="width: 60px;">
|
|
90
91
|
</label>
|
|
92
|
+
<label id="trace-step-container" class="hide">
|
|
93
|
+
Trace Step (mm): <input type="number" id="trace-step" value="0.5" min="0.1" max="5" step="0.1" style="width: 60px;">
|
|
94
|
+
</label>
|
|
95
|
+
<label id="radial-v3-container" class="hide">
|
|
96
|
+
<input type="checkbox" id="radial-v3"> Use V3 (experimental)
|
|
97
|
+
</label>
|
|
91
98
|
</div>
|
|
92
99
|
|
|
93
100
|
<div class="section">
|
package/build/raster-path.js
CHANGED
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
/**
|
|
46
46
|
* Configuration options for RasterPath
|
|
47
47
|
* @typedef {Object} RasterPathConfig
|
|
48
|
-
* @property {'planar'|'radial'} mode - Rasterization mode (default: 'planar')
|
|
48
|
+
* @property {'planar'|'radial'|'tracing'} mode - Rasterization mode (default: 'planar')
|
|
49
49
|
* @property {boolean} autoTiling - Automatically tile large datasets (default: true)
|
|
50
50
|
* @property {number} gpuMemorySafetyMargin - Safety margin as percentage (default: 0.8 = 80%)
|
|
51
51
|
* @property {number} maxGPUMemoryMB - Maximum GPU memory per tile (default: 256MB)
|
|
@@ -80,8 +80,8 @@ export class RasterPath {
|
|
|
80
80
|
|
|
81
81
|
// Validate mode
|
|
82
82
|
const mode = config.mode || 'planar';
|
|
83
|
-
if (mode !== 'planar' && mode !== 'radial') {
|
|
84
|
-
throw new Error(`Invalid mode: ${mode}. Must be 'planar' or '
|
|
83
|
+
if (mode !== 'planar' && mode !== 'radial' && mode !== 'tracing') {
|
|
84
|
+
throw new Error(`Invalid mode: ${mode}. Must be 'planar', 'radial', or 'tracing'`);
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
// Validate rotationStep for radial mode
|
|
@@ -111,6 +111,7 @@ export class RasterPath {
|
|
|
111
111
|
gpuMemorySafetyMargin: config.gpuMemorySafetyMargin ?? 0.8,
|
|
112
112
|
autoTiling: config.autoTiling ?? true,
|
|
113
113
|
batchDivisor: config.batchDivisor ?? 1, // For testing batching overhead
|
|
114
|
+
radialV3: config.radialV3 ?? false, // Use radial V3 pipeline (rotate-filter-toolpath)
|
|
114
115
|
debug: config.debug,
|
|
115
116
|
quiet: config.quiet
|
|
116
117
|
};
|
|
@@ -221,8 +222,8 @@ export class RasterPath {
|
|
|
221
222
|
throw new Error('RasterPath not initialized. Call init() first.');
|
|
222
223
|
}
|
|
223
224
|
|
|
224
|
-
if (this.mode === 'planar') {
|
|
225
|
-
// Planar: rasterize and return
|
|
225
|
+
if (this.mode === 'planar' || this.mode === 'tracing') {
|
|
226
|
+
// Planar/Tracing: rasterize and return (tracing reuses planar terrain rasterization)
|
|
226
227
|
const terrainData = await this.#rasterizePlanar({ triangles, zFloor, boundsOverride, isForTool: false, onProgress });
|
|
227
228
|
this.terrainData = terrainData;
|
|
228
229
|
return terrainData;
|
|
@@ -269,7 +270,7 @@ export class RasterPath {
|
|
|
269
270
|
* @param {function} params.onProgress - Optional progress callback (progress: number, info?: string) => void
|
|
270
271
|
* @returns {Promise<object>} Planar: {pathData, width, height} | Radial: {strips[], numStrips, totalPoints}
|
|
271
272
|
*/
|
|
272
|
-
async generateToolpaths({ xStep, yStep, zFloor, onProgress }) {
|
|
273
|
+
async generateToolpaths({ xStep, yStep, zFloor, onProgress, paths, step }) {
|
|
273
274
|
if (!this.isInitialized) {
|
|
274
275
|
throw new Error('RasterPath not initialized. Call init() first.');
|
|
275
276
|
}
|
|
@@ -278,7 +279,7 @@ export class RasterPath {
|
|
|
278
279
|
throw new Error('Tool not loaded. Call loadTool() first.');
|
|
279
280
|
}
|
|
280
281
|
|
|
281
|
-
debug.log('gen.paths', { xStep, yStep, zFloor });
|
|
282
|
+
debug.log('gen.paths', { xStep, yStep, zFloor, paths: paths?.length, step });
|
|
282
283
|
|
|
283
284
|
if (this.mode === 'planar') {
|
|
284
285
|
if (!this.terrainData) {
|
|
@@ -292,7 +293,7 @@ export class RasterPath {
|
|
|
292
293
|
zFloor,
|
|
293
294
|
onProgress
|
|
294
295
|
});
|
|
295
|
-
} else {
|
|
296
|
+
} else if (this.mode === 'radial') {
|
|
296
297
|
// Radial mode: use stored triangles
|
|
297
298
|
if (!this.terrainTriangles) {
|
|
298
299
|
throw new Error('Terrain not loaded. Call loadTerrain() first.');
|
|
@@ -306,14 +307,83 @@ export class RasterPath {
|
|
|
306
307
|
zFloor: zFloor ?? this.terrainZFloor,
|
|
307
308
|
onProgress
|
|
308
309
|
});
|
|
310
|
+
} else if (this.mode === 'tracing') {
|
|
311
|
+
// Tracing mode: follow input paths
|
|
312
|
+
if (!this.terrainData) {
|
|
313
|
+
throw new Error('Terrain not loaded. Call loadTerrain() first.');
|
|
314
|
+
}
|
|
315
|
+
if (!paths || paths.length === 0) {
|
|
316
|
+
throw new Error('Tracing mode requires paths parameter (array of Float32Array XY coordinates)');
|
|
317
|
+
}
|
|
318
|
+
if (!step || step <= 0) {
|
|
319
|
+
throw new Error('Tracing mode requires step parameter (sampling resolution in world units)');
|
|
320
|
+
}
|
|
321
|
+
return this.#generateToolpathsTracing({
|
|
322
|
+
paths,
|
|
323
|
+
step,
|
|
324
|
+
zFloor,
|
|
325
|
+
onProgress
|
|
326
|
+
});
|
|
309
327
|
}
|
|
310
328
|
}
|
|
311
329
|
|
|
330
|
+
/**
|
|
331
|
+
* Create reusable GPU buffers for tracing mode (optimization for iterative tracing)
|
|
332
|
+
* Call this after loadTerrain() and loadTool() to cache buffers across multiple trace calls
|
|
333
|
+
* @returns {Promise<void>}
|
|
334
|
+
*/
|
|
335
|
+
async createTracingBuffers() {
|
|
336
|
+
if (this.mode !== 'tracing') {
|
|
337
|
+
throw new Error('createTracingBuffers() only available in tracing mode');
|
|
338
|
+
}
|
|
339
|
+
if (!this.terrainData || !this.toolData) {
|
|
340
|
+
throw new Error('Must call loadTerrain() and loadTool() before createTracingBuffers()');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return new Promise((resolve, reject) => {
|
|
344
|
+
const handler = () => resolve();
|
|
345
|
+
this.#sendMessage(
|
|
346
|
+
'create-tracing-buffers',
|
|
347
|
+
{
|
|
348
|
+
terrainPositions: this.terrainData.positions,
|
|
349
|
+
toolPositions: this.toolData.positions
|
|
350
|
+
},
|
|
351
|
+
'tracing-buffers-created',
|
|
352
|
+
handler
|
|
353
|
+
);
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Destroy reusable tracing buffers
|
|
359
|
+
* @returns {Promise<void>}
|
|
360
|
+
*/
|
|
361
|
+
async destroyTracingBuffers() {
|
|
362
|
+
if (this.mode !== 'tracing') {
|
|
363
|
+
return; // No-op for non-tracing modes
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return new Promise((resolve, reject) => {
|
|
367
|
+
const handler = () => resolve();
|
|
368
|
+
this.#sendMessage(
|
|
369
|
+
'destroy-tracing-buffers',
|
|
370
|
+
{},
|
|
371
|
+
'tracing-buffers-destroyed',
|
|
372
|
+
handler
|
|
373
|
+
);
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
312
377
|
/**
|
|
313
378
|
* Terminate worker and cleanup resources
|
|
314
379
|
*/
|
|
315
|
-
terminate() {
|
|
380
|
+
async terminate() {
|
|
316
381
|
if (this.worker) {
|
|
382
|
+
// Cleanup tracing buffers if in tracing mode
|
|
383
|
+
if (this.mode === 'tracing') {
|
|
384
|
+
await this.destroyTracingBuffers();
|
|
385
|
+
}
|
|
386
|
+
|
|
317
387
|
this.worker.terminate();
|
|
318
388
|
this.worker = null;
|
|
319
389
|
this.isInitialized = false;
|
|
@@ -388,6 +458,46 @@ export class RasterPath {
|
|
|
388
458
|
});
|
|
389
459
|
}
|
|
390
460
|
|
|
461
|
+
async #generateToolpathsTracing({ paths, step, zFloor, onProgress }) {
|
|
462
|
+
return new Promise((resolve, reject) => {
|
|
463
|
+
// Set up progress handler if callback provided
|
|
464
|
+
if (onProgress) {
|
|
465
|
+
const progressHandler = (data) => {
|
|
466
|
+
onProgress(data.percent, { current: data.current, total: data.total, pathIndex: data.pathIndex });
|
|
467
|
+
};
|
|
468
|
+
this.messageHandlers.set('tracing-progress', progressHandler);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const handler = (data) => {
|
|
472
|
+
// Clean up progress handler
|
|
473
|
+
if (onProgress) {
|
|
474
|
+
this.messageHandlers.delete('tracing-progress');
|
|
475
|
+
}
|
|
476
|
+
resolve(data);
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
this.#sendMessage(
|
|
480
|
+
'tracing-generate-toolpaths',
|
|
481
|
+
{
|
|
482
|
+
paths,
|
|
483
|
+
terrainPositions: this.terrainData.positions,
|
|
484
|
+
terrainData: {
|
|
485
|
+
width: this.terrainData.gridWidth,
|
|
486
|
+
height: this.terrainData.gridHeight,
|
|
487
|
+
bounds: this.terrainData.bounds
|
|
488
|
+
},
|
|
489
|
+
toolPositions: this.toolData.positions,
|
|
490
|
+
step,
|
|
491
|
+
gridStep: this.resolution,
|
|
492
|
+
terrainBounds: this.terrainData.bounds,
|
|
493
|
+
zFloor: zFloor ?? 0
|
|
494
|
+
},
|
|
495
|
+
'tracing-toolpaths-complete',
|
|
496
|
+
handler
|
|
497
|
+
);
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
|
|
391
501
|
async #generateToolpathsRadial({ triangles, bounds, toolData, xStep, yStep, zFloor, onProgress }) {
|
|
392
502
|
const maxRadius = this.#calculateMaxRadius(triangles);
|
|
393
503
|
|
|
@@ -422,9 +532,13 @@ export class RasterPath {
|
|
|
422
532
|
resolve(data);
|
|
423
533
|
};
|
|
424
534
|
|
|
425
|
-
// Send entire pipeline to worker
|
|
535
|
+
// Send entire pipeline to worker (use V3 if configured)
|
|
536
|
+
const messageType = this.config.radialV3
|
|
537
|
+
? 'radial-generate-toolpaths-v3'
|
|
538
|
+
: 'radial-generate-toolpaths';
|
|
539
|
+
|
|
426
540
|
this.#sendMessage(
|
|
427
|
-
|
|
541
|
+
messageType,
|
|
428
542
|
{
|
|
429
543
|
triangles: triangles,
|
|
430
544
|
bucketData,
|