|
|
|
|
@@ -1,6 +1,14 @@
|
|
|
|
|
import * as THREE from "../vendor/three/three.module.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) },
|
|
|
|
|
};
|
|
|
|
|
const MAX_TOOLPATH_POINTS = 1600;
|
|
|
|
|
const EMPTY_GEOMETRY = new THREE.BufferGeometry().setFromPoints([]);
|
|
|
|
|
|
|
|
|
|
export function renderFiveAxisScene(canvas, state) {
|
|
|
|
|
let preview = scenes.get(canvas);
|
|
|
|
|
@@ -16,7 +24,7 @@ export function renderFiveAxisScene(canvas, state) {
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
resizeRenderer(preview);
|
|
|
|
|
updateMachinePose(preview, state);
|
|
|
|
|
updateToolpathPreview(preview, state);
|
|
|
|
|
preview.renderer.render(preview.scene, preview.camera);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
const fallback = createFallbackPreview(canvas, error);
|
|
|
|
|
@@ -25,12 +33,17 @@ export function renderFiveAxisScene(canvas, state) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const pointCount = preview.pathLine.geometry.getAttribute("position").count;
|
|
|
|
|
const pointCount = preview.previewPath.geometry.getAttribute("position").count;
|
|
|
|
|
const executedPointCount = preview.executedPath.geometry.getAttribute("position").count;
|
|
|
|
|
exposePreviewDataset(canvas, state, {
|
|
|
|
|
pointCount,
|
|
|
|
|
executedPointCount,
|
|
|
|
|
sceneObjectCount: countSceneObjects(preview.scene),
|
|
|
|
|
toolhead: preview.toolGroup.position,
|
|
|
|
|
toolhead: preview.currentToolhead,
|
|
|
|
|
renderer: "webgl",
|
|
|
|
|
sceneMode: "program-preview-and-tool-execution",
|
|
|
|
|
cameraControls: preview.controls.enabled,
|
|
|
|
|
toolExecutionMarker: preview.toolMarker.visible,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -48,87 +61,45 @@ function createScene(canvas) {
|
|
|
|
|
antialias: true,
|
|
|
|
|
preserveDrawingBuffer: true,
|
|
|
|
|
});
|
|
|
|
|
renderer.setClearColor(0x07100d, 1);
|
|
|
|
|
renderer.setClearColor(0x030405, 1);
|
|
|
|
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
|
|
|
|
|
|
|
|
|
const scene = new THREE.Scene();
|
|
|
|
|
scene.fog = new THREE.Fog(0x07100d, 7, 15);
|
|
|
|
|
const camera = new THREE.PerspectiveCamera(42, 1, 0.1, 100);
|
|
|
|
|
camera.position.set(3.4, -5.0, 3.2);
|
|
|
|
|
camera.lookAt(0, 0, 0);
|
|
|
|
|
|
|
|
|
|
const ambient = new THREE.HemisphereLight(0xdffbff, 0x17110d, 0.86);
|
|
|
|
|
const key = new THREE.DirectionalLight(0xffffff, 1.35);
|
|
|
|
|
key.position.set(3, -5, 7);
|
|
|
|
|
const fill = new THREE.DirectionalLight(0x4fd5ff, 0.46);
|
|
|
|
|
fill.position.set(-4, 3, 3);
|
|
|
|
|
scene.add(ambient, key, fill);
|
|
|
|
|
|
|
|
|
|
const floor = new THREE.Mesh(
|
|
|
|
|
new THREE.BoxGeometry(6.2, 4.5, 0.08),
|
|
|
|
|
new THREE.MeshStandardMaterial({ color: 0x151b1f, roughness: 0.78, metalness: 0.12 }),
|
|
|
|
|
);
|
|
|
|
|
floor.position.z = -0.98;
|
|
|
|
|
scene.add(floor);
|
|
|
|
|
|
|
|
|
|
const grid = new THREE.GridHelper(6.2, 14, 0x4b555a, 0x252f32);
|
|
|
|
|
grid.rotation.x = Math.PI / 2;
|
|
|
|
|
grid.position.z = -0.93;
|
|
|
|
|
scene.add(grid);
|
|
|
|
|
|
|
|
|
|
const axes = new THREE.AxesHelper(1.45);
|
|
|
|
|
axes.position.set(-2.85, -2.0, -0.86);
|
|
|
|
|
scene.add(axes);
|
|
|
|
|
|
|
|
|
|
const envelope = buildEnvelope();
|
|
|
|
|
scene.add(envelope);
|
|
|
|
|
|
|
|
|
|
const tableGroup = new THREE.Group();
|
|
|
|
|
const table = new THREE.Mesh(
|
|
|
|
|
new THREE.BoxGeometry(3.0, 2.15, 0.18),
|
|
|
|
|
new THREE.MeshStandardMaterial({ color: 0x42484e, roughness: 0.68, metalness: 0.22 }),
|
|
|
|
|
);
|
|
|
|
|
const platter = new THREE.Mesh(
|
|
|
|
|
new THREE.CylinderGeometry(0.84, 0.84, 0.16, 64),
|
|
|
|
|
new THREE.MeshStandardMaterial({ color: 0x6d7880, roughness: 0.48, metalness: 0.42 }),
|
|
|
|
|
);
|
|
|
|
|
platter.rotation.x = Math.PI / 2;
|
|
|
|
|
platter.position.z = 0.14;
|
|
|
|
|
tableGroup.add(table, platter);
|
|
|
|
|
scene.add(tableGroup);
|
|
|
|
|
|
|
|
|
|
const pathLine = buildToolpath();
|
|
|
|
|
scene.add(pathLine);
|
|
|
|
|
|
|
|
|
|
const toolGroup = new THREE.Group();
|
|
|
|
|
const toolBody = new THREE.Mesh(
|
|
|
|
|
new THREE.CylinderGeometry(0.045, 0.07, 1.05, 28),
|
|
|
|
|
new THREE.MeshStandardMaterial({ color: 0x29ecf0, emissive: 0x0a4f52, roughness: 0.28 }),
|
|
|
|
|
);
|
|
|
|
|
toolBody.rotation.x = Math.PI / 2;
|
|
|
|
|
toolBody.position.z = 0.52;
|
|
|
|
|
const tcpPoint = new THREE.Mesh(
|
|
|
|
|
new THREE.SphereGeometry(0.095, 28, 18),
|
|
|
|
|
new THREE.MeshStandardMaterial({ color: 0x1ffff4, emissive: 0x0b6f6f, roughness: 0.18 }),
|
|
|
|
|
const previewPath = createLine(0x808892, 0.56);
|
|
|
|
|
const executedPath = createLine(0x1ffff4, 1);
|
|
|
|
|
const rapidPath = createLine(0xffb13b, 0.82);
|
|
|
|
|
const toolMarker = new THREE.Mesh(
|
|
|
|
|
new THREE.SphereGeometry(0.065, 18, 12),
|
|
|
|
|
new THREE.MeshBasicMaterial({ color: 0x1ffff4 }),
|
|
|
|
|
);
|
|
|
|
|
const toolAxis = new THREE.Line(
|
|
|
|
|
new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(), new THREE.Vector3(0, 0, 1)]),
|
|
|
|
|
new THREE.LineBasicMaterial({ color: 0x21f2f2, linewidth: 2 }),
|
|
|
|
|
EMPTY_GEOMETRY.clone(),
|
|
|
|
|
new THREE.LineBasicMaterial({ color: 0x1ffff4, transparent: true, opacity: 0.9 }),
|
|
|
|
|
);
|
|
|
|
|
toolGroup.add(toolBody, tcpPoint, toolAxis);
|
|
|
|
|
scene.add(toolGroup);
|
|
|
|
|
scene.add(previewPath, rapidPath, executedPath, toolAxis, toolMarker);
|
|
|
|
|
|
|
|
|
|
const controls = createToolpathCameraControls(canvas, camera, () => {
|
|
|
|
|
renderer.render(scene, camera);
|
|
|
|
|
});
|
|
|
|
|
const preview = {
|
|
|
|
|
kind: "webgl",
|
|
|
|
|
renderer,
|
|
|
|
|
scene,
|
|
|
|
|
camera,
|
|
|
|
|
tableGroup,
|
|
|
|
|
toolGroup,
|
|
|
|
|
controls,
|
|
|
|
|
previewPath,
|
|
|
|
|
executedPath,
|
|
|
|
|
rapidPath,
|
|
|
|
|
toolMarker,
|
|
|
|
|
toolAxis,
|
|
|
|
|
pathLine,
|
|
|
|
|
currentToolhead: new THREE.Vector3(),
|
|
|
|
|
lastSelectedView: null,
|
|
|
|
|
lastCameraRevision: null,
|
|
|
|
|
};
|
|
|
|
|
resizeRenderer(preview);
|
|
|
|
|
resetCamera(preview, "iso");
|
|
|
|
|
return preview;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -153,101 +124,65 @@ function renderFallbackPreview(preview, state) {
|
|
|
|
|
if (!ctx) return;
|
|
|
|
|
|
|
|
|
|
ctx.clearRect(0, 0, width, height);
|
|
|
|
|
ctx.fillStyle = "#07100d";
|
|
|
|
|
ctx.fillStyle = "#030405";
|
|
|
|
|
ctx.fillRect(0, 0, width, height);
|
|
|
|
|
|
|
|
|
|
const cx = width * 0.5;
|
|
|
|
|
const cy = height * 0.53;
|
|
|
|
|
const scale = Math.min(width / 7.2, height / 4.8);
|
|
|
|
|
|
|
|
|
|
drawFallbackGrid(ctx, cx, cy, scale);
|
|
|
|
|
|
|
|
|
|
ctx.strokeStyle = "#e43a35";
|
|
|
|
|
ctx.lineWidth = 1.5;
|
|
|
|
|
ctx.strokeRect(cx - 2.95 * scale, cy - 1.9 * scale, 5.9 * scale, 3.8 * scale);
|
|
|
|
|
|
|
|
|
|
ctx.fillStyle = "#42484e";
|
|
|
|
|
ctx.strokeStyle = "#79838a";
|
|
|
|
|
ctx.lineWidth = 2;
|
|
|
|
|
ctx.beginPath();
|
|
|
|
|
ctx.roundRect(cx - 1.5 * scale, cy - 0.78 * scale, 3.0 * scale, 1.56 * scale, 4);
|
|
|
|
|
ctx.fill();
|
|
|
|
|
ctx.stroke();
|
|
|
|
|
|
|
|
|
|
ctx.fillStyle = "#6d7880";
|
|
|
|
|
ctx.strokeStyle = "#a3b0b8";
|
|
|
|
|
ctx.beginPath();
|
|
|
|
|
ctx.ellipse(cx, cy, 0.84 * scale, 0.48 * scale, 0, 0, Math.PI * 2);
|
|
|
|
|
ctx.fill();
|
|
|
|
|
ctx.stroke();
|
|
|
|
|
|
|
|
|
|
ctx.strokeStyle = "#ffffff";
|
|
|
|
|
ctx.lineWidth = 2;
|
|
|
|
|
ctx.beginPath();
|
|
|
|
|
for (let index = 0; index < 72; index += 1) {
|
|
|
|
|
const t = index / 71;
|
|
|
|
|
const x = cx + (-2.45 + t * 4.9) * scale;
|
|
|
|
|
const y = cy + Math.sin(t * Math.PI * 13) * 0.36 * scale;
|
|
|
|
|
if (index === 0) ctx.moveTo(x, y);
|
|
|
|
|
else ctx.lineTo(x, y);
|
|
|
|
|
const previewPoints = buildProgramPreviewPoints(state);
|
|
|
|
|
const executedPoints = buildExecutedProgramPoints(state, previewPoints);
|
|
|
|
|
const pointCount = previewPoints.length;
|
|
|
|
|
const executedPointCount = executedPoints.length;
|
|
|
|
|
if (pointCount > 0) {
|
|
|
|
|
ctx.strokeStyle = "#8d95a0";
|
|
|
|
|
ctx.lineWidth = 2;
|
|
|
|
|
ctx.beginPath();
|
|
|
|
|
drawFallbackPolyline(ctx, previewPoints, cx, cy, scale);
|
|
|
|
|
ctx.stroke();
|
|
|
|
|
}
|
|
|
|
|
ctx.stroke();
|
|
|
|
|
|
|
|
|
|
const tcp = state.tcpPose;
|
|
|
|
|
const tool = state.toolAxisVector;
|
|
|
|
|
const toolX = cx + clamp(tcp.x * 0.035, -2.7, 2.7) * scale;
|
|
|
|
|
const toolY = cy - clamp(tcp.y * 0.035, -2.0, 2.0) * scale;
|
|
|
|
|
const axisX = toolX + tool.x * 0.85 * scale;
|
|
|
|
|
const axisY = toolY - (tool.y || 0.2) * 0.85 * scale;
|
|
|
|
|
ctx.strokeStyle = "#21f2f2";
|
|
|
|
|
ctx.fillStyle = "#1ffff4";
|
|
|
|
|
ctx.lineWidth = 3;
|
|
|
|
|
ctx.beginPath();
|
|
|
|
|
ctx.moveTo(toolX, toolY);
|
|
|
|
|
ctx.lineTo(axisX, axisY);
|
|
|
|
|
ctx.stroke();
|
|
|
|
|
ctx.beginPath();
|
|
|
|
|
ctx.arc(toolX, toolY, 0.09 * scale, 0, Math.PI * 2);
|
|
|
|
|
ctx.fill();
|
|
|
|
|
if (executedPointCount > 0) {
|
|
|
|
|
ctx.strokeStyle = "#1ffff4";
|
|
|
|
|
ctx.lineWidth = 3;
|
|
|
|
|
ctx.beginPath();
|
|
|
|
|
drawFallbackPolyline(ctx, executedPoints, cx, cy, scale);
|
|
|
|
|
ctx.stroke();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const toolPosition = executionToolPosition(state, previewPoints);
|
|
|
|
|
if (toolPosition) {
|
|
|
|
|
const toolX = cx + toolPosition.x * scale;
|
|
|
|
|
const toolY = cy - toolPosition.y * scale;
|
|
|
|
|
ctx.fillStyle = "#1ffff4";
|
|
|
|
|
ctx.beginPath();
|
|
|
|
|
ctx.arc(toolX, toolY, 0.055 * scale, 0, Math.PI * 2);
|
|
|
|
|
ctx.fill();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctx.fillStyle = "#b7c7b8";
|
|
|
|
|
ctx.font = "12px Courier New, monospace";
|
|
|
|
|
ctx.fillText("2D RTCP fallback", 12, height - 14);
|
|
|
|
|
|
|
|
|
|
exposePreviewDataset(canvas, state, {
|
|
|
|
|
pointCount: 72,
|
|
|
|
|
sceneObjectCount: 12,
|
|
|
|
|
toolhead: {
|
|
|
|
|
x: clamp(tcp.x * 0.035, -2.7, 2.7),
|
|
|
|
|
y: clamp(tcp.y * 0.035, -2.0, 2.0),
|
|
|
|
|
z: clamp(tcp.z * 0.04 + 0.35, -1.1, 1.9),
|
|
|
|
|
},
|
|
|
|
|
pointCount,
|
|
|
|
|
executedPointCount,
|
|
|
|
|
sceneObjectCount: (pointCount > 0 ? 1 : 0) + (executedPointCount > 0 ? 1 : 0),
|
|
|
|
|
toolhead: toolPosition || { x: 0, y: 0, z: 0 },
|
|
|
|
|
renderer: "2d-fallback",
|
|
|
|
|
sceneMode: "program-preview-and-tool-execution",
|
|
|
|
|
cameraControls: false,
|
|
|
|
|
toolExecutionMarker: Boolean(toolPosition),
|
|
|
|
|
});
|
|
|
|
|
canvas.dataset.threeFallbackReason = preview.errorMessage;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function drawFallbackGrid(ctx, cx, cy, scale) {
|
|
|
|
|
ctx.strokeStyle = "#273236";
|
|
|
|
|
ctx.lineWidth = 1;
|
|
|
|
|
for (let x = -3; x <= 3; x += 0.5) {
|
|
|
|
|
ctx.beginPath();
|
|
|
|
|
ctx.moveTo(cx + x * scale, cy - 2.1 * scale);
|
|
|
|
|
ctx.lineTo(cx + x * scale, cy + 2.1 * scale);
|
|
|
|
|
ctx.stroke();
|
|
|
|
|
}
|
|
|
|
|
for (let y = -2; y <= 2; y += 0.5) {
|
|
|
|
|
ctx.beginPath();
|
|
|
|
|
ctx.moveTo(cx - 3.1 * scale, cy + y * scale);
|
|
|
|
|
ctx.lineTo(cx + 3.1 * scale, cy + y * scale);
|
|
|
|
|
ctx.stroke();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function exposePreviewDataset(canvas, state, preview) {
|
|
|
|
|
canvas.dataset.threeReady = "true";
|
|
|
|
|
canvas.dataset.threeRevision = THREE.REVISION;
|
|
|
|
|
canvas.dataset.threePathPoints = String(preview.pointCount);
|
|
|
|
|
canvas.dataset.threeExecutedPathPoints = String(preview.executedPointCount);
|
|
|
|
|
canvas.dataset.threeSceneObjects = String(preview.sceneObjectCount);
|
|
|
|
|
canvas.dataset.threeToolhead = JSON.stringify(toRoundedVector(preview.toolhead));
|
|
|
|
|
canvas.dataset.threeToolAxis = JSON.stringify(toRoundedVector(state.toolAxisVector));
|
|
|
|
|
@@ -256,62 +191,346 @@ function exposePreviewDataset(canvas, state, preview) {
|
|
|
|
|
canvas.dataset.threeSelectedView = state.preview.selectedView;
|
|
|
|
|
canvas.dataset.threeFrameApi = state.rtcpFrame.apiName;
|
|
|
|
|
canvas.dataset.threeRenderer = preview.renderer;
|
|
|
|
|
canvas.dataset.threeSceneMode = preview.sceneMode;
|
|
|
|
|
canvas.dataset.threeCameraControls = preview.cameraControls ? "orbit-pan-zoom" : "none";
|
|
|
|
|
canvas.dataset.threeProgramPreviewSource = previewSourceMode(state);
|
|
|
|
|
canvas.dataset.threeToolExecutionMarker = preview.toolExecutionMarker ? "true" : "false";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildEnvelope() {
|
|
|
|
|
const geometry = new THREE.BoxGeometry(5.9, 4.25, 2.7);
|
|
|
|
|
const edges = new THREE.EdgesGeometry(geometry);
|
|
|
|
|
const line = new THREE.LineSegments(edges, new THREE.LineBasicMaterial({ color: 0xe43a35 }));
|
|
|
|
|
line.position.z = 0.2;
|
|
|
|
|
return line;
|
|
|
|
|
function createLine(color, opacity) {
|
|
|
|
|
return new THREE.Line(
|
|
|
|
|
EMPTY_GEOMETRY.clone(),
|
|
|
|
|
new THREE.LineBasicMaterial({
|
|
|
|
|
color,
|
|
|
|
|
transparent: opacity < 1,
|
|
|
|
|
opacity,
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildToolpath() {
|
|
|
|
|
function updateToolpathPreview(preview, state) {
|
|
|
|
|
const previewPoints = buildProgramPreviewPoints(state);
|
|
|
|
|
const executedPoints = buildExecutedProgramPoints(state, previewPoints);
|
|
|
|
|
const rapidPoints = buildRapidPreviewPoints(state);
|
|
|
|
|
const toolPosition = executionToolPosition(state, previewPoints);
|
|
|
|
|
|
|
|
|
|
updateLineGeometry(preview.previewPath, previewPoints);
|
|
|
|
|
updateLineGeometry(preview.executedPath, executedPoints);
|
|
|
|
|
updateLineGeometry(preview.rapidPath, rapidPoints);
|
|
|
|
|
updateToolExecutionMarker(preview, state, toolPosition);
|
|
|
|
|
|
|
|
|
|
const cameraRevision = state.preview.cameraRevision ?? 0;
|
|
|
|
|
if (
|
|
|
|
|
preview.lastSelectedView !== state.preview.selectedView ||
|
|
|
|
|
preview.lastCameraRevision !== cameraRevision
|
|
|
|
|
) {
|
|
|
|
|
resetCamera(preview, state.preview.selectedView);
|
|
|
|
|
preview.lastSelectedView = state.preview.selectedView;
|
|
|
|
|
preview.lastCameraRevision = cameraRevision;
|
|
|
|
|
} else {
|
|
|
|
|
applyCameraControls(preview.controls);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updateToolExecutionMarker(preview, state, toolPosition) {
|
|
|
|
|
if (!toolPosition) {
|
|
|
|
|
preview.currentToolhead.set(0, 0, 0);
|
|
|
|
|
preview.toolMarker.visible = false;
|
|
|
|
|
preview.toolAxis.visible = false;
|
|
|
|
|
updateLineGeometry(preview.toolAxis, []);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const vector = toToolVector(state.toolAxisVector);
|
|
|
|
|
preview.currentToolhead.copy(toolPosition);
|
|
|
|
|
preview.toolMarker.visible = true;
|
|
|
|
|
preview.toolMarker.position.copy(toolPosition);
|
|
|
|
|
preview.toolAxis.visible = true;
|
|
|
|
|
updateLineGeometry(preview.toolAxis, [
|
|
|
|
|
toolPosition,
|
|
|
|
|
toolPosition.clone().add(vector.multiplyScalar(0.7)),
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updateLineGeometry(line, points) {
|
|
|
|
|
line.visible = points.length > 0;
|
|
|
|
|
line.geometry.dispose();
|
|
|
|
|
line.geometry = points.length > 0
|
|
|
|
|
? new THREE.BufferGeometry().setFromPoints(points)
|
|
|
|
|
: EMPTY_GEOMETRY.clone();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const pointCount = normalizePathPointCount(state.preview.pathPoints);
|
|
|
|
|
if (pointCount === 0) return [];
|
|
|
|
|
return buildFixturePreviewPoints(pointCount, toPreviewVector(state.tcpPose));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildExecutedProgramPoints(state, previewPoints) {
|
|
|
|
|
if (state.preview.pathPoints === 0 || previewPoints.length === 0) return [];
|
|
|
|
|
const samples = state.programExecutionTiming?.samples;
|
|
|
|
|
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)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const motionIndex = clamp(Math.round(Number(state.programExecutionMotionIndex || 0)), 0, previewPoints.length - 1);
|
|
|
|
|
return previewPoints.slice(0, motionIndex + 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildRapidPreviewPoints(state) {
|
|
|
|
|
const motion = state.programExecution?.motion;
|
|
|
|
|
if (!Array.isArray(motion) || state.preview.pathPoints === 0) return [];
|
|
|
|
|
return limitPoints(
|
|
|
|
|
motion
|
|
|
|
|
.filter((event) => event.type === "STRAIGHT_TRAVERSE")
|
|
|
|
|
.map((event) => vectorFromAxes(event.axes)),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildFixturePreviewPoints(pointCount, tcpPosition) {
|
|
|
|
|
const points = [];
|
|
|
|
|
for (let index = 0; index < 72; index += 1) {
|
|
|
|
|
const t = index / 71;
|
|
|
|
|
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;
|
|
|
|
|
points.push(new THREE.Vector3(x, y, z));
|
|
|
|
|
}
|
|
|
|
|
const geometry = new THREE.BufferGeometry().setFromPoints(points);
|
|
|
|
|
return new THREE.Line(geometry, new THREE.LineBasicMaterial({ color: 0xffffff }));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updateMachinePose(preview, state) {
|
|
|
|
|
const tcp = state.tcpPose;
|
|
|
|
|
const tool = state.toolAxisVector;
|
|
|
|
|
const tcpPosition = new THREE.Vector3(
|
|
|
|
|
clamp(tcp.x * 0.035, -2.7, 2.7),
|
|
|
|
|
clamp(tcp.y * 0.035, -2.0, 2.0),
|
|
|
|
|
clamp(tcp.z * 0.04 + 0.35, -1.1, 1.9),
|
|
|
|
|
);
|
|
|
|
|
const toolAxisVector = new THREE.Vector3(tool.x, tool.y, tool.z || 1).normalize();
|
|
|
|
|
|
|
|
|
|
preview.toolGroup.position.copy(tcpPosition);
|
|
|
|
|
preview.toolGroup.quaternion.setFromUnitVectors(new THREE.Vector3(0, 0, 1), toolAxisVector);
|
|
|
|
|
preview.toolAxis.geometry.setFromPoints([
|
|
|
|
|
new THREE.Vector3(0, 0, 0),
|
|
|
|
|
toolAxisVector.clone().multiplyScalar(state.rtcpState === "on" ? 1.2 : 0.85),
|
|
|
|
|
]);
|
|
|
|
|
preview.tableGroup.rotation.x = THREE.MathUtils.degToRad(state.axisPose.a);
|
|
|
|
|
preview.tableGroup.rotation.z = THREE.MathUtils.degToRad(state.axisPose.c);
|
|
|
|
|
setCameraView(preview.camera, state.preview.selectedView);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setCameraView(camera, selectedView) {
|
|
|
|
|
if (selectedView === "x") {
|
|
|
|
|
camera.position.set(6, 0.02, 0.45);
|
|
|
|
|
} else if (selectedView === "y") {
|
|
|
|
|
camera.position.set(0.02, -6, 0.7);
|
|
|
|
|
} else if (selectedView === "z") {
|
|
|
|
|
camera.position.set(0.01, -0.02, 6.6);
|
|
|
|
|
} else {
|
|
|
|
|
camera.position.set(3.4, -5.0, 3.2);
|
|
|
|
|
if (points.length > 0 && tcpPosition) {
|
|
|
|
|
points[points.length - 1] = tcpPosition.clone();
|
|
|
|
|
}
|
|
|
|
|
camera.lookAt(0, 0, 0);
|
|
|
|
|
camera.updateProjectionMatrix();
|
|
|
|
|
return points;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function executionToolPosition(state, previewPoints) {
|
|
|
|
|
if (state.preview.pathPoints === 0) return null;
|
|
|
|
|
const feedbackAxes = state.programRuntimeFeedback?.axisPose || state.programRuntimeFeedback;
|
|
|
|
|
if (feedbackAxes && hasLinearAxes(feedbackAxes)) return vectorFromAxes(feedbackAxes);
|
|
|
|
|
if (hasLinearAxes(state.axisPose)) return vectorFromAxes(state.axisPose);
|
|
|
|
|
return previewPoints.at(-1) || null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function vectorFromAxes(axes = {}) {
|
|
|
|
|
return new THREE.Vector3(
|
|
|
|
|
scaleLinearAxis(axes.x),
|
|
|
|
|
scaleLinearAxis(axes.y),
|
|
|
|
|
scaleZAxis(axes.z),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toPreviewVector(pose = {}) {
|
|
|
|
|
return new THREE.Vector3(
|
|
|
|
|
scaleLinearAxis(pose.x),
|
|
|
|
|
scaleLinearAxis(pose.y),
|
|
|
|
|
scaleZAxis(pose.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 toToolVector(vector = {}) {
|
|
|
|
|
return new THREE.Vector3(
|
|
|
|
|
Number(vector.x) || 0,
|
|
|
|
|
Number(vector.y) || 0,
|
|
|
|
|
Number(vector.z) || 1,
|
|
|
|
|
).normalize();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function hasLinearAxes(value = {}) {
|
|
|
|
|
return ["x", "y", "z"].some((axis) => Number.isFinite(Number(value[axis])));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function limitPoints(points) {
|
|
|
|
|
if (points.length <= MAX_TOOLPATH_POINTS) return points;
|
|
|
|
|
const stride = Math.ceil(points.length / MAX_TOOLPATH_POINTS);
|
|
|
|
|
const sampled = points.filter((_, index) => index % stride === 0);
|
|
|
|
|
const last = points.at(-1);
|
|
|
|
|
if (last && sampled.at(-1) !== last) sampled.push(last);
|
|
|
|
|
return sampled;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function previewSourceMode(state) {
|
|
|
|
|
if (state.programExecution?.sourceMode) return state.programExecution.sourceMode;
|
|
|
|
|
return state.programExecutionSourceMode || "fixture-line-playback";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function drawFallbackPolyline(ctx, points, cx, cy, scale) {
|
|
|
|
|
for (let index = 0; index < points.length; index += 1) {
|
|
|
|
|
const point = points[index];
|
|
|
|
|
const x = cx + point.x * scale;
|
|
|
|
|
const y = cy - point.y * scale;
|
|
|
|
|
if (index === 0) ctx.moveTo(x, y);
|
|
|
|
|
else ctx.lineTo(x, y);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createToolpathCameraControls(canvas, camera, renderFrame) {
|
|
|
|
|
const controls = {
|
|
|
|
|
enabled: true,
|
|
|
|
|
camera,
|
|
|
|
|
target: new THREE.Vector3(),
|
|
|
|
|
theta: CAMERA_PRESETS.iso.theta,
|
|
|
|
|
phi: CAMERA_PRESETS.iso.phi,
|
|
|
|
|
radius: CAMERA_PRESETS.iso.radius,
|
|
|
|
|
pointerMode: null,
|
|
|
|
|
pointers: new Map(),
|
|
|
|
|
lastPointer: null,
|
|
|
|
|
lastPinchCenter: null,
|
|
|
|
|
lastPinchDistance: 0,
|
|
|
|
|
renderFrame,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
canvas.addEventListener("contextmenu", (event) => event.preventDefault());
|
|
|
|
|
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);
|
|
|
|
|
applyCameraControls(controls);
|
|
|
|
|
controls.renderFrame();
|
|
|
|
|
}, { passive: false });
|
|
|
|
|
canvas.addEventListener("pointerdown", (event) => {
|
|
|
|
|
safelySetPointerCapture(canvas, event.pointerId);
|
|
|
|
|
controls.pointers.set(event.pointerId, { x: event.clientX, y: event.clientY });
|
|
|
|
|
controls.lastPointer = { x: event.clientX, y: event.clientY };
|
|
|
|
|
controls.pointerMode = event.button === 1 || event.button === 2 || event.shiftKey ? "pan" : "rotate";
|
|
|
|
|
if (controls.pointers.size === 2) {
|
|
|
|
|
controls.pointerMode = "pinch";
|
|
|
|
|
controls.lastPinchDistance = getPointerDistance(controls.pointers);
|
|
|
|
|
controls.lastPinchCenter = getPointerCenter(controls.pointers);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
canvas.addEventListener("pointermove", (event) => {
|
|
|
|
|
if (!controls.pointers.has(event.pointerId)) return;
|
|
|
|
|
const previous = controls.pointers.get(event.pointerId);
|
|
|
|
|
controls.pointers.set(event.pointerId, { x: event.clientX, y: event.clientY });
|
|
|
|
|
|
|
|
|
|
if (controls.pointerMode === "pinch" && controls.pointers.size >= 2) {
|
|
|
|
|
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);
|
|
|
|
|
if (controls.lastPinchCenter) {
|
|
|
|
|
panCamera(
|
|
|
|
|
controls,
|
|
|
|
|
center.x - controls.lastPinchCenter.x,
|
|
|
|
|
center.y - controls.lastPinchCenter.y,
|
|
|
|
|
canvas,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
controls.lastPinchDistance = distance;
|
|
|
|
|
controls.lastPinchCenter = center;
|
|
|
|
|
applyCameraControls(controls);
|
|
|
|
|
controls.renderFrame();
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const dx = event.clientX - previous.x;
|
|
|
|
|
const dy = event.clientY - previous.y;
|
|
|
|
|
if (controls.pointerMode === "pan") {
|
|
|
|
|
panCamera(controls, dx, dy, canvas);
|
|
|
|
|
} else {
|
|
|
|
|
controls.theta -= dx * 0.006;
|
|
|
|
|
controls.phi = clamp(controls.phi - dy * 0.006, 0.001, Math.PI - 0.001);
|
|
|
|
|
}
|
|
|
|
|
applyCameraControls(controls);
|
|
|
|
|
controls.renderFrame();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const releasePointer = (event) => {
|
|
|
|
|
controls.pointers.delete(event.pointerId);
|
|
|
|
|
safelyReleasePointerCapture(canvas, event.pointerId);
|
|
|
|
|
if (controls.pointers.size === 0) {
|
|
|
|
|
controls.pointerMode = null;
|
|
|
|
|
controls.lastPointer = null;
|
|
|
|
|
controls.lastPinchCenter = null;
|
|
|
|
|
controls.lastPinchDistance = 0;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
canvas.addEventListener("pointerup", releasePointer);
|
|
|
|
|
canvas.addEventListener("pointercancel", releasePointer);
|
|
|
|
|
return controls;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function safelySetPointerCapture(canvas, pointerId) {
|
|
|
|
|
try {
|
|
|
|
|
canvas.setPointerCapture?.(pointerId);
|
|
|
|
|
} catch {
|
|
|
|
|
// Synthetic browser-smoke events do not always have an active pointer capture target.
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function safelyReleasePointerCapture(canvas, pointerId) {
|
|
|
|
|
try {
|
|
|
|
|
canvas.releasePointerCapture?.(pointerId);
|
|
|
|
|
} catch {
|
|
|
|
|
// Matching setPointerCapture guard for synthetic and cancelled pointer streams.
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizePathPointCount(value) {
|
|
|
|
|
const requestedCount = Number(value);
|
|
|
|
|
return Number.isFinite(requestedCount) && requestedCount > 0
|
|
|
|
|
? Math.max(2, Math.min(Math.round(requestedCount), 512))
|
|
|
|
|
: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getPointerDistance(pointers) {
|
|
|
|
|
const values = [...pointers.values()];
|
|
|
|
|
if (values.length < 2) return 0;
|
|
|
|
|
return Math.hypot(values[0].x - values[1].x, values[0].y - values[1].y);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getPointerCenter(pointers) {
|
|
|
|
|
const values = [...pointers.values()];
|
|
|
|
|
if (values.length < 2) return null;
|
|
|
|
|
return {
|
|
|
|
|
x: (values[0].x + values[1].x) / 2,
|
|
|
|
|
y: (values[0].y + values[1].y) / 2,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function panCamera(controls, dx, dy, canvas) {
|
|
|
|
|
const cameraDirection = new THREE.Vector3();
|
|
|
|
|
controls.camera.getWorldDirection(cameraDirection);
|
|
|
|
|
const right = new THREE.Vector3().crossVectors(cameraDirection, controls.camera.up).normalize();
|
|
|
|
|
const up = new THREE.Vector3().crossVectors(right, cameraDirection).normalize();
|
|
|
|
|
const speed = controls.radius / Math.max(canvas.clientWidth, canvas.clientHeight, 1);
|
|
|
|
|
controls.target.addScaledVector(right, -dx * speed);
|
|
|
|
|
controls.target.addScaledVector(up, dy * speed);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resetCamera(preview, selectedView) {
|
|
|
|
|
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);
|
|
|
|
|
applyCameraControls(preview.controls);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function applyCameraControls(controls) {
|
|
|
|
|
const sinPhiRadius = Math.sin(controls.phi) * controls.radius;
|
|
|
|
|
controls.camera.position.set(
|
|
|
|
|
controls.target.x + sinPhiRadius * Math.cos(controls.theta),
|
|
|
|
|
controls.target.y + sinPhiRadius * Math.sin(controls.theta),
|
|
|
|
|
controls.target.z + Math.cos(controls.phi) * controls.radius,
|
|
|
|
|
);
|
|
|
|
|
controls.camera.lookAt(controls.target);
|
|
|
|
|
controls.camera.updateProjectionMatrix();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resizeRenderer(preview) {
|
|
|
|
|
|