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_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) {
const textureNames = [
@@ -3643,73 +3650,92 @@ function loadReferenceVehicleModel(THREE, scene, vehicleSystems, fallback) {
reject(new Error("FBXLoader is unavailable"));
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();
manager.setURLModifier((url) => {
const cleanUrl = String(url).replaceAll("\\", "/");
const cleanUrl = String(url).replaceAll("\\", "/").split(/[?#]/, 1)[0];
const fileName = decodeURIComponent(cleanUrl.split("/").pop() || "");
if (!/\.(png|jpe?g)$/i.test(fileName)) return url;
return `${REFERENCE_VEHICLE_ROOT}${encodeURIComponent(fileName)}`;
return referenceVehicleAssetUrl(fileName);
});
const loader = new FBXLoaderClass(manager);
loader.setResourcePath(REFERENCE_VEHICLE_ROOT);
loader.load(
REFERENCE_VEHICLE_URL,
(vehicle) => {
const normalized = normalizeReferenceVehicle(THREE, vehicle);
const driverCushion = measureReferenceDriverCushion(THREE, vehicle);
const passengerCushion = measureReferencePassengerCushion(THREE, vehicle);
const removedFrontSeatTriangles = carveReferenceFrontSeats(THREE, vehicle);
const referenceWindowPanes = extractReferenceWindowPanes(THREE, vehicle);
const report = configureReferenceVehicleModel(THREE, vehicle);
styleReferencePassiveGlass(THREE, vehicle);
const passiveRearExterior = styleReferencePassiveRearExterior(THREE, vehicle);
const referenceFrontLampMeshes = collectReferenceFrontLampMeshes(vehicle);
emphasizeReferenceWindowPanes(THREE, referenceWindowPanes);
const referenceFrontLampMaterials = emphasizeReferenceFrontLampMeshes(
THREE,
referenceFrontLampMeshes,
);
const referenceFrontLampHighlights = createReferenceFrontLampHighlights(
THREE,
referenceFrontLampMeshes,
);
const referenceFrontLampLensOutlines = createReferenceFrontLampLensOutlines(
THREE,
vehicle,
);
vehicleSystems.referenceWindowPanes = referenceWindowPanes;
vehicleSystems.referenceFrontLampMaterials = referenceFrontLampMaterials;
vehicleSystems.referenceFrontLampMeshes = referenceFrontLampMeshes;
vehicleSystems.referenceFrontLampHighlights = referenceFrontLampHighlights;
vehicleSystems.referenceFrontLampLensOutlines = referenceFrontLampLensOutlines;
applyTwinVehicleTheme(vehicleSystems);
vehicle.name = "REFERENCE_VEHICLE_OUTLINE";
vehicle.userData.twinRole = "reference_vehicle";
vehicle.traverse((object) => object.layers.set(1));
scene.add(vehicle);
vehicleSystems.fallbackExteriorObjects.forEach((object) => {
object.visible = false;
});
vehicleSystems.fallbackInteriorObjects.forEach((object) => {
object.visible = false;
});
console.info("[vehicle-twin] reference vehicle model loaded", JSON.stringify({
meshes: report.meshes,
size: normalized.size.toArray().map((value) => Number(value.toFixed(3))),
removedFrontSeatTriangles,
windowPanes: Object.fromEntries(
Object.entries(referenceWindowPanes).map(([key, value]) => [key, value.length]),
),
frontLampMaterials: referenceFrontLampMaterials.length,
frontLampMeshes: referenceFrontLampMeshes.length,
frontLampHighlights: referenceFrontLampHighlights.length,
frontLampLensOutlines: referenceFrontLampLensOutlines.length,
rearLampMeshes: passiveRearExterior.rearLampMeshes.length,
rearBumperMeshes: passiveRearExterior.rearBumperMeshes.length,
driverCushion,
passengerCushion,
}));
resolve({ vehicle, report, normalized, driverCushion, passengerCushion });
if (settled) return;
try {
const normalized = normalizeReferenceVehicle(THREE, vehicle);
const driverCushion = measureReferenceDriverCushion(THREE, vehicle);
const passengerCushion = measureReferencePassengerCushion(THREE, vehicle);
const removedFrontSeatTriangles = carveReferenceFrontSeats(THREE, vehicle);
const referenceWindowPanes = extractReferenceWindowPanes(THREE, vehicle);
const report = configureReferenceVehicleModel(THREE, vehicle);
styleReferencePassiveGlass(THREE, vehicle);
const passiveRearExterior = styleReferencePassiveRearExterior(THREE, vehicle);
const referenceFrontLampMeshes = collectReferenceFrontLampMeshes(vehicle);
emphasizeReferenceWindowPanes(THREE, referenceWindowPanes);
const referenceFrontLampMaterials = emphasizeReferenceFrontLampMeshes(
THREE,
referenceFrontLampMeshes,
);
const referenceFrontLampHighlights = createReferenceFrontLampHighlights(
THREE,
referenceFrontLampMeshes,
);
const referenceFrontLampLensOutlines = createReferenceFrontLampLensOutlines(
THREE,
vehicle,
);
vehicleSystems.referenceWindowPanes = referenceWindowPanes;
vehicleSystems.referenceFrontLampMaterials = referenceFrontLampMaterials;
vehicleSystems.referenceFrontLampMeshes = referenceFrontLampMeshes;
vehicleSystems.referenceFrontLampHighlights = referenceFrontLampHighlights;
vehicleSystems.referenceFrontLampLensOutlines = referenceFrontLampLensOutlines;
applyTwinVehicleTheme(vehicleSystems);
vehicle.name = "REFERENCE_VEHICLE_OUTLINE";
vehicle.userData.twinRole = "reference_vehicle";
vehicle.traverse((object) => object.layers.set(1));
scene.add(vehicle);
vehicleSystems.fallbackExteriorObjects.forEach((object) => {
object.visible = false;
});
vehicleSystems.fallbackInteriorObjects.forEach((object) => {
object.visible = false;
});
console.info("[vehicle-twin] reference vehicle model loaded", JSON.stringify({
meshes: report.meshes,
size: normalized.size.toArray().map((value) => Number(value.toFixed(3))),
removedFrontSeatTriangles,
windowPanes: Object.fromEntries(
Object.entries(referenceWindowPanes).map(([key, value]) => [key, value.length]),
),
frontLampMaterials: referenceFrontLampMaterials.length,
frontLampMeshes: referenceFrontLampMeshes.length,
frontLampHighlights: referenceFrontLampHighlights.length,
frontLampLensOutlines: referenceFrontLampLensOutlines.length,
rearLampMeshes: passiveRearExterior.rearLampMeshes.length,
rearBumperMeshes: passiveRearExterior.rearBumperMeshes.length,
driverCushion,
passengerCushion,
}));
settle(resolve, { vehicle, report, normalized, driverCushion, passengerCushion });
} catch (error) {
settle(reject, error);
}
},
(event) => {
if (!fallback || !event.total) return;
@@ -3717,7 +3743,7 @@ function loadReferenceVehicleModel(THREE, scene, vehicleSystems, fallback) {
const label = fallback.querySelector("span");
if (label) label.textContent = `正在加载整车参考模型 · ${progress}%`;
},
reject,
(error) => settle(reject, error),
);
});
}
@@ -3726,7 +3752,33 @@ function initSeatTwin() {
const canvas = $("#seatTwinCanvas");
const stage = $("#seatTwin");
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) {
fallback.classList.add("error");
fallback.innerHTML = '<i data-lucide="triangle-alert"></i><span>3D 引擎加载失败,精确状态仍可用</span>';
@@ -3735,7 +3787,16 @@ function initSeatTwin() {
}
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.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
if ("outputColorSpace" in renderer) renderer.outputColorSpace = THREE.SRGBColorSpace;
@@ -3883,6 +3944,7 @@ function initSeatTwin() {
applyTwinSceneTheme(state.seatTwin);
});
canvas.dataset.referenceModelState = "loading";
loadReferenceVehicleModel(THREE, scene, vehicleSystems, fallback)
.then(({ vehicle, report, normalized, driverCushion, passengerCushion }) => {
if (!state.seatTwin) return;
@@ -3920,11 +3982,12 @@ function initSeatTwin() {
.toArray()
.map((value) => value.toFixed(2))
.join("x");
canvas.dataset.referenceModelLoaded = "true";
canvas.dataset.referenceModelState = "loaded";
fallback.hidden = true;
})
.catch((error) => {
console.error("[vehicle-twin] failed to load reference vehicle model", error);
canvas.dataset.referenceModelState = "failed";
fallback.classList.add("error");
fallback.innerHTML = '<i data-lucide="triangle-alert"></i><span>实车模型加载失败,已切换轻量回退模型</span>';
refreshIcons();
@@ -3940,17 +4003,64 @@ function initSeatTwin() {
});
}
let stageWasVisible = false;
function resize() {
const width = Math.max(1, canvas.clientWidth);
const height = Math.max(1, canvas.clientHeight);
const width = Math.max(1, Math.round(stage.clientWidth));
const height = Math.max(1, Math.round(stage.clientHeight));
renderer.setSize(width, height, false);
camera.aspect = width / height;
camera.updateProjectionMatrix();
state.seatTwin.narrowViewport = width < 600;
}
new ResizeObserver(resize).observe(stage);
resize();
function synchronizeTwinViewport({ reset = false } = {}) {
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) => {
state.seatTwin.dragging = true;
state.seatTwin.pointer = { x: event.clientX, y: event.clientY };
@@ -4052,6 +4162,12 @@ function initSeatTwin() {
function animate(timestamp = 0) {
const twin = state.seatTwin;
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.keys(model.positions).forEach((axis) => {
const delta = model.targets[axis] - model.positions[axis];
@@ -6556,7 +6672,7 @@ document.addEventListener("DOMContentLoaded", async () => {
initJointTwinLayout();
initResultWorkspace();
refreshIcons();
initSeatTwin();
window.requestAnimationFrame(initSeatTwin);
$("#requestForm").addEventListener("submit", submitRequest);
$("#utterance").addEventListener("keydown", (event) => {
if (event.key === "Enter" && !event.shiftKey && !event.isComposing) {