Add OPFS G-code staging and browser simulation checks

This commit is contained in:
2026-06-22 10:56:21 +08:00
parent 8321055934
commit 61e2fe8441
19 changed files with 1869 additions and 35 deletions

View File

@@ -38,12 +38,17 @@ export function renderFiveAxisScene(canvas, state) {
exposePreviewDataset(canvas, state, {
pointCount,
executedPointCount,
feedPointCount: preview.feedPath.geometry.getAttribute("position").count,
rapidPointCount: preview.rapidPath.geometry.getAttribute("position").count,
arcPointCount: preview.arcPath.geometry.getAttribute("position").count,
currentSegmentPointCount: preview.currentSegmentPath.geometry.getAttribute("position").count,
sceneObjectCount: countSceneObjects(preview.scene),
toolhead: preview.currentToolhead,
renderer: "webgl",
sceneMode: "program-preview-and-tool-execution",
cameraControls: preview.controls.enabled,
toolExecutionMarker: preview.toolMarker.visible,
pathFitBounds: preview.pathFitBoundsReady,
});
}
@@ -68,8 +73,11 @@ function createScene(canvas) {
const camera = new THREE.PerspectiveCamera(42, 1, 0.1, 100);
const previewPath = createLine(0x808892, 0.56);
const feedPath = createLine(0x4fb3ff, 0.92);
const executedPath = createLine(0x1ffff4, 1);
const rapidPath = createLine(0xffb13b, 0.82);
const arcPath = createLine(0xd7ff62, 0.95);
const currentSegmentPath = createLine(0xff4fd8, 1);
const toolMarker = new THREE.Mesh(
new THREE.SphereGeometry(0.065, 18, 12),
new THREE.MeshBasicMaterial({ color: 0x1ffff4 }),
@@ -78,7 +86,7 @@ function createScene(canvas) {
EMPTY_GEOMETRY.clone(),
new THREE.LineBasicMaterial({ color: 0x1ffff4, transparent: true, opacity: 0.9 }),
);
scene.add(previewPath, rapidPath, executedPath, toolAxis, toolMarker);
scene.add(previewPath, feedPath, rapidPath, arcPath, executedPath, currentSegmentPath, toolAxis, toolMarker);
const controls = createToolpathCameraControls(canvas, camera, () => {
renderer.render(scene, camera);
@@ -90,13 +98,18 @@ function createScene(canvas) {
camera,
controls,
previewPath,
feedPath,
executedPath,
rapidPath,
arcPath,
currentSegmentPath,
toolMarker,
toolAxis,
currentToolhead: new THREE.Vector3(),
lastSelectedView: null,
lastCameraRevision: null,
lastFitKey: null,
pathFitBoundsReady: false,
};
resizeRenderer(preview);
resetCamera(preview, "iso");
@@ -133,6 +146,10 @@ function renderFallbackPreview(preview, state) {
const previewPoints = buildProgramPreviewPoints(state);
const executedPoints = buildExecutedProgramPoints(state, previewPoints);
const rapidPoints = buildRapidPreviewPoints(state);
const feedPoints = buildTypedPreviewPoints(state, "STRAIGHT_FEED");
const arcPoints = buildTypedPreviewPoints(state, "ARC_FEED");
const currentSegmentPoints = buildCurrentSegmentPoints(state);
const pointCount = previewPoints.length;
const executedPointCount = executedPoints.length;
if (pointCount > 0) {
@@ -150,8 +167,24 @@ function renderFallbackPreview(preview, state) {
drawFallbackPolyline(ctx, executedPoints, cx, cy, scale);
ctx.stroke();
}
if (currentSegmentPoints.length > 0) {
ctx.strokeStyle = "#ff4fd8";
ctx.lineWidth = 4;
ctx.beginPath();
drawFallbackPolyline(ctx, currentSegmentPoints, cx, cy, scale);
ctx.stroke();
}
const toolPosition = executionToolPosition(state, previewPoints);
const fitPoints = collectFitPoints(previewPoints, executedPoints, currentSegmentPoints, toolPosition);
const fitKey = [
previewPoints.length,
executedPoints.length,
currentSegmentPoints.length,
previewSourceMode(state),
state.programExecutionMotionIndex || 0,
state.programExecutionSampleIndex || 0,
].join(":");
if (toolPosition) {
const toolX = cx + toolPosition.x * scale;
const toolY = cy - toolPosition.y * scale;
@@ -174,6 +207,11 @@ function renderFallbackPreview(preview, state) {
sceneMode: "program-preview-and-tool-execution",
cameraControls: false,
toolExecutionMarker: Boolean(toolPosition),
feedPointCount: feedPoints.length,
rapidPointCount: rapidPoints.length,
arcPointCount: arcPoints.length,
currentSegmentPointCount: currentSegmentPoints.length,
pathFitBounds: computePointBounds(previewPoints.concat(executedPoints, currentSegmentPoints)) !== null,
});
canvas.dataset.threeFallbackReason = preview.errorMessage;
}
@@ -195,6 +233,17 @@ function exposePreviewDataset(canvas, state, preview) {
canvas.dataset.threeCameraControls = preview.cameraControls ? "orbit-pan-zoom" : "none";
canvas.dataset.threeProgramPreviewSource = previewSourceMode(state);
canvas.dataset.threeToolExecutionMarker = preview.toolExecutionMarker ? "true" : "false";
canvas.dataset.threeToolpathPreviewSource = toolpathPreviewSource(state);
canvas.dataset.threeToolExecutionTraceSource = toolExecutionTraceSource(state);
canvas.dataset.threePathFitBounds = preview.pathFitBounds ? "ok" : "pending";
canvas.dataset.threeCurrentSegmentHighlight = preview.currentSegmentPointCount > 0 ? "ok" : "pending";
canvas.dataset.threeRapidFeedVisualDistinction = preview.rapidPointCount > 0 || preview.feedPointCount > 0 || preview.arcPointCount > 0 ? "ok" : "pending";
canvas.dataset.threeNoGcodeSemanticsGeneration = "ok";
canvas.dataset.threeRapidPathPoints = String(preview.rapidPointCount || 0);
canvas.dataset.threeFeedPathPoints = String(preview.feedPointCount || 0);
canvas.dataset.threeArcPathPoints = String(preview.arcPointCount || 0);
canvas.dataset.threeCurrentSegmentPoints = String(preview.currentSegmentPointCount || 0);
canvas.dataset.threeCurrentSegmentType = currentSegmentType(state);
}
function createLine(color, opacity) {
@@ -212,21 +261,29 @@ function updateToolpathPreview(preview, state) {
const previewPoints = buildProgramPreviewPoints(state);
const executedPoints = buildExecutedProgramPoints(state, previewPoints);
const rapidPoints = buildRapidPreviewPoints(state);
const feedPoints = buildTypedPreviewPoints(state, "STRAIGHT_FEED");
const arcPoints = buildTypedPreviewPoints(state, "ARC_FEED");
const currentSegmentPoints = buildCurrentSegmentPoints(state);
const toolPosition = executionToolPosition(state, previewPoints);
updateLineGeometry(preview.previewPath, previewPoints);
updateLineGeometry(preview.feedPath, feedPoints);
updateLineGeometry(preview.executedPath, executedPoints);
updateLineGeometry(preview.rapidPath, rapidPoints);
updateLineGeometry(preview.arcPath, arcPoints);
updateLineGeometry(preview.currentSegmentPath, currentSegmentPoints);
updateToolExecutionMarker(preview, state, toolPosition);
const cameraRevision = state.preview.cameraRevision ?? 0;
if (
preview.lastSelectedView !== state.preview.selectedView ||
preview.lastCameraRevision !== cameraRevision
preview.lastCameraRevision !== cameraRevision ||
preview.lastFitKey !== fitKey
) {
resetCamera(preview, state.preview.selectedView);
resetCamera(preview, state.preview.selectedView, fitPoints);
preview.lastSelectedView = state.preview.selectedView;
preview.lastCameraRevision = cameraRevision;
preview.lastFitKey = fitKey;
} else {
applyCameraControls(preview.controls);
}
@@ -285,15 +342,32 @@ function buildExecutedProgramPoints(state, previewPoints) {
}
function buildRapidPreviewPoints(state) {
return buildTypedPreviewPoints(state, "STRAIGHT_TRAVERSE");
}
function buildTypedPreviewPoints(state, type) {
const motion = state.programExecution?.motion;
if (!Array.isArray(motion) || state.preview.pathPoints === 0) return [];
return limitPoints(
motion
.filter((event) => event.type === "STRAIGHT_TRAVERSE")
.filter((event) => event.type === type)
.map((event) => vectorFromAxes(event.axes)),
);
}
function buildCurrentSegmentPoints(state) {
if (state.preview.pathPoints === 0) return [];
const motion = state.programExecution?.motion;
if (!Array.isArray(motion) || motion.length === 0) return [];
const motionIndex = clampMotionIndex(state, currentMotionIndex(state));
const current = motion[motionIndex];
const previous = motion[Math.max(motionIndex - 1, 0)];
if (!current) return [];
const start = motionIndex === 0 ? vectorFromAxes(previous?.axes || current.axes) : vectorFromAxes(previous.axes);
const end = vectorFromAxes(current.axes);
return start.distanceTo(end) > 0 ? [start, end] : [end];
}
function buildFixturePreviewPoints(pointCount, tcpPosition) {
const points = [];
for (let index = 0; index < pointCount; index += 1) {
@@ -367,6 +441,60 @@ function previewSourceMode(state) {
return state.programExecutionSourceMode || "fixture-line-playback";
}
function toolpathPreviewSource(state) {
if (state.programExecution?.sourceMode === "linuxcnc-interpreter-wasm") {
return "linuxcnc_interpreter_canonical_motion";
}
if (state.programExecution?.sourceMode === "linuxcnc-machine-file-remap-wasm") {
return "linuxcnc_machine_file_remap_canonical_motion";
}
return "fixture_line_playback_not_promoted";
}
function toolExecutionTraceSource(state) {
if (Array.isArray(state.programExecutionTiming?.samples) && state.programExecutionTiming.samples.length > 0) {
return "linuxcnc_tp_samples_or_task_motion_hal_feedback";
}
if (state.programRuntimeFeedback?.sourceMode === "linuxcnc-task-motion-hal-wasm") {
return "linuxcnc_tp_samples_or_task_motion_hal_feedback";
}
return "fixture_line_playback_not_promoted";
}
function currentSegmentType(state) {
const motion = state.programExecution?.motion;
if (!Array.isArray(motion) || motion.length === 0) return "-";
return motion[clampMotionIndex(state, currentMotionIndex(state))]?.type || "-";
}
function currentMotionIndex(state) {
const sample = state.programExecutionTiming?.samples?.[Number(state.programExecutionSampleIndex || 0)];
if (Number.isFinite(Number(sample?.motionIndex))) return Number(sample.motionIndex);
return Number(state.programExecutionMotionIndex || 0);
}
function clampMotionIndex(state, index) {
const count = state.programExecution?.motion?.length || 0;
if (count <= 0) return 0;
return clamp(Math.round(Number(index) || 0), 0, count - 1);
}
function collectFitPoints(...groups) {
return groups.flatMap((group) => {
if (!group) return [];
if (Array.isArray(group)) return group.filter(Boolean);
return [group];
});
}
function computePointBounds(points) {
const valid = points.filter((point) => point && Number.isFinite(point.x) && Number.isFinite(point.y) && Number.isFinite(point.z));
if (valid.length === 0) return null;
const box = new THREE.Box3().setFromPoints(valid);
if (box.isEmpty()) return null;
return box;
}
function drawFallbackPolyline(ctx, points, cx, cy, scale) {
for (let index = 0; index < points.length; index += 1) {
const point = points[index];
@@ -513,15 +641,30 @@ function panCamera(controls, dx, dy, canvas) {
controls.target.addScaledVector(up, dy * speed);
}
function resetCamera(preview, selectedView) {
function resetCamera(preview, selectedView, fitPoints = []) {
const preset = CAMERA_PRESETS[selectedView] || CAMERA_PRESETS.iso;
preview.controls.theta = preset.theta;
preview.controls.phi = preset.phi;
preview.controls.radius = preset.radius;
preview.controls.target.copy(preset.target);
preview.pathFitBoundsReady = applyFitBounds(preview.controls, selectedView, fitPoints);
applyCameraControls(preview.controls);
}
function applyFitBounds(controls, selectedView, fitPoints) {
const bounds = computePointBounds(fitPoints);
if (!bounds) return false;
const center = new THREE.Vector3();
const size = new THREE.Vector3();
bounds.getCenter(center);
bounds.getSize(size);
controls.target.copy(center);
const maxSpan = Math.max(size.x, size.y, size.z, 0.8);
const fitRadius = clamp(maxSpan * 1.8, 2.2, 28);
controls.radius = selectedView === "z" ? Math.max(fitRadius, 4.2) : fitRadius;
return true;
}
function applyCameraControls(controls) {
const sinPhiRadius = Math.sin(controls.phi) * controls.radius;
controls.camera.position.set(