完善云端RUN执行反馈
This commit is contained in:
@@ -12,6 +12,7 @@ await cp(join(appRoot, "index.html"), join(distDir, "index.html"));
|
||||
await cp(join(appRoot, "src"), join(distDir, "src"), { recursive: true });
|
||||
await copyLinuxCncManifest();
|
||||
await copyLinuxCncConfigAssets();
|
||||
await copyBundledTestLinuxCncSourceAssets();
|
||||
await copyKinematicsRuntimeAssets();
|
||||
await copyInterpreterRuntimeAssets();
|
||||
await copyTpRuntimeAssets();
|
||||
@@ -68,6 +69,13 @@ async function copyLinuxCncConfigAssets() {
|
||||
await cp(configSrcDir, vendorConfigDistDir, { recursive: true });
|
||||
}
|
||||
|
||||
async function copyBundledTestLinuxCncSourceAssets() {
|
||||
const testSourceDir = join(repoRoot, "web-rtcp-5axis-sim-plan/working_run/test_linuxcnc_source");
|
||||
const testSourceDistDir = join(distDir, "working_run/test_linuxcnc_source");
|
||||
await mkdir(testSourceDistDir, { recursive: true });
|
||||
await cp(testSourceDir, testSourceDistDir, { recursive: true });
|
||||
}
|
||||
|
||||
async function copyLinuxCncManifest() {
|
||||
const manifestDistDir = join(distDir, "wasm-port/tools");
|
||||
await mkdir(manifestDistDir, { recursive: true });
|
||||
|
||||
@@ -7,6 +7,11 @@ const DEFAULT_VENDOR_ROOT_URLS = [
|
||||
new URL("../../../../wasm-port/vendor/linuxcnc/", import.meta.url).href,
|
||||
new URL("../../wasm-port/vendor/linuxcnc/", import.meta.url).href,
|
||||
];
|
||||
const DEFAULT_TEST_SOURCE_ROOT_URLS = [
|
||||
new URL("../../../working_run/test_linuxcnc_source/", import.meta.url).href,
|
||||
new URL("../../working_run/test_linuxcnc_source/", import.meta.url).href,
|
||||
new URL("../../../../working_run/test_linuxcnc_source/", import.meta.url).href,
|
||||
];
|
||||
const TRT_MACHINE_REL = "axis/vismach/5axis/table-rotary-tilting";
|
||||
const TRT_DEMO_SOURCE_PREFIX = `configs/sim/${TRT_MACHINE_REL}/demos/`;
|
||||
const OPFS_ROOT = "web-rtcp-5axis-sim-plan/machines";
|
||||
@@ -125,9 +130,12 @@ export async function saveMachineFileStagingPlan(plan, options = {}) {
|
||||
throw new Error("saveMachineFileStagingPlan requires a machine-file staging plan");
|
||||
}
|
||||
const storage = resolveMachineFileStorage(options);
|
||||
const sourceTextOverrides = options.sourceTextOverrides || {};
|
||||
const savedFiles = [];
|
||||
for (const file of plan.files) {
|
||||
const text = await readTextFromCandidateUrls(sourceUrlsFor(file.sourceRel));
|
||||
const text = typeof sourceTextOverrides[file.sourceRel] === "string"
|
||||
? sourceTextOverrides[file.sourceRel]
|
||||
: await readTextFromCandidateUrls(sourceUrlsFor(file.sourceRel));
|
||||
await saveTextFile(file.opfsPath, text, storage.storage);
|
||||
savedFiles.push({
|
||||
sourceRel: file.sourceRel,
|
||||
@@ -182,6 +190,7 @@ export async function stageProfileMachineFiles(profile, options = {}) {
|
||||
const save = await saveMachineFileStagingPlan(plan, {
|
||||
storage: options.storage,
|
||||
storageMode: options.storageMode,
|
||||
sourceTextOverrides: options.sourceTextOverrides,
|
||||
});
|
||||
return { plan, save };
|
||||
}
|
||||
@@ -441,7 +450,22 @@ function splitPath(path) {
|
||||
}
|
||||
|
||||
function sourceUrlsFor(sourceRel) {
|
||||
return DEFAULT_VENDOR_ROOT_URLS.map((rootUrl) => new URL(sourceRel, rootUrl).href);
|
||||
return sourceOverrideUrlsFor(sourceRel).concat(
|
||||
DEFAULT_VENDOR_ROOT_URLS.map((rootUrl) => new URL(sourceRel, rootUrl).href),
|
||||
);
|
||||
}
|
||||
|
||||
function sourceOverrideUrlsFor(sourceRel) {
|
||||
const basenameValue = basename(sourceRel);
|
||||
if (!basenameValue) return [];
|
||||
|
||||
if (sourceRel === `configs/sim/${TRT_MACHINE_REL}/xyzac-trt.ini`) {
|
||||
return DEFAULT_TEST_SOURCE_ROOT_URLS.map((rootUrl) => new URL("xyzac-trt.ini", rootUrl).href);
|
||||
}
|
||||
if (sourceRel === `${TRT_DEMO_SOURCE_PREFIX}impeller-7bl-xyzac.ngc`) {
|
||||
return DEFAULT_TEST_SOURCE_ROOT_URLS.map((rootUrl) => new URL("impeller-7bl-xyzac.ngc", rootUrl).href);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function basename(path) {
|
||||
|
||||
@@ -172,6 +172,7 @@ const initialState = {
|
||||
programExecutionSampleIndex: 0,
|
||||
programRuntimeFeedback: null,
|
||||
programRuntimeFeedbackHistory: [],
|
||||
programLineExecution: {},
|
||||
taskHalRuntime: null,
|
||||
taskHalRuntimeReadiness: null,
|
||||
taskHalStatus: null,
|
||||
@@ -327,6 +328,25 @@ export function createSimulationStore(seed = {}) {
|
||||
scheduleAsyncKinematicsRefresh();
|
||||
};
|
||||
|
||||
const waitForStatePredicate = (predicate, timeoutMs = 10000) => {
|
||||
if (predicate(state)) return Promise.resolve(state);
|
||||
return new Promise((resolve, reject) => {
|
||||
const startedAt = Date.now();
|
||||
const listener = (nextState) => {
|
||||
if (predicate(nextState)) {
|
||||
listeners.delete(listener);
|
||||
resolve(nextState);
|
||||
return;
|
||||
}
|
||||
if (Date.now() - startedAt > timeoutMs) {
|
||||
listeners.delete(listener);
|
||||
reject(new Error("timed out waiting for store state"));
|
||||
}
|
||||
};
|
||||
listeners.add(listener);
|
||||
});
|
||||
};
|
||||
|
||||
const dispatch = (action) => {
|
||||
switch (action.type) {
|
||||
case "BOOT_READY":
|
||||
@@ -568,6 +588,10 @@ export function createSimulationStore(seed = {}) {
|
||||
programExecutionMotionIndex: 0,
|
||||
programExecutionSampleIndex: 0,
|
||||
programRuntimeFeedback: firstFeedback,
|
||||
programLineExecution: createProgramLineExecutionPatch(state.programLineExecution, firstFeedback, {
|
||||
status: "ready",
|
||||
source: execution.sourceMode,
|
||||
}),
|
||||
programElapsedSeconds: firstTiming.elapsedSeconds,
|
||||
programRemainingSeconds: firstTiming.remainingSeconds,
|
||||
interpreterExecutionPending: false,
|
||||
@@ -603,6 +627,7 @@ export function createSimulationStore(seed = {}) {
|
||||
programExecutionSourceMode: "fixture-line-playback",
|
||||
programExecutionSampleIndex: 0,
|
||||
programRuntimeFeedback: null,
|
||||
programLineExecution: {},
|
||||
interpreterExecutionPending: false,
|
||||
operatorMessage: `LinuxCNC interpreter blocked: ${action.error}`,
|
||||
});
|
||||
@@ -815,6 +840,7 @@ export function createSimulationStore(seed = {}) {
|
||||
axisPose: initialAxisPose,
|
||||
runState: "idle",
|
||||
programRuntimeFeedback: null,
|
||||
programLineExecution: {},
|
||||
preview: {
|
||||
...state.preview,
|
||||
pathPoints: Math.max(loadedProgram.programLines.length, 1),
|
||||
@@ -909,6 +935,14 @@ export function createSimulationStore(seed = {}) {
|
||||
case "RUN_FULL_BOUNDARY_AUDIT_REQUEST":
|
||||
runFullBoundaryAudit(action.options || {}).catch(() => {});
|
||||
break;
|
||||
case "RUN_READY":
|
||||
runReadySequence().catch((error) => {
|
||||
dispatch({
|
||||
type: "TASK_HAL_COMMAND_FAILED",
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
});
|
||||
break;
|
||||
case "SET_FRAME_SOURCE":
|
||||
setState({
|
||||
sourceMode: action.sourceMode,
|
||||
@@ -1138,6 +1172,7 @@ export function createSimulationStore(seed = {}) {
|
||||
axisPose: initialAxisPose,
|
||||
runState: "idle",
|
||||
programRuntimeFeedback: null,
|
||||
programLineExecution: {},
|
||||
preview: {
|
||||
...state.preview,
|
||||
pathPoints: Math.max(loadedProgram.programLines.length, 1),
|
||||
@@ -1182,6 +1217,10 @@ export function createSimulationStore(seed = {}) {
|
||||
programExecutionMotionIndex: playback.motionIndex,
|
||||
programExecutionSampleIndex: playback.sampleIndex,
|
||||
programRuntimeFeedback: playback.runtimeFeedback,
|
||||
programLineExecution: createProgramLineExecutionPatch(state.programLineExecution, playback.runtimeFeedback, {
|
||||
status: playback.complete ? "done" : "running",
|
||||
source: playback.runtimeFeedback?.sourceMode,
|
||||
}),
|
||||
programElapsedSeconds: playback.timing.elapsedSeconds,
|
||||
programRemainingSeconds: playback.timing.remainingSeconds,
|
||||
feed: {
|
||||
@@ -1375,7 +1414,13 @@ export function createSimulationStore(seed = {}) {
|
||||
if (state.taskHalRuntime?.loaded) {
|
||||
runTaskHalCommandSequence([
|
||||
{ type: "EMC_JOINT_HOME", joint: -1 },
|
||||
], { operatorMessage: "task/HAL machine homed" }).catch(() => {});
|
||||
], {
|
||||
operatorMessage: "task/HAL machine homed",
|
||||
preserveMachine: {
|
||||
...state.machine,
|
||||
allHomed: true,
|
||||
},
|
||||
}).catch(() => {});
|
||||
}
|
||||
setState({
|
||||
machine: {
|
||||
@@ -1493,6 +1538,7 @@ export function createSimulationStore(seed = {}) {
|
||||
programExecutionMotionIndex: 0,
|
||||
programExecutionSampleIndex: 0,
|
||||
programRuntimeFeedback: null,
|
||||
programLineExecution: {},
|
||||
axisPose: initialAxisPose,
|
||||
preview: { ...state.preview, pathPoints: Math.max(state.programLines.length, 1) },
|
||||
operatorMessage: "program reloaded",
|
||||
@@ -1758,6 +1804,57 @@ export function createSimulationStore(seed = {}) {
|
||||
return status;
|
||||
};
|
||||
|
||||
const runReadySequence = async () => {
|
||||
if (!state.machineFileStaging?.selectedGcodeSourceRel) {
|
||||
const sourceRel = defaultLinuxCncGcodeSourceForState(state)?.sourceRel;
|
||||
if (sourceRel) {
|
||||
dispatch({ type: "LOAD_LINUXCNC_GCODE_SOURCE", sourceRel });
|
||||
await waitForStatePredicate((nextState) => nextState.machineFileStaging?.selectedGcodeSourceRel === sourceRel);
|
||||
}
|
||||
}
|
||||
if (state.taskHalRuntime?.loaded) {
|
||||
await initializeTaskHalSession({ openProgram: true });
|
||||
await runTaskHalCommandSequence([
|
||||
{ type: "EMC_TASK_SET_STATE", state: "ON" },
|
||||
{ type: "EMC_JOINT_HOME", joint: -1 },
|
||||
{ type: "EMC_TASK_SET_MODE", mode: "AUTO" },
|
||||
], {
|
||||
taskCycles: 3,
|
||||
operatorMessage: "RUN ready: power on, homed, auto mode",
|
||||
allowFixtureSession: false,
|
||||
preserveMachine: {
|
||||
powerOn: true,
|
||||
estopActive: false,
|
||||
taskState: "on",
|
||||
mode: "auto",
|
||||
allHomed: true,
|
||||
},
|
||||
});
|
||||
const tcpKinsType = state.profile.kinematicsParameters.switchkinsTypes.find((type) => type.value === 1)?.webKinsType || "tcp-xyzac";
|
||||
dispatch({ type: "SET_KINS_TYPE", kinsType: tcpKinsType });
|
||||
return state;
|
||||
}
|
||||
const tcpKinsType = state.profile.kinematicsParameters.switchkinsTypes.find((type) => type.value === 1)?.webKinsType || "tcp-xyzac";
|
||||
setState({
|
||||
machine: {
|
||||
...state.machine,
|
||||
powerOn: true,
|
||||
estopActive: false,
|
||||
taskState: "on",
|
||||
mode: "auto",
|
||||
allHomed: true,
|
||||
interpState: "idle",
|
||||
interpResumeState: "idle",
|
||||
taskPaused: false,
|
||||
},
|
||||
runState: "idle",
|
||||
kinsType: tcpKinsType,
|
||||
rtcpState: "on",
|
||||
operatorMessage: "RUN ready: power on, homed, auto mode",
|
||||
});
|
||||
return state;
|
||||
};
|
||||
|
||||
const loadTaskHalMotionPlanForSession = async (session = state.taskHalSession) => {
|
||||
if (!state.taskHalRuntime?.loaded || typeof state.taskHalRuntime.loadProgramMotionPlan !== "function") {
|
||||
return null;
|
||||
@@ -1882,6 +1979,7 @@ export function createSimulationStore(seed = {}) {
|
||||
operatorMessage = "task/HAL command complete",
|
||||
pendingJogCommand = null,
|
||||
allowFixtureSession = true,
|
||||
preserveMachine = null,
|
||||
} = {}) => {
|
||||
if (!state.taskHalRuntime?.loaded) {
|
||||
throw new Error("LinuxCNC task/HAL runtime not attached");
|
||||
@@ -1910,7 +2008,7 @@ export function createSimulationStore(seed = {}) {
|
||||
if (state.taskHalExecutionSequence !== sequence) {
|
||||
return status;
|
||||
}
|
||||
dispatch({ type: "TASK_HAL_STATUS_APPLIED", status, operatorMessage });
|
||||
dispatch({ type: "TASK_HAL_STATUS_APPLIED", status, operatorMessage, preserveMachine });
|
||||
return status;
|
||||
} catch (error) {
|
||||
dispatch({
|
||||
@@ -2277,6 +2375,11 @@ function applyTaskHalStatusPatch(state, status, operatorMessage, {
|
||||
&& (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);
|
||||
const lineStatus = runState === "complete"
|
||||
? "done"
|
||||
: runState === "running" || runState === "mdi"
|
||||
? "running"
|
||||
: runState;
|
||||
|
||||
return {
|
||||
taskHalStatus: status,
|
||||
@@ -2315,6 +2418,10 @@ function applyTaskHalStatusPatch(state, status, operatorMessage, {
|
||||
},
|
||||
programRuntimeFeedback: runtimeFeedback,
|
||||
programRuntimeFeedbackHistory: feedbackHistory,
|
||||
programLineExecution: createProgramLineExecutionPatch(state.programLineExecution, runtimeFeedback, {
|
||||
status: lineStatus,
|
||||
source: "linuxcnc-task-motion-hal-wasm",
|
||||
}),
|
||||
operatorMessage,
|
||||
};
|
||||
}
|
||||
@@ -2451,6 +2558,41 @@ function createTaskHalRuntimeFeedback(state, status, axisPose, activeLine) {
|
||||
};
|
||||
}
|
||||
|
||||
function createProgramLineExecutionPatch(previous = {}, feedback = null, {
|
||||
status = "running",
|
||||
source = null,
|
||||
} = {}) {
|
||||
const line = Number(feedback?.line || 0);
|
||||
if (!Number.isFinite(line) || line <= 0) {
|
||||
return previous || {};
|
||||
}
|
||||
const axisPose = feedback.axisPose || {};
|
||||
return {
|
||||
...(previous || {}),
|
||||
[line]: {
|
||||
status,
|
||||
source: source || feedback.sourceMode || "unknown",
|
||||
line,
|
||||
feed: Number(feedback.currentVelocityMmPerMin || 0),
|
||||
requestedFeed: Number(feedback.requestedVelocityMmPerMin || 0),
|
||||
feedMode: feedback.feedMode || null,
|
||||
axisPose: pickExecutionAxes(axisPose),
|
||||
taskCycle: Number(feedback.taskCycle || 0),
|
||||
servoCycle: Number(feedback.cycle || 0),
|
||||
sampleIndex: Number(feedback.sampleIndex || 0),
|
||||
motionIndex: Number(feedback.motionIndex || 0),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function pickExecutionAxes(axisPose = {}) {
|
||||
return Object.fromEntries(["x", "y", "z", "a", "b", "c"].map((axis) => [
|
||||
axis,
|
||||
Number(axisPose[axis] || 0),
|
||||
]));
|
||||
}
|
||||
|
||||
function normalizeTaskHalTaskState(value) {
|
||||
const state = String(value || "").toLowerCase().replaceAll("_", "-");
|
||||
if (state === "on") return "on";
|
||||
|
||||
@@ -21,16 +21,22 @@
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 100%;
|
||||
min-width: 1180px;
|
||||
min-height: 640px;
|
||||
height: 100%;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: #c8c4bc;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
button {
|
||||
border: 1px solid #bdb8ad;
|
||||
border-radius: 5px;
|
||||
@@ -66,21 +72,21 @@ button:active {
|
||||
.gmoccapy-shell {
|
||||
display: grid;
|
||||
grid-template-columns:
|
||||
minmax(610px, 1.34fr)
|
||||
minmax(270px, 0.58fr)
|
||||
minmax(260px, 0.56fr)
|
||||
minmax(0, 1.34fr)
|
||||
minmax(0, 0.58fr)
|
||||
minmax(0, 0.56fr)
|
||||
108px;
|
||||
grid-template-rows: 40px minmax(210px, 1fr) minmax(170px, 0.78fr) 150px 68px;
|
||||
grid-template-rows: 40px minmax(0, 1fr) minmax(0, 0.82fr) minmax(110px, 0.48fr) 68px;
|
||||
grid-template-areas:
|
||||
"title title title title"
|
||||
"preview dro dro side"
|
||||
"preview gcode gcode side"
|
||||
"info override spindle side"
|
||||
"bottom bottom bottom side";
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
min-width: 1180px;
|
||||
min-height: 640px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--panel);
|
||||
}
|
||||
@@ -140,6 +146,35 @@ button:active {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.current-line-indicator {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(44px, auto);
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 0 0 auto;
|
||||
min-width: 0;
|
||||
padding: 4px 8px;
|
||||
border: 1px solid #8d887f;
|
||||
background: #f7f1d0;
|
||||
color: #171717;
|
||||
}
|
||||
|
||||
.current-line-indicator span {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
text-overflow: ellipsis;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.current-line-indicator strong {
|
||||
color: #005bab;
|
||||
font: 700 20px/1 "Courier New", monospace;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.preview-panel {
|
||||
grid-area: preview;
|
||||
position: relative;
|
||||
@@ -336,7 +371,7 @@ button:active {
|
||||
.gcode-panel {
|
||||
grid-area: gcode;
|
||||
display: grid;
|
||||
grid-template-rows: 32px minmax(0, 1fr) 20px 54px;
|
||||
grid-template-rows: auto auto minmax(96px, 1fr) 20px 54px;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
@@ -357,6 +392,38 @@ button:active {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.linuxcnc-source-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(260px, 1fr) auto minmax(180px, 1fr);
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
padding: 5px 8px;
|
||||
border-bottom: 1px solid #d5d0c7;
|
||||
background: #f6f4ef;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.linuxcnc-source-row label {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(150px, 240px);
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.linuxcnc-source-row select {
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.linuxcnc-source-row [data-linuxcnc-gcode-source="status"] {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.gcode-header strong,
|
||||
.gcode-header span {
|
||||
min-width: 0;
|
||||
@@ -385,26 +452,54 @@ button:active {
|
||||
display: grid;
|
||||
grid-template-columns: 42px minmax(0, 1fr);
|
||||
gap: 6px;
|
||||
min-height: 18px;
|
||||
min-height: 34px;
|
||||
line-height: 1.25;
|
||||
padding: 2px 4px;
|
||||
border-left: 3px solid transparent;
|
||||
}
|
||||
|
||||
.gcode-row.active {
|
||||
background: #242424;
|
||||
color: #202020;
|
||||
border-left-color: #2ebf63;
|
||||
}
|
||||
|
||||
.gcode-row[data-line-status="done"] {
|
||||
background: #eef6ec;
|
||||
border-left-color: #79a96c;
|
||||
}
|
||||
|
||||
.gcode-row[data-line-status="running"] {
|
||||
background: #242424;
|
||||
border-left-color: #2ebf63;
|
||||
}
|
||||
|
||||
.gcode-row.active span,
|
||||
.gcode-row.active code {
|
||||
.gcode-row.active code,
|
||||
.gcode-row.active small,
|
||||
.gcode-row[data-line-status="running"] span,
|
||||
.gcode-row[data-line-status="running"] code,
|
||||
.gcode-row[data-line-status="running"] small {
|
||||
color: #f2f2f2;
|
||||
}
|
||||
|
||||
.gcode-row code {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.gcode-row small {
|
||||
grid-column: 2;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
color: #6d695f;
|
||||
font: 11px/1.2 "Courier New", monospace;
|
||||
}
|
||||
|
||||
.gcode-progress {
|
||||
display: grid;
|
||||
grid-template-columns: 120px 1fr;
|
||||
@@ -723,7 +818,7 @@ button:active {
|
||||
.bottom-controls {
|
||||
grid-area: bottom;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(15, minmax(48px, 1fr));
|
||||
grid-template-columns: repeat(18, minmax(44px, 1fr));
|
||||
gap: 5px;
|
||||
padding: 6px 8px;
|
||||
border-top: 2px solid var(--border);
|
||||
@@ -746,10 +841,23 @@ button:active {
|
||||
@media (max-width: 1180px) {
|
||||
.gmoccapy-shell {
|
||||
grid-template-columns:
|
||||
minmax(560px, 1.28fr)
|
||||
minmax(250px, 0.58fr)
|
||||
minmax(230px, 0.54fr)
|
||||
100px;
|
||||
minmax(0, 1.18fr)
|
||||
minmax(0, 0.7fr)
|
||||
minmax(0, 0.62fr)
|
||||
96px;
|
||||
}
|
||||
|
||||
.titlebar {
|
||||
gap: 6px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.profile-select-label {
|
||||
min-width: 160px;
|
||||
}
|
||||
|
||||
.current-line-indicator strong {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.dro-row strong {
|
||||
@@ -765,6 +873,14 @@ button:active {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.linuxcnc-source-row {
|
||||
grid-template-columns: minmax(180px, 1fr) auto minmax(140px, 1fr);
|
||||
}
|
||||
|
||||
.linuxcnc-source-row label {
|
||||
grid-template-columns: auto minmax(120px, 1fr);
|
||||
}
|
||||
|
||||
.tabs button,
|
||||
.meter-card h2,
|
||||
.override-control h2,
|
||||
@@ -773,3 +889,180 @@ button:active {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-height: 720px) {
|
||||
.gmoccapy-shell {
|
||||
grid-template-rows: 40px minmax(0, 1fr) minmax(0, 1fr) minmax(84px, 0.4fr) 58px;
|
||||
}
|
||||
|
||||
.preview-toolbar button,
|
||||
.bottom-controls button,
|
||||
.status-sidebar button {
|
||||
min-height: 34px;
|
||||
}
|
||||
|
||||
.gcode-panel {
|
||||
grid-template-rows: auto auto minmax(0, 1fr) 18px 48px;
|
||||
}
|
||||
|
||||
.gcode-list {
|
||||
padding: 4px 6px 2px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.gcode-row {
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
.gcode-row small {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.bottom-controls {
|
||||
grid-template-columns: repeat(18, minmax(36px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-height: 420px) {
|
||||
.gmoccapy-shell {
|
||||
grid-template-rows: 24px minmax(24px, 0.14fr) minmax(0, 1.92fr) 0 28px;
|
||||
}
|
||||
|
||||
.titlebar {
|
||||
gap: 4px;
|
||||
padding: 2px 5px;
|
||||
}
|
||||
|
||||
.title-stack strong {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.title-stack span,
|
||||
.run-state {
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.profile-select-label {
|
||||
min-width: 96px;
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.profile-select-label select {
|
||||
min-height: 16px;
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.current-line-indicator {
|
||||
padding: 1px 4px;
|
||||
}
|
||||
|
||||
.current-line-indicator span {
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.current-line-indicator strong {
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.info-tabs,
|
||||
.override-panel,
|
||||
.spindle-coolant-panel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.gcode-panel {
|
||||
grid-template-rows: auto auto minmax(0, 1fr) 12px;
|
||||
}
|
||||
|
||||
.gcode-header {
|
||||
gap: 4px;
|
||||
padding: 1px 3px;
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.linuxcnc-source-row {
|
||||
gap: 4px;
|
||||
padding: 1px 3px;
|
||||
font-size: 8px;
|
||||
grid-template-columns: minmax(120px, 1fr) auto minmax(72px, 0.82fr);
|
||||
}
|
||||
|
||||
.linuxcnc-source-row label {
|
||||
gap: 4px;
|
||||
grid-template-columns: auto minmax(72px, 1fr);
|
||||
}
|
||||
|
||||
.linuxcnc-source-row button,
|
||||
.linuxcnc-source-row select {
|
||||
min-height: 16px;
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.gcode-list {
|
||||
padding: 2px 4px;
|
||||
font-size: 8px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.gcode-row {
|
||||
gap: 4px;
|
||||
min-height: 8px;
|
||||
padding: 0 2px;
|
||||
border-left-width: 2px;
|
||||
}
|
||||
|
||||
.gcode-row span {
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.gcode-row code {
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.mdi-panel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dro-panel {
|
||||
grid-template-rows: 1fr;
|
||||
}
|
||||
|
||||
.dro-grid {
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
grid-template-rows: 1fr;
|
||||
}
|
||||
|
||||
.dro-row {
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
gap: 3px;
|
||||
padding: 1px 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dro-axis {
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.dro-mode,
|
||||
.dro-dtg,
|
||||
.tcp-strip {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dro-row strong {
|
||||
font-size: 9px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.bottom-controls {
|
||||
gap: 3px;
|
||||
padding: 2px 3px;
|
||||
}
|
||||
|
||||
.bottom-controls button {
|
||||
font-size: 8px;
|
||||
line-height: 1;
|
||||
min-height: 18px;
|
||||
padding: 1px 2px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ function render(regions, state, dispatch) {
|
||||
}
|
||||
|
||||
function renderTitlebar(element, state) {
|
||||
const currentLine = currentGcodeExecutionLine(state);
|
||||
element.innerHTML = `
|
||||
<div class="brand-dot" aria-hidden="true">NS</div>
|
||||
<div class="title-stack">
|
||||
@@ -73,6 +74,10 @@ function renderTitlebar(element, state) {
|
||||
`).join("")}
|
||||
</select>
|
||||
</label>
|
||||
<div class="current-line-indicator" data-current-gcode-line="${currentLine}" data-current-gcode-source="${escapeHtml(currentGcodeLineSource(state))}">
|
||||
<span>G-code line</span>
|
||||
<strong>${currentLine}</strong>
|
||||
</div>
|
||||
<div class="run-state" data-run-state="${state.runState}">${state.runState}</div>
|
||||
`;
|
||||
element.querySelector('[data-action="select-profile"]').addEventListener("change", (event) => {
|
||||
@@ -192,11 +197,20 @@ function droRow(axis, value, dtg) {
|
||||
}
|
||||
|
||||
function renderGcode(element, state, dispatch) {
|
||||
const currentLine = currentGcodeExecutionLine(state);
|
||||
const rows = state.programLines
|
||||
.map((line, index) => {
|
||||
const lineNumber = state.programStartLine + index;
|
||||
const execution = state.programLineExecution?.[lineNumber] || null;
|
||||
const active = lineNumber === state.activeLine ? " active" : "";
|
||||
return `<li class="gcode-row${active}" data-program-line="${lineNumber}"><span>${lineNumber}</span><code>${escapeHtml(line)}</code></li>`;
|
||||
const status = execution?.status || (lineNumber < state.activeLine ? "done" : "pending");
|
||||
return `
|
||||
<li class="gcode-row${active}" data-program-line="${lineNumber}" data-line-status="${escapeHtml(status)}">
|
||||
<span>${lineNumber}</span>
|
||||
<code>${escapeHtml(line)}</code>
|
||||
<small data-line-execution="${lineNumber}">${formatLineExecution(execution, active)}</small>
|
||||
</li>
|
||||
`;
|
||||
})
|
||||
.join("");
|
||||
const programEndLine = state.programStartLine + Math.max(state.programLines.length - 1, 0);
|
||||
@@ -210,7 +224,7 @@ function renderGcode(element, state, dispatch) {
|
||||
<div class="gcode-header">
|
||||
<strong>${escapeHtml(state.activeProgram)}</strong>
|
||||
<span data-program-source="${state.programSource}">${state.programSource}</span>
|
||||
<span data-active-program-line="${state.activeLine}">Current line ${state.activeLine}</span>
|
||||
<span data-active-program-line="${currentLine}" data-current-gcode-line="${currentLine}">Executing line ${currentLine}</span>
|
||||
</div>
|
||||
<div class="linuxcnc-source-row" data-linuxcnc-gcode-source="row">
|
||||
<label>
|
||||
@@ -272,6 +286,19 @@ function renderGcode(element, state, dispatch) {
|
||||
});
|
||||
}
|
||||
|
||||
function currentGcodeExecutionLine(state) {
|
||||
const feedbackLine = Number(state.programRuntimeFeedback?.line);
|
||||
if (Number.isFinite(feedbackLine) && feedbackLine > 0) {
|
||||
return feedbackLine;
|
||||
}
|
||||
const activeLine = Number(state.activeLine);
|
||||
return Number.isFinite(activeLine) && activeLine > 0 ? activeLine : state.programStartLine;
|
||||
}
|
||||
|
||||
function currentGcodeLineSource(state) {
|
||||
return state.programRuntimeFeedback?.sourceMode || state.programExecutionSourceMode || "ui-state";
|
||||
}
|
||||
|
||||
function renderLinuxCncGcodeSourceOptions(state) {
|
||||
const sources = state.machineFileStaging.gcodeSources || [];
|
||||
if (sources.length === 0) {
|
||||
@@ -444,6 +471,21 @@ function formatProgramRuntimeDtg(state) {
|
||||
return `DTG ${formatNumber(dtg.x, 3)} / ${formatNumber(dtg.y, 3)} / ${formatNumber(dtg.z, 3)} distance ${formatNumber(feedback.distanceToGo, 3)}`;
|
||||
}
|
||||
|
||||
function formatLineExecution(execution, active) {
|
||||
if (!execution) return active ? "running" : "pending";
|
||||
const axes = execution.axisPose || {};
|
||||
return [
|
||||
execution.status || (active ? "running" : "done"),
|
||||
`F ${formatNumber(execution.feed, 1)}`,
|
||||
`X ${formatNumber(axes.x, 3)}`,
|
||||
`Y ${formatNumber(axes.y, 3)}`,
|
||||
`Z ${formatNumber(axes.z, 3)}`,
|
||||
`A ${formatNumber(axes.a, 3)}`,
|
||||
`C ${formatNumber(axes.c, 3)}`,
|
||||
`cycle ${execution.taskCycle || 0}/${execution.servoCycle || 0}`,
|
||||
].join(" | ");
|
||||
}
|
||||
|
||||
function formatDuration(seconds) {
|
||||
const safeSeconds = Math.max(Number(seconds) || 0, 0);
|
||||
const minutes = Math.floor(safeSeconds / 60);
|
||||
@@ -604,6 +646,7 @@ function renderBottomControls(element, state, dispatch) {
|
||||
const controls = [
|
||||
["Open", "OPEN", null],
|
||||
["Reload", "RELOAD", () => dispatch({ type: "RELOAD_PROGRAM" })],
|
||||
["Run Ready", "RUN_READY", () => dispatch({ type: "RUN_READY" })],
|
||||
["Run", "RUN", () => dispatch({ type: "RUN" })],
|
||||
["Stop", "STOP", () => dispatch({ type: "STOP" })],
|
||||
["Pause", "PAUSE", () => dispatch({ type: "PAUSE" })],
|
||||
@@ -659,6 +702,7 @@ function renderBottomControls(element, state, dispatch) {
|
||||
function bottomControlGate(state, action) {
|
||||
const actionMap = {
|
||||
RUN: { type: "RUN" },
|
||||
RUN_READY: { type: "UI_CONTROL" },
|
||||
STEP: { type: "STEP" },
|
||||
PAUSE: { type: "PAUSE" },
|
||||
RESUME: { type: "RESUME" },
|
||||
|
||||
Reference in New Issue
Block a user