From 761a52646c07500f55fb728cf57ddd7cb658fdd4 Mon Sep 17 00:00:00 2001 From: que01 Date: Wed, 12 Aug 2026 11:29:13 +0800 Subject: [PATCH] feat(preview): add vehicle incident cards --- .trellis/spec/preview/index.md | 15 +++ assets/preview/vehicle-accident.png | Bin 0 -> 1598 bytes assets/preview/vehicle-breakdown.png | Bin 0 -> 4826 bytes scripts/lib/area-preview.js | 24 ++++- scripts/lib/cesium-preview.css | 94 ++++++++++++++++++- scripts/lib/cesium-preview.js | 132 ++++++++++++++++++++++++++- scripts/test-preview-assets.js | 12 +++ 7 files changed, 271 insertions(+), 6 deletions(-) create mode 100644 assets/preview/vehicle-accident.png create mode 100644 assets/preview/vehicle-breakdown.png diff --git a/.trellis/spec/preview/index.md b/.trellis/spec/preview/index.md index af4e3c0..f0c6d25 100644 --- a/.trellis/spec/preview/index.md +++ b/.trellis/spec/preview/index.md @@ -190,6 +190,21 @@ let baseStatus = ""; 这会让用户误判材质和模型质量。若确实需要性能模式,应做成显式开关,而不是默认牺牲 预览清晰度。 +### 车辆事件仅属于预览会话 + +点击车辆的信息卡可在 `normal`、`breakdown`、`accident` 三态间切换。这是验证交互, +不得写入 `package/`、路线 JSON 或 OSM。状态必须附着在 `addCruiseVehicle()` 返回的记录上; +`createTrafficAwarePositions()` 只在状态为 `normal` 时推进已有的 route distance,恢复正常从 +当前停点继续。 + +- `breakdown`:黄色扳手 label,路线保留原色。 +- `accident`:红色警示 label,路线设为红色。 +- `normal`:隐藏 label,恢复原路线颜色。 + +车辆实体以 `properties.vehicleId` 标识;点击拾取必须只处理此属性,不能把静态模型、路线或 +信号灯当作车辆。Cesium InfoBox 在本预览中关闭,信息卡必须使用 HTML/CSS,并把新增 DOM 句柄 +集中在 `cesium-preview.js` 顶部。 + ### 单资产 vs 多资产的开关 ```js diff --git a/assets/preview/vehicle-accident.png b/assets/preview/vehicle-accident.png new file mode 100644 index 0000000000000000000000000000000000000000..12372d6d2237b362a7df3ebdf70d9adea5843f77 GIT binary patch literal 1598 zcmV-E2EqA>P)jeH zXlY1#a%EF`PE=!hYhyWNB0oL~Ja{^IZE$U6bYUQPZES9HI(R)IVPtP&WjbziI&Eci zVJ{*ecsh7(aCB=uB3MmOAVY6*Wgs>#FfK46J_;Z_a%5&YQba}|cx`NMb2@TlW<4Tk zbaZe!FE4j@cP@7`E^l&YFEKeeIWI6WFETPMa%5&Lb9rubVR$WWb0Z=?3Lqdna%5&Y zL}hbha%pgMX>V>Ia%5&YVPbD}bUh*>FFpz&JTG!&W;#+tMm`EWFL*k5ZE$U6bYVUU zJU@7FVPk7$bRcDJWIZBsB0oNtTj>e_0004VQb$4nuFf3k000EhNklN2*)1*I-P%T>O@p=-tr`>5_z_Cm7X_m60~3r-YBU<{gEd&TNP-XQi-uQ2 z!i&L3_#1rC7maB32?B`?q>v~L8(Zm?;rxc3$(`A`Gk4YnJjw07_k8~L%>AyQ7U{PG zg~9dp`g7vCc*qviCy4sk6$uhuT<+G+v(y}CJHUtRftoz5_@ysq;wJqB81{%2Mu`}s)q zGTM<83-FiO{9gW4M;TZt(o;eq16|^w@zw)JYTref8Y&?J5~n}R9~9T^Z`%zHG2s)w z5d*PQI6?(~e$k)(>(Z5DZ1{r8u$_F)=7~+c#r*^CFRldrHf0dMrWJ5zu7`%-`;ms< z{ejGm?qHnAJ`&N0*_5y*fWOXm&b5-c^27nkmPaXD8Kd&fk0Jqu2@w;qgEAjLOb8wv z2ZKs+{S7@s)cwe7o?7>#uT#2vtJl|VaBK&IWZ(hBQsT^3Ua};)Rs6*dk-J`-XD0P2WS)|`G1I_A4nX@~dWVsFX;s_#C7s(QDZljzKU3=E2cM+Q z;R%VO*8~=9snB)+@^^3kMsdD4LzSsRKCSvce-nyKm1)*t-K2#59ySI}C5(79se`WZ zS9n<&@tK`D`V67Sgk`7e!Ixx~RvVar&A^dK%R*8T)&?q1d@ONpHm@x&5=xNDyXJV7 zr-SU+KCl@$&&6s66nb{NLFrmBU4Bc zih#}F;hP8`z*!w#i2^~{>exgj2APC_4YODqV5)FeOP|UG)wAPGN^R5*2Qvm}V~5cr zObjEHQ7Rkh9HFQPw&h*TlS`!!Y!>gl7rD`?RB8w^rTYj=OBCJx^4C#fN7gmIk1l`z zC9N*~5vvPq2B+^rye1JATw!2DsL8HhBa|qDkMh)fl!_+-uJ^>2@96)03KgR zU;q}E=>fz;4&F*QrP^Dc>!e*ZVgrNxsN?}e5a%IB2q(4e3Yy3d#|}OwF=#*WL=k+$ z|Ay<^@-E{>=TCd${v%gkzBBQ3Y>gat==+Y}J%$ zm0c1(#vEs4o-=nSm^s%^Y~cUx1E%{6Flc$&5AiRoa;dreR%`fxFZc{jh%leA8I+B% wefA?%OKtM?fN&w4Xr|{m&uJb0RR9107*qoM6N<$g3PnZv;Y7A literal 0 HcmV?d00001 diff --git a/assets/preview/vehicle-breakdown.png b/assets/preview/vehicle-breakdown.png new file mode 100644 index 0000000000000000000000000000000000000000..621cd62460a735a3b66e19841b8f5a40c4363e08 GIT binary patch literal 4826 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 AmH+?% literal 0 HcmV?d00001 diff --git a/scripts/lib/area-preview.js b/scripts/lib/area-preview.js index 3a910ed..4522338 100644 --- a/scripts/lib/area-preview.js +++ b/scripts/lib/area-preview.js @@ -4,8 +4,13 @@ const fs = require("fs"); const path = require("path"); function writeCesiumPreviewSupportFiles(outDir) { - for (const file of ["cesium-preview.css", "cesium-preview.js"]) { - const source = path.join(__dirname, file); + const files = [ + [path.join(__dirname, "cesium-preview.css"), "cesium-preview.css"], + [path.join(__dirname, "cesium-preview.js"), "cesium-preview.js"], + [path.join(__dirname, "..", "..", "assets", "preview", "vehicle-breakdown.png"), "vehicle-breakdown.png"], + [path.join(__dirname, "..", "..", "assets", "preview", "vehicle-accident.png"), "vehicle-accident.png"], + ]; + for (const [source, file] of files) { if (!fs.existsSync(source)) { throw new Error(`Cesium preview support file '${file}' not found: ${source}`); } @@ -69,6 +74,21 @@ function cesiumPreviewHtml(glbName, metadataName, routeName, vehicleModelName, a
Loading diagnostics...
Loading ${escapeHtml(glbName)}...
+
diff --git a/scripts/lib/cesium-preview.css b/scripts/lib/cesium-preview.css index 957ca2f..9e5b221 100644 --- a/scripts/lib/cesium-preview.css +++ b/scripts/lib/cesium-preview.css @@ -20,7 +20,8 @@ body.scene-ready #cesiumContainer { #controls, #diagnostics, -#status { +#status, +#vehicleInfoCard { position: absolute; z-index: 1; border-radius: 4px; @@ -160,6 +161,96 @@ body.scene-error .loading-bar span { white-space: pre-line; } +#vehicleInfoCard { + left: 0; + top: 0; + width: min(300px, calc(100vw - 24px)); + padding: 10px 12px; + transform: translate(-50%, calc(-100% - 18px)); + transform-origin: bottom center; + pointer-events: auto; +} + +#vehicleInfoCard::after { + position: absolute; + left: 50%; + bottom: -7px; + width: 12px; + height: 12px; + content: ""; + transform: translateX(-50%) rotate(45deg); + background: rgba(20, 24, 28, 0.82); +} + +.vehicle-card-heading, +.vehicle-card-actions { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.vehicle-card-heading strong { + font-size: 13px; +} + +#vehicleInfoCard button { + height: 27px; + border: 0; + border-radius: 4px; + padding: 0 8px; + background: #f2f5f7; + color: #111; + cursor: pointer; +} + +#vehicleInfoDetails { + display: grid; + grid-template-columns: 88px 1fr; + gap: 4px 8px; + margin: 10px 0; +} + +#vehicleInfoDetails dt { + color: rgba(255, 255, 255, 0.62); +} + +#vehicleInfoDetails dd { + margin: 0; + overflow-wrap: anywhere; +} + +.vehicle-card-field { + display: grid; + gap: 4px; + color: rgba(255, 255, 255, 0.78); +} + +#vehicleIncidentNote { + box-sizing: border-box; + width: 100%; + resize: vertical; + border: 1px solid rgba(255, 255, 255, 0.30); + border-radius: 4px; + padding: 6px; + background: rgba(255, 255, 255, 0.12); + color: #fff; + font: inherit; +} + +.vehicle-card-actions { + justify-content: flex-start; + margin-top: 10px; +} + +.vehicle-card-actions button[data-vehicle-status="breakdown"] { + background: #f3c84b; +} + +.vehicle-card-actions button[data-vehicle-status="accident"] { + background: #e95950; +} + #controls button, #controls select { height: 28px; @@ -285,4 +376,5 @@ body.scene-error .loading-bar span { bottom: 10px; max-width: none; } + } diff --git a/scripts/lib/cesium-preview.js b/scripts/lib/cesium-preview.js index 153b2da..ded3754 100644 --- a/scripts/lib/cesium-preview.js +++ b/scripts/lib/cesium-preview.js @@ -16,6 +16,12 @@ const toggleFps = document.getElementById("toggleFps"); const toggleDiagnostics = document.getElementById("toggleDiagnostics"); const assetToggles = document.getElementById("assetToggles"); + const vehicleInfoCard = document.getElementById("vehicleInfoCard"); + const vehicleInfoTitle = document.getElementById("vehicleInfoTitle"); + const vehicleInfoDetails = document.getElementById("vehicleInfoDetails"); + const vehicleIncidentNote = document.getElementById("vehicleIncidentNote"); + const closeVehicleInfo = document.getElementById("closeVehicleInfo"); + const vehicleStatusButtons = Array.from(document.querySelectorAll("[data-vehicle-status]")); // The exported road surface sits at the 0.35m scene anchor plus 0.03m. // Vehicle models have their wheels at local Y=0, so keep them just clear // of the asphalt instead of using the old visibly floating 1.15m height. @@ -56,6 +62,7 @@ buildAssetToggles(assets); buildSemanticToggles(viewer, assets, placement); bindRuntimeControls(viewer, assets, cruise, cameras, placement, trafficSignals); + bindVehicleInfoCard(viewer, cruise); startDiagnostics(viewer, metadata, assets, cruise, placement, trafficSignals); cameras.overview(); baseStatus = summaryText(metadata, assets, cruise); @@ -726,7 +733,8 @@ function addCruiseVehicle(viewer, segment, index, start, speed, signalData, vehicleModelName) { const route = prepareRoute(segment, signalData); - const trafficMotion = createTrafficAwarePositions(viewer, route, start, speed); + let record = null; + const trafficMotion = createTrafficAwarePositions(viewer, route, start, speed, () => record?.status === "normal"); const positions = trafficMotion.positions; const flat = []; for (const coord of segment.coordinates) { @@ -761,14 +769,41 @@ minimumPixelSize: 0 } }); - return { + record = { + id: "vehicle-" + (index + 1), entity: vehicle, routeEntity, positions, route, segment, label: routeLabel(segment, index), + modelName: vehicleModelLabel(vehicleModelName), + status: "normal", + incidentNote: "", + motion: trafficMotion, + routeColor, + markerEntity: null, }; + vehicle.properties = new Cesium.PropertyBag({ vehicleId: record.id }); + record.markerEntity = viewer.entities.add({ + name: "Vehicle incident marker " + (index + 1), + position: positions, + billboard: { + image: "", + show: false, + width: 36, + height: 44, + pixelOffset: new Cesium.Cartesian2(0, -40), + verticalOrigin: Cesium.VerticalOrigin.BOTTOM, + disableDepthTestDistance: Number.POSITIVE_INFINITY, + }, + }); + return record; + } + + function vehicleModelLabel(name) { + const file = String(name || "").split("/").pop().replace(/\.gltf$/i, ""); + return file.replace(/^.*-vehicle-/, "").replaceAll("_", " "); } function routeLabel(route, index) { @@ -789,6 +824,96 @@ return cruise.vehicles[cruise.state.selectedIndex] || cruise.vehicles[0]; } + function vehicleById(cruise, id) { + return cruise.vehicles.find((vehicle) => vehicle.id === id) || null; + } + + function setVehicleStatus(viewer, vehicle, status) { + if (!vehicle || !["normal", "breakdown", "accident"].includes(status)) return; + vehicle.status = status; + const incident = status !== "normal"; + const accident = status === "accident"; + vehicle.markerEntity.billboard.image = status === "breakdown" + ? "vehicle-breakdown.png" + : accident ? "vehicle-accident.png" : ""; + vehicle.markerEntity.billboard.show = incident; + vehicle.routeEntity.polyline.material = accident ? Cesium.Color.RED : vehicle.routeColor; + if (viewer.scene.requestRender) viewer.scene.requestRender(); + } + + function bindVehicleInfoCard(viewer, cruise) { + if (!vehicleInfoCard || !cruise.vehicles.length) return; + let activeVehicle = null; + const render = () => { + if (!activeVehicle) return; + vehicleInfoTitle.textContent = activeVehicle.label; + vehicleIncidentNote.value = activeVehicle.incidentNote; + const details = [ + ["Vehicle", activeVehicle.id], + ["Model", activeVehicle.modelName], + ["Route", activeVehicle.label], + ["Speed", cruise.baseSpeed + " m/s"], + ["Status", activeVehicle.status], + ]; + vehicleInfoDetails.replaceChildren(...details.flatMap(([term, value]) => { + const dt = document.createElement("dt"); + const dd = document.createElement("dd"); + dt.textContent = term; + dd.textContent = value; + return [dt, dd]; + })); + for (const button of vehicleStatusButtons) { + button.disabled = button.dataset.vehicleStatus === activeVehicle.status; + } + }; + const open = (vehicle) => { + activeVehicle = vehicle; + const index = cruise.vehicles.indexOf(vehicle); + cruise.state.selectedIndex = index; + vehicleSelect.value = String(index); + syncSelectedRouteVisibility(cruise); + vehicleInfoCard.classList.remove("hidden"); + render(); + }; + const positionCard = () => { + if (!activeVehicle || vehicleInfoCard.classList.contains("hidden")) return; + const position = activeVehicle.entity.position?.getValue(viewer.clock.currentTime); + const windowPosition = position && Cesium.SceneTransforms.worldToWindowCoordinates(viewer.scene, position); + if (!windowPosition) { + vehicleInfoCard.classList.add("hidden"); + return; + } + const margin = 12; + const width = vehicleInfoCard.offsetWidth || 300; + const x = Cesium.Math.clamp(windowPosition.x, margin + width / 2, window.innerWidth - margin - width / 2); + vehicleInfoCard.style.left = x + "px"; + vehicleInfoCard.style.top = Math.max(windowPosition.y, margin + 20) + "px"; + }; + closeVehicleInfo.addEventListener("click", () => { + activeVehicle = null; + vehicleInfoCard.classList.add("hidden"); + }); + vehicleIncidentNote.addEventListener("input", () => { + if (activeVehicle) activeVehicle.incidentNote = vehicleIncidentNote.value.trim(); + }); + for (const button of vehicleStatusButtons) { + button.addEventListener("click", () => { + if (!activeVehicle) return; + setVehicleStatus(viewer, activeVehicle, button.dataset.vehicleStatus); + render(); + setStatus(activeVehicle.label + " " + activeVehicle.status); + }); + } + const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas); + handler.setInputAction((movement) => { + const picked = viewer.scene.pick(movement.position); + const id = picked?.id?.properties?.vehicleId?.getValue?.(); + const vehicle = vehicleById(cruise, id); + if (vehicle) open(vehicle); + }, Cesium.ScreenSpaceEventType.LEFT_CLICK); + viewer.scene.postRender.addEventListener(positionCard); + } + function prepareRoute(segment, signalData) { const distances = [0.0]; for (let i = 1; i < segment.coordinates.length; i += 1) { @@ -820,12 +945,13 @@ return [...found.values()].sort((a, b) => a.distance - b.distance); } - function createTrafficAwarePositions(viewer, route, start, speed) { + function createTrafficAwarePositions(viewer, route, start, speed, isMoving = () => true) { const state = { distance: 0, lastTime: start.clone() }; viewer.clock.onTick.addEventListener((clock) => { const elapsed = Cesium.JulianDate.secondsDifference(clock.currentTime, state.lastTime); Cesium.JulianDate.clone(clock.currentTime, state.lastTime); if (elapsed <= 0) return; + if (!isMoving()) return; const next = nextRouteStop(route, state.distance); const advance = elapsed * speed; if (next && signalPhase(next.signal.phaseGroup, clock.currentTime, start).active !== "green") { diff --git a/scripts/test-preview-assets.js b/scripts/test-preview-assets.js index 3a656e9..24b1156 100644 --- a/scripts/test-preview-assets.js +++ b/scripts/test-preview-assets.js @@ -251,6 +251,10 @@ assert.match(html, /"glbName":"scene\\u003c\\u0026\\u003e\.glb"/); assert.match(html, /"vehicleModelNames":\["car-a\.gltf","truck-a\.gltf"\]/); assert.match(html, /"trafficSignalsName":"traffic-signals\.json"/); assert.match(html, /id="toggleSignals"/); +assert.match(html, /id="vehicleInfoCard" class="hidden"/); +assert.match(html, /id="vehicleIncidentNote"/); +assert.match(html, /data-vehicle-status="breakdown"/); +assert.match(html, /data-vehicle-status="accident"/); assert.match(html, /id="toggleBuildingGhost"/); assert.match(html, /id="viewMode"/); assert.match(html, /data-view-mode="inspect"/); @@ -264,6 +268,14 @@ assert.ok(fs.existsSync(countdownFont), "7LED countdown font must be versioned w assert.doesNotMatch(previewRuntime, /cylinder: \{ length: 6\.7/); assert.doesNotMatch(previewRuntime, /Traffic Signal Housing/); assert.match(previewRuntime, /asset\.category === "dynamic"/); +assert.match(previewRuntime, /new Cesium\.ScreenSpaceEventHandler/); +assert.match(previewRuntime, /vehicleId: record\.id/); +assert.match(previewRuntime, /status === "breakdown"\s+\? "vehicle-breakdown\.png"/); +assert.match(previewRuntime, /accident \? "vehicle-accident\.png"/); +assert.match(previewRuntime, /disableDepthTestDistance: Number\.POSITIVE_INFINITY/); +assert.match(previewRuntime, /routeEntity\.polyline\.material = accident \? Cesium\.Color\.RED : vehicle\.routeColor/); +assert.match(previewRuntime, /\(\) => record\?\.status === "normal"/); +assert.match(previewRuntime, /Cesium\.SceneTransforms\.worldToWindowCoordinates/); assert.match(previewRuntime, /TrafficSignalDynamic_/); assert.match(previewRuntime, /TrafficSignalDynamic_\$\{nodeKey\}_countdown_\$\{String\(value\)\.padStart\(2, "0"\)\}/); assert.match(previewRuntime, /ColorBlendMode\.REPLACE/);