522 lines
20 KiB
JavaScript
522 lines
20 KiB
JavaScript
(function () {
|
|
"use strict";
|
|
|
|
function initSeat3d(options) {
|
|
const {
|
|
canvasId,
|
|
viewerId,
|
|
stateEvent,
|
|
resetEvent,
|
|
initialAxes = [35, 42, 48, 20],
|
|
readyClass = "ready",
|
|
axisMode = "repair",
|
|
} = options;
|
|
const THREE = window.THREE;
|
|
const canvas = document.getElementById(canvasId);
|
|
const viewer = document.getElementById(viewerId);
|
|
if (!canvas || !viewer || viewer.dataset.seat3dInitialized === "true") return;
|
|
viewer.dataset.seat3dInitialized = "true";
|
|
|
|
const markReady = (fallback = false) => {
|
|
viewer.classList.add(readyClass);
|
|
if (fallback) viewer.classList.add("fallback");
|
|
};
|
|
|
|
const buildFallback = () => {
|
|
const fallback = document.createElement("div");
|
|
fallback.className = "v2-repair-seat-fallback";
|
|
fallback.setAttribute("aria-hidden", "true");
|
|
fallback.innerHTML = `
|
|
<div class="repair-css-seat">
|
|
<div class="repair-css-headrest"></div>
|
|
<div class="repair-css-back"><i></i></div>
|
|
<div class="repair-css-cushion"><i></i></div>
|
|
<div class="repair-css-legrest"></div>
|
|
<div class="repair-css-base"></div>
|
|
<div class="repair-css-rails"><i></i><i></i></div>
|
|
</div>
|
|
<span class="repair-css-halo"></span>`;
|
|
viewer.insertBefore(fallback, canvas.nextSibling);
|
|
window.addEventListener(stateEvent, (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) ?? initialAxes[0])) * 0.18}px`);
|
|
fallback.style.setProperty("--repair-lift", `${((axes.get(1) ?? initialAxes[1]) - 50) * 0.1}px`);
|
|
fallback.style.setProperty("--repair-back", `${-8 - (axes.get(2) ?? initialAxes[2]) * 0.24}deg`);
|
|
fallback.style.setProperty("--repair-leg", `${-5 + (axes.get(3) ?? initialAxes[3]) * 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");
|
|
});
|
|
markReady(true);
|
|
};
|
|
|
|
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: [...initialAxes], ventilation: false, heating: 0, active: false, state: "IDLE" };
|
|
const desired = { axes: [...initialAxes], 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);
|
|
if (axisMode === "calibration") {
|
|
slideRoot.position.y = THREE.MathUtils.mapLinear(current.axes[1], 0, 100, -0.12, 0.16);
|
|
cushionPivot.rotation.x = 0;
|
|
} else {
|
|
slideRoot.position.y = 0;
|
|
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(stateEvent, onHealingState);
|
|
window.addEventListener(resetEvent, resetOrbit);
|
|
|
|
if (window.ResizeObserver) new ResizeObserver(resize).observe(viewer);
|
|
window.addEventListener("resize", resize);
|
|
resize();
|
|
resetOrbit();
|
|
animate();
|
|
window.requestAnimationFrame(() => markReady());
|
|
|
|
}
|
|
|
|
function init() {
|
|
initSeat3d({
|
|
canvasId: "repairSeatCanvas",
|
|
viewerId: "repairSeatViewer",
|
|
stateEvent: "seat-healing-state",
|
|
resetEvent: "self-repair-seat-reset",
|
|
});
|
|
initSeat3d({
|
|
canvasId: "calibrationSeatCanvas",
|
|
viewerId: "calibrationSeatViewer",
|
|
stateEvent: "calibration-seat-state",
|
|
resetEvent: "self-calibration-seat-reset",
|
|
initialAxes: [50, 50, 20, 0],
|
|
readyClass: "is-ready",
|
|
axisMode: "calibration",
|
|
});
|
|
}
|
|
|
|
if (document.readyState === "complete") {
|
|
init();
|
|
} else {
|
|
document.addEventListener("DOMContentLoaded", init, { once: true });
|
|
}
|
|
}());
|