@gridspace/raster-path 1.0.2 → 1.0.4
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/README.md +3 -5
- package/build/app.js +363 -39
- package/build/index.html +39 -1
- package/build/raster-path.js +13 -13
- package/build/style.css +65 -0
- package/build/webgpu-worker.js +475 -686
- package/package.json +7 -4
- package/scripts/build-shaders.js +1 -1
- package/src/etc/serve.json +12 -0
- package/src/index.js +13 -13
- package/src/shaders/{radial-raster-v2.wgsl → radial-raster.wgsl} +8 -2
- package/src/test/batch-divisor-benchmark.cjs +286 -0
- package/src/test/lathe-cylinder-2-debug.cjs +334 -0
- package/src/test/lathe-cylinder-2-test.cjs +157 -0
- package/src/test/lathe-cylinder-test.cjs +198 -0
- package/src/test/planar-test.cjs +253 -0
- package/src/test/planar-tiling-test.cjs +230 -0
- package/src/test/radial-test.cjs +269 -0
- package/src/test/work-estimation-profile.cjs +406 -0
- package/src/test/workload-calculator-demo.cjs +113 -0
- package/src/test/workload-calibration.cjs +310 -0
- package/src/web/app.js +363 -39
- package/src/web/index.html +130 -0
- package/src/web/style.css +223 -0
- package/src/web/webgpu-worker.js +470 -687
- package/src/workload-calculator.js +318 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workload Calculator for Radial Toolpath Generation
|
|
3
|
+
*
|
|
4
|
+
* Estimates GPU workload and optimal batch sizing based on:
|
|
5
|
+
* - Model geometry (triangle count, bounds)
|
|
6
|
+
* - Rasterization parameters (resolution, angular step)
|
|
7
|
+
* - Tool characteristics (diameter)
|
|
8
|
+
*
|
|
9
|
+
* Based on empirical testing:
|
|
10
|
+
* - Tool diameter scales LINEARLY (not quadratically)
|
|
11
|
+
* - Resolution scales INVERSELY SQUARED (finer = more pixels)
|
|
12
|
+
* - Angular step scales INVERSELY LINEAR (finer = more strips)
|
|
13
|
+
* - Triangle count has complex relationship with geometry
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export class WorkloadCalculator {
|
|
17
|
+
constructor() {
|
|
18
|
+
// Calibration constants (to be tuned from test data)
|
|
19
|
+
this.BASELINE_TOOL_DIAMETER = 2.5; // mm (most common size)
|
|
20
|
+
this.BASELINE_RESOLUTION = 0.05; // mm (reference resolution)
|
|
21
|
+
this.BASELINE_ANGULAR_STEP = 1.0; // degrees
|
|
22
|
+
|
|
23
|
+
// Minimum batch parameters
|
|
24
|
+
this.MIN_ANGLES_PER_BATCH = 20; // Absolute minimum
|
|
25
|
+
this.TARGET_MIN_BATCH_TIME_MS = 400; // Target minimum batch duration
|
|
26
|
+
|
|
27
|
+
// GPU overhead constants
|
|
28
|
+
this.GPU_DISPATCH_OVERHEAD_MS = 25; // Per-batch overhead
|
|
29
|
+
|
|
30
|
+
// Watchdog timeout thresholds
|
|
31
|
+
this.WATCHDOG_WARNING_MS = 2000; // Warn if single operation > 2s
|
|
32
|
+
this.WATCHDOG_CRITICAL_MS = 5000; // Critical if single operation > 5s
|
|
33
|
+
this.WATCHDOG_MAX_SAFE_MS = 1500; // Target to stay under watchdog
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Calculate workload for radial toolpath generation
|
|
38
|
+
*
|
|
39
|
+
* @param {Object} params
|
|
40
|
+
* @param {number} params.triangleCount - Number of terrain triangles
|
|
41
|
+
* @param {Object} params.bounds - Model bounds {minX, maxX, minY, maxY, minZ, maxZ}
|
|
42
|
+
* @param {number} params.resolution - Grid resolution in mm
|
|
43
|
+
* @param {number} params.rotationStep - Angular step in degrees
|
|
44
|
+
* @param {number} params.toolDiameter - Tool diameter in mm
|
|
45
|
+
* @returns {Object} Workload estimate with recommended batch size
|
|
46
|
+
*/
|
|
47
|
+
calculateRadialWorkload(params) {
|
|
48
|
+
const {
|
|
49
|
+
triangleCount,
|
|
50
|
+
bounds,
|
|
51
|
+
resolution,
|
|
52
|
+
rotationStep,
|
|
53
|
+
toolDiameter
|
|
54
|
+
} = params;
|
|
55
|
+
|
|
56
|
+
// Calculate radial geometry
|
|
57
|
+
const radialRadius = Math.sqrt(
|
|
58
|
+
Math.max(Math.abs(bounds.minY), Math.abs(bounds.maxY)) ** 2 +
|
|
59
|
+
Math.max(Math.abs(bounds.minZ), Math.abs(bounds.maxZ)) ** 2
|
|
60
|
+
);
|
|
61
|
+
const radialHeight = bounds.maxX - bounds.minX;
|
|
62
|
+
const radialVolume = Math.PI * radialRadius * radialRadius * radialHeight;
|
|
63
|
+
|
|
64
|
+
// Calculate grid dimensions
|
|
65
|
+
const gridWidth = Math.ceil((2 * radialRadius) / resolution);
|
|
66
|
+
const gridHeight = Math.ceil(radialHeight / resolution);
|
|
67
|
+
const pixelsPerAngle = gridWidth * gridHeight;
|
|
68
|
+
|
|
69
|
+
// Calculate number of strips
|
|
70
|
+
const numAngles = Math.ceil(360 / rotationStep);
|
|
71
|
+
|
|
72
|
+
// Calculate triangle density
|
|
73
|
+
const triangleDensity = triangleCount / radialVolume;
|
|
74
|
+
|
|
75
|
+
// Calculate work factors (normalized to baseline)
|
|
76
|
+
const resolutionFactor = (this.BASELINE_RESOLUTION / resolution) ** 2; // Inverse square
|
|
77
|
+
const angularFactor = this.BASELINE_ANGULAR_STEP / rotationStep; // Inverse linear
|
|
78
|
+
const toolFactor = toolDiameter / this.BASELINE_TOOL_DIAMETER; // Linear
|
|
79
|
+
|
|
80
|
+
// Formula 6 (User's proposal + tool diameter):
|
|
81
|
+
// work ∝ triangleDensity × (1/resolution²) × (1/angularStep) × toolDiameter
|
|
82
|
+
const workScore = triangleDensity * resolutionFactor * angularFactor * toolFactor;
|
|
83
|
+
|
|
84
|
+
// Formula 2 (Simpler):
|
|
85
|
+
// work ∝ triangleCount × pixelsPerAngle × toolDiameter
|
|
86
|
+
const workPerAngle = triangleCount * pixelsPerAngle * toolFactor;
|
|
87
|
+
|
|
88
|
+
// Estimate time per angle (calibrated from empirical data)
|
|
89
|
+
// Base calibration: ~3-5ms per angle for baseline parameters
|
|
90
|
+
const baseTimePerAngle = 4.0; // ms (to be tuned from test data)
|
|
91
|
+
const estimatedTimePerAngle = baseTimePerAngle * workScore / 1000;
|
|
92
|
+
|
|
93
|
+
// Calculate optimal batch size based on target batch duration
|
|
94
|
+
const optimalAnglesPerBatch = Math.ceil(
|
|
95
|
+
this.TARGET_MIN_BATCH_TIME_MS / estimatedTimePerAngle
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
// Apply constraints
|
|
99
|
+
const recommendedAnglesPerBatch = Math.max(
|
|
100
|
+
this.MIN_ANGLES_PER_BATCH,
|
|
101
|
+
Math.min(optimalAnglesPerBatch, numAngles)
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const numBatches = Math.ceil(numAngles / recommendedAnglesPerBatch);
|
|
105
|
+
|
|
106
|
+
// Estimate total time
|
|
107
|
+
const rasterTimePerAngle = estimatedTimePerAngle * 0.3; // ~30% raster
|
|
108
|
+
const toolpathTimePerAngle = estimatedTimePerAngle * 0.7; // ~70% toolpath
|
|
109
|
+
|
|
110
|
+
const totalRasterTime = rasterTimePerAngle * numAngles;
|
|
111
|
+
const totalToolpathTime = toolpathTimePerAngle * numAngles;
|
|
112
|
+
const totalOverhead = this.GPU_DISPATCH_OVERHEAD_MS * numBatches;
|
|
113
|
+
const estimatedTotalTime = totalRasterTime + totalToolpathTime + totalOverhead;
|
|
114
|
+
|
|
115
|
+
// Estimate time per batch (for watchdog detection)
|
|
116
|
+
const estimatedBatchTime = (estimatedTimePerAngle * recommendedAnglesPerBatch) + this.GPU_DISPATCH_OVERHEAD_MS;
|
|
117
|
+
|
|
118
|
+
// Watchdog timeout risk assessment
|
|
119
|
+
let watchdogRisk = 'safe';
|
|
120
|
+
let watchdogMessage = 'Batch size is within safe watchdog limits';
|
|
121
|
+
let watchdogSuggestion = null;
|
|
122
|
+
|
|
123
|
+
if (estimatedBatchTime >= this.WATCHDOG_CRITICAL_MS) {
|
|
124
|
+
watchdogRisk = 'critical';
|
|
125
|
+
watchdogMessage = `CRITICAL: Estimated batch time (${estimatedBatchTime.toFixed(0)}ms) exceeds watchdog kill threshold (${this.WATCHDOG_CRITICAL_MS}ms)`;
|
|
126
|
+
// Calculate safer batch size
|
|
127
|
+
const safeAnglesPerBatch = Math.floor(this.WATCHDOG_MAX_SAFE_MS / estimatedTimePerAngle);
|
|
128
|
+
watchdogSuggestion = {
|
|
129
|
+
anglesPerBatch: Math.max(this.MIN_ANGLES_PER_BATCH, safeAnglesPerBatch),
|
|
130
|
+
message: `Reduce batch size to ${Math.max(this.MIN_ANGLES_PER_BATCH, safeAnglesPerBatch)} angles or adjust parameters`
|
|
131
|
+
};
|
|
132
|
+
} else if (estimatedBatchTime >= this.WATCHDOG_WARNING_MS) {
|
|
133
|
+
watchdogRisk = 'warning';
|
|
134
|
+
watchdogMessage = `WARNING: Estimated batch time (${estimatedBatchTime.toFixed(0)}ms) approaching watchdog threshold (${this.WATCHDOG_WARNING_MS}ms)`;
|
|
135
|
+
// Calculate safer batch size
|
|
136
|
+
const safeAnglesPerBatch = Math.floor(this.WATCHDOG_MAX_SAFE_MS / estimatedTimePerAngle);
|
|
137
|
+
watchdogSuggestion = {
|
|
138
|
+
anglesPerBatch: Math.max(this.MIN_ANGLES_PER_BATCH, safeAnglesPerBatch),
|
|
139
|
+
message: `Consider reducing batch size to ${Math.max(this.MIN_ANGLES_PER_BATCH, safeAnglesPerBatch)} angles`
|
|
140
|
+
};
|
|
141
|
+
} else if (estimatedBatchTime >= this.WATCHDOG_MAX_SAFE_MS) {
|
|
142
|
+
watchdogRisk = 'caution';
|
|
143
|
+
watchdogMessage = `CAUTION: Estimated batch time (${estimatedBatchTime.toFixed(0)}ms) above safe target (${this.WATCHDOG_MAX_SAFE_MS}ms)`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
// Workload metrics
|
|
148
|
+
workScore,
|
|
149
|
+
workPerAngle,
|
|
150
|
+
triangleDensity,
|
|
151
|
+
pixelsPerAngle,
|
|
152
|
+
|
|
153
|
+
// Grid info
|
|
154
|
+
gridWidth,
|
|
155
|
+
gridHeight,
|
|
156
|
+
numAngles,
|
|
157
|
+
|
|
158
|
+
// Timing estimates
|
|
159
|
+
estimatedTimePerAngle,
|
|
160
|
+
estimatedBatchTime,
|
|
161
|
+
estimatedTotalTime,
|
|
162
|
+
totalRasterTime,
|
|
163
|
+
totalToolpathTime,
|
|
164
|
+
totalOverhead,
|
|
165
|
+
|
|
166
|
+
// Batch recommendations
|
|
167
|
+
recommendedAnglesPerBatch,
|
|
168
|
+
numBatches,
|
|
169
|
+
overheadPercent: (totalOverhead / estimatedTotalTime) * 100,
|
|
170
|
+
|
|
171
|
+
// Watchdog risk assessment
|
|
172
|
+
watchdog: {
|
|
173
|
+
risk: watchdogRisk,
|
|
174
|
+
message: watchdogMessage,
|
|
175
|
+
estimatedBatchTime,
|
|
176
|
+
suggestion: watchdogSuggestion
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
// Work factors
|
|
180
|
+
factors: {
|
|
181
|
+
resolution: resolutionFactor,
|
|
182
|
+
angular: angularFactor,
|
|
183
|
+
tool: toolFactor,
|
|
184
|
+
density: triangleDensity
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Calculate memory requirements for radial mode
|
|
191
|
+
*
|
|
192
|
+
* @param {Object} params - Same as calculateRadialWorkload
|
|
193
|
+
* @returns {Object} Memory estimate in MB
|
|
194
|
+
*/
|
|
195
|
+
calculateRadialMemory(params) {
|
|
196
|
+
const { bounds, resolution, rotationStep, toolDiameter } = params;
|
|
197
|
+
|
|
198
|
+
const radialRadius = Math.sqrt(
|
|
199
|
+
Math.max(Math.abs(bounds.minY), Math.abs(bounds.maxY)) ** 2 +
|
|
200
|
+
Math.max(Math.abs(bounds.minZ), Math.abs(bounds.maxZ)) ** 2
|
|
201
|
+
);
|
|
202
|
+
const radialHeight = bounds.maxX - bounds.minX;
|
|
203
|
+
|
|
204
|
+
const gridWidth = Math.ceil((2 * radialRadius) / resolution);
|
|
205
|
+
const gridHeight = Math.ceil(radialHeight / resolution);
|
|
206
|
+
const numAngles = Math.ceil(360 / rotationStep);
|
|
207
|
+
|
|
208
|
+
// Each pixel is 4 bytes (float32)
|
|
209
|
+
const bytesPerAngle = gridWidth * gridHeight * 4;
|
|
210
|
+
const totalMemoryBytes = bytesPerAngle * numAngles;
|
|
211
|
+
const totalMemoryMB = totalMemoryBytes / (1024 * 1024);
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
gridWidth,
|
|
215
|
+
gridHeight,
|
|
216
|
+
numAngles,
|
|
217
|
+
bytesPerAngle,
|
|
218
|
+
totalMemoryBytes,
|
|
219
|
+
totalMemoryMB,
|
|
220
|
+
exceedsLimit: totalMemoryMB > 256 // Typical GPU limit
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Suggest optimal parameters for memory-constrained scenarios
|
|
226
|
+
*
|
|
227
|
+
* @param {Object} params - Same as calculateRadialWorkload
|
|
228
|
+
* @param {number} maxMemoryMB - Maximum allowed memory in MB
|
|
229
|
+
* @returns {Object} Suggested parameter adjustments
|
|
230
|
+
*/
|
|
231
|
+
suggestOptimalParameters(params, maxMemoryMB = 256) {
|
|
232
|
+
const memory = this.calculateRadialMemory(params);
|
|
233
|
+
|
|
234
|
+
if (!memory.exceedsLimit && memory.totalMemoryMB < maxMemoryMB) {
|
|
235
|
+
return {
|
|
236
|
+
needsAdjustment: false,
|
|
237
|
+
currentMemory: memory.totalMemoryMB,
|
|
238
|
+
message: 'Current parameters are within memory limits'
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Calculate required scale factor
|
|
243
|
+
const scaleFactor = Math.sqrt(maxMemoryMB / memory.totalMemoryMB);
|
|
244
|
+
|
|
245
|
+
// Suggest coarser resolution
|
|
246
|
+
const suggestedResolution = params.resolution / scaleFactor;
|
|
247
|
+
const roundedResolution = Math.ceil(suggestedResolution * 1000) / 1000;
|
|
248
|
+
|
|
249
|
+
// Suggest coarser angular step
|
|
250
|
+
const suggestedAngularStep = params.rotationStep / scaleFactor;
|
|
251
|
+
const roundedAngularStep = Math.ceil(suggestedAngularStep * 10) / 10;
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
needsAdjustment: true,
|
|
255
|
+
currentMemory: memory.totalMemoryMB,
|
|
256
|
+
targetMemory: maxMemoryMB,
|
|
257
|
+
suggestions: {
|
|
258
|
+
resolution: roundedResolution,
|
|
259
|
+
angularStep: roundedAngularStep
|
|
260
|
+
},
|
|
261
|
+
message: `Memory limit exceeded. Suggested: resolution=${roundedResolution}mm or angularStep=${roundedAngularStep}°`
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Pretty-print workload analysis
|
|
267
|
+
*
|
|
268
|
+
* @param {Object} params - Workload parameters
|
|
269
|
+
*/
|
|
270
|
+
printAnalysis(params) {
|
|
271
|
+
const workload = this.calculateRadialWorkload(params);
|
|
272
|
+
const memory = this.calculateRadialMemory(params);
|
|
273
|
+
|
|
274
|
+
console.log('=== Workload Analysis ===');
|
|
275
|
+
console.log(`Model: ${params.triangleCount.toLocaleString()} triangles`);
|
|
276
|
+
console.log(`Resolution: ${params.resolution}mm`);
|
|
277
|
+
console.log(`Angular step: ${params.rotationStep}°`);
|
|
278
|
+
console.log(`Tool diameter: ${params.toolDiameter}mm`);
|
|
279
|
+
console.log('');
|
|
280
|
+
console.log('Grid:');
|
|
281
|
+
console.log(` ${workload.gridWidth} × ${workload.gridHeight} pixels`);
|
|
282
|
+
console.log(` ${workload.numAngles} strips`);
|
|
283
|
+
console.log(` ${workload.pixelsPerAngle.toLocaleString()} pixels per strip`);
|
|
284
|
+
console.log('');
|
|
285
|
+
console.log('Work factors (vs baseline):');
|
|
286
|
+
console.log(` Resolution: ${workload.factors.resolution.toFixed(2)}x`);
|
|
287
|
+
console.log(` Angular: ${workload.factors.angular.toFixed(2)}x`);
|
|
288
|
+
console.log(` Tool: ${workload.factors.tool.toFixed(2)}x`);
|
|
289
|
+
console.log(` Density: ${workload.factors.density.toFixed(2)} tri/mm³`);
|
|
290
|
+
console.log('');
|
|
291
|
+
console.log('Estimated timing:');
|
|
292
|
+
console.log(` Per angle: ${workload.estimatedTimePerAngle.toFixed(2)}ms`);
|
|
293
|
+
console.log(` Total: ${workload.estimatedTotalTime.toFixed(0)}ms`);
|
|
294
|
+
console.log('');
|
|
295
|
+
console.log('Batching:');
|
|
296
|
+
console.log(` Recommended: ${workload.recommendedAnglesPerBatch} angles/batch`);
|
|
297
|
+
console.log(` Batches: ${workload.numBatches}`);
|
|
298
|
+
console.log(` Overhead: ${workload.overheadPercent.toFixed(1)}%`);
|
|
299
|
+
console.log(` Est. batch time: ${workload.estimatedBatchTime.toFixed(0)}ms`);
|
|
300
|
+
console.log('');
|
|
301
|
+
console.log('Watchdog Risk:');
|
|
302
|
+
const riskSymbol = {
|
|
303
|
+
'safe': '✓',
|
|
304
|
+
'caution': '⚠️',
|
|
305
|
+
'warning': '⚠️⚠️',
|
|
306
|
+
'critical': '🛑'
|
|
307
|
+
}[workload.watchdog.risk];
|
|
308
|
+
console.log(` ${riskSymbol} ${workload.watchdog.risk.toUpperCase()}`);
|
|
309
|
+
console.log(` ${workload.watchdog.message}`);
|
|
310
|
+
if (workload.watchdog.suggestion) {
|
|
311
|
+
console.log(` Suggestion: ${workload.watchdog.suggestion.message}`);
|
|
312
|
+
}
|
|
313
|
+
console.log('');
|
|
314
|
+
console.log('Memory:');
|
|
315
|
+
console.log(` ${memory.totalMemoryMB.toFixed(1)} MB`);
|
|
316
|
+
console.log(` ${memory.exceedsLimit ? '⚠️ EXCEEDS LIMIT' : '✓ Within limits'}`);
|
|
317
|
+
}
|
|
318
|
+
}
|