feat(n7web): add pet settings and day night themes

This commit is contained in:
Miayui
2026-09-14 17:12:08 +08:00
parent aa6f59d3a6
commit 58f1a2da42
13 changed files with 901 additions and 114 deletions
+161 -15
View File
@@ -211,6 +211,60 @@ function refreshIcons() {
}
}
const GLASS_THEME_STORAGE_KEY = "cognitive-agent.glass-theme";
function currentGlassTheme() {
return document.documentElement.dataset.glassTheme === "light" ? "light" : "dark";
}
function applyGlassTheme(theme, { persist = false, announce = true } = {}) {
const normalized = theme === "light" ? "light" : "dark";
document.documentElement.dataset.glassTheme = normalized;
document.documentElement.style.colorScheme = normalized;
const toggle = $("#glassThemeToggle");
const nextLabel = normalized === "dark" ? "切换到白日模式" : "切换到夜间模式";
if (toggle) {
toggle.dataset.theme = normalized;
toggle.setAttribute("aria-label", nextLabel);
toggle.setAttribute("aria-pressed", String(normalized === "dark"));
toggle.title = nextLabel;
}
const themeColor = $("#themeColorMeta");
if (themeColor) {
themeColor.setAttribute("content", normalized === "dark" ? "#17182b" : "#eef7ff");
}
if (persist) {
try {
localStorage.setItem(GLASS_THEME_STORAGE_KEY, normalized);
} catch (_error) {
// Keep the in-memory theme active if storage is unavailable.
}
try {
document.cookie = `${GLASS_THEME_STORAGE_KEY}=${normalized}; Path=/; Max-Age=31536000; SameSite=Lax`;
} catch (_error) {
// Cookie fallback can also be disabled; theme switching remains usable.
}
}
if (announce) {
window.dispatchEvent(new CustomEvent("cognitive-agent:theme-change", {
detail: { theme: normalized },
}));
}
return normalized;
}
function initGlassThemeToggle() {
const toggle = $("#glassThemeToggle");
if (!toggle || toggle.dataset.themeToggleBound === "true") return;
toggle.dataset.themeToggleBound = "true";
applyGlassTheme(currentGlassTheme(), { announce: false });
toggle.addEventListener("click", () => {
applyGlassTheme(currentGlassTheme() === "dark" ? "light" : "dark", {
persist: true,
});
});
}
const TWIN_DEFAULT_POSITIONS = {
longitudinal: 50,
cushion_tilt: 50,
@@ -1566,6 +1620,15 @@ function createTwinCabin(THREE, scene) {
root: cabin,
fallbackExteriorObjects,
fallbackInteriorObjects,
themeMaterials: {
vehicleBody: [shellMaterial, bodyPanelMaterial],
vehicleDetail: [softTrim, consoleTop],
vehicleInterior: [trim, cockpit],
vehicleTrim: [exteriorTrim, tireMaterial],
vehicleMetal: [rimMaterial],
vehicleOutline: [shellEdge],
glass: [glassMaterial, greenhouseMaterial],
},
shellMaterial,
glassMaterial,
referenceWindowPanes: {
@@ -1642,7 +1705,7 @@ function createTwinCabin(THREE, scene) {
}
const TWIN_THEME_PALETTES = {
user: {
dark: {
accent: 0x7b70f5,
accentStrong: 0x5d4dd9,
accentSoft: 0x9ba8ff,
@@ -1675,8 +1738,49 @@ const TWIN_THEME_PALETTES = {
lampEmissive: 0x55acf4,
lampEdge: 0x3476b9,
},
light: {
accent: 0x2f80ed,
accentStrong: 0x1d5fbf,
accentSoft: 0xdbeeff,
sceneBackground: 0xeff6ff,
floor: 0xe5f0fd,
gridStrong: 0x75a9e8,
gridSoft: 0xc7daf2,
fillLight: 0xb7d9ff,
vehicleBody: 0x91c6f5,
vehicleDetail: 0x77a9d8,
vehicleInterior: 0x4d79a6,
vehicleTrim: 0x284c70,
vehicleMetal: 0x93b9da,
vehicleOutline: 0x2879d8,
vehicleOccluded: 0x9abbdc,
seatShell: 0x2b76c9,
seatInsert: 0xcfe7ff,
seatThermal: 0x72b5f5,
seatBolster: 0x1b5fae,
seatStructure: 0x25476e,
seatEdge: 0x69b4ff,
seatSeam: 0xe4f2ff,
glass: 0x4b9bea,
glassEmissive: 0x164f91,
glassEdge: 0x1b6fc4,
glassGlow: 0x79c7ff,
roofClear: 0x67aef4,
roofDark: 0x183b70,
lamp: 0x2e7fd4,
lampEmissive: 0x48a4f2,
lampEdge: 0x1e6bb8,
},
};
function currentTwinThemeRole() {
return currentGlassTheme();
}
function currentTwinThemePalette() {
return TWIN_THEME_PALETTES[currentTwinThemeRole()];
}
const TWIN_DEVICE_HOTSPOT_LAYOUT = Object.freeze({
windows: { normalized: [0.08, 0.62, 0.54], labelOffset: [-115, 0] },
shade: { normalized: [0.5, 0.94, 0.43], labelOffset: [0, -65] },
@@ -1783,11 +1887,17 @@ function applyTwinGroundTheme(twin, palette) {
if (previousTexture && previousTexture !== nextTexture) previousTexture.dispose();
}
function applyTwinVehicleTheme(systems) {
function applyTwinVehicleTheme(systems, palette = currentTwinThemePalette()) {
if (!systems) return;
const palette = TWIN_THEME_PALETTES.user;
systems.themeRole = "user";
systems.themeRole = currentTwinThemeRole();
systems.themePalette = palette;
Object.entries(systems.themeMaterials || {}).forEach(([token, materials]) => {
const color = palette[token];
if (color === undefined) return;
materials.forEach((material) => {
if (material?.color) material.color.setHex(color);
});
});
systems.colors.roofClear.setHex(palette.roofClear);
systems.colors.roofDark.setHex(palette.roofDark);
@@ -1871,15 +1981,32 @@ function applyTwinSeatTheme(model, palette) {
});
}
function applyTwinReferenceVehicleTheme(root, palette) {
if (!root) return;
root.traverse((object) => {
if (!object.isMesh) return;
const materials = Array.isArray(object.material) ? object.material : [object.material];
materials.filter(Boolean).forEach((material) => {
const token = material.userData?.twinThemeToken;
if (token && palette[token] !== undefined && material.color) {
material.color.setHex(palette[token]);
material.needsUpdate = true;
}
});
});
}
function applyTwinSceneTheme(twin) {
if (!twin) return;
const palette = TWIN_THEME_PALETTES.user;
applyTwinVehicleTheme(twin.vehicleSystems);
const role = currentTwinThemeRole();
const palette = currentTwinThemePalette();
applyTwinVehicleTheme(twin.vehicleSystems, palette);
twin.scene.background.setHex(palette.sceneBackground);
if (twin.scene.fog?.color) twin.scene.fog.color.setHex(palette.sceneBackground);
applyTwinGroundTheme(twin, palette);
if (twin.fillLight?.color) twin.fillLight.color.setHex(palette.fillLight);
Object.values(twin.models || {}).forEach((model) => applyTwinSeatTheme(model, palette));
applyTwinReferenceVehicleTheme(twin.referenceVehicle, palette);
[twin.vehicleSystems?.root, twin.referenceVehicle, twin.fixedModels?.rear]
.filter(Boolean)
.forEach((root) => root.traverse((object) => {
@@ -1892,10 +2019,10 @@ function applyTwinSceneTheme(twin) {
}));
// Controllable glass and lamp entities use stronger semantic variants from
// the same palette after the neutral vehicle outline pass above.
applyTwinVehicleTheme(twin.vehicleSystems);
applyTwinVehicleTheme(twin.vehicleSystems, palette);
const canvas = twin.renderer?.domElement;
if (canvas) {
canvas.dataset.themeRole = "user";
canvas.dataset.themeRole = role;
canvas.dataset.themeAccent = `#${palette.accent.toString(16).padStart(6, "0")}`;
}
}
@@ -1980,7 +2107,7 @@ function updateTwinVehicleVisual(systems, timestamp) {
: (fogStrength * 0.72 > mainLampStrength ? 0xfff0b3 : (isHighBeam ? 0xeafaff : 0xcdefff));
const updateFrontLampMaterial = (material, isReference = false) => {
const isLit = lampStrength > 0.02;
const palette = systems.themePalette || TWIN_THEME_PALETTES.user;
const palette = systems.themePalette || currentTwinThemePalette();
if (material.color) material.color.setHex(isLit ? lampColor : palette.lamp);
if (material.emissive) material.emissive.setHex(isLit ? lampColor : palette.lampEmissive);
if ("emissiveIntensity" in material) {
@@ -2001,7 +2128,7 @@ function updateTwinVehicleVisual(systems, timestamp) {
activeOpacity,
isHalo,
}) => {
const palette = systems.themePalette || TWIN_THEME_PALETTES.user;
const palette = systems.themePalette || currentTwinThemePalette();
const idleColor = isHalo ? palette.glassGlow : palette.lamp;
material.color.setHex(lampStrength > 0.02 ? lampColor : idleColor);
material.opacity = idleOpacity + lampStrength * (activeOpacity - idleOpacity);
@@ -3000,7 +3127,7 @@ function twinDeviceWorldAnchor(THREE, twin, deviceId, target) {
}
function configureReferenceVehicleModel(THREE, vehicle) {
const palette = TWIN_THEME_PALETTES.user;
const palette = currentTwinThemePalette();
const materialCache = new Map();
const report = { meshes: 0, materials: new Map(), nodes: [] };
vehicle.traverse((object) => {
@@ -3016,6 +3143,7 @@ function configureReferenceVehicleModel(THREE, vehicle) {
const material = sourceMaterial.clone();
const descriptor = referenceMaterialDescriptor(material);
const category = classifyReferenceMaterial(material);
let themeToken = "vehicleDetail";
const outlineStyle = TWIN_OUTLINE_STYLE[category] || TWIN_OUTLINE_STYLE.detail;
const key = `${category}:${material.name || "unnamed"}`;
report.materials.set(key, (report.materials.get(key) || 0) + 1);
@@ -3035,6 +3163,7 @@ function configureReferenceVehicleModel(THREE, vehicle) {
if ("emissiveIntensity" in material) material.emissiveIntensity = 0;
}
if (category === "glass") {
themeToken = "glass";
if (material.color) material.color.setHex(palette.glass);
material.transparent = true;
material.opacity = 0.3;
@@ -3043,25 +3172,37 @@ function configureReferenceVehicleModel(THREE, vehicle) {
object.castShadow = false;
object.renderOrder = 12;
} else if (category === "body") {
themeToken = "vehicleBody";
if (material.color) material.color.setHex(palette.vehicleBody);
if ("shininess" in material) material.shininess = 72;
} else if (category === "light") {
themeToken = "lamp";
material.map = null;
if (material.color) material.color.setHex(palette.lamp);
if (material.emissive) material.emissive.setHex(0x000000);
if ("emissiveIntensity" in material) material.emissiveIntensity = 0;
} else if (category === "interior" && material.color) {
themeToken = "vehicleInterior";
material.color.setHex(palette.vehicleInterior);
} else if (/lines|\badd\b/.test(descriptor) && material.color) {
themeToken = "vehicleTrim";
material.map = null;
material.color.setHex(palette.vehicleTrim);
} else if (/map[_ -]?[abc]/.test(descriptor) && material.color) {
if (/chrome|metal/.test(descriptor)) material.color.setHex(palette.vehicleMetal);
else if (/plastic/.test(descriptor)) material.color.setHex(palette.vehicleTrim);
else material.color.setHex(palette.vehicleDetail);
if (/chrome|metal/.test(descriptor)) {
themeToken = "vehicleMetal";
material.color.setHex(palette.vehicleMetal);
} else if (/plastic/.test(descriptor)) {
themeToken = "vehicleTrim";
material.color.setHex(palette.vehicleTrim);
} else {
material.color.setHex(palette.vehicleDetail);
}
} else if (/plastic|under|black/.test(descriptor) && material.color) {
themeToken = "vehicleTrim";
material.color.setHex(palette.vehicleTrim);
} else if (/chrome|metal|alloy|rim/.test(descriptor) && material.color) {
themeToken = "vehicleMetal";
material.color.setHex(palette.vehicleMetal);
}
material.transparent = true;
@@ -3075,6 +3216,7 @@ function configureReferenceVehicleModel(THREE, vehicle) {
if ("metalness" in material) material.metalness = category === "wheel" ? 0.48 : 0.08;
if (material.emissive) material.emissive.setHex(0x000000);
if ("emissiveIntensity" in material) material.emissiveIntensity = 0;
material.userData = { ...(material.userData || {}), twinThemeToken: themeToken };
material.needsUpdate = true;
materialCache.set(sourceMaterial, material);
return material;
@@ -3945,7 +4087,7 @@ function initSeatTwin() {
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
const scene = new THREE.Scene();
const twinPalette = TWIN_THEME_PALETTES.user;
const twinPalette = currentTwinThemePalette();
scene.background = new THREE.Color(twinPalette.sceneBackground);
scene.fog = new THREE.Fog(twinPalette.sceneBackground, 12, 19);
const camera = new THREE.PerspectiveCamera(34, 1, 0.1, 40);
@@ -4081,6 +4223,9 @@ function initSeatTwin() {
window.addEventListener("cognitive-agent:module-change", () => {
applyTwinSceneTheme(state.seatTwin);
});
window.addEventListener("cognitive-agent:theme-change", () => {
applyTwinSceneTheme(state.seatTwin);
});
canvas.dataset.referenceModelState = "loading";
loadReferenceVehicleModel(THREE, scene, vehicleSystems, fallback)
@@ -8866,6 +9011,7 @@ window.addEventListener("cognitive-agent:request-complete", (event) => {
});
document.addEventListener("DOMContentLoaded", async () => {
initGlassThemeToggle();
initJointTwinLayout();
resetVehicleStatusDisplay();
initResultWorkspace();