上传真实浏览器仿真推进

结论:新增 LinuxCNC WASM 驱动的真实浏览器仿真页面,接入 browser smoke、release gate、SDK 与项目交付文档,并完成当前推进批次的验收记录。
This commit is contained in:
2026-06-16 20:59:21 +08:00
parent 19eabdf06e
commit 24e1fc18b1
39 changed files with 7563 additions and 59 deletions

View File

@@ -91,6 +91,7 @@
<a data-entry-id="edit-run" href="./index.html">Open edit and run panel</a>
<a data-entry-id="control-view" href="./control-page.html">Open read-only control view</a>
<a data-entry-id="workflow-overview" href="./workflow-overview.html">Open shell handoff overview</a>
<a data-entry-id="real-simulation" href="../simulation/index.html">Open real browser simulation</a>
</div>
<section class="handoff-status" aria-label="Shell handoff compatibility" data-handoff-shell>
<p data-handoff-status>Checking shell handoff compatibility.</p>
@@ -297,15 +298,124 @@
mountWorkflowOverviewEmbedding: (options) => (
shellViewModel.mountWorkflowOverviewEmbedding(options)
),
runWorkflowOverviewReleaseReadinessArtifactJsonWorkflow: (options) => (
shellViewModel.runWorkflowOverviewReleaseReadinessArtifactJsonWorkflow(options)
),
getWorkflowOverviewReleaseReadinessArtifactJsonWorkflowSummaryViewModel: (workflow) => (
shellViewModel.createWorkflowOverviewReleaseReadinessArtifactJsonWorkflowSummaryViewModel(workflow)
),
getWorkflowOverviewReleaseReadinessArtifactJsonWorkflowActionPlan: (workflow) => (
shellViewModel.createWorkflowOverviewReleaseReadinessArtifactJsonWorkflowActionPlan(workflow)
),
getWorkflowOverviewReleaseReadinessArtifactJsonWorkflowRenderState: (summaryViewModel) => (
shellViewModel.createWorkflowOverviewReleaseReadinessArtifactJsonWorkflowRenderState(summaryViewModel)
),
getWorkflowOverviewReleaseReadinessArtifactJsonWorkflowDomContract: (options) => (
shellViewModel.createWorkflowOverviewReleaseReadinessArtifactJsonWorkflowDomContract(options)
),
getWorkflowOverviewReleaseReadinessArtifactJsonWorkflowDomReadiness: (options) => (
shellViewModel.createWorkflowOverviewReleaseReadinessArtifactJsonWorkflowDomReadiness(
options ?? { documentRef: document },
)
),
renderWorkflowOverviewReleaseReadinessArtifactJsonWorkflowState: (renderState, options) => (
shellViewModel.renderWorkflowOverviewReleaseReadinessArtifactJsonWorkflowState(
renderState,
options ?? { documentRef: document },
)
),
mountWorkflowOverviewReleaseReadinessArtifactJsonWorkflowState: (options) => (
shellViewModel.mountWorkflowOverviewReleaseReadinessArtifactJsonWorkflowState(
options ?? { documentRef: document },
)
),
loadWorkflowOverviewReleaseReadinessArtifactUrl: (options) => (
shellViewModel.loadWorkflowOverviewReleaseReadinessArtifactUrl(options)
),
getWorkflowOverviewReleaseReadinessArtifactUrlWorkflowSummaryViewModel: (workflow) => (
shellViewModel.createWorkflowOverviewReleaseReadinessArtifactUrlWorkflowSummaryViewModel(workflow)
),
getWorkflowOverviewReleaseReadinessArtifactUrlWorkflowActionPlan: (workflow) => (
shellViewModel.createWorkflowOverviewReleaseReadinessArtifactUrlWorkflowActionPlan(workflow)
),
getWorkflowOverviewReleaseReadinessArtifactUrlWorkflowRenderState: (summaryViewModel) => (
shellViewModel.createWorkflowOverviewReleaseReadinessArtifactUrlWorkflowRenderState(summaryViewModel)
),
getWorkflowOverviewReleaseReadinessArtifactUrlWorkflowDomContract: (options) => (
shellViewModel.createWorkflowOverviewReleaseReadinessArtifactUrlWorkflowDomContract(options)
),
getWorkflowOverviewReleaseReadinessArtifactUrlWorkflowDomReadiness: (options) => (
shellViewModel.createWorkflowOverviewReleaseReadinessArtifactUrlWorkflowDomReadiness(
options ?? { documentRef: document },
)
),
renderWorkflowOverviewReleaseReadinessArtifactUrlWorkflowState: (renderState, options) => (
shellViewModel.renderWorkflowOverviewReleaseReadinessArtifactUrlWorkflowState(
renderState,
options ?? { documentRef: document },
)
),
mountWorkflowOverviewReleaseReadinessArtifactUrlWorkflowState: (options) => (
shellViewModel.mountWorkflowOverviewReleaseReadinessArtifactUrlWorkflowState(
options ?? { documentRef: document },
)
),
getWorkflowOverviewReleaseReadinessArtifactValidationSummaryViewModel: (validation) => (
shellViewModel.createWorkflowOverviewReleaseReadinessArtifactValidationSummaryViewModel(validation)
),
getWorkflowOverviewReleaseReadinessArtifactValidationActionPlan: (validation) => (
shellViewModel.createWorkflowOverviewReleaseReadinessArtifactValidationActionPlan(validation)
),
getWorkflowOverviewReleaseReadinessArtifactValidationActionPlanRenderState: (actionPlan) => (
shellViewModel.createWorkflowOverviewReleaseReadinessArtifactValidationActionPlanRenderState(actionPlan)
),
getWorkflowOverviewReleaseReadinessArtifactValidationActionPlanDomContract: (options) => (
shellViewModel.createWorkflowOverviewReleaseReadinessArtifactValidationActionPlanDomContract(options)
),
getWorkflowOverviewReleaseReadinessArtifactValidationActionPlanDomReadiness: (options) => (
shellViewModel.createWorkflowOverviewReleaseReadinessArtifactValidationActionPlanDomReadiness(
options ?? { documentRef: document },
)
),
renderWorkflowOverviewReleaseReadinessArtifactValidationActionPlanState: (renderState, options) => (
shellViewModel.renderWorkflowOverviewReleaseReadinessArtifactValidationActionPlanState(
renderState,
options ?? { documentRef: document },
)
),
mountWorkflowOverviewReleaseReadinessArtifactValidationActionPlanState: (options) => (
shellViewModel.mountWorkflowOverviewReleaseReadinessArtifactValidationActionPlanState(
options ?? { documentRef: document },
)
),
getWorkflowOverviewReleaseReadinessArtifactGateExecutionSummaryViewModel: (validation) => (
shellViewModel.createWorkflowOverviewReleaseReadinessArtifactGateExecutionSummaryViewModel(validation)
),
getWorkflowOverviewReleaseReadinessArtifactUrlGateExecutionSummaryViewModel: (workflow) => (
shellViewModel.createWorkflowOverviewReleaseReadinessArtifactUrlGateExecutionSummaryViewModel(workflow)
),
getWorkflowOverviewReleaseReadinessArtifactGateExecutionSummaryRenderState: (summaryViewModel) => (
shellViewModel.createWorkflowOverviewReleaseReadinessArtifactGateExecutionSummaryRenderState(summaryViewModel)
),
getWorkflowOverviewReleaseReadinessArtifactGateExecutionSummaryDomContract: (options) => (
shellViewModel.createWorkflowOverviewReleaseReadinessArtifactGateExecutionSummaryDomContract(options)
),
getWorkflowOverviewReleaseReadinessArtifactGateExecutionSummaryDomReadiness: (options) => (
shellViewModel.createWorkflowOverviewReleaseReadinessArtifactGateExecutionSummaryDomReadiness(
options ?? { documentRef: document },
)
),
renderWorkflowOverviewReleaseReadinessArtifactGateExecutionSummaryState: (renderState, options) => (
shellViewModel.renderWorkflowOverviewReleaseReadinessArtifactGateExecutionSummaryState(
renderState,
options ?? { documentRef: document },
)
),
mountWorkflowOverviewReleaseReadinessArtifactGateExecutionSummaryState: (options) => (
shellViewModel.mountWorkflowOverviewReleaseReadinessArtifactGateExecutionSummaryState(
options ?? { documentRef: document },
)
),
getWorkflowOverviewEmbeddingReport: (embeddingResult, contract) => (
shellViewModel.createWorkflowOverviewEmbeddingReport(embeddingResult, contract)
),

File diff suppressed because it is too large Load Diff

View File

@@ -106,9 +106,36 @@
renderIniPanelShellWorkflowOverviewEmbeddingMountState,
mountIniPanelShellWorkflowOverviewEmbeddingMountState,
validateIniPanelShellWorkflowOverviewReleaseReadinessArtifactJson,
runIniPanelShellWorkflowOverviewReleaseReadinessArtifactJsonWorkflow,
createIniPanelShellWorkflowOverviewReleaseReadinessArtifactJsonWorkflowSummaryViewModel,
createIniPanelShellWorkflowOverviewReleaseReadinessArtifactJsonWorkflowActionPlan,
createIniPanelShellWorkflowOverviewReleaseReadinessArtifactJsonWorkflowRenderState,
createIniPanelShellWorkflowOverviewReleaseReadinessArtifactJsonWorkflowDomContract,
createIniPanelShellWorkflowOverviewReleaseReadinessArtifactJsonWorkflowDomReadiness,
renderIniPanelShellWorkflowOverviewReleaseReadinessArtifactJsonWorkflowState,
mountIniPanelShellWorkflowOverviewReleaseReadinessArtifactJsonWorkflowState,
loadIniPanelShellWorkflowOverviewReleaseReadinessArtifactUrl,
createIniPanelShellWorkflowOverviewReleaseReadinessArtifactUrlWorkflowSummaryViewModel,
createIniPanelShellWorkflowOverviewReleaseReadinessArtifactUrlWorkflowActionPlan,
createIniPanelShellWorkflowOverviewReleaseReadinessArtifactUrlWorkflowRenderState,
createIniPanelShellWorkflowOverviewReleaseReadinessArtifactUrlWorkflowDomContract,
createIniPanelShellWorkflowOverviewReleaseReadinessArtifactUrlWorkflowDomReadiness,
renderIniPanelShellWorkflowOverviewReleaseReadinessArtifactUrlWorkflowState,
mountIniPanelShellWorkflowOverviewReleaseReadinessArtifactUrlWorkflowState,
createIniPanelShellWorkflowOverviewReleaseReadinessArtifactValidationSummaryViewModel,
createIniPanelShellWorkflowOverviewReleaseReadinessArtifactValidationActionPlan,
createIniPanelShellWorkflowOverviewReleaseReadinessArtifactGateExecutionSummaryViewModel,
createIniPanelShellWorkflowOverviewReleaseReadinessArtifactUrlGateExecutionSummaryViewModel,
createIniPanelShellWorkflowOverviewReleaseReadinessArtifactGateExecutionSummaryRenderState,
createIniPanelShellWorkflowOverviewReleaseReadinessArtifactGateExecutionSummaryDomContract,
createIniPanelShellWorkflowOverviewReleaseReadinessArtifactGateExecutionSummaryDomReadiness,
renderIniPanelShellWorkflowOverviewReleaseReadinessArtifactGateExecutionSummaryState,
mountIniPanelShellWorkflowOverviewReleaseReadinessArtifactGateExecutionSummaryState,
createIniPanelShellWorkflowOverviewReleaseReadinessArtifactValidationActionPlanRenderState,
createIniPanelShellWorkflowOverviewReleaseReadinessArtifactValidationActionPlanDomContract,
createIniPanelShellWorkflowOverviewReleaseReadinessArtifactValidationActionPlanDomReadiness,
renderIniPanelShellWorkflowOverviewReleaseReadinessArtifactValidationActionPlanState,
mountIniPanelShellWorkflowOverviewReleaseReadinessArtifactValidationActionPlanState,
createIniPanelShellWorkflowOverviewReleaseReadinessArtifactDisplayViewModel,
createIniPanelShellWorkflowOverviewReleaseReadinessArtifactRenderState,
createIniPanelShellWorkflowOverviewReleaseReadinessArtifactDomContract,
@@ -146,15 +173,124 @@
validateWorkflowOverviewReleaseReadinessArtifactJson: (artifactJson) => (
validateIniPanelShellWorkflowOverviewReleaseReadinessArtifactJson(artifactJson)
),
runWorkflowOverviewReleaseReadinessArtifactJsonWorkflow: (options) => (
runIniPanelShellWorkflowOverviewReleaseReadinessArtifactJsonWorkflow(options)
),
getWorkflowOverviewReleaseReadinessArtifactJsonWorkflowSummaryViewModel: (workflow) => (
createIniPanelShellWorkflowOverviewReleaseReadinessArtifactJsonWorkflowSummaryViewModel(workflow)
),
getWorkflowOverviewReleaseReadinessArtifactJsonWorkflowActionPlan: (workflow) => (
createIniPanelShellWorkflowOverviewReleaseReadinessArtifactJsonWorkflowActionPlan(workflow)
),
getWorkflowOverviewReleaseReadinessArtifactJsonWorkflowRenderState: (summaryViewModel) => (
createIniPanelShellWorkflowOverviewReleaseReadinessArtifactJsonWorkflowRenderState(summaryViewModel)
),
getWorkflowOverviewReleaseReadinessArtifactJsonWorkflowDomContract: (options) => (
createIniPanelShellWorkflowOverviewReleaseReadinessArtifactJsonWorkflowDomContract(options)
),
getWorkflowOverviewReleaseReadinessArtifactJsonWorkflowDomReadiness: (options) => (
createIniPanelShellWorkflowOverviewReleaseReadinessArtifactJsonWorkflowDomReadiness(
options ?? { documentRef: document },
)
),
renderWorkflowOverviewReleaseReadinessArtifactJsonWorkflowState: (renderState, options) => (
renderIniPanelShellWorkflowOverviewReleaseReadinessArtifactJsonWorkflowState(
renderState,
options ?? { documentRef: document },
)
),
mountWorkflowOverviewReleaseReadinessArtifactJsonWorkflowState: (options) => (
mountIniPanelShellWorkflowOverviewReleaseReadinessArtifactJsonWorkflowState(
options ?? { documentRef: document },
)
),
loadWorkflowOverviewReleaseReadinessArtifactUrl: (options) => (
loadIniPanelShellWorkflowOverviewReleaseReadinessArtifactUrl(options)
),
getWorkflowOverviewReleaseReadinessArtifactUrlWorkflowSummaryViewModel: (workflow) => (
createIniPanelShellWorkflowOverviewReleaseReadinessArtifactUrlWorkflowSummaryViewModel(workflow)
),
getWorkflowOverviewReleaseReadinessArtifactUrlWorkflowActionPlan: (workflow) => (
createIniPanelShellWorkflowOverviewReleaseReadinessArtifactUrlWorkflowActionPlan(workflow)
),
getWorkflowOverviewReleaseReadinessArtifactUrlWorkflowRenderState: (summaryViewModel) => (
createIniPanelShellWorkflowOverviewReleaseReadinessArtifactUrlWorkflowRenderState(summaryViewModel)
),
getWorkflowOverviewReleaseReadinessArtifactUrlWorkflowDomContract: (options) => (
createIniPanelShellWorkflowOverviewReleaseReadinessArtifactUrlWorkflowDomContract(options)
),
getWorkflowOverviewReleaseReadinessArtifactUrlWorkflowDomReadiness: (options) => (
createIniPanelShellWorkflowOverviewReleaseReadinessArtifactUrlWorkflowDomReadiness(
options ?? { documentRef: document },
)
),
renderWorkflowOverviewReleaseReadinessArtifactUrlWorkflowState: (renderState, options) => (
renderIniPanelShellWorkflowOverviewReleaseReadinessArtifactUrlWorkflowState(
renderState,
options ?? { documentRef: document },
)
),
mountWorkflowOverviewReleaseReadinessArtifactUrlWorkflowState: (options) => (
mountIniPanelShellWorkflowOverviewReleaseReadinessArtifactUrlWorkflowState(
options ?? { documentRef: document },
)
),
getWorkflowOverviewReleaseReadinessArtifactValidationSummaryViewModel: (validation) => (
createIniPanelShellWorkflowOverviewReleaseReadinessArtifactValidationSummaryViewModel(validation)
),
getWorkflowOverviewReleaseReadinessArtifactValidationActionPlan: (validation) => (
createIniPanelShellWorkflowOverviewReleaseReadinessArtifactValidationActionPlan(validation)
),
getWorkflowOverviewReleaseReadinessArtifactValidationActionPlanRenderState: (actionPlan) => (
createIniPanelShellWorkflowOverviewReleaseReadinessArtifactValidationActionPlanRenderState(actionPlan)
),
getWorkflowOverviewReleaseReadinessArtifactValidationActionPlanDomContract: (options) => (
createIniPanelShellWorkflowOverviewReleaseReadinessArtifactValidationActionPlanDomContract(options)
),
getWorkflowOverviewReleaseReadinessArtifactValidationActionPlanDomReadiness: (options) => (
createIniPanelShellWorkflowOverviewReleaseReadinessArtifactValidationActionPlanDomReadiness(
options ?? { documentRef: document },
)
),
renderWorkflowOverviewReleaseReadinessArtifactValidationActionPlanState: (renderState, options) => (
renderIniPanelShellWorkflowOverviewReleaseReadinessArtifactValidationActionPlanState(
renderState,
options ?? { documentRef: document },
)
),
mountWorkflowOverviewReleaseReadinessArtifactValidationActionPlanState: (options) => (
mountIniPanelShellWorkflowOverviewReleaseReadinessArtifactValidationActionPlanState(
options ?? { documentRef: document },
)
),
getWorkflowOverviewReleaseReadinessArtifactGateExecutionSummaryViewModel: (validation) => (
createIniPanelShellWorkflowOverviewReleaseReadinessArtifactGateExecutionSummaryViewModel(validation)
),
getWorkflowOverviewReleaseReadinessArtifactUrlGateExecutionSummaryViewModel: (workflow) => (
createIniPanelShellWorkflowOverviewReleaseReadinessArtifactUrlGateExecutionSummaryViewModel(workflow)
),
getWorkflowOverviewReleaseReadinessArtifactGateExecutionSummaryRenderState: (summaryViewModel) => (
createIniPanelShellWorkflowOverviewReleaseReadinessArtifactGateExecutionSummaryRenderState(summaryViewModel)
),
getWorkflowOverviewReleaseReadinessArtifactGateExecutionSummaryDomContract: (options) => (
createIniPanelShellWorkflowOverviewReleaseReadinessArtifactGateExecutionSummaryDomContract(options)
),
getWorkflowOverviewReleaseReadinessArtifactGateExecutionSummaryDomReadiness: (options) => (
createIniPanelShellWorkflowOverviewReleaseReadinessArtifactGateExecutionSummaryDomReadiness(
options ?? { documentRef: document },
)
),
renderWorkflowOverviewReleaseReadinessArtifactGateExecutionSummaryState: (renderState, options) => (
renderIniPanelShellWorkflowOverviewReleaseReadinessArtifactGateExecutionSummaryState(
renderState,
options ?? { documentRef: document },
)
),
mountWorkflowOverviewReleaseReadinessArtifactGateExecutionSummaryState: (options) => (
mountIniPanelShellWorkflowOverviewReleaseReadinessArtifactGateExecutionSummaryState(
options ?? { documentRef: document },
)
),
getWorkflowOverviewReleaseReadinessArtifactDisplayViewModel: (validation) => (
createIniPanelShellWorkflowOverviewReleaseReadinessArtifactDisplayViewModel(validation)
),

View File

@@ -0,0 +1,338 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>LinuxCNC Browser Simulation</title>
<style>
:root {
color-scheme: light;
--page: #eef2f5;
--surface: #ffffff;
--line: #c8d3dc;
--text: #1a252f;
--muted: #596875;
--blue: #1263a3;
--green: #188160;
--steel: #6f7f8d;
--warn: #a9561b;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
font-family: "Segoe UI", "Noto Sans", sans-serif;
color: var(--text);
background: var(--page);
}
main {
display: grid;
grid-template-rows: auto minmax(0, 1fr);
min-height: 100vh;
}
header {
border-bottom: 1px solid var(--line);
background: var(--surface);
padding: 14px 20px;
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 16px;
align-items: center;
}
h1 {
margin: 0;
font-size: 18px;
font-weight: 650;
}
.status-strip {
display: flex;
gap: 8px;
align-items: center;
flex-wrap: wrap;
justify-content: end;
}
.badge {
border: 1px solid var(--line);
border-radius: 6px;
padding: 5px 8px;
background: #f8fafb;
color: var(--muted);
font: 12px/1.2 "Segoe UI", sans-serif;
}
.badge strong {
color: var(--green);
font-weight: 700;
}
.workspace {
display: grid;
grid-template-columns: minmax(280px, 0.75fr) minmax(420px, 1.35fr) minmax(300px, 0.9fr);
min-height: 0;
}
section {
min-width: 0;
min-height: 0;
border-right: 1px solid var(--line);
background: var(--surface);
display: grid;
grid-template-rows: auto minmax(0, 1fr);
}
section:last-child { border-right: 0; }
.section-head {
border-bottom: 1px solid var(--line);
padding: 12px 14px;
display: grid;
gap: 4px;
}
h2 {
margin: 0;
font-size: 14px;
font-weight: 650;
}
.hint {
margin: 0;
color: var(--muted);
font-size: 12px;
line-height: 1.4;
}
.program-lines {
overflow: auto;
padding: 10px 0;
background: #fbfcfd;
font: 13px/1.45 "SFMono-Regular", "Consolas", monospace;
}
.program-lines div {
display: grid;
grid-template-columns: 48px minmax(0, 1fr);
gap: 10px;
padding: 4px 12px;
border-left: 3px solid transparent;
}
.program-lines div[data-active="true"] {
background: #e9f4ef;
border-left-color: var(--green);
}
.program-lines span {
color: var(--steel);
text-align: right;
user-select: none;
}
.program-lines code {
color: var(--text);
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.preview {
background: #f8fafb;
display: grid;
grid-template-rows: minmax(0, 1fr) auto;
}
.toolpath-wrap {
min-height: 0;
padding: 18px;
display: grid;
}
svg {
width: 100%;
height: 100%;
min-height: 320px;
border: 1px solid var(--line);
background: #ffffff;
}
.grid-line {
stroke: #edf1f4;
stroke-width: 0.01;
}
.toolpath {
fill: none;
stroke: var(--blue);
stroke-width: 0.035;
stroke-linecap: round;
stroke-linejoin: round;
}
.toolhead {
fill: var(--warn);
stroke: #ffffff;
stroke-width: 0.025;
}
.readout {
border-top: 1px solid var(--line);
padding: 12px 14px;
display: grid;
gap: 10px;
background: var(--surface);
}
.axis-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
}
.axis-grid div {
border: 1px solid var(--line);
border-radius: 6px;
padding: 8px;
display: grid;
gap: 4px;
background: #fbfcfd;
}
.axis-grid span:first-child {
color: var(--muted);
font-size: 11px;
font-weight: 700;
}
.axis-grid span:last-child {
font: 14px/1.2 "SFMono-Regular", "Consolas", monospace;
}
dl {
margin: 0;
display: grid;
grid-template-columns: 128px minmax(0, 1fr);
gap: 7px 12px;
font-size: 12px;
}
dt { color: var(--muted); }
dd {
margin: 0;
overflow-wrap: anywhere;
font-family: "SFMono-Regular", "Consolas", monospace;
}
.motion-table-wrap {
overflow: auto;
min-height: 0;
}
table {
width: 100%;
border-collapse: collapse;
font: 12px/1.4 "Segoe UI", sans-serif;
}
th, td {
border-bottom: 1px solid #edf1f4;
padding: 7px 8px;
text-align: left;
vertical-align: top;
}
th {
position: sticky;
top: 0;
background: #f8fafb;
z-index: 1;
color: var(--muted);
font-size: 11px;
text-transform: uppercase;
}
td:nth-child(3), td:nth-child(4) {
font-family: "SFMono-Regular", "Consolas", monospace;
}
pre {
margin: 0;
overflow: auto;
padding: 12px;
background: #111820;
color: #dbe7ef;
font: 11px/1.45 "SFMono-Regular", "Consolas", monospace;
max-height: 170px;
}
@media (max-width: 1040px) {
.workspace { grid-template-columns: 1fr; }
section { border-right: 0; border-bottom: 1px solid var(--line); }
svg { min-height: 280px; }
}
@media (max-width: 560px) {
header { grid-template-columns: 1fr; }
.status-strip { justify-content: start; }
.axis-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
dl { grid-template-columns: 1fr; }
}
</style>
</head>
<body data-simulation-ready="false">
<main>
<header>
<h1>LinuxCNC Browser Simulation</h1>
<div class="status-strip">
<span class="badge">Simulation <strong data-simulation-status>loading</strong></span>
<span class="badge" data-runtime-status>LinuxCNC interpreter WASM</span>
</div>
</header>
<div class="workspace">
<section aria-label="G-code program">
<div class="section-head">
<h2>Program</h2>
<p class="hint">Executed through the LinuxCNC-backed WASM interpreter.</p>
</div>
<div class="program-lines" data-program-lines></div>
</section>
<section aria-label="Toolpath preview">
<div class="section-head">
<h2>Toolpath Preview</h2>
<p class="hint">Rendered from LinuxCNC canonical motion events.</p>
</div>
<div class="preview">
<div class="toolpath-wrap">
<svg data-toolpath-svg role="img" aria-label="LinuxCNC canonical toolpath preview" viewBox="-1 -1 2 2">
<line class="grid-line" x1="-10" y1="0" x2="10" y2="0"></line>
<line class="grid-line" x1="0" y1="-10" x2="0" y2="10"></line>
<polyline class="toolpath" data-toolpath-polyline points=""></polyline>
<circle class="toolhead" data-toolpath-head cx="0" cy="0" r="0.055"></circle>
</svg>
</div>
<div class="readout">
<div class="axis-grid">
<div><span>X</span><span data-axis="x">0.000</span></div>
<div><span>Y</span><span data-axis="y">0.000</span></div>
<div><span>Z</span><span data-axis="z">0.000</span></div>
<div><span>A</span><span data-axis="a">0.000</span></div>
<div><span>B</span><span data-axis="b">0.000</span></div>
<div><span>C</span><span data-axis="c">0.000</span></div>
</div>
<dl data-simulation-summary-rows></dl>
</div>
</div>
</section>
<section aria-label="Machine state">
<div class="section-head">
<h2>Machine State</h2>
<p class="hint">Active line, statement, motion rows, and canonical output.</p>
</div>
<div class="motion-table-wrap">
<table>
<thead>
<tr>
<th>Motion</th>
<th>Line</th>
<th>Position</th>
<th>Statement</th>
</tr>
</thead>
<tbody data-motion-rows></tbody>
</table>
<div class="readout">
<dl>
<dt>Active line</dt>
<dd data-active-line>-</dd>
<dt>Statement</dt>
<dd data-active-statement>-</dd>
</dl>
</div>
<pre data-canonical-output></pre>
</div>
</section>
</div>
</main>
<script type="module">
import { runRealBrowserSimulation } from "./simulation-app.js";
try {
const state = await runRealBrowserSimulation({ documentRef: document });
window.linuxCncRealSimulationState = state;
window.linuxCncRealSimulationApi = {
getState: () => window.linuxCncRealSimulationState,
};
} catch (error) {
document.body.dataset.simulationReady = "false";
document.querySelector("[data-simulation-status]").textContent = "failed";
document.querySelector("[data-canonical-output]").textContent = error?.stack ?? error?.message ?? String(error);
throw error;
}
</script>
</body>
</html>

View File

@@ -0,0 +1,239 @@
import { createLinuxCncInterpSdk } from "../../sdk/src/index.js";
export const DEFAULT_SIMULATION_PROGRAM = [
"G0 X0 Y0 Z0",
"G1 X1 Y0 Z0 F100",
"G1 X1 Y1 Z0",
"G1 X0 Y1 Z0",
"G1 X0 Y0 Z0",
"M2",
"",
].join("\n");
const AXES = ["x", "y", "z", "a", "b", "c", "u", "v", "w"];
function readCanonicalNumber(line, name) {
const match = line.match(new RegExp(`(?:^| )${name}=([-+0-9.eE]+)`));
return match ? Number(match[1]) : null;
}
function programLineMap(programText) {
const lines = new Map();
programText.split(/\r?\n/).forEach((line, index) => {
lines.set(index + 1, line.trim() || "(blank)");
});
return lines;
}
export function parseLinuxCncCanonicalMotion(resultText, programText = "") {
const axes = Object.fromEntries(AXES.map((axis) => [axis, 0]));
const sourceLines = programLineMap(programText);
const motion = [];
for (const line of String(resultText).split("\n")) {
const event = line.match(/^canon_event=(STRAIGHT_TRAVERSE|STRAIGHT_FEED|ARC_FEED)\b/);
if (!event) {
continue;
}
for (const axis of AXES) {
const value = readCanonicalNumber(line, axis);
if (value !== null && Number.isFinite(value)) {
axes[axis] = value;
}
}
const sourceLine = readCanonicalNumber(line, "line");
motion.push({
type: event[1],
line: Number.isFinite(sourceLine) ? sourceLine : null,
statement: Number.isFinite(sourceLine) ? (sourceLines.get(sourceLine) ?? "-") : "-",
axes: { ...axes },
raw: line,
});
}
return motion;
}
export function createSimulationSummary({ programText, resultText, motion }) {
const canonicalLines = String(resultText).split("\n").filter((line) => line.startsWith("canon_event="));
const finalAxes = motion.at(-1)?.axes ?? Object.fromEntries(AXES.map((axis) => [axis, 0]));
return {
apiName: "real-browser-simulation-summary",
summaryVersion: 1,
ready: motion.length > 0,
phase: motion.length > 0 ? "ready" : "blocked",
programLineCount: programText.split(/\r?\n/).filter(Boolean).length,
canonicalEventCount: canonicalLines.length,
motionEventCount: motion.length,
finalAxes,
rows: [
{ id: "runtime", label: "Runtime", value: "LinuxCNC interpreter WASM" },
{ id: "program-lines", label: "Program lines", value: `${programText.split(/\r?\n/).filter(Boolean).length}` },
{ id: "canonical-events", label: "Canonical events", value: `${canonicalLines.length}` },
{ id: "motion-events", label: "Motion events", value: `${motion.length}` },
{ id: "final-position", label: "Final XYZ", value: formatPosition(finalAxes) },
],
};
}
export function createToolpathViewBox(motion, padding = 0.25) {
const points = motion.map(({ axes }) => ({ x: axes.x ?? 0, y: axes.y ?? 0 }));
if (points.length === 0) {
return { minX: -1, minY: -1, width: 2, height: 2 };
}
const xs = points.map(({ x }) => x);
const ys = points.map(({ y }) => y);
const minX = Math.min(...xs) - padding;
const maxX = Math.max(...xs) + padding;
const minY = Math.min(...ys) - padding;
const maxY = Math.max(...ys) + padding;
return {
minX,
minY,
width: Math.max(0.1, maxX - minX),
height: Math.max(0.1, maxY - minY),
};
}
export function createToolpathPolylinePoints(motion) {
return motion.map(({ axes }) => `${axes.x ?? 0},${-(axes.y ?? 0)}`).join(" ");
}
export function formatPosition(axes = {}) {
return `X ${formatAxis(axes.x)} Y ${formatAxis(axes.y)} Z ${formatAxis(axes.z)}`;
}
function formatAxis(value) {
return Number.isFinite(value) ? Number(value).toFixed(3) : "0.000";
}
function setText(documentRef, selector, value) {
const node = documentRef.querySelector(selector);
if (node) {
node.textContent = value;
}
}
function renderRows(documentRef, rows) {
const container = documentRef.querySelector("[data-simulation-summary-rows]");
if (!container) {
return;
}
container.textContent = "";
for (const row of rows) {
const term = documentRef.createElement("dt");
term.textContent = row.label;
term.dataset.simulationRow = row.id;
const detail = documentRef.createElement("dd");
detail.textContent = row.value;
detail.dataset.simulationValue = row.id;
container.append(term, detail);
}
}
function renderProgramLines(documentRef, programText, activeLine) {
const container = documentRef.querySelector("[data-program-lines]");
if (!container) {
return;
}
container.textContent = "";
programText.split(/\r?\n/).forEach((line, index) => {
if (!line && index === programText.split(/\r?\n/).length - 1) {
return;
}
const row = documentRef.createElement("div");
row.dataset.programLine = `${index + 1}`;
row.dataset.active = activeLine === index + 1 ? "true" : "false";
const number = documentRef.createElement("span");
number.textContent = `${index + 1}`;
const text = documentRef.createElement("code");
text.textContent = line || " ";
row.append(number, text);
container.append(row);
});
}
function renderAxisReadout(documentRef, axes) {
for (const axis of ["x", "y", "z", "a", "b", "c"]) {
setText(documentRef, `[data-axis="${axis}"]`, formatAxis(axes?.[axis]));
}
}
function renderToolpath(documentRef, motion) {
const svg = documentRef.querySelector("[data-toolpath-svg]");
const polyline = documentRef.querySelector("[data-toolpath-polyline]");
const head = documentRef.querySelector("[data-toolpath-head]");
if (!svg || !polyline || !head) {
return;
}
const viewBox = createToolpathViewBox(motion);
svg.setAttribute(
"viewBox",
`${viewBox.minX} ${-(viewBox.minY + viewBox.height)} ${viewBox.width} ${viewBox.height}`,
);
polyline.setAttribute("points", createToolpathPolylinePoints(motion));
const last = motion.at(-1)?.axes ?? { x: 0, y: 0 };
head.setAttribute("cx", `${last.x ?? 0}`);
head.setAttribute("cy", `${-(last.y ?? 0)}`);
}
function renderMotionTable(documentRef, motion) {
const body = documentRef.querySelector("[data-motion-rows]");
if (!body) {
return;
}
body.textContent = "";
for (const [index, item] of motion.entries()) {
const row = documentRef.createElement("tr");
row.dataset.motionRow = `${index}`;
for (const value of [
item.type,
item.line ?? "-",
formatPosition(item.axes),
item.statement,
]) {
const cell = documentRef.createElement("td");
cell.textContent = `${value}`;
row.append(cell);
}
body.append(row);
}
}
export function renderSimulationState(documentRef, state) {
documentRef.body.dataset.simulationReady = state.summary.ready ? "true" : "false";
setText(documentRef, "[data-simulation-status]", state.summary.ready ? "ready" : "blocked");
setText(documentRef, "[data-runtime-status]", state.summary.rows[0].value);
setText(documentRef, "[data-active-line]", `${state.motion.at(-1)?.line ?? "-"}`);
setText(documentRef, "[data-active-statement]", state.motion.at(-1)?.statement ?? "-");
setText(documentRef, "[data-canonical-output]", state.resultText);
renderRows(documentRef, state.summary.rows);
renderProgramLines(documentRef, state.programText, state.motion.at(-1)?.line ?? null);
renderAxisReadout(documentRef, state.summary.finalAxes);
renderToolpath(documentRef, state.motion);
renderMotionTable(documentRef, state.motion);
}
export async function runRealBrowserSimulation({
documentRef = document,
programText = DEFAULT_SIMULATION_PROGRAM,
interpFactory = createLinuxCncInterpSdk,
} = {}) {
const interp = await interpFactory();
const resultText = interp.runProgram(programText);
const motion = parseLinuxCncCanonicalMotion(resultText, programText);
const summary = createSimulationSummary({ programText, resultText, motion });
const state = {
apiName: "real-browser-simulation-state",
stateVersion: 1,
programText,
resultText,
motion,
summary,
};
renderSimulationState(documentRef, state);
return state;
}