feat: sync latest run execution updates

This commit is contained in:
2026-06-22 21:47:16 -04:00
parent 0b1aad39e1
commit 8d3177cb73
92 changed files with 22837 additions and 246 deletions

View File

@@ -1,13 +1,19 @@
import * as THREE from "../vendor/three/three.module.js";
import {
linearUnitsLabel,
linearUnitsToMetersFactor,
linearValueToMeters,
resolveStateLinearUnits,
} from "../runtime/linear-units.js";
const scenes = new WeakMap();
const CAMERA_PRESETS = {
iso: { theta: -0.96, phi: 1.02, radius: 7.0, target: new THREE.Vector3(0, 0, 0) },
x: { theta: 0, phi: Math.PI / 2, radius: 6.2, target: new THREE.Vector3(0, 0, 0) },
y: { theta: -Math.PI / 2, phi: Math.PI / 2, radius: 6.2, target: new THREE.Vector3(0, 0, 0) },
z: { theta: 0, phi: 0.001, radius: 6.6, target: new THREE.Vector3(0, 0, 0) },
iso: { theta: -0.96, phi: 1.02, radius: 0.72, target: new THREE.Vector3(0, 0, 0) },
x: { theta: 0, phi: Math.PI / 2, radius: 0.64, target: new THREE.Vector3(0, 0, 0) },
y: { theta: -Math.PI / 2, phi: Math.PI / 2, radius: 0.64, target: new THREE.Vector3(0, 0, 0) },
z: { theta: 0, phi: 0.001, radius: 0.68, target: new THREE.Vector3(0, 0, 0) },
};
const MAX_TOOLPATH_POINTS = 1600;
const MAX_TOOLPATH_POINTS = Number.POSITIVE_INFINITY;
const EMPTY_GEOMETRY = new THREE.BufferGeometry().setFromPoints([]);
export function renderFiveAxisScene(canvas, state) {
@@ -51,6 +57,11 @@ export function renderFiveAxisScene(canvas, state) {
toolExecutionMarker: preview.toolMarker.visible,
toolAxisMarker: preview.toolAxis.visible,
pathFitBounds: preview.pathFitBoundsReady,
pathBounds: computePointBoundsFromGeometryGroups([
preview.previewPath.geometry,
preview.executedPath.geometry,
preview.currentSegmentPath.geometry,
]),
});
}
@@ -72,7 +83,7 @@ function createScene(canvas) {
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(42, 1, 0.1, 100);
const camera = new THREE.PerspectiveCamera(42, 1, 0.001, 10);
const machineModel = createMachineReferenceModel();
scene.add(machineModel.root);
@@ -84,7 +95,7 @@ function createScene(canvas) {
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.SphereGeometry(0.0065, 18, 12),
new THREE.MeshBasicMaterial({ color: 0x1ffff4 }),
);
const toolAxis = new THREE.Line(
@@ -165,6 +176,7 @@ function renderFallbackPreview(preview, state) {
arcPointCount: arcPoints.length,
currentSegmentPointCount: currentSegmentPoints.length,
pathFitBounds: computePointBounds(previewPoints.concat(executedPoints, currentSegmentPoints)) !== null,
pathBounds: summarizeBounds(computePointBounds(previewPoints.concat(executedPoints, currentSegmentPoints))),
});
canvas.dataset.threeFallbackReason = preview.errorMessage;
@@ -177,7 +189,7 @@ function renderFallbackPreview(preview, state) {
const cx = width * 0.5;
const cy = height * 0.53;
const scale = Math.min(width / 7.2, height / 4.8);
const scale = Math.min(width / 0.72, height / 0.48);
drawFallbackMachineReference(ctx, cx, cy, scale, state);
@@ -225,6 +237,9 @@ function exposePreviewDataset(canvas, state, preview) {
canvas.dataset.threeExecutedPathPoints = String(preview.executedPointCount);
canvas.dataset.threeSceneObjects = String(preview.sceneObjectCount);
canvas.dataset.threeToolhead = JSON.stringify(toRoundedVector(preview.toolhead));
canvas.dataset.threeSceneUnits = "m";
canvas.dataset.threeLinearUnits = linearUnitsLabel(resolveSceneLinearUnits(state));
canvas.dataset.threeLinearUnitScaleToMeters = String(linearUnitsToMetersFactor(resolveSceneLinearUnits(state)));
canvas.dataset.threeToolAxis = JSON.stringify(toRoundedVector(state.toolAxisVector));
canvas.dataset.threeTcpPose = JSON.stringify(toRoundedPose(state.tcpPose));
canvas.dataset.threeRtcpState = state.rtcpState;
@@ -244,6 +259,7 @@ function exposePreviewDataset(canvas, state, preview) {
canvas.dataset.threeToolpathPreviewSource = toolpathPreviewSource(state);
canvas.dataset.threeToolExecutionTraceSource = toolExecutionTraceSource(state);
canvas.dataset.threePathFitBounds = preview.pathFitBounds ? "ok" : "pending";
canvas.dataset.threePathBoundsMeters = JSON.stringify(preview.pathBounds || null);
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";
@@ -270,47 +286,47 @@ function createMachineReferenceModel() {
root.name = "five-axis-machine-reference";
const base = new THREE.Mesh(
new THREE.BoxGeometry(4.8, 3.2, 0.08),
new THREE.BoxGeometry(0.48, 0.32, 0.008),
new THREE.MeshBasicMaterial({ color: 0x222930 }),
);
base.position.z = -0.16;
base.position.z = -0.016;
const table = new THREE.Mesh(
new THREE.BoxGeometry(3.7, 2.35, 0.05),
new THREE.BoxGeometry(0.37, 0.235, 0.005),
new THREE.MeshBasicMaterial({ color: 0x3a444d, transparent: true, opacity: 0.78 }),
);
table.position.z = -0.08;
table.position.z = -0.008;
const xAxis = createStaticLine([new THREE.Vector3(-2.2, 0, 0), new THREE.Vector3(2.25, 0, 0)], 0xff4d4d);
const yAxis = createStaticLine([new THREE.Vector3(0, -1.55, 0), new THREE.Vector3(0, 1.6, 0)], 0x70df7d);
const zAxis = createStaticLine([new THREE.Vector3(0, 0, -0.08), new THREE.Vector3(0, 0, 1.75)], 0x5aa7ff);
const xAxis = createStaticLine([new THREE.Vector3(-0.22, 0, 0), new THREE.Vector3(0.225, 0, 0)], 0xff4d4d);
const yAxis = createStaticLine([new THREE.Vector3(0, -0.155, 0), new THREE.Vector3(0, 0.16, 0)], 0x70df7d);
const zAxis = createStaticLine([new THREE.Vector3(0, 0, -0.008), new THREE.Vector3(0, 0, 0.175)], 0x5aa7ff);
const rotaryA = new THREE.Mesh(
new THREE.TorusGeometry(0.88, 0.018, 8, 72),
new THREE.TorusGeometry(0.088, 0.0018, 8, 72),
new THREE.MeshBasicMaterial({ color: 0x1ffff4, transparent: true, opacity: 0.92 }),
);
rotaryA.rotation.y = Math.PI / 2;
const rotaryC = new THREE.Mesh(
new THREE.TorusGeometry(1.1, 0.016, 8, 72),
new THREE.TorusGeometry(0.11, 0.0016, 8, 72),
new THREE.MeshBasicMaterial({ color: 0xffd166, transparent: true, opacity: 0.9 }),
);
rotaryC.rotation.x = Math.PI / 2;
rotaryC.position.z = 0.04;
rotaryC.position.z = 0.004;
const toolHolder = new THREE.Group();
const holderBody = new THREE.Mesh(
new THREE.CylinderGeometry(0.08, 0.08, 0.42, 18),
new THREE.CylinderGeometry(0.008, 0.008, 0.042, 18),
new THREE.MeshBasicMaterial({ color: 0xf1f5f9 }),
);
holderBody.rotation.x = Math.PI / 2;
holderBody.position.z = 0.32;
holderBody.position.z = 0.032;
const cutter = new THREE.Mesh(
new THREE.ConeGeometry(0.06, 0.25, 18),
new THREE.ConeGeometry(0.006, 0.025, 18),
new THREE.MeshBasicMaterial({ color: 0xfff176 }),
);
cutter.rotation.x = Math.PI;
cutter.position.z = 0.08;
cutter.position.z = 0.008;
toolHolder.add(holderBody, cutter);
root.add(base, table, xAxis, yAxis, zAxis, rotaryA, rotaryC, toolHolder);
@@ -387,7 +403,7 @@ function updateToolExecutionMarker(preview, state, toolPosition) {
preview.toolAxis.visible = true;
updateLineGeometry(preview.toolAxis, [
toolPosition,
toolPosition.clone().add(vector.multiplyScalar(0.7)),
toolPosition.clone().add(vector.multiplyScalar(0.07)),
]);
}
@@ -401,7 +417,7 @@ function updateMachineReferenceModel(preview, state, toolPosition) {
model.rotaryA.rotation.y = Math.PI / 2 + b;
model.rotaryC.rotation.z = c;
const tcpPosition = toolPosition || toPreviewVector(state.tcpPose || state.axisPose);
const tcpPosition = toolPosition || toPreviewVector(state.tcpPose || state.axisPose, state);
model.toolHolder.position.copy(tcpPosition);
const toolVector = toToolVector(state.toolAxisVector);
model.toolHolder.lookAt(tcpPosition.clone().add(toolVector));
@@ -422,12 +438,12 @@ function geometryPointCount(geometry) {
function buildProgramPreviewPoints(state) {
const motion = state.programExecution?.motion;
if (Array.isArray(motion) && motion.length > 0 && state.preview.pathPoints !== 0) {
return limitPoints(motion.map((event) => vectorFromAxes(event.axes)));
return limitPoints(motion.map((event) => vectorFromAxes(event.axes, state, event.linearUnits)));
}
const pointCount = normalizePathPointCount(state.preview.pathPoints);
if (pointCount === 0) return [];
return buildFixturePreviewPoints(pointCount, toPreviewVector(state.tcpPose));
return buildFixturePreviewPoints(pointCount, toPreviewVector(state.tcpPose, state));
}
function buildExecutedProgramPoints(state, previewPoints) {
@@ -436,7 +452,7 @@ function buildExecutedProgramPoints(state, previewPoints) {
const sampleIndex = Number(state.programExecutionSampleIndex || 0);
if (Array.isArray(samples) && samples.length > 0) {
const end = clamp(Math.round(sampleIndex), 0, samples.length - 1);
return limitPoints(samples.slice(0, end + 1).map((sample) => vectorFromAxes(sample)));
return limitPoints(samples.slice(0, end + 1).map((sample) => vectorFromAxes(sample, state, sample.linearUnits)));
}
const motionIndex = clamp(Math.round(Number(state.programExecutionMotionIndex || 0)), 0, previewPoints.length - 1);
@@ -453,7 +469,7 @@ function buildTypedPreviewPoints(state, type) {
return limitPoints(
motion
.filter((event) => event.type === type)
.map((event) => vectorFromAxes(event.axes)),
.map((event) => vectorFromAxes(event.axes, state, event.linearUnits)),
);
}
@@ -465,8 +481,10 @@ function buildCurrentSegmentPoints(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);
const start = motionIndex === 0
? vectorFromAxes(previous?.axes || current.axes, state, previous?.linearUnits || current.linearUnits)
: vectorFromAxes(previous.axes, state, previous.linearUnits);
const end = vectorFromAxes(current.axes, state, current.linearUnits);
return start.distanceTo(end) > 0 ? [start, end] : [end];
}
@@ -474,9 +492,9 @@ function buildFixturePreviewPoints(pointCount, tcpPosition) {
const points = [];
for (let index = 0; index < pointCount; index += 1) {
const t = pointCount === 1 ? 0 : index / (pointCount - 1);
const x = -2.45 + t * 4.9;
const y = Math.sin(t * Math.PI * 13) * 0.36;
const z = -0.68 + Math.sin(t * Math.PI * 2) * 0.42;
const x = -0.085 + t * 0.17;
const y = Math.sin(t * Math.PI * 13) * 0.012;
const z = 0.012 + Math.sin(t * Math.PI * 2) * 0.018;
points.push(new THREE.Vector3(x, y, z));
}
if (points.length > 0 && tcpPosition) {
@@ -487,33 +505,42 @@ function buildFixturePreviewPoints(pointCount, tcpPosition) {
function executionToolPosition(state, previewPoints) {
const feedbackAxes = state.programRuntimeFeedback?.axisPose || state.programRuntimeFeedback;
if (feedbackAxes && hasLinearAxes(feedbackAxes)) return vectorFromAxes(feedbackAxes);
if (hasLinearAxes(state.axisPose)) return vectorFromAxes(state.axisPose);
if (feedbackAxes && hasLinearAxes(feedbackAxes)) {
return vectorFromAxes(feedbackAxes, state, state.programRuntimeFeedback?.linearUnits);
}
if (hasLinearAxes(state.axisPose)) return vectorFromAxes(state.axisPose, state);
return previewPoints.at(-1) || null;
}
function vectorFromAxes(axes = {}) {
export function axesToSceneMeters(axes = {}, state = {}, linearUnits = null) {
const units = linearUnits || axes.linearUnits || resolveSceneLinearUnits(state);
return {
x: linearValueToMeters(axes.x, units),
y: linearValueToMeters(axes.y, units),
z: linearValueToMeters(axes.z, units),
};
}
function vectorFromAxes(axes = {}, state = {}, linearUnits = null) {
const point = axesToSceneMeters(axes, state, linearUnits);
return new THREE.Vector3(
scaleLinearAxis(axes.x),
scaleLinearAxis(axes.y),
scaleZAxis(axes.z),
point.x,
point.y,
point.z,
);
}
function toPreviewVector(pose = {}) {
function toPreviewVector(pose = {}, state = {}) {
const point = axesToSceneMeters(pose, state);
return new THREE.Vector3(
scaleLinearAxis(pose.x),
scaleLinearAxis(pose.y),
scaleZAxis(pose.z),
point.x,
point.y,
point.z,
);
}
function scaleLinearAxis(value) {
return clamp((Number(value) || 0) * 0.035, -2.7, 2.7);
}
function scaleZAxis(value) {
return clamp((Number(value) || 0) * 0.04 + 0.35, -1.1, 1.9);
function resolveSceneLinearUnits(state) {
return resolveStateLinearUnits(state);
}
function toToolVector(vector = {}) {
@@ -596,6 +623,35 @@ function computePointBounds(points) {
return box;
}
function computePointBoundsFromGeometryGroups(geometries) {
const points = [];
for (const geometry of geometries) {
const position = geometry?.getAttribute("position");
if (!position) continue;
for (let index = 0; index < position.count; index += 1) {
points.push(new THREE.Vector3(
position.getX(index),
position.getY(index),
position.getZ(index),
));
}
}
return summarizeBounds(computePointBounds(points));
}
function summarizeBounds(bounds) {
if (!bounds) return null;
const center = new THREE.Vector3();
const size = new THREE.Vector3();
bounds.getCenter(center);
bounds.getSize(size);
return {
center: toRoundedVector(center),
size: toRoundedVector(size),
maxSpan: Number(Math.max(size.x, size.y, size.z).toFixed(6)),
};
}
function drawFallbackPolyline(ctx, points, cx, cy, scale) {
for (let index = 0; index < points.length; index += 1) {
const point = points[index];
@@ -607,27 +663,27 @@ function drawFallbackPolyline(ctx, points, cx, cy, scale) {
}
function drawFallbackMachineReference(ctx, cx, cy, scale, state) {
const tableWidth = 4.8 * scale;
const tableHeight = 3.2 * scale;
const tableWidth = 0.48 * scale;
const tableHeight = 0.32 * scale;
ctx.fillStyle = "#20272e";
ctx.strokeStyle = "#56616b";
ctx.lineWidth = 2;
ctx.fillRect(cx - tableWidth / 2, cy - tableHeight / 2, tableWidth, tableHeight);
ctx.strokeRect(cx - tableWidth / 2, cy - tableHeight / 2, tableWidth, tableHeight);
drawFallbackAxis(ctx, cx - 2.25 * scale, cy, cx + 2.25 * scale, cy, "#ff4d4d");
drawFallbackAxis(ctx, cx, cy + 1.55 * scale, cx, cy - 1.6 * scale, "#70df7d");
drawFallbackAxis(ctx, cx, cy + 0.2 * scale, cx, cy - 1.15 * scale, "#5aa7ff");
drawFallbackAxis(ctx, cx - 0.225 * scale, cy, cx + 0.225 * scale, cy, "#ff4d4d");
drawFallbackAxis(ctx, cx, cy + 0.155 * scale, cx, cy - 0.16 * scale, "#70df7d");
drawFallbackAxis(ctx, cx, cy + 0.02 * scale, cx, cy - 0.115 * scale, "#5aa7ff");
ctx.strokeStyle = "#1ffff4";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.ellipse(cx, cy, 0.92 * scale, 0.42 * scale, degreesToRadians(state.axisPose?.a), 0, Math.PI * 2);
ctx.ellipse(cx, cy, 0.092 * scale, 0.042 * scale, degreesToRadians(state.axisPose?.a), 0, Math.PI * 2);
ctx.stroke();
ctx.strokeStyle = "#ffd166";
ctx.beginPath();
ctx.arc(cx, cy, 0.7 * scale, 0, Math.PI * 2);
ctx.arc(cx, cy, 0.07 * scale, 0, Math.PI * 2);
ctx.stroke();
const tcp = executionToolPosition(state, []);
@@ -637,12 +693,12 @@ function drawFallbackMachineReference(ctx, cx, cy, scale, state) {
ctx.strokeStyle = "#f1f5f9";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(toolX, toolY - 0.38 * scale);
ctx.lineTo(toolX, toolY - 0.08 * scale);
ctx.moveTo(toolX, toolY - 0.038 * scale);
ctx.lineTo(toolX, toolY - 0.008 * scale);
ctx.stroke();
ctx.fillStyle = "#1ffff4";
ctx.beginPath();
ctx.arc(toolX, toolY, 0.07 * scale, 0, Math.PI * 2);
ctx.arc(toolX, toolY, 0.007 * scale, 0, Math.PI * 2);
ctx.fill();
}
}
@@ -676,7 +732,7 @@ function createToolpathCameraControls(canvas, camera, renderFrame) {
canvas.addEventListener("wheel", (event) => {
event.preventDefault();
const scale = Math.exp(Math.sign(event.deltaY) * 0.12);
controls.radius = clamp(controls.radius * scale, 1.2, 28);
controls.radius = clamp(controls.radius * scale, 0.06, 4);
applyCameraControls(controls);
controls.renderFrame();
}, { passive: false });
@@ -700,7 +756,7 @@ function createToolpathCameraControls(canvas, camera, renderFrame) {
const distance = getPointerDistance(controls.pointers);
const center = getPointerCenter(controls.pointers);
if (distance > 0 && controls.lastPinchDistance > 0) {
controls.radius = clamp(controls.radius * (controls.lastPinchDistance / distance), 1.2, 28);
controls.radius = clamp(controls.radius * (controls.lastPinchDistance / distance), 0.06, 4);
if (controls.lastPinchCenter) {
panCamera(
controls,
@@ -810,9 +866,9 @@ function applyFitBounds(controls, selectedView, fitPoints) {
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;
const maxSpan = Math.max(size.x, size.y, size.z, 0.08);
const fitRadius = clamp(maxSpan * 1.8, 0.22, 4);
controls.radius = selectedView === "z" ? Math.max(fitRadius, 0.42) : fitRadius;
return true;
}