update model
This commit is contained in:
+183
-67
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user