diff --git a/ClumsyPilot/TrajectoryPlanningVisualization/Web/app.css b/ClumsyPilot/TrajectoryPlanningVisualization/Web/app.css index 9c57d8c..9cf1062 100644 --- a/ClumsyPilot/TrajectoryPlanningVisualization/Web/app.css +++ b/ClumsyPilot/TrajectoryPlanningVisualization/Web/app.css @@ -21,13 +21,34 @@ main { max-width: 1500px; margin: auto; padding: 22px 30px 36px; } .summary-value { border-bottom: 1px solid var(--grid); padding: 4px 0; } .summary-value small { display: block; color: #66707a; } .world-stack { position: relative; width: 100%; min-height: 520px; border: 1px solid var(--grid); overflow: hidden; background: #fff; } #occupancy-grid, #world-overlay { position: absolute; inset: 0; width: 100%; height: 100%; } #world-overlay { pointer-events: none; } -#ls-st, #kinematics { display: grid; grid-template-columns: repeat(auto-fit, minmax(360px, 1fr)); gap: 18px; } +main > section[hidden] { display: none !important; } +#ls-st:not([hidden]), #kinematics:not([hidden]) { display: grid; grid-template-columns: repeat(auto-fit, minmax(360px, 1fr)); gap: 18px; } .chart { min-height: 280px; border: 1px solid var(--grid); padding: 8px; } .chart svg { display: block; width: 100%; height: 244px; } .chart-title { font-size: 14px; font-weight: 600; } .chart-note { min-height: 20px; color: #66707a; font-size: 12px; } .chart-grid { stroke: var(--grid); stroke-width: .55; } .chart-axis { stroke: #59636d; stroke-width: .8; } .chart-data { fill: none; stroke-width: 1.1; vector-effect: non-scaling-stroke; } .chart-solid { stroke: var(--current-trajectory); } .chart-dashed { stroke: var(--previous-trajectory); stroke-dasharray: 5 4; } .chart-limit { stroke: var(--failure); stroke-dasharray: 3 3; } .axis-label { fill: #48515b; font: 11px sans-serif; } .axis-tick { fill: #66707a; font: 10px sans-serif; } -.world-line { fill: none; stroke-width: 1.1; vector-effect: non-scaling-stroke; } .world-static { stroke: #8d959d; } .world-future { stroke: #b6bec6; stroke-dasharray: 5 4; } .world-active { stroke: #1769aa; } .world-previous { stroke: #8d959d; stroke-dasharray: 5 4; } .world-marker { fill: #d87918; stroke: #ffffff; stroke-width: .8; } +.axis-label-x, .axis-label-y { fill: #48515b; font: 11px sans-serif; } .axis-tick-x, .axis-tick-y { fill: #66707a; font: 10px sans-serif; } +.chart-annotation-line { stroke: var(--handoff); stroke-width: .8; stroke-dasharray: 2 2; } +.chart-annotation-point { fill: var(--handoff); stroke: #ffffff; stroke-width: .6; } +.chart-annotation-label { fill: var(--handoff); font: 10px sans-serif; } +.world-line { fill: none; vector-effect: non-scaling-stroke; } +.world-static { stroke: #8d959d; stroke-width: .9; } +.world-future { stroke: #b6bec6; stroke-dasharray: 5 4; stroke-width: .85; } +.world-active { stroke: #1769aa; stroke-width: 1.1; } +.world-previous { stroke: #8d959d; stroke-dasharray: 5 4; stroke-width: .85; } +.world-coarse { stroke: #9aa1a8; stroke-opacity: .48; stroke-dasharray: 5 4; stroke-width: .85; } +.world-local-g2 { stroke: #4f5963; stroke-width: .95; } +.world-segment-active { stroke: #5d91bd; stroke-opacity: .62; stroke-width: 1.0; } +.world-direction-inactive { stroke: #c4cad0; stroke-opacity: .52; stroke-dasharray: 4 4; stroke-width: .8; } +.world-current { stroke: #1769aa; stroke-width: 1.15; } +.world-marker { fill: #d87918; stroke: #ffffff; stroke-width: .8; } +.marker-vehicle .vehicle-outline { fill: none; stroke: #20252b; stroke-width: .7; } +.marker-vehicle .vehicle-heading { stroke: #20252b; stroke-width: .8; } +.marker-plan-start { fill: #3f7d46; stroke: #ffffff; stroke-width: .6; } +.marker-gear-switch { fill: #d87918; stroke: #ffffff; stroke-width: .7; } +.marker-final-goal { fill: #1769aa; stroke: #ffffff; stroke-width: .7; } +.world-empty-state { fill: #66707a; font-size: .42; } .history-panel, .configuration-panel { margin-bottom: 26px; } #cycle-history { overflow-x: auto; } table { width: 100%; border-collapse: collapse; font-size: 12px; } th, td { padding: 6px 8px; border-bottom: 1px solid var(--grid); text-align: left; white-space: nowrap; } th { color: #59636d; font-weight: 600; } .config-group { margin: 13px 0; border-top: 1px solid var(--grid); } .config-group h3 { margin: 8px 0; font-size: 13px; } .config-entry { display: grid; grid-template-columns: minmax(110px, 1fr) minmax(130px, 1fr) minmax(90px, 1fr) 70px; gap: 8px; padding: 4px 0; border-bottom: 1px dotted #e5e8eb; } .raw { color: #66707a; font-family: ui-monospace, Consolas, monospace; } .failure { color: var(--failure); } .notice { color: var(--handoff); } diff --git a/ClumsyPilot/TrajectoryPlanningVisualization/Web/app.js b/ClumsyPilot/TrajectoryPlanningVisualization/Web/app.js index 546d4ff..4062da1 100644 --- a/ClumsyPilot/TrajectoryPlanningVisualization/Web/app.js +++ b/ClumsyPilot/TrajectoryPlanningVisualization/Web/app.js @@ -25,7 +25,7 @@ function getBounds() { const values = []; - const include = points => safeArray(points).forEach(point => { if (finite(point.x) && finite(point.y)) values.push(point); }); + const include = points => safeArray(points).forEach(point => { if (point && finite(point.x) && finite(point.y)) values.push(point); }); const snapshot = state.staticSnapshot || {}; include([ { x: snapshot.worldBounds && snapshot.worldBounds.xMin, y: snapshot.worldBounds && snapshot.worldBounds.yMin }, { x: snapshot.worldBounds && snapshot.worldBounds.xMax, y: snapshot.worldBounds && snapshot.worldBounds.yMax } ]); safeArray(snapshot.staticPolylines).forEach(line => include(line.points)); safeArray(snapshot.directionSegments).forEach(line => include(line.points)); safeArray(snapshot.staticMarkers).forEach(marker => include([marker.position])); @@ -52,21 +52,89 @@ const bounds = getBounds(); drawOccupancy(bounds); overlay.replaceChildren(); overlay.setAttribute("viewBox", `${bounds.xMin} ${-bounds.yMax} ${bounds.xMax - bounds.xMin} ${bounds.yMax - bounds.yMin}`); overlay.setAttribute("preserveAspectRatio", "xMidYMid meet"); const snapshot = state.staticSnapshot || {}; const frame = state.frame || {}; const addLine = (line, css) => { const path = worldPath(line.points); if (path) overlay.append(svg("path", { d: path, class: "world-line " + css })); }; - safeArray(snapshot.staticPolylines).forEach(line => addLine(line, "world-static")); - safeArray(snapshot.directionSegments).forEach(segment => addLine(segment, segment.segmentIndex === frame.activeSegmentIndex ? "world-active" : segment.segmentIndex < frame.activeSegmentIndex ? "world-static" : "world-future")); - safeArray(frame.dynamicPolylines).forEach(line => addLine(line, line.lineStyle === 1 ? "world-previous" : "world-active")); - safeArray(snapshot.staticMarkers).concat(safeArray(frame.dynamicMarkers)).forEach(marker => { if (!marker.position || !finite(marker.position.x) || !finite(marker.position.y)) return; const diamond = svg("path", { d: `M ${marker.position.x} ${-marker.position.y - .18} L ${marker.position.x + .18} ${-marker.position.y} L ${marker.position.x} ${-marker.position.y + .18} L ${marker.position.x - .18} ${-marker.position.y} Z`, class: "world-marker" }); overlay.append(diamond); const label = svg("text", { x: marker.position.x + .2, y: -marker.position.y - .2, fill: "#20252b", "font-size": ".32" }); label.textContent = marker.labelChinese || ""; overlay.append(label); }); - if (frame.vehiclePose && finite(frame.vehiclePose.x) && finite(frame.vehiclePose.y)) overlay.append(svg("circle", { cx: frame.vehiclePose.x, cy: -frame.vehiclePose.y, r: ".18", fill: "#20252b" })); + const staticClass = line => { const kind = String(line.kind || "").toLowerCase(); if (kind === "global" || kind === "coarse") return "world-coarse"; if (kind.includes("local-g2") || kind.includes("g2")) return "world-local-g2"; return "world-static"; }; + const dynamicClass = line => { const kind = String(line.kind || "").toLowerCase(); if (kind === "current") return "world-current"; if (kind === "previous" || line.lineStyle === 1) return "world-previous"; return "world-current"; }; + safeArray(snapshot.staticPolylines).forEach(line => addLine(line, staticClass(line))); + safeArray(snapshot.directionSegments).forEach(segment => addLine(segment, segment.segmentIndex === frame.activeSegmentIndex ? "world-segment-active" : "world-direction-inactive")); + safeArray(frame.dynamicPolylines).forEach(line => addLine(line, dynamicClass(line))); + const hasPath = safeArray(snapshot.staticPolylines).concat(safeArray(snapshot.directionSegments), safeArray(frame.dynamicPolylines)) + .some(line => safeArray(line && line.points).some(point => point && finite(point.x) && finite(point.y))); + const appendMarker = marker => { + if (!marker.position || !finite(marker.position.x) || !finite(marker.position.y)) return; + const x = marker.position.x, y = -marker.position.y; const kind = String(marker.kind || "").toLowerCase(); + if (kind === "vehicle") { + const pose = frame.vehiclePose; const heading = pose && finite(pose.headingRadians) ? pose.headingRadians : 0; + const cos = Math.cos(heading), sin = Math.sin(heading); + const corners = [[.38, .18], [-.38, .18], [-.38, -.18], [.38, -.18]].map(([dx, dy]) => { + const wx = marker.position.x + dx * cos - dy * sin; const wy = marker.position.y + dx * sin + dy * cos; return `${wx} ${-wy}`; + }); + const group = svg("g", { class: "marker-vehicle" }); + group.append(svg("path", { d: `M ${corners[0]} L ${corners[1]} L ${corners[2]} L ${corners[3]} Z`, class: "vehicle-outline" })); + group.append(svg("line", { x1: x, y1: y, x2: x + .22 * cos, y2: y - .22 * sin, class: "vehicle-heading" })); + overlay.append(group); + } else if (kind === "plan-start") { + overlay.append(svg("circle", { cx: x, cy: y, r: ".12", class: "marker-plan-start" })); + } else if (kind === "gear-switch" || kind === "gear-switch-end") { + const size = ".14"; overlay.append(svg("path", { d: `M ${x} ${y - size} L ${x + size} ${y} L ${x} ${y + size} L ${x - size} ${y} Z`, class: "marker-gear-switch" })); + } else if (kind === "final-goal") { + const size = ".13"; overlay.append(svg("path", { d: `M ${x - size} ${y - size} L ${x + size} ${y - size} L ${x + size} ${y + size} L ${x - size} ${y + size} Z`, class: "marker-final-goal" })); + } else { + const size = ".18"; overlay.append(svg("path", { d: `M ${x} ${y - size} L ${x + size} ${y} L ${x} ${y + size} L ${x - size} ${y} Z`, class: "world-marker" })); + } + const label = svg("text", { x: x + .2, y: y - .2, fill: "#20252b", "font-size": ".32" }); label.textContent = marker.labelChinese || ""; overlay.append(label); + }; + safeArray(snapshot.staticMarkers).concat(safeArray(frame.dynamicMarkers)).forEach(appendMarker); + if (!hasPath) { + const empty = svg("text", { x: (bounds.xMin + bounds.xMax) / 2, y: -((bounds.yMin + bounds.yMax) / 2), class: "world-empty-state", "text-anchor": "middle" }); + empty.textContent = "未收到 Local G2 路径"; overlay.append(empty); + } } - function range(points) { const flat = safeArray(points).filter(point => finite(point.x) && finite(point.y)); if (!flat.length) return { x0: 0, x1: 1, y0: 0, y1: 1 }; let x0 = Math.min(...flat.map(point => point.x)), x1 = Math.max(...flat.map(point => point.x)), y0 = Math.min(...flat.map(point => point.y)), y1 = Math.max(...flat.map(point => point.y)); const xPad = Math.max(x1 - x0, 1) * .05, yPad = Math.max(y1 - y0, 1) * .08; return { x0: x0 - xPad, x1: x1 + xPad, y0: y0 - yPad, y1: y1 + yPad }; } + function niceDomain(values, padding = .05, unitFloor = 1e-6) { + const flat = safeArray(values).filter(finite); + if (!flat.length) return { min: 0, max: 1 }; + let min = Math.min(...flat), max = Math.max(...flat); + const span = max - min; + if (span < unitFloor) { + const pad = Math.max(Math.abs(min) * padding, unitFloor); min -= pad; max += pad; + } else { + const pad = span * padding; min -= pad; max += pad; + } + const tickSpan = max - min; + if (tickSpan > 0) { + const roughStep = tickSpan / 4; + const magnitude = Math.pow(10, Math.floor(Math.log10(roughStep))); + const normalized = roughStep / magnitude; + const step = normalized >= 5 ? 5 : normalized >= 2 ? 2 : 1; + const niceStep = step * magnitude; min = Math.floor(min / niceStep) * niceStep; max = Math.ceil(max / niceStep) * niceStep; + } + if (!(max > min)) max = min + 1; + return { min, max }; + } + function formatTick(value) { + if (!finite(value)) return ""; + if (value === 0) return "0"; + const abs = Math.abs(value); const precision = abs >= 100 ? 1 : abs >= 1 ? 2 : 4; + return Number(value.toFixed(precision)).toString(); + } function renderChart(id, chart) { const host = document.getElementById(id); if (!host) return; host.replaceChildren(); const title = element("div", "chart-title", chart ? chart.chineseTitle : id); const noteText = id === "jerk-t" ? "末点后无时间区间" : chart ? chart.noteChinese : "等待当前周期数据"; const note = element("div", "chart-note", noteText); host.append(title, note); - const all = chart ? safeArray(chart.series).flatMap(series => safeArray(series.points)) : []; const domain = range(all); const width = 640, height = 244, left = 52, right = 16, top = 8, bottom = 36; const sx = value => left + (value - domain.x0) * (width - left - right) / (domain.x1 - domain.x0); const sy = value => height - bottom - (value - domain.y0) * (height - top - bottom) / (domain.y1 - domain.y0); const graph = svg("svg", { viewBox: `0 0 ${width} ${height}`, role: "img", "aria-label": chart ? chart.chineseTitle : id }); - for (let tick = 0; tick <= 4; tick += 1) { const x = left + tick * (width - left - right) / 4, y = top + tick * (height - top - bottom) / 4; graph.append(svg("line", { x1: x, y1: top, x2: x, y2: height - bottom, class: "chart-grid" }), svg("line", { x1: left, y1: y, x2: width - right, y2: y, class: "chart-grid" })); const xt = svg("text", { x, y: height - bottom + 14, class: "axis-tick", "text-anchor": "middle" }); xt.textContent = (domain.x0 + tick * (domain.x1 - domain.x0) / 4).toPrecision(3); graph.append(xt); } + const all = chart ? safeArray(chart.series).flatMap(series => safeArray(series.points)) : []; + const xDomain = niceDomain(all.map(point => point.x)), yDomain = niceDomain(all.map(point => point.y)); + const domain = { x0: xDomain.min, x1: xDomain.max, y0: yDomain.min, y1: yDomain.max }; + const width = 640, height = 244, left = 58, right = 16, top = 8, bottom = 36; + const plotWidth = width - left - right, plotHeight = height - top - bottom; + const sx = value => left + (value - domain.x0) * plotWidth / (domain.x1 - domain.x0); const sy = value => height - bottom - (value - domain.y0) * plotHeight / (domain.y1 - domain.y0); const graph = svg("svg", { viewBox: `0 0 ${width} ${height}`, role: "img", "aria-label": chart ? chart.chineseTitle : id }); + for (let tick = 0; tick <= 4; tick += 1) { + const x = left + tick * plotWidth / 4, y = top + tick * plotHeight / 4; + graph.append(svg("line", { x1: x, y1: top, x2: x, y2: height - bottom, class: "chart-grid" }), svg("line", { x1: left, y1: y, x2: width - right, y2: y, class: "chart-grid" })); + const xt = svg("text", { x, y: height - bottom + 14, class: "axis-tick axis-tick-x", "text-anchor": "middle" }); xt.textContent = formatTick(domain.x0 + tick * (domain.x1 - domain.x0) / 4); graph.append(xt); + const yt = svg("text", { x: left - 7, y, class: "axis-tick axis-tick-y", "text-anchor": "end", "dominant-baseline": "middle" }); yt.textContent = formatTick(domain.y1 - tick * (domain.y1 - domain.y0) / 4); graph.append(yt); + } graph.append(svg("line", { x1: left, y1: top, x2: left, y2: height - bottom, class: "chart-axis" }), svg("line", { x1: left, y1: height - bottom, x2: width - right, y2: height - bottom, class: "chart-axis" })); - safeArray(chart && chart.series).forEach(series => { const points = safeArray(series.points).filter(point => finite(point.x) && finite(point.y)); if (!points.length) return; const d = points.map((point, index) => (index ? "L" : "M") + sx(point.x) + " " + sy(point.y)).join(" "); const lineClass = series.lineStyle === 1 ? "chart-dashed" : series.lineStyle === 2 ? "chart-limit" : "chart-solid"; graph.append(svg("path", { d, class: "chart-data " + lineClass })); }); - const xLabel = svg("text", { x: (left + width - right) / 2, y: height - 4, class: "axis-label", "text-anchor": "middle" }); xLabel.textContent = chart ? chart.xAxisLabel : "x"; const yLabel = svg("text", { x: 13, y: (top + height - bottom) / 2, class: "axis-label", transform: `rotate(-90 13 ${(top + height - bottom) / 2})`, "text-anchor": "middle" }); yLabel.textContent = chart ? chart.yAxisLabel : "y"; graph.append(xLabel, yLabel); host.append(graph); + safeArray(chart && chart.series).forEach(series => { const points = safeArray(series.points).filter(point => finite(point.x) && finite(point.y)); if (!points.length) return; const d = points.map((point, index) => (index ? "L" : "M") + sx(point.x) + " " + sy(point.y)).join(" "); const kind = String(series.kind || "").toLowerCase(); const lineClass = kind === "limit" || series.lineStyle === 2 ? "chart-limit" : kind === "previous" || series.lineStyle === 1 ? "chart-dashed" : "chart-solid"; graph.append(svg("path", { d, class: "chart-data " + lineClass })); }); + safeArray(chart && chart.annotations).forEach(annotation => { if (!finite(annotation.x)) return; const ax = sx(annotation.x); graph.append(svg("line", { x1: ax, y1: top, x2: ax, y2: height - bottom, class: "chart-annotation-line" })); if (finite(annotation.y)) graph.append(svg("circle", { cx: ax, cy: sy(annotation.y), r: 3, class: "chart-annotation-point" })); const label = svg("text", { x: Math.min(ax + 4, width - right - 2), y: top + 10, class: "chart-annotation-label" }); label.textContent = annotation.labelChinese || ""; graph.append(label); }); + const xLabel = svg("text", { x: (left + width - right) / 2, y: height - 4, class: "axis-label axis-label-x", "text-anchor": "middle" }); xLabel.textContent = chart ? chart.xAxisLabel : "x"; const yLabel = svg("text", { x: 13, y: (top + height - bottom) / 2, class: "axis-label axis-label-y", transform: `rotate(-90 13 ${(top + height - bottom) / 2})`, "text-anchor": "middle" }); yLabel.textContent = chart ? chart.yAxisLabel : "y"; graph.append(xLabel, yLabel); host.append(graph); } function renderOverview() { renderWorld(); const summary = document.getElementById("overview-summary"); summary.replaceChildren(); const frame = state.frame || {}; safeArray(frame.statusValues).concat([{ chineseName: "活动方向段", rawName: "SegmentIndex", value: String(frame.activeSegmentIndex ?? "不可用"), unit: "", severity: "normal" }, { chineseName: "方向", rawName: "Direction", value: frame.activeDirection || "不可用", unit: "", severity: "normal" }]).forEach(value => { const cell = element("div", "summary-value " + (value.severity || "")); const name = element("small", "", `${value.chineseName || "不可用"} · ${value.rawName || ""}`); const data = element("div", "", `${value.value || "不可用"} ${value.unit || ""}`.trim()); cell.append(name, data); summary.append(cell); }); } @@ -79,5 +147,17 @@ function receiveFrame(payload) { state.frame = payload.snapshot || null; state.history = safeArray(payload.history); state.lastFrameAt = Date.now(); if (!state.ended) setLiveState(state.frame && state.frame.sessionStateChinese || "运行中"); renderAll(); } function startEvents() { const events = new EventSource(authorized("/api/events")); events.addEventListener("frame", event => { try { receiveFrame(JSON.parse(event.data)); } catch (error) { state.redrawEnabled[state.activeTab] = false; setLiveState("页面绘图异常", "failure"); } }); events.addEventListener("end", () => { state.ended = true; events.close(); setLiveState("会话已结束"); }); events.onerror = () => { if (!state.ended) setLiveState("连接中断,正在重连", "notice"); }; } async function boot() { installTabs(); ["overview", "ls-st", "kinematics", "history-config"].forEach(tab => { state.redrawEnabled[tab] = true; }); try { const response = await fetch(authorized("/api/bootstrap"), { cache: "no-store" }); if (!response.ok) throw new Error("bootstrap failed"); const bootstrap = await response.json(); state.staticSnapshot = bootstrap.staticSnapshot || bootstrap; state.refreshRateHz = finite(bootstrap.refreshRateHz) && bootstrap.refreshRateHz > 0 ? bootstrap.refreshRateHz : 10; renderAll(); startEvents(); window.setInterval(updateConnectionState, 250); } catch (error) { setLiveState("页面绘图异常", "failure"); } } - window.addEventListener("error", () => { state.redrawEnabled[state.activeTab] = false; setLiveState("页面绘图异常", "failure"); }); window.addEventListener("unhandledrejection", () => { state.redrawEnabled[state.activeTab] = false; setLiveState("页面绘图异常", "failure"); }); boot(); + window.addEventListener("error", () => { state.redrawEnabled[state.activeTab] = false; setLiveState("页面绘图异常", "failure"); }); window.addEventListener("unhandledrejection", () => { state.redrawEnabled[state.activeTab] = false; setLiveState("页面绘图异常", "failure"); }); + if (window.__TRAJECTORY_VISUALIZATION_TEST__ === true) { + ["overview", "ls-st", "kinematics", "history-config"].forEach(tab => { state.redrawEnabled[tab] = true; }); + window.__trajectoryVisualizationTestHooks = Object.freeze({ + setBootstrap(snapshot) { state.staticSnapshot = snapshot || null; }, + receiveFrame, + renderActiveTab, + installTabs + }); + installTabs(); + } else { + boot(); + } })(); diff --git a/ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/WebAssetChecks.cs b/ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/WebAssetChecks.cs index 63940df..4a9bde5 100644 --- a/ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/WebAssetChecks.cs +++ b/ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/WebAssetChecks.cs @@ -20,7 +20,23 @@ internal static class WebAssetChecks Verification.True(html.Contains("生效配置"), "configuration panel"); Verification.True(css.Contains("--current-trajectory: #1769aa"), "scientific current color"); Verification.True(css.Contains("stroke-width: 1.1"), "thin scientific line"); + Verification.True(css.Contains("main > section[hidden] { display: none !important; }"), + "hidden sections cannot be overridden by grid rules"); + Verification.True(css.Contains(".axis-tick-y"), "SVG y ticks have a semantic class"); + Verification.True(css.Contains(".axis-label-y"), "SVG y labels have a semantic class"); + Verification.True(css.Contains(".world-empty-state"), "empty overview has a semantic state"); + Verification.True(css.Contains(".world-coarse"), "coarse path semantic class"); + Verification.True(css.Contains(".world-local-g2"), "Local G2 semantic class"); + Verification.True(css.Contains(".world-segment-active"), "active direction semantic class"); + Verification.True(css.Contains(".world-current"), "current trajectory semantic class"); + Verification.True(css.Contains(".marker-vehicle"), "vehicle marker semantic class"); + Verification.True(css.Contains(".marker-plan-start"), "plan start marker semantic class"); + Verification.True(css.Contains(".marker-gear-switch"), "gear-switch marker semantic class"); + Verification.True(css.Contains(".marker-final-goal"), "final goal marker semantic class"); Verification.True(js.Contains("末点后无时间区间"), "jerk terminal explanation"); + Verification.True(js.Contains("__TRAJECTORY_VISUALIZATION_TEST__"), "test hook flag is explicit"); + Verification.True(js.Contains("Object.freeze"), "test hooks are frozen"); + Verification.True(js.Contains("未收到 Local G2 路径"), "empty overview names the missing source"); Verification.True(html.Contains("occupancy-grid"), "single occupancy canvas exists"); Verification.True(html.Contains("world-overlay"), "SVG trajectory overlay exists"); Verification.True(js.Contains("atob"), "compact occupancy bitset is decoded in browser"); diff --git a/ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom/dashboard.dom.test.mjs b/ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom/dashboard.dom.test.mjs new file mode 100644 index 0000000..c28cb10 --- /dev/null +++ b/ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom/dashboard.dom.test.mjs @@ -0,0 +1,333 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { test } from "node:test"; +import { JSDOM } from "jsdom"; + +const webDir = new URL("../../TrajectoryPlanningVisualization/Web/", import.meta.url); +const readWebFile = (name) => readFileSync(new URL(name, webDir), "utf8"); + +function makeStaticSnapshot({ empty = false } = {}) { + return { + sessionNameChinese: "合成轨迹规划观察", + worldBounds: { xMin: 0, xMax: 12, yMin: 0, yMax: 8 }, + occupancyGrid: null, + staticPolylines: empty + ? [] + : [ + { + id: "global", + legendChinese: "全局参考路径", + kind: "global", + lineStyle: 0, + points: [ + { x: 0, y: 1 }, + { x: 3, y: 1 }, + { x: 6, y: 2 }, + { x: 8, y: 4 }, + { x: 11, y: 6 } + ] + }, + { + id: "local-g2", + legendChinese: "完整 Local G2 路径", + kind: "local-g2", + lineStyle: 0, + points: [ + { x: 0.2, y: 1 }, + { x: 3.2, y: 1.1 }, + { x: 6.1, y: 2.1 }, + { x: 8.1, y: 4.1 }, + { x: 10.9, y: 5.9 } + ] + } + ], + staticMarkers: empty + ? [] + : [ + { id: "gear-1", kind: "gear-switch", labelChinese: "换向 1", position: { x: 6, y: 2 } }, + { id: "gear-1-end", kind: "gear-switch-end", labelChinese: "换向点 1 / s_end", position: { x: 6, y: 2 } }, + { id: "final-goal", kind: "final-goal", labelChinese: "终点 / s_end", position: { x: 11, y: 6 } } + ], + directionSegments: empty + ? [] + : [ + { + segmentIndex: 0, + direction: "forward", + startsAtGearSwitch: false, + endsAtGearSwitch: true, + points: [ + { x: 0, y: 1 }, + { x: 3, y: 1 }, + { x: 6, y: 2 } + ] + }, + { + segmentIndex: 1, + direction: "reverse", + startsAtGearSwitch: true, + endsAtGearSwitch: false, + points: [ + { x: 6, y: 2 }, + { x: 8, y: 4 }, + { x: 11, y: 6 } + ] + } + ], + configurationGroups: [] + }; +} + +function chart(id, title, xAxis, yAxis, points, annotations = []) { + return { + id, + chineseTitle: title, + xAxisLabel: xAxis, + yAxisLabel: yAxis, + noteChinese: "", + series: [ + { + id: id + "-current", + legend: "当前轨迹", + lineStyle: 0, + points: points.map(([x, y]) => ({ x, y })) + } + ], + annotations + }; +} + +function makeDynamicSnapshot({ empty = false } = {}) { + if (empty) { + return { + sequence: 1, + observedAtUtc: "2026-08-06T00:00:00Z", + sessionStateChinese: "运行中", + activeSegmentIndex: 0, + activeDirection: "forward", + vehiclePose: null, + dynamicPolylines: [], + dynamicMarkers: [], + charts: [], + statusValues: [], + cycleSummary: { + cycleVersion: 1, + occurredAtUtc: "2026-08-06T00:00:00Z", + status: "成功", + published: true, + planningElapsedMilliseconds: 4.2, + segmentIndex: 0, + direction: "forward", + longitudinalMode: "full", + terminalType: "goal", + terminalVelocity: 0, + terminalAcceleration: 0, + failureReason: "" + } + }; + } + + return { + sequence: 1, + observedAtUtc: "2026-08-06T00:00:00Z", + sessionStateChinese: "合成数据运行中", + activeSegmentIndex: 0, + activeDirection: "forward", + vehiclePose: { x: 2, y: 1, headingRadians: 0 }, + dynamicPolylines: [ + { + id: "previous", + legendChinese: "上一轮轨迹", + kind: "previous", + lineStyle: 1, + points: [ + { x: 1, y: 1 }, + { x: 2.5, y: 1 }, + { x: 4, y: 1.3 } + ] + }, + { + id: "current", + legendChinese: "当前轨迹", + kind: "current", + lineStyle: 0, + points: [ + { x: 2, y: 1 }, + { x: 3.8, y: 1.2 }, + { x: 5.8, y: 1.9 } + ] + } + ], + dynamicMarkers: [ + { id: "vehicle", kind: "vehicle", labelChinese: "车辆", position: { x: 2, y: 1 } }, + { id: "plan-start", kind: "plan-start", labelChinese: "计划起点", position: { x: 2, y: 1 } }, + { id: "gear-switch-end", kind: "gear-switch-end", labelChinese: "换向点 1 / s_end", position: { x: 6, y: 2 } } + ], + charts: [ + chart("ls", "横向偏移", "ReferenceS (m)", "l (m)", [[0, 0.05], [2, 0.08], [4, 0.02]], [ + { id: "ls-s-end", kind: "s-end", labelChinese: "s_end", x: 4 } + ]), + chart("st", "时空轨迹", "t (s)", "PathS (m)", [[0, 0], [1, 1.2], [2, 3]], [ + { id: "st-s-end", kind: "s-end", labelChinese: "s_end", x: 2 } + ]) + ], + statusValues: [], + cycleSummary: { + cycleVersion: 1, + occurredAtUtc: "2026-08-06T00:00:00Z", + status: "成功", + published: true, + planningElapsedMilliseconds: 4.2, + segmentIndex: 0, + direction: "forward", + longitudinalMode: "full", + terminalType: "goal", + terminalVelocity: 0, + terminalAcceleration: 0, + failureReason: "" + } + }; +} + +function fakeContext() { + return { + setTransform() {}, + clearRect() {}, + fillRect() {} + }; +} + +function loadApp({ staticSnapshot = makeStaticSnapshot(), dynamicSnapshot = makeDynamicSnapshot() } = {}) { + const dom = new JSDOM(readWebFile("index.html"), { + runScripts: "outside-only", + pretendToBeVisual: true, + url: "http://127.0.0.1/observation?token=test" + }); + const { window } = dom; + window.devicePixelRatio = 1; + window.__TRAJECTORY_VISUALIZATION_TEST__ = true; + window.__fetchCalled = false; + window.__eventSourceCreated = false; + window.fetch = async () => { + window.__fetchCalled = true; + throw new Error("test mode must not fetch"); + }; + window.EventSource = class { + constructor() { + window.__eventSourceCreated = true; + } + + close() {} + }; + const style = window.document.createElement("style"); + style.textContent = readWebFile("app.css"); + window.document.head.append(style); + window.HTMLCanvasElement.prototype.getContext = () => fakeContext(); + window.eval(readWebFile("app.js")); + + if (window.__trajectoryVisualizationTestHooks) { + const hooks = window.__trajectoryVisualizationTestHooks; + hooks.setBootstrap(staticSnapshot); + hooks.receiveFrame({ snapshot: dynamicSnapshot, history: [] }); + } + + return dom; +} + +test("tab switching hides inactive sections despite CSS grid rules", () => { + const dom = loadApp(); + const { document, getComputedStyle } = dom.window; + const overview = document.querySelector("#overview"); + const lsSt = document.querySelector("#ls-st"); + const kinematics = document.querySelector("#kinematics"); + + assert.equal(overview.hidden, false); + assert.notEqual(getComputedStyle(overview).display, "none"); + + document.querySelector('button[data-tab="ls-st"]').click(); + + assert.equal(overview.hidden, true); + assert.equal(getComputedStyle(overview).display, "none"); + assert.equal(lsSt.hidden, false); + assert.notEqual(getComputedStyle(lsSt).display, "none"); + assert.equal(kinematics.hidden, true); + assert.equal(getComputedStyle(kinematics).display, "none"); +}); + +test("ST chart renders y ticks, units, and s_end annotation", () => { + const dom = loadApp(); + const { document } = dom.window; + document.querySelector('button[data-tab="ls-st"]').click(); + + const yTicks = document.querySelectorAll("#st .axis-tick-y"); + const xTicks = document.querySelectorAll("#st .axis-tick-x"); + const yLabel = document.querySelector("#st .axis-label-y"); + const xLabel = document.querySelector("#st .axis-label-x"); + + assert.ok(yTicks.length >= 4, "ST renders y tick text"); + assert.ok(xTicks.length >= 4, "ST renders x tick text"); + assert.ok([...yTicks].every(tick => tick.textContent.length > 0), "y ticks have readable values"); + assert.equal(yLabel.textContent, "PathS (m)"); + assert.equal(xLabel.textContent, "t (s)"); + assert.ok(document.querySelector("#st .chart-annotation-label")?.textContent.includes("s_end")); +}); + +test("ST PathS polyline rises and semantic world layers are ordered", () => { + const dom = loadApp(); + const { document } = dom.window; + document.querySelector('button[data-tab="ls-st"]').click(); + const stPath = document.querySelector("#st path.chart-data"); + assert.ok(stPath, "ST chart has a polyline"); + const segments = stPath.getAttribute("d").split(/(?=[ML])/).filter(Boolean); + const yValues = segments.map(segment => Number(segment.slice(1).trim().split(/[\s,]+/)[1])); + assert.ok(yValues.length >= 3, "ST polyline has at least three rendered points"); + assert.ok(yValues[1] < yValues[0] && yValues[2] < yValues[1], + "ST PathS increases upward in rendered pixel coordinates"); + + document.querySelector('button[data-tab="overview"]').click(); + const overlay = document.querySelector("#world-overlay"); + const paths = [...overlay.querySelectorAll("path")]; + const layerClasses = ["world-coarse", "world-local-g2", "world-segment-active", "world-previous", "world-current"]; + const indices = layerClasses.map(className => paths.findIndex(path => path.classList.contains(className))); + layerClasses.forEach((className, index) => assert.ok(indices[index] !== -1, className + " exists")); + for (let index = 1; index < indices.length; index += 1) { + assert.ok(indices[index - 1] < indices[index], "semantic world layer order is preserved"); + } +}); + +test("vehicle, plan start, gear switch, and final goal use semantic markers", () => { + const dom = loadApp(); + const { document } = dom.window; + const overlay = document.querySelector("#world-overlay"); + + for (const className of ["marker-vehicle", "marker-plan-start", "marker-gear-switch", "marker-final-goal"]) { + assert.ok(overlay.querySelector("." + className), className + " is rendered"); + } + assert.equal(overlay.querySelectorAll(".marker-vehicle circle").length, 0, + "vehicle marker is not a fixed pixel circle"); +}); + +test("empty overview names the missing Local G2 source", () => { + const dom = loadApp({ + staticSnapshot: makeStaticSnapshot({ empty: true }), + dynamicSnapshot: makeDynamicSnapshot({ empty: true }) + }); + const emptyState = dom.window.document.querySelector("#world-overlay .world-empty-state"); + + assert.ok(emptyState, "empty overview renders a visible state"); + assert.match(emptyState.textContent, /未收到.*Local G2 路径/); +}); + +test("test mode exposes frozen hooks without fetch or SSE", () => { + const dom = loadApp(); + const { window } = dom; + + assert.equal(window.__fetchCalled, false); + assert.equal(window.__eventSourceCreated, false); + assert.ok(window.__trajectoryVisualizationTestHooks, "test hooks are exposed"); + assert.equal(Object.isFrozen(window.__trajectoryVisualizationTestHooks), true); + for (const name of ["setBootstrap", "receiveFrame", "renderActiveTab", "installTabs"]) { + assert.equal(typeof window.__trajectoryVisualizationTestHooks[name], "function", name + " hook exists"); + } + assert.equal("state" in window.__trajectoryVisualizationTestHooks, false); +}); diff --git a/ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom/package-lock.json b/ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom/package-lock.json new file mode 100644 index 0000000..852499e --- /dev/null +++ b/ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom/package-lock.json @@ -0,0 +1,558 @@ +{ + "name": "trajectory-planning-visualization-web-dom-tests", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "trajectory-planning-visualization-web-dom-tests", + "devDependencies": { + "jsdom": "29.1.1" + }, + "engines": { + "node": ">=24" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmmirror.com/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmmirror.com/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmmirror.com/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.1.0", + "resolved": "https://registry.npmmirror.com/@csstools/color-helpers/-/color-helpers-6.1.0.tgz", + "integrity": "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/@csstools/css-calc/-/css-calc-3.3.0.tgz", + "integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.1.10", + "resolved": "https://registry.npmmirror.com/@csstools/css-color-parser/-/css-color-parser-4.1.10.tgz", + "integrity": "sha512-UZhQLIUyJaaMepqehrCODwCg2KW25vFvLWBmqYFaPclYvvxzj/sG8LBOhBFCp11i9uE7t1EyS+RAoV9tztPFyw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.1.0", + "@csstools/css-calc": "^3.3.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.7", + "resolved": "https://registry.npmmirror.com/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.7.tgz", + "integrity": "sha512-fQ+05118eQS1cofO3aJpB5efgpBZMvIzwr/sbC8kDLVA5XLG8q1kJV5yzrUAI1f7lvhPnm8fgIjzFB8/O/5Dig==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmmirror.com/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmmirror.com/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmmirror.com/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "29.1.1", + "resolved": "https://registry.npmmirror.com/jsdom/-/jsdom-29.1.1.tgz", + "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmmirror.com/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmmirror.com/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmmirror.com/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tldts": { + "version": "7.4.10", + "resolved": "https://registry.npmmirror.com/tldts/-/tldts-7.4.10.tgz", + "integrity": "sha512-GgouD1B+sWwvkaEq8vXC15DjQitxbvs12oIXELpconwm+Tg3zfcEv4jgzq3vtKverDXsg3VI8aRgNL2Nra0Iog==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.10" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.10", + "resolved": "https://registry.npmmirror.com/tldts-core/-/tldts-core-7.4.10.tgz", + "integrity": "sha512-KnQjp53ZekKgm/r3l+u8kJGGzYgrWdP8+Mql7a4vijh2WE0IrZWspQj/TpTxDho/YxO+AnOZnIjQcCD+q6iJsw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/tough-cookie/-/tough-cookie-6.0.2.tgz", + "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/undici": { + "version": "7.29.0", + "resolved": "https://registry.npmmirror.com/undici/-/undici-7.29.0.tgz", + "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom/package.json b/ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom/package.json new file mode 100644 index 0000000..3f21a39 --- /dev/null +++ b/ClumsyPilot/tests/TrajectoryPlanningVisualizationWebDom/package.json @@ -0,0 +1,14 @@ +{ + "name": "trajectory-planning-visualization-web-dom-tests", + "private": true, + "type": "module", + "scripts": { + "test": "node --test dashboard.dom.test.mjs" + }, + "devDependencies": { + "jsdom": "29.1.1" + }, + "engines": { + "node": ">=24" + } +}