From 444cc3af2a632a7de2e0b99ddbdc8dbe32b36a6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A2=81=E8=96=84=E4=BA=91?= Date: Sun, 9 Aug 2026 19:43:47 +0800 Subject: [PATCH] fix: resolve EM overview marker and segment overlaps --- .../Web/app.css | 2 +- .../Web/app.js | 38 +++++++++++-------- .../SampleSnapshotFactory.cs | 4 +- .../WebAssetChecks.cs | 34 +++++++++++++++++ 4 files changed, 60 insertions(+), 18 deletions(-) diff --git a/ClumsyPilot/TrajectoryPlanningVisualization/Web/app.css b/ClumsyPilot/TrajectoryPlanningVisualization/Web/app.css index 5eb7bcb..ac7bfa6 100644 --- a/ClumsyPilot/TrajectoryPlanningVisualization/Web/app.css +++ b/ClumsyPilot/TrajectoryPlanningVisualization/Web/app.css @@ -62,7 +62,7 @@ main > section[hidden] { display: none !important; } .marker-vehicle .vehicle-outline { fill: rgba(32, 37, 43, .16); stroke: #20252b; stroke-width: 1.5; } .marker-vehicle .vehicle-heading { stroke: #20252b; stroke-width: 1.5; stroke-linecap: round; vector-effect: non-scaling-stroke; } .marker-smooth-start { fill: #2f8f56; stroke: #ffffff; stroke-width: 1.5; } -.marker-plan-start { fill: #ffffff; stroke: #2f8f56; stroke-width: 2; } +.marker-plan-start { fill: none; stroke: #2f8f56; stroke-width: 2; } .marker-gear-switch { fill: #d87918; stroke: #ffffff; stroke-width: 1.5; } .marker-final-goal { fill: #1769aa; stroke: #ffffff; stroke-width: 1.5; } .world-legend { position: absolute; top: 14px; right: 14px; z-index: 3; display: grid; gap: 6px; min-width: 154px; max-width: min(260px, calc(100% - 28px)); padding: 10px 12px; border: 1px solid rgba(112, 123, 133, .38); border-radius: 6px; background: rgba(255, 255, 255, .88); box-shadow: 0 2px 10px rgba(32, 37, 43, .08); color: #36404a; font-size: 12px; pointer-events: none; backdrop-filter: blur(2px); } diff --git a/ClumsyPilot/TrajectoryPlanningVisualization/Web/app.js b/ClumsyPilot/TrajectoryPlanningVisualization/Web/app.js index 721832b..7d81ade 100644 --- a/ClumsyPilot/TrajectoryPlanningVisualization/Web/app.js +++ b/ClumsyPilot/TrajectoryPlanningVisualization/Web/app.js @@ -146,7 +146,6 @@ if (!worldLegend) return; worldLegend.replaceChildren(); const staticLines = safeArray(snapshot.staticPolylines); - const directionSegments = safeArray(snapshot.directionSegments); const dynamicLines = safeArray(frame.dynamicPolylines); const markers = safeArray(snapshot.staticMarkers).concat(safeArray(frame.dynamicMarkers)); const entries = []; @@ -163,20 +162,25 @@ const current = dynamicLines.find(line => lineKind(line) !== "active-segment" && lineKind(line) !== "previous" && line.lineStyle !== 1); if (coarse) add("coarse", coarse.legendChinese || "粗路径", "legend-line legend-coarse"); if (smooth) add("smooth", smooth.legendChinese || "完整 Local G2 路径", "legend-line legend-smooth"); - if (active || directionSegments.length) - add("segment", active && active.legendChinese || "当前方向段", "legend-line legend-segment"); + if (active) + add("segment", active.legendChinese || "当前方向段", "legend-line legend-segment"); if (previous) add("previous", previous.legendChinese || "上一条 EM 轨迹", "legend-line legend-previous"); if (current) add("current", current.legendChinese || "当前 EM 轨迹", "legend-line legend-current"); - if (markers.some(marker => lineKind(marker) === "smooth-start")) - add("smooth-start", "平滑路径起点", "legend-marker legend-smooth-start"); - if (markers.some(marker => lineKind(marker) === "plan-start")) - add("plan-start", "EM 规划起点", "legend-marker legend-plan-start"); - if (markers.some(marker => lineKind(marker) === "vehicle")) - add("vehicle", "当前车辆", "legend-marker legend-vehicle"); - if (markers.some(marker => lineKind(marker) === "gear-switch" || lineKind(marker) === "gear-switch-end")) - add("gear", "换向点 / s_end", "legend-marker legend-gear"); - if (markers.some(marker => lineKind(marker) === "final-goal")) - add("goal", "平滑路径终点", "legend-marker legend-goal"); + const smoothStartMarker = markers.find(marker => lineKind(marker) === "smooth-start"); + const planStartMarker = markers.find(marker => lineKind(marker) === "plan-start"); + const vehicleMarker = markers.find(marker => lineKind(marker) === "vehicle"); + const gearMarker = markers.find(marker => lineKind(marker) === "gear-switch" || lineKind(marker) === "gear-switch-end"); + const goalMarker = markers.find(marker => lineKind(marker) === "final-goal"); + if (smoothStartMarker) + add("smooth-start", smoothStartMarker.labelChinese || "平滑路径起点", "legend-marker legend-smooth-start"); + if (planStartMarker) + add("plan-start", planStartMarker.labelChinese || "EM 规划起点", "legend-marker legend-plan-start"); + if (vehicleMarker) + add("vehicle", vehicleMarker.labelChinese || "当前车辆", "legend-marker legend-vehicle"); + if (gearMarker) + add("gear", gearMarker.labelChinese || "换向点 / s_end", "legend-marker legend-gear"); + if (goalMarker) + add("goal", goalMarker.labelChinese || "平滑路径终点", "legend-marker legend-goal"); entries.forEach(entry => { const item = element("div", "world-legend-item"); item.append(element("span", "world-legend-swatch " + entry.swatchClass), @@ -201,15 +205,17 @@ if (path) overlay.append(svg("path", { d: path, class: "world-line " + css })); }; const staticLines = safeArray(snapshot.staticPolylines); + const dynamicLines = safeArray(frame.dynamicPolylines); + const hasDynamicActiveSegment = dynamicLines.some(line => lineKind(line) === "active-segment"); staticLines.filter(line => lineKind(line) === "global" || lineKind(line) === "coarse") .forEach(line => addLine(line, "world-coarse")); staticLines.filter(line => lineKind(line).includes("local-g2") || lineKind(line).includes("g2")) .forEach(line => addLine(line, "world-local-g2")); staticLines.filter(line => lineKind(line) !== "global" && lineKind(line) !== "coarse" && !lineKind(line).includes("local-g2") && !lineKind(line).includes("g2")) .forEach(line => addLine(line, "world-static")); - safeArray(snapshot.directionSegments).forEach(segment => addLine(segment, - segment.segmentIndex === frame.activeSegmentIndex ? "world-segment-active" : "world-direction-inactive")); - const dynamicLines = safeArray(frame.dynamicPolylines); + safeArray(snapshot.directionSegments) + .filter(segment => !hasDynamicActiveSegment || segment.segmentIndex !== frame.activeSegmentIndex) + .forEach(segment => addLine(segment, "world-direction-inactive")); dynamicLines.filter(line => lineKind(line) === "active-segment") .forEach(line => addLine(line, "world-segment-active")); dynamicLines.filter(line => lineKind(line) === "previous" || line.lineStyle === 1) diff --git a/ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/SampleSnapshotFactory.cs b/ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/SampleSnapshotFactory.cs index 73622cd..e50ac2c 100644 --- a/ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/SampleSnapshotFactory.cs +++ b/ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/SampleSnapshotFactory.cs @@ -60,13 +60,15 @@ internal static class SampleSnapshotFactory new VisualizationPose(2d + offset, 1d, 0d), new[] { + new VisualizationPolyline("active-segment", "活动方向段", "active-segment", VisualizationLineStyle.Solid, + Points((0d, 1d), (3d, 1d), (6d, 2d))), new VisualizationPolyline("previous", "上一轮轨迹", "previous", VisualizationLineStyle.Dashed, Points((1d, 1d), (2.5d, 1d), (4d, 1.3d))), new VisualizationPolyline("current", "当前轨迹", "current", VisualizationLineStyle.Solid, Points((2d + offset, 1d), (3.8d + offset, 1.2d), (5.8d + offset, 1.9d))) }, new[] { new VisualizationMarker("vehicle", "vehicle", "车辆", new VisualizationPoint(2d + offset, 1d)), - new VisualizationMarker("plan-start", "plan-start", "计划起点", new VisualizationPoint(2d + offset, 1d)), + new VisualizationMarker("plan-start", "plan-start", "计划起点", new VisualizationPoint(0d, 1d)), new VisualizationMarker("gear-switch-end", "gear-switch-end", "换向点 1 / s_end", new VisualizationPoint(6d, 2d)) }, CreateCharts(offset), diff --git a/ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/WebAssetChecks.cs b/ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/WebAssetChecks.cs index 005e99e..b82c489 100644 --- a/ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/WebAssetChecks.cs +++ b/ClumsyPilot/tests/TrajectoryPlanningVisualizationVerificationHost/WebAssetChecks.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using TrajectoryPlanningVisualization; @@ -65,6 +66,8 @@ internal static class WebAssetChecks "vehicle marker no longer uses world-scale heavy strokes"); 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-plan-start { fill: none;"), + "plan start marker remains hollow when it overlaps the smoothed-path start"); 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"); @@ -85,6 +88,29 @@ internal static class WebAssetChecks "overview builds legend entries from current snapshot data"); Verification.True(js.Contains("legendChinese"), "overview reuses exported Chinese polyline legends"); + Verification.True( + js.Contains("smoothStartMarker.labelChinese || \"平滑路径起点\"") && + js.Contains("planStartMarker.labelChinese || \"EM 规划起点\"") && + js.Contains("vehicleMarker.labelChinese || \"当前车辆\"") && + js.Contains("gearMarker.labelChinese || \"换向点 / s_end\"") && + js.Contains("goalMarker.labelChinese || \"平滑路径终点\""), + "overview marker legends prefer the first matching marker label with canonical fallbacks"); + Verification.True(js.Contains("const hasDynamicActiveSegment = dynamicLines.some("), + "overview detects whether the frame supplies a dynamic active segment"); + Verification.True(js.Contains( + ".filter(segment => !hasDynamicActiveSegment || segment.segmentIndex !== frame.activeSegmentIndex)"), + "overview omits the matching static segment when its dynamic active segment exists"); + Verification.True(js.Contains("if (active)") && + !js.Contains("if (active || directionSegments.length)"), + "overview legend activates the current direction segment from dynamic data only"); + int renderWorldStart = js.IndexOf("function renderWorld()", StringComparison.Ordinal); + int renderDynamicLines = js.IndexOf("const dynamicLines = safeArray(frame.dynamicPolylines);", + renderWorldStart, StringComparison.Ordinal); + int renderStaticSegments = js.IndexOf("safeArray(snapshot.directionSegments)", + renderDynamicLines, StringComparison.Ordinal); + Verification.True(renderWorldStart >= 0 && renderDynamicLines > renderWorldStart && + renderStaticSegments > renderDynamicLines, + "overview derives dynamic lines before rendering static direction segments"); Verification.True(js.Contains("atob"), "compact occupancy bitset is decoded in browser"); Verification.True(!html.Contains("http://") && !html.Contains("https://"), "page has no CDN URL"); } @@ -100,6 +126,14 @@ internal static class WebAssetChecks "smoke snapshot exposes the smoothed comparison path"); Verification.True(staticSnapshot.StaticMarkers.Any(marker => marker.Kind == "smooth-start"), "smoke snapshot exposes the smoothed-path start marker"); + Verification.Equal(1, dynamicSnapshot.DynamicPolylines.Count(line => line.Kind == "active-segment"), + "smoke snapshot has exactly one dynamic active segment"); + VisualizationMarker smoothStart = staticSnapshot.StaticMarkers.Single(marker => marker.Kind == "smooth-start"); + VisualizationMarker planStart = dynamicSnapshot.DynamicMarkers.Single(marker => marker.Kind == "plan-start"); + Verification.NearlyEqual(smoothStart.Position.X, planStart.Position.X, + "smoke snapshot overlaps smoothed-path and plan-start x coordinates"); + Verification.NearlyEqual(smoothStart.Position.Y, planStart.Position.Y, + "smoke snapshot overlaps smoothed-path and plan-start y coordinates"); Verification.Equal(2, staticSnapshot.DirectionSegments.Count, "smoke snapshot has two direction segments"); Verification.True(staticSnapshot.StaticMarkers.Any(marker => marker.Kind == "gear-switch"), "smoke snapshot has a gear marker"); Verification.True(staticSnapshot.ConfigurationGroups.Count > 0, "smoke snapshot has effective configuration");