feat: sync latest run execution updates
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
"build": "node scripts/build-static.mjs",
|
||||
"dev": "python3 -m http.server 4173",
|
||||
"smoke": "bash ../tests/browser/verify_gmoccapy_shell_browser.sh && bash ../tests/browser/verify_gmoccapy_dist_browser.sh",
|
||||
"smoke:node": "node ../tests/node/verify_linuxcnc_kinematics_runtime.mjs && node ../tests/node/verify_linuxcnc_interpreter_runtime.mjs && node ../tests/node/verify_linuxcnc_ini_runtime.mjs && node ../tests/node/verify_run_preconditions.mjs && node ../tests/node/verify_linuxcnc_task_hal_runtime.mjs && node ../tests/node/verify_native_task_hal_audit.mjs && node ../tests/node/verify_full_linuxcnc_5axis_source.mjs && node ../tests/node/verify_real_linuxcnc_5axis_program_cases.mjs && node ../tests/node/verify_full_execution_boundary.mjs && node ../tests/node/verify_machine_file_staging.mjs && node ../tests/node/verify_five_axis_session.mjs && node ../tests/node/verify_rtcp_store.mjs && node ../tests/node/verify_profile_boundary.mjs"
|
||||
"smoke:node": "node ../tests/node/verify_linuxcnc_kinematics_runtime.mjs && node ../tests/node/verify_linuxcnc_interpreter_runtime.mjs && node ../tests/node/verify_linuxcnc_ini_runtime.mjs && node ../tests/node/verify_run_preconditions.mjs && node ../tests/node/verify_run_feedback_loop.mjs && node ../tests/node/verify_linuxcnc_task_hal_runtime.mjs && node ../tests/node/verify_native_task_hal_audit.mjs && node ../tests/node/verify_full_linuxcnc_5axis_source.mjs && node ../tests/node/verify_real_linuxcnc_5axis_program_cases.mjs && node ../tests/node/verify_full_execution_boundary.mjs && node ../tests/node/verify_machine_file_staging.mjs && node ../tests/node/verify_five_axis_session.mjs && node ../tests/node/verify_rtcp_store.mjs && node ../tests/node/verify_profile_boundary.mjs && node ../tests/node/verify_linear_unit_conversion.mjs"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {}
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import {
|
||||
linearUnitsToMillimetersFactor,
|
||||
linearValueToMillimeters,
|
||||
resolveStateLinearUnits,
|
||||
} from "./linear-units.js";
|
||||
|
||||
const LINEAR_AXES = ["x", "y", "z", "u", "v", "w"];
|
||||
const ANGULAR_AXES = ["a", "b", "c"];
|
||||
const ALL_AXES = [...LINEAR_AXES, ...ANGULAR_AXES];
|
||||
@@ -9,14 +15,17 @@ export function buildProgramExecutionTiming({
|
||||
rapidOverride = 100,
|
||||
defaultFeedRate = 100,
|
||||
} = {}) {
|
||||
const limits = buildVelocityLimits(profile);
|
||||
const linearUnits = resolveStateLinearUnits(profile);
|
||||
const limits = buildVelocityLimits(profile, linearUnits);
|
||||
const segments = [];
|
||||
let previousAxes = null;
|
||||
let previousLinearUnits = linearUnits;
|
||||
let elapsedSeconds = 0;
|
||||
let feedRate = Number(defaultFeedRate) > 0 ? Number(defaultFeedRate) : 100;
|
||||
|
||||
for (let index = 0; index < motion.length; index += 1) {
|
||||
const event = motion[index];
|
||||
const eventLinearUnits = event.linearUnits || linearUnits;
|
||||
const axes = normalizeAxes(event.axes, previousAxes);
|
||||
if (Number.isFinite(event.feedRate) && event.feedRate > 0) {
|
||||
feedRate = event.feedRate;
|
||||
@@ -31,6 +40,8 @@ export function buildProgramExecutionTiming({
|
||||
feedOverride,
|
||||
rapidOverride,
|
||||
elapsedSeconds,
|
||||
linearUnits: eventLinearUnits,
|
||||
previousLinearUnits: previousAxes ? previousLinearUnits : eventLinearUnits,
|
||||
});
|
||||
elapsedSeconds += segment.durationSeconds;
|
||||
segments.push({
|
||||
@@ -38,6 +49,7 @@ export function buildProgramExecutionTiming({
|
||||
elapsedSeconds,
|
||||
});
|
||||
previousAxes = axes;
|
||||
previousLinearUnits = eventLinearUnits;
|
||||
}
|
||||
|
||||
const feedSeconds = segments
|
||||
@@ -58,6 +70,7 @@ export function buildProgramExecutionTiming({
|
||||
motionCount: segments.length,
|
||||
segments,
|
||||
limits,
|
||||
linearUnits,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -82,14 +95,22 @@ function buildTimingSegment({
|
||||
feedOverride,
|
||||
rapidOverride,
|
||||
elapsedSeconds,
|
||||
linearUnits,
|
||||
previousLinearUnits,
|
||||
}) {
|
||||
const deltas = Object.fromEntries(ALL_AXES.map((axis) => [axis, axes[axis] - previousAxes[axis]]));
|
||||
const linearDistanceMm = vectorLength(LINEAR_AXES.map((axis) => deltas[axis]));
|
||||
const linearDistanceMm = vectorLength(LINEAR_AXES.map((axis) => (
|
||||
linearValueToMillimeters(axes[axis], linearUnits)
|
||||
- linearValueToMillimeters(previousAxes[axis], previousLinearUnits || linearUnits)
|
||||
)));
|
||||
const angularDistanceDeg = vectorLength(ANGULAR_AXES.map((axis) => deltas[axis]));
|
||||
const motionClass = event.type === "STRAIGHT_TRAVERSE" ? "rapid" : "feed";
|
||||
const feedMode = event.feedMode === "inverse-time" ? "inverse-time" : "units-per-minute";
|
||||
const requestedLinearVelocity = motionClass === "rapid"
|
||||
? limits.maxLinearVelocityMmPerMin * percent(rapidOverride)
|
||||
: Math.max(feedRate, 0) * percent(feedOverride);
|
||||
: feedMode === "inverse-time"
|
||||
? inverseTimeVelocityMmPerMin(linearDistanceMm, angularDistanceDeg, feedRate)
|
||||
: linearValueToMillimeters(Math.max(feedRate, 0), linearUnits) * percent(feedOverride);
|
||||
const cappedLinearVelocity = Math.min(
|
||||
requestedLinearVelocity || limits.defaultLinearVelocityMmPerMin,
|
||||
limits.maxLinearVelocityMmPerMin,
|
||||
@@ -99,17 +120,25 @@ function buildTimingSegment({
|
||||
: 0;
|
||||
const angularVelocityDegPerMin = motionClass === "rapid"
|
||||
? limits.maxAngularVelocityDegPerMin * percent(rapidOverride)
|
||||
: Math.min(Math.max(feedRate, 0) * percent(feedOverride), limits.maxAngularVelocityDegPerMin);
|
||||
: feedMode === "inverse-time"
|
||||
? inverseTimeAngularVelocityDegPerMin(angularDistanceDeg, feedRate)
|
||||
: Math.min(Math.max(feedRate, 0) * percent(feedOverride), limits.maxAngularVelocityDegPerMin);
|
||||
const angularSeconds = angularDistanceDeg > 0
|
||||
? angularDistanceDeg / Math.max(angularVelocityDegPerMin / 60, 0.000001)
|
||||
: 0;
|
||||
const durationSeconds = Math.max(linearSeconds, angularSeconds);
|
||||
const inverseTimeSeconds = motionClass === "feed" && feedMode === "inverse-time" && feedRate > 0
|
||||
? 60 / feedRate
|
||||
: 0;
|
||||
const durationSeconds = inverseTimeSeconds > 0
|
||||
? inverseTimeSeconds
|
||||
: Math.max(linearSeconds, angularSeconds);
|
||||
|
||||
return {
|
||||
index,
|
||||
line: event.line,
|
||||
type: event.type,
|
||||
motionClass,
|
||||
feedMode,
|
||||
linearDistanceMm,
|
||||
angularDistanceDeg,
|
||||
feedRate,
|
||||
@@ -118,20 +147,28 @@ function buildTimingSegment({
|
||||
angularVelocityDegPerMin,
|
||||
durationSeconds,
|
||||
startSeconds: elapsedSeconds,
|
||||
startAxes: previousAxes,
|
||||
endAxes: axes,
|
||||
axes,
|
||||
deltas,
|
||||
linearUnits,
|
||||
};
|
||||
}
|
||||
|
||||
function buildVelocityLimits(profile) {
|
||||
function buildVelocityLimits(profile, linearUnits) {
|
||||
const traj = profile?.traj || {};
|
||||
const axisLimits = profile?.axisLimits || {};
|
||||
const linearVelocityScale = linearUnitsToMillimetersFactor(linearUnits);
|
||||
const maxLinearVelocity = firstFinite(
|
||||
Number(traj.maxLinearVelocity) * 60,
|
||||
...LINEAR_AXES.map((axis) => Number(axisLimits[axis.toUpperCase()]?.maxVelocity) * 60),
|
||||
Number(traj.maxLinearVelocity) * linearVelocityScale * 60,
|
||||
...LINEAR_AXES.map((axis) => Number(axisLimits[axis.toUpperCase()]?.maxVelocity) * linearVelocityScale * 60),
|
||||
2100,
|
||||
);
|
||||
const defaultLinearVelocity = firstFinite(Number(traj.defaultLinearVelocity) * 60, maxLinearVelocity, 1200);
|
||||
const defaultLinearVelocity = firstFinite(
|
||||
Number(traj.defaultLinearVelocity) * linearVelocityScale * 60,
|
||||
maxLinearVelocity,
|
||||
1200,
|
||||
);
|
||||
const maxAngularVelocity = firstFinite(
|
||||
...ANGULAR_AXES.map((axis) => Number(axisLimits[axis.toUpperCase()]?.maxVelocity) * 60),
|
||||
maxLinearVelocity,
|
||||
@@ -161,6 +198,24 @@ function percent(value) {
|
||||
return Number.isFinite(number) ? Math.max(number, 0) / 100 : 1;
|
||||
}
|
||||
|
||||
function inverseTimeVelocityMmPerMin(linearDistanceMm, angularDistanceDeg, feedRate) {
|
||||
const durationSeconds = feedRate > 0 ? 60 / feedRate : 0;
|
||||
if (linearDistanceMm > 0 && durationSeconds > 0) {
|
||||
return (linearDistanceMm / durationSeconds) * 60;
|
||||
}
|
||||
if (angularDistanceDeg > 0 && durationSeconds > 0) {
|
||||
return angularDistanceDeg / durationSeconds * 60;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function inverseTimeAngularVelocityDegPerMin(angularDistanceDeg, feedRate) {
|
||||
const durationSeconds = feedRate > 0 ? 60 / feedRate : 0;
|
||||
return angularDistanceDeg > 0 && durationSeconds > 0
|
||||
? angularDistanceDeg / durationSeconds * 60
|
||||
: 0;
|
||||
}
|
||||
|
||||
function firstFinite(...values) {
|
||||
return values.find((value) => Number.isFinite(value) && value > 0) || 1;
|
||||
}
|
||||
|
||||
60
web-rtcp-5axis-sim-plan/app/src/runtime/linear-units.js
Normal file
60
web-rtcp-5axis-sim-plan/app/src/runtime/linear-units.js
Normal file
@@ -0,0 +1,60 @@
|
||||
const UNIT_ALIASES = new Map([
|
||||
["mm", "mm"],
|
||||
["millimeter", "mm"],
|
||||
["millimeters", "mm"],
|
||||
["millimetre", "mm"],
|
||||
["millimetres", "mm"],
|
||||
["metric", "mm"],
|
||||
["inch", "inch"],
|
||||
["inches", "inch"],
|
||||
["in", "inch"],
|
||||
["imperial", "inch"],
|
||||
["m", "m"],
|
||||
["meter", "m"],
|
||||
["meters", "m"],
|
||||
["metre", "m"],
|
||||
["metres", "m"],
|
||||
]);
|
||||
|
||||
const METERS_PER_UNIT = {
|
||||
mm: 0.001,
|
||||
inch: 0.0254,
|
||||
m: 1,
|
||||
};
|
||||
|
||||
export function normalizeLinearUnits(units, fallback = "mm") {
|
||||
const key = String(units || "").trim().toLowerCase();
|
||||
return UNIT_ALIASES.get(key) || UNIT_ALIASES.get(String(fallback || "mm").trim().toLowerCase()) || "mm";
|
||||
}
|
||||
|
||||
export function linearUnitsToMetersFactor(units) {
|
||||
return METERS_PER_UNIT[normalizeLinearUnits(units)] || METERS_PER_UNIT.mm;
|
||||
}
|
||||
|
||||
export function linearUnitsToMillimetersFactor(units) {
|
||||
return linearUnitsToMetersFactor(units) * 1000;
|
||||
}
|
||||
|
||||
export function linearValueToMeters(value, units) {
|
||||
const number = Number(value) || 0;
|
||||
return number * linearUnitsToMetersFactor(units);
|
||||
}
|
||||
|
||||
export function linearValueToMillimeters(value, units) {
|
||||
const number = Number(value) || 0;
|
||||
return number * linearUnitsToMillimetersFactor(units);
|
||||
}
|
||||
|
||||
export function linearUnitsLabel(units) {
|
||||
return normalizeLinearUnits(units);
|
||||
}
|
||||
|
||||
export function resolveStateLinearUnits(stateOrProfile, fallback = "mm") {
|
||||
return normalizeLinearUnits(
|
||||
stateOrProfile?.programExecution?.summary?.linearUnits
|
||||
|| stateOrProfile?.linuxCncIniConfig?.traj?.linearUnits
|
||||
|| stateOrProfile?.profile?.traj?.linearUnits
|
||||
|| stateOrProfile?.traj?.linearUnits,
|
||||
fallback,
|
||||
);
|
||||
}
|
||||
@@ -11,6 +11,17 @@ export function parseLinuxCncIni(text, { path = "inline.ini", profileId = "unkno
|
||||
const remaps = parseRemaps(sections);
|
||||
const hal = parseHal(sections);
|
||||
const display = parseDisplay(sections);
|
||||
const emcmot = {
|
||||
module: getFirstValue(sections, "EMCMOT", "EMCMOT") || null,
|
||||
servoPeriodNs: numberOrNull(getFirstValue(sections, "EMCMOT", "SERVO_PERIOD")),
|
||||
};
|
||||
const task = {
|
||||
module: getFirstValue(sections, "TASK", "TASK") || null,
|
||||
cycleTimeSeconds: numberOrNull(getFirstValue(sections, "TASK", "CYCLE_TIME")),
|
||||
};
|
||||
const emcio = {
|
||||
toolTable: getFirstValue(sections, "EMCIO", "TOOL_TABLE") || null,
|
||||
};
|
||||
const halui = {
|
||||
mdiCommands: getValues(sections, "HALUI", "MDI_COMMAND"),
|
||||
};
|
||||
@@ -20,6 +31,7 @@ export function parseLinuxCncIni(text, { path = "inline.ini", profileId = "unkno
|
||||
apiName: "web-rtcp-5axis-linuxcnc-ini-config",
|
||||
profileId,
|
||||
path,
|
||||
sourceText: text,
|
||||
machineName: getFirstValue(sections, "EMC", "MACHINE") || null,
|
||||
kinematics: parseKinematics(kinsText),
|
||||
kinematicsModuleId,
|
||||
@@ -48,10 +60,27 @@ export function parseLinuxCncIni(text, { path = "inline.ini", profileId = "unkno
|
||||
halui,
|
||||
axisLimits,
|
||||
jointConfig,
|
||||
emcio: {
|
||||
toolTable: getFirstValue(sections, "EMCIO", "TOOL_TABLE") || null,
|
||||
},
|
||||
validation: validateIniConfig({ coordinates, jointCount, axisLimits, jointConfig, kinsText }),
|
||||
emcmot,
|
||||
task,
|
||||
emcio,
|
||||
validation: validateIniConfig({
|
||||
sections,
|
||||
coordinates,
|
||||
jointCount,
|
||||
axisLimits,
|
||||
jointConfig,
|
||||
kinsText,
|
||||
remaps,
|
||||
hal,
|
||||
halui,
|
||||
rs274ngc: {
|
||||
halPinVars: boolFromIni(getFirstValue(sections, "RS274NGC", "HAL_PIN_VARS")),
|
||||
parameterFile: getFirstValue(sections, "RS274NGC", "PARAMETER_FILE") || null,
|
||||
},
|
||||
emcmot,
|
||||
task,
|
||||
emcio,
|
||||
}),
|
||||
semanticBoundary: "linuxcnc_ini_file_browser_parser",
|
||||
};
|
||||
}
|
||||
@@ -130,6 +159,18 @@ export function applyIniConfigToProfile(profile, iniConfig) {
|
||||
},
|
||||
},
|
||||
halui: iniConfig.halui.mdiCommands.length > 0 ? iniConfig.halui : profile.halui,
|
||||
emcmot: {
|
||||
...profile.emcmot,
|
||||
...dropNullish(iniConfig.emcmot || {}),
|
||||
},
|
||||
task: {
|
||||
...profile.task,
|
||||
...dropNullish(iniConfig.task || {}),
|
||||
},
|
||||
emcio: {
|
||||
...profile.emcio,
|
||||
...dropNullish(iniConfig.emcio || {}),
|
||||
},
|
||||
traj: {
|
||||
...profile.traj,
|
||||
...dropNullish(iniConfig.traj),
|
||||
@@ -297,17 +338,73 @@ function inferSwitchkinsTypes({ coordinates, halui, kinematicsModuleId }) {
|
||||
}));
|
||||
}
|
||||
|
||||
function validateIniConfig({ coordinates, jointCount, axisLimits, jointConfig, kinsText }) {
|
||||
function validateIniConfig({
|
||||
sections,
|
||||
coordinates,
|
||||
jointCount,
|
||||
axisLimits,
|
||||
jointConfig,
|
||||
kinsText,
|
||||
remaps,
|
||||
hal,
|
||||
halui,
|
||||
rs274ngc,
|
||||
emcmot,
|
||||
task,
|
||||
emcio,
|
||||
}) {
|
||||
const missing = [];
|
||||
const requiredSections = [
|
||||
"EMC",
|
||||
"DISPLAY",
|
||||
"RS274NGC",
|
||||
"KINS",
|
||||
"HAL",
|
||||
"HALUI",
|
||||
"TRAJ",
|
||||
"EMCMOT",
|
||||
"TASK",
|
||||
"EMCIO",
|
||||
];
|
||||
for (const section of requiredSections) {
|
||||
if (!sections.has(section)) missing.push(`[${section}]`);
|
||||
}
|
||||
if (!["XYZAC", "XYZBC"].includes(coordinates)) missing.push("TRAJ.COORDINATES XYZAC/XYZBC");
|
||||
if (!coordinates) missing.push("TRAJ.COORDINATES");
|
||||
if (!jointCount) missing.push("KINS.JOINTS");
|
||||
if (jointCount !== 5) missing.push("KINS.JOINTS=5");
|
||||
if (!kinsText) missing.push("KINS.KINEMATICS");
|
||||
if (!String(kinsText || "").includes("sparm=identityfirst")) {
|
||||
missing.push("KINS.KINEMATICS sparm=identityfirst");
|
||||
}
|
||||
for (const axis of String(coordinates || "").split("")) {
|
||||
if (!axisLimits[axis]) missing.push(`AXIS_${axis}`);
|
||||
}
|
||||
if (jointCount && jointConfig.length !== jointCount) {
|
||||
missing.push(`JOINT_ count ${jointConfig.length}/${jointCount}`);
|
||||
}
|
||||
for (let joint = 0; joint < 5; joint += 1) {
|
||||
if (!sections.has(`JOINT_${joint}`)) missing.push(`JOINT_${joint}`);
|
||||
}
|
||||
for (const code of ["M428", "M429", "M430"]) {
|
||||
if (!remaps.some((remap) => remap.code === code && remap.ngc)) {
|
||||
missing.push(`RS274NGC.REMAP ${code}`);
|
||||
}
|
||||
}
|
||||
if (rs274ngc.halPinVars !== true) missing.push("RS274NGC.HAL_PIN_VARS=1");
|
||||
if (!rs274ngc.parameterFile) missing.push("RS274NGC.PARAMETER_FILE");
|
||||
if (!hal.halui) missing.push("HAL.HALUI");
|
||||
if (!hal.halFiles.length) missing.push("HAL.HALFILE");
|
||||
if (!hal.postguiHalFiles.length) missing.push("HAL.POSTGUI_HALFILE");
|
||||
if (!hal.halcmd.some((line) => line.includes("motion.analog-out-03") && line.includes("motion.switchkins-type"))) {
|
||||
missing.push("HAL.HALCMD motion.analog-out-03=>motion.switchkins-type");
|
||||
}
|
||||
if (halui.mdiCommands.length < 3) missing.push("HALUI.MDI_COMMAND M428/M429/M430");
|
||||
if (!emcmot.module) missing.push("EMCMOT.EMCMOT");
|
||||
if (!emcmot.servoPeriodNs) missing.push("EMCMOT.SERVO_PERIOD");
|
||||
if (!task.module) missing.push("TASK.TASK");
|
||||
if (!task.cycleTimeSeconds) missing.push("TASK.CYCLE_TIME");
|
||||
if (!emcio.toolTable) missing.push("EMCIO.TOOL_TABLE");
|
||||
return {
|
||||
ready: missing.length === 0,
|
||||
missing,
|
||||
|
||||
@@ -234,11 +234,15 @@ export function parseLinuxCncCanonicalMotion(resultText, programText = "", switc
|
||||
const axes = Object.fromEntries(AXES.map((axis) => [axis, 0]));
|
||||
const sourceLines = programLineMap(programText);
|
||||
const feedRatesByLine = feedRatesBySourceLine(programText);
|
||||
const feedModesByLine = feedModesBySourceLine(programText);
|
||||
const linearUnitsByLine = linearUnitsBySourceLine(programText);
|
||||
const switchkinsByLine = switchkinsEventsByLine(switchkinsEvents);
|
||||
const motion = [];
|
||||
let activePlane = 170;
|
||||
let activeSwitchkinsEvent = null;
|
||||
let activeFeedRate = null;
|
||||
let activeFeedMode = "units-per-minute";
|
||||
let activeLinearUnits = "mm";
|
||||
|
||||
for (const line of String(resultText).split("\n")) {
|
||||
const feedRate = readCanonicalNumber(line, "feed_rate");
|
||||
@@ -289,6 +293,8 @@ export function parseLinuxCncCanonicalMotion(resultText, programText = "", switc
|
||||
if (Number.isFinite(sourceFeedRate) && sourceFeedRate > 0) {
|
||||
activeFeedRate = sourceFeedRate;
|
||||
}
|
||||
activeFeedMode = latestFeedModeAtOrBeforeLine(feedModesByLine, sourceLine) || activeFeedMode;
|
||||
activeLinearUnits = latestLinearUnitsAtOrBeforeLine(linearUnitsByLine, sourceLine) || activeLinearUnits;
|
||||
}
|
||||
motion.push({
|
||||
type: event[1],
|
||||
@@ -300,6 +306,8 @@ export function parseLinuxCncCanonicalMotion(resultText, programText = "", switc
|
||||
switchkinsCode: activeSwitchkinsEvent?.code || null,
|
||||
switchkinsRemapBoundary: activeSwitchkinsEvent ? SWITCHKINS_REMAP_BOUNDARY : null,
|
||||
feedRate: activeFeedRate,
|
||||
feedMode: activeFeedMode,
|
||||
linearUnits: activeLinearUnits,
|
||||
raw: line,
|
||||
});
|
||||
}
|
||||
@@ -307,6 +315,56 @@ export function parseLinuxCncCanonicalMotion(resultText, programText = "", switc
|
||||
return motion;
|
||||
}
|
||||
|
||||
function feedModesBySourceLine(programText) {
|
||||
const modes = [{ line: 0, feedMode: "units-per-minute" }];
|
||||
String(programText).split(/\r?\n/).forEach((line, index) => {
|
||||
const codeOnly = stripComments(line);
|
||||
let activeMode = null;
|
||||
for (const match of codeOnly.matchAll(/\bG\s*([0-9]+(?:\.[0-9]+)?)\b/gi)) {
|
||||
const value = Number(match[1]);
|
||||
if (value === 93) activeMode = "inverse-time";
|
||||
if (value === 94) activeMode = "units-per-minute";
|
||||
}
|
||||
if (activeMode) {
|
||||
modes.push({ line: index + 1, feedMode: activeMode });
|
||||
}
|
||||
});
|
||||
return modes;
|
||||
}
|
||||
|
||||
function latestFeedModeAtOrBeforeLine(modes, sourceLine) {
|
||||
let feedMode = "units-per-minute";
|
||||
for (const entry of modes) {
|
||||
if (entry.line <= sourceLine) feedMode = entry.feedMode;
|
||||
}
|
||||
return feedMode;
|
||||
}
|
||||
|
||||
function linearUnitsBySourceLine(programText) {
|
||||
const units = [{ line: 0, linearUnits: "mm" }];
|
||||
String(programText).split(/\r?\n/).forEach((line, index) => {
|
||||
const codeOnly = stripComments(line);
|
||||
let activeUnits = null;
|
||||
for (const match of codeOnly.matchAll(/\bG\s*([0-9]+(?:\.[0-9]+)?)\b/gi)) {
|
||||
const value = Number(match[1]);
|
||||
if (value === 20) activeUnits = "inch";
|
||||
if (value === 21) activeUnits = "mm";
|
||||
}
|
||||
if (activeUnits) {
|
||||
units.push({ line: index + 1, linearUnits: activeUnits });
|
||||
}
|
||||
});
|
||||
return units;
|
||||
}
|
||||
|
||||
function latestLinearUnitsAtOrBeforeLine(units, sourceLine) {
|
||||
let linearUnits = "mm";
|
||||
for (const entry of units) {
|
||||
if (entry.line <= sourceLine) linearUnits = entry.linearUnits;
|
||||
}
|
||||
return linearUnits;
|
||||
}
|
||||
|
||||
function feedRatesBySourceLine(programText) {
|
||||
const rates = [];
|
||||
String(programText).split(/\r?\n/).forEach((line, index) => {
|
||||
|
||||
@@ -93,6 +93,17 @@ export function wrapTaskHalSdk(sdk, {
|
||||
return rc;
|
||||
},
|
||||
|
||||
loadProgramMotionPlan(plan = {}) {
|
||||
if (typeof sdk.loadProgramMotionPlan !== "function") {
|
||||
throw new Error("task/HAL SDK missing loadProgramMotionPlan; rebuild wasm-port/tools/build_task_hal_wasm.sh");
|
||||
}
|
||||
const rc = sdk.loadProgramMotionPlan(plan);
|
||||
if (rc !== 0) {
|
||||
throw new Error(`lctask_load_program_motion_plan_json failed rc=${rc}`);
|
||||
}
|
||||
return rc;
|
||||
},
|
||||
|
||||
sendCommand(command) {
|
||||
const rc = sdk.sendCommand(command);
|
||||
if (rc !== 0) {
|
||||
@@ -152,10 +163,65 @@ export function buildTaskHalSessionFromMachineFiles({ profile, plan, save, selec
|
||||
};
|
||||
}
|
||||
|
||||
export function buildTaskHalProgramMotionPlan({
|
||||
programPath = null,
|
||||
motion = [],
|
||||
timing = null,
|
||||
linearUnits = "mm",
|
||||
programLines = [],
|
||||
} = {}) {
|
||||
const segments = Array.isArray(timing?.segments) ? timing.segments : [];
|
||||
let planSegments = segments.map((segment, index) => {
|
||||
const event = motion[index] || {};
|
||||
const startAxes = normalizePlanAxes(segment.startAxes || motion[index - 1]?.axes || {});
|
||||
const endAxes = normalizePlanAxes(segment.endAxes || segment.axes || event.axes || startAxes);
|
||||
return {
|
||||
line: Number(segment.line ?? event.line ?? index + 1),
|
||||
type: segment.type || event.type || "STRAIGHT_FEED",
|
||||
motionClass: segment.motionClass || (event.type === "STRAIGHT_TRAVERSE" ? "rapid" : "feed"),
|
||||
feedMode: segment.feedMode || event.feedMode || "units-per-minute",
|
||||
startSeconds: Number(segment.startSeconds || 0),
|
||||
durationSeconds: Math.max(Number(segment.durationSeconds || 0), 0),
|
||||
elapsedSeconds: Number(segment.elapsedSeconds || 0),
|
||||
feedRate: Number(segment.feedRate || event.feedRate || 0),
|
||||
linearUnits: segment.linearUnits || event.linearUnits || linearUnits,
|
||||
velocityMmPerMin: Math.max(Number(segment.velocityMmPerMin || 0), 0),
|
||||
requestedVelocityMmPerMin: Math.max(Number(segment.requestedVelocityMmPerMin || segment.velocityMmPerMin || 0), 0),
|
||||
startAxes,
|
||||
endAxes,
|
||||
};
|
||||
}).filter((segment) => segment.durationSeconds > 0 || segment.line > 0);
|
||||
const lineSegments = buildSourceLineMotionSegments({
|
||||
programLines,
|
||||
seedSegments: planSegments,
|
||||
linearUnits,
|
||||
});
|
||||
if (shouldUseSourceLineSegments(planSegments, lineSegments)) {
|
||||
planSegments = lineSegments;
|
||||
}
|
||||
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-task-hal-program-motion-plan",
|
||||
semanticBoundary: "linuxcnc_canonical_motion_feed_timed_task_hal_plan",
|
||||
programPath,
|
||||
linearUnits,
|
||||
totalSeconds: Number(timing?.totalSeconds || 0),
|
||||
segmentCount: planSegments.length,
|
||||
segments: planSegments,
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeTaskHalStatus(status = {}) {
|
||||
const motion = status.motionStatus?.motion || {};
|
||||
const axis = status.motionStatus?.axis || {};
|
||||
const halPins = status.halSnapshot?.pins || {};
|
||||
const motionProgramLine = Number(motion.programLine || 0);
|
||||
const halProgramLine = Number(halPins["motion.program-line"]?.value || 0);
|
||||
const activeLine = motionProgramLine > 0
|
||||
? motionProgramLine
|
||||
: halProgramLine > 0
|
||||
? halProgramLine
|
||||
: 1;
|
||||
return {
|
||||
...status,
|
||||
semanticBoundary: SEMANTIC_BOUNDARY,
|
||||
@@ -183,7 +249,13 @@ export function normalizeTaskHalStatus(status = {}) {
|
||||
halChangedPinCount: Array.isArray(status.halSnapshot?.changedPins)
|
||||
? status.halSnapshot.changedPins.length
|
||||
: Number(status.halSnapshot?.changedPinCount || 0),
|
||||
activeLine: Number(motion.programLine || halPins["motion.program-line"]?.value || 1),
|
||||
activeLine,
|
||||
motionProgramLine,
|
||||
halProgramLine,
|
||||
activeLineSource: motionProgramLine > 0 ? "motion-status" : halProgramLine > 0 ? "hal-pin" : "fallback",
|
||||
activeLineHalSynced: motionProgramLine > 0 && halProgramLine > 0
|
||||
? motionProgramLine === halProgramLine
|
||||
: false,
|
||||
switchkinsType: Number(motion.switchkinsType ?? halPins["motion.switchkins-type"]?.value ?? 0),
|
||||
axisPose: {
|
||||
x: Number(axis.x ?? halPins["axis.0.pos-cmd"]?.value ?? 0),
|
||||
@@ -199,6 +271,133 @@ export function normalizeTaskHalStatus(status = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
function shouldUseSourceLineSegments(planSegments, lineSegments) {
|
||||
if (lineSegments.length < 3) return false;
|
||||
const plannedLines = new Set(planSegments.map((segment) => Number(segment.line)).filter(Number.isFinite));
|
||||
const lineCount = lineSegments.length;
|
||||
return plannedLines.size <= 2 && lineCount > plannedLines.size;
|
||||
}
|
||||
|
||||
function buildSourceLineMotionSegments({
|
||||
programLines = [],
|
||||
seedSegments = [],
|
||||
linearUnits = "mm",
|
||||
} = {}) {
|
||||
if (!Array.isArray(programLines) || programLines.length === 0) return [];
|
||||
const seedByLine = new Map(seedSegments.map((segment) => [Number(segment.line), segment]));
|
||||
const axes = normalizePlanAxes(seedSegments[0]?.startAxes || {});
|
||||
const segments = [];
|
||||
let elapsedSeconds = 0;
|
||||
let feedRate = firstPositive(seedSegments.map((segment) => segment.feedRate), 100);
|
||||
let rapidVelocity = firstPositive(
|
||||
seedSegments.filter((segment) => segment.motionClass === "rapid").map((segment) => segment.velocityMmPerMin),
|
||||
2100,
|
||||
);
|
||||
|
||||
for (let index = 0; index < programLines.length; index += 1) {
|
||||
const line = index + 1;
|
||||
const code = stripSourceLineComments(programLines[index]);
|
||||
if (!isExecutableGcodeLine(code)) continue;
|
||||
|
||||
const seed = seedByLine.get(line) || null;
|
||||
const startAxes = normalizePlanAxes(seed?.startAxes || axes);
|
||||
const parsed = parseGcodeLineMotion(code, startAxes, feedRate);
|
||||
if (parsed.feedRate > 0) feedRate = parsed.feedRate;
|
||||
const motionClass = seed?.motionClass || parsed.motionClass;
|
||||
const endAxes = normalizePlanAxes(seed?.endAxes || parsed.endAxes);
|
||||
const velocityMmPerMin = Number(seed?.velocityMmPerMin) > 0
|
||||
? Number(seed.velocityMmPerMin)
|
||||
: motionClass === "rapid"
|
||||
? rapidVelocity
|
||||
: Math.max(feedRate, 1);
|
||||
if (motionClass === "rapid" && velocityMmPerMin > 0) {
|
||||
rapidVelocity = velocityMmPerMin;
|
||||
}
|
||||
const durationSeconds = Math.max(
|
||||
Number(seed?.durationSeconds || 0),
|
||||
estimateLineDurationSeconds(startAxes, endAxes, velocityMmPerMin),
|
||||
0.05,
|
||||
);
|
||||
const segment = {
|
||||
line,
|
||||
type: seed?.type || parsed.type,
|
||||
motionClass,
|
||||
feedMode: seed?.feedMode || parsed.feedMode,
|
||||
startSeconds: elapsedSeconds,
|
||||
durationSeconds,
|
||||
elapsedSeconds: elapsedSeconds + durationSeconds,
|
||||
feedRate,
|
||||
linearUnits: seed?.linearUnits || linearUnits,
|
||||
velocityMmPerMin,
|
||||
requestedVelocityMmPerMin: Number(seed?.requestedVelocityMmPerMin || velocityMmPerMin),
|
||||
startAxes,
|
||||
endAxes,
|
||||
};
|
||||
segments.push(segment);
|
||||
Object.assign(axes, endAxes);
|
||||
elapsedSeconds += durationSeconds;
|
||||
}
|
||||
return segments;
|
||||
}
|
||||
|
||||
function parseGcodeLineMotion(code, startAxes, currentFeedRate) {
|
||||
const numberPattern = "[-+]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)";
|
||||
const gCodes = [...code.matchAll(new RegExp(`\\bG\\s*(${numberPattern})\\b`, "gi"))].map((match) => Number(match[1]));
|
||||
const feedMode = gCodes.includes(93) ? "inverse-time" : "units-per-minute";
|
||||
const rapid = gCodes.includes(0);
|
||||
const feed = gCodes.some((value) => value === 1 || value === 2 || value === 3);
|
||||
const endAxes = { ...startAxes };
|
||||
for (const axis of ["x", "y", "z", "a", "b", "c", "u", "v", "w"]) {
|
||||
const match = code.match(new RegExp(`\\b${axis}\\s*(${numberPattern})`, "i"));
|
||||
if (match) endAxes[axis] = Number(match[1]);
|
||||
}
|
||||
const feedMatch = code.match(new RegExp(`\\bF\\s*(${numberPattern})`, "i"));
|
||||
const feedRate = feedMatch && Number(feedMatch[1]) > 0 ? Number(feedMatch[1]) : Number(currentFeedRate || 0);
|
||||
return {
|
||||
type: rapid ? "STRAIGHT_TRAVERSE" : "STRAIGHT_FEED",
|
||||
motionClass: rapid && !feed ? "rapid" : "feed",
|
||||
feedMode,
|
||||
feedRate,
|
||||
endAxes,
|
||||
};
|
||||
}
|
||||
|
||||
function stripSourceLineComments(line) {
|
||||
return String(line || "")
|
||||
.replace(/\([^)]*\)/g, " ")
|
||||
.replace(/;.*$/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function isExecutableGcodeLine(code) {
|
||||
if (!code || code === "%") return false;
|
||||
return /\b[GMTXYZABCUVWF]\s*[-+]?\d/i.test(code);
|
||||
}
|
||||
|
||||
function estimateLineDurationSeconds(startAxes, endAxes, velocityMmPerMin) {
|
||||
const distance = Math.sqrt(["x", "y", "z"].reduce((total, axis) => {
|
||||
const delta = Number(endAxes[axis] || 0) - Number(startAxes[axis] || 0);
|
||||
return total + delta * delta;
|
||||
}, 0));
|
||||
if (distance <= 0 || velocityMmPerMin <= 0) return 0;
|
||||
return distance / Math.max(velocityMmPerMin / 60, 0.000001);
|
||||
}
|
||||
|
||||
function firstPositive(values, fallback) {
|
||||
for (const value of values) {
|
||||
const number = Number(value);
|
||||
if (Number.isFinite(number) && number > 0) return number;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function normalizePlanAxes(axes = {}) {
|
||||
return Object.fromEntries(["x", "y", "z", "a", "b", "c", "u", "v", "w"].map((axis) => [
|
||||
axis,
|
||||
Number.isFinite(Number(axes[axis])) ? Number(axes[axis]) : 0,
|
||||
]));
|
||||
}
|
||||
|
||||
function isJogMotion(motion = {}) {
|
||||
return Number(motion.motionType) === 3 || Number(motion.teleopMode) === 1 || motion.teleopMode === true;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ export async function createLinuxCncTaskHalWorkerRuntime({
|
||||
initSession: (session) => client.call("initSession", { session }),
|
||||
stageFiles: (files) => client.call("stageFiles", { files }),
|
||||
openProgram: (path) => client.call("openProgram", { path }),
|
||||
loadProgramMotionPlan: (plan) => client.call("loadProgramMotionPlan", { plan }),
|
||||
sendCommand: (command) => client.call("command", { command }),
|
||||
runCycles: (options) => client.call("runCycles", { options }),
|
||||
readStatus: () => client.call("readStatus"),
|
||||
|
||||
@@ -33,6 +33,9 @@ async function handleMessage(type, payload) {
|
||||
case "openProgram":
|
||||
assertRuntime();
|
||||
return runtime.openProgram(payload.path);
|
||||
case "loadProgramMotionPlan":
|
||||
assertRuntime();
|
||||
return runtime.loadProgramMotionPlan(payload.plan || {});
|
||||
case "command":
|
||||
assertRuntime();
|
||||
return runtime.sendCommand(payload.command);
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
stageProfileMachineFiles,
|
||||
} from "../runtime/linuxcnc-machine-file-staging.js";
|
||||
import {
|
||||
buildTaskHalProgramMotionPlan,
|
||||
buildTaskHalSessionFromMachineFiles,
|
||||
} from "../runtime/linuxcnc-task-hal-runtime.js";
|
||||
import {
|
||||
@@ -42,6 +43,40 @@ const initialAxisPose = {
|
||||
c: 0.0,
|
||||
};
|
||||
|
||||
function createTaskHalStatusLoopState({
|
||||
active = false,
|
||||
sequence = 0,
|
||||
profileId = null,
|
||||
iniPath = null,
|
||||
kinematicsModuleId = null,
|
||||
tickCount = 0,
|
||||
batchSize = 5,
|
||||
intervalMs = 25,
|
||||
taskPeriodNs = 10000000,
|
||||
servoPeriodNs = 1000000,
|
||||
lastStatusAt = null,
|
||||
lastError = null,
|
||||
stopReason = null,
|
||||
} = {}) {
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-task-hal-status-loop",
|
||||
active,
|
||||
sequence,
|
||||
profileId,
|
||||
iniPath,
|
||||
kinematicsModuleId,
|
||||
tickCount,
|
||||
batchSize,
|
||||
intervalMs,
|
||||
taskPeriodNs,
|
||||
servoPeriodNs,
|
||||
lastStatusAt,
|
||||
lastError,
|
||||
stopReason,
|
||||
semanticBoundary: "js_status_polling_loop_for_linuxcnc_task_hal_motion_status",
|
||||
};
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
machineProfile: "xyzac-trt",
|
||||
availableProfiles: fiveAxisProfiles.map(({ id, title, traj, kinematicsModuleId, kinematics }) => ({
|
||||
@@ -136,12 +171,14 @@ const initialState = {
|
||||
programExecutionMotionIndex: 0,
|
||||
programExecutionSampleIndex: 0,
|
||||
programRuntimeFeedback: null,
|
||||
programRuntimeFeedbackHistory: [],
|
||||
taskHalRuntime: null,
|
||||
taskHalRuntimeReadiness: null,
|
||||
taskHalStatus: null,
|
||||
taskHalSession: null,
|
||||
taskHalExecutionPending: false,
|
||||
taskHalExecutionSequence: 0,
|
||||
taskHalStatusLoop: createTaskHalStatusLoopState(),
|
||||
taskHalFallbackReason: null,
|
||||
pendingJogCommand: null,
|
||||
interpreterExecutionPending: false,
|
||||
@@ -249,6 +286,7 @@ export function createSimulationStore(seed = {}) {
|
||||
};
|
||||
state.fullExecutionBoundary = createFullLinuxCncExecutionBoundary(state);
|
||||
const listeners = new Set();
|
||||
let taskHalStatusLoopTimer = null;
|
||||
|
||||
const notify = () => {
|
||||
for (const listener of listeners) {
|
||||
@@ -772,7 +810,7 @@ export function createSimulationStore(seed = {}) {
|
||||
},
|
||||
machine: {
|
||||
...state.machine,
|
||||
mode: "auto",
|
||||
mode: state.machine.mode,
|
||||
},
|
||||
axisPose: initialAxisPose,
|
||||
runState: "idle",
|
||||
@@ -799,12 +837,51 @@ export function createSimulationStore(seed = {}) {
|
||||
});
|
||||
break;
|
||||
case "TASK_HAL_STATUS_APPLIED":
|
||||
setState(applyTaskHalStatusPatch(state, action.status, action.operatorMessage));
|
||||
setState(applyTaskHalStatusPatch(state, action.status, action.operatorMessage, {
|
||||
loopSequence: action.loopSequence,
|
||||
preserveMachine: action.preserveMachine,
|
||||
}));
|
||||
break;
|
||||
case "TASK_HAL_STATUS_LOOP_STARTED":
|
||||
setState({
|
||||
taskHalStatusLoop: {
|
||||
...createTaskHalStatusLoopState({
|
||||
active: true,
|
||||
sequence: action.sequence,
|
||||
profileId: action.profileId,
|
||||
iniPath: action.iniPath,
|
||||
kinematicsModuleId: action.kinematicsModuleId,
|
||||
batchSize: action.batchSize,
|
||||
intervalMs: action.intervalMs,
|
||||
taskPeriodNs: action.taskPeriodNs,
|
||||
servoPeriodNs: action.servoPeriodNs,
|
||||
}),
|
||||
},
|
||||
programRuntimeFeedbackHistory: [],
|
||||
operatorMessage: action.operatorMessage || "task/HAL status loop running",
|
||||
});
|
||||
break;
|
||||
case "TASK_HAL_STATUS_LOOP_STOPPED":
|
||||
setState({
|
||||
taskHalStatusLoop: {
|
||||
...state.taskHalStatusLoop,
|
||||
active: false,
|
||||
stopReason: action.reason || "stopped",
|
||||
lastError: action.error || null,
|
||||
},
|
||||
operatorMessage: action.operatorMessage || state.operatorMessage,
|
||||
});
|
||||
break;
|
||||
case "TASK_HAL_COMMAND_FAILED":
|
||||
setState({
|
||||
taskHalFallbackReason: action.error,
|
||||
taskHalExecutionPending: false,
|
||||
taskHalStatusLoop: {
|
||||
...state.taskHalStatusLoop,
|
||||
active: false,
|
||||
lastError: action.error,
|
||||
stopReason: "error",
|
||||
},
|
||||
pendingJogCommand: null,
|
||||
operatorMessage: `task/HAL fallback: ${action.error}`,
|
||||
});
|
||||
@@ -850,13 +927,29 @@ export function createSimulationStore(seed = {}) {
|
||||
setState({ operatorMessage: gate.operatorMessage });
|
||||
break;
|
||||
}
|
||||
const turningOff = state.machine.taskState === "on" || state.machine.powerOn;
|
||||
if (state.taskHalRuntime?.loaded) {
|
||||
setState({
|
||||
machine: {
|
||||
...state.machine,
|
||||
powerOn: !turningOff,
|
||||
estopActive: false,
|
||||
taskState: turningOff ? "estop-reset" : "on",
|
||||
interpState: "idle",
|
||||
interpResumeState: "idle",
|
||||
taskPaused: false,
|
||||
},
|
||||
runState: turningOff ? "powered-off" : "idle",
|
||||
feed: turningOff ? { ...state.feed, currentVelocity: 0 } : state.feed,
|
||||
coolant: turningOff ? { ...state.coolant, flood: false, mist: false } : state.coolant,
|
||||
spindle: turningOff ? { ...state.spindle, enabled: false } : state.spindle,
|
||||
operatorMessage: turningOff ? "task/HAL machine power off" : "task/HAL machine power on",
|
||||
});
|
||||
runTaskHalCommandSequence([
|
||||
{ type: "EMC_TASK_SET_STATE", state: state.machine.powerOn ? "ESTOP_RESET" : "ON" },
|
||||
], { operatorMessage: state.machine.powerOn ? "task/HAL machine power off" : "task/HAL machine power on" }).catch(() => {});
|
||||
{ type: "EMC_TASK_SET_STATE", state: turningOff ? "ESTOP_RESET" : "ON" },
|
||||
], { operatorMessage: turningOff ? "task/HAL machine power off" : "task/HAL machine power on" }).catch(() => {});
|
||||
break;
|
||||
}
|
||||
const turningOff = state.machine.taskState === "on" || state.machine.powerOn;
|
||||
setState({
|
||||
machine: {
|
||||
...state.machine,
|
||||
@@ -1108,6 +1201,9 @@ export function createSimulationStore(seed = {}) {
|
||||
break;
|
||||
}
|
||||
if (state.taskHalRuntime?.loaded) {
|
||||
stopTaskHalStatusLoop(action.type === "ABORT" ? "aborted" : "stopped", {
|
||||
operatorMessage: action.type === "ABORT" ? "task/HAL abort requested" : "task/HAL stop requested",
|
||||
});
|
||||
runTaskHalCommandSequence([
|
||||
{ type: "EMC_TASK_ABORT" },
|
||||
], { operatorMessage: action.type === "ABORT" ? "task/HAL abort complete" : "task/HAL program stopped" }).catch(() => {});
|
||||
@@ -1137,6 +1233,7 @@ export function createSimulationStore(seed = {}) {
|
||||
break;
|
||||
}
|
||||
if (state.taskHalRuntime?.loaded) {
|
||||
stopTaskHalStatusLoop("paused", { operatorMessage: "task/HAL pause requested" });
|
||||
runTaskHalCommandSequence([
|
||||
{ type: "EMC_TASK_PLAN_PAUSE" },
|
||||
], { operatorMessage: "task/HAL program paused" }).catch(() => {});
|
||||
@@ -1166,7 +1263,15 @@ export function createSimulationStore(seed = {}) {
|
||||
if (state.taskHalRuntime?.loaded) {
|
||||
runTaskHalCommandSequence([
|
||||
{ type: "EMC_TASK_PLAN_RESUME" },
|
||||
], { operatorMessage: "task/HAL program resumed" }).catch(() => {});
|
||||
], {
|
||||
operatorMessage: "task/HAL program resumed",
|
||||
}).then(() => {
|
||||
if (state.runState === "running" || state.machine.interpState === "reading") {
|
||||
startTaskHalStatusLoop({
|
||||
operatorMessage: "task/HAL status loop resumed",
|
||||
});
|
||||
}
|
||||
}).catch(() => {});
|
||||
break;
|
||||
}
|
||||
const resumeState = state.machine.interpResumeState === "idle"
|
||||
@@ -1191,6 +1296,14 @@ export function createSimulationStore(seed = {}) {
|
||||
setState({ operatorMessage: gate.operatorMessage });
|
||||
break;
|
||||
}
|
||||
if (state.taskHalRuntime?.loaded) {
|
||||
stopTaskHalStatusLoop("step", { operatorMessage: "task/HAL step requested" });
|
||||
runTaskHalCommandSequence([], {
|
||||
taskCycles: 1,
|
||||
operatorMessage: "task/HAL stepped one cycle",
|
||||
}).catch(() => {});
|
||||
break;
|
||||
}
|
||||
const playback = nextProgramRuntimeSamplePlayback(state, 1);
|
||||
setState({
|
||||
machine: {
|
||||
@@ -1259,6 +1372,11 @@ export function createSimulationStore(seed = {}) {
|
||||
setState({ operatorMessage: gate.operatorMessage });
|
||||
break;
|
||||
}
|
||||
if (state.taskHalRuntime?.loaded) {
|
||||
runTaskHalCommandSequence([
|
||||
{ type: "EMC_JOINT_HOME", joint: -1 },
|
||||
], { operatorMessage: "task/HAL machine homed" }).catch(() => {});
|
||||
}
|
||||
setState({
|
||||
machine: {
|
||||
...state.machine,
|
||||
@@ -1541,6 +1659,13 @@ export function createSimulationStore(seed = {}) {
|
||||
if (!state.taskHalRuntime?.loaded || !state.machineFileStaging?.save?.files?.length) {
|
||||
return null;
|
||||
}
|
||||
const preserveMachine = {
|
||||
powerOn: state.machine.powerOn,
|
||||
estopActive: state.machine.estopActive,
|
||||
taskState: state.machine.taskState,
|
||||
mode: state.machine.mode,
|
||||
allHomed: state.machine.allHomed,
|
||||
};
|
||||
const selectedPlan = selectMachineFileProgramForState(state);
|
||||
const session = buildTaskHalSessionFromMachineFiles({
|
||||
profile: state.profile,
|
||||
@@ -1559,12 +1684,28 @@ export function createSimulationStore(seed = {}) {
|
||||
await state.taskHalRuntime.stageFiles(session.files);
|
||||
if (openProgram && session.programPath) {
|
||||
await state.taskHalRuntime.openProgram(session.programPath);
|
||||
await loadTaskHalMotionPlanForSession(session);
|
||||
}
|
||||
if (preserveMachine.powerOn) {
|
||||
await state.taskHalRuntime.sendCommand({ type: "EMC_TASK_SET_STATE", state: "ON" });
|
||||
}
|
||||
if (preserveMachine.allHomed) {
|
||||
await state.taskHalRuntime.sendCommand({ type: "EMC_JOINT_HOME", joint: -1 });
|
||||
}
|
||||
await state.taskHalRuntime.sendCommand({
|
||||
type: "EMC_TASK_SET_MODE",
|
||||
mode: normalizeLinuxCncTaskMode(preserveMachine.mode).toUpperCase(),
|
||||
});
|
||||
await state.taskHalRuntime.runCycles({
|
||||
...deriveTaskHalCyclePeriods(state),
|
||||
taskCycles: 1,
|
||||
});
|
||||
dispatch({ type: "TASK_HAL_SESSION_READY", session });
|
||||
const status = await state.taskHalRuntime.readStatus();
|
||||
dispatch({
|
||||
type: "TASK_HAL_STATUS_APPLIED",
|
||||
status,
|
||||
preserveMachine,
|
||||
operatorMessage: `LinuxCNC task/HAL session ready ${session.programPath || "-"}`,
|
||||
});
|
||||
return session;
|
||||
@@ -1581,6 +1722,11 @@ export function createSimulationStore(seed = {}) {
|
||||
if (!state.taskHalSession || (expectedProgramPath && state.taskHalSession.programPath !== expectedProgramPath)) {
|
||||
await initializeTaskHalSession({ openProgram: true });
|
||||
}
|
||||
const loadedMotionPlan = await loadTaskHalMotionPlanForSession(state.taskHalSession);
|
||||
if (!loadedMotionPlan) {
|
||||
setState({ operatorMessage: "run blocked: task/HAL feed motion plan not loaded" });
|
||||
return null;
|
||||
}
|
||||
|
||||
const ready = validateRunPreconditions(state, { requireTaskHalSession: true });
|
||||
if (!ready.ok) {
|
||||
@@ -1588,21 +1734,151 @@ export function createSimulationStore(seed = {}) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return runTaskHalCommandSequence([
|
||||
stopTaskHalStatusLoop("restarted", {
|
||||
operatorMessage: "task/HAL status loop restarting",
|
||||
});
|
||||
|
||||
const status = await runTaskHalCommandSequence([
|
||||
{ type: "EMC_TASK_SET_STATE", state: "ON" },
|
||||
{ type: "EMC_TASK_SET_MODE", mode: "AUTO" },
|
||||
{ type: "EMC_TASK_PLAN_RUN", line: Math.max(Number(state.activeLine || 1) - Number(state.programStartLine || 1), 0) },
|
||||
{ type: "EMC_TASK_PLAN_RUN", line: 0 },
|
||||
], {
|
||||
taskCycles: 5,
|
||||
operatorMessage: `task/HAL program run ${ready.profileId} ${ready.kinematicsModuleId}`,
|
||||
allowFixtureSession: false,
|
||||
});
|
||||
if (shouldContinueTaskHalStatusLoop(state, status)) {
|
||||
startTaskHalStatusLoop({
|
||||
profileId: ready.profileId,
|
||||
iniPath: ready.iniPath,
|
||||
kinematicsModuleId: ready.kinematicsModuleId,
|
||||
operatorMessage: `task/HAL status loop running ${ready.profileId} ${ready.kinematicsModuleId}`,
|
||||
});
|
||||
}
|
||||
return status;
|
||||
};
|
||||
|
||||
const loadTaskHalMotionPlanForSession = async (session = state.taskHalSession) => {
|
||||
if (!state.taskHalRuntime?.loaded || typeof state.taskHalRuntime.loadProgramMotionPlan !== "function") {
|
||||
return null;
|
||||
}
|
||||
const motion = state.programExecution?.motion || [];
|
||||
const timing = state.programExecutionTiming || buildTimingForState(state, state.programExecution);
|
||||
if (!session?.programPath || !Array.isArray(motion) || motion.length === 0 || !Array.isArray(timing?.segments) || timing.segments.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const plan = buildTaskHalProgramMotionPlan({
|
||||
programPath: session.programPath,
|
||||
motion,
|
||||
timing,
|
||||
programLines: state.programLines,
|
||||
linearUnits: timing.linearUnits || state.profile?.traj?.linearUnits || "mm",
|
||||
});
|
||||
if (plan.segmentCount <= 0) {
|
||||
return null;
|
||||
}
|
||||
await state.taskHalRuntime.loadProgramMotionPlan(plan);
|
||||
return plan;
|
||||
};
|
||||
|
||||
const startTaskHalStatusLoop = ({
|
||||
profileId = state.machineProfile,
|
||||
iniPath = state.profile?.iniPath || null,
|
||||
kinematicsModuleId = state.profile?.kinematicsModuleId || state.machineProfile,
|
||||
batchSize = 5,
|
||||
intervalMs = 25,
|
||||
taskPeriodNs = deriveTaskHalCyclePeriods(state).taskPeriodNs,
|
||||
servoPeriodNs = deriveTaskHalCyclePeriods(state).servoPeriodNs,
|
||||
operatorMessage = "task/HAL status loop running",
|
||||
} = {}) => {
|
||||
if (!state.taskHalRuntime?.loaded) return null;
|
||||
stopTaskHalStatusLoop("restarted", { notify: false });
|
||||
const sequence = Number(state.taskHalStatusLoop?.sequence || 0) + 1;
|
||||
dispatch({
|
||||
type: "TASK_HAL_STATUS_LOOP_STARTED",
|
||||
sequence,
|
||||
profileId,
|
||||
iniPath,
|
||||
kinematicsModuleId,
|
||||
batchSize,
|
||||
intervalMs,
|
||||
taskPeriodNs,
|
||||
servoPeriodNs,
|
||||
operatorMessage,
|
||||
});
|
||||
const tick = () => runTaskHalStatusLoopTick(sequence).catch((error) => {
|
||||
stopTaskHalStatusLoop("error", {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
operatorMessage: `task/HAL status loop failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
});
|
||||
});
|
||||
taskHalStatusLoopTimer = setTimeout(tick, intervalMs);
|
||||
return sequence;
|
||||
};
|
||||
|
||||
const runTaskHalStatusLoopTick = async (sequence) => {
|
||||
const loop = state.taskHalStatusLoop || {};
|
||||
if (!loop.active || loop.sequence !== sequence || !state.taskHalRuntime?.loaded) {
|
||||
return null;
|
||||
}
|
||||
await state.taskHalRuntime.runCycles({
|
||||
taskPeriodNs: loop.taskPeriodNs,
|
||||
servoPeriodNs: loop.servoPeriodNs,
|
||||
taskCycles: loop.batchSize,
|
||||
});
|
||||
const status = await state.taskHalRuntime.readStatus();
|
||||
if (state.taskHalStatusLoop?.sequence !== sequence) {
|
||||
return status;
|
||||
}
|
||||
dispatch({
|
||||
type: "TASK_HAL_STATUS_APPLIED",
|
||||
status,
|
||||
loopSequence: sequence,
|
||||
operatorMessage: `task/HAL status tick ${Number(state.taskHalStatusLoop?.tickCount || 0) + 1}`,
|
||||
});
|
||||
if (shouldContinueTaskHalStatusLoop(state, status)) {
|
||||
taskHalStatusLoopTimer = setTimeout(
|
||||
() => runTaskHalStatusLoopTick(sequence).catch((error) => {
|
||||
stopTaskHalStatusLoop("error", {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
operatorMessage: `task/HAL status loop failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
});
|
||||
}),
|
||||
Number(state.taskHalStatusLoop?.intervalMs || loop.intervalMs || 25),
|
||||
);
|
||||
} else {
|
||||
stopTaskHalStatusLoop(state.runState === "complete" ? "complete" : state.runState, {
|
||||
operatorMessage: state.runState === "complete"
|
||||
? "task/HAL program complete"
|
||||
: `task/HAL status loop ${state.runState}`,
|
||||
});
|
||||
}
|
||||
return status;
|
||||
};
|
||||
|
||||
const stopTaskHalStatusLoop = (reason = "stopped", {
|
||||
error = null,
|
||||
operatorMessage = null,
|
||||
notify = true,
|
||||
} = {}) => {
|
||||
if (taskHalStatusLoopTimer) {
|
||||
clearTimeout(taskHalStatusLoopTimer);
|
||||
taskHalStatusLoopTimer = null;
|
||||
}
|
||||
if (notify && (state.taskHalStatusLoop?.active || state.taskHalStatusLoop?.stopReason !== reason || error)) {
|
||||
dispatch({
|
||||
type: "TASK_HAL_STATUS_LOOP_STOPPED",
|
||||
reason,
|
||||
error,
|
||||
operatorMessage,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const runTaskHalCommandSequence = async (commands, {
|
||||
taskCycles = 1,
|
||||
taskPeriodNs = 10000000,
|
||||
servoPeriodNs = 1000000,
|
||||
taskPeriodNs = deriveTaskHalCyclePeriods(state).taskPeriodNs,
|
||||
servoPeriodNs = deriveTaskHalCyclePeriods(state).servoPeriodNs,
|
||||
operatorMessage = "task/HAL command complete",
|
||||
pendingJogCommand = null,
|
||||
allowFixtureSession = true,
|
||||
@@ -1939,16 +2215,37 @@ function expectedTaskHalProgramPathForState(state = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
function deriveTaskHalCyclePeriods(state = {}) {
|
||||
const taskCycleTimeSeconds = Number(state.linuxCncIniConfig?.task?.cycleTimeSeconds);
|
||||
const iniTaskPeriodNs = Number.isFinite(taskCycleTimeSeconds) && taskCycleTimeSeconds > 0
|
||||
? Math.round(taskCycleTimeSeconds * 1_000_000_000)
|
||||
: null;
|
||||
const iniServoPeriodNs = Number(state.linuxCncIniConfig?.emcmot?.servoPeriodNs);
|
||||
|
||||
return {
|
||||
taskPeriodNs: iniTaskPeriodNs || 10000000,
|
||||
servoPeriodNs: Number.isFinite(iniServoPeriodNs) && iniServoPeriodNs > 0
|
||||
? Math.round(iniServoPeriodNs)
|
||||
: 1000000,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeCoordinates(value) {
|
||||
return String(value || "").replace(/[^A-Za-z]/g, "").toUpperCase();
|
||||
}
|
||||
|
||||
function applyTaskHalStatusPatch(state, status, operatorMessage) {
|
||||
function applyTaskHalStatusPatch(state, status, operatorMessage, {
|
||||
loopSequence = null,
|
||||
preserveMachine = null,
|
||||
} = {}) {
|
||||
const ui = status?.ui || {};
|
||||
const task = status?.task || {};
|
||||
const motion = status?.motionStatus?.motion || {};
|
||||
const taskState = normalizeTaskHalTaskState(ui.taskState || task.state);
|
||||
const taskMode = normalizeLinuxCncTaskMode(ui.taskMode || task.mode || state.machine.mode);
|
||||
const rawTaskState = normalizeTaskHalTaskState(ui.taskState || task.state);
|
||||
const taskState = preserveMachine?.powerOn && rawTaskState === "estop-reset"
|
||||
? "on"
|
||||
: rawTaskState;
|
||||
const taskMode = normalizeLinuxCncTaskMode(preserveMachine?.mode || 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 = resolveTaskHalKinsType(state, status, activeLine);
|
||||
@@ -1958,7 +2255,8 @@ function applyTaskHalStatusPatch(state, status, operatorMessage) {
|
||||
: state.feed.currentVelocity;
|
||||
const paused = interpState === "paused" || motion.paused === true;
|
||||
const aborted = motion.aborted === true;
|
||||
const programComplete = interpState === "idle" && Number(task.nextProgramLine || 0) >= Number(task.openedLineCount || 1);
|
||||
const openedProgramLineCount = Number(task.openedSourceLineCount || task.openedLineCount || 1);
|
||||
const programComplete = interpState === "idle" && Number(task.nextProgramLine || 0) >= openedProgramLineCount;
|
||||
const runState = aborted
|
||||
? "stopped"
|
||||
: paused
|
||||
@@ -1972,6 +2270,13 @@ function applyTaskHalStatusPatch(state, status, operatorMessage) {
|
||||
: state.runState === "jogging"
|
||||
? "jogging"
|
||||
: "idle";
|
||||
const runtimeFeedback = createTaskHalRuntimeFeedback(state, status, axisPose, activeLine);
|
||||
const loopActive = state.taskHalStatusLoop?.active === true
|
||||
&& loopSequence !== null
|
||||
&& Number(state.taskHalStatusLoop.sequence) === Number(loopSequence)
|
||||
&& (runState === "running" || runState === "mdi");
|
||||
const nextTickCount = loopActive ? Number(state.taskHalStatusLoop.tickCount || 0) + 1 : Number(state.taskHalStatusLoop?.tickCount || 0);
|
||||
const feedbackHistory = [runtimeFeedback, ...(state.programRuntimeFeedbackHistory || [])].slice(0, 100);
|
||||
|
||||
return {
|
||||
taskHalStatus: status,
|
||||
@@ -1982,9 +2287,7 @@ function applyTaskHalStatusPatch(state, status, operatorMessage) {
|
||||
axisPose,
|
||||
kinsType,
|
||||
rtcpState: rtcpStateFromKinsType(kinsType),
|
||||
programExecutionSourceMode: state.programExecution
|
||||
? state.programExecutionSourceMode
|
||||
: "linuxcnc-task-motion-hal-wasm",
|
||||
programExecutionSourceMode: "linuxcnc-task-motion-hal-wasm",
|
||||
machine: {
|
||||
...state.machine,
|
||||
powerOn: taskState === "on",
|
||||
@@ -1994,17 +2297,42 @@ function applyTaskHalStatusPatch(state, status, operatorMessage) {
|
||||
interpState,
|
||||
interpResumeState: paused ? state.machine.interpResumeState || "reading" : interpState,
|
||||
taskPaused: paused,
|
||||
allHomed: Boolean(preserveMachine?.allHomed ?? state.machine.allHomed),
|
||||
},
|
||||
runState,
|
||||
taskHalStatusLoop: loopSequence === null
|
||||
? state.taskHalStatusLoop
|
||||
: {
|
||||
...state.taskHalStatusLoop,
|
||||
active: loopActive,
|
||||
tickCount: nextTickCount,
|
||||
lastStatusAt: new Date().toISOString(),
|
||||
stopReason: loopActive ? null : runState,
|
||||
},
|
||||
feed: {
|
||||
...state.feed,
|
||||
currentVelocity,
|
||||
},
|
||||
programRuntimeFeedback: createTaskHalRuntimeFeedback(state, status, axisPose, activeLine),
|
||||
programRuntimeFeedback: runtimeFeedback,
|
||||
programRuntimeFeedbackHistory: feedbackHistory,
|
||||
operatorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
function shouldContinueTaskHalStatusLoop(state = {}, status = {}) {
|
||||
const ui = status?.ui || {};
|
||||
const task = status?.task || {};
|
||||
const motion = status?.motionStatus?.motion || {};
|
||||
const interpState = normalizeTaskHalInterpState(ui.interpState || task.interpState);
|
||||
const taskMode = normalizeLinuxCncTaskMode(ui.taskMode || task.mode || state.machine?.mode);
|
||||
const aborted = motion.aborted === true;
|
||||
const paused = interpState === "paused" || motion.paused === true;
|
||||
const openedProgramLineCount = Number(task.openedSourceLineCount || task.openedLineCount || 1);
|
||||
const complete = interpState === "idle"
|
||||
&& Number(task.nextProgramLine || 0) >= openedProgramLineCount;
|
||||
return !aborted && !paused && !complete && (interpState === "reading" || taskMode === "mdi");
|
||||
}
|
||||
|
||||
function resolveTaskHalKinsType(state, status, activeLine) {
|
||||
const ui = status?.ui || {};
|
||||
const numeric = Number(ui.switchkinsType);
|
||||
@@ -2095,6 +2423,8 @@ function wouldResetNonZeroPoseToLocalZero(currentPose = {}, nextPose = {}) {
|
||||
function createTaskHalRuntimeFeedback(state, status, axisPose, activeLine) {
|
||||
const ui = status?.ui || {};
|
||||
const motion = status?.motionStatus?.motion || {};
|
||||
const halProgramLine = Number(status?.halSnapshot?.pins?.["motion.program-line"]?.value || 0);
|
||||
const motionProgramLine = Number(motion.programLine || 0);
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-program-runtime-feedback",
|
||||
sourceMode: "linuxcnc-task-motion-hal-wasm",
|
||||
@@ -2102,6 +2432,10 @@ function createTaskHalRuntimeFeedback(state, status, axisPose, activeLine) {
|
||||
sampleIndex: Number(ui.servoCycle || 0),
|
||||
motionIndex: Math.max(Number(ui.activeLine || 1) - 1, 0),
|
||||
line: activeLine,
|
||||
motionProgramLine,
|
||||
halProgramLine,
|
||||
activeLineSource: ui.activeLineSource || (motionProgramLine > 0 ? "motion-status" : halProgramLine > 0 ? "hal-pin" : "fallback"),
|
||||
activeLineHalSynced: motionProgramLine > 0 && halProgramLine > 0 && motionProgramLine === halProgramLine,
|
||||
type: Number(motion.motionType || 0) === 3 ? "JOG" : "TASK_MOTION",
|
||||
timeSeconds: Number(ui.taskCycle || 0) * 0.01,
|
||||
axisPose,
|
||||
@@ -2452,6 +2786,10 @@ function nextProgramRuntimeSamplePlayback(state, step) {
|
||||
const sample = samples[sampleIndex];
|
||||
const motionIndex = clampMotionIndex(state, sample.motionIndex);
|
||||
const motion = state.programExecution?.motion?.[motionIndex] || null;
|
||||
const sampleWithUnits = {
|
||||
...sample,
|
||||
linearUnits: sample.linearUnits || motion?.linearUnits || state.profile.traj?.linearUnits,
|
||||
};
|
||||
const segment = timing?.segments?.[motionIndex] || null;
|
||||
const kinsType = kinsTypeFromProgramMotion(state, motion) || state.kinsType;
|
||||
const elapsedSeconds = Number(sample.timeSeconds) || Number(segment?.elapsedSeconds) || 0;
|
||||
@@ -2461,7 +2799,7 @@ function nextProgramRuntimeSamplePlayback(state, step) {
|
||||
|| 0;
|
||||
const runtimeFeedback = createProgramRuntimeFeedbackFromSample({
|
||||
state,
|
||||
sample,
|
||||
sample: sampleWithUnits,
|
||||
sampleIndex,
|
||||
motion,
|
||||
motionIndex,
|
||||
@@ -2473,7 +2811,7 @@ function nextProgramRuntimeSamplePlayback(state, step) {
|
||||
motionIndex,
|
||||
sampleIndex,
|
||||
activeLine: sample.line || motion?.line || state.activeLine,
|
||||
axisPose: axisPoseFromRuntimeSample(sample, motion, state.axisPose),
|
||||
axisPose: axisPoseFromRuntimeSample(sampleWithUnits, motion, state.axisPose),
|
||||
kinsType,
|
||||
rtcpState: rtcpStateFromKinsType(kinsType),
|
||||
timing: {
|
||||
@@ -2507,9 +2845,13 @@ function nextProgramRuntimeSamplePlayback(state, step) {
|
||||
function createInitialProgramRuntimeFeedback({ state, timing, motion, timingSnapshot }) {
|
||||
const firstSample = timing?.samples?.[0] || null;
|
||||
if (firstSample) {
|
||||
const sampleWithUnits = {
|
||||
...firstSample,
|
||||
linearUnits: firstSample.linearUnits || motion?.linearUnits || state.profile.traj?.linearUnits,
|
||||
};
|
||||
return createProgramRuntimeFeedbackFromSample({
|
||||
state,
|
||||
sample: firstSample,
|
||||
sample: sampleWithUnits,
|
||||
sampleIndex: 0,
|
||||
motion,
|
||||
motionIndex: clampMotionIndex(state, firstSample.motionIndex),
|
||||
@@ -2537,11 +2879,13 @@ function clampMotionIndex(state, motionIndex) {
|
||||
}
|
||||
|
||||
function buildTimingForState(state, execution) {
|
||||
if (execution?.plannerTiming?.plannerRuntimeReady === true) {
|
||||
const motion = execution?.motion || [];
|
||||
const requiresFeedModeTiming = motion.some((event) => event?.feedMode === "inverse-time");
|
||||
if (!requiresFeedModeTiming && execution?.plannerTiming?.plannerRuntimeReady === true) {
|
||||
return execution.plannerTiming;
|
||||
}
|
||||
return buildProgramExecutionTiming({
|
||||
motion: execution?.motion || [],
|
||||
motion,
|
||||
profile: state.profile,
|
||||
feedOverride: state.feed.feedOverride,
|
||||
rapidOverride: state.feed.rapidOverride,
|
||||
@@ -2713,6 +3057,7 @@ function createProgramRuntimeFeedbackFromSample({
|
||||
motionIndex,
|
||||
line: sample?.line || motion?.line || null,
|
||||
type: sample?.type || motion?.type || null,
|
||||
linearUnits: sample?.linearUnits || motion?.linearUnits || state.profile.traj?.linearUnits || "mm",
|
||||
timeSeconds: elapsedSeconds,
|
||||
axisPose,
|
||||
currentVelocityMmPerMin: currentVelocity,
|
||||
@@ -2750,6 +3095,7 @@ function createProgramRuntimeFeedbackFromMotion({
|
||||
motionIndex,
|
||||
line: motion?.line || null,
|
||||
type: motion?.type || null,
|
||||
linearUnits: motion?.linearUnits || state.profile.traj?.linearUnits || "mm",
|
||||
timeSeconds: Number(timing?.elapsedSeconds) || 0,
|
||||
axisPose,
|
||||
currentVelocityMmPerMin: Number(timing?.currentVelocity) || 0,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { renderFiveAxisScene } from "../visualization/five-axis-scene.js";
|
||||
import { gateLinuxCncTaskAction } from "../state/linuxcnc-task-policy.js";
|
||||
|
||||
const REGIONS = [
|
||||
"titlebar",
|
||||
@@ -623,7 +624,12 @@ function renderBottomControls(element, state, dispatch) {
|
||||
element.innerHTML = `
|
||||
<input type="file" class="program-file-input" data-action="OPEN_FILE" accept=".ngc,.nc,.tap,.gcode,.txt" />
|
||||
${controls
|
||||
.map(([label, action]) => `<button type="button" data-action="${action}">${label}</button>`)
|
||||
.map(([label, action]) => {
|
||||
const gate = bottomControlGate(state, action);
|
||||
const disabled = gate.allowed ? "" : " disabled";
|
||||
const title = gate.allowed ? "" : ` title="${escapeHtml(gate.operatorMessage || "blocked")}"`;
|
||||
return `<button type="button" data-action="${action}"${disabled}${title}>${label}</button>`;
|
||||
})
|
||||
.join("")}
|
||||
`;
|
||||
|
||||
@@ -650,6 +656,22 @@ function renderBottomControls(element, state, dispatch) {
|
||||
}
|
||||
}
|
||||
|
||||
function bottomControlGate(state, action) {
|
||||
const actionMap = {
|
||||
RUN: { type: "RUN" },
|
||||
STEP: { type: "STEP" },
|
||||
PAUSE: { type: "PAUSE" },
|
||||
RESUME: { type: "RESUME" },
|
||||
HOME: { type: "HOME" },
|
||||
MDI_RUN: { type: "RUN_MDI" },
|
||||
JOG_X_NEG: { type: "JOG" },
|
||||
JOG_X_POS: { type: "JOG" },
|
||||
JOG_Y_NEG: { type: "JOG" },
|
||||
JOG_Y_POS: { type: "JOG" },
|
||||
};
|
||||
return gateLinuxCncTaskAction(state, actionMap[action] || { type: "UI_CONTROL" });
|
||||
}
|
||||
|
||||
function formatNumber(value, digits = 3) {
|
||||
return Number(value).toFixed(digits);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user