feat(n7web): add pet settings and day night themes
This commit is contained in:
+161
-15
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user