(function () { "use strict"; function init() { const THREE = window.THREE; const canvas = document.getElementById("repairSeatCanvas"); const viewer = document.getElementById("repairSeatViewer"); if (!canvas || !viewer || viewer.dataset.seat3dInitialized === "true") return; viewer.dataset.seat3dInitialized = "true"; const buildFallback = () => { const fallback = document.createElement("div"); fallback.className = "v2-repair-seat-fallback"; fallback.setAttribute("aria-hidden", "true"); fallback.innerHTML = `
`; viewer.insertBefore(fallback, canvas.nextSibling); window.addEventListener("seat-healing-state", (event) => { const detail = event.detail || {}; const axes = new Map((detail.axes || []).map((axis) => [Number(axis.motor), Number(axis.position_pct || 0)])); fallback.style.setProperty("--repair-slide", `${(50 - (axes.get(0) ?? 35)) * 0.18}px`); fallback.style.setProperty("--repair-lift", `${((axes.get(1) ?? 42) - 50) * 0.1}px`); fallback.style.setProperty("--repair-back", `${-8 - (axes.get(2) ?? 48) * 0.24}deg`); fallback.style.setProperty("--repair-leg", `${-5 + (axes.get(3) ?? 20) * 0.58}deg`); fallback.classList.toggle("heating", Number(detail.heatingLevel || 0) > 0); fallback.classList.toggle("ventilating", Boolean(detail.ventilation)); fallback.classList.toggle("active", !["IDLE", "PASSED"].includes(detail.state)); fallback.classList.toggle("failed", detail.state === "FAILED"); }); viewer.classList.add("ready", "fallback"); }; if (!THREE) { buildFallback(); return; } const themeProbe = document.createElement("div"); themeProbe.className = "console-v2"; themeProbe.dataset.demoRole = "admin"; themeProbe.style.display = "none"; document.body.append(themeProbe); const consoleStyles = window.getComputedStyle(themeProbe); const cssThemeColor = (name, fallback) => { const value = consoleStyles?.getPropertyValue(name).trim() || fallback; try { return new THREE.Color(value); } catch (_error) { return new THREE.Color(fallback); } }; const white = new THREE.Color(0xffffff); const black = new THREE.Color(0x000000); const themeAccent = cssThemeColor("--v2-accent", "#315eaa"); const themeStrong = cssThemeColor("--v2-accent-strong", "#234785"); const themeSoft = cssThemeColor("--v2-accent-soft", "#e1e9fa"); themeProbe.remove(); const mixHex = (from, to, amount) => from.clone().lerp(to, amount).getHex(); const scene = new THREE.Scene(); scene.fog = new THREE.Fog(mixHex(themeSoft, white, 0.12), 8, 15); let renderer; try { renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true, powerPreference: "high-performance", }); } catch (_error) { buildFallback(); return; } renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2)); renderer.outputEncoding = THREE.sRGBEncoding; renderer.toneMapping = THREE.ACESFilmicToneMapping; renderer.toneMappingExposure = 0.92; renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFSoftShadowMap; const camera = new THREE.PerspectiveCamera(33, 1, 0.05, 40); const target = new THREE.Vector3(0, 1.15, 0); const orbit = { yaw: 0.67, pitch: 0.21, distance: 6.8 }; const pointer = { active: false, x: 0, y: 0 }; scene.add(new THREE.HemisphereLight(0xffffff, mixHex(themeStrong, white, 0.46), 1.35)); const keyLight = new THREE.DirectionalLight(0xffffff, 1.8); keyLight.position.set(-4, 7, 6); keyLight.castShadow = true; keyLight.shadow.mapSize.set(1024, 1024); scene.add(keyLight); const fillLight = new THREE.DirectionalLight(mixHex(themeAccent, white, 0.42), 0.72); fillLight.position.set(5, 3, -4); scene.add(fillLight); const floor = new THREE.Mesh( new THREE.CircleGeometry(4.8, 80), new THREE.MeshStandardMaterial({ color: mixHex(themeSoft, white, 0.18), roughness: 0.94 }), ); floor.rotation.x = -Math.PI / 2; floor.position.y = -0.16; floor.receiveShadow = true; scene.add(floor); const grid = new THREE.GridHelper(8, 20, themeAccent.getHex(), mixHex(themeAccent, white, 0.7)); grid.position.y = -0.153; grid.material.transparent = true; grid.material.opacity = 0.14; scene.add(grid); const seatShell = new THREE.MeshPhysicalMaterial({ color: mixHex(themeStrong, black, 0.06), roughness: 0.54, metalness: 0.02, clearcoat: 0.16, clearcoatRoughness: 0.7, emissive: mixHex(themeStrong, black, 0.64), emissiveIntensity: 0.08, }); const seatInsert = new THREE.MeshPhysicalMaterial({ color: mixHex(themeAccent, white, 0.48), roughness: 0.72, metalness: 0, clearcoat: 0.06, emissive: 0x000000, emissiveIntensity: 0, }); const thermalInsert = seatInsert.clone(); const bolsterMaterial = new THREE.MeshStandardMaterial({ color: themeAccent.getHex(), roughness: 0.76, emissive: 0x000000, emissiveIntensity: 0, }); const structureMaterial = new THREE.MeshStandardMaterial({ color: mixHex(themeStrong, black, 0.34), roughness: 0.62, metalness: 0.35, }); const railMaterial = new THREE.MeshStandardMaterial({ color: 0x7b8798, roughness: 0.42, metalness: 0.72 }); const edgeMaterial = new THREE.LineBasicMaterial({ color: mixHex(themeStrong, black, 0.38), transparent: true, opacity: 0.3, }); const seamMaterial = new THREE.LineBasicMaterial({ color: mixHex(themeAccent, white, 0.42), transparent: true, opacity: 0.72, }); function addEdges(mesh, geometry, opacity = 0.3) { const material = edgeMaterial.clone(); material.opacity = opacity; mesh.add(new THREE.LineSegments(new THREE.EdgesGeometry(geometry, 38), material)); } function makeBox(parent, size, position, material) { const geometry = new THREE.BoxGeometry(...size); const mesh = new THREE.Mesh(geometry, material); mesh.position.set(...position); mesh.castShadow = true; mesh.receiveShadow = true; addEdges(mesh, geometry, 0.26); parent.add(mesh); return mesh; } function makePanel(parent, shape, depth, position, rotation, material) { const geometry = new THREE.ExtrudeGeometry(shape, { depth, steps: 1, curveSegments: 20, bevelEnabled: true, bevelSegments: 4, bevelSize: 0.045, bevelThickness: 0.04, }); geometry.translate(0, 0, -depth / 2); geometry.computeVertexNormals(); const mesh = new THREE.Mesh(geometry, material); mesh.position.set(...position); mesh.rotation.set(...rotation); mesh.castShadow = true; mesh.receiveShadow = true; addEdges(mesh, geometry); parent.add(mesh); return mesh; } function cushionShape(inset = 0) { const shape = new THREE.Shape(); const half = 0.69 - inset; shape.moveTo(-0.52 + inset, 0.61 - inset); shape.quadraticCurveTo(-half, 0.55 - inset, -half, 0.34); shape.lineTo(-half + 0.025, -0.39 + inset); shape.quadraticCurveTo(-half + 0.02, -0.61 + inset, -0.48 + inset, -0.68 + inset); shape.quadraticCurveTo(0, -0.77 + inset, 0.48 - inset, -0.68 + inset); shape.quadraticCurveTo(half - 0.02, -0.61 + inset, half - 0.025, -0.39 + inset); shape.lineTo(half, 0.34); shape.quadraticCurveTo(half, 0.55 - inset, 0.52 - inset, 0.61 - inset); shape.quadraticCurveTo(0, 0.68 - inset, -0.52 + inset, 0.61 - inset); return shape; } function backrestShape(inset = 0) { const shape = new THREE.Shape(); const half = 0.7 - inset; shape.moveTo(-0.52 + inset, 0); shape.quadraticCurveTo(-half, 0.08, -half, 0.34); shape.lineTo(-half + 0.035, 1.28 - inset); shape.quadraticCurveTo(-half + 0.06, 1.52 - inset, -0.46 + inset, 1.65 - inset); shape.quadraticCurveTo(0, 1.82 - inset, 0.46 - inset, 1.65 - inset); shape.quadraticCurveTo(half - 0.06, 1.52 - inset, half - 0.035, 1.28 - inset); shape.lineTo(half, 0.34); shape.quadraticCurveTo(half, 0.08, 0.52 - inset, 0); shape.quadraticCurveTo(0, -0.08 + inset, -0.52 + inset, 0); return shape; } function makeSeam(parent, points) { const geometry = new THREE.BufferGeometry().setFromPoints(points.map((point) => new THREE.Vector3(...point))); parent.add(new THREE.Line(geometry, seamMaterial)); } const seatRoot = new THREE.Group(); seatRoot.rotation.y = -0.38; seatRoot.position.set(0, 0.02, 0); scene.add(seatRoot); makeBox(seatRoot, [0.11, 0.08, 2.65], [-0.48, 0.12, 0], railMaterial); makeBox(seatRoot, [0.11, 0.08, 2.65], [0.48, 0.12, 0], railMaterial); makeBox(seatRoot, [1.15, 0.07, 0.13], [0, 0.15, -0.92], structureMaterial); makeBox(seatRoot, [1.15, 0.07, 0.13], [0, 0.15, 0.92], structureMaterial); const slideRoot = new THREE.Group(); seatRoot.add(slideRoot); const base = new THREE.Mesh(new THREE.CapsuleGeometry(0.47, 0.5, 6, 18), structureMaterial); base.rotation.x = Math.PI / 2; base.scale.set(1.42, 1, 0.55); base.position.set(0, 0.34, -0.02); base.castShadow = true; slideRoot.add(base); const cushionPivot = new THREE.Group(); cushionPivot.position.set(0, 0.57, 0.06); slideRoot.add(cushionPivot); makePanel(cushionPivot, cushionShape(), 0.22, [0, 0, 0.08], [-Math.PI / 2, 0, 0], seatShell); makePanel(cushionPivot, cushionShape(0.16), 0.055, [0, 0.145, 0.03], [-Math.PI / 2, 0, 0], thermalInsert); [-1, 1].forEach((side) => { const wing = new THREE.Mesh(new THREE.CapsuleGeometry(0.105, 0.86, 5, 12), bolsterMaterial); wing.rotation.x = Math.PI / 2; wing.position.set(side * 0.61, 0.17, 0.08); wing.castShadow = true; cushionPivot.add(wing); }); makeSeam(cushionPivot, [[-0.28, 0.181, 0.5], [-0.22, 0.181, 0.08], [-0.28, 0.181, -0.42]]); makeSeam(cushionPivot, [[0.28, 0.181, 0.5], [0.22, 0.181, 0.08], [0.28, 0.181, -0.42]]); const backrestPivot = new THREE.Group(); backrestPivot.position.set(0, 0.62, -0.5); backrestPivot.rotation.x = -0.09; slideRoot.add(backrestPivot); makePanel(backrestPivot, backrestShape(), 0.3, [0, 0, 0], [0, 0, 0], seatShell); makePanel(backrestPivot, backrestShape(0.16), 0.07, [0, 0.04, 0.19], [0, 0, 0], thermalInsert); [-1, 1].forEach((side) => { const wing = new THREE.Mesh(new THREE.CapsuleGeometry(0.115, 1.2, 6, 14), bolsterMaterial); wing.position.set(side * 0.61, 0.78, 0.18); wing.scale.set(1, 1, 0.72); wing.castShadow = true; backrestPivot.add(wing); }); makeSeam(backrestPivot, [[-0.28, 0.2, 0.232], [-0.22, 0.83, 0.232], [-0.32, 1.46, 0.232]]); makeSeam(backrestPivot, [[0.28, 0.2, 0.232], [0.22, 0.83, 0.232], [0.32, 1.46, 0.232]]); makeSeam(backrestPivot, [[-0.32, 0.86, 0.234], [0, 0.78, 0.234], [0.32, 0.86, 0.234]]); makeBox(backrestPivot, [0.045, 0.42, 0.045], [-0.24, 1.74, 0], railMaterial); makeBox(backrestPivot, [0.045, 0.42, 0.045], [0.24, 1.74, 0], railMaterial); const headrest = new THREE.Mesh(new THREE.CapsuleGeometry(0.31, 0.3, 8, 18), seatShell); headrest.scale.set(1.35, 1, 0.5); headrest.position.set(0, 1.98, 0.01); headrest.castShadow = true; backrestPivot.add(headrest); const headrestInsert = new THREE.Mesh(new THREE.CapsuleGeometry(0.245, 0.23, 8, 18), seatInsert); headrestInsert.scale.set(1.32, 1, 0.28); headrestInsert.position.set(0, 1.98, 0.18); backrestPivot.add(headrestInsert); const legrestPivot = new THREE.Group(); legrestPivot.position.set(0, 0.64, 0.7); legrestPivot.rotation.x = 0.1; slideRoot.add(legrestPivot); const legShell = makePanel(legrestPivot, cushionShape(0.08), 0.18, [0, 0, 0.36], [-Math.PI / 2, 0, 0], seatShell); legShell.scale.set(0.9, 0.72, 0.54); const legInsert = makePanel(legrestPivot, cushionShape(0.2), 0.045, [0, 0.12, 0.4], [-Math.PI / 2, 0, 0], seatInsert); legInsert.scale.set(0.92, 0.74, 0.56); const statusHalo = new THREE.Mesh( new THREE.RingGeometry(1.12, 1.18, 72), new THREE.MeshBasicMaterial({ color: themeAccent.getHex(), transparent: true, opacity: 0.12, side: THREE.DoubleSide, depthWrite: false, }), ); statusHalo.position.set(0, 1.35, -0.35); statusHalo.renderOrder = 2; seatRoot.add(statusHalo); const palettes = { normal: { shell: mixHex(themeStrong, black, 0.06), insert: mixHex(themeAccent, white, 0.48), bolster: themeAccent.getHex(), emissive: mixHex(themeStrong, black, 0.7), }, working: { shell: themeAccent.getHex(), insert: mixHex(themeAccent, white, 0.58), bolster: mixHex(themeAccent, themeStrong, 0.38), emissive: mixHex(themeAccent, black, 0.54), }, passed: { shell: themeStrong.getHex(), insert: mixHex(themeAccent, white, 0.52), bolster: themeAccent.getHex(), emissive: mixHex(themeStrong, black, 0.62), }, failed: { shell: 0x7a3f43, insert: 0xc97a78, bolster: 0x85494c, emissive: 0x4a100d }, }; const current = { axes: [35, 42, 48, 20], ventilation: false, heating: 0, active: false, state: "IDLE" }; const desired = { axes: [35, 42, 48, 20], ventilation: false, heating: 0, active: false, state: "IDLE" }; function updateCamera() { const cosPitch = Math.cos(orbit.pitch); camera.position.set( target.x + Math.sin(orbit.yaw) * cosPitch * orbit.distance, target.y + Math.sin(orbit.pitch) * orbit.distance, target.z + Math.cos(orbit.yaw) * cosPitch * orbit.distance, ); camera.lookAt(target); viewer.dataset.orbitYaw = orbit.yaw.toFixed(3); viewer.dataset.orbitPitch = orbit.pitch.toFixed(3); } function resetOrbit() { const aspect = Math.max(0.55, viewer.clientWidth / Math.max(1, viewer.clientHeight)); orbit.yaw = 0.67; orbit.pitch = 0.21; orbit.distance = 6.8 * Math.max(1, Math.min(1.3, 0.78 / aspect)); updateCamera(); } function resize() { const width = Math.max(1, viewer.clientWidth); const height = Math.max(1, viewer.clientHeight); const pixelRatio = Math.min(window.devicePixelRatio || 1, 2); renderer.setPixelRatio(pixelRatio); renderer.setSize(width, height, false); camera.aspect = width / height; camera.updateProjectionMatrix(); } function onHealingState(event) { const detail = event.detail || {}; (detail.axes || []).forEach((axis) => { const motor = Number(axis.motor); if (motor >= 0 && motor < desired.axes.length) { desired.axes[motor] = Math.max(0, Math.min(100, Number(axis.position_pct || 0))); } }); desired.ventilation = Boolean(detail.ventilation); desired.heating = Math.max(0, Math.min(3, Number(detail.heatingLevel || 0))); desired.active = !["IDLE", "PASSED"].includes(detail.state); desired.state = String(detail.state || "IDLE"); statusHalo.material.color.setHex(detail.state === "FAILED" ? 0xc94d51 : themeAccent.getHex()); } function animate() { window.requestAnimationFrame(animate); for (let index = 0; index < current.axes.length; index += 1) { current.axes[index] += (desired.axes[index] - current.axes[index]) * 0.055; } current.heating += (desired.heating - current.heating) * 0.05; current.ventilation = desired.ventilation; current.active = desired.active; current.state = desired.state; slideRoot.position.z = THREE.MathUtils.mapLinear(current.axes[0], 0, 100, -0.46, 0.46); cushionPivot.rotation.x = THREE.MathUtils.mapLinear(current.axes[1], 0, 100, -0.08, 0.16); backrestPivot.rotation.x = THREE.MathUtils.mapLinear(current.axes[2], 0, 100, 0.16, -0.44); legrestPivot.rotation.x = THREE.MathUtils.mapLinear(current.axes[3], 0, 100, 0.08, -0.68); const paletteName = current.state === "FAILED" ? "failed" : current.state === "PASSED" ? "passed" : current.active ? "working" : "normal"; const palette = palettes[paletteName]; seatShell.color.lerp(new THREE.Color(palette.shell), 0.09); seatInsert.color.lerp(new THREE.Color(palette.insert), 0.09); thermalInsert.color.lerp(new THREE.Color(palette.insert), 0.09); bolsterMaterial.color.lerp(new THREE.Color(palette.bolster), 0.09); const heatRatio = current.heating / 3; seatShell.emissive.lerp(new THREE.Color(heatRatio > 0 ? 0x8e2f18 : palette.emissive), 0.08); seatShell.emissiveIntensity = 0.08 + heatRatio * 0.42; thermalInsert.emissive.lerp(new THREE.Color(current.ventilation ? themeAccent.getHex() : palette.emissive), 0.08); thermalInsert.emissiveIntensity = current.ventilation ? 0.32 : 0.06; bolsterMaterial.emissive.lerp(new THREE.Color(palette.emissive), 0.08); bolsterMaterial.emissiveIntensity = current.active ? 0.12 : 0.04; statusHalo.rotation.z += current.active ? 0.006 : 0.0015; statusHalo.material.opacity = current.state === "FAILED" ? 0.2 + Math.sin(performance.now() * 0.005) * 0.06 : current.active ? 0.12 + Math.sin(performance.now() * 0.005) * 0.04 : 0.06; renderer.render(scene, camera); } viewer.addEventListener("pointerdown", (event) => { if (event.button !== 0) return; event.preventDefault(); pointer.active = true; pointer.x = event.clientX; pointer.y = event.clientY; viewer.classList.add("dragging"); }); window.addEventListener("pointermove", (event) => { if (!pointer.active) return; event.preventDefault(); orbit.yaw -= (event.clientX - pointer.x) * 0.008; orbit.pitch = Math.max(-0.08, Math.min(0.72, orbit.pitch + (event.clientY - pointer.y) * 0.006)); pointer.x = event.clientX; pointer.y = event.clientY; updateCamera(); }, { passive: false }); const endOrbit = () => { pointer.active = false; viewer.classList.remove("dragging"); }; window.addEventListener("pointerup", endOrbit); window.addEventListener("pointercancel", endOrbit); window.addEventListener("blur", endOrbit); viewer.addEventListener("wheel", (event) => { event.preventDefault(); orbit.distance = Math.max(5.1, Math.min(10, orbit.distance + event.deltaY * 0.003)); updateCamera(); }, { passive: false }); viewer.addEventListener("dblclick", resetOrbit); window.addEventListener("seat-healing-state", onHealingState); window.addEventListener("self-repair-seat-reset", resetOrbit); if (window.ResizeObserver) new ResizeObserver(resize).observe(viewer); window.addEventListener("resize", resize); resize(); resetOrbit(); animate(); window.requestAnimationFrame(() => viewer.classList.add("ready")); } if (document.readyState === "complete") { init(); } else { document.addEventListener("DOMContentLoaded", init, { once: true }); } }());