Add RTCP simulation QA updates
This commit is contained in:
@@ -25,6 +25,12 @@ let attachedKinematicsProfile = store.getState().machineProfile;
|
||||
let attachedIniProfile = store.getState().machineProfile;
|
||||
let attachedMachineFileProfile = store.getState().machineProfile;
|
||||
let machineFileSeedPromise = machineFileSeedReady;
|
||||
const autoLoadedProgramProfiles = new Set();
|
||||
|
||||
Promise.allSettled([interpreterRuntimeReady, machineFileSeedReady]).then(() => {
|
||||
ensureDefaultLinuxCncProgramPreview(store).catch(() => {});
|
||||
});
|
||||
|
||||
store.subscribe((state) => {
|
||||
if (state.machineProfile !== attachedIniProfile) {
|
||||
attachedIniProfile = state.machineProfile;
|
||||
@@ -32,12 +38,20 @@ store.subscribe((state) => {
|
||||
}
|
||||
if (state.machineProfile !== attachedKinematicsProfile) {
|
||||
attachedKinematicsProfile = state.machineProfile;
|
||||
attachDefaultKinematicsRuntime(store, state.profile.kinematicsModuleId || state.machineProfile).catch(() => {});
|
||||
const profileId = state.machineProfile;
|
||||
attachDefaultKinematicsRuntime(store, state.profile.kinematicsModuleId || state.machineProfile)
|
||||
.then(() => store.refreshKinematicsFrame({
|
||||
operatorMessage: `LinuxCNC kinematics ${profileId} profile frame refreshed`,
|
||||
}))
|
||||
.catch(() => {});
|
||||
}
|
||||
if (state.machineProfile !== attachedMachineFileProfile) {
|
||||
attachedMachineFileProfile = state.machineProfile;
|
||||
machineFileSeedPromise = ensureMachineFilesForProfile(store);
|
||||
window.webRtcp5AxisSimulation.machineFileSeedReady = machineFileSeedPromise;
|
||||
machineFileSeedPromise.finally(() => {
|
||||
ensureDefaultLinuxCncProgramPreview(store).catch(() => {});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -86,6 +100,58 @@ async function ensureMachineFilesForProfile(store) {
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureDefaultLinuxCncProgramPreview(store) {
|
||||
const state = store.getState();
|
||||
if (!state.interpreterRuntime?.loaded) return null;
|
||||
if (state.machineFileStaging?.status !== "staged" || !state.machineFileStaging?.gcodeSources?.length) return null;
|
||||
|
||||
const profileId = state.machineProfile;
|
||||
if (autoLoadedProgramProfiles.has(profileId)) return state.programExecution;
|
||||
|
||||
const defaultSource = selectDefaultLinuxCncSource(state);
|
||||
if (!defaultSource) return null;
|
||||
|
||||
autoLoadedProgramProfiles.add(profileId);
|
||||
store.dispatch({ type: "LOAD_LINUXCNC_GCODE_SOURCE", sourceRel: defaultSource.sourceRel });
|
||||
await waitForStore(
|
||||
store,
|
||||
(nextState) => !nextState.interpreterExecutionPending && nextState.programExecution?.sourceMode === "linuxcnc-interpreter-wasm",
|
||||
15000,
|
||||
).catch((error) => {
|
||||
autoLoadedProgramProfiles.delete(profileId);
|
||||
throw error;
|
||||
});
|
||||
return store.getState().programExecution;
|
||||
}
|
||||
|
||||
function selectDefaultLinuxCncSource(state) {
|
||||
const sources = state.machineFileStaging?.gcodeSources || [];
|
||||
const preferredFilename = `${state.machineProfile}_switchkins.ngc`;
|
||||
return sources.find((source) => source.filename === preferredFilename)
|
||||
|| sources.find((source) => source.filename.includes(state.machineProfile))
|
||||
|| sources[0]
|
||||
|| null;
|
||||
}
|
||||
|
||||
function waitForStore(store, predicate, timeoutMs = 10000) {
|
||||
const initialState = store.getState();
|
||||
if (predicate(initialState)) {
|
||||
return Promise.resolve(initialState);
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeoutId = window.setTimeout(() => {
|
||||
unsubscribe();
|
||||
reject(new Error(`Timed out after ${timeoutMs}ms waiting for store state`));
|
||||
}, timeoutMs);
|
||||
const unsubscribe = store.subscribe((state) => {
|
||||
if (!predicate(state)) return;
|
||||
window.clearTimeout(timeoutId);
|
||||
unsubscribe();
|
||||
resolve(state);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function attachDefaultKinematicsRuntime(store, moduleId = "xyzac-trt") {
|
||||
const sdkModuleUrls = [
|
||||
new URL("../../../wasm-port/runtime/sdk/src/linuxcnc-kinematics.js", import.meta.url).href,
|
||||
|
||||
@@ -193,11 +193,16 @@ export function normalizeTaskHalStatus(status = {}) {
|
||||
b: Number(axis.b ?? halPins["axis.4.pos-cmd"]?.value ?? 0),
|
||||
c: Number(axis.c ?? halPins["axis.5.pos-cmd"]?.value ?? 0),
|
||||
},
|
||||
axisPoseFrame: isJogMotion(motion) ? "task-local" : "work",
|
||||
currentVelocity: Number(motion.currentVel || motion.currentVelocity || 0) * 60,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function isJogMotion(motion = {}) {
|
||||
return Number(motion.motionType) === 3 || Number(motion.teleopMode) === 1 || motion.teleopMode === true;
|
||||
}
|
||||
|
||||
function sessionFileDescriptor(file) {
|
||||
return {
|
||||
sourceRel: file.sourceRel,
|
||||
|
||||
@@ -82,6 +82,7 @@ const initialState = {
|
||||
},
|
||||
sourceMode: "fixture-ui-only",
|
||||
frameSourceMode: "fixture-ui-only",
|
||||
desiredFrameSourceMode: "fixture-ui-only",
|
||||
machine: {
|
||||
powerOn: false,
|
||||
estopActive: false,
|
||||
@@ -142,6 +143,7 @@ const initialState = {
|
||||
taskHalExecutionPending: false,
|
||||
taskHalExecutionSequence: 0,
|
||||
taskHalFallbackReason: null,
|
||||
pendingJogCommand: null,
|
||||
interpreterExecutionPending: false,
|
||||
interpreterExecutionSequence: 0,
|
||||
machineFileExecution: null,
|
||||
@@ -230,7 +232,7 @@ export function createSimulationStore(seed = {}) {
|
||||
activeLine: seed.activeLine || initialState.activeLine,
|
||||
kinsType: seedKinsType,
|
||||
rtcpEnabled: seedRtcpState === "on" || seedKinsType === "tcp-xyzac",
|
||||
sourceMode: seed.sourceMode || seed.frameSourceMode || initialState.sourceMode,
|
||||
sourceMode: seed.desiredFrameSourceMode || seed.sourceMode || seed.frameSourceMode || initialState.sourceMode,
|
||||
});
|
||||
let state = {
|
||||
...initialState,
|
||||
@@ -268,6 +270,7 @@ export function createSimulationStore(seed = {}) {
|
||||
...next,
|
||||
sourceMode: frame.sourceMode,
|
||||
frameSourceMode: frame.sourceMode,
|
||||
desiredFrameSourceMode: next.desiredFrameSourceMode || frame.sourceMode,
|
||||
axisPose: frame.axisPose,
|
||||
jointPose: frame.jointPose,
|
||||
tcpPose: frame.tcpPose,
|
||||
@@ -321,8 +324,7 @@ export function createSimulationStore(seed = {}) {
|
||||
kinematicsExecutionContext: runtime?.executionContext || (runtime?.loaded ? "direct" : "none"),
|
||||
linuxCncBoundaryAdapter: adapter,
|
||||
linuxCncBoundaryReadiness: createLinuxCncBoundaryReadiness(adapter),
|
||||
sourceMode: runtime?.loaded ? "source-derived-kinematics-wasm" : "fixture-ui-only",
|
||||
frameSourceMode: runtime?.loaded ? "source-derived-kinematics-wasm" : "fixture-ui-only",
|
||||
desiredFrameSourceMode: runtime?.loaded ? "source-derived-kinematics-wasm" : "fixture-ui-only",
|
||||
operatorMessage: runtime?.loaded
|
||||
? `LinuxCNC kinematics ${runtime.moduleId} ready`
|
||||
: "LinuxCNC kinematics runtime missing",
|
||||
@@ -803,6 +805,7 @@ export function createSimulationStore(seed = {}) {
|
||||
setState({
|
||||
taskHalFallbackReason: action.error,
|
||||
taskHalExecutionPending: false,
|
||||
pendingJogCommand: null,
|
||||
operatorMessage: `task/HAL fallback: ${action.error}`,
|
||||
});
|
||||
break;
|
||||
@@ -833,6 +836,7 @@ export function createSimulationStore(seed = {}) {
|
||||
setState({
|
||||
sourceMode: action.sourceMode,
|
||||
frameSourceMode: action.sourceMode,
|
||||
desiredFrameSourceMode: action.sourceMode,
|
||||
operatorMessage: `frame source ${action.sourceMode}`,
|
||||
});
|
||||
break;
|
||||
@@ -971,6 +975,13 @@ export function createSimulationStore(seed = {}) {
|
||||
const direction = Number(action.direction || 1);
|
||||
const increment = Number(action.increment || state.machine.jogIncrement);
|
||||
if (state.taskHalRuntime?.loaded) {
|
||||
const pendingJogCommand = {
|
||||
axis,
|
||||
direction,
|
||||
increment,
|
||||
basePose: { ...state.axisPose },
|
||||
createdAtLine: state.activeLine,
|
||||
};
|
||||
runTaskHalCommandSequence([
|
||||
{
|
||||
type: "EMC_JOG_INCR",
|
||||
@@ -978,7 +989,10 @@ export function createSimulationStore(seed = {}) {
|
||||
distance: direction * increment,
|
||||
velocity: Number(action.velocity || 60),
|
||||
},
|
||||
], { operatorMessage: `task/HAL jog ${axis.toUpperCase()} ${direction > 0 ? "+" : "-"}${increment}` }).catch(() => {});
|
||||
], {
|
||||
pendingJogCommand,
|
||||
operatorMessage: `task/HAL jog ${axis.toUpperCase()} ${direction > 0 ? "+" : "-"}${increment}`,
|
||||
}).catch(() => {});
|
||||
break;
|
||||
}
|
||||
setState({
|
||||
@@ -1371,6 +1385,9 @@ export function createSimulationStore(seed = {}) {
|
||||
};
|
||||
|
||||
const refreshAsyncKinematicsFrame = async ({ operatorMessage = state.operatorMessage } = {}) => {
|
||||
if (state.desiredFrameSourceMode !== "source-derived-kinematics-wasm") {
|
||||
return state.rtcpFrame;
|
||||
}
|
||||
if (!state.kinematicsRuntime?.loaded || !isAsyncKinematicsRuntime(state.kinematicsRuntime)) {
|
||||
return state.rtcpFrame;
|
||||
}
|
||||
@@ -1381,44 +1398,58 @@ export function createSimulationStore(seed = {}) {
|
||||
asyncFrameRefreshSequence: sequence,
|
||||
};
|
||||
notify();
|
||||
await switchKinematicsRuntimeForState(state);
|
||||
const frameSource = await state.kinematicsRuntime.frameForJoints(
|
||||
jointsFromAxisPose(state.axisPose, state.profile),
|
||||
{ jointCount: state.kinematicsRuntime.jointCount || 5 },
|
||||
);
|
||||
if (state.asyncFrameRefreshSequence !== sequence) {
|
||||
try {
|
||||
await switchKinematicsRuntimeForState(state);
|
||||
const frameSource = await state.kinematicsRuntime.frameForJoints(
|
||||
jointsFromAxisPose(state.axisPose, state.profile),
|
||||
{ jointCount: state.kinematicsRuntime.jointCount || 5 },
|
||||
);
|
||||
if (state.asyncFrameRefreshSequence !== sequence) {
|
||||
return state.rtcpFrame;
|
||||
}
|
||||
const frame = buildRtcpFrame({
|
||||
axisPose: state.axisPose,
|
||||
activeLine: state.activeLine,
|
||||
kinsType: state.kinsType,
|
||||
rtcpEnabled: state.rtcpState === "on" || state.kinsType.startsWith("tcp-"),
|
||||
sourceMode: "source-derived-kinematics-wasm",
|
||||
profile: state.profile,
|
||||
linuxCncKinematicsResult: frameSource,
|
||||
});
|
||||
const nextState = {
|
||||
...state,
|
||||
sourceMode: "source-derived-kinematics-wasm",
|
||||
frameSourceMode: "source-derived-kinematics-wasm",
|
||||
desiredFrameSourceMode: "source-derived-kinematics-wasm",
|
||||
axisPose: frame.axisPose,
|
||||
jointPose: frame.jointPose,
|
||||
tcpPose: frame.tcpPose,
|
||||
toolAxisVector: frame.toolAxisVector,
|
||||
rtcpState: frame.rtcpState,
|
||||
rtcpFrame: frame,
|
||||
lastKinematicsResult: frameSource,
|
||||
dro: buildDroFromFrame(frame, state.programRuntimeFeedback),
|
||||
asyncFrameRefreshPending: false,
|
||||
operatorMessage,
|
||||
};
|
||||
state = {
|
||||
...nextState,
|
||||
fullExecutionBoundary: createFullLinuxCncExecutionBoundary(nextState),
|
||||
};
|
||||
notify();
|
||||
return frame;
|
||||
} catch (error) {
|
||||
if (state.asyncFrameRefreshSequence !== sequence) {
|
||||
return state.rtcpFrame;
|
||||
}
|
||||
state = {
|
||||
...state,
|
||||
asyncFrameRefreshPending: false,
|
||||
operatorMessage: `LinuxCNC kinematics refresh failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
};
|
||||
notify();
|
||||
return state.rtcpFrame;
|
||||
}
|
||||
const frame = buildRtcpFrame({
|
||||
axisPose: state.axisPose,
|
||||
activeLine: state.activeLine,
|
||||
kinsType: state.kinsType,
|
||||
rtcpEnabled: state.rtcpState === "on" || state.kinsType.startsWith("tcp-"),
|
||||
sourceMode: "source-derived-kinematics-wasm",
|
||||
profile: state.profile,
|
||||
linuxCncKinematicsResult: frameSource,
|
||||
});
|
||||
const nextState = {
|
||||
...state,
|
||||
sourceMode: frame.sourceMode,
|
||||
frameSourceMode: frame.sourceMode,
|
||||
axisPose: frame.axisPose,
|
||||
jointPose: frame.jointPose,
|
||||
tcpPose: frame.tcpPose,
|
||||
toolAxisVector: frame.toolAxisVector,
|
||||
rtcpState: frame.rtcpState,
|
||||
rtcpFrame: frame,
|
||||
lastKinematicsResult: frameSource,
|
||||
dro: buildDroFromFrame(frame, state.programRuntimeFeedback),
|
||||
asyncFrameRefreshPending: false,
|
||||
operatorMessage,
|
||||
};
|
||||
state = {
|
||||
...nextState,
|
||||
fullExecutionBoundary: createFullLinuxCncExecutionBoundary(nextState),
|
||||
};
|
||||
notify();
|
||||
return frame;
|
||||
};
|
||||
|
||||
const saveSession = async (options = {}) => {
|
||||
@@ -1543,6 +1574,7 @@ export function createSimulationStore(seed = {}) {
|
||||
taskPeriodNs = 10000000,
|
||||
servoPeriodNs = 1000000,
|
||||
operatorMessage = "task/HAL command complete",
|
||||
pendingJogCommand = null,
|
||||
} = {}) => {
|
||||
if (!state.taskHalRuntime?.loaded) {
|
||||
throw new Error("LinuxCNC task/HAL runtime not attached");
|
||||
@@ -1551,6 +1583,7 @@ export function createSimulationStore(seed = {}) {
|
||||
setState({
|
||||
taskHalExecutionPending: true,
|
||||
taskHalExecutionSequence: sequence,
|
||||
pendingJogCommand,
|
||||
operatorMessage: "LinuxCNC task/HAL command running",
|
||||
});
|
||||
try {
|
||||
@@ -1614,13 +1647,21 @@ export function createSimulationStore(seed = {}) {
|
||||
};
|
||||
|
||||
const scheduleAsyncKinematicsRefresh = () => {
|
||||
if (state.asyncFrameRefreshPending) return null;
|
||||
if (state.desiredFrameSourceMode !== "source-derived-kinematics-wasm") return null;
|
||||
if (!state.kinematicsRuntime?.loaded || !isAsyncKinematicsRuntime(state.kinematicsRuntime)) return null;
|
||||
if (state.frameSourceMode !== "source-derived-kinematics-wasm") return null;
|
||||
const frame = state.rtcpFrame;
|
||||
if (
|
||||
frame?.sourceMode === "source-derived-kinematics-wasm" &&
|
||||
frame.readiness?.linuxCncKinematicsReady === true &&
|
||||
frame.activeLine === state.activeLine
|
||||
frame.activeLine === state.activeLine &&
|
||||
frame.kinsType === state.kinsType &&
|
||||
frame.axisPose?.x === state.axisPose.x &&
|
||||
frame.axisPose?.y === state.axisPose.y &&
|
||||
frame.axisPose?.z === state.axisPose.z &&
|
||||
frame.axisPose?.a === state.axisPose.a &&
|
||||
frame.axisPose?.b === state.axisPose.b &&
|
||||
frame.axisPose?.c === state.axisPose.c
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
@@ -1645,10 +1686,9 @@ export function createSimulationStore(seed = {}) {
|
||||
}
|
||||
|
||||
function buildFrameForState(state, patch = {}) {
|
||||
const requestedSourceMode = state.frameSourceMode || state.sourceMode;
|
||||
const requestedSourceMode = state.desiredFrameSourceMode || state.frameSourceMode || state.sourceMode;
|
||||
let linuxCncKinematicsResult = patch.lastKinematicsResult || null;
|
||||
let sourceMode = requestedSourceMode;
|
||||
let operatorMessage = state.operatorMessage;
|
||||
|
||||
if (requestedSourceMode === "source-derived-kinematics-wasm") {
|
||||
if (state.kinematicsRuntime?.loaded && !isAsyncKinematicsRuntime(state.kinematicsRuntime)) {
|
||||
@@ -1657,10 +1697,12 @@ function buildFrameForState(state, patch = {}) {
|
||||
jointsFromAxisPose(state.axisPose, state.profile),
|
||||
{ jointCount: state.kinematicsRuntime.jointCount || 5 },
|
||||
);
|
||||
} else if (state.kinematicsRuntime?.loaded && isAsyncKinematicsRuntime(state.kinematicsRuntime)) {
|
||||
sourceMode = "fixture-ui-only";
|
||||
linuxCncKinematicsResult = null;
|
||||
} else {
|
||||
sourceMode = "fixture-ui-only";
|
||||
linuxCncKinematicsResult = null;
|
||||
operatorMessage = "LinuxCNC kinematics runtime missing; using fixture frame";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1674,10 +1716,6 @@ function buildFrameForState(state, patch = {}) {
|
||||
linuxCncKinematicsResult,
|
||||
});
|
||||
|
||||
if (operatorMessage !== state.operatorMessage) {
|
||||
state.operatorMessage = operatorMessage;
|
||||
}
|
||||
|
||||
return {
|
||||
frame,
|
||||
lastKinematicsResult: linuxCncKinematicsResult,
|
||||
@@ -1743,11 +1781,8 @@ function applyTaskHalStatusPatch(state, status, operatorMessage) {
|
||||
const taskMode = normalizeLinuxCncTaskMode(ui.taskMode || task.mode || state.machine.mode);
|
||||
const interpState = normalizeTaskHalInterpState(ui.interpState || task.interpState);
|
||||
const activeLine = state.programStartLine + Math.max(Number(ui.activeLine || 1) - 1, 0);
|
||||
const kinsType = kinsTypeFromSwitchkinsTypeValue(state, ui.switchkinsType);
|
||||
const axisPose = clampAxisPoseToProfile({
|
||||
...state.axisPose,
|
||||
...ui.axisPose,
|
||||
}, state.profile);
|
||||
const kinsType = resolveTaskHalKinsType(state, status, activeLine);
|
||||
const axisPose = resolveTaskHalAxisPose(state, status);
|
||||
const currentVelocity = Number.isFinite(ui.currentVelocity) && ui.currentVelocity > 0
|
||||
? ui.currentVelocity
|
||||
: state.feed.currentVelocity;
|
||||
@@ -1772,11 +1807,14 @@ function applyTaskHalStatusPatch(state, status, operatorMessage) {
|
||||
taskHalStatus: status,
|
||||
taskHalExecutionPending: false,
|
||||
taskHalFallbackReason: null,
|
||||
pendingJogCommand: null,
|
||||
activeLine,
|
||||
axisPose,
|
||||
kinsType,
|
||||
rtcpState: rtcpStateFromKinsType(kinsType),
|
||||
programExecutionSourceMode: "linuxcnc-task-motion-hal-wasm",
|
||||
programExecutionSourceMode: state.programExecution
|
||||
? state.programExecutionSourceMode
|
||||
: "linuxcnc-task-motion-hal-wasm",
|
||||
machine: {
|
||||
...state.machine,
|
||||
powerOn: taskState === "on",
|
||||
@@ -1797,6 +1835,93 @@ function applyTaskHalStatusPatch(state, status, operatorMessage) {
|
||||
};
|
||||
}
|
||||
|
||||
function resolveTaskHalKinsType(state, status, activeLine) {
|
||||
const ui = status?.ui || {};
|
||||
const numeric = Number(ui.switchkinsType);
|
||||
if (Number.isFinite(numeric) && numeric !== 0) {
|
||||
return kinsTypeFromSwitchkinsTypeValue(state, numeric);
|
||||
}
|
||||
|
||||
const programKinsType = kinsTypeFromProgramActiveLine(state, activeLine);
|
||||
if (programKinsType) {
|
||||
return programKinsType;
|
||||
}
|
||||
|
||||
return kinsTypeFromSwitchkinsTypeValue(state, ui.switchkinsType);
|
||||
}
|
||||
|
||||
function kinsTypeFromProgramActiveLine(state, activeLine) {
|
||||
const motion = programMotionAtOrBeforeLine(state, activeLine)
|
||||
|| state.programExecution?.motion?.[clampMotionIndex(state, state.programExecutionMotionIndex)];
|
||||
return kinsTypeFromProgramMotion(state, motion);
|
||||
}
|
||||
|
||||
function programMotionAtOrBeforeLine(state, activeLine) {
|
||||
const motion = state.programExecution?.motion;
|
||||
if (!Array.isArray(motion) || motion.length === 0) return null;
|
||||
const line = Number(activeLine);
|
||||
if (!Number.isFinite(line)) return null;
|
||||
let candidate = null;
|
||||
for (const item of motion) {
|
||||
const itemLine = Number(item?.line);
|
||||
if (!Number.isFinite(itemLine)) continue;
|
||||
if (itemLine > line) break;
|
||||
candidate = item;
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
function resolveTaskHalAxisPose(state, status) {
|
||||
const ui = status?.ui || {};
|
||||
const axisPose = ui.axisPose;
|
||||
if (!axisPose || typeof axisPose !== "object") {
|
||||
return state.axisPose;
|
||||
}
|
||||
|
||||
if (ui.axisPoseFrame === "work") {
|
||||
return clampAxisPoseToProfile({ ...state.axisPose, ...axisPose }, state.profile);
|
||||
}
|
||||
|
||||
if (ui.axisPoseDelta && typeof ui.axisPoseDelta === "object") {
|
||||
return addAxisDelta(state.axisPose, ui.axisPoseDelta, state.profile);
|
||||
}
|
||||
|
||||
if (state.pendingJogCommand && isJogStatus(status)) {
|
||||
const { axis, direction, increment, basePose } = state.pendingJogCommand;
|
||||
return clampAxisPoseToProfile({
|
||||
...basePose,
|
||||
[axis]: Number(basePose?.[axis] || 0) + direction * increment,
|
||||
}, state.profile);
|
||||
}
|
||||
|
||||
if (!ui.axisPoseFrame && wouldResetNonZeroPoseToLocalZero(state.axisPose, axisPose)) {
|
||||
return state.axisPose;
|
||||
}
|
||||
|
||||
return clampAxisPoseToProfile({ ...state.axisPose, ...axisPose }, state.profile);
|
||||
}
|
||||
|
||||
function addAxisDelta(axisPose, delta, profile) {
|
||||
const next = { ...axisPose };
|
||||
for (const axis of ["x", "y", "z", "a", "b", "c"]) {
|
||||
if (!Number.isFinite(Number(delta[axis]))) continue;
|
||||
next[axis] = Number(next[axis] || 0) + Number(delta[axis]);
|
||||
}
|
||||
return clampAxisPoseToProfile(next, profile);
|
||||
}
|
||||
|
||||
function isJogStatus(status) {
|
||||
const motion = status?.motionStatus?.motion || {};
|
||||
return Number(motion.motionType) === 3 || Number(motion.teleopMode) === 1 || motion.teleopMode === true;
|
||||
}
|
||||
|
||||
function wouldResetNonZeroPoseToLocalZero(currentPose = {}, nextPose = {}) {
|
||||
const axes = ["x", "y", "z", "a", "b", "c"];
|
||||
const currentHasNonZero = axes.some((axis) => Math.abs(Number(currentPose[axis] || 0)) > 0.001);
|
||||
const nextIsNearZero = axes.every((axis) => Math.abs(Number(nextPose[axis] || 0)) <= 0.001);
|
||||
return currentHasNonZero && nextIsNearZero;
|
||||
}
|
||||
|
||||
function createTaskHalRuntimeFeedback(state, status, axisPose, activeLine) {
|
||||
const ui = status?.ui || {};
|
||||
const motion = status?.motionStatus?.motion || {};
|
||||
|
||||
@@ -160,7 +160,7 @@ button:active {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.machine-preview {
|
||||
.toolpath-preview {
|
||||
width: 100%;
|
||||
height: calc(100% - 64px);
|
||||
margin-top: 32px;
|
||||
@@ -169,7 +169,7 @@ button:active {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.machine-preview:active {
|
||||
.toolpath-preview:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
@@ -204,43 +204,6 @@ button:active {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.machine-envelope,
|
||||
.machine-grid {
|
||||
fill: none;
|
||||
stroke: #d21f1f;
|
||||
stroke-width: 1.4;
|
||||
}
|
||||
|
||||
.machine-grid {
|
||||
stroke: #3d3d3d;
|
||||
}
|
||||
|
||||
.machine-rapid {
|
||||
fill: none;
|
||||
stroke: #9e6b00;
|
||||
stroke-width: 1.6;
|
||||
}
|
||||
|
||||
.toolpath {
|
||||
fill: none;
|
||||
stroke: #f7f7f7;
|
||||
stroke-width: 1.8;
|
||||
}
|
||||
|
||||
.tool-axis {
|
||||
stroke: #22e6e6;
|
||||
stroke-width: 1.4;
|
||||
}
|
||||
|
||||
.tcp-point {
|
||||
fill: #21f2f2;
|
||||
}
|
||||
|
||||
.axis-label {
|
||||
fill: #3864ff;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.rtcp-preview-badge {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
|
||||
@@ -86,20 +86,18 @@ function renderPreview(element, state, dispatch) {
|
||||
const tcp = state.tcpPose;
|
||||
const tool = state.toolAxisVector;
|
||||
|
||||
element.innerHTML = `
|
||||
<div class="program-path">${escapeHtml(state.activeProgram)}</div>
|
||||
<canvas class="machine-preview" data-five-axis-canvas="true" aria-label="5 axis Three.js preview"></canvas>
|
||||
if (element.dataset.previewMounted !== "true") {
|
||||
element.innerHTML = `
|
||||
<div class="program-path" data-preview-program-path></div>
|
||||
<canvas class="toolpath-preview" data-five-axis-canvas="true" aria-label="5 axis toolpath preview"></canvas>
|
||||
<div class="tool-preview-card" data-tool-preview="summary">
|
||||
<strong>T${state.toolPreview.toolNumber}</strong>
|
||||
<span>D ${formatNumber(state.toolPreview.diameter, 2)} ${state.toolPreview.units}</span>
|
||||
<span>L ${formatNumber(state.toolPreview.length, 3)} ${state.toolPreview.units}</span>
|
||||
<span>${state.toolPreview.holder}</span>
|
||||
<strong data-tool-preview-number></strong>
|
||||
<span data-tool-preview-diameter></span>
|
||||
<span data-tool-preview-length></span>
|
||||
<span data-tool-preview-holder></span>
|
||||
</div>
|
||||
<div class="rtcp-preview-badge" data-rtcp-preview-state="${state.rtcpState}">
|
||||
RTCP ${state.rtcpState} | TCP ${formatNumber(tcp.x)} ${formatNumber(tcp.y)} ${formatNumber(tcp.z)}
|
||||
| V ${formatNumber(tool.x, 3)} ${formatNumber(tool.y, 3)} ${formatNumber(tool.z, 3)}
|
||||
</div>
|
||||
<div class="preview-toolbar" data-preview-points="${state.preview.pathPoints}">
|
||||
<div class="rtcp-preview-badge" data-rtcp-preview-state></div>
|
||||
<div class="preview-toolbar" data-preview-points>
|
||||
<button type="button" data-action="view-x">X</button>
|
||||
<button type="button" data-action="view-y">Y</button>
|
||||
<button type="button" data-action="view-z">Z</button>
|
||||
@@ -108,18 +106,56 @@ function renderPreview(element, state, dispatch) {
|
||||
</div>
|
||||
`;
|
||||
|
||||
element.querySelector('[data-action="reset-view"]').addEventListener("click", () => {
|
||||
dispatch({ type: "RESET_VIEW" });
|
||||
});
|
||||
element.querySelector('[data-action="clear-preview"]').addEventListener("click", () => {
|
||||
dispatch({ type: "CLEAR_PREVIEW" });
|
||||
});
|
||||
for (const view of ["x", "y", "z"]) {
|
||||
element.querySelector(`[data-action="view-${view}"]`).addEventListener("click", () => {
|
||||
dispatch({ type: "SET_VIEW", view });
|
||||
element.querySelector('[data-action="reset-view"]').addEventListener("click", () => {
|
||||
dispatch({ type: "RESET_VIEW" });
|
||||
});
|
||||
element.querySelector('[data-action="clear-preview"]').addEventListener("click", () => {
|
||||
dispatch({ type: "CLEAR_PREVIEW" });
|
||||
});
|
||||
for (const view of ["x", "y", "z"]) {
|
||||
element.querySelector(`[data-action="view-${view}"]`).addEventListener("click", () => {
|
||||
dispatch({ type: "SET_VIEW", view });
|
||||
});
|
||||
}
|
||||
element.dataset.previewMounted = "true";
|
||||
}
|
||||
|
||||
setText(element, "[data-preview-program-path]", state.activeProgram);
|
||||
setText(element, "[data-tool-preview-number]", `T${state.toolPreview.toolNumber}`);
|
||||
setText(element, "[data-tool-preview-diameter]", `D ${formatNumber(state.toolPreview.diameter, 2)} ${state.toolPreview.units}`);
|
||||
setText(element, "[data-tool-preview-length]", `L ${formatNumber(state.toolPreview.length, 3)} ${state.toolPreview.units}`);
|
||||
setText(element, "[data-tool-preview-holder]", state.toolPreview.holder);
|
||||
|
||||
const rtcpBadge = element.querySelector("[data-rtcp-preview-state]");
|
||||
if (rtcpBadge) {
|
||||
rtcpBadge.dataset.rtcpPreviewState = state.rtcpState;
|
||||
rtcpBadge.textContent = [
|
||||
`RTCP ${state.rtcpState}`,
|
||||
`TCP ${formatNumber(tcp.x)} ${formatNumber(tcp.y)} ${formatNumber(tcp.z)}`,
|
||||
`V ${formatNumber(tool.x, 3)} ${formatNumber(tool.y, 3)} ${formatNumber(tool.z, 3)}`,
|
||||
].join(" | ");
|
||||
}
|
||||
|
||||
const toolbar = element.querySelector("[data-preview-points]");
|
||||
if (toolbar) {
|
||||
toolbar.dataset.previewPoints = String(state.preview.pathPoints);
|
||||
for (const view of ["x", "y", "z"]) {
|
||||
toolbar.querySelector(`[data-action="view-${view}"]`)?.setAttribute(
|
||||
"data-active",
|
||||
state.preview.selectedView === view ? "true" : "false",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const canvas = element.querySelector("[data-five-axis-canvas]");
|
||||
renderFiveAxisScene(canvas, state);
|
||||
}
|
||||
|
||||
function setText(root, selector, value) {
|
||||
const element = root.querySelector(selector);
|
||||
if (element) {
|
||||
element.textContent = value;
|
||||
}
|
||||
renderFiveAxisScene(element.querySelector("[data-five-axis-canvas]"), state);
|
||||
}
|
||||
|
||||
function renderDro(element, state) {
|
||||
|
||||
@@ -33,21 +33,23 @@ export function renderFiveAxisScene(canvas, state) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pointCount = preview.previewPath.geometry.getAttribute("position").count;
|
||||
const executedPointCount = preview.executedPath.geometry.getAttribute("position").count;
|
||||
const pointCount = geometryPointCount(preview.previewPath.geometry);
|
||||
const executedPointCount = geometryPointCount(preview.executedPath.geometry);
|
||||
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,
|
||||
feedPointCount: geometryPointCount(preview.feedPath.geometry),
|
||||
rapidPointCount: geometryPointCount(preview.rapidPath.geometry),
|
||||
arcPointCount: geometryPointCount(preview.arcPath.geometry),
|
||||
currentSegmentPointCount: geometryPointCount(preview.currentSegmentPath.geometry),
|
||||
sceneObjectCount: countSceneObjects(preview.scene),
|
||||
toolhead: preview.currentToolhead,
|
||||
renderer: "webgl",
|
||||
sceneMode: "program-preview-and-tool-execution",
|
||||
machineReferenceModel: "webgl-five-axis-reference",
|
||||
cameraControls: preview.controls.enabled,
|
||||
toolExecutionMarker: preview.toolMarker.visible,
|
||||
toolAxisMarker: preview.toolAxis.visible,
|
||||
pathFitBounds: preview.pathFitBoundsReady,
|
||||
});
|
||||
}
|
||||
@@ -72,6 +74,9 @@ function createScene(canvas) {
|
||||
const scene = new THREE.Scene();
|
||||
const camera = new THREE.PerspectiveCamera(42, 1, 0.1, 100);
|
||||
|
||||
const machineModel = createMachineReferenceModel();
|
||||
scene.add(machineModel.root);
|
||||
|
||||
const previewPath = createLine(0x808892, 0.56);
|
||||
const feedPath = createLine(0x4fb3ff, 0.92);
|
||||
const executedPath = createLine(0x1ffff4, 1);
|
||||
@@ -103,6 +108,7 @@ function createScene(canvas) {
|
||||
rapidPath,
|
||||
arcPath,
|
||||
currentSegmentPath,
|
||||
machineModel,
|
||||
toolMarker,
|
||||
toolAxis,
|
||||
currentToolhead: new THREE.Vector3(),
|
||||
@@ -133,6 +139,35 @@ function renderFallbackPreview(preview, state) {
|
||||
canvas.height = height;
|
||||
}
|
||||
|
||||
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;
|
||||
const toolPosition = executionToolPosition(state, previewPoints);
|
||||
|
||||
exposePreviewDataset(canvas, state, {
|
||||
pointCount,
|
||||
executedPointCount,
|
||||
sceneObjectCount: 8 + (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",
|
||||
machineReferenceModel: "2d-five-axis-reference",
|
||||
cameraControls: false,
|
||||
toolExecutionMarker: Boolean(toolPosition),
|
||||
toolAxisMarker: 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;
|
||||
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
@@ -144,14 +179,8 @@ function renderFallbackPreview(preview, state) {
|
||||
const cy = height * 0.53;
|
||||
const scale = Math.min(width / 7.2, height / 4.8);
|
||||
|
||||
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;
|
||||
drawFallbackMachineReference(ctx, cx, cy, scale, state);
|
||||
|
||||
if (pointCount > 0) {
|
||||
ctx.strokeStyle = "#8d95a0";
|
||||
ctx.lineWidth = 2;
|
||||
@@ -175,16 +204,6 @@ function renderFallbackPreview(preview, state) {
|
||||
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;
|
||||
@@ -197,23 +216,6 @@ function renderFallbackPreview(preview, state) {
|
||||
ctx.fillStyle = "#b7c7b8";
|
||||
ctx.font = "12px Courier New, monospace";
|
||||
ctx.fillText("2D RTCP fallback", 12, height - 14);
|
||||
|
||||
exposePreviewDataset(canvas, state, {
|
||||
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),
|
||||
feedPointCount: feedPoints.length,
|
||||
rapidPointCount: rapidPoints.length,
|
||||
arcPointCount: arcPoints.length,
|
||||
currentSegmentPointCount: currentSegmentPoints.length,
|
||||
pathFitBounds: computePointBounds(previewPoints.concat(executedPoints, currentSegmentPoints)) !== null,
|
||||
});
|
||||
canvas.dataset.threeFallbackReason = preview.errorMessage;
|
||||
}
|
||||
|
||||
function exposePreviewDataset(canvas, state, preview) {
|
||||
@@ -230,9 +232,15 @@ function exposePreviewDataset(canvas, state, preview) {
|
||||
canvas.dataset.threeFrameApi = state.rtcpFrame.apiName;
|
||||
canvas.dataset.threeRenderer = preview.renderer;
|
||||
canvas.dataset.threeSceneMode = preview.sceneMode;
|
||||
canvas.dataset.threePreviewScope = preview.machineReferenceModel
|
||||
? "machine-reference-and-toolpath"
|
||||
: "toolpath-only";
|
||||
canvas.dataset.threeMachineReferenceModel = preview.machineReferenceModel || "none";
|
||||
canvas.dataset.threeCameraControls = preview.cameraControls ? "orbit-pan-zoom" : "none";
|
||||
canvas.dataset.threeProgramPreviewSource = previewSourceMode(state);
|
||||
canvas.dataset.threeToolExecutionMarker = preview.toolExecutionMarker ? "true" : "false";
|
||||
canvas.dataset.threeTcpMarker = preview.toolExecutionMarker ? "sphere" : "hidden";
|
||||
canvas.dataset.threeToolAxisMarker = preview.toolAxisMarker ? "line" : "hidden";
|
||||
canvas.dataset.threeToolpathPreviewSource = toolpathPreviewSource(state);
|
||||
canvas.dataset.threeToolExecutionTraceSource = toolExecutionTraceSource(state);
|
||||
canvas.dataset.threePathFitBounds = preview.pathFitBounds ? "ok" : "pending";
|
||||
@@ -257,6 +265,70 @@ function createLine(color, opacity) {
|
||||
);
|
||||
}
|
||||
|
||||
function createMachineReferenceModel() {
|
||||
const root = new THREE.Group();
|
||||
root.name = "five-axis-machine-reference";
|
||||
|
||||
const base = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(4.8, 3.2, 0.08),
|
||||
new THREE.MeshBasicMaterial({ color: 0x222930 }),
|
||||
);
|
||||
base.position.z = -0.16;
|
||||
|
||||
const table = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(3.7, 2.35, 0.05),
|
||||
new THREE.MeshBasicMaterial({ color: 0x3a444d, transparent: true, opacity: 0.78 }),
|
||||
);
|
||||
table.position.z = -0.08;
|
||||
|
||||
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 rotaryA = new THREE.Mesh(
|
||||
new THREE.TorusGeometry(0.88, 0.018, 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.MeshBasicMaterial({ color: 0xffd166, transparent: true, opacity: 0.9 }),
|
||||
);
|
||||
rotaryC.rotation.x = Math.PI / 2;
|
||||
rotaryC.position.z = 0.04;
|
||||
|
||||
const toolHolder = new THREE.Group();
|
||||
const holderBody = new THREE.Mesh(
|
||||
new THREE.CylinderGeometry(0.08, 0.08, 0.42, 18),
|
||||
new THREE.MeshBasicMaterial({ color: 0xf1f5f9 }),
|
||||
);
|
||||
holderBody.rotation.x = Math.PI / 2;
|
||||
holderBody.position.z = 0.32;
|
||||
const cutter = new THREE.Mesh(
|
||||
new THREE.ConeGeometry(0.06, 0.25, 18),
|
||||
new THREE.MeshBasicMaterial({ color: 0xfff176 }),
|
||||
);
|
||||
cutter.rotation.x = Math.PI;
|
||||
cutter.position.z = 0.08;
|
||||
toolHolder.add(holderBody, cutter);
|
||||
|
||||
root.add(base, table, xAxis, yAxis, zAxis, rotaryA, rotaryC, toolHolder);
|
||||
return {
|
||||
root,
|
||||
rotaryA,
|
||||
rotaryC,
|
||||
toolHolder,
|
||||
};
|
||||
}
|
||||
|
||||
function createStaticLine(points, color) {
|
||||
return new THREE.Line(
|
||||
new THREE.BufferGeometry().setFromPoints(points),
|
||||
new THREE.LineBasicMaterial({ color, transparent: true, opacity: 0.95 }),
|
||||
);
|
||||
}
|
||||
|
||||
function updateToolpathPreview(preview, state) {
|
||||
const previewPoints = buildProgramPreviewPoints(state);
|
||||
const executedPoints = buildExecutedProgramPoints(state, previewPoints);
|
||||
@@ -265,6 +337,15 @@ function updateToolpathPreview(preview, state) {
|
||||
const arcPoints = buildTypedPreviewPoints(state, "ARC_FEED");
|
||||
const currentSegmentPoints = buildCurrentSegmentPoints(state);
|
||||
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(":");
|
||||
|
||||
updateLineGeometry(preview.previewPath, previewPoints);
|
||||
updateLineGeometry(preview.feedPath, feedPoints);
|
||||
@@ -273,6 +354,7 @@ function updateToolpathPreview(preview, state) {
|
||||
updateLineGeometry(preview.arcPath, arcPoints);
|
||||
updateLineGeometry(preview.currentSegmentPath, currentSegmentPoints);
|
||||
updateToolExecutionMarker(preview, state, toolPosition);
|
||||
updateMachineReferenceModel(preview, state, toolPosition);
|
||||
|
||||
const cameraRevision = state.preview.cameraRevision ?? 0;
|
||||
if (
|
||||
@@ -309,6 +391,22 @@ function updateToolExecutionMarker(preview, state, toolPosition) {
|
||||
]);
|
||||
}
|
||||
|
||||
function updateMachineReferenceModel(preview, state, toolPosition) {
|
||||
const model = preview.machineModel;
|
||||
if (!model) return;
|
||||
const a = degreesToRadians(state.axisPose?.a);
|
||||
const b = degreesToRadians(state.axisPose?.b);
|
||||
const c = degreesToRadians(state.axisPose?.c);
|
||||
model.rotaryA.rotation.x = a;
|
||||
model.rotaryA.rotation.y = Math.PI / 2 + b;
|
||||
model.rotaryC.rotation.z = c;
|
||||
|
||||
const tcpPosition = toolPosition || toPreviewVector(state.tcpPose || state.axisPose);
|
||||
model.toolHolder.position.copy(tcpPosition);
|
||||
const toolVector = toToolVector(state.toolAxisVector);
|
||||
model.toolHolder.lookAt(tcpPosition.clone().add(toolVector));
|
||||
}
|
||||
|
||||
function updateLineGeometry(line, points) {
|
||||
line.visible = points.length > 0;
|
||||
line.geometry.dispose();
|
||||
@@ -317,6 +415,10 @@ function updateLineGeometry(line, points) {
|
||||
: EMPTY_GEOMETRY.clone();
|
||||
}
|
||||
|
||||
function geometryPointCount(geometry) {
|
||||
return geometry?.getAttribute("position")?.count || 0;
|
||||
}
|
||||
|
||||
function buildProgramPreviewPoints(state) {
|
||||
const motion = state.programExecution?.motion;
|
||||
if (Array.isArray(motion) && motion.length > 0 && state.preview.pathPoints !== 0) {
|
||||
@@ -384,7 +486,6 @@ function buildFixturePreviewPoints(pointCount, tcpPosition) {
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -505,6 +606,56 @@ function drawFallbackPolyline(ctx, points, cx, cy, scale) {
|
||||
}
|
||||
}
|
||||
|
||||
function drawFallbackMachineReference(ctx, cx, cy, scale, state) {
|
||||
const tableWidth = 4.8 * scale;
|
||||
const tableHeight = 3.2 * 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");
|
||||
|
||||
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.stroke();
|
||||
|
||||
ctx.strokeStyle = "#ffd166";
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, 0.7 * scale, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
|
||||
const tcp = executionToolPosition(state, []);
|
||||
if (tcp) {
|
||||
const toolX = cx + tcp.x * scale;
|
||||
const toolY = cy - tcp.y * scale;
|
||||
ctx.strokeStyle = "#f1f5f9";
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(toolX, toolY - 0.38 * scale);
|
||||
ctx.lineTo(toolX, toolY - 0.08 * scale);
|
||||
ctx.stroke();
|
||||
ctx.fillStyle = "#1ffff4";
|
||||
ctx.beginPath();
|
||||
ctx.arc(toolX, toolY, 0.07 * scale, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
function drawFallbackAxis(ctx, x1, y1, x2, y2, color) {
|
||||
ctx.strokeStyle = color;
|
||||
ctx.lineWidth = 3;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x1, y1);
|
||||
ctx.lineTo(x2, y2);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
function createToolpathCameraControls(canvas, camera, renderFrame) {
|
||||
const controls = {
|
||||
enabled: true,
|
||||
@@ -677,7 +828,7 @@ function applyCameraControls(controls) {
|
||||
}
|
||||
|
||||
function resizeRenderer(preview) {
|
||||
const { canvas } = preview.renderer.domElement;
|
||||
const canvas = preview.renderer.domElement;
|
||||
const width = Math.max(canvas.clientWidth, 320);
|
||||
const height = Math.max(canvas.clientHeight, 240);
|
||||
if (canvas.width !== width || canvas.height !== height) {
|
||||
@@ -717,6 +868,10 @@ function round(value) {
|
||||
return Math.round(Number(value) * 1000) / 1000;
|
||||
}
|
||||
|
||||
function degreesToRadians(value) {
|
||||
return (Number(value) || 0) * Math.PI / 180;
|
||||
}
|
||||
|
||||
function clamp(value, min, max) {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user