update model

This commit is contained in:
vee
2026-08-14 11:17:17 +08:00
parent 1a96d39426
commit 6d768aaef4
56 changed files with 424 additions and 148 deletions
+183 -67
View File
@@ -2549,7 +2549,14 @@ function updateSeatTwin(actuatorState) {
} }
const REFERENCE_VEHICLE_ROOT = "/assets/models/byd-tang/"; const REFERENCE_VEHICLE_ROOT = "/assets/models/byd-tang/";
const REFERENCE_VEHICLE_URL = `${REFERENCE_VEHICLE_ROOT}byd-tang-2024.fbx`; const REFERENCE_VEHICLE_ASSET_VERSION = "77a0fae7";
const REFERENCE_VEHICLE_LOAD_TIMEOUT_MS = 30000;
function referenceVehicleAssetUrl(fileName) {
return `${REFERENCE_VEHICLE_ROOT}${encodeURIComponent(fileName)}?v=${REFERENCE_VEHICLE_ASSET_VERSION}`;
}
const REFERENCE_VEHICLE_URL = referenceVehicleAssetUrl("byd-tang-2024.fbx");
function referenceMaterialDescriptor(material) { function referenceMaterialDescriptor(material) {
const textureNames = [ const textureNames = [
@@ -3643,73 +3650,92 @@ function loadReferenceVehicleModel(THREE, scene, vehicleSystems, fallback) {
reject(new Error("FBXLoader is unavailable")); reject(new Error("FBXLoader is unavailable"));
return; return;
} }
let settled = false;
const settle = (callback, value) => {
if (settled) return false;
settled = true;
window.clearTimeout(timeoutId);
callback(value);
return true;
};
const timeoutId = window.setTimeout(() => {
settle(
reject,
new Error(`Reference vehicle load timed out after ${REFERENCE_VEHICLE_LOAD_TIMEOUT_MS} ms`),
);
}, REFERENCE_VEHICLE_LOAD_TIMEOUT_MS);
const manager = new THREE.LoadingManager(); const manager = new THREE.LoadingManager();
manager.setURLModifier((url) => { manager.setURLModifier((url) => {
const cleanUrl = String(url).replaceAll("\\", "/"); const cleanUrl = String(url).replaceAll("\\", "/").split(/[?#]/, 1)[0];
const fileName = decodeURIComponent(cleanUrl.split("/").pop() || ""); const fileName = decodeURIComponent(cleanUrl.split("/").pop() || "");
if (!/\.(png|jpe?g)$/i.test(fileName)) return url; if (!/\.(png|jpe?g)$/i.test(fileName)) return url;
return `${REFERENCE_VEHICLE_ROOT}${encodeURIComponent(fileName)}`; return referenceVehicleAssetUrl(fileName);
}); });
const loader = new FBXLoaderClass(manager); const loader = new FBXLoaderClass(manager);
loader.setResourcePath(REFERENCE_VEHICLE_ROOT); loader.setResourcePath(REFERENCE_VEHICLE_ROOT);
loader.load( loader.load(
REFERENCE_VEHICLE_URL, REFERENCE_VEHICLE_URL,
(vehicle) => { (vehicle) => {
const normalized = normalizeReferenceVehicle(THREE, vehicle); if (settled) return;
const driverCushion = measureReferenceDriverCushion(THREE, vehicle); try {
const passengerCushion = measureReferencePassengerCushion(THREE, vehicle); const normalized = normalizeReferenceVehicle(THREE, vehicle);
const removedFrontSeatTriangles = carveReferenceFrontSeats(THREE, vehicle); const driverCushion = measureReferenceDriverCushion(THREE, vehicle);
const referenceWindowPanes = extractReferenceWindowPanes(THREE, vehicle); const passengerCushion = measureReferencePassengerCushion(THREE, vehicle);
const report = configureReferenceVehicleModel(THREE, vehicle); const removedFrontSeatTriangles = carveReferenceFrontSeats(THREE, vehicle);
styleReferencePassiveGlass(THREE, vehicle); const referenceWindowPanes = extractReferenceWindowPanes(THREE, vehicle);
const passiveRearExterior = styleReferencePassiveRearExterior(THREE, vehicle); const report = configureReferenceVehicleModel(THREE, vehicle);
const referenceFrontLampMeshes = collectReferenceFrontLampMeshes(vehicle); styleReferencePassiveGlass(THREE, vehicle);
emphasizeReferenceWindowPanes(THREE, referenceWindowPanes); const passiveRearExterior = styleReferencePassiveRearExterior(THREE, vehicle);
const referenceFrontLampMaterials = emphasizeReferenceFrontLampMeshes( const referenceFrontLampMeshes = collectReferenceFrontLampMeshes(vehicle);
THREE, emphasizeReferenceWindowPanes(THREE, referenceWindowPanes);
referenceFrontLampMeshes, const referenceFrontLampMaterials = emphasizeReferenceFrontLampMeshes(
); THREE,
const referenceFrontLampHighlights = createReferenceFrontLampHighlights( referenceFrontLampMeshes,
THREE, );
referenceFrontLampMeshes, const referenceFrontLampHighlights = createReferenceFrontLampHighlights(
); THREE,
const referenceFrontLampLensOutlines = createReferenceFrontLampLensOutlines( referenceFrontLampMeshes,
THREE, );
vehicle, const referenceFrontLampLensOutlines = createReferenceFrontLampLensOutlines(
); THREE,
vehicleSystems.referenceWindowPanes = referenceWindowPanes; vehicle,
vehicleSystems.referenceFrontLampMaterials = referenceFrontLampMaterials; );
vehicleSystems.referenceFrontLampMeshes = referenceFrontLampMeshes; vehicleSystems.referenceWindowPanes = referenceWindowPanes;
vehicleSystems.referenceFrontLampHighlights = referenceFrontLampHighlights; vehicleSystems.referenceFrontLampMaterials = referenceFrontLampMaterials;
vehicleSystems.referenceFrontLampLensOutlines = referenceFrontLampLensOutlines; vehicleSystems.referenceFrontLampMeshes = referenceFrontLampMeshes;
applyTwinVehicleTheme(vehicleSystems); vehicleSystems.referenceFrontLampHighlights = referenceFrontLampHighlights;
vehicle.name = "REFERENCE_VEHICLE_OUTLINE"; vehicleSystems.referenceFrontLampLensOutlines = referenceFrontLampLensOutlines;
vehicle.userData.twinRole = "reference_vehicle"; applyTwinVehicleTheme(vehicleSystems);
vehicle.traverse((object) => object.layers.set(1)); vehicle.name = "REFERENCE_VEHICLE_OUTLINE";
scene.add(vehicle); vehicle.userData.twinRole = "reference_vehicle";
vehicleSystems.fallbackExteriorObjects.forEach((object) => { vehicle.traverse((object) => object.layers.set(1));
object.visible = false; scene.add(vehicle);
}); vehicleSystems.fallbackExteriorObjects.forEach((object) => {
vehicleSystems.fallbackInteriorObjects.forEach((object) => { object.visible = false;
object.visible = false; });
}); vehicleSystems.fallbackInteriorObjects.forEach((object) => {
console.info("[vehicle-twin] reference vehicle model loaded", JSON.stringify({ object.visible = false;
meshes: report.meshes, });
size: normalized.size.toArray().map((value) => Number(value.toFixed(3))), console.info("[vehicle-twin] reference vehicle model loaded", JSON.stringify({
removedFrontSeatTriangles, meshes: report.meshes,
windowPanes: Object.fromEntries( size: normalized.size.toArray().map((value) => Number(value.toFixed(3))),
Object.entries(referenceWindowPanes).map(([key, value]) => [key, value.length]), removedFrontSeatTriangles,
), windowPanes: Object.fromEntries(
frontLampMaterials: referenceFrontLampMaterials.length, Object.entries(referenceWindowPanes).map(([key, value]) => [key, value.length]),
frontLampMeshes: referenceFrontLampMeshes.length, ),
frontLampHighlights: referenceFrontLampHighlights.length, frontLampMaterials: referenceFrontLampMaterials.length,
frontLampLensOutlines: referenceFrontLampLensOutlines.length, frontLampMeshes: referenceFrontLampMeshes.length,
rearLampMeshes: passiveRearExterior.rearLampMeshes.length, frontLampHighlights: referenceFrontLampHighlights.length,
rearBumperMeshes: passiveRearExterior.rearBumperMeshes.length, frontLampLensOutlines: referenceFrontLampLensOutlines.length,
driverCushion, rearLampMeshes: passiveRearExterior.rearLampMeshes.length,
passengerCushion, rearBumperMeshes: passiveRearExterior.rearBumperMeshes.length,
})); driverCushion,
resolve({ vehicle, report, normalized, driverCushion, passengerCushion }); passengerCushion,
}));
settle(resolve, { vehicle, report, normalized, driverCushion, passengerCushion });
} catch (error) {
settle(reject, error);
}
}, },
(event) => { (event) => {
if (!fallback || !event.total) return; if (!fallback || !event.total) return;
@@ -3717,7 +3743,7 @@ function loadReferenceVehicleModel(THREE, scene, vehicleSystems, fallback) {
const label = fallback.querySelector("span"); const label = fallback.querySelector("span");
if (label) label.textContent = `正在加载整车参考模型 · ${progress}%`; if (label) label.textContent = `正在加载整车参考模型 · ${progress}%`;
}, },
reject, (error) => settle(reject, error),
); );
}); });
} }
@@ -3726,7 +3752,33 @@ function initSeatTwin() {
const canvas = $("#seatTwinCanvas"); const canvas = $("#seatTwinCanvas");
const stage = $("#seatTwin"); const stage = $("#seatTwin");
const fallback = $("#twinFallback"); const fallback = $("#twinFallback");
if (!canvas || !stage) return; if (!canvas || !stage || state.seatTwin) return;
const stageHasLayout = () => {
const bounds = stage.getBoundingClientRect();
return stage.isConnected && bounds.width >= 2 && bounds.height >= 2;
};
if (!stageHasLayout()) {
if (stage.dataset.twinInitPending === "true") return;
stage.dataset.twinInitPending = "true";
let pendingObserver = null;
const retryInitialization = () => {
if (!stageHasLayout()) return;
pendingObserver?.disconnect();
window.removeEventListener("resize", scheduleRetry);
window.removeEventListener("cognitive-agent:module-change", scheduleRetry);
delete stage.dataset.twinInitPending;
initSeatTwin();
};
const scheduleRetry = () => window.requestAnimationFrame(retryInitialization);
if (window.ResizeObserver) {
pendingObserver = new ResizeObserver(retryInitialization);
pendingObserver.observe(stage);
}
window.addEventListener("resize", scheduleRetry);
window.addEventListener("cognitive-agent:module-change", scheduleRetry);
scheduleRetry();
return;
}
if (!window.THREE) { if (!window.THREE) {
fallback.classList.add("error"); fallback.classList.add("error");
fallback.innerHTML = '<i data-lucide="triangle-alert"></i><span>3D 引擎加载失败,精确状态仍可用</span>'; fallback.innerHTML = '<i data-lucide="triangle-alert"></i><span>3D 引擎加载失败,精确状态仍可用</span>';
@@ -3735,7 +3787,16 @@ function initSeatTwin() {
} }
const THREE = window.THREE; const THREE = window.THREE;
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, powerPreference: "high-performance" }); let renderer;
try {
renderer = new THREE.WebGLRenderer({ canvas, antialias: true, powerPreference: "high-performance" });
} catch (error) {
console.error("[vehicle-twin] failed to initialize WebGL", error);
fallback.classList.add("error");
fallback.innerHTML = '<i data-lucide="triangle-alert"></i><span>3D 渲染器初始化失败,状态数据仍可用</span>';
refreshIcons();
return;
}
renderer.autoClear = false; renderer.autoClear = false;
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2)); renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
if ("outputColorSpace" in renderer) renderer.outputColorSpace = THREE.SRGBColorSpace; if ("outputColorSpace" in renderer) renderer.outputColorSpace = THREE.SRGBColorSpace;
@@ -3883,6 +3944,7 @@ function initSeatTwin() {
applyTwinSceneTheme(state.seatTwin); applyTwinSceneTheme(state.seatTwin);
}); });
canvas.dataset.referenceModelState = "loading";
loadReferenceVehicleModel(THREE, scene, vehicleSystems, fallback) loadReferenceVehicleModel(THREE, scene, vehicleSystems, fallback)
.then(({ vehicle, report, normalized, driverCushion, passengerCushion }) => { .then(({ vehicle, report, normalized, driverCushion, passengerCushion }) => {
if (!state.seatTwin) return; if (!state.seatTwin) return;
@@ -3920,11 +3982,12 @@ function initSeatTwin() {
.toArray() .toArray()
.map((value) => value.toFixed(2)) .map((value) => value.toFixed(2))
.join("x"); .join("x");
canvas.dataset.referenceModelLoaded = "true"; canvas.dataset.referenceModelState = "loaded";
fallback.hidden = true; fallback.hidden = true;
}) })
.catch((error) => { .catch((error) => {
console.error("[vehicle-twin] failed to load reference vehicle model", error); console.error("[vehicle-twin] failed to load reference vehicle model", error);
canvas.dataset.referenceModelState = "failed";
fallback.classList.add("error"); fallback.classList.add("error");
fallback.innerHTML = '<i data-lucide="triangle-alert"></i><span>实车模型加载失败,已切换轻量回退模型</span>'; fallback.innerHTML = '<i data-lucide="triangle-alert"></i><span>实车模型加载失败,已切换轻量回退模型</span>';
refreshIcons(); refreshIcons();
@@ -3940,17 +4003,64 @@ function initSeatTwin() {
}); });
} }
let stageWasVisible = false;
function resize() { function resize() {
const width = Math.max(1, canvas.clientWidth); const width = Math.max(1, Math.round(stage.clientWidth));
const height = Math.max(1, canvas.clientHeight); const height = Math.max(1, Math.round(stage.clientHeight));
renderer.setSize(width, height, false); renderer.setSize(width, height, false);
camera.aspect = width / height; camera.aspect = width / height;
camera.updateProjectionMatrix(); camera.updateProjectionMatrix();
state.seatTwin.narrowViewport = width < 600; state.seatTwin.narrowViewport = width < 600;
} }
new ResizeObserver(resize).observe(stage); function synchronizeTwinViewport({ reset = false } = {}) {
resize(); if (!stageHasLayout()) {
stageWasVisible = false;
return false;
}
resize();
if (reset || !stageWasVisible) resetView();
stageWasVisible = true;
return true;
}
const scheduleViewportSync = () => {
window.requestAnimationFrame(() => window.requestAnimationFrame(() => {
synchronizeTwinViewport();
}));
};
if (window.ResizeObserver) {
const stageObserver = new ResizeObserver(() => synchronizeTwinViewport());
stageObserver.observe(stage);
state.seatTwin.resizeObserver = stageObserver;
}
window.addEventListener("resize", scheduleViewportSync);
window.addEventListener("cognitive-agent:module-change", scheduleViewportSync);
canvas.addEventListener("webglcontextlost", (event) => {
event.preventDefault();
canvas.dataset.contextLost = "true";
fallback.hidden = false;
fallback.classList.add("error");
fallback.innerHTML = '<i data-lucide="triangle-alert"></i><span>3D 上下文已中断,正在等待恢复</span>';
refreshIcons();
});
canvas.addEventListener("webglcontextrestored", () => {
delete canvas.dataset.contextLost;
synchronizeTwinViewport({ reset: true });
const modelState = canvas.dataset.referenceModelState;
fallback.hidden = modelState === "loaded";
if (modelState === "loading") {
fallback.classList.remove("error");
fallback.innerHTML = '<i data-lucide="loader-circle"></i><span>正在加载整车参考模型</span>';
refreshIcons();
} else if (modelState === "failed") {
fallback.classList.add("error");
fallback.innerHTML = '<i data-lucide="triangle-alert"></i><span>实车模型加载失败,已切换轻量回退模型</span>';
refreshIcons();
window.setTimeout(() => { fallback.hidden = true; }, 3200);
}
});
synchronizeTwinViewport({ reset: true });
stage.addEventListener("pointerdown", (event) => { stage.addEventListener("pointerdown", (event) => {
state.seatTwin.dragging = true; state.seatTwin.dragging = true;
state.seatTwin.pointer = { x: event.clientX, y: event.clientY }; state.seatTwin.pointer = { x: event.clientX, y: event.clientY };
@@ -4052,6 +4162,12 @@ function initSeatTwin() {
function animate(timestamp = 0) { function animate(timestamp = 0) {
const twin = state.seatTwin; const twin = state.seatTwin;
if (!twin) return; if (!twin) return;
if (!stageHasLayout() || canvas.dataset.contextLost === "true") {
stageWasVisible = false;
window.requestAnimationFrame(animate);
return;
}
if (!stageWasVisible) synchronizeTwinViewport({ reset: true });
Object.values(twin.models).forEach((model) => { Object.values(twin.models).forEach((model) => {
Object.keys(model.positions).forEach((axis) => { Object.keys(model.positions).forEach((axis) => {
const delta = model.targets[axis] - model.positions[axis]; const delta = model.targets[axis] - model.positions[axis];
@@ -6556,7 +6672,7 @@ document.addEventListener("DOMContentLoaded", async () => {
initJointTwinLayout(); initJointTwinLayout();
initResultWorkspace(); initResultWorkspace();
refreshIcons(); refreshIcons();
initSeatTwin(); window.requestAnimationFrame(initSeatTwin);
$("#requestForm").addEventListener("submit", submitRequest); $("#requestForm").addEventListener("submit", submitRequest);
$("#utterance").addEventListener("keydown", (event) => { $("#utterance").addEventListener("keydown", (event) => {
if (event.key === "Enter" && !event.shiftKey && !event.isComposing) { if (event.key === "Enter" && !event.shiftKey && !event.isComposing) {
+2 -2
View File
@@ -19,14 +19,14 @@
<script src="/vendor/three/0.147.0/fflate.min.js" defer></script> <script src="/vendor/three/0.147.0/fflate.min.js" defer></script>
<script src="/vendor/three/0.147.0/FBXLoader.js" defer></script> <script src="/vendor/three/0.147.0/FBXLoader.js" defer></script>
<script src="/network.js?v=android-pad-1" defer></script> <script src="/network.js?v=android-pad-1" defer></script>
<script src="/app.js?v=dual-seat-80" defer></script> <script src="/app.js?v=dual-seat-81" defer></script>
<script type="module" src="/gesture-control.js?v=android-pad-7"></script> <script type="module" src="/gesture-control.js?v=android-pad-7"></script>
<script src="/pwa.js?v=android-pad-1" defer></script> <script src="/pwa.js?v=android-pad-1" defer></script>
<script src="/console-v2.js?v=desktop-refactor-35" defer></script> <script src="/console-v2.js?v=desktop-refactor-35" defer></script>
<script src="/self-extension.js?v=extension-20" defer></script> <script src="/self-extension.js?v=extension-20" defer></script>
<script src="/self-calibration.js?v=calibration-6" defer></script> <script src="/self-calibration.js?v=calibration-6" defer></script>
<script src="/self-repair.js?v=repair-20" defer></script> <script src="/self-repair.js?v=repair-20" defer></script>
<script src="/self-repair-seat3d.js?v=repair-18" defer></script> <script src="/self-repair-seat3d.js?v=repair-19" defer></script>
</head> </head>
<body> <body>
<header class="topbar"> <header class="topbar">
+71 -4
View File
@@ -1,6 +1,33 @@
(function () { (function () {
"use strict"; "use strict";
function viewerHasLayout(viewer) {
const bounds = viewer.getBoundingClientRect();
return viewer.isConnected && bounds.width >= 2 && bounds.height >= 2;
}
function deferSeat3dUntilVisible(viewer, options) {
if (viewer.dataset.seat3dPending === "true") return;
viewer.dataset.seat3dPending = "true";
let observer = null;
const retry = () => {
if (!viewerHasLayout(viewer)) return;
observer?.disconnect();
window.removeEventListener("resize", scheduleRetry);
window.removeEventListener("cognitive-agent:module-change", scheduleRetry);
delete viewer.dataset.seat3dPending;
initSeat3d(options);
};
const scheduleRetry = () => window.requestAnimationFrame(retry);
if (window.ResizeObserver) {
observer = new ResizeObserver(retry);
observer.observe(viewer);
}
window.addEventListener("resize", scheduleRetry);
window.addEventListener("cognitive-agent:module-change", scheduleRetry);
scheduleRetry();
}
function initSeat3d(options) { function initSeat3d(options) {
const { const {
canvasId, canvasId,
@@ -15,6 +42,10 @@
const canvas = document.getElementById(canvasId); const canvas = document.getElementById(canvasId);
const viewer = document.getElementById(viewerId); const viewer = document.getElementById(viewerId);
if (!canvas || !viewer || viewer.dataset.seat3dInitialized === "true") return; if (!canvas || !viewer || viewer.dataset.seat3dInitialized === "true") return;
if (!viewerHasLayout(viewer)) {
deferSeat3dUntilVisible(viewer, options);
return;
}
viewer.dataset.seat3dInitialized = "true"; viewer.dataset.seat3dInitialized = "true";
const markReady = (fallback = false) => { const markReady = (fallback = false) => {
@@ -387,6 +418,24 @@
camera.updateProjectionMatrix(); camera.updateProjectionMatrix();
} }
let viewerWasVisible = false;
function synchronizeViewport({ reset = false } = {}) {
if (!viewerHasLayout(viewer)) {
viewerWasVisible = false;
return false;
}
resize();
if (reset || !viewerWasVisible) resetOrbit();
viewerWasVisible = true;
return true;
}
const scheduleViewportSync = () => {
window.requestAnimationFrame(() => window.requestAnimationFrame(() => {
synchronizeViewport();
}));
};
function onHealingState(event) { function onHealingState(event) {
const detail = event.detail || {}; const detail = event.detail || {};
(detail.axes || []).forEach((axis) => { (detail.axes || []).forEach((axis) => {
@@ -404,6 +453,11 @@
function animate() { function animate() {
window.requestAnimationFrame(animate); window.requestAnimationFrame(animate);
if (!viewerHasLayout(viewer) || canvas.dataset.contextLost === "true") {
viewerWasVisible = false;
return;
}
if (!viewerWasVisible) synchronizeViewport({ reset: true });
for (let index = 0; index < current.axes.length; index += 1) { for (let index = 0; index < current.axes.length; index += 1) {
current.axes[index] += (desired.axes[index] - current.axes[index]) * 0.055; current.axes[index] += (desired.axes[index] - current.axes[index]) * 0.055;
} }
@@ -486,10 +540,23 @@
window.addEventListener(stateEvent, onHealingState); window.addEventListener(stateEvent, onHealingState);
window.addEventListener(resetEvent, resetOrbit); window.addEventListener(resetEvent, resetOrbit);
if (window.ResizeObserver) new ResizeObserver(resize).observe(viewer); if (window.ResizeObserver) {
window.addEventListener("resize", resize); const resizeObserver = new ResizeObserver(() => synchronizeViewport());
resize(); resizeObserver.observe(viewer);
resetOrbit(); }
window.addEventListener("resize", scheduleViewportSync);
window.addEventListener("cognitive-agent:module-change", scheduleViewportSync);
canvas.addEventListener("webglcontextlost", (event) => {
event.preventDefault();
canvas.dataset.contextLost = "true";
viewer.classList.add("context-lost");
});
canvas.addEventListener("webglcontextrestored", () => {
delete canvas.dataset.contextLost;
viewer.classList.remove("context-lost");
synchronizeViewport({ reset: true });
});
synchronizeViewport({ reset: true });
animate(); animate();
window.requestAnimationFrame(() => markReady()); window.requestAnimationFrame(() => markReady());
+4 -4
View File
@@ -1,4 +1,4 @@
const CACHE_VERSION = "cognitive-agent-pad-v116"; const CACHE_VERSION = "cognitive-agent-pad-v117";
const SHELL_CACHE = `${CACHE_VERSION}-shell`; const SHELL_CACHE = `${CACHE_VERSION}-shell`;
const RUNTIME_CACHE = `${CACHE_VERSION}-runtime`; const RUNTIME_CACHE = `${CACHE_VERSION}-runtime`;
const APP_SHELL = [ const APP_SHELL = [
@@ -6,7 +6,7 @@ const APP_SHELL = [
"/index.html", "/index.html",
"/styles.css?v=dual-seat-9", "/styles.css?v=dual-seat-9",
"/network.js?v=android-pad-1", "/network.js?v=android-pad-1",
"/app.js?v=dual-seat-80", "/app.js?v=dual-seat-81",
"/gesture-control.js?v=android-pad-7", "/gesture-control.js?v=android-pad-7",
"/pwa.js?v=android-pad-1", "/pwa.js?v=android-pad-1",
"/console-v2.css?v=desktop-refactor-106", "/console-v2.css?v=desktop-refactor-106",
@@ -17,8 +17,8 @@ const APP_SHELL = [
"/self-calibration.js?v=calibration-6", "/self-calibration.js?v=calibration-6",
"/self-repair.css?v=repair-21", "/self-repair.css?v=repair-21",
"/self-repair.js?v=repair-20", "/self-repair.js?v=repair-20",
"/self-repair-seat3d.js?v=repair-18", "/self-repair-seat3d.js?v=repair-19",
"/assets/models/n7_seat.glb", "/assets/models/n7_seat.glb?v=e7f50f03",
"/manifest.webmanifest", "/manifest.webmanifest",
"/icons/app-icon-192.png", "/icons/app-icon-192.png",
"/icons/app-icon-512.png", "/icons/app-icon-512.png",
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
9136
@@ -1,61 +0,0 @@
id: k-c78dff
name: K歌模式
aliases:
- K歌模式
- K歌
description: 用户固化的长期模式配置。
icon: bookmark
scope:
vehicle_ids:
- denza_n7
operations:
- domain: ambient_light
capability: ambient_light.set_power
target_scope: vehicle
operation: activate
parameters:
state: 'on'
reason: 开启氛围灯以营造K歌场景。
- domain: ambient_light
capability: ambient_light.set_illumination
target_scope: vehicle
operation: set
parameters:
color_hex: '#B84DFF'
brightness_pct: 65
reason: 使用明快紫色和中高亮度营造活跃舞台氛围。
- domain: climate
capability: climate.set_power
target_scope: vehicle
operation: activate
parameters:
state: 'on'
reason: 空调当前关闭,需先开启以维持K歌时的座舱舒适度。
- domain: climate
capability: climate.set_temperature
target_scope: driver
operation: set
parameters:
temperature_c: 24
reason: 采用舒适且保守的座舱温度。
- domain: climate
capability: climate.set_fan_level
target_scope: vehicle
operation: set
parameters:
level: 2
reason: 采用低风量,兼顾通风并减少对歌声的干扰。
- domain: climate
capability: climate.set_ac
target_scope: vehicle
operation: activate
parameters:
state: 'on'
reason: 启用制冷除湿以维持多人演唱时的舒适度。
- domain: climate
capability: climate.set_cycle_mode
target_scope: vehicle
operation: set
parameters:
mode: internal
reason: 采用内循环以稳定座舱温度并减少外部噪声和气味。
@@ -0,0 +1,46 @@
id: mode-ceff921663
name: 躺平模式
aliases:
- 躺平模式
- 躺平
description: 用户固化的长期模式配置。
icon: bookmark
scope:
vehicle_ids:
- denza_n7
operations:
- domain: seat
capability: seat.set_longitudinal_position
target_scope: passenger
operation: set
parameters:
position_pct: 90
reason: 用户固化的座椅位置模式动作。
- domain: seat
capability: seat.set_cushion_tilt
target_scope: passenger
operation: set
parameters:
position_pct: 40
reason: 用户固化的座椅位置模式动作。
- domain: seat
capability: seat.set_backrest_recline
target_scope: passenger
operation: set
parameters:
position_pct: 80
reason: 用户固化的座椅位置模式动作。
- domain: seat
capability: seat.set_legrest_height
target_scope: passenger
operation: set
parameters:
position_pct: 75
reason: 用户固化的座椅位置模式动作。
- domain: seat
capability: seat.set_legrest_extension
target_scope: passenger
operation: set
parameters:
position_pct: 80
reason: 用户固化的座椅位置模式动作。
+16 -5
View File
@@ -243,7 +243,7 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
if path.startswith("/api/requests/"): if path.startswith("/api/requests/"):
self._serve_request(path.removeprefix("/api/requests/")) self._serve_request(path.removeprefix("/api/requests/"))
return return
self._serve_static(path) self._serve_static(path, parsed_url.query)
def do_POST(self) -> None: # noqa: N802 - stdlib handler API def do_POST(self) -> None: # noqa: N802 - stdlib handler API
path = unquote(urlparse(self.path).path) path = unquote(urlparse(self.path).path)
@@ -942,17 +942,24 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
return return
self._json(json.loads(result_path.read_text(encoding="utf-8"))) self._json(json.loads(result_path.read_text(encoding="utf-8")))
def _serve_static(self, path: str) -> None: def _serve_static(self, path: str, query: str = "") -> None:
relative_path = "index.html" if path in {"", "/"} else path.lstrip("/") relative_path = "index.html" if path in {"", "/"} else path.lstrip("/")
self._serve_static_file(relative_path) versioned = bool(parse_qs(query).get("v"))
self._serve_static_file(relative_path, versioned=versioned)
def _serve_static_file(self, relative_path: str) -> None: def _serve_static_file(self, relative_path: str, *, versioned: bool = False) -> None:
candidate = (STATIC_ROOT / relative_path).resolve() candidate = (STATIC_ROOT / relative_path).resolve()
try: try:
candidate.relative_to(STATIC_ROOT.resolve()) candidate.relative_to(STATIC_ROOT.resolve())
except ValueError: except ValueError:
self._json({"error": "not_found"}, HTTPStatus.NOT_FOUND) self._json({"error": "not_found"}, HTTPStatus.NOT_FOUND)
return return
if not candidate.is_file() and (
Path(relative_path).suffix
or relative_path.startswith(("assets/", "vendor/", "icons/"))
):
self._json({"error": "not_found"}, HTTPStatus.NOT_FOUND)
return
if not candidate.is_file(): if not candidate.is_file():
candidate = STATIC_ROOT / "index.html" candidate = STATIC_ROOT / "index.html"
content_type, _ = mimetypes.guess_type(candidate.name) content_type, _ = mimetypes.guess_type(candidate.name)
@@ -972,8 +979,12 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
) )
self.send_header("Content-Length", str(len(content))) self.send_header("Content-Length", str(len(content)))
cache_control = "no-cache" cache_control = "no-cache"
if relative_path.startswith(("vendor/", "assets/", "icons/")): if relative_path.startswith(("vendor/", "icons/")) or (
relative_path.startswith("assets/") and versioned
):
cache_control = "public, max-age=31536000, immutable" cache_control = "public, max-age=31536000, immutable"
elif relative_path.startswith("assets/"):
cache_control = "no-cache"
elif candidate.name in {"index.html", "login.html", "service-worker.js"}: elif candidate.name in {"index.html", "login.html", "service-worker.js"}:
cache_control = "no-cache" cache_control = "no-cache"
self.send_header("Cache-Control", cache_control) self.send_header("Cache-Control", cache_control)
+24 -3
View File
@@ -177,7 +177,7 @@ operations:
def test_static_console_and_runtime_endpoint(self) -> None: def test_static_console_and_runtime_endpoint(self) -> None:
with self.opener.open(f"{self.base_url}/", timeout=3) as response: with self.opener.open(f"{self.base_url}/", timeout=3) as response:
html = response.read().decode("utf-8") html = response.read().decode("utf-8")
with self.opener.open(f"{self.base_url}/app.js?v=dual-seat-80", timeout=3) as response: with self.opener.open(f"{self.base_url}/app.js?v=dual-seat-81", timeout=3) as response:
app_script = response.read().decode("utf-8") app_script = response.read().decode("utf-8")
with self.opener.open( with self.opener.open(
f"{self.base_url}/gesture-control.js?v=android-pad-7", timeout=3 f"{self.base_url}/gesture-control.js?v=android-pad-7", timeout=3
@@ -199,6 +199,10 @@ operations:
f"{self.base_url}/self-extension.css?v=extension-18", timeout=3 f"{self.base_url}/self-extension.css?v=extension-18", timeout=3
) as response: ) as response:
extension_styles = response.read().decode("utf-8").replace("\r\n", "\n") extension_styles = response.read().decode("utf-8").replace("\r\n", "\n")
with self.opener.open(
f"{self.base_url}/service-worker.js", timeout=3
) as response:
service_worker = response.read().decode("utf-8")
with self.opener.open( with self.opener.open(
f"{self.base_url}/api/extension/health", timeout=3 f"{self.base_url}/api/extension/health", timeout=3
) as response: ) as response:
@@ -219,7 +223,7 @@ operations:
self.assertIn("CMD 0x431 · FB 0x41A · CH1", html) self.assertIn("CMD 0x431 · FB 0x41A · CH1", html)
self.assertIn("CMD 0x431 · FB 0x388 · CH1", html) self.assertIn("CMD 0x431 · FB 0x388 · CH1", html)
self.assertNotIn("透明车体 · 实体设备", html) self.assertNotIn("透明车体 · 实体设备", html)
self.assertIn('src="/app.js?v=dual-seat-80"', html) self.assertIn('src="/app.js?v=dual-seat-81"', html)
self.assertIn('href="/styles.css?v=dual-seat-9"', html) self.assertIn('href="/styles.css?v=dual-seat-9"', html)
self.assertIn('src="/gesture-control.js?v=android-pad-7"', html) self.assertIn('src="/gesture-control.js?v=android-pad-7"', html)
self.assertNotIn('class="vehicle-system-hud"', html) self.assertNotIn('class="vehicle-system-hud"', html)
@@ -438,6 +442,23 @@ operations:
self.assertIn("function createReferenceFrontLampHighlights", app_script) self.assertIn("function createReferenceFrontLampHighlights", app_script)
self.assertIn("referenceFrontLampMaterials", app_script) self.assertIn("referenceFrontLampMaterials", app_script)
self.assertIn("targets.windowPositions", app_script) self.assertIn("targets.windowPositions", app_script)
self.assertIn(
'const REFERENCE_VEHICLE_ASSET_VERSION = "77a0fae7"', app_script
)
self.assertIn(
"const REFERENCE_VEHICLE_LOAD_TIMEOUT_MS = 30000", app_script
)
self.assertIn(
'referenceVehicleAssetUrl("byd-tang-2024.fbx")', app_script
)
self.assertIn("const stageHasLayout = () =>", app_script)
self.assertIn("window.requestAnimationFrame(initSeatTwin)", app_script)
self.assertIn('canvas.addEventListener("webglcontextlost"', app_script)
self.assertIn('"cognitive-agent:module-change"', app_script)
self.assertIn('const CACHE_VERSION = "cognitive-agent-pad-v117"', service_worker)
self.assertIn(
'"/assets/models/n7_seat.glb?v=e7f50f03"', service_worker
)
self.assertIn("hardware_access_required", app_script) self.assertIn("hardware_access_required", app_script)
self.assertIn('button.setAttribute("aria-label", "关闭摄像头")', gesture_script) self.assertIn('button.setAttribute("aria-label", "关闭摄像头")', gesture_script)
self.assertIn("function refreshCameraAvailability", gesture_script) self.assertIn("function refreshCameraAvailability", gesture_script)
@@ -647,7 +668,7 @@ operations:
) as response: ) as response:
extension_script = response.read().decode("utf-8") extension_script = response.read().decode("utf-8")
with self.opener.open( with self.opener.open(
f"{self.base_url}/app.js?v=dual-seat-80", timeout=3 f"{self.base_url}/app.js?v=dual-seat-81", timeout=3
) as response: ) as response:
app_script = response.read().decode("utf-8") app_script = response.read().decode("utf-8")
+16
View File
@@ -147,6 +147,18 @@ class TestSelfCalibrationApi:
model = response.read() model = response.read()
content_type = response.headers["Content-Type"] content_type = response.headers["Content-Type"]
content_security_policy = response.headers["Content-Security-Policy"] content_security_policy = response.headers["Content-Security-Policy"]
unversioned_cache_control = response.headers["Cache-Control"]
with urllib.request.urlopen(
f"{self.base_url}/assets/models/n7_seat.glb?v=e7f50f03",
timeout=3,
) as response:
versioned_model_prefix = response.read(4)
versioned_cache_control = response.headers["Cache-Control"]
with pytest.raises(urllib.error.HTTPError) as missing_model:
urllib.request.urlopen(
f"{self.base_url}/assets/models/missing-seat.glb?v=missing",
timeout=3,
)
assert 'href="/self-calibration.css?v=calibration-10"' in html assert 'href="/self-calibration.css?v=calibration-10"' in html
assert 'src="/self-calibration.js?v=calibration-6"' in html assert 'src="/self-calibration.js?v=calibration-6"' in html
@@ -173,6 +185,10 @@ class TestSelfCalibrationApi:
assert '["self-calibration", "self-repair"].includes(module)' in console_script assert '["self-calibration", "self-repair"].includes(module)' in console_script
assert 'setRole("admin", { allowModuleTransition: true })' in console_script assert 'setRole("admin", { allowModuleTransition: true })' in console_script
assert len(model) == 845_992 assert len(model) == 845_992
assert versioned_model_prefix == model[:4]
assert content_type.startswith("model/gltf-binary") assert content_type.startswith("model/gltf-binary")
assert unversioned_cache_control == "no-cache"
assert versioned_cache_control == "public, max-age=31536000, immutable"
assert missing_model.value.code == 404
assert "img-src 'self' data: blob:;" in content_security_policy assert "img-src 'self' data: blob:;" in content_security_policy
assert "connect-src 'self' blob:;" in content_security_policy assert "connect-src 'self' blob:;" in content_security_policy
+7 -2
View File
@@ -154,7 +154,7 @@ class TestSelfRepairApi:
) as response: ) as response:
styles = response.read().decode("utf-8") styles = response.read().decode("utf-8")
with urllib.request.urlopen( with urllib.request.urlopen(
f"{self.base_url}/self-repair-seat3d.js?v=repair-18", timeout=3 f"{self.base_url}/self-repair-seat3d.js?v=repair-19", timeout=3
) as response: ) as response:
seat3d = response.read().decode("utf-8") seat3d = response.read().decode("utf-8")
with urllib.request.urlopen( with urllib.request.urlopen(
@@ -168,7 +168,7 @@ class TestSelfRepairApi:
assert 'SELF-REPAIR' in script assert 'SELF-REPAIR' in script
assert 'padding: var(--v2-module-title-block) 0 0' in styles assert 'padding: var(--v2-module-title-block) 0 0' in styles
assert 'margin-inline: var(--v2-module-title-inline)' in styles assert 'margin-inline: var(--v2-module-title-inline)' in styles
assert 'src="/self-repair-seat3d.js?v=repair-18"' in html assert 'src="/self-repair-seat3d.js?v=repair-19"' in html
assert "量产故障注入" in script assert "量产故障注入" in script
assert "运行与自修复日志" in script assert "运行与自修复日志" in script
assert "启动座椅运行" in script assert "启动座椅运行" in script
@@ -203,6 +203,11 @@ class TestSelfRepairApi:
assert 'viewer.addEventListener("pointerdown"' in seat3d assert 'viewer.addEventListener("pointerdown"' in seat3d
assert 'window.addEventListener("pointermove"' in seat3d assert 'window.addEventListener("pointermove"' in seat3d
assert "window.addEventListener(resetEvent" in seat3d assert "window.addEventListener(resetEvent" in seat3d
assert "function viewerHasLayout(viewer)" in seat3d
assert "function deferSeat3dUntilVisible(viewer, options)" in seat3d
assert "if (!viewerHasLayout(viewer))" in seat3d
assert 'window.addEventListener("cognitive-agent:module-change"' in seat3d
assert 'canvas.addEventListener("webglcontextlost"' in seat3d
assert '["self-calibration", "self-repair"].includes(module)' in console_script assert '["self-calibration", "self-repair"].includes(module)' in console_script
assert 'roleSelect.disabled = ui.activeModule === "self-repair"' in console_script assert 'roleSelect.disabled = ui.activeModule === "self-repair"' in console_script
@@ -0,0 +1 @@
{"heartbeat_epoch": 1786612896.954201, "bridge_state": "RUNNING", "current_request_id": "SELF_HEAL_BENCH_1"}
@@ -0,0 +1,3 @@
BRIDGE_VERSION = 'runtime-test-v1'
def run_bridge(transport=None, stop_event=None):
stop_event.wait(2.0)
@@ -0,0 +1 @@
{"bridge_version": "runtime-test-v1", "heartbeat_epoch": 1786612902.0072305, "current_request_id": null, "system": {"firmware_revision": 15}, "firmware_identity": {"firmware_build_id": 15, "qualified": false}, "frame_epochs": {"0x301": 1786612902.0072305, "0x302": 1786612902.0072305, "0x303": 1786612902.0072305, "0x304": 1786612902.0072305, "0x305": 1786612902.0072305, "0x306": 1786612902.0072305, "0x307": 1786612902.0072305}}
@@ -0,0 +1 @@
{"bridge_version": "runtime-test-v1", "heartbeat_epoch": 1786612902.1554296, "current_request_id": null, "system": {"firmware_revision": 15}, "firmware_identity": {"firmware_build_id": 16, "qualified": true}, "frame_epochs": {"0x301": 1786612902.1554296, "0x302": 1786612902.1554296, "0x303": 1786612902.1554296, "0x304": 1786612902.1554296, "0x305": 1786612902.1554296, "0x306": 1786612902.1554296, "0x307": 1786612902.1554296}}
@@ -0,0 +1 @@
{"heartbeat_epoch": 1786614297.8563914, "bridge_state": "RUNNING", "current_request_id": "SELF_HEAL_BENCH_1"}
@@ -0,0 +1,3 @@
BRIDGE_VERSION = 'runtime-test-v1'
def run_bridge(transport=None, stop_event=None):
stop_event.wait(2.0)
@@ -0,0 +1 @@
{"bridge_version": "runtime-test-v1", "heartbeat_epoch": 1786614302.9093528, "current_request_id": null, "system": {"firmware_revision": 15}, "firmware_identity": {"firmware_build_id": 15, "qualified": false}, "frame_epochs": {"0x301": 1786614302.9093528, "0x302": 1786614302.9093528, "0x303": 1786614302.9093528, "0x304": 1786614302.9093528, "0x305": 1786614302.9093528, "0x306": 1786614302.9093528, "0x307": 1786614302.9093528}}
@@ -0,0 +1 @@
{"bridge_version": "runtime-test-v1", "heartbeat_epoch": 1786614303.057585, "current_request_id": null, "system": {"firmware_revision": 15}, "firmware_identity": {"firmware_build_id": 16, "qualified": true}, "frame_epochs": {"0x301": 1786614303.057585, "0x302": 1786614303.057585, "0x303": 1786614303.057585, "0x304": 1786614303.057585, "0x305": 1786614303.057585, "0x306": 1786614303.057585, "0x307": 1786614303.057585}}
@@ -0,0 +1 @@
{"heartbeat_epoch": 1786614211.5981486, "bridge_state": "RUNNING", "current_request_id": "SELF_HEAL_BENCH_1"}
@@ -0,0 +1,3 @@
BRIDGE_VERSION = 'runtime-test-v1'
def run_bridge(transport=None, stop_event=None):
stop_event.wait(2.0)
@@ -0,0 +1 @@
{"bridge_version": "runtime-test-v1", "heartbeat_epoch": 1786614216.6407876, "current_request_id": null, "system": {"firmware_revision": 15}, "firmware_identity": {"firmware_build_id": 15, "qualified": false}, "frame_epochs": {"0x301": 1786614216.6407876, "0x302": 1786614216.6407876, "0x303": 1786614216.6407876, "0x304": 1786614216.6407876, "0x305": 1786614216.6407876, "0x306": 1786614216.6407876, "0x307": 1786614216.6407876}}
@@ -0,0 +1 @@
{"bridge_version": "runtime-test-v1", "heartbeat_epoch": 1786614216.7809346, "current_request_id": null, "system": {"firmware_revision": 15}, "firmware_identity": {"firmware_build_id": 16, "qualified": true}, "frame_epochs": {"0x301": 1786614216.7809346, "0x302": 1786614216.7809346, "0x303": 1786614216.7809346, "0x304": 1786614216.7809346, "0x305": 1786614216.7809346, "0x306": 1786614216.7809346, "0x307": 1786614216.7809346}}
@@ -0,0 +1 @@
{"heartbeat_epoch": 1786628979.608127, "bridge_state": "RUNNING", "current_request_id": "SELF_HEAL_BENCH_1"}
@@ -0,0 +1,3 @@
BRIDGE_VERSION = 'runtime-test-v1'
def run_bridge(transport=None, stop_event=None):
stop_event.wait(2.0)
@@ -0,0 +1 @@
{"bridge_version": "runtime-test-v1", "heartbeat_epoch": 1786628984.6589825, "current_request_id": null, "system": {"firmware_revision": 15}, "firmware_identity": {"firmware_build_id": 15, "qualified": false}, "frame_epochs": {"0x301": 1786628984.6589825, "0x302": 1786628984.6589825, "0x303": 1786628984.6589825, "0x304": 1786628984.6589825, "0x305": 1786628984.6589825, "0x306": 1786628984.6589825, "0x307": 1786628984.6589825}}
@@ -0,0 +1 @@
{"bridge_version": "runtime-test-v1", "heartbeat_epoch": 1786628984.8052018, "current_request_id": null, "system": {"firmware_revision": 15}, "firmware_identity": {"firmware_build_id": 16, "qualified": true}, "frame_epochs": {"0x301": 1786628984.8052018, "0x302": 1786628984.8052018, "0x303": 1786628984.8052018, "0x304": 1786628984.8052018, "0x305": 1786628984.8052018, "0x306": 1786628984.8052018, "0x307": 1786628984.8052018}}
@@ -0,0 +1 @@
{"heartbeat_epoch": 1786608369.3573637, "bridge_state": "RUNNING", "current_request_id": "SELF_HEAL_BENCH_1"}
@@ -0,0 +1,3 @@
BRIDGE_VERSION = 'runtime-test-v1'
def run_bridge(transport=None, stop_event=None):
stop_event.wait(2.0)
@@ -0,0 +1 @@
{"bridge_version": "runtime-test-v1", "heartbeat_epoch": 1786608374.4077742, "current_request_id": null, "system": {"firmware_revision": 15}, "firmware_identity": {"firmware_build_id": 15, "qualified": false}, "frame_epochs": {"0x301": 1786608374.4077742, "0x302": 1786608374.4077742, "0x303": 1786608374.4077742, "0x304": 1786608374.4077742, "0x305": 1786608374.4077742, "0x306": 1786608374.4077742, "0x307": 1786608374.4077742}}
@@ -0,0 +1 @@
{"bridge_version": "runtime-test-v1", "heartbeat_epoch": 1786608374.5544136, "current_request_id": null, "system": {"firmware_revision": 15}, "firmware_identity": {"firmware_build_id": 16, "qualified": true}, "frame_epochs": {"0x301": 1786608374.5544136, "0x302": 1786608374.5544136, "0x303": 1786608374.5544136, "0x304": 1786608374.5544136, "0x305": 1786608374.5544136, "0x306": 1786608374.5544136, "0x307": 1786608374.5544136}}
@@ -0,0 +1 @@
{"heartbeat_epoch": 1786607635.6544752, "bridge_state": "RUNNING", "current_request_id": "SELF_HEAL_BENCH_1"}
@@ -0,0 +1,3 @@
BRIDGE_VERSION = 'runtime-test-v1'
def run_bridge(transport=None, stop_event=None):
stop_event.wait(2.0)
@@ -0,0 +1 @@
{"bridge_version": "runtime-test-v1", "heartbeat_epoch": 1786607640.7259998, "current_request_id": null, "system": {"firmware_revision": 15}, "firmware_identity": {"firmware_build_id": 15, "qualified": false}, "frame_epochs": {"0x301": 1786607640.7259998, "0x302": 1786607640.7259998, "0x303": 1786607640.7259998, "0x304": 1786607640.7259998, "0x305": 1786607640.7259998, "0x306": 1786607640.7259998, "0x307": 1786607640.7259998}}
@@ -0,0 +1 @@
{"bridge_version": "runtime-test-v1", "heartbeat_epoch": 1786607640.8686898, "current_request_id": null, "system": {"firmware_revision": 15}, "firmware_identity": {"firmware_build_id": 16, "qualified": true}, "frame_epochs": {"0x301": 1786607640.8686898, "0x302": 1786607640.8686898, "0x303": 1786607640.8686898, "0x304": 1786607640.8686898, "0x305": 1786607640.8686898, "0x306": 1786607640.8686898, "0x307": 1786607640.8686898}}