Advance AXIS-style Three.js simulation UI
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>LinuxCNC AXIS Screenshot Smoke</title>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: #bfc5c9;
|
||||
}
|
||||
iframe {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
border: 0;
|
||||
}
|
||||
#status,
|
||||
#axis-diagnostics-artifact {
|
||||
position: fixed;
|
||||
left: -10000px;
|
||||
top: 0;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<pre id="status">running</pre>
|
||||
<pre id="axis-diagnostics-artifact">{}</pre>
|
||||
<script type="module">
|
||||
const status = document.getElementById("status");
|
||||
const artifactNode = document.getElementById("axis-diagnostics-artifact");
|
||||
const okStatusPrefix = ["axis_screenshot_smoke", "ok"].join("=");
|
||||
const failStatusPrefix = ["axis_screenshot_smoke", "fail"].join("=");
|
||||
|
||||
async function loadFrame() {
|
||||
const frame = document.createElement("iframe");
|
||||
frame.src = `../../runtime/ui/simulation/index.html${window.location.search}`;
|
||||
frame.width = `${window.innerWidth}`;
|
||||
frame.height = `${window.innerHeight}`;
|
||||
document.body.append(frame);
|
||||
await new Promise((resolve, reject) => {
|
||||
frame.addEventListener("load", resolve, { once: true });
|
||||
frame.addEventListener("error", reject, { once: true });
|
||||
});
|
||||
return frame;
|
||||
}
|
||||
|
||||
async function waitForReady(frame) {
|
||||
for (let i = 0; i < 1000; i += 1) {
|
||||
const state = frame.contentWindow?.linuxCncRealSimulationState;
|
||||
const canvas = frame.contentDocument?.querySelector("[data-toolpath-three]");
|
||||
const api = frame.contentWindow?.linuxCncRealSimulationApi;
|
||||
if (state?.summary?.ready && api?.exportDiagnosticsArtifact && canvas) {
|
||||
return state;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
}
|
||||
throw new Error("AXIS screenshot smoke did not reach ready simulation API state");
|
||||
}
|
||||
|
||||
function rectOf(doc, selector) {
|
||||
const node = doc.querySelector(selector);
|
||||
if (!node) {
|
||||
throw new Error(`missing screenshot target ${selector}`);
|
||||
}
|
||||
return node.getBoundingClientRect();
|
||||
}
|
||||
|
||||
function assertVisibleRect(name, rect, viewport) {
|
||||
if (rect.width <= 0 || rect.height <= 0) {
|
||||
throw new Error(`${name} has empty rect ${JSON.stringify(rect.toJSON?.() ?? rect)}`);
|
||||
}
|
||||
if (rect.left < -2 || rect.right > viewport.width + 2) {
|
||||
throw new Error(`${name} overflows viewport horizontally: ${rect.left}..${rect.right} of ${viewport.width}`);
|
||||
}
|
||||
if (rect.top < -2 || rect.top > viewport.height + 2) {
|
||||
throw new Error(`${name} starts outside viewport vertically: ${rect.top} of ${viewport.height}`);
|
||||
}
|
||||
}
|
||||
|
||||
function intersectionArea(a, b) {
|
||||
const x = Math.max(0, Math.min(a.right, b.right) - Math.max(a.left, b.left));
|
||||
const y = Math.max(0, Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top));
|
||||
return x * y;
|
||||
}
|
||||
|
||||
function assertNoOverlap(name, a, b) {
|
||||
const area = intersectionArea(a, b);
|
||||
if (area > 1) {
|
||||
throw new Error(`${name} overlap area ${area}`);
|
||||
}
|
||||
}
|
||||
|
||||
function assertCanvasPixel(canvas) {
|
||||
const gl = canvas.getContext("webgl2") || canvas.getContext("webgl");
|
||||
if (!gl) {
|
||||
throw new Error("AXIS screenshot smoke missing WebGL context");
|
||||
}
|
||||
const pixel = new Uint8Array(4);
|
||||
gl.readPixels(Math.floor(canvas.width / 2), Math.floor(canvas.height / 2), 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixel);
|
||||
if (pixel[0] === 0 && pixel[1] === 0 && pixel[2] === 0 && pixel[3] === 0) {
|
||||
throw new Error("AXIS screenshot smoke Three.js center pixel is blank");
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const frame = await loadFrame();
|
||||
await waitForReady(frame);
|
||||
frame.contentWindow.dispatchEvent(new Event("resize"));
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
const doc = frame.contentDocument;
|
||||
const viewport = {
|
||||
width: frame.contentWindow.innerWidth,
|
||||
height: frame.contentWindow.innerHeight,
|
||||
};
|
||||
const canvas = doc.querySelector("[data-toolpath-three]");
|
||||
const canvasRect = canvas.getBoundingClientRect();
|
||||
const hudRect = rectOf(doc, '[data-axis-shell="preview-hud"]');
|
||||
const legendRect = rectOf(doc, '[data-axis-shell="preview-legend"]');
|
||||
const diagnosticsRect = rectOf(doc, '[data-axis-shell="diagnostics"]');
|
||||
const toolTableRect = rectOf(doc, '[data-axis-shell="tool-table"]');
|
||||
const limitsHomeRect = rectOf(doc, '[data-axis-shell="limits-home"]');
|
||||
const historyRect = rectOf(doc, '[data-axis-shell="status-history"]');
|
||||
const gcodeRect = rectOf(doc, '[data-axis-shell="gcode-pane"]');
|
||||
const machineRect = rectOf(doc, '[data-axis-shell="machine-state"]');
|
||||
const statusbarRect = rectOf(doc, '[data-axis-shell="statusbar"]');
|
||||
const activeCodesRect = rectOf(doc, '[data-axis-shell="active-codes"]');
|
||||
|
||||
for (const [name, rect] of [
|
||||
["three canvas", canvasRect],
|
||||
["preview HUD", hudRect],
|
||||
["preview legend", legendRect],
|
||||
["diagnostics", diagnosticsRect],
|
||||
["tool table", toolTableRect],
|
||||
["limits/home", limitsHomeRect],
|
||||
["status history", historyRect],
|
||||
["gcode pane", gcodeRect],
|
||||
["machine state", machineRect],
|
||||
["statusbar", statusbarRect],
|
||||
["active modal codes", activeCodesRect],
|
||||
]) {
|
||||
assertVisibleRect(name, rect, viewport);
|
||||
}
|
||||
const modalText = doc.querySelector("[data-modal-rows]")?.textContent ?? "";
|
||||
if (!modalText.includes("Plane:") || !modalText.includes("Origin:")) {
|
||||
throw new Error(`AXIS screenshot smoke missing LinuxCNC modal code rows: ${modalText}`);
|
||||
}
|
||||
assertNoOverlap("gcode/machine state", gcodeRect, machineRect);
|
||||
assertNoOverlap("diagnostics/status history", diagnosticsRect, historyRect);
|
||||
if (hudRect.left < canvasRect.left || hudRect.top < canvasRect.top || hudRect.right > canvasRect.right + 1 || hudRect.bottom > canvasRect.bottom + 1) {
|
||||
throw new Error("preview HUD should remain inside the Three.js canvas stage");
|
||||
}
|
||||
const statusbarText = doc.querySelector('[data-axis-shell="statusbar"]')?.textContent ?? "";
|
||||
if (!statusbarText.includes("Tool") || !statusbarText.includes("Line") || !statusbarText.includes("Preview")) {
|
||||
throw new Error(`AXIS screenshot smoke missing structured statusbar fields: ${statusbarText}`);
|
||||
}
|
||||
if (toolTableRect.top < diagnosticsRect.top || toolTableRect.bottom > diagnosticsRect.bottom + 1) {
|
||||
throw new Error("tool table should remain inside diagnostics panel");
|
||||
}
|
||||
if (limitsHomeRect.top < diagnosticsRect.top || limitsHomeRect.bottom > diagnosticsRect.bottom + 1) {
|
||||
throw new Error("limits/home panel should remain inside diagnostics panel");
|
||||
}
|
||||
if (legendRect.top < canvasRect.top || legendRect.bottom < canvasRect.bottom) {
|
||||
throw new Error("preview legend should sit below the Three.js canvas");
|
||||
}
|
||||
assertCanvasPixel(canvas);
|
||||
if (!doc.querySelector("[data-tool-table-rows]") || !doc.querySelector("[data-limits-home-rows]") || !doc.querySelector("[data-status-history]")) {
|
||||
throw new Error("AXIS screenshot smoke missing compact panels");
|
||||
}
|
||||
const artifact = frame.contentWindow.linuxCncRealSimulationApi?.exportDiagnosticsArtifact?.();
|
||||
if (artifact?.apiName !== "real-browser-simulation-diagnostics-artifact") {
|
||||
throw new Error("AXIS screenshot smoke missing diagnostics artifact");
|
||||
}
|
||||
if (
|
||||
artifact.preview?.renderer !== "threejs" ||
|
||||
artifact.preview?.three?.pathPoints <= 0 ||
|
||||
artifact.preview?.three?.toolMarkerObjects < 3 ||
|
||||
artifact.preview?.three?.scaleBarObjects !== 4 ||
|
||||
artifact.preview?.three?.orientationTriadObjects !== 6 ||
|
||||
!artifact.preview?.three?.toolGeometry ||
|
||||
!artifact.preview?.three?.scaleBar ||
|
||||
!artifact.preview?.three?.orientationTriad
|
||||
) {
|
||||
throw new Error(`AXIS screenshot smoke diagnostics missing Three.js preview state: ${JSON.stringify(artifact.preview)}`);
|
||||
}
|
||||
artifact.viewport = viewport;
|
||||
artifactNode.textContent = JSON.stringify(artifact);
|
||||
|
||||
status.textContent = `${okStatusPrefix} ${viewport.width}x${viewport.height}`;
|
||||
} catch (error) {
|
||||
status.textContent = `${failStatusPrefix} ${error.stack || error.message}`;
|
||||
throw error;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
302
wasm-port/tests/browser/verify_real_simulation_axis_screenshot_browser.sh
Executable file
302
wasm-port/tests/browser/verify_real_simulation_axis_screenshot_browser.sh
Executable file
@@ -0,0 +1,302 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
CHROMIUM="${CHROMIUM:-$(command -v chromium || command -v chromium-browser || command -v google-chrome || command -v google-chrome-stable || true)}"
|
||||
|
||||
if [[ -z "$CHROMIUM" ]]; then
|
||||
echo "missing Chromium-compatible browser; set CHROMIUM=/path/to/browser" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "${SKIP_INTERP_BUILD:-0}" != "1" ]]; then
|
||||
"$ROOT_DIR/tools/build_wasm_core.sh"
|
||||
fi
|
||||
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
PORT_FILE="$TMP_DIR/port"
|
||||
SERVER_LOG="$TMP_DIR/server.log"
|
||||
CHROME_PROFILE="$TMP_DIR/chrome-profile"
|
||||
mkdir -p "$CHROME_PROFILE"
|
||||
ARTIFACT_DIR="${AXIS_SCREENSHOT_ARTIFACT_DIR:-}"
|
||||
FULL_DIAGNOSTICS="${AXIS_SCREENSHOT_FULL_DIAGNOSTICS:-0}"
|
||||
if [[ -n "$ARTIFACT_DIR" ]]; then
|
||||
mkdir -p "$ARTIFACT_DIR"
|
||||
fi
|
||||
|
||||
cleanup() {
|
||||
if [[ -n "${SERVER_PID:-}" ]]; then
|
||||
kill "$SERVER_PID" 2>/dev/null || true
|
||||
wait "$SERVER_PID" 2>/dev/null || true
|
||||
fi
|
||||
rm -rf "$TMP_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
python3 - <<'PY' "$ROOT_DIR" "$PORT_FILE" >"$SERVER_LOG" 2>&1 &
|
||||
import functools
|
||||
import http.server
|
||||
import pathlib
|
||||
import socketserver
|
||||
import sys
|
||||
|
||||
root = pathlib.Path(sys.argv[1])
|
||||
port_file = pathlib.Path(sys.argv[2])
|
||||
|
||||
handler = functools.partial(http.server.SimpleHTTPRequestHandler, directory=str(root))
|
||||
with socketserver.TCPServer(("127.0.0.1", 0), handler) as httpd:
|
||||
port_file.write_text(str(httpd.server_address[1]), encoding="ascii")
|
||||
httpd.serve_forever()
|
||||
PY
|
||||
SERVER_PID=$!
|
||||
|
||||
for _ in $(seq 1 100); do
|
||||
[[ -s "$PORT_FILE" ]] && break
|
||||
sleep 0.05
|
||||
done
|
||||
|
||||
if [[ ! -s "$PORT_FILE" ]]; then
|
||||
echo "AXIS screenshot browser smoke HTTP server did not start" >&2
|
||||
cat "$SERVER_LOG" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PORT="$(cat "$PORT_FILE")"
|
||||
SMOKE_URL="http://127.0.0.1:$PORT/tests/browser/real_simulation_page_smoke.html"
|
||||
SCREENSHOT_URL="http://127.0.0.1:$PORT/runtime/ui/simulation/index.html"
|
||||
|
||||
run_case() {
|
||||
local name="$1"
|
||||
local size="$2"
|
||||
local query="${3:-}"
|
||||
local out="$TMP_DIR/axis-${name}.dom"
|
||||
local shot="$TMP_DIR/axis-${name}.png"
|
||||
local diagnostics_json="$TMP_DIR/axis-${name}-diagnostics.json"
|
||||
local full_diagnostics_json="$TMP_DIR/axis-${name}-full-diagnostics.json"
|
||||
local screenshot_url="$SCREENSHOT_URL$query"
|
||||
|
||||
"$CHROMIUM" \
|
||||
--headless=new \
|
||||
--disable-gpu \
|
||||
--no-sandbox \
|
||||
--user-data-dir="$CHROME_PROFILE" \
|
||||
--virtual-time-budget=10000 \
|
||||
--dump-dom \
|
||||
"$SMOKE_URL" >"$out" 2>&1
|
||||
|
||||
if ! grep -Fq "browser_real_simulation_page_smoke=ok" "$out"; then
|
||||
echo "AXIS screenshot prerequisite smoke failed for $name" >&2
|
||||
sed -n '1,260p' "$out" >&2
|
||||
exit 1
|
||||
fi
|
||||
"$CHROMIUM" \
|
||||
--headless=new \
|
||||
--disable-gpu \
|
||||
--no-sandbox \
|
||||
--user-data-dir="$CHROME_PROFILE" \
|
||||
--window-size="$size" \
|
||||
--virtual-time-budget=60000 \
|
||||
--screenshot="$shot" \
|
||||
"$screenshot_url" \
|
||||
>/dev/null 2>&1
|
||||
|
||||
if [[ ! -s "$shot" ]]; then
|
||||
echo "AXIS screenshot output missing for $name" >&2
|
||||
exit 1
|
||||
fi
|
||||
local bytes
|
||||
bytes="$(wc -c <"$shot")"
|
||||
if [[ "$bytes" -lt 10000 ]]; then
|
||||
echo "AXIS screenshot output too small for $name: $bytes bytes" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$FULL_DIAGNOSTICS" == "1" ]]; then
|
||||
local cdp_profile="$TMP_DIR/cdp-${name}"
|
||||
mkdir -p "$cdp_profile"
|
||||
"$CHROMIUM" \
|
||||
--headless=new \
|
||||
--disable-gpu \
|
||||
--no-sandbox \
|
||||
--remote-debugging-port=0 \
|
||||
--user-data-dir="$cdp_profile" \
|
||||
--window-size="$size" \
|
||||
"about:blank" \
|
||||
>/dev/null 2>&1 &
|
||||
local cdp_pid=$!
|
||||
node - "$cdp_profile" "$full_diagnostics_json" "$screenshot_url" <<'NODE'
|
||||
const [profile, targetPath, targetUrl] = process.argv.slice(2);
|
||||
const fs = await import("node:fs/promises");
|
||||
const path = await import("node:path");
|
||||
|
||||
async function sleep(ms) {
|
||||
await new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function readDevToolsPort() {
|
||||
const portFile = path.resolve(profile, "DevToolsActivePort");
|
||||
for (let i = 0; i < 200; i += 1) {
|
||||
try {
|
||||
const [port] = (await fs.readFile(portFile, "utf8")).trim().split(/\r?\n/);
|
||||
if (port) {
|
||||
return port;
|
||||
}
|
||||
} catch {
|
||||
// Wait for Chromium to publish the DevTools endpoint.
|
||||
}
|
||||
await sleep(50);
|
||||
}
|
||||
throw new Error("missing DevToolsActivePort");
|
||||
}
|
||||
|
||||
function call(ws, id, method, params = {}) {
|
||||
ws.send(JSON.stringify({ id, method, params }));
|
||||
}
|
||||
|
||||
function sendCommand(ws, pending, id, method, params = {}, timeoutMs = 15000) {
|
||||
call(ws, id, method, params);
|
||||
return Promise.race([
|
||||
new Promise((resolve) => pending.set(id, resolve)),
|
||||
new Promise((_, reject) => setTimeout(() => reject(new Error(`${method} timed out`)), timeoutMs)),
|
||||
]).finally(() => pending.delete(id));
|
||||
}
|
||||
|
||||
async function evaluateArtifact(webSocketDebuggerUrl) {
|
||||
const ws = new WebSocket(webSocketDebuggerUrl);
|
||||
let nextId = 1;
|
||||
const pending = new Map();
|
||||
ws.addEventListener("message", (event) => {
|
||||
const message = JSON.parse(event.data);
|
||||
const waiter = pending.get(message.id);
|
||||
if (waiter) {
|
||||
pending.delete(message.id);
|
||||
waiter(message);
|
||||
}
|
||||
});
|
||||
await new Promise((resolve, reject) => {
|
||||
ws.addEventListener("open", resolve, { once: true });
|
||||
ws.addEventListener("error", reject, { once: true });
|
||||
});
|
||||
await sendCommand(ws, pending, nextId++, "Page.enable");
|
||||
await sendCommand(ws, pending, nextId++, "Runtime.enable");
|
||||
await sendCommand(ws, pending, nextId++, "Page.navigate", { url: targetUrl });
|
||||
await sleep(500);
|
||||
const expression = `(
|
||||
async () => {
|
||||
for (let i = 0; i < 600; i += 1) {
|
||||
const api = window.linuxCncRealSimulationApi;
|
||||
const state = window.linuxCncRealSimulationState;
|
||||
if (api?.exportDiagnosticsArtifact && state?.summary?.ready) {
|
||||
const artifact = api.exportDiagnosticsArtifact();
|
||||
artifact.viewport = { width: window.innerWidth, height: window.innerHeight };
|
||||
return JSON.stringify(artifact);
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
}
|
||||
throw new Error("AXIS diagnostics API did not become ready");
|
||||
}
|
||||
)()`;
|
||||
let lastError = null;
|
||||
for (let attempt = 0; attempt < 30; attempt += 1) {
|
||||
const id = nextId;
|
||||
nextId += 1;
|
||||
const response = await sendCommand(ws, pending, id, "Runtime.evaluate", {
|
||||
expression,
|
||||
awaitPromise: true,
|
||||
returnByValue: true,
|
||||
}, 35000);
|
||||
if (!response.error && !response.result.exceptionDetails) {
|
||||
ws.close();
|
||||
return response.result.result.value;
|
||||
}
|
||||
lastError = response.error?.message ?? response.result.exceptionDetails?.text ?? "Runtime.evaluate failed";
|
||||
if (!/context|destroyed|navigate|ready/i.test(lastError)) {
|
||||
break;
|
||||
}
|
||||
await sleep(250);
|
||||
}
|
||||
ws.close();
|
||||
throw new Error(lastError ?? "Runtime.evaluate failed");
|
||||
}
|
||||
|
||||
const port = await readDevToolsPort();
|
||||
const pages = await (await fetch(`http://127.0.0.1:${port}/json/list`)).json();
|
||||
const page = pages.find((entry) => entry.type === "page" && entry.webSocketDebuggerUrl);
|
||||
if (!page) {
|
||||
throw new Error("missing Chromium page target");
|
||||
}
|
||||
const artifactText = await evaluateArtifact(page.webSocketDebuggerUrl);
|
||||
const artifact = JSON.parse(artifactText);
|
||||
if (artifact.apiName !== "real-browser-simulation-diagnostics-artifact") {
|
||||
throw new Error(`unexpected diagnostics artifact ${artifact.apiName}`);
|
||||
}
|
||||
if (artifact.preview?.renderer !== "threejs" || Number(artifact.preview?.three?.pathPoints ?? 0) <= 0) {
|
||||
throw new Error("diagnostics artifact missing Three.js preview state");
|
||||
}
|
||||
await fs.writeFile(targetPath, `${JSON.stringify(artifact, null, 2)}\n`);
|
||||
NODE
|
||||
local cdp_status=$?
|
||||
kill "$cdp_pid" 2>/dev/null || true
|
||||
wait "$cdp_pid" 2>/dev/null || true
|
||||
if [[ "$cdp_status" -ne 0 || ! -s "$full_diagnostics_json" ]]; then
|
||||
echo "AXIS full diagnostics artifact missing for $name" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
python3 - "$diagnostics_json" "$full_diagnostics_json" "$name" "$size" "$bytes" "$query" <<'PY'
|
||||
import json
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
target, full_diagnostics, name, size, bytes_count, query = sys.argv[1:7]
|
||||
target_path = pathlib.Path(target)
|
||||
screenshot_path = target_path.with_name(target_path.name.replace("-diagnostics.json", ".png"))
|
||||
full_diagnostics_path = pathlib.Path(full_diagnostics)
|
||||
full_artifact = json.loads(full_diagnostics_path.read_text(encoding="utf-8")) if full_diagnostics_path.exists() else {}
|
||||
pathlib.Path(target).write_text(json.dumps({
|
||||
"apiName": "real-browser-simulation-axis-screenshot-artifact",
|
||||
"artifactVersion": 1,
|
||||
"viewportName": name,
|
||||
"windowSize": size,
|
||||
"query": query,
|
||||
"screenshotBytes": int(bytes_count),
|
||||
"validatedBy": "browser_real_simulation_page_smoke",
|
||||
"fullDiagnosticsApiName": full_artifact.get("apiName"),
|
||||
"fullDiagnosticsPath": str(full_diagnostics_path) if full_artifact else None,
|
||||
"previewRenderer": full_artifact.get("preview", {}).get("renderer"),
|
||||
"previewViewMode": full_artifact.get("preview", {}).get("viewMode"),
|
||||
"threeReady": full_artifact.get("preview", {}).get("three", {}).get("ready") is True,
|
||||
"threePathPoints": full_artifact.get("preview", {}).get("three", {}).get("pathPoints"),
|
||||
"screenshotPath": str(screenshot_path),
|
||||
"diagnosticsPath": str(target_path),
|
||||
}, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||
PY
|
||||
if [[ ! -s "$diagnostics_json" ]]; then
|
||||
echo "AXIS diagnostics artifact missing for $name" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ -n "$ARTIFACT_DIR" ]]; then
|
||||
cp "$shot" "$ARTIFACT_DIR/axis-${name}.png"
|
||||
if [[ -s "$full_diagnostics_json" ]]; then
|
||||
cp "$full_diagnostics_json" "$ARTIFACT_DIR/axis-${name}-full-diagnostics.json"
|
||||
fi
|
||||
python3 - "$diagnostics_json" "$ARTIFACT_DIR/axis-${name}-diagnostics.json" "$ARTIFACT_DIR/axis-${name}.png" "$ARTIFACT_DIR/axis-${name}-full-diagnostics.json" <<'PY'
|
||||
import json
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
source, target, screenshot, full_diagnostics = sys.argv[1:5]
|
||||
artifact = json.loads(pathlib.Path(source).read_text(encoding="utf-8"))
|
||||
artifact["diagnosticsPath"] = str(pathlib.Path(target))
|
||||
artifact["screenshotPath"] = str(pathlib.Path(screenshot))
|
||||
artifact["fullDiagnosticsPath"] = str(pathlib.Path(full_diagnostics)) if pathlib.Path(full_diagnostics).exists() else None
|
||||
pathlib.Path(target).write_text(json.dumps(artifact, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||
PY
|
||||
fi
|
||||
}
|
||||
|
||||
run_case desktop-preview 1280,900
|
||||
run_case desktop-dro 1280,900 "?axis_right=dro"
|
||||
run_case mobile-preview 390,920
|
||||
run_case mobile-mdi 390,920 "?axis_left=mdi"
|
||||
|
||||
echo "axis_screenshot_browser_smoke=ok"
|
||||
@@ -7,6 +7,8 @@ const root = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
|
||||
const doc = readFileSync(resolve(root, "docs/axis-style-simulation-implementation.md"), "utf8");
|
||||
const readme = readFileSync(resolve(root, "README.md"), "utf8");
|
||||
const priority = readFileSync(resolve(root, "docs/real-browser-simulation-priority.md"), "utf8");
|
||||
const simulationPackage = JSON.parse(readFileSync(resolve(root, "runtime/ui/simulation/package.json"), "utf8"));
|
||||
const threeVendor = readFileSync(resolve(root, "runtime/ui/simulation/vendor/three/three.core.js"), "utf8");
|
||||
|
||||
for (const required of [
|
||||
"AXIS-Style Browser Simulation Implementation Plan",
|
||||
@@ -15,7 +17,18 @@ for (const required of [
|
||||
"Preview and DRO",
|
||||
"G-code source with active execution line",
|
||||
"window.linuxCncRealSimulationApi",
|
||||
"reloadCurrentProgram()",
|
||||
"getMdiHistory()",
|
||||
"getRecentPrograms()",
|
||||
"Three.js `^0.183.2`",
|
||||
"exportDiagnosticsArtifact()",
|
||||
"getLimitsHomeState()",
|
||||
"getAxisStatusbarState()",
|
||||
"applyAxisViewFromUrl(search)",
|
||||
"AXIS_SCREENSHOT_ARTIFACT_DIR",
|
||||
"AXIS_SCREENSHOT_FULL_DIAGNOSTICS=1",
|
||||
"SKIP_INTERP_BUILD=1 wasm-port/tests/browser/verify_real_simulation_browser.sh",
|
||||
"SKIP_INTERP_BUILD=1 wasm-port/tests/browser/verify_real_simulation_axis_screenshot_browser.sh",
|
||||
"Immediate Next Batch",
|
||||
]) {
|
||||
assert.ok(doc.includes(required), `axis-style simulation doc missing ${required}`);
|
||||
@@ -35,5 +48,11 @@ assert.ok(
|
||||
priority.includes("docs/axis-style-simulation-implementation.md"),
|
||||
"priority doc should link AXIS-style implementation plan",
|
||||
);
|
||||
assert.equal(
|
||||
simulationPackage.dependencies?.three,
|
||||
"^0.183.2",
|
||||
"simulation package should pin the requested Three.js dependency range",
|
||||
);
|
||||
assert.match(threeVendor, /REVISION = '183'/, "vendored Three.js build should match the requested 0.183.x runtime");
|
||||
|
||||
console.log("axis_style_simulation_docs_node_smoke=ok");
|
||||
|
||||
@@ -18,6 +18,7 @@ const hostSmokeText = readFileSync(resolve(root, "tests/host/verify_host_smokes.
|
||||
const releaseGateText = readFileSync(resolve(root, "tests/host/verify_project_release_gate.sh"), "utf8");
|
||||
const simulationHtmlText = readFileSync(resolve(root, "runtime/ui/simulation/index.html"), "utf8");
|
||||
const simulationSmokeText = readFileSync(resolve(root, "tests/browser/verify_real_simulation_browser.sh"), "utf8");
|
||||
const simulationScreenshotSmokeText = readFileSync(resolve(root, "tests/browser/verify_real_simulation_axis_screenshot_browser.sh"), "utf8");
|
||||
|
||||
for (const phrase of [
|
||||
"Real Browser Simulation Priority",
|
||||
@@ -44,10 +45,17 @@ for (const text of [handoffText, readmeText, trackerText]) {
|
||||
|
||||
assert.match(hostSmokeText, /verify_real_browser_simulation_priority_docs\.sh/);
|
||||
assert.match(hostSmokeText, /verify_real_simulation_browser\.sh/);
|
||||
assert.match(hostSmokeText, /verify_real_simulation_axis_screenshot_browser\.sh/);
|
||||
assert.match(releaseGateText, /verify_real_simulation_browser\.sh/);
|
||||
assert.match(releaseGateText, /verify_real_simulation_axis_screenshot_browser\.sh/);
|
||||
assert.match(simulationHtmlText, /LinuxCNC Browser Simulation/);
|
||||
assert.match(simulationHtmlText, /data-toolpath-svg/);
|
||||
assert.match(simulationHtmlText, /data-toolpath-three/);
|
||||
assert.match(simulationHtmlText, /data-program-lines/);
|
||||
assert.match(simulationSmokeText, /browser_real_simulation_page_smoke=ok/);
|
||||
assert.match(simulationScreenshotSmokeText, /axis_screenshot_browser_smoke=ok/);
|
||||
assert.match(simulationScreenshotSmokeText, /--screenshot=/);
|
||||
assert.match(simulationScreenshotSmokeText, /AXIS_SCREENSHOT_ARTIFACT_DIR/);
|
||||
assert.match(simulationScreenshotSmokeText, /axis-\$\{name\}-diagnostics\.json/);
|
||||
|
||||
console.log("real_browser_simulation_priority_docs_node_smoke=ok");
|
||||
|
||||
@@ -23,6 +23,7 @@ SKIP_TP_BUILD=1 "$ROOT_DIR/tests/wasm/node/verify_tp_wasm.sh"
|
||||
"$ROOT_DIR/tests/ui/node/verify_ui_node_smokes.sh"
|
||||
SKIP_INI_BUILD=1 SKIP_INTERP_BUILD=1 "$ROOT_DIR/tests/browser/verify_ini_panel_browser.sh"
|
||||
SKIP_INTERP_BUILD=1 "$ROOT_DIR/tests/browser/verify_real_simulation_browser.sh"
|
||||
SKIP_INTERP_BUILD=1 "$ROOT_DIR/tests/browser/verify_real_simulation_axis_screenshot_browser.sh"
|
||||
SKIP_INI_BUILD=1 SKIP_INTERP_BUILD=1 "$ROOT_DIR/tests/browser/verify_opfs_session_workflow_browser.sh"
|
||||
SKIP_INI_BUILD=1 SKIP_INTERP_BUILD=1 "$ROOT_DIR/tests/browser/verify_interp_browser.sh"
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ SKIP_INTERP_BUILD=1 "$ROOT_DIR/tests/wasm/node/verify_interp_wasm.sh"
|
||||
SKIP_INTERP_BUILD=1 "$ROOT_DIR/tests/wasm/node/verify_sim_configs_inventory_wasm.sh"
|
||||
SKIP_INI_BUILD=1 SKIP_INTERP_BUILD=1 "$ROOT_DIR/tests/browser/verify_ini_panel_browser.sh"
|
||||
SKIP_INTERP_BUILD=1 "$ROOT_DIR/tests/browser/verify_real_simulation_browser.sh"
|
||||
SKIP_INTERP_BUILD=1 "$ROOT_DIR/tests/browser/verify_real_simulation_axis_screenshot_browser.sh"
|
||||
SKIP_INI_BUILD=1 SKIP_INTERP_BUILD=1 "$ROOT_DIR/tests/browser/verify_opfs_session_workflow_browser.sh"
|
||||
SKIP_INI_BUILD=1 SKIP_INTERP_BUILD=1 "$ROOT_DIR/tests/browser/verify_release_artifact_url_workflow_browser.sh"
|
||||
"$ROOT_DIR/tests/sdk/node/verify_project_batch_acceptance_workflow.sh"
|
||||
|
||||
@@ -27,6 +27,8 @@ assert.equal(validation.gateExecutionManifestReady, true);
|
||||
assert.equal(validation.gateExecutionSummaryReady, true);
|
||||
assert.equal(validation.gateResultMatrixReady, true);
|
||||
assert.equal(validation.gateActionPlanReady, true);
|
||||
assert.equal(validation.axisScreenshotArtifactSummaryReady, true);
|
||||
assert.ok(validation.axisScreenshotArtifactCount === 0 || validation.axisScreenshotArtifactCount === 4);
|
||||
assert.deepEqual(validation.expectedGateIds, [
|
||||
"diff-check",
|
||||
"vendor-sync",
|
||||
@@ -50,5 +52,11 @@ assert.equal(artifact.gateResultMatrix.apiName, "project-release-gate-result-mat
|
||||
assert.equal(artifact.gateActionPlan.apiName, "project-release-gate-action-plan");
|
||||
assert.equal(artifact.gateActionPlan.pendingCount, 0);
|
||||
assert.equal(artifact.gateActionPlan.nextCommand, null);
|
||||
assert.equal(artifact.axisScreenshotArtifactSummary.apiName, "project-release-axis-screenshot-artifact-summary");
|
||||
if (artifact.axisScreenshotArtifactSummary.artifactCount > 0) {
|
||||
assert.equal(artifact.axisScreenshotArtifactSummary.artifactCount, 4);
|
||||
assert.equal(artifact.axisScreenshotArtifactSummary.ready, true);
|
||||
assert.deepEqual(artifact.axisScreenshotArtifactSummary.missingViewports, []);
|
||||
}
|
||||
|
||||
console.log("project_release_readiness_artifact_node_smoke=ok");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mkdirSync, renameSync, writeFileSync } from "node:fs";
|
||||
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
@@ -7,6 +7,9 @@ import { createProjectReleaseReadinessReport } from "../../runtime/sdk/src/index
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const root = resolve(__dirname, "../..");
|
||||
const outputPath = process.argv[2] ?? resolve(root, "build/project-release-readiness.json");
|
||||
const axisScreenshotArtifactDir = process.env.AXIS_SCREENSHOT_ARTIFACT_DIR
|
||||
? resolve(process.env.AXIS_SCREENSHOT_ARTIFACT_DIR)
|
||||
: null;
|
||||
|
||||
const passedGateResults = {
|
||||
"diff-check": true,
|
||||
@@ -23,6 +26,25 @@ const passedGateResults = {
|
||||
"project-release-gate": true,
|
||||
};
|
||||
|
||||
function loadAxisScreenshotArtifacts(directory) {
|
||||
if (!directory || !existsSync(directory)) {
|
||||
return [];
|
||||
}
|
||||
return readdirSync(directory)
|
||||
.filter((filename) => /^axis-.+-diagnostics\.json$/.test(filename))
|
||||
.sort()
|
||||
.map((filename) => {
|
||||
const diagnosticsPath = resolve(directory, filename);
|
||||
const artifact = JSON.parse(readFileSync(diagnosticsPath, "utf8"));
|
||||
const screenshotPath = resolve(directory, filename.replace(/-diagnostics\.json$/, ".png"));
|
||||
return {
|
||||
...artifact,
|
||||
diagnosticsPath,
|
||||
screenshotPath,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const report = createProjectReleaseReadinessReport({
|
||||
gateResults: passedGateResults,
|
||||
observedOutputs: [
|
||||
@@ -38,6 +60,7 @@ const report = createProjectReleaseReadinessReport({
|
||||
"host_wasm_opfs_browser_smokes=ok",
|
||||
"project_release_gate=ok",
|
||||
],
|
||||
axisScreenshotArtifacts: loadAxisScreenshotArtifacts(axisScreenshotArtifactDir),
|
||||
});
|
||||
|
||||
mkdirSync(dirname(outputPath), { recursive: true });
|
||||
|
||||
@@ -248,6 +248,11 @@ assert.deepEqual(createProjectReleaseReadinessSummaryViewModel(report), {
|
||||
label: "Blocked runtime families",
|
||||
value: "L4-USER-M-PROCESS, L4-TOOL-DB, L4-PYTHON-REMAP",
|
||||
},
|
||||
{
|
||||
id: "axis-screenshot-artifacts",
|
||||
label: "AXIS screenshot artifacts",
|
||||
value: "not captured",
|
||||
},
|
||||
{
|
||||
id: "missing",
|
||||
label: "Missing readiness",
|
||||
|
||||
@@ -545,6 +545,37 @@ assert.equal(createProjectReleaseGateActionPlan({
|
||||
gateResults: Object.fromEntries(createProjectReleaseGateManifest().gateIds.map((id) => [id, true])),
|
||||
}).shellScript, "# Project release gates are already marked passed.\n");
|
||||
assert.equal(createProjectReleaseReadinessSummaryViewModel(releaseReadinessReady).statusLine, "Ready: No blocking reasons");
|
||||
assert.equal(
|
||||
createProjectReleaseReadinessSummaryViewModel(releaseReadinessReady).rows.find(({ id }) => id === "axis-screenshot-artifacts")?.value,
|
||||
"not captured",
|
||||
);
|
||||
const releaseReadinessWithScreenshots = createProjectReleaseReadinessReport({
|
||||
gateResults: Object.fromEntries(createProjectReleaseGateManifest().gateIds.map((id) => [id, true])),
|
||||
axisScreenshotArtifacts: ["desktop-preview", "desktop-dro", "mobile-preview", "mobile-mdi"].map((viewportName) => ({
|
||||
apiName: "real-browser-simulation-axis-screenshot-artifact",
|
||||
artifactVersion: 1,
|
||||
viewportName,
|
||||
windowSize: viewportName.startsWith("mobile") ? "390,920" : "1280,900",
|
||||
query: viewportName.endsWith("dro") ? "?axis_right=dro" : "",
|
||||
screenshotBytes: 20000,
|
||||
validatedBy: "browser_real_simulation_page_smoke",
|
||||
fullDiagnosticsApiName: "real-browser-simulation-diagnostics-artifact",
|
||||
fullDiagnosticsPath: `/tmp/${viewportName}-full.json`,
|
||||
previewRenderer: "threejs",
|
||||
previewViewMode: viewportName.endsWith("dro") ? "top" : "top",
|
||||
threeReady: true,
|
||||
threePathPoints: 3,
|
||||
screenshotPath: `/tmp/${viewportName}.png`,
|
||||
diagnosticsPath: `/tmp/${viewportName}.json`,
|
||||
})),
|
||||
});
|
||||
assert.equal(releaseReadinessWithScreenshots.ready, true);
|
||||
assert.equal(releaseReadinessWithScreenshots.axisScreenshotArtifactSummary.ready, true);
|
||||
assert.equal(releaseReadinessWithScreenshots.axisScreenshotArtifactSummary.artifactCount, 4);
|
||||
assert.equal(
|
||||
createProjectReleaseReadinessSummaryViewModel(releaseReadinessWithScreenshots).rows.find(({ id }) => id === "axis-screenshot-artifacts")?.value,
|
||||
"4/4 captured",
|
||||
);
|
||||
assert.equal(
|
||||
createProjectReleaseGateResultMatrix({
|
||||
observedOutputs: ["project_release_gate=ok"],
|
||||
@@ -572,6 +603,8 @@ assert.deepEqual(releaseReadinessArtifactValidation, {
|
||||
gateExecutionSummaryReady: true,
|
||||
gateResultMatrixReady: true,
|
||||
gateActionPlanReady: true,
|
||||
axisScreenshotArtifactSummaryReady: true,
|
||||
axisScreenshotArtifactCount: 0,
|
||||
blockedRuntimeFamilies: [
|
||||
"L4-USER-M-PROCESS",
|
||||
"L4-TOOL-DB",
|
||||
@@ -613,6 +646,11 @@ assert.deepEqual(releaseReadinessArtifactValidation, {
|
||||
label: "Gate action plan",
|
||||
value: "ready",
|
||||
},
|
||||
{
|
||||
id: "axis-screenshot-artifacts",
|
||||
label: "AXIS screenshot artifacts",
|
||||
value: "not captured",
|
||||
},
|
||||
{
|
||||
id: "validation",
|
||||
label: "Artifact validation",
|
||||
|
||||
@@ -1655,7 +1655,7 @@ const releaseArtifactMountResult = mountIniPanelShellWorkflowOverviewReleaseRead
|
||||
rowsNode: releaseArtifactRowsNode,
|
||||
});
|
||||
assert.equal(releaseArtifactRenderResult.rendered, true);
|
||||
assert.equal(releaseArtifactRenderResult.rowCount, 8);
|
||||
assert.equal(releaseArtifactRenderResult.rowCount, 9);
|
||||
assert.equal(releaseArtifactRenderResult.dataset.handoffScope, "workflow-overview-release-readiness-artifact");
|
||||
assert.equal(
|
||||
releaseArtifactRenderResult.rowIds.includes("gate-execution-manifest"),
|
||||
@@ -1669,8 +1669,12 @@ assert.equal(
|
||||
releaseArtifactRenderResult.rowIds.includes("gate-action-plan"),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
releaseArtifactRenderResult.rowIds.includes("axis-screenshot-artifacts"),
|
||||
true,
|
||||
);
|
||||
assert.equal(releaseArtifactMountResult.ready, true);
|
||||
assert.equal(releaseArtifactMountResult.renderResult.rowCount, 8);
|
||||
assert.equal(releaseArtifactMountResult.renderResult.rowCount, 9);
|
||||
assert.deepEqual(
|
||||
validateIniPanelShellWorkflowOverviewReleaseReadinessArtifactJson("{").missing,
|
||||
["artifact-json"],
|
||||
@@ -1711,7 +1715,7 @@ assert.equal(
|
||||
);
|
||||
assert.equal(releaseArtifactUrlWorkflow.renderState.statusLine, "Ready: No blocking reasons");
|
||||
assert.equal(releaseArtifactUrlWorkflow.mountResult.ready, true);
|
||||
assert.equal(releaseArtifactUrlWorkflow.mountResult.renderResult.rowCount, 8);
|
||||
assert.equal(releaseArtifactUrlWorkflow.mountResult.renderResult.rowCount, 9);
|
||||
const releaseArtifactUrlWorkflowSummary =
|
||||
createIniPanelShellWorkflowOverviewReleaseReadinessArtifactUrlWorkflowSummaryViewModel(
|
||||
releaseArtifactUrlWorkflow,
|
||||
|
||||
@@ -7,12 +7,19 @@ import {
|
||||
DEFAULT_SIMULATION_PROGRAM,
|
||||
DEFAULT_SIMULATION_PROGRAM_ID,
|
||||
createPlaybackFrame,
|
||||
createDroState,
|
||||
createMachineStatusState,
|
||||
createModalState,
|
||||
createSimulationSummary,
|
||||
createToolpathPolylinePoints,
|
||||
createToolpathViewBox,
|
||||
getSimulationTestProgram,
|
||||
getSimulationTestPrograms,
|
||||
panToolpathViewBox,
|
||||
parseLinuxCncCanonicalMotion,
|
||||
runLinuxCncProgram,
|
||||
runRealBrowserSimulation,
|
||||
zoomToolpathViewBox,
|
||||
} from "../../../runtime/ui/simulation/simulation-app.js";
|
||||
import {
|
||||
SIMULATION_TEST_PROGRAMS as directoryPrograms,
|
||||
@@ -64,16 +71,59 @@ const arcCanonical = [
|
||||
"canon_event=ARC_FEED line=4 first_end=0 second_end=0 first_axis=1 second_axis=0 rotation=1 axis_end_point=0 a=0 b=0 c=0 u=0 v=0 w=0",
|
||||
].join("\n");
|
||||
const arcMotion = parseLinuxCncCanonicalMotion(arcCanonical, arcProgram.text);
|
||||
const arcModal = createModalState(arcCanonical);
|
||||
const unavailableMachineStatus = createMachineStatusState(arcCanonical);
|
||||
|
||||
assert.equal(arcMotion.length, 3);
|
||||
assert.equal(arcMotion[1].type, "ARC_FEED");
|
||||
assert.equal(arcMotion[1].statement, "G2 X1 Y1 I1 J0 F80");
|
||||
assert.deepEqual(arcMotion[1].arc.planeAxes, ["x", "y", "z"]);
|
||||
assert.equal(arcMotion[1].arc.center.x, 1);
|
||||
assert.equal(arcMotion[1].arc.center.y, 0);
|
||||
assert.equal(arcMotion[1].arc.rotation, -1);
|
||||
assert.deepEqual(
|
||||
{ x: arcMotion[1].axes.x, y: arcMotion[1].axes.y, z: arcMotion[1].axes.z },
|
||||
{ x: 1, y: 1, z: 0 },
|
||||
"arc parser should map LinuxCNC canonical arc endpoints onto active-plane axes",
|
||||
);
|
||||
assert.equal(createToolpathPolylinePoints(arcMotion), "0,0 1,-1 0,0");
|
||||
assert.equal(arcModal.apiName, "real-browser-simulation-modal-state");
|
||||
assert.equal(arcModal.source, "linuxcnc-update-tag");
|
||||
assert.ok(arcModal.raw.includes("canon_event=UPDATE_TAG line=1"));
|
||||
assert.ok(arcModal.rows.some(({ id, value, source }) => id === "plane" && value === "plane=170" && source === "linuxcnc-update-tag"));
|
||||
assert.equal(unavailableMachineStatus.spindle.state, "n/a");
|
||||
assert.equal(unavailableMachineStatus.coolant.mist, "n/a");
|
||||
|
||||
const runtimeMachineStatus = createMachineStatusState([
|
||||
"canon_event=SET_SPINDLE_SPEED spindle=0 speed=1200",
|
||||
"canon_event=START_SPINDLE_CLOCKWISE spindle=0 wait_for_at_speed=1",
|
||||
"canon_event=STOP_SPINDLE_TURNING spindle=0",
|
||||
"canon_event=MIST_ON",
|
||||
"canon_event=FLOOD_ON",
|
||||
"canon_event=MIST_OFF",
|
||||
"canon_event=FLOOD_OFF",
|
||||
"canon_event=DISABLE_FEED_OVERRIDE",
|
||||
"canon_event=ENABLE_FEED_OVERRIDE",
|
||||
"canon_event=DISABLE_SPEED_OVERRIDE spindle=0",
|
||||
"canon_event=ENABLE_SPEED_OVERRIDE spindle=0",
|
||||
].join("\n"));
|
||||
assert.equal(runtimeMachineStatus.spindle.state, "off");
|
||||
assert.equal(runtimeMachineStatus.spindle.direction, "cw");
|
||||
assert.equal(runtimeMachineStatus.spindle.speed, "1200");
|
||||
assert.equal(runtimeMachineStatus.coolant.mist, "off");
|
||||
assert.equal(runtimeMachineStatus.coolant.flood, "off");
|
||||
assert.equal(runtimeMachineStatus.overrides.feed, "enabled");
|
||||
assert.equal(runtimeMachineStatus.overrides.speed, "enabled");
|
||||
|
||||
const toolMachineStatus = createMachineStatusState([
|
||||
"canon_event=SELECT_TOOL tool=2",
|
||||
"canon_event=USE_TOOL_LENGTH_OFFSET x=0 y=0 z=1.25 a=0 b=0 c=0 u=0 v=0 w=0",
|
||||
"canon_event=CHANGE_TOOL_NUMBER pocket=2",
|
||||
].join("\n"));
|
||||
assert.equal(toolMachineStatus.tool.selected, "2");
|
||||
assert.equal(toolMachineStatus.tool.current, "2");
|
||||
assert.equal(toolMachineStatus.tool.pocket, "2");
|
||||
assert.equal(toolMachineStatus.tool.lengthOffset, "z=1.250");
|
||||
|
||||
const xzArcCanonical = [
|
||||
"canon_event=UPDATE_TAG line=1 g0=-1 motion=0 plane=180 origin=530 feed=0 speed=0 flags=1048994",
|
||||
@@ -101,12 +151,20 @@ const playbackState = {
|
||||
motion: arcMotion,
|
||||
};
|
||||
const firstFrame = createPlaybackFrame(playbackState, 0);
|
||||
const firstDro = createDroState(firstFrame);
|
||||
assert.equal(firstFrame.apiName, "real-browser-simulation-playback-frame");
|
||||
assert.equal(firstFrame.index, 0);
|
||||
assert.equal(firstFrame.step, 1);
|
||||
assert.equal(firstFrame.total, 3);
|
||||
assert.equal(firstFrame.activeLine, 1);
|
||||
assert.equal(firstFrame.visibleMotion.length, 1);
|
||||
assert.equal(firstDro.apiName, "real-browser-simulation-dro-state");
|
||||
assert.equal(firstDro.actual.x, "0.000");
|
||||
assert.equal(firstDro.distanceToGo.x, "n/a");
|
||||
assert.equal(firstDro.workOffsetG54.x, "n/a");
|
||||
assert.equal(firstDro.g92Offset.x, "n/a");
|
||||
assert.equal(firstDro.toolLengthOffset.x, "n/a");
|
||||
assert.equal(firstDro.velocity, "n/a");
|
||||
|
||||
const arcFrame = createPlaybackFrame(playbackState, 1);
|
||||
assert.equal(arcFrame.index, 1);
|
||||
@@ -120,6 +178,34 @@ const clampedFrame = createPlaybackFrame(playbackState, 99);
|
||||
assert.equal(clampedFrame.index, 2);
|
||||
assert.equal(clampedFrame.progress, 100);
|
||||
|
||||
const fitViewBox = createToolpathViewBox(arcMotion);
|
||||
const zoomedViewBox = zoomToolpathViewBox(fitViewBox, 2);
|
||||
assert.ok(zoomedViewBox.width < fitViewBox.width, "zoomed toolpath viewBox should narrow around the fit center");
|
||||
assert.ok(zoomedViewBox.height < fitViewBox.height, "zoomed toolpath viewBox should narrow around the fit center");
|
||||
const pannedViewBox = panToolpathViewBox(zoomedViewBox, { x: 0.5, y: -0.25 });
|
||||
assert.equal(pannedViewBox.minX, zoomedViewBox.minX + 0.5);
|
||||
assert.equal(pannedViewBox.minY, zoomedViewBox.minY - 0.25);
|
||||
assert.equal(pannedViewBox.width, zoomedViewBox.width);
|
||||
assert.equal(pannedViewBox.height, zoomedViewBox.height);
|
||||
|
||||
const interpCalls = [];
|
||||
const fakeInterp = {
|
||||
runProgram(programText) {
|
||||
interpCalls.push(["runProgram", programText]);
|
||||
return arcCanonical;
|
||||
},
|
||||
runProgramWithIni(programText, iniPath) {
|
||||
interpCalls.push(["runProgramWithIni", programText, iniPath]);
|
||||
return arcCanonical;
|
||||
},
|
||||
};
|
||||
assert.equal(runLinuxCncProgram(fakeInterp, arcProgram.text), arcCanonical);
|
||||
assert.equal(runLinuxCncProgram(fakeInterp, arcProgram.text, { iniPath: "/work/axis.ini" }), arcCanonical);
|
||||
assert.deepEqual(interpCalls, [
|
||||
["runProgram", arcProgram.text],
|
||||
["runProgramWithIni", arcProgram.text, "/work/axis.ini"],
|
||||
]);
|
||||
|
||||
const customRunDocument = {
|
||||
body: { dataset: {} },
|
||||
querySelector: () => null,
|
||||
@@ -133,5 +219,22 @@ assert.equal(customRun.program.id, "custom");
|
||||
assert.equal(customRun.program.source, "custom");
|
||||
assert.ok(customRun.resultText.includes("canon_event=STRAIGHT_FEED line=2 x=4.25 y=2.5"));
|
||||
assert.ok(customRun.summary.ready);
|
||||
assert.equal(customRun.modal.apiName, "real-browser-simulation-modal-state");
|
||||
|
||||
const sessionRunCalls = [];
|
||||
const sessionRun = await runRealBrowserSimulation({
|
||||
documentRef: customRunDocument,
|
||||
programText: arcProgram.text,
|
||||
interp: {
|
||||
runProgramWithIni(programText, iniPath) {
|
||||
sessionRunCalls.push([programText, iniPath]);
|
||||
return arcCanonical;
|
||||
},
|
||||
},
|
||||
iniPath: "/work/session.ini",
|
||||
});
|
||||
assert.equal(sessionRun.execution.mode, "linuxcnc-wasm-with-ini");
|
||||
assert.equal(sessionRun.execution.iniPath, "/work/session.ini");
|
||||
assert.deepEqual(sessionRunCalls, [[arcProgram.text, "/work/session.ini"]]);
|
||||
|
||||
console.log("real_simulation_programs_node_smoke=ok");
|
||||
|
||||
Reference in New Issue
Block a user