import * as THREE from "three";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import { TransformControls } from "three/examples/jsm/controls/TransformControls.js";
import { RectAreaLightUniformsLib } from "three/examples/jsm/lights/RectAreaLightUniformsLib.js";

const state = {
  renderer: null,
  scene: null,
  camera: null,
  controls: null,
  model: null,
  floor: null,
  floorAlphaTexture: null,
  floorLight: null,
  floorLightTexture: null,
  floorVisible: true,
  floorGroundY: null,
  environmentTexture: null,
  environmentTarget: null,
  realContactShadow: null,
  previewLights: null,
  sceneConfigs: null,
  sceneConfigsLoaded: false,
  sceneConfigPromise: null,
  backgroundTexture: null,
  sceneMode: "light",
  contactShadow: null,
  contactShadowTexture: null,
  exteriorModel: null,
  auxiliaryModels: [],
  loadId: 0,
  raf: 0,
  lastTextureUrl: "",
  bounds: null,
  cameraPreset: "free",
  numberplate: null,
  numberplateRequestId: 0,
  numberplateFit: null,
  numberplateCalibrations: {},
  numberplateCalibrationsLoaded: false,
  materialOverlay: {
    enabled: false,
    requestId: 0,
    originals: new Map(),
    materials: [],
    texture: null,
    colorTexture: null,
    carbonTexture: null,
    aoTexture: null,
  },
  wheelTuner: {
    enabled: false,
    target: "all",
    controls: null,
    helper: null,
    anchor: null,
    selected: [],
    lastPosition: new THREE.Vector3(),
    dirty: false,
  },
  numberplateTuner: {
    enabled: false,
    mode: "translate",
    controls: null,
    helper: null,
    anchor: null,
    dirty: false,
    targetMesh: null,
    dragging: false,
    controlPointerActive: false,
  },
  numberplatePicking: {
    raycaster: new THREE.Raycaster(),
    pointer: new THREE.Vector2(),
    hovered: null,
    pointerDown: null,
  },
  cameraRoll: {
    active: false,
    pointerId: null,
    startX: 0,
    startUp: new THREE.Vector3(0, 1, 0),
    viewAxis: new THREE.Vector3(0, 0, -1),
    controlsEnabled: true,
  },
};

const CAMERA_PRESETS = {
  front: { direction: new THREE.Vector3(0, 0.12, 1), fov: 45, distanceScale: 1.08, targetOffset: new THREE.Vector3(0, 0, 0) },
  rear: { direction: new THREE.Vector3(0, 0.12, -1), fov: 45, distanceScale: 1.08, targetOffset: new THREE.Vector3(0, 0, 0) },
  left: { direction: new THREE.Vector3(1, 0.12, 0), fov: 45, distanceScale: 1.08, targetOffset: new THREE.Vector3(0, 0, 0) },
  right: { direction: new THREE.Vector3(-1, 0.12, 0), fov: 45, distanceScale: 1.08, targetOffset: new THREE.Vector3(0, 0, 0) },
  top: { direction: new THREE.Vector3(0, 1, 0.001), fov: 45, distanceScale: 1.08, targetOffset: new THREE.Vector3(0, 0, 0), up: new THREE.Vector3(0, 0, -1) },
  "front-three-quarter": { direction: new THREE.Vector3(0.85, 0.38, 0.85), fov: 45, distanceScale: 1.08, targetOffset: new THREE.Vector3(0, 0, 0) },
  "rear-three-quarter": { direction: new THREE.Vector3(-0.85, 0.38, -0.85), fov: 45, distanceScale: 1.08, targetOffset: new THREE.Vector3(0, 0, 0) },
};

const DEFAULT_CAMERA_PRESETS = Object.fromEntries(
  Object.entries(CAMERA_PRESETS).map(([key, preset]) => [key, {
    direction: preset.direction.clone(),
    fov: preset.fov,
    distanceScale: preset.distanceScale,
    targetOffset: preset.targetOffset.clone(),
    up: preset.up?.clone?.(),
  }])
);

const NUMBERPLATE_CALIBRATIONS_URL = "./assets/numberplate_calibrations.json";
const PREVIEW3D_SCENES_URL = "./assets/preview3d_scenes.json";
let numberplateCalibrationsPromise = null;
const imageDataUrlCache = new Map();
const numberplateFontAtlasCache = new Map();
const DEFAULT_FLOOR_Y = -0.006;
const FLOOR_TYRE_CLEARANCE = 0.002;
const FLOOR_LIGHT_Y_OFFSET = 0.001;
const CONTACT_SHADOW_Y_OFFSET = 0.002;
const REAL_CONTACT_SHADOW_Y_OFFSET = 0.003;

const DEFAULT_NUMBERPLATE_FIT = Object.freeze({
  boundsScale: 1,
  widthScale: 1,
  lengthScale: 1,
  heightScale: 1,
  plateScale: 0.85,
  lengthDirection: 1,
  xOffset: 0,
  yOffset: 0,
  zOffset: 0,
  hoodYOffset: 0,
  hoodZOffset: 0,
  hoodScale: 1,
  hoodPitch: 0,
  hoodYaw: 0,
  hoodRoll: 0,
  doorPitch: 0,
  doorYaw: 0,
  doorRoll: 0,
  doorPlacementMode: "actor",
});

const DEFAULT_MATERIAL_LOOK = {
  evoApproxEnabled: false,
  maskThreshold: 0.08,
  paintBrightness: 1,
  paintSpecular: 0.35,
  paintLightInfluence: 0,
  paintRimStrength: 0.012,
  paintHighlightStrength: 0.025,
  paintToneMapped: false,
  bodyBaseColor: "#7a00ff",
  carbonScale: 18,
  carbonBrightness: 1,
  matteColor: "#15181b",
  glassOpacity: 0.82,
  lightEmissive: 1,
  rimMetalness: 0.58,
  tyreRoughness: 0.7,
  tyreEnvMapIntensity: 0,
  environmentIntensity: 0,
  paintReflectionStrength: 0.32,
  paintStripStrength: 0.72,
  paintStripSharpness: 56,
};

const DEFAULT_REAL_SHADOW = {
  enabled: false,
  opacity: 0.34,
  radius: 5,
  bias: -0.0002,
  normalBias: 0.02,
  mapSize: 2048,
  cameraSize: 8,
};

const DEFAULT_SCENE_CONFIGS = {
  light: {
    renderer: { exposure: 1.02 },
    background: "#f3f4f6",
    floor: { color: "#f3f4f6", size: 120, fade: false },
    floorLight: { color: "#d7dce2", opacity: 0.34, scaleX: 3.6, scaleZ: 2.36 },
    contactShadow: { visible: true, opacity: 0.38, scaleX: 2.04, scaleZ: 1.68 },
    realShadow: { ...DEFAULT_REAL_SHADOW },
    environment: { enabled: false, intensity: 0 },
    numberplates: { visible: true },
    hemi: { sky: "#ffffff", ground: "#a8b0ba", intensity: 1.25 },
    key: { color: "#ffffff", intensity: 1.55, position: [4.8, 6.5, 5.2] },
    fill: { color: "#dce8ff", intensity: 0.42, position: [-5.2, 3.1, -3.8] },
    rim: { color: "#ffffff", intensity: 0.58, position: [-3.8, 4.8, 6.5] },
    materialLook: { ...DEFAULT_MATERIAL_LOOK },
    rectAreaLights: [],
  },
  dark: {
    renderer: { exposure: 1.12 },
    background: "#111318",
    floor: { color: "#111318", size: 120, fade: false },
    floorLight: { color: "#242a32", opacity: 0.68, scaleX: 3.84, scaleZ: 2.48 },
    contactShadow: { visible: true, opacity: 0.62, scaleX: 2.12, scaleZ: 1.72 },
    realShadow: { ...DEFAULT_REAL_SHADOW },
    environment: { enabled: false, intensity: 0 },
    numberplates: { visible: true },
    hemi: { sky: "#eaf1ff", ground: "#090c11", intensity: 0.72 },
    key: { color: "#ffffff", intensity: 0.85, position: [4.8, 6.5, 5.2] },
    fill: { color: "#82a5ff", intensity: 0.18, position: [-5.2, 3.1, -3.8] },
    rim: { color: "#ffffff", intensity: 1.05, position: [-3.8, 4.8, 6.5] },
    materialLook: { ...DEFAULT_MATERIAL_LOOK },
    rectAreaLights: [],
  },
};

const LEGACY_SCENE_SURFACES = {
  light: {
    background: DEFAULT_SCENE_CONFIGS.light.background,
    floor: { ...DEFAULT_SCENE_CONFIGS.light.floor },
  },
  dark: {
    background: DEFAULT_SCENE_CONFIGS.dark.background,
    floor: { ...DEFAULT_SCENE_CONFIGS.dark.floor },
  },
};

DEFAULT_SCENE_CONFIGS.studio = cloneJson(DEFAULT_SCENE_CONFIGS.dark);
DEFAULT_SCENE_CONFIGS.studio.renderer.exposure = 1.04;
DEFAULT_SCENE_CONFIGS.studio.environment = { enabled: true, intensity: 1 };
DEFAULT_SCENE_CONFIGS.studio.realShadow = { ...DEFAULT_REAL_SHADOW, enabled: true, opacity: 0.38, radius: 6, cameraSize: 9 };
DEFAULT_SCENE_CONFIGS.studio.hemi = { sky: "#f6f9ff", ground: "#06070a", intensity: 0.42 };
DEFAULT_SCENE_CONFIGS.studio.key = { color: "#ffffff", intensity: 1.25, position: [4.8, 6.5, 5.2] };
DEFAULT_SCENE_CONFIGS.studio.fill = { color: "#7f9ed7", intensity: 0.1, position: [-5.2, 3.1, -3.8] };
DEFAULT_SCENE_CONFIGS.studio.rim = { color: "#ffffff", intensity: 2.1, position: [-3.8, 4.8, 6.5] };
DEFAULT_SCENE_CONFIGS.studio.materialLook = {
  ...DEFAULT_SCENE_CONFIGS.dark.materialLook,
  tyreRoughness: 0.92,
  tyreEnvMapIntensity: 0,
  environmentIntensity: 1,
  paintReflectionStrength: 0.28,
  paintStripStrength: 0.82,
  paintStripSharpness: 62,
};
DEFAULT_SCENE_CONFIGS.studio.rectAreaLights = [
  { color: "#ffffff", intensity: 5.8, width: 5.6, height: 0.42, position: [-2.8, 4.1, 2.2], target: [0, 0.55, 0] },
  { color: "#ffffff", intensity: 4.2, width: 4.8, height: 0.36, position: [3.2, 3.2, -1.4], target: [0, 0.45, 0] },
  { color: "#eaf2ff", intensity: 2.6, width: 5.8, height: 1.2, position: [0.2, 5.6, 0.4], target: [0, 0.35, 0] },
];

function applyStudioSceneLogic(sceneName, overrides = {}) {
  DEFAULT_SCENE_CONFIGS[sceneName] = mergeDeep(cloneJson(DEFAULT_SCENE_CONFIGS.studio), overrides);
}

applyStudioSceneLogic("light", {
  background: LEGACY_SCENE_SURFACES.light.background,
  floor: {
    ...LEGACY_SCENE_SURFACES.light.floor,
    color: LEGACY_SCENE_SURFACES.light.floor.color,
  },
  numberplates: { visible: true },
});

applyStudioSceneLogic("dark", {
  background: LEGACY_SCENE_SURFACES.dark.background,
  floor: {
    ...LEGACY_SCENE_SURFACES.dark.floor,
    color: LEGACY_SCENE_SURFACES.dark.floor.color,
  },
  numberplates: { visible: true },
});

const SCENE_CONFIG_NAMES = Object.freeze(["light", "dark", "studio"]);

function disposeObject(object) {
  object.traverse((child) => {
    if (child.geometry) child.geometry.dispose();
    const materials = Array.isArray(child.material) ? child.material : child.material ? [child.material] : [];
    materials.forEach((material) => {
      Object.values(material).forEach((value) => {
        if (value?.isTexture) value.dispose();
      });
      material.dispose();
    });
  });
}

function disposeMaterialOverlayResources() {
  state.materialOverlay.materials.forEach((material) => material.dispose());
  state.materialOverlay.materials = [];
  state.materialOverlay.texture?.dispose?.();
  state.materialOverlay.texture = null;
  state.materialOverlay.colorTexture?.dispose?.();
  state.materialOverlay.colorTexture = null;
  state.materialOverlay.carbonTexture?.dispose?.();
  state.materialOverlay.carbonTexture = null;
  state.materialOverlay.aoTexture?.dispose?.();
  state.materialOverlay.aoTexture = null;
}

function applyAuxiliaryTransform(object, modelInfo = {}) {
  if (!object) return;
  const transform = modelInfo.transform || {};
  const label = String(modelInfo.label || "").trim();
  const calibrationOverride = modelInfo.calibrationOverride && typeof modelInfo.calibrationOverride === "object"
    ? modelInfo.calibrationOverride
    : {};
  object.userData.wheelLabel = label;
  object.userData.wheelBaseTransform = {
    x: Number(transform.x ?? 0),
    y: Number(transform.y ?? 0),
    z: Number(transform.z ?? 0),
  };
  object.userData.wheelCalibrationBaseOverride = {
    x: Number(calibrationOverride.x ?? 0),
    y: Number(calibrationOverride.y ?? 0),
    z: Number(calibrationOverride.z ?? 0),
  };
  object.position.set(
    Number(transform.x ?? 0),
    Number(transform.y ?? 0),
    Number(transform.z ?? 0),
  );
  object.rotation.set(
    THREE.MathUtils.degToRad(Number(transform.rx ?? 0)),
    THREE.MathUtils.degToRad(Number(transform.ry ?? 0)),
    THREE.MathUtils.degToRad(Number(transform.rz ?? 0)),
  );
  object.scale.set(
    Number(transform.sx ?? 1),
    Number(transform.sy ?? 1),
    Number(transform.sz ?? 1),
  );
  object.traverse((child) => {
    if (!child.isMesh) return;
    child.castShadow = true;
    child.receiveShadow = true;
  });
}

function hiddenMaterialNameSet(names = []) {
  return new Set((Array.isArray(names) ? names : [])
    .map((name) => String(name || "").trim())
    .filter(Boolean));
}

function materialNameIsHidden(name = "", hiddenNames = new Set()) {
  const materialName = String(name || "").trim();
  if (hiddenNames.has(materialName)) return true;
  for (const hiddenName of hiddenNames) {
    const target = String(hiddenName || "").trim();
    if (target && materialName.startsWith(`${target}_`)) return true;
  }
  return false;
}

function applyHiddenPreviewMaterials(model, hiddenMaterialNames = []) {
  if (!model) return 0;
  const hiddenNames = hiddenMaterialNameSet(hiddenMaterialNames);
  if (!hiddenNames.size) return 0;
  let hidden = 0;
  model.traverse((child) => {
    if (!child.isMesh || !child.material) return;
    const materials = Array.isArray(child.material) ? child.material : [child.material];
    materials.forEach((material) => {
      if (!materialNameIsHidden(material?.name, hiddenNames)) return;
      material.transparent = true;
      material.opacity = 0;
      material.colorWrite = false;
      material.depthWrite = false;
      material.depthTest = true;
      material.needsUpdate = true;
      hidden += 1;
    });
  });
  return hidden;
}

function createContactShadowTexture() {
  const canvas = document.createElement("canvas");
  canvas.width = 512;
  canvas.height = 512;
  const context = canvas.getContext("2d");
  const gradient = context.createRadialGradient(256, 256, 24, 256, 256, 246);
  gradient.addColorStop(0, "rgba(0, 0, 0, 0.34)");
  gradient.addColorStop(0.45, "rgba(0, 0, 0, 0.2)");
  gradient.addColorStop(0.78, "rgba(0, 0, 0, 0.07)");
  gradient.addColorStop(1, "rgba(0, 0, 0, 0)");
  context.fillStyle = gradient;
  context.fillRect(0, 0, canvas.width, canvas.height);
  const texture = new THREE.CanvasTexture(canvas);
  texture.colorSpace = THREE.SRGBColorSpace;
  texture.needsUpdate = true;
  return texture;
}

function isPlainObject(value) {
  return value && typeof value === "object" && !Array.isArray(value);
}

function mergeDeep(base, override) {
  const result = { ...(base || {}) };
  Object.entries(override || {}).forEach(([key, value]) => {
    if (isPlainObject(value) && isPlainObject(result[key])) {
      result[key] = mergeDeep(result[key], value);
    } else {
      result[key] = Array.isArray(value) ? [...value] : value;
    }
  });
  return result;
}

function sceneConfigs() {
  return state.sceneConfigs || DEFAULT_SCENE_CONFIGS;
}

function sceneConfig(sceneName = "light") {
  const configs = sceneConfigs();
  return configs[sceneName] || configs.light || DEFAULT_SCENE_CONFIGS.light;
}

function cloneJson(value) {
  return JSON.parse(JSON.stringify(value || {}));
}

function normalizeSceneConfigData(data = {}) {
  const source = data?.settings && typeof data.settings === "object" ? data.settings : data;
  const scenes = source?.scenes && typeof source.scenes === "object" ? source.scenes : source || {};
  return Object.fromEntries(
    SCENE_CONFIG_NAMES.map((name) => [
      name,
      mergeDeep(DEFAULT_SCENE_CONFIGS[name], scenes[name] || {}),
    ])
  );
}

async function loadSceneConfigData() {
  if (window.pywebview?.api?.load_preview3d_scene_settings) {
    try {
      const result = await window.pywebview.api.load_preview3d_scene_settings();
      if (result?.ok && result.data) return normalizeSceneConfigData(result.data);
    } catch (_error) {
      // Fall back to bundled defaults.
    }
  }
  return await fetch(PREVIEW3D_SCENES_URL, { cache: "no-store" })
    .then(async (response) => {
      if (!response.ok) throw new Error(`Scene JSON not found (${response.status})`);
      const data = await response.json();
      return normalizeSceneConfigData(data);
    })
    .catch(() => cloneJson(DEFAULT_SCENE_CONFIGS));
}

async function ensureSceneConfigs() {
  if (state.sceneConfigsLoaded) return sceneConfigs();
  if (!state.sceneConfigPromise) {
    state.sceneConfigPromise = loadSceneConfigData();
  }
  state.sceneConfigs = await state.sceneConfigPromise;
  state.sceneConfigsLoaded = true;
  return state.sceneConfigs;
}

function getSceneConfigs() {
  return cloneJson(sceneConfigs());
}

function getDefaultSceneConfigs() {
  return cloneJson(DEFAULT_SCENE_CONFIGS);
}

function setSceneConfigs(configs = {}) {
  state.sceneConfigs = normalizeSceneConfigData({ scenes: configs });
  state.sceneConfigsLoaded = true;
  state.sceneConfigPromise = Promise.resolve(state.sceneConfigs);
  setSceneMode(state.sceneMode || "light");
  return { sceneMode: state.sceneMode, scenes: getSceneConfigs() };
}

function setSceneConfig(sceneName = "light", config = {}) {
  const next = SCENE_CONFIG_NAMES.includes(sceneName) ? sceneName : "light";
  const current = sceneConfigs();
  current[next] = mergeDeep(DEFAULT_SCENE_CONFIGS[next], config || {});
  state.sceneConfigs = current;
  state.sceneConfigsLoaded = true;
  state.sceneConfigPromise = Promise.resolve(state.sceneConfigs);
  if (state.sceneMode === next) setSceneMode(next);
  return { sceneMode: state.sceneMode, scene: cloneJson(current[next]) };
}

function colorValue(value, fallback = "#ffffff") {
  return value == null ? fallback : value;
}

function numericValue(value, fallback, lo, hi) {
  const number = Number(value);
  const next = Number.isFinite(number) ? number : fallback;
  return Math.max(lo, Math.min(hi, next));
}

function materialLookConfig(override = null) {
  return {
    ...DEFAULT_MATERIAL_LOOK,
    ...(sceneConfig(state.sceneMode)?.materialLook || {}),
    ...(override || {}),
  };
}

function vectorFromArray(value, fallback = [0, 0, 0]) {
  const source = Array.isArray(value) && value.length >= 3 ? value : fallback;
  return new THREE.Vector3(Number(source[0] || 0), Number(source[1] || 0), Number(source[2] || 0));
}

function createGradientBackground(top = "#eef1f4", bottom = "#c6ccd3") {
  const canvas = document.createElement("canvas");
  canvas.width = 32;
  canvas.height = 256;
  const context = canvas.getContext("2d");
  const gradient = context.createLinearGradient(0, 0, 0, canvas.height);
  gradient.addColorStop(0, top);
  gradient.addColorStop(1, bottom);
  context.fillStyle = gradient;
  context.fillRect(0, 0, canvas.width, canvas.height);
  const texture = new THREE.CanvasTexture(canvas);
  texture.colorSpace = THREE.SRGBColorSpace;
  texture.needsUpdate = true;
  return texture;
}

function createStudioEnvironmentTexture() {
  const canvas = document.createElement("canvas");
  canvas.width = 1024;
  canvas.height = 512;
  const context = canvas.getContext("2d");
  const background = context.createLinearGradient(0, 0, 0, canvas.height);
  background.addColorStop(0, "#10151c");
  background.addColorStop(0.38, "#07090d");
  background.addColorStop(0.62, "#171b20");
  background.addColorStop(1, "#030406");
  context.fillStyle = background;
  context.fillRect(0, 0, canvas.width, canvas.height);

  [
    { x: 70, y: 104, w: 210, h: 18, alpha: 0.95 },
    { x: 736, y: 122, w: 230, h: 16, alpha: 0.9 },
    { x: 320, y: 54, w: 390, h: 28, alpha: 0.86 },
    { x: 42, y: 192, w: 34, h: 170, alpha: 0.82 },
    { x: 930, y: 176, w: 42, h: 190, alpha: 0.78 },
    { x: 384, y: 392, w: 252, h: 22, alpha: 0.34 },
  ].forEach((panel) => {
    const glow = context.createLinearGradient(panel.x, panel.y, panel.x + panel.w, panel.y + panel.h);
    glow.addColorStop(0, `rgba(255,255,255,${panel.alpha * 0.18})`);
    glow.addColorStop(0.18, `rgba(255,255,255,${panel.alpha})`);
    glow.addColorStop(0.82, `rgba(255,255,255,${panel.alpha})`);
    glow.addColorStop(1, `rgba(255,255,255,${panel.alpha * 0.18})`);
    context.fillStyle = glow;
    context.fillRect(panel.x, panel.y, panel.w, panel.h);
  });

  const texture = new THREE.CanvasTexture(canvas);
  texture.mapping = THREE.EquirectangularReflectionMapping;
  texture.colorSpace = THREE.SRGBColorSpace;
  texture.needsUpdate = true;
  return texture;
}

function createStudioEnvironmentMap() {
  const sourceTexture = createStudioEnvironmentTexture();
  if (!state.renderer) return { texture: sourceTexture, target: null };
  const pmremGenerator = new THREE.PMREMGenerator(state.renderer);
  const target = pmremGenerator.fromEquirectangular(sourceTexture);
  sourceTexture.dispose();
  pmremGenerator.dispose();
  return { texture: target.texture, target };
}

function updateEnvironmentIntensity(intensity = 0) {
  const value = numericValue(intensity, 0, 0, 4);
  [state.model, ...(state.auxiliaryModels || [])].forEach((root) => root?.traverse?.((child) => {
    if (!child.isMesh || !child.material) return;
    const materials = Array.isArray(child.material) ? child.material : [child.material];
    materials.forEach((material) => {
      if (!material || !("envMapIntensity" in material)) return;
      material.envMapIntensity = material.userData?.liveryLabWheelKind === "tyre" ? 0 : value;
      material.needsUpdate = true;
    });
  }));
}

function createFloorAlphaTexture(fadeStart = 0.36, fadeEnd = 0.94, fadePower = 1.65) {
  const canvas = document.createElement("canvas");
  canvas.width = 512;
  canvas.height = 512;
  const context = canvas.getContext("2d");
  const image = context.createImageData(canvas.width, canvas.height);
  const start = Math.max(0.01, Math.min(0.98, Number(fadeStart) || 0.36));
  const end = Math.max(start + 0.01, Math.min(1, Number(fadeEnd) || 0.94));
  const power = Math.max(0.2, Number(fadePower) || 1.65);
  for (let y = 0; y < canvas.height; y += 1) {
    for (let x = 0; x < canvas.width; x += 1) {
      const u = Math.abs((x / (canvas.width - 1)) * 2 - 1);
      const v = Math.abs((y / (canvas.height - 1)) * 2 - 1);
      const distance = Math.max(u, v);
      const fade = Math.max(0, Math.min(1, (end - distance) / (end - start)));
      const alpha = Math.round(Math.pow(fade, power) * 255);
      const offset = (y * canvas.width + x) * 4;
      image.data[offset] = 255;
      image.data[offset + 1] = 255;
      image.data[offset + 2] = 255;
      image.data[offset + 3] = alpha;
    }
  }
  context.putImageData(image, 0, 0);
  const texture = new THREE.CanvasTexture(canvas);
  texture.colorSpace = THREE.NoColorSpace;
  texture.needsUpdate = true;
  return texture;
}

function createFloorLightTexture() {
  const canvas = document.createElement("canvas");
  canvas.width = 512;
  canvas.height = 512;
  const context = canvas.getContext("2d");
  const gradient = context.createRadialGradient(256, 256, 20, 256, 256, 248);
  gradient.addColorStop(0, "rgba(255, 255, 255, 1)");
  gradient.addColorStop(0.34, "rgba(255, 255, 255, 0.72)");
  gradient.addColorStop(0.72, "rgba(255, 255, 255, 0.18)");
  gradient.addColorStop(1, "rgba(255, 255, 255, 0)");
  context.fillStyle = gradient;
  context.fillRect(0, 0, canvas.width, canvas.height);
  const texture = new THREE.CanvasTexture(canvas);
  texture.colorSpace = THREE.SRGBColorSpace;
  texture.needsUpdate = true;
  return texture;
}

function setLightTransform(light, position = [], target = [0, 0, 0]) {
  light.position.copy(vectorFromArray(position));
  const targetPosition = vectorFromArray(target);
  if (light.target?.isObject3D) {
    light.target.position.copy(targetPosition);
    if (state.scene && light.target.parent !== state.scene) state.scene.add(light.target);
    light.target.updateMatrixWorld();
  }
  light.lookAt(targetPosition);
}

function shadowSettings() {
  return {
    ...DEFAULT_REAL_SHADOW,
    ...(sceneConfig(state.sceneMode).realShadow || {}),
  };
}

function currentFloorY() {
  return Number.isFinite(state.floorGroundY) ? state.floorGroundY : DEFAULT_FLOOR_Y;
}

function isTyreAuxiliaryModel(object) {
  const label = String(object?.userData?.wheelLabel || "").toLowerCase();
  return label.startsWith("tyre_");
}

function updateFloorGroundFromTyres() {
  let tyreMinY = Infinity;
  const box = new THREE.Box3();
  (state.auxiliaryModels || []).forEach((object) => {
    if (!isTyreAuxiliaryModel(object)) return;
    object.updateMatrixWorld(true);
    box.setFromObject(object);
    if (!box.isEmpty() && Number.isFinite(box.min.y)) {
      tyreMinY = Math.min(tyreMinY, box.min.y);
    }
  });
  state.floorGroundY = Number.isFinite(tyreMinY)
    ? tyreMinY - FLOOR_TYRE_CLEARANCE
    : null;
  applyFloorGroundHeight();
}

function applyFloorGroundHeight() {
  const floorY = currentFloorY();
  if (state.floor) state.floor.position.y = floorY;
  if (state.floorLight) state.floorLight.position.y = floorY + FLOOR_LIGHT_Y_OFFSET;
  if (state.contactShadow) state.contactShadow.position.y = floorY + CONTACT_SHADOW_Y_OFFSET;
  if (state.realContactShadow) state.realContactShadow.position.y = floorY + REAL_CONTACT_SHADOW_Y_OFFSET;
}

function refreshGroundedSceneElements() {
  updateFloorGroundFromTyres();
  updateFloorLight();
  updateContactShadow();
  updateRealContactShadow();
}

function updateRealContactShadow() {
  if (!state.realContactShadow || !state.bounds) return;
  const { size, center } = state.bounds;
  const shadow = shadowSettings();
  const floorVisible = state.floorVisible && sceneConfig(state.sceneMode).floor?.visible !== false;
  const width = Math.max(size.x, size.z * 0.46, 1.2);
  const depth = Math.max(size.z, size.x * 0.46, 1.2);
  state.realContactShadow.position.set(center.x, currentFloorY() + REAL_CONTACT_SHADOW_Y_OFFSET, center.z);
  state.realContactShadow.scale.set(width * 2.8, depth * 2.8, 1);
  state.realContactShadow.visible = Boolean(shadow.enabled) && floorVisible && Number(shadow.opacity ?? 0) > 0;
  state.realContactShadow.receiveShadow = state.realContactShadow.visible;
  if (state.realContactShadow.material) {
    state.realContactShadow.material.opacity = Number(shadow.opacity ?? DEFAULT_REAL_SHADOW.opacity);
    state.realContactShadow.material.needsUpdate = true;
  }

  const key = state.previewLights?.key;
  if (!key?.isDirectionalLight) return;
  key.castShadow = Boolean(shadow.enabled);
  if (!key.castShadow) return;
  key.shadow.radius = numericValue(shadow.radius, DEFAULT_REAL_SHADOW.radius, 0, 20);
  key.shadow.bias = numericValue(shadow.bias, DEFAULT_REAL_SHADOW.bias, -0.01, 0.01);
  key.shadow.normalBias = numericValue(shadow.normalBias, DEFAULT_REAL_SHADOW.normalBias, 0, 0.2);
  const mapSize = Math.max(256, Math.min(4096, Math.round(Number(shadow.mapSize || DEFAULT_REAL_SHADOW.mapSize))));
  if (key.shadow.mapSize.x !== mapSize || key.shadow.mapSize.y !== mapSize) {
    key.shadow.mapSize.set(mapSize, mapSize);
    key.shadow.map?.dispose?.();
    key.shadow.map = null;
  }
  const cameraSize = numericValue(shadow.cameraSize, DEFAULT_REAL_SHADOW.cameraSize, 1, 40);
  const shadowCamera = key.shadow.camera;
  shadowCamera.left = -cameraSize;
  shadowCamera.right = cameraSize;
  shadowCamera.top = cameraSize;
  shadowCamera.bottom = -cameraSize;
  shadowCamera.near = 0.1;
  shadowCamera.far = Math.max(20, Math.max(size.x, size.y, size.z) * 8);
  shadowCamera.updateProjectionMatrix();
  const targetPosition = sceneConfig(state.sceneMode).key?.target || [center.x, center.y, center.z];
  setLightTransform(key, sceneConfig(state.sceneMode).key?.position, targetPosition);
}

function applySceneShadowFlags(object) {
  object?.traverse?.((child) => {
    if (!child.isMesh) return;
    child.castShadow = true;
    child.receiveShadow = true;
  });
}

function createNumberplateMaterial(texture, options = {}) {
  const material = new THREE.MeshStandardMaterial({
    map: texture,
    transparent: true,
    opacity: normalizedOpacity(options.opacity),
    side: THREE.DoubleSide,
    depthWrite: false,
    depthTest: Boolean(options.occludeByModel),
    roughness: 0.72,
    metalness: 0,
  });
  material.alphaTest = 0.02;
  return material;
}

function applyNumberplateSceneVisibility() {
  if (!state.numberplate?.mesh) return;
  state.numberplate.mesh.visible = sceneConfig(state.sceneMode).numberplates?.visible !== false;
}

function clearRectAreaLights() {
  const rectLights = state.previewLights?.rectAreaLights || [];
  rectLights.forEach((light) => state.scene?.remove(light));
  if (state.previewLights) state.previewLights.rectAreaLights = [];
}

function updateContactShadow() {
  if (!state.contactShadow || !state.bounds) return;
  const { size, center } = state.bounds;
  const shadow = sceneConfig(state.sceneMode).contactShadow || {};
  const width = Math.max(size.x, size.z * 0.46, 1.2);
  const depth = Math.max(size.z, size.x * 0.46, 1.2);
  state.contactShadow.position.set(center.x, currentFloorY() + CONTACT_SHADOW_Y_OFFSET, center.z);
  state.contactShadow.scale.set(width * Number(shadow.scaleX ?? 0.9), depth * Number(shadow.scaleZ ?? 0.72), 1);
  state.contactShadow.visible = state.floorVisible && shadow.visible !== false && Number(shadow.opacity ?? 0) > 0;
}

function updateFloorLight() {
  if (!state.floorLight || !state.bounds) return;
  const { size, center } = state.bounds;
  const config = sceneConfig(state.sceneMode).floorLight || {};
  const width = Math.max(size.x, size.z * 0.46, 1.2);
  const depth = Math.max(size.z, size.x * 0.46, 1.2);
  state.floorLight.position.set(center.x, currentFloorY() + FLOOR_LIGHT_Y_OFFSET, center.z);
  state.floorLight.scale.set(width * Number(config.scaleX ?? 1.8), depth * Number(config.scaleZ ?? 1.18), 1);
  state.floorLight.visible = state.floorVisible && Number(config.opacity ?? 0) > 0;
}

function setSceneMode(sceneName = "light") {
  const configs = sceneConfigs();
  const sceneMode = configs[sceneName] ? sceneName : "light";
  const config = sceneConfig(sceneMode);
  state.sceneMode = sceneMode;
  if (state.renderer) state.renderer.toneMappingExposure = Number(config.renderer?.exposure ?? 1.05);
  if (state.scene) {
    state.backgroundTexture?.dispose?.();
    const background = config.background || {};
    if (isPlainObject(background)) {
      state.backgroundTexture = createGradientBackground(background.top, background.bottom);
      state.scene.background = state.backgroundTexture;
    } else {
      state.backgroundTexture = null;
      state.scene.background = new THREE.Color(colorValue(background, "#121417"));
    }
    const hadEnvironmentTarget = Boolean(state.environmentTarget);
    state.environmentTarget?.dispose?.();
    state.environmentTarget = null;
    if (!hadEnvironmentTarget) state.environmentTexture?.dispose?.();
    state.environmentTexture = null;
    const environment = config.environment || {};
    const environmentIntensity = numericValue(environment.intensity, materialLookConfig().environmentIntensity, 0, 4);
    if (environment.enabled !== false && environmentIntensity > 0) {
      const environmentMap = createStudioEnvironmentMap();
      state.environmentTexture = environmentMap.texture;
      state.environmentTarget = environmentMap.target;
      state.scene.environment = state.environmentTexture;
    } else {
      state.scene.environment = null;
    }
    updateEnvironmentIntensity(environmentIntensity);
  }
  if (state.floor?.material) {
    state.floor.visible = state.floorVisible && config.floor?.visible !== false;
    state.floor.material.color.set(colorValue(config.floor?.color, "#24282e"));
    state.floor.material.transparent = true;
    state.floor.material.opacity = Number(config.floor?.opacity ?? 1);
    state.floor.material.depthWrite = true;
    if (config.floor?.fade === false) {
      state.floorAlphaTexture?.dispose?.();
      state.floorAlphaTexture = null;
      state.floor.material.alphaMap = null;
    } else {
      state.floorAlphaTexture?.dispose?.();
      state.floorAlphaTexture = createFloorAlphaTexture(
        config.floor?.fadeStart,
        config.floor?.fadeEnd,
        config.floor?.fadePower,
      );
      state.floor.material.alphaMap = state.floorAlphaTexture;
      state.floor.material.depthWrite = false;
    }
    state.floor.material.needsUpdate = true;
    const floorSize = Math.max(4, Number(config.floor?.size ?? 8));
    state.floor.scale.set(floorSize, floorSize, 1);
    state.floor.position.y = currentFloorY();
  }
  if (state.floorLight?.material) {
    state.floorLight.material.color.set(colorValue(config.floorLight?.color, "#ffffff"));
    state.floorLight.material.opacity = Number(config.floorLight?.opacity ?? 0);
    state.floorLight.material.needsUpdate = true;
  }
  if (state.contactShadow?.material) {
    state.contactShadow.material.opacity = Number(config.contactShadow?.opacity ?? 0.72);
    state.contactShadow.material.needsUpdate = true;
  }
  const lights = state.previewLights || {};
  if (lights.hemi) {
    lights.hemi.color.set(colorValue(config.hemi?.sky, "#ffffff"));
    lights.hemi.groundColor.set(colorValue(config.hemi?.ground, "#2c3138"));
    lights.hemi.intensity = Number(config.hemi?.intensity ?? 1.45);
  }
  if (lights.key) {
    lights.key.color.set(colorValue(config.key?.color, "#ffffff"));
    lights.key.intensity = Number(config.key?.intensity ?? 2.65);
    setLightTransform(lights.key, config.key?.position, config.key?.target || [0, 0, 0]);
  }
  if (lights.fill) {
    lights.fill.color.set(colorValue(config.fill?.color, "#dce8ff"));
    lights.fill.intensity = Number(config.fill?.intensity ?? 0.9);
    lights.fill.castShadow = false;
    setLightTransform(lights.fill, config.fill?.position, config.fill?.target || [0, 0, 0]);
  }
  if (lights.rim) {
    lights.rim.color.set(colorValue(config.rim?.color, "#ffffff"));
    lights.rim.intensity = Number(config.rim?.intensity ?? 1.15);
    lights.rim.castShadow = false;
    setLightTransform(lights.rim, config.rim?.position, config.rim?.target || [0, 0, 0]);
  }
  clearRectAreaLights();
  const rectAreaLights = Array.isArray(config.rectAreaLights) ? config.rectAreaLights : [];
  if (state.scene && state.previewLights) {
    state.previewLights.rectAreaLights = rectAreaLights.map((item) => {
      const light = new THREE.RectAreaLight(
        colorValue(item.color, "#ffffff"),
        Number(item.intensity ?? 1),
        Number(item.width ?? 3),
        Number(item.height ?? 1.5),
      );
      setLightTransform(light, item.position, item.target);
      state.scene.add(light);
      return light;
    });
  }
  applyFloorGroundHeight();
  updateFloorLight();
  updateContactShadow();
  updateRealContactShadow();
  updateMaterialViewSceneLook();
  applyNumberplateSceneVisibility();
  return { sceneMode };
}

function setFloorVisible(visible = true) {
  state.floorVisible = Boolean(visible);
  setSceneMode(state.sceneMode || "light");
  return { visible: state.floorVisible };
}

function restoreMaterialChannelOverlay() {
  let restored = 0;
  state.materialOverlay.originals.forEach((material, mesh) => {
    if (!mesh?.isMesh) return;
    mesh.material = material;
    restored += Array.isArray(material) ? material.length : 1;
  });
  state.materialOverlay.originals.clear();
  state.materialOverlay.enabled = false;
  disposeMaterialOverlayResources();
  return { enabled: false, appliedMaterials: restored };
}

function createChannelOverlayMaterial(maskTexture, sourceMaterial, options = {}) {
  const transparent = Boolean(sourceMaterial?.transparent);
  const opacity = sourceMaterial?.opacity == null ? 1 : sourceMaterial.opacity;
  const displayMode = String(options.displayMode || "").trim() === "grayscale" ? "grayscale" : "channels";
  const material = new THREE.ShaderMaterial({
    uniforms: {
      maskMap: { value: maskTexture },
      baseColor: { value: new THREE.Color(0xff2d3a) },
      greenColor: { value: new THREE.Color(0x29d86d) },
      blueColor: { value: new THREE.Color(0x2f7cff) },
      yellowColor: { value: new THREE.Color(0xffd84f) },
      otherColor: { value: new THREE.Color(0x626873) },
      threshold: { value: 0.08 },
      opacity: { value: opacity },
    },
    vertexShader: `
      varying vec2 vUv;
      void main() {
        vUv = uv;
        gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
      }
    `,
    fragmentShader: `
      uniform sampler2D maskMap;
      uniform vec3 baseColor;
      uniform vec3 greenColor;
      uniform vec3 blueColor;
      uniform vec3 yellowColor;
      uniform vec3 otherColor;
      uniform float threshold;
      uniform float opacity;
      varying vec2 vUv;
      void main() {
        vec3 mask = texture2D(maskMap, vUv).rgb;
        if (${displayMode === "grayscale" ? "true" : "false"}) {
          float value = dot(mask, vec3(0.299, 0.587, 0.114));
          gl_FragColor = vec4(vec3(value), opacity);
          return;
        }
        bool redActive = mask.r > threshold;
        bool greenActive = mask.g > threshold;
        bool blueActive = mask.b > threshold;
        vec3 color = baseColor;
        if (redActive && greenActive && !blueActive) {
          color = yellowColor;
        } else if (blueActive && mask.b >= mask.r && mask.b >= mask.g) {
          color = blueColor;
        } else if (greenActive && mask.g >= mask.r) {
          color = greenColor;
        } else if (redActive) {
          color = baseColor;
        } else if (mask.r + mask.g + mask.b > threshold) {
          color = otherColor;
        }
        gl_FragColor = vec4(color, opacity);
      }
    `,
    side: sourceMaterial?.side ?? THREE.FrontSide,
    transparent,
    depthWrite: !transparent,
  });
  material.name = `${sourceMaterial?.name || "EXT_SKIN"}_CHANNEL_OVERLAY`;
  material.userData.liveryLabMaterialMaskDisplayMode = displayMode;
  return material;
}

function createApproxMaterial(maskTexture, colorTexture, carbonTexture, aoTexture, sourceMaterial, materialLook = null) {
  const look = materialLookConfig(materialLook);
  const transparent = Boolean(sourceMaterial?.transparent);
  const opacity = sourceMaterial?.opacity == null ? 1 : sourceMaterial.opacity;
  const bodyBaseColorEnabled = look.bodyBaseColorEnabled === true || look.bodyBaseColorEnabled === 1 ? 1 : 0;
  const material = new THREE.ShaderMaterial({
    uniforms: {
      maskMap: { value: maskTexture },
      colorMap: { value: colorTexture },
      carbonMap: { value: carbonTexture || colorTexture },
      aoMap: { value: aoTexture || colorTexture },
      useCarbonMap: { value: carbonTexture ? 1 : 0 },
      useAoMap: { value: aoTexture ? 1 : 0 },
      carbonScale: { value: numericValue(look.carbonScale, 18, 1, 80) },
      carbonBrightness: { value: numericValue(look.carbonBrightness, 1, 0.2, 2.5) },
      matteColor: { value: new THREE.Color(colorValue(look.matteColor, "#15181b")) },
      carbonA: { value: new THREE.Color(0x171d23) },
      carbonB: { value: new THREE.Color(0x68737d) },
      bodyBaseColor: { value: new THREE.Color(colorValue(look.bodyBaseColor, "#7a00ff")) },
      bodyBaseColorEnabled: { value: bodyBaseColorEnabled },
      threshold: { value: numericValue(look.maskThreshold, 0.08, 0, 0.5) },
      paintBrightness: { value: numericValue(look.paintBrightness, 1, 0.2, 2.5) },
      paintSpecular: { value: numericValue(look.paintSpecular, 0.35, 0, 3) },
      paintLightInfluence: { value: numericValue(look.paintLightInfluence, 0, 0, 1) },
      paintRimStrength: { value: numericValue(look.paintRimStrength, 0.012, 0, 0.25) },
      paintHighlightStrength: { value: numericValue(look.paintHighlightStrength, 0.025, 0, 0.5) },
      opacity: { value: opacity },
    },
    vertexShader: `
      varying vec2 vUv;
      varying vec3 vNormalView;
      varying vec3 vViewPosition;
      void main() {
        vUv = uv;
        vNormalView = normalize(normalMatrix * normal);
        vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
        vViewPosition = -mvPosition.xyz;
        gl_Position = projectionMatrix * mvPosition;
      }
    `,
    fragmentShader: `
      uniform sampler2D maskMap;
      uniform sampler2D colorMap;
      uniform sampler2D carbonMap;
      uniform sampler2D aoMap;
      uniform float useCarbonMap;
      uniform float useAoMap;
      uniform float carbonScale;
      uniform vec3 matteColor;
      uniform vec3 carbonA;
      uniform vec3 carbonB;
      uniform vec3 bodyBaseColor;
      uniform float bodyBaseColorEnabled;
      uniform float threshold;
      uniform float paintBrightness;
      uniform float paintSpecular;
      uniform float paintLightInfluence;
      uniform float paintRimStrength;
      uniform float paintHighlightStrength;
      uniform float carbonBrightness;
      uniform float opacity;
      varying vec2 vUv;
      varying vec3 vNormalView;
      float stripe(vec2 uv, float angle, float scale) {
        vec2 dir = vec2(cos(angle), sin(angle));
        float line = fract(dot(uv, dir) * scale);
        return smoothstep(0.34, 0.5, line) * (1.0 - smoothstep(0.5, 0.68, line));
      }
      void main() {
        vec3 mask = texture2D(maskMap, vUv).rgb;
        vec4 skinSample = texture2D(colorMap, vUv);
        vec3 skin = skinSample.rgb;
        float ao = mix(1.0, clamp(texture2D(aoMap, vUv).r, 0.0, 1.0), useAoMap);
        float liveryAlpha = skinSample.a;
        vec3 normal = normalize(vNormalView);
        vec3 keyDir = normalize(vec3(0.35, 0.72, 0.6));
        vec3 fillDir = normalize(vec3(-0.62, 0.35, -0.42));
        float keyLight = max(dot(normal, keyDir), 0.0);
        float fillLight = max(dot(normal, fillDir), 0.0);
        float rimLight = pow(1.0 - abs(normal.z), 2.2);
        float bodyLight = clamp(0.96 + keyLight * 0.1 + fillLight * 0.05, 0.82, 1.1);
        float softSpec = pow(max(dot(normal, normalize(vec3(0.22, 0.58, 0.78))), 0.0), 24.0);
        bool redActive = mask.r > threshold;
        bool greenActive = mask.g > threshold;
        bool blueActive = mask.b > threshold;
        float weaveA = stripe(vUv, 0.78, 180.0);
        float weaveB = stripe(vUv, -0.78, 180.0);
        float broadFiber = stripe(vUv, 0.78, 48.0) * 0.18;
        float checker = step(0.5, fract((vUv.x + vUv.y) * 64.0)) * 0.05;
        float weave = clamp(weaveA * 0.58 + weaveB * 0.44 + broadFiber + checker, 0.0, 1.0);
        float fresnel = pow(1.0 - abs(normal.z), 2.0);
        vec3 carbon = mix(carbonA, carbonB, weave);
        vec3 carbonTex = texture2D(carbonMap, vUv * carbonScale).rgb;
        carbon = mix(carbon, carbonTex * 1.42 + vec3(0.08), useCarbonMap);
        carbon = carbon * clamp(0.78 + keyLight * 0.3 + fillLight * 0.16, 0.62, 1.16) * carbonBrightness;
        carbon += (vec3(0.16, 0.18, 0.21) * fresnel + vec3(0.14) * softSpec) * carbonBrightness;
        float paintLight = mix(1.0, bodyLight, paintLightInfluence);
        vec3 paint = skin * paintBrightness * paintLight + vec3(paintRimStrength) * rimLight + vec3(paintHighlightStrength) * softSpec * paintSpecular;
        vec3 matte = matteColor * clamp(1.02 + keyLight * 0.26 + fillLight * 0.14, 0.74, 1.22) + vec3(0.05) * rimLight;
        vec3 basePaint = bodyBaseColor * clamp(0.98 + keyLight * 0.18 + fillLight * 0.1, 0.72, 1.18) + vec3(0.04) * rimLight;
        vec3 liveryMatte = mix(basePaint, skin * paintBrightness, liveryAlpha) * clamp(1.0 + keyLight * 0.16 + fillLight * 0.08, 0.78, 1.16) + vec3(0.035) * rimLight;
        paint *= ao;
        matte *= ao;
        basePaint *= ao;
        liveryMatte *= ao;
        vec3 color = paint;
        if (blueActive && mask.b >= mask.r && mask.b >= mask.g) {
          color = carbon;
        } else if (redActive && greenActive && bodyBaseColorEnabled > 0.5) {
          color = liveryMatte;
        } else if (greenActive && bodyBaseColorEnabled > 0.5) {
          color = basePaint;
        } else if (redActive) {
          color = paint;
        } else if (!redActive && mask.r + mask.g + mask.b > threshold) {
          color = matte;
        }
        color = max(color, vec3(0.0));
        gl_FragColor = vec4(color, opacity);
        #include <tonemapping_fragment>
        #include <colorspace_fragment>
      }
    `,
    side: sourceMaterial?.side ?? THREE.FrontSide,
    transparent,
    depthWrite: !transparent,
  });
  material.name = `${sourceMaterial?.name || "EXT_SKIN"}_APPROX_MATERIAL`;
  material.toneMapped = Boolean(look.paintToneMapped);
  return material;
}

function setApproxBodyBaseColor(color = "#7a00ff") {
  const nextColor = new THREE.Color(colorValue(color, "#7a00ff"));
  let updated = 0;
  state.materialOverlay.materials.forEach((material) => {
    const uniform = material?.uniforms?.bodyBaseColor;
    if (!uniform?.value?.copy) return;
    uniform.value.copy(nextColor);
    material.needsUpdate = true;
    updated += 1;
  });
  return { color, updated };
}

function approxSpecialMaterialKind(name = "") {
  const tokens = new Set(
    String(name || "")
      .toLowerCase()
      .split(/[^a-z0-9]+/)
      .filter(Boolean),
  );
  const has = (...values) => values.some((value) => tokens.has(value));
  const hasLampHint = has("light", "lights", "lamp", "lamps", "lens", "lenses", "emissive", "glow");
  const hasRearHint = has("rear", "back", "brake", "stop", "red");
  const hasFrontHint = has("front", "head");
  const hasRearShortName = has("rlight", "rlights") || (tokens.has("r") && has("light", "lights", "lamp", "lamps"));
  const hasFrontShortName = has("flamp", "flight", "flights") || (tokens.has("f") && has("light", "lights", "lamp", "lamps"));

  if (has("indicator", "blinker", "turn", "amber", "orange")) return "amber";
  if (has("tail", "taillight", "taillights") || hasRearShortName || (hasRearHint && hasLampHint)) return "tail";
  if (has("headlight", "headlights") || hasFrontShortName || (hasFrontHint && hasLampHint)) return "head";
  if (has("window", "windows", "windscreen", "windshield", "glass", "screen")) return "glass";
  if (has("light", "lights", "lamp", "lamps")) return "head";
  return "";
}

function createApproxSpecialMaterial(kind, sourceMaterial, materialLook = null) {
  const look = materialLookConfig(materialLook);
  const side = sourceMaterial?.side ?? THREE.FrontSide;
  const base = {
    side,
    transparent: true,
    depthWrite: true,
  };
  if (kind === "glass") {
    const material = new THREE.MeshPhysicalMaterial({
      ...base,
      color: new THREE.Color(0x18232c),
      opacity: numericValue(look.glassOpacity, 0.82, 0.05, 1),
      roughness: 0.18,
      metalness: 0,
      clearcoat: 1,
      clearcoatRoughness: 0.06,
      reflectivity: 0.75,
    });
    material.name = `${sourceMaterial?.name || "GLASS"}_EVO_APPROX_GLASS`;
    return material;
  }
  const lightStyles = {
    head: { color: 0x343638, emissive: 0x111111, intensity: 0.04, opacity: 0.74 },
    tail: { color: 0x7a1016, emissive: 0x6f070c, intensity: 0.16, opacity: 0.7 },
    amber: { color: 0x9a5a12, emissive: 0x9d4f08, intensity: 0.18, opacity: 0.74 },
  };
  const style = lightStyles[kind] || lightStyles.head;
  const emissiveScale = numericValue(look.lightEmissive, 1, 0, 4);
  const material = new THREE.MeshPhysicalMaterial({
    ...base,
    color: new THREE.Color(style.color),
    emissive: new THREE.Color(style.emissive),
    emissiveIntensity: style.intensity * emissiveScale,
    opacity: style.opacity,
    roughness: kind === "tail" ? 0.18 : 0.1,
    metalness: 0,
    clearcoat: 1,
    clearcoatRoughness: kind === "tail" ? 0.08 : kind === "head" ? 0.12 : 0.04,
    reflectivity: kind === "tail" ? 0.56 : kind === "head" ? 0.52 : 0.72,
  });
  material.name = `${sourceMaterial?.name || "LIGHT"}_EVO_APPROX_${kind.toUpperCase()}`;
  return material;
}

function wheelApproxKind(material = {}) {
  const source = String(material.source || "").toLowerCase();
  const name = String(material.name || "").toLowerCase();
  if (source === "tyre" || source === "tire" || /tyre|tire|rubber/.test(name)) return "tyre";
  if (source === "rim" || /rim|wheel/.test(name)) return "rim";
  return "wheel";
}

function wheelBaseColorSlot(material = {}) {
  const slots = Array.isArray(material.textureSlots) ? material.textureSlots : [];
  return slots.find((slot) => String(slot.name || "").toLowerCase() === "base_basecolormap")
    || slots.find((slot) => slot.previewChannel === "baseColor")
    || slots.find((slot) => /base|color|diffuse|albedo/i.test(String(slot.name || "")))
    || null;
}

function wheelDetailSlot(material = {}, channel = "normal") {
  const slots = Array.isArray(material.textureSlots) ? material.textureSlots : [];
  const source = String(material.source || "").toLowerCase();
  const candidates = slots.filter((slot) => slot.previewChannel === channel);
  if (!candidates.length) return null;
  if (channel === "normal" && (source === "tyre" || source === "tire")) {
    return candidates.find((slot) => /detail/i.test(String(slot.name || "")))
      || candidates.find((slot) => /scuff/i.test(String(slot.name || "")))
      || candidates.find((slot) => /^base_/i.test(String(slot.name || "")))
      || candidates[0];
  }
  return candidates.find((slot) => /^base_/i.test(String(slot.name || ""))) || candidates[0];
}

function createWheelPreviewMaterial(kind, sourceMaterial, maps = {}, materialLook = null) {
  const look = materialLookConfig(materialLook);
  const isTyre = kind === "tyre";
  const rimMetalness = numericValue(look.rimMetalness, 0.58, 0, 1);
  const material = new THREE.MeshStandardMaterial({
    map: isTyre ? (maps.baseColor || null) : null,
    normalMap: isTyre ? (maps.normal || null) : null,
    roughnessMap: null,
    aoMap: maps.ao || null,
    aoMapIntensity: maps.ao ? (isTyre ? 1.35 : 1.15) : 0,
    color: new THREE.Color(isTyre ? (maps.baseColor ? 0xffffff : 0x303030) : 0x868782),
    emissive: new THREE.Color(isTyre ? 0x202020 : 0x030303),
    emissiveMap: null,
    emissiveIntensity: isTyre ? 0.12 : 0.01,
    roughness: isTyre ? 1 : Math.max(0.08, Math.min(1, 0.9 - rimMetalness * 0.76)),
    metalness: isTyre ? 0 : rimMetalness,
    side: sourceMaterial?.side ?? THREE.FrontSide,
  });
  if (isTyre && maps.normal) material.normalScale = new THREE.Vector2(1.05, 1.05);
  material.envMapIntensity = isTyre ? 0 : 1.15;
  if (isTyre) material.envMap = null;
  material.userData.liveryLabWheelKind = kind;
  material.name = `${sourceMaterial?.name || kind.toUpperCase()}_WHEEL_PREVIEW_${kind.toUpperCase()}`;
  return material;
}

async function applyWheelPreviewMaterials(wheelMaterials = [], options = {}) {
  if (!state.auxiliaryModels.length || !Array.isArray(wheelMaterials) || !wheelMaterials.length) {
    return { applied: 0, textured: 0, fallback: 0 };
  }
  const trackOverlayResources = Boolean(options.trackOverlayResources);
  const trackOverlayOriginals = Boolean(options.trackOverlayOriginals);
  const expectedLoadId = Number(options.loadId || state.loadId || 0);
  const isCurrentLoad = () => !expectedLoadId || expectedLoadId === state.loadId;
  if (!isCurrentLoad()) return { applied: 0, textured: 0, fallback: 0 };
  const byName = new Map();
  wheelMaterials.forEach((material) => {
    const name = String(material?.name || "").trim().toLowerCase();
    if (!name || byName.has(name)) return;
    byName.set(name, material);
  });
  const textureCache = new Map();
  async function textureFor(slot, colorSpace = THREE.SRGBColorSpace) {
    const path = String(slot?.resolvedPath || slot?.value || "").trim();
    if (!path) return null;
    if (textureCache.has(path)) return textureCache.get(path);
    if (!isCurrentLoad()) return null;
    try {
      const url = await loadTextureDataUrl(path, { cacheNamespace: "rims" });
      if (!isCurrentLoad()) return null;
      const texture = await loadTexture(url);
      if (!isCurrentLoad()) {
        texture.dispose?.();
        return null;
      }
      texture.colorSpace = colorSpace;
      texture.flipY = false;
      texture.wrapS = THREE.RepeatWrapping;
      texture.wrapT = THREE.RepeatWrapping;
      texture.anisotropy = Math.min(state.renderer?.capabilities?.getMaxAnisotropy?.() || 1, 8);
      textureCache.set(path, texture);
      if (trackOverlayResources) state.materialOverlay.materials.push(texture);
      return texture;
    } catch (_error) {
      textureCache.set(path, null);
      return null;
    }
  }
  async function mapsFor(material) {
    const baseColor = await textureFor(wheelBaseColorSlot(material), THREE.SRGBColorSpace);
    const normal = await textureFor(wheelDetailSlot(material, "normal"), THREE.NoColorSpace || "");
    const roughness = await textureFor(wheelDetailSlot(material, "roughness"), THREE.NoColorSpace || "");
    const ao = await textureFor(wheelDetailSlot(material, "ao"), THREE.NoColorSpace || "");
    return { baseColor, normal, roughness, ao };
  }

  let applied = 0;
  let textured = 0;
  let fallback = 0;
  let detailMaps = 0;
  for (const scene of state.auxiliaryModels) {
    const meshes = [];
    scene.traverse((child) => {
      if (child.isMesh && child.material) meshes.push(child);
    });
    for (const child of meshes) {
      const original = child.material;
      const materials = Array.isArray(original) ? original : [original];
      let meshApplied = false;
      const nextMaterials = [];
      for (const sourceMaterial of materials) {
        const name = String(sourceMaterial?.name || "").trim().toLowerCase();
        const wheelMaterial = byName.get(name);
        if (!wheelMaterial) {
          nextMaterials.push(sourceMaterial);
          continue;
        }
        const maps = await mapsFor(wheelMaterial);
        if (!isCurrentLoad()) return { applied, textured, fallback, detailMaps };
        if (maps.ao) ensureAoUvSet(child);
        const nextMaterial = createWheelPreviewMaterial(wheelApproxKind(wheelMaterial), sourceMaterial, maps, options.materialLook || null);
        if (trackOverlayResources) state.materialOverlay.materials.push(nextMaterial);
        applied += 1;
        if (maps.baseColor) textured += 1;
        else fallback += 1;
        if (maps.normal || maps.roughness || maps.ao) {
          detailMaps += Number(Boolean(maps.normal)) + Number(Boolean(maps.roughness)) + Number(Boolean(maps.ao));
        }
        meshApplied = true;
        nextMaterials.push(nextMaterial);
      }
      if (!meshApplied) continue;
      if (trackOverlayOriginals && !state.materialOverlay.originals.has(child)) state.materialOverlay.originals.set(child, original);
      child.material = Array.isArray(original) ? nextMaterials : nextMaterials[0];
    }
  }
  return { applied, textured, fallback, detailMaps };
}

function numberplatePickableMeshes() {
  if (state.numberplate?.mode !== "mapped-group" || !state.numberplate?.mesh) return [];
  if (state.numberplate.mesh.visible === false) return [];
  const meshes = [];
  state.numberplate.mesh.traverse?.((child) => {
    if (child.isMesh && child.userData?.numberplatePickable) meshes.push(child);
  });
  return meshes;
}

function ensureNumberplateVisualBase(mesh) {
  if (!mesh?.material || mesh.userData?.numberplateVisualBase) return;
  const material = Array.isArray(mesh.material) ? mesh.material[0] : mesh.material;
  mesh.userData.numberplateVisualBase = {
    color: material?.color?.getHex?.() ?? 0xffffff,
    opacity: Number(material?.opacity ?? 1),
  };
}

function applyNumberplateMeshVisual(mesh) {
  if (!mesh?.material) return;
  ensureNumberplateVisualBase(mesh);
  const base = mesh.userData.numberplateVisualBase || { color: 0xffffff, opacity: 1 };
  const isActive = state.numberplateTuner.targetMesh === mesh;
  const isHovered = state.numberplatePicking.hovered === mesh;
  const color = isHovered ? 0xfff0a8 : isActive ? 0x95f3ff : base.color;
  const opacity = isActive ? Math.min(1, Math.max(0.9, base.opacity)) : isHovered ? Math.min(1, Math.max(0.78, base.opacity)) : base.opacity;
  const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
  materials.forEach((material) => {
    material.color?.setHex?.(color);
    material.opacity = opacity;
    material.needsUpdate = true;
  });
}

function setActiveNumberplateMesh(mesh = null) {
  const previous = state.numberplateTuner.targetMesh;
  if (previous === mesh) {
    applyNumberplateMeshVisual(mesh);
    return mesh;
  }
  state.numberplateTuner.targetMesh = mesh;
  applyNumberplateMeshVisual(previous);
  applyNumberplateMeshVisual(mesh);
  if (state.numberplateTuner.enabled) updateNumberplateTunerTarget();
  return mesh;
}

function setNumberplateHover(mesh = null) {
  const current = state.numberplatePicking.hovered;
  if (current === mesh) return;
  state.numberplatePicking.hovered = mesh;
  applyNumberplateMeshVisual(current);
  applyNumberplateMeshVisual(mesh);
  if (state.renderer?.domElement) {
    state.renderer.domElement.style.cursor = mesh ? "pointer" : "";
  }
}

function updateNumberplatePointer(event) {
  const canvas = state.renderer?.domElement;
  if (!canvas) return false;
  const rect = canvas.getBoundingClientRect();
  if (!rect.width || !rect.height) return false;
  state.numberplatePicking.pointer.set(
    ((event.clientX - rect.left) / rect.width) * 2 - 1,
    -(((event.clientY - rect.top) / rect.height) * 2 - 1),
  );
  return true;
}

function pickNumberplateMesh(event) {
  if (!state.camera || !updateNumberplatePointer(event)) return null;
  const meshes = numberplatePickableMeshes();
  if (!meshes.length) return null;
  state.numberplatePicking.raycaster.setFromCamera(state.numberplatePicking.pointer, state.camera);
  return state.numberplatePicking.raycaster.intersectObjects(meshes, false)[0]?.object || null;
}

function dispatchNumberplateSelection(mesh) {
  if (!mesh?.userData?.numberplatePickable) return;
  const detail = {
    placementIndex: mesh.userData.numberplatePlacementIndex,
    actorIndex: mesh.userData.numberplateActorIndex,
    label: mesh.userData.numberplateLabel || "",
    surface: mesh.userData.numberplateSurface ?? "",
    numberplateIndex: mesh.userData.numberplateIndex ?? "",
  };
  window.dispatchEvent(new CustomEvent("liverylab-numberplate-selected", { detail }));
}

function dispatchNumberplateDeselection() {
  window.dispatchEvent(new CustomEvent("liverylab-numberplate-deselected"));
}

function dispatchNumberplateTransformStart() {
  const target = numberplateTunerTarget();
  if (!target) return;
  window.dispatchEvent(new CustomEvent("liverylab-numberplate-transform-start", {
    detail: { transform: transformFromObject(target) },
  }));
}

function findMappedNumberplateMesh(options = {}) {
  const placementIndex = Number(options.placementIndex);
  const actorIndex = String(options.actorIndex ?? "");
  const label = String(options.label || "");
  const surface = String(options.surface ?? "");
  const meshes = numberplatePickableMeshes();
  if (Number.isInteger(placementIndex)) {
    const byPlacement = meshes.find((mesh) => Number(mesh.userData.numberplatePlacementIndex) === placementIndex);
    if (byPlacement) return byPlacement;
  }
  if (actorIndex) {
    const byActor = meshes.find((mesh) => String(mesh.userData.numberplateActorIndex ?? "") === actorIndex);
    if (byActor) return byActor;
  }
  if (label || surface) {
    return meshes.find((mesh) => (
      (!label || String(mesh.userData.numberplateLabel || "") === label)
      && (!surface || String(mesh.userData.numberplateSurface ?? "") === surface)
    )) || null;
  }
  return null;
}

function selectMappedNumberplate(options = {}) {
  const mesh = findMappedNumberplateMesh(options);
  setActiveNumberplateMesh(mesh);
  return {
    selected: Boolean(mesh),
    placementIndex: mesh?.userData?.numberplatePlacementIndex ?? null,
    actorIndex: mesh?.userData?.numberplateActorIndex ?? null,
    label: mesh?.userData?.numberplateLabel || "",
    transform: mesh ? transformFromObject(mesh) : null,
  };
}

function attachNumberplatePicking(canvas) {
  if (!canvas || canvas.__liveryLabNumberplatePickingAttached) return;
  canvas.__liveryLabNumberplatePickingAttached = true;
  canvas.addEventListener("pointermove", (event) => {
    if (event.buttons) return;
    setNumberplateHover(pickNumberplateMesh(event));
  });
  canvas.addEventListener("pointerleave", () => {
    setNumberplateHover(null);
    state.numberplatePicking.pointerDown = null;
  });
  canvas.addEventListener("pointerdown", (event) => {
    const mesh = pickNumberplateMesh(event);
    state.numberplatePicking.pointerDown = { mesh, x: event.clientX, y: event.clientY };
  });
  canvas.addEventListener("pointerup", (event) => {
    const down = state.numberplatePicking.pointerDown;
    state.numberplatePicking.pointerDown = null;
    if (!down) return;
    const moved = Math.hypot(event.clientX - down.x, event.clientY - down.y);
    if (moved > 6 || state.numberplateTuner.dragging || state.numberplateTuner.controlPointerActive) return;
    const mesh = pickNumberplateMesh(event);
    if (mesh && mesh === down.mesh) {
      dispatchNumberplateSelection(mesh);
      return;
    }
    if (!mesh && !down.mesh && state.numberplateTuner.targetMesh) dispatchNumberplateDeselection();
  });
}

function currentCameraViewAxis() {
  if (!state.camera || !state.controls) return new THREE.Vector3(0, 0, -1);
  const axis = state.controls.target.clone().sub(state.camera.position);
  if (!axis.lengthSq()) return new THREE.Vector3(0, 0, -1);
  return axis.normalize();
}

function applyCameraRollAngle(angle) {
  if (!state.camera || !state.controls) return;
  const quaternion = new THREE.Quaternion().setFromAxisAngle(state.cameraRoll.viewAxis, angle);
  state.camera.up.copy(state.cameraRoll.startUp).applyQuaternion(quaternion).normalize();
  state.camera.lookAt(state.controls.target);
  state.camera.updateMatrixWorld(true);
  state.controls.update();
}

function startCameraRoll(event) {
  if (!state.camera || !state.controls || event.button !== 1) return;
  event.preventDefault();
  event.stopPropagation();
  state.cameraRoll.active = true;
  state.cameraRoll.pointerId = event.pointerId;
  state.cameraRoll.startX = event.clientX;
  state.cameraRoll.startUp.copy(state.camera.up).normalize();
  state.cameraRoll.viewAxis.copy(currentCameraViewAxis());
  state.cameraRoll.controlsEnabled = state.controls.enabled !== false;
  state.controls.enabled = false;
  event.currentTarget?.setPointerCapture?.(event.pointerId);
}

function updateCameraRoll(event) {
  if (!state.cameraRoll.active || event.pointerId !== state.cameraRoll.pointerId) return;
  event.preventDefault();
  event.stopPropagation();
  const rawAngle = (event.clientX - state.cameraRoll.startX) * 0.01;
  const snapStep = Math.PI / 4;
  const angle = event.shiftKey ? Math.round(rawAngle / snapStep) * snapStep : rawAngle;
  applyCameraRollAngle(angle);
}

function endCameraRoll(event) {
  if (!state.cameraRoll.active || event.pointerId !== state.cameraRoll.pointerId) return;
  event.preventDefault();
  event.stopPropagation();
  if (state.controls) state.controls.enabled = state.cameraRoll.controlsEnabled;
  state.cameraRoll.active = false;
  state.cameraRoll.pointerId = null;
  event.currentTarget?.releasePointerCapture?.(event.pointerId);
}

function attachCameraRollControls(canvas) {
  if (!canvas || canvas.__liveryLabCameraRollAttached) return;
  canvas.__liveryLabCameraRollAttached = true;
  canvas.addEventListener("pointerdown", startCameraRoll, true);
  canvas.addEventListener("pointermove", updateCameraRoll, true);
  canvas.addEventListener("pointerup", endCameraRoll, true);
  canvas.addEventListener("pointercancel", endCameraRoll, true);
  canvas.addEventListener("auxclick", (event) => {
    if (event.button === 1) event.preventDefault();
  });
}

function ensureViewer(canvas) {
  if (state.renderer && state.renderer.domElement !== canvas) {
    if (state.model) {
      restoreMaterialChannelOverlay();
      state.scene.remove(state.model);
      disposeObject(state.model);
      state.model = null;
    }
    window.cancelAnimationFrame(state.raf);
    if (state.contactShadow) {
      state.scene.remove(state.contactShadow);
      state.contactShadow.geometry?.dispose?.();
      state.contactShadow.material?.dispose?.();
      state.contactShadow = null;
    }
    state.contactShadowTexture?.dispose?.();
    state.contactShadowTexture = null;
    state.backgroundTexture?.dispose?.();
    state.backgroundTexture = null;
    state.floorAlphaTexture?.dispose?.();
    state.floorAlphaTexture = null;
    if (state.floorLight) {
      state.scene.remove(state.floorLight);
      state.floorLight.geometry?.dispose?.();
      state.floorLight.material?.dispose?.();
      state.floorLight = null;
    }
    if (state.realContactShadow) {
      state.scene.remove(state.realContactShadow);
      state.realContactShadow.geometry?.dispose?.();
      state.realContactShadow.material?.dispose?.();
      state.realContactShadow = null;
    }
    state.floorLightTexture?.dispose?.();
    state.floorLightTexture = null;
    const hadEnvironmentTarget = Boolean(state.environmentTarget);
    state.environmentTarget?.dispose?.();
    state.environmentTarget = null;
    if (!hadEnvironmentTarget) state.environmentTexture?.dispose?.();
    state.environmentTexture = null;
    state.controls?.dispose();
    state.renderer.dispose();
    state.renderer = null;
    state.scene = null;
    state.camera = null;
    state.controls = null;
    state.floor = null;
    state.floorAlphaTexture = null;
    state.floorLight = null;
    state.floorLightTexture = null;
    state.environmentTexture = null;
    state.environmentTarget = null;
    state.realContactShadow = null;
    state.previewLights = null;
  }
  if (state.renderer) return;
  RectAreaLightUniformsLib.init();
  state.renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true, preserveDrawingBuffer: true });
  state.renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
  state.renderer.outputColorSpace = THREE.SRGBColorSpace;
  state.renderer.toneMapping = THREE.ACESFilmicToneMapping;
  state.renderer.toneMappingExposure = 1.05;
  state.renderer.shadowMap.enabled = true;
  state.renderer.shadowMap.type = THREE.PCFSoftShadowMap;

  state.scene = new THREE.Scene();

  state.camera = new THREE.PerspectiveCamera(45, 1, 0.01, 1000);
  state.camera.position.set(3.5, 2, 4.5);
  state.camera.layers.enable(1);

  state.controls = new OrbitControls(state.camera, canvas);
  state.controls.enableDamping = true;
  state.controls.dampingFactor = 0.08;
  state.controls.screenSpacePanning = true;
  state.controls.keyPanSpeed = 0;
  state.controls.keys = { LEFT: "", UP: "", RIGHT: "", BOTTOM: "" };
  state.controls.stopListenToKeyEvents?.();
  attachNumberplatePicking(canvas);
  attachCameraRollControls(canvas);

  const hemi = new THREE.HemisphereLight(0xffffff, 0x2c3138, 1.45);
  state.scene.add(hemi);
  const key = new THREE.DirectionalLight(0xffffff, 2.65);
  key.position.set(4.8, 6.5, 5.2);
  key.castShadow = false;
  state.scene.add(key);
  const fill = new THREE.DirectionalLight(0xdce8ff, 0.9);
  fill.position.set(-5.2, 3.1, -3.8);
  fill.castShadow = false;
  state.scene.add(fill);
  const rim = new THREE.DirectionalLight(0xffffff, 1.15);
  rim.position.set(-3.8, 4.8, 6.5);
  rim.castShadow = false;
  state.scene.add(rim);
  state.previewLights = { hemi, key, fill, rim, rectAreaLights: [] };

  const floor = new THREE.Mesh(
    new THREE.PlaneGeometry(1, 1),
    new THREE.MeshBasicMaterial({
      color: 0x24282e,
      transparent: true,
      side: THREE.DoubleSide,
    }),
  );
  floor.name = "Preview stance floor";
  floor.rotation.x = -Math.PI / 2;
  floor.position.y = currentFloorY();
  floor.receiveShadow = true;
  floor.layers.set(1);
  state.scene.add(floor);
  state.floor = floor;

  state.floorLightTexture = createFloorLightTexture();
  state.floorLight = new THREE.Mesh(
    new THREE.PlaneGeometry(1, 1),
    new THREE.MeshBasicMaterial({
      map: state.floorLightTexture,
      transparent: true,
      opacity: 0.4,
      depthWrite: false,
      side: THREE.DoubleSide,
    }),
  );
  state.floorLight.name = "Preview floor local light pool";
  state.floorLight.rotation.x = -Math.PI / 2;
  state.floorLight.position.y = currentFloorY() + FLOOR_LIGHT_Y_OFFSET;
  state.floorLight.renderOrder = 0;
  state.floorLight.visible = false;
  state.floorLight.layers.set(1);
  state.scene.add(state.floorLight);

  state.contactShadowTexture = createContactShadowTexture();
  state.contactShadow = new THREE.Mesh(
    new THREE.PlaneGeometry(1, 1),
    new THREE.MeshBasicMaterial({
      map: state.contactShadowTexture,
      transparent: true,
      opacity: 0.72,
      depthWrite: false,
    }),
  );
  state.contactShadow.name = "Preview vehicle contact shadow";
  state.contactShadow.rotation.x = -Math.PI / 2;
  state.contactShadow.position.y = currentFloorY() + CONTACT_SHADOW_Y_OFFSET;
  state.contactShadow.renderOrder = 1;
  state.contactShadow.visible = false;
  state.contactShadow.layers.set(1);
  state.scene.add(state.contactShadow);

  state.realContactShadow = new THREE.Mesh(
    new THREE.PlaneGeometry(1, 1),
    new THREE.ShadowMaterial({
      opacity: DEFAULT_REAL_SHADOW.opacity,
      transparent: true,
      depthWrite: false,
      side: THREE.DoubleSide,
    }),
  );
  state.realContactShadow.name = "Preview Three.js contact shadow receiver";
  state.realContactShadow.rotation.x = -Math.PI / 2;
  state.realContactShadow.position.y = currentFloorY() + REAL_CONTACT_SHADOW_Y_OFFSET;
  state.realContactShadow.renderOrder = 2;
  state.realContactShadow.visible = false;
  state.realContactShadow.receiveShadow = true;
  state.realContactShadow.layers.set(1);
  state.scene.add(state.realContactShadow);
  setSceneMode(state.sceneMode || "light");

  const render = () => {
    state.raf = window.requestAnimationFrame(render);
    resize();
    state.controls.update();
    state.renderer.render(state.scene, state.camera);
  };
  render();
  window.addEventListener("resize", resize);
}

function resize() {
  if (!state.renderer || !state.camera) return;
  const canvas = state.renderer.domElement;
  const rect = canvas.getBoundingClientRect();
  const width = Math.max(1, Math.floor(rect.width));
  const height = Math.max(1, Math.floor(rect.height));
  if (canvas.width !== width || canvas.height !== height) {
    state.renderer.setSize(width, height, false);
    state.camera.aspect = width / height;
    state.camera.updateProjectionMatrix();
  }
}

function canvasSize() {
  if (!state.renderer) return { width: 0, height: 0 };
  const canvas = state.renderer.domElement;
  const rect = canvas.getBoundingClientRect();
  return {
    width: Math.max(1, Math.floor(rect.width)),
    height: Math.max(1, Math.floor(rect.height)),
  };
}

function captureCameraViewState() {
  if (!state.camera || !state.controls) return null;
  return {
    position: state.camera.position.toArray(),
    target: state.controls.target.toArray(),
    up: state.camera.up.toArray(),
    fov: Number(state.camera.fov || 45),
    zoom: Number(state.camera.zoom || 1),
  };
}

function restoreCameraViewState(view) {
  if (!view || !state.camera || !state.controls) return false;
  const validVector = (value) => Array.isArray(value) && value.length === 3 && value.every((item) => Number.isFinite(Number(item)));
  if (!validVector(view.position) || !validVector(view.target) || !validVector(view.up)) return false;
  state.camera.position.fromArray(view.position.map(Number));
  state.controls.target.fromArray(view.target.map(Number));
  state.camera.up.fromArray(view.up.map(Number));
  if (state.camera.up.lengthSq()) state.camera.up.normalize();
  const fov = Number(view.fov);
  const zoom = Number(view.zoom);
  if (Number.isFinite(fov) && fov > 0) state.camera.fov = fov;
  if (Number.isFinite(zoom) && zoom > 0) state.camera.zoom = zoom;
  state.camera.lookAt(state.controls.target);
  state.camera.updateProjectionMatrix();
  state.camera.updateMatrixWorld(true);
  state.controls.update();
  return true;
}

function fitCamera(object) {
  const box = new THREE.Box3().setFromObject(object);
  const size = box.getSize(new THREE.Vector3());
  const center = box.getCenter(new THREE.Vector3());
  state.bounds = { box, size, center };
  const radius = Math.max(size.x, size.y, size.z) * 0.72 || 1;
  const distance = radius / Math.sin((state.camera.fov * Math.PI) / 360);
  state.controls.target.copy(center);
  state.camera.position.copy(center).add(new THREE.Vector3(distance * 0.8, distance * 0.45, distance * 0.95));
  state.camera.near = Math.max(0.01, distance / 100);
  state.camera.far = Math.max(100, distance * 10);
  state.camera.updateProjectionMatrix();
  state.controls.update();
}

function setCameraPreset(preset = "free") {
  if (!state.camera || !state.controls || !state.bounds) return { preset: state.cameraPreset, locked: false };
  const normalized = String(preset || "free");
  state.cameraPreset = CAMERA_PRESETS[normalized] ? normalized : "free";
  const { size, center } = state.bounds;

  if (state.cameraPreset === "free") {
    state.controls.enableRotate = true;
    state.controls.enablePan = true;
    state.controls.enableZoom = true;
    state.controls.mouseButtons.LEFT = THREE.MOUSE.ROTATE;
    state.controls.mouseButtons.MIDDLE = THREE.MOUSE.DOLLY;
    state.controls.mouseButtons.RIGHT = THREE.MOUSE.PAN;
    return { preset: state.cameraPreset, locked: false };
  }

  const cameraPreset = CAMERA_PRESETS[state.cameraPreset];
  const target = center.clone().add(cameraPreset.targetOffset || new THREE.Vector3());
  const fov = Number(cameraPreset.fov || 45);
  const radius = Math.max(size.x, size.y, size.z) * 0.72 || 1;
  const distance = radius / Math.sin((fov * Math.PI) / 360);
  const direction = cameraPreset.direction.clone().normalize();
  state.camera.fov = fov;
  state.camera.position.copy(target).add(direction.multiplyScalar(distance * Number(cameraPreset.distanceScale || 1.08)));
  state.camera.up.copy(cameraPreset.up || new THREE.Vector3(0, 1, 0));
  state.controls.target.copy(target);
  state.controls.enableRotate = false;
  state.controls.enablePan = true;
  state.controls.enableZoom = true;
  state.controls.mouseButtons.LEFT = THREE.MOUSE.PAN;
  state.controls.mouseButtons.MIDDLE = THREE.MOUSE.DOLLY;
  state.controls.mouseButtons.RIGHT = THREE.MOUSE.PAN;
  state.camera.near = Math.max(0.01, distance / 100);
  state.camera.far = Math.max(100, distance * 10);
  state.camera.updateProjectionMatrix();
  state.controls.update();
  return { preset: state.cameraPreset, locked: true };
}

function getCameraPreset() {
  return { preset: state.cameraPreset, locked: state.cameraPreset !== "free" };
}

function cameraPresetToJson(presetName = "front-three-quarter", source = CAMERA_PRESETS) {
  const preset = source[presetName] || source["front-three-quarter"];
  return {
    preset: presetName,
    direction: {
      x: Number(preset.direction?.x ?? 0),
      y: Number(preset.direction?.y ?? 0),
      z: Number(preset.direction?.z ?? 0),
    },
    fov: Number(preset.fov || 45),
    distanceScale: Number(preset.distanceScale || 1.08),
    targetOffset: {
      x: Number(preset.targetOffset?.x ?? 0),
      y: Number(preset.targetOffset?.y ?? 0),
      z: Number(preset.targetOffset?.z ?? 0),
    },
  };
}

function getCameraPresetConfig(presetName = "front-three-quarter", options = {}) {
  const normalized = CAMERA_PRESETS[presetName] ? presetName : "front-three-quarter";
  return cameraPresetToJson(normalized, options.defaults ? DEFAULT_CAMERA_PRESETS : CAMERA_PRESETS);
}

function finiteNumber(value, fallback) {
  const number = Number(value);
  return Number.isFinite(number) ? number : fallback;
}

function setCameraPresetConfig(presetName = "front-three-quarter", config = {}) {
  const normalized = CAMERA_PRESETS[presetName] ? presetName : "front-three-quarter";
  const current = CAMERA_PRESETS[normalized];
  const direction = config.direction || {};
  const targetOffset = config.targetOffset || {};
  current.direction = new THREE.Vector3(
    finiteNumber(direction.x, current.direction.x),
    finiteNumber(direction.y, current.direction.y),
    finiteNumber(direction.z, current.direction.z)
  );
  current.fov = finiteNumber(config.fov, current.fov);
  current.distanceScale = finiteNumber(config.distanceScale, current.distanceScale);
  current.targetOffset = new THREE.Vector3(
    finiteNumber(targetOffset.x, current.targetOffset?.x ?? 0),
    finiteNumber(targetOffset.y, current.targetOffset?.y ?? 0),
    finiteNumber(targetOffset.z, current.targetOffset?.z ?? 0)
  );
  if (state.cameraPreset === normalized) setCameraPreset(normalized);
  return getCameraPresetConfig(normalized);
}

function resetCameraPresetConfig(presetName = "front-three-quarter") {
  const normalized = CAMERA_PRESETS[presetName] ? presetName : "front-three-quarter";
  const defaults = DEFAULT_CAMERA_PRESETS[normalized];
  CAMERA_PRESETS[normalized] = {
    direction: defaults.direction.clone(),
    fov: defaults.fov,
    distanceScale: defaults.distanceScale,
    targetOffset: defaults.targetOffset.clone(),
    up: defaults.up?.clone?.(),
  };
  if (state.cameraPreset === normalized) setCameraPreset(normalized);
  return getCameraPresetConfig(normalized);
}

function captureCameraPresetFromView(presetName = "front-three-quarter") {
  const normalized = CAMERA_PRESETS[presetName] ? presetName : "front-three-quarter";
  if (!state.camera || !state.controls || !state.bounds) {
    throw new Error("3D camera is not ready");
  }
  state.controls.update();
  const { size, center } = state.bounds;
  const target = state.controls.target.clone();
  const cameraOffset = state.camera.position.clone().sub(target);
  const cameraDistance = cameraOffset.length();
  const direction = cameraDistance > 0 ? cameraOffset.clone().normalize() : CAMERA_PRESETS[normalized].direction.clone();
  const fov = Number(state.camera.fov || CAMERA_PRESETS[normalized].fov || 45);
  const radius = Math.max(size.x, size.y, size.z) * 0.72 || 1;
  const baseDistance = radius / Math.sin((fov * Math.PI) / 360);
  const distanceScale = baseDistance > 0 ? cameraDistance / baseDistance : CAMERA_PRESETS[normalized].distanceScale;
  return setCameraPresetConfig(normalized, {
    direction: { x: direction.x, y: direction.y, z: direction.z },
    fov,
    distanceScale,
    targetOffset: {
      x: target.x - center.x,
      y: target.y - center.y,
      z: target.z - center.z,
    },
  });
}

function modelMeshStats() {
  let meshCount = 0;
  let visibleMeshCount = 0;
  if (!state.model) return { meshCount, visibleMeshCount };
  state.model.updateMatrixWorld(true);
  state.model.traverse((child) => {
    if (!child.isMesh) return;
    meshCount += 1;
    if (child.visible !== false && child.geometry && state.model.visible !== false) visibleMeshCount += 1;
  });
  return { meshCount, visibleMeshCount };
}

function finiteVector(vector) {
  return Boolean(vector && Number.isFinite(vector.x) && Number.isFinite(vector.y) && Number.isFinite(vector.z));
}

function previewHealth() {
  const canvas = state.renderer?.domElement || null;
  const size = canvasSize();
  const drawingBuffer = state.renderer?.getDrawingBufferSize
    ? state.renderer.getDrawingBufferSize(new THREE.Vector2())
    : new THREE.Vector2(canvas?.width || 0, canvas?.height || 0);
  const { meshCount, visibleMeshCount } = modelMeshStats();
  const bounds = state.bounds || null;
  const boundsSize = bounds?.size || null;
  const boundsCenter = bounds?.center || null;
  const boundsValid = finiteVector(boundsSize)
    && finiteVector(boundsCenter)
    && Math.max(Math.abs(boundsSize.x), Math.abs(boundsSize.y), Math.abs(boundsSize.z)) > 0.001;
  let cameraDistance = 0;
  let frustumIntersects = false;
  if (state.camera && state.controls && bounds?.box && boundsValid) {
    state.camera.updateMatrixWorld(true);
    state.camera.updateProjectionMatrix();
    cameraDistance = state.camera.position.distanceTo(state.controls.target);
    const matrix = new THREE.Matrix4().multiplyMatrices(state.camera.projectionMatrix, state.camera.matrixWorldInverse);
    const frustum = new THREE.Frustum().setFromProjectionMatrix(matrix);
    frustumIntersects = frustum.intersectsBox(bounds.box);
  }
  return {
    loaded: Boolean(state.model),
    sceneMode: state.sceneMode || "light",
    canvasWidth: size.width,
    canvasHeight: size.height,
    drawingBufferWidth: Math.round(drawingBuffer.x || 0),
    drawingBufferHeight: Math.round(drawingBuffer.y || 0),
    meshCount,
    visibleMeshCount,
    modelVisible: Boolean(state.model) && state.model.visible !== false,
    exteriorVisible: Boolean(state.exteriorModel) && state.exteriorModel.visible !== false,
    auxiliaryModelCount: state.auxiliaryModels.length,
    boundsValid,
    boundsSize: boundsSize ? { x: boundsSize.x, y: boundsSize.y, z: boundsSize.z } : null,
    cameraDistance,
    frustumIntersects,
    loadId: state.loadId,
  };
}

function renderHealth() {
  if (state.renderer && state.scene && state.camera) {
    resize();
    state.controls?.update?.();
    state.renderer.render(state.scene, state.camera);
  }
  return previewHealth();
}

function hasLoadedModel() {
  return previewHealth();
}

function clearPreview(options = {}) {
  state.loadId += 1;
  restoreMaterialChannelOverlay();
  removeWheelTunerAnchor();
  removeNumberplateOverlay();
  if (state.model) {
    state.scene?.remove?.(state.model);
    disposeObject(state.model);
  }
  state.model = null;
  state.exteriorModel = null;
  state.auxiliaryModels = [];
  state.bounds = null;
  state.floorGroundY = null;
  applyFloorGroundHeight();
  state.wheelTuner.dirty = false;
  const emptyId = options.emptyId || "preview3dViewerEmpty";
  const empty = document.getElementById(emptyId);
  if (empty) empty.style.display = "";
  resize();
  if (state.renderer && state.scene && state.camera) {
    state.renderer.render(state.scene, state.camera);
  }
  return { cleared: true, loadId: state.loadId };
}

async function loadModelDataUrl(modelPath) {
  const result = await window.pywebview.api.load_preview_model(modelPath);
  if (!result?.ok) throw new Error(result?.message || "Could not load preview model");
  return result.data.data_url;
}

async function loadTextureDataUrl(texturePath, options = {}) {
  if (!texturePath) return "";
  const result = await window.pywebview.api.load_image_preview(texturePath, options.cacheNamespace || "");
  if (!result?.ok) throw new Error(result?.message || "Could not load live texture");
  return result.data.data_url;
}

async function loadImageFromPath(imagePath, options = {}) {
  const path = String(imagePath || "").trim();
  if (!path) return null;
  const loadImage = async () => {
    const dataUrl = await loadTextureDataUrl(path, options);
    return await new Promise((resolve, reject) => {
      const img = new Image();
      img.onload = () => resolve(img);
      img.onerror = reject;
      img.src = dataUrl;
    });
  };
  if (options.cache === false) return await loadImage();
  const cacheKey = `${options.cacheNamespace || ""}|${path}`;
  if (!imageDataUrlCache.has(cacheKey)) {
    imageDataUrlCache.set(cacheKey, loadImage());
  }
  try {
    return await imageDataUrlCache.get(cacheKey);
  } catch (error) {
    imageDataUrlCache.delete(cacheKey);
    throw error;
  }
}

function dataUrlToArrayBuffer(dataUrl) {
  const text = String(dataUrl || "");
  const comma = text.indexOf(",");
  if (!text.startsWith("data:") || comma < 0) {
    throw new Error("Preview model data URL is invalid");
  }
  const meta = text.slice(0, comma).toLowerCase();
  const payload = text.slice(comma + 1);
  if (!meta.includes(";base64")) {
    const decoded = decodeURIComponent(payload);
    const bytes = new Uint8Array(decoded.length);
    for (let index = 0; index < decoded.length; index += 1) {
      bytes[index] = decoded.charCodeAt(index) & 0xff;
    }
    return bytes.buffer;
  }
  const binary = window.atob(payload);
  const bytes = new Uint8Array(binary.length);
  for (let index = 0; index < binary.length; index += 1) {
    bytes[index] = binary.charCodeAt(index);
  }
  return bytes.buffer;
}

function parseGltf(dataUrl) {
  return new Promise((resolve, reject) => {
    const loader = new GLTFLoader();
    try {
      loader.parse(dataUrlToArrayBuffer(dataUrl), "", resolve, reject);
    } catch (error) {
      reject(error);
    }
  });
}

function loadTexture(dataUrl) {
  return new Promise((resolve, reject) => {
    const loader = new THREE.TextureLoader();
    loader.load(dataUrl, resolve, undefined, reject);
  });
}

function setPreviewLoadStatus(project, message) {
  const statusId = String(project?.statusId || "").trim();
  if (!statusId) return;
  const status = document.getElementById(statusId);
  if (!status) return;
  const prefix = String(project?.statusPrefix || "3D preview").trim();
  status.textContent = `${prefix}\n${message}`;
}

async function withPreviewAssetTimeout(promise, timeoutMs, message) {
  const ms = Number(timeoutMs || 0);
  if (!Number.isFinite(ms) || ms <= 0) return await promise;
  let timeoutId = 0;
  try {
    return await Promise.race([
      promise,
      new Promise((_, reject) => {
        timeoutId = window.setTimeout(() => reject(new Error(message)), ms);
      }),
    ]);
  } finally {
    if (timeoutId) window.clearTimeout(timeoutId);
  }
}

function alphaRuns(counts, threshold) {
  const runs = [];
  let start = -1;
  for (let index = 0; index < counts.length; index += 1) {
    if (counts[index] > threshold) {
      if (start < 0) start = index;
    } else if (start >= 0) {
      runs.push({ start, end: index - 1, size: index - start });
      start = -1;
    }
  }
  if (start >= 0) runs.push({ start, end: counts.length - 1, size: counts.length - start });
  return runs;
}

function fallbackNumberplateGlyphBoxes(image) {
  const width = image.naturalWidth || image.width || 1024;
  const height = image.naturalHeight || image.height || 512;
  const cellWidth = width / 8;
  const cellHeight = height / 2;
  return Array.from({ length: 10 }, (_item, index) => {
    const row = index < 8 ? 0 : 1;
    const column = index < 8 ? index : index - 8;
    return {
      x: Math.round(column * cellWidth),
      y: Math.round(row * cellHeight),
      w: Math.round(cellWidth),
      h: Math.round(cellHeight),
    };
  });
}

function detectNumberplateGlyphBoxes(image) {
  const width = image.naturalWidth || image.width || 1024;
  const height = image.naturalHeight || image.height || 512;
  const canvas = document.createElement("canvas");
  canvas.width = width;
  canvas.height = height;
  const context = canvas.getContext("2d", { willReadFrequently: true });
  context.drawImage(image, 0, 0, width, height);
  const data = context.getImageData(0, 0, width, height).data;
  const rowCounts = new Array(height).fill(0);
  for (let y = 0; y < height; y += 1) {
    let count = 0;
    for (let x = 0; x < width; x += 1) {
      if (data[(y * width + x) * 4 + 3] > 16) count += 1;
    }
    rowCounts[y] = count;
  }
  const rows = alphaRuns(rowCounts, Math.max(4, Math.round(width * 0.006)))
    .filter((row) => row.size > height * 0.08)
    .slice(0, 2);
  if (rows.length < 2) return fallbackNumberplateGlyphBoxes(image);

  const boxes = [];
  rows.forEach((row, rowIndex) => {
    const colCounts = new Array(width).fill(0);
    for (let y = row.start; y <= row.end; y += 1) {
      for (let x = 0; x < width; x += 1) {
        if (data[(y * width + x) * 4 + 3] > 16) colCounts[x] += 1;
      }
    }
    const expected = rowIndex === 0 ? 8 : 2;
    const cols = alphaRuns(colCounts, Math.max(3, Math.round(row.size * 0.025)))
      .filter((col) => col.size > width * 0.03)
      .slice(0, expected);
    cols.forEach((col) => {
      boxes.push({
        x: col.start,
        y: row.start,
        w: col.end - col.start + 1,
        h: row.end - row.start + 1,
      });
    });
  });
  return boxes.length === 10 ? boxes : fallbackNumberplateGlyphBoxes(image);
}

async function loadNumberplateFontAtlas(fontPath, fontKey = "") {
  const path = String(fontPath || "").trim();
  if (!path) return null;
  const cacheKey = String(fontKey || path);
  if (!numberplateFontAtlasCache.has(cacheKey)) {
    numberplateFontAtlasCache.set(cacheKey, (async () => {
      const image = await loadImageFromPath(path, { cacheNamespace: "numberplate-font", cache: false });
      return { image, glyphs: detectNumberplateGlyphBoxes(image) };
    })());
  }
  try {
    return await numberplateFontAtlasCache.get(cacheKey);
  } catch (error) {
    numberplateFontAtlasCache.delete(cacheKey);
    throw error;
  }
}

function coerceNumberplateTextColor(options = {}) {
  const source = options.textColor || options.fontColor || options.overlay || null;
  if (Array.isArray(source)) {
    const values = source.slice(0, 4).map(Number);
    if (values.length >= 4 && values.every(Number.isFinite)) {
      return values.map((value, index) => Math.max(0, Math.min(index === 3 ? 1 : 1, value)));
    }
  }
  if (source && typeof source === "object") {
    const values = [source.r, source.g, source.b, source.a ?? 1].map(Number);
    if (values.every(Number.isFinite)) {
      return values.map((value) => Math.max(0, Math.min(1, value)));
    }
  }
  return null;
}

function linearChannelToSrgb(value) {
  const clamped = Math.max(0, Math.min(1, Number(value) || 0));
  return clamped <= 0.0031308 ? clamped * 12.92 : 1.055 * Math.pow(clamped, 1 / 2.4) - 0.055;
}

function numberplateTextColorCss(color) {
  if (!Array.isArray(color) || color.length < 4) return "";
  const [r, g, b, a] = color;
  const srgb = [r, g, b].map((value) => Math.round(linearChannelToSrgb(value) * 255));
  return `rgba(${srgb[0]}, ${srgb[1]}, ${srgb[2]}, ${Math.max(0, Math.min(1, Number(a) || 0))})`;
}

function drawTintedGlyph(context, image, glyph, x, y, width, height, fillStyle) {
  const buffer = document.createElement("canvas");
  buffer.width = Math.max(1, Math.ceil(width));
  buffer.height = Math.max(1, Math.ceil(height));
  const bufferContext = buffer.getContext("2d");
  bufferContext.imageSmoothingEnabled = true;
  bufferContext.imageSmoothingQuality = "high";
  bufferContext.drawImage(image, glyph.x, glyph.y, glyph.w, glyph.h, 0, 0, buffer.width, buffer.height);
  bufferContext.globalCompositeOperation = "source-in";
  bufferContext.fillStyle = fillStyle;
  bufferContext.fillRect(0, 0, buffer.width, buffer.height);
  bufferContext.globalCompositeOperation = "source-over";
  context.drawImage(buffer, x, y, width, height);
}

function drawFallbackNumberplateText(context, canvas, text, options = {}) {
  const textColor = coerceNumberplateTextColor(options);
  context.font = `700 ${Math.round(canvas.height * 0.62)}px Arial, sans-serif`;
  context.textAlign = "center";
  context.textBaseline = "middle";
  context.fillStyle = numberplateTextColorCss(textColor) || "#111";
  if (!textColor) {
    context.lineWidth = Math.max(3, canvas.height * 0.035);
    context.strokeStyle = "rgba(255,255,255,0.95)";
    context.strokeText(String(text), canvas.width * 0.5, canvas.height * 0.53);
  }
  context.fillText(String(text), canvas.width * 0.5, canvas.height * 0.53);
}

function drawNumberplateDebugMark(context, canvas, placement = {}, index = 0) {
  const label = String(placement.label || `PLATE ${index}`).toUpperCase();
  const surface = placement.surface ?? placement.outer_field_number ?? "-";
  const actorIndex = placement.index ?? index;
  context.save();
  context.fillStyle = "rgba(255, 255, 255, 0.86)";
  context.fillRect(canvas.width * 0.06, canvas.height * 0.12, canvas.width * 0.88, canvas.height * 0.76);
  context.lineWidth = Math.max(4, canvas.height * 0.025);
  context.strokeStyle = "#ff0033";
  context.strokeRect(canvas.width * 0.06, canvas.height * 0.12, canvas.width * 0.88, canvas.height * 0.76);
  context.fillStyle = "#111";
  context.textAlign = "center";
  context.textBaseline = "middle";
  context.font = `800 ${Math.round(canvas.height * 0.28)}px Arial, sans-serif`;
  context.fillText(label.replace("DOOR_", ""), canvas.width * 0.5, canvas.height * 0.38);
  context.font = `700 ${Math.round(canvas.height * 0.18)}px Arial, sans-serif`;
  context.fillText(`TEX >  S${surface} #${actorIndex}`, canvas.width * 0.5, canvas.height * 0.64);
  context.restore();
}

function coerceNumberplateTextLayer(options = {}) {
  const layer = options.textLayer || options.textPlacement || {};
  const numeric = (key, fallback) => {
    const value = Number(layer[key]);
    return Number.isFinite(value) ? value : fallback;
  };
  return {
    x: Math.max(-1, Math.min(1, numeric("x", 0))),
    y: Math.max(-1, Math.min(1, numeric("y", 0))),
    scale: Math.max(0.05, Math.min(1, numeric("scale", 0.3))),
    spacing: Math.max(-0.1, Math.min(0.5, numeric("spacing", 0.02))),
  };
}

function drawNumberplateAtlasText(context, canvas, text, atlas, options = {}) {
  const digits = String(text || "").replace(/\D/g, "").slice(0, 3);
  if (!digits || !atlas?.image || !Array.isArray(atlas.glyphs) || atlas.glyphs.length < 10) return false;
  const glyphs = digits.split("").map((digit) => atlas.glyphs[Number(digit)]).filter(Boolean);
  if (glyphs.length !== digits.length) return false;
  const textLayer = coerceNumberplateTextLayer(options);
  const textColor = coerceNumberplateTextColor(options);
  const fillStyle = numberplateTextColorCss(textColor);
  const maxGlyphHeight = Math.max(...glyphs.map((glyph) => glyph.h || 1), 1);
  const targetHeight = canvas.height * textLayer.scale;
  const scale = targetHeight / maxGlyphHeight;
  const spacing = canvas.height * textLayer.spacing;
  const totalWidth = glyphs.reduce((sum, glyph) => sum + glyph.w * scale, 0) + spacing * Math.max(0, glyphs.length - 1);
  let x = canvas.width * (0.5 + textLayer.x) - totalWidth * 0.5;
  const centerY = canvas.height * (0.53 + textLayer.y);
  const smoothing = context.imageSmoothingEnabled;
  const smoothingQuality = context.imageSmoothingQuality;
  context.imageSmoothingEnabled = true;
  context.imageSmoothingQuality = "high";
  glyphs.forEach((glyph) => {
    const width = glyph.w * scale;
    const height = glyph.h * scale;
    const y = centerY - height * 0.5;
    if (fillStyle) {
      drawTintedGlyph(context, atlas.image, glyph, x, y, width, height, fillStyle);
    } else {
      context.drawImage(atlas.image, glyph.x, glyph.y, glyph.w, glyph.h, x, y, width, height);
    }
    x += width + spacing;
  });
  context.imageSmoothingEnabled = smoothing;
  context.imageSmoothingQuality = smoothingQuality;
  return true;
}

async function loadNumberplateTexture(backgroundPath, text = "", options = {}) {
  const image = await loadImageFromPath(backgroundPath, { cacheNamespace: "numberplate-background", cache: false });
  const canvas = document.createElement("canvas");
  const sourceWidth = image.naturalWidth || image.width || 512;
  const sourceHeight = image.naturalHeight || image.height || 512;
  const scale = Math.max(1, 512 / Math.max(sourceWidth, sourceHeight));
  canvas.width = Math.max(1, Math.round(sourceWidth * scale));
  canvas.height = Math.max(1, Math.round(sourceHeight * scale));
  const context = canvas.getContext("2d");
  context.clearRect(0, 0, canvas.width, canvas.height);
  if (options.forceOpaque) {
    context.fillStyle = "#ffffff";
    context.fillRect(0, 0, canvas.width, canvas.height);
  }
  context.drawImage(image, 0, 0, canvas.width, canvas.height);
  if (options.debugFrame) {
    context.lineWidth = Math.max(8, canvas.height * 0.05);
    context.strokeStyle = "#ff0033";
    context.strokeRect(context.lineWidth * 0.5, context.lineWidth * 0.5, canvas.width - context.lineWidth, canvas.height - context.lineWidth);
  }
  if (options.debugLabel) {
    drawNumberplateDebugMark(context, canvas, options.debugPlacement || {}, options.debugPlacementIndex || 0);
  }
  if (text) {
    let rendered = false;
    if (options.fontPath) {
      try {
        rendered = drawNumberplateAtlasText(context, canvas, text, await loadNumberplateFontAtlas(options.fontPath, options.fontKey), options);
      } catch (_error) {
        rendered = false;
      }
    }
    if (!rendered) drawFallbackNumberplateText(context, canvas, text, options);
  }
  const texture = new THREE.CanvasTexture(canvas);
  texture.colorSpace = THREE.SRGBColorSpace;
  texture.flipY = true;
  texture.needsUpdate = true;
  return {
    texture,
    width: canvas.width,
    height: canvas.height,
    aspect: canvas.width / Math.max(1, canvas.height),
  };
}

function disposeNumberplateRoot(root) {
  if (!root) return;
  state.scene?.remove(root);
  root.traverse?.((child) => {
    child.geometry?.dispose();
    const materials = Array.isArray(child.material) ? child.material : child.material ? [child.material] : [];
    materials.forEach((material) => {
      material.map?.dispose();
      material.dispose();
    });
  });
  if (!root.traverse) {
    root.geometry?.dispose();
    root.material?.map?.dispose();
    root.material?.dispose();
  }
}

function removeNumberplateSceneObjects() {
  if (!state.scene) return 0;
  const stale = [];
  state.scene.traverse((object) => {
    if (!object || object === state.scene) return;
    const name = String(object.name || "");
    if (name.startsWith("LiveryLab_Numberplate_")) stale.push(object);
  });
  const roots = stale.filter((object) => !stale.includes(object.parent));
  roots.forEach((root) => disposeNumberplateRoot(root));
  return roots.length;
}

function removeNumberplateOverlay(options = {}) {
  if (options.cancel !== false) state.numberplateRequestId += 1;
  detachNumberplateTuner();
  setNumberplateHover(null);
  setActiveNumberplateMesh(null);
  if (state.numberplate?.mesh) disposeNumberplateRoot(state.numberplate.mesh);
  removeNumberplateSceneObjects();
  state.numberplate = null;
}

function numberplateTunerTarget() {
  if (state.numberplate?.mode === "single" && state.numberplate?.mesh?.isMesh) return state.numberplate.mesh;
  if (state.numberplate?.mode === "mapped-group" && state.numberplateTuner.targetMesh?.parent) return state.numberplateTuner.targetMesh;
  return null;
}

function ensureNumberplateTransformControls() {
  if (state.numberplateTuner.controls) {
    const helper = state.numberplateTuner.helper || state.numberplateTuner.controls.getHelper?.();
    if (helper && helper.parent !== state.scene) {
      state.scene?.add(helper);
      state.numberplateTuner.helper = helper;
    }
    return state.numberplateTuner.controls;
  }
  if (!state.camera || !state.renderer) return null;
  const controls = new TransformControls(state.camera, state.renderer.domElement);
  controls.setMode(state.numberplateTuner.mode || "translate");
  controls.setSpace("local");
  controls.setSize(1);
  controls.addEventListener("dragging-changed", (event) => {
    state.numberplateTuner.dragging = Boolean(event.value);
    if (event.value) dispatchNumberplateTransformStart();
    if (state.controls) state.controls.enabled = !event.value;
  });
  controls.addEventListener("mouseDown", () => {
    state.numberplateTuner.controlPointerActive = true;
  });
  controls.addEventListener("mouseUp", () => {
    window.setTimeout(() => {
      state.numberplateTuner.controlPointerActive = false;
    }, 0);
  });
  controls.addEventListener("objectChange", () => {
    const target = numberplateTunerTarget();
    const anchor = state.numberplateTuner.anchor;
    if (!target || !anchor) return;
    target.position.copy(anchor.position);
    target.quaternion.copy(anchor.quaternion);
    target.scale.copy(anchor.scale);
    if (state.numberplateTuner.mode === "scale") {
      normalizeNumberplateMeshScale(target);
      anchor.scale.copy(target.scale);
    }
    state.numberplateTuner.dirty = true;
    dispatchNumberplateTunerChange();
  });
  const helper = controls.getHelper?.();
  if (helper) {
    helper.traverse?.((child) => {
      child.renderOrder = 2000;
      const materials = Array.isArray(child.material) ? child.material : child.material ? [child.material] : [];
      materials.forEach((material) => {
        material.depthTest = false;
        material.depthWrite = false;
        material.needsUpdate = true;
      });
    });
    state.scene?.add(helper);
    state.numberplateTuner.helper = helper;
  }
  state.numberplateTuner.controls = controls;
  return controls;
}

function dispatchNumberplateTunerChange() {
  window.dispatchEvent(new CustomEvent("liverylab-numberplate-tuner-change", { detail: getNumberplateTunerState() }));
}

function detachNumberplateTuner() {
  state.numberplateTuner.controls?.detach();
  if (state.numberplateTuner.anchor) {
    state.scene?.remove(state.numberplateTuner.anchor);
  }
  state.numberplateTuner.anchor = null;
}

function updateNumberplateTunerTarget() {
  detachNumberplateTuner();
  if (!state.numberplateTuner.enabled) return getNumberplateTunerState();
  const target = numberplateTunerTarget();
  if (!target) return getNumberplateTunerState();
  const anchor = new THREE.Object3D();
  anchor.name = "LiveryLab_Numberplate_Tuner_Anchor";
  anchor.position.copy(target.position);
  anchor.quaternion.copy(target.quaternion);
  anchor.scale.copy(target.scale);
  state.scene.add(anchor);
  state.numberplateTuner.anchor = anchor;
  const controls = ensureNumberplateTransformControls();
  controls?.setMode(state.numberplateTuner.mode || "translate");
  controls?.attach(anchor);
  return getNumberplateTunerState();
}

function setNumberplateTunerEnabled(enabled = true, mode = state.numberplateTuner.mode) {
  state.numberplateTuner.enabled = Boolean(enabled);
  state.numberplateTuner.mode = ["translate", "rotate", "scale"].includes(String(mode || "")) ? String(mode) : "translate";
  if (!state.numberplateTuner.enabled) {
    state.numberplateTuner.dragging = false;
    state.numberplateTuner.controlPointerActive = false;
    detachNumberplateTuner();
    return getNumberplateTunerState();
  }
  return updateNumberplateTunerTarget();
}

function setNumberplateTunerMode(mode = "translate") {
  state.numberplateTuner.mode = ["translate", "rotate", "scale"].includes(String(mode || "")) ? String(mode) : "translate";
  state.numberplateTuner.controls?.setMode(state.numberplateTuner.mode);
  return updateNumberplateTunerTarget();
}

function getNumberplateTunerState() {
  const target = numberplateTunerTarget();
  const helper = state.numberplateTuner.helper;
  return {
    enabled: Boolean(state.numberplateTuner.enabled),
    mode: state.numberplateTuner.mode || "translate",
    active: Boolean(target && state.numberplateTuner.anchor && state.numberplateTuner.enabled),
    hasTarget: Boolean(target),
    hasAnchor: Boolean(state.numberplateTuner.anchor),
    hasControls: Boolean(state.numberplateTuner.controls),
    numberplateMode: state.numberplate?.mode || "",
    helperInScene: Boolean(helper && helper.parent === state.scene),
    helperVisible: Boolean(helper?.visible),
    dirty: Boolean(state.numberplateTuner.dirty),
    transform: target ? transformFromObject(target) : null,
  };
}

function markNumberplateTunerSaved() {
  state.numberplateTuner.dirty = false;
  dispatchNumberplateTunerChange();
  return getNumberplateTunerState();
}

function updateSelectedNumberplateTransform(transform = {}) {
  const target = numberplateTunerTarget();
  if (!target) return { updated: false, transform: null };
  setObjectTransform(target, transform);
  normalizeNumberplateMeshScale(target);
  if (state.numberplateTuner.anchor) {
    state.numberplateTuner.anchor.position.copy(target.position);
    state.numberplateTuner.anchor.quaternion.copy(target.quaternion);
    state.numberplateTuner.anchor.scale.copy(target.scale);
  }
  state.numberplateTuner.dirty = true;
  dispatchNumberplateTunerChange();
  return { updated: true, transform: transformFromObject(target) };
}

function setObjectTransform(object, transform = {}) {
  object.position.set(
    Number(transform.x ?? 0),
    Number(transform.y ?? 0),
    Number(transform.z ?? 0),
  );
  object.rotation.set(
    THREE.MathUtils.degToRad(Number(transform.rx ?? 0)),
    THREE.MathUtils.degToRad(Number(transform.ry ?? 0)),
    THREE.MathUtils.degToRad(Number(transform.rz ?? 0)),
  );
  object.scale.set(
    Number(transform.sx ?? 1),
    Number(transform.sy ?? 0.28),
    1,
  );
}

function transformFromMapped(mapped) {
  return {
    x: mapped.position.x,
    y: mapped.position.y,
    z: mapped.position.z,
    rx: THREE.MathUtils.radToDeg(mapped.rotation.x),
    ry: THREE.MathUtils.radToDeg(mapped.rotation.y),
    rz: THREE.MathUtils.radToDeg(mapped.rotation.z),
    sx: mapped.scale.x,
    sy: mapped.scale.y,
  };
}

function transformFromObject(object) {
  return {
    x: object.position.x,
    y: object.position.y,
    z: object.position.z,
    rx: THREE.MathUtils.radToDeg(object.rotation.x),
    ry: THREE.MathUtils.radToDeg(object.rotation.y),
    rz: THREE.MathUtils.radToDeg(object.rotation.z),
    sx: object.scale.x,
    sy: object.scale.y,
  };
}

function normalizeNumberplateMeshScale(mesh) {
  if (!mesh) return;
  const aspect = Number(mesh.userData?.numberplateAspect || 0);
  if (!Number.isFinite(aspect) || aspect <= 0) {
    mesh.scale.z = 1;
    return;
  }
  const previous = mesh.userData?.numberplateLastScale || { x: mesh.scale.x, y: mesh.scale.y };
  const deltaX = Math.abs(Number(mesh.scale.x) - Number(previous.x || 0));
  const deltaY = Math.abs(Number(mesh.scale.y) - Number(previous.y || 0));
  const widthFromY = Math.abs(Number(mesh.scale.y || 0)) * aspect;
  const width = Math.max(0.001, deltaY > deltaX ? widthFromY : Math.abs(Number(mesh.scale.x || 0)));
  mesh.scale.set(width, width / aspect, 1);
  mesh.userData.numberplateLastScale = { x: mesh.scale.x, y: mesh.scale.y };
}

function mappedFromTransform(transform = {}) {
  return {
    position: new THREE.Vector3(
      Number(transform.x ?? 0),
      Number(transform.y ?? 0),
      Number(transform.z ?? 0),
    ),
    rotation: new THREE.Euler(
      THREE.MathUtils.degToRad(Number(transform.rx ?? 0)),
      THREE.MathUtils.degToRad(Number(transform.ry ?? 0)),
      THREE.MathUtils.degToRad(Number(transform.rz ?? 0)),
    ),
    scale: new THREE.Vector3(
      Number(transform.sx ?? 1),
      Number(transform.sy ?? 1),
      1,
    ),
  };
}

async function ensureNumberplateCalibrations() {
  if (state.numberplateCalibrationsLoaded) return state.numberplateCalibrations;
  if (!numberplateCalibrationsPromise) {
    numberplateCalibrationsPromise = fetch(NUMBERPLATE_CALIBRATIONS_URL, { cache: "no-store" })
      .then(async (response) => {
        if (!response.ok) throw new Error(`Calibration JSON not found (${response.status})`);
        const data = await response.json();
        return data?.cars && typeof data.cars === "object" ? data.cars : data || {};
      })
      .catch(() => ({}));
  }
  state.numberplateCalibrations = await numberplateCalibrationsPromise;
  state.numberplateCalibrationsLoaded = true;
  return state.numberplateCalibrations;
}

function calibrationForPlacement(placement = {}, options = {}) {
  if (options.ignoreCalibrations) return null;
  const customCalibrations = options.calibrations && typeof options.calibrations === "object" ? options.calibrations : null;
  const carId = String(options.carId || "").trim().toLowerCase();
  const label = String(placement.label || "").toUpperCase();
  const candidateKeys = calibrationCandidateKeys(placement);
  if (customCalibrations) {
    const custom = calibrationByKeys(customCalibrations, candidateKeys) || calibrationByActorBaseline(customCalibrations, placement, label) || null;
    if (custom) return custom;
  }
  if (!carId) return null;
  const carCalibration = state.numberplateCalibrations?.[carId];
  if (!carCalibration) return null;
  return calibrationByKeys(carCalibration, candidateKeys) || calibrationByActorBaseline(carCalibration, placement, label) || null;
}

function calibrationCandidateKeys(placement = {}) {
  const keys = [];
  const push = (value) => {
    const key = String(value || "").trim();
    if (key && !keys.includes(key)) keys.push(key);
  };
  const label = String(placement.label || "").toUpperCase();
  const actorIndex = placement.index ?? "";
  const surface = Number(placement.surface ?? placement.outer_field_number ?? 0);
  if (label && actorIndex !== "") push(`${label}:${actorIndex}`);
  push(placement.sourceCalibrationKey || placement.calibrationSourceKey || "");
  if (surface) {
    push(`SURFACE_${surface}`);
    push(`SURFACE:${surface}`);
    if (surface === 1) push("HOOD");
    if (surface === 3) push("DOOR_L");
    if (surface === 4) push("DOOR_R");
  }
  push(label);
  return keys;
}

function calibrationByKeys(calibrations = {}, keys = []) {
  for (const key of keys) {
    const row = calibrationByKey(calibrations, key);
    if (row) return row;
  }
  return null;
}

function calibrationByKey(calibrations = {}, key = "") {
  const normalized = String(key || "").trim();
  if (!normalized) return null;
  return calibrations[normalized] || calibrations[normalized.toUpperCase()] || null;
}

function actorNumberClose(left, right, tolerance = 0.005) {
  if (left == null || left === "" || right == null || right === "") return true;
  const a = Number(left);
  const b = Number(right);
  return Number.isFinite(a) && Number.isFinite(b) && Math.abs(a - b) <= tolerance;
}

function actorRotationClose(left, right) {
  return actorNumberClose(left, right, 1.5);
}

function calibrationByActorBaseline(calibrations = {}, placement = {}, label = "") {
  const expectedLabel = String(label || placement.label || "").toUpperCase();
  const surface = Number(placement.surface ?? placement.outer_field_number ?? 0);
  const position = Array.isArray(placement.position) ? placement.position : [];
  const rotation = Array.isArray(placement.rotation) ? placement.rotation : [];
  const scale = Array.isArray(placement.scale) ? placement.scale : [];
  for (const [key, rawRow] of Object.entries(calibrations || {})) {
    const rowKey = String(key || "").toUpperCase();
    if (!calibrationRowMatchesPlacement(rowKey, expectedLabel, surface)) continue;
    const row = rawRow?.transform || rawRow;
    const baseline = row?.actorBaseline;
    if (!baseline || typeof baseline !== "object") continue;
    const matches =
      actorNumberClose(baseline.position_x, position[0]) &&
      actorNumberClose(baseline.position_y, position[1]) &&
      actorNumberClose(baseline.position_z, position[2]) &&
      actorRotationClose(baseline.rotation_x, rotation[0]) &&
      actorRotationClose(baseline.rotation_y, rotation[1]) &&
      actorRotationClose(baseline.rotation_z, rotation[2]) &&
      actorNumberClose(baseline.scale_x, scale[0]) &&
      actorNumberClose(baseline.scale_y, scale[1]) &&
      actorNumberClose(baseline.scale_z, scale[2]);
    if (matches) return rawRow;
  }
  return null;
}

function calibrationRowMatchesPlacement(rowKey = "", label = "", surface = 0) {
  if (rowKey.startsWith("__")) return false;
  if (label && rowKey.startsWith(label)) return true;
  if (surface && (rowKey === `SURFACE_${surface}` || rowKey === `SURFACE:${surface}`)) return true;
  if (surface === 1 && rowKey === "HOOD") return true;
  if (surface === 3 && rowKey === "DOOR_L") return true;
  if (surface === 4 && rowKey === "DOOR_R") return true;
  return false;
}

function makePlacementDiagnosticTexture(text = "PLACEMENT", color = "#ff0033", secondary = "") {
  const canvas = document.createElement("canvas");
  canvas.width = 960;
  canvas.height = 320;
  const context = canvas.getContext("2d");
  context.fillStyle = color;
  context.fillRect(0, 0, canvas.width, canvas.height);
  context.fillStyle = "rgba(0,0,0,0.72)";
  context.fillRect(16, 16, canvas.width - 32, canvas.height - 32);
  context.strokeStyle = "#ffffff";
  context.lineWidth = 10;
  context.strokeRect(28, 28, canvas.width - 56, canvas.height - 56);
  context.fillStyle = "#ffffff";
  context.font = "900 62px Arial, sans-serif";
  context.textAlign = "center";
  context.textBaseline = "middle";
  context.fillText(String(text || "PLACEMENT"), canvas.width * 0.5, secondary ? canvas.height * 0.42 : canvas.height * 0.5);
  if (secondary) {
    context.font = "700 34px Arial, sans-serif";
    context.fillStyle = "rgba(255,255,255,0.86)";
    context.fillText(String(secondary), canvas.width * 0.5, canvas.height * 0.66);
  }
  const texture = new THREE.CanvasTexture(canvas);
  texture.colorSpace = THREE.SRGBColorSpace;
  texture.flipY = true;
  texture.needsUpdate = true;
  return texture;
}

function getDefaultNumberplateTransform() {
  const size = state.bounds?.size || new THREE.Vector3(2, 1, 4);
  const center = state.bounds?.center || new THREE.Vector3();
  const width = Math.max(size.x * 0.26, 0.55);
  const height = Math.max(width * 0.28, 0.16);
  return {
    x: center.x,
    y: center.y + size.y * 0.18,
    z: center.z + size.z * 0.51,
    rx: 0,
    ry: 0,
    rz: 0,
    sx: width,
    sy: height,
  };
}

function numberplateFitValue(value, fallback, min = -10, max = 10) {
  const numeric = Number(value);
  if (!Number.isFinite(numeric)) return fallback;
  return Math.max(min, Math.min(max, numeric));
}

function numberplateDoorPlacementMode(value) {
  return String(value || "").trim().toLowerCase() === "fallback" ? "fallback" : "actor";
}

function normalizeNumberplateFit(raw = {}) {
  const fit = raw && typeof raw === "object" ? raw : {};
  return {
    boundsScale: numberplateFitValue(fit.boundsScale, DEFAULT_NUMBERPLATE_FIT.boundsScale, -5, 5),
    widthScale: numberplateFitValue(fit.widthScale, DEFAULT_NUMBERPLATE_FIT.widthScale, -5, 5),
    lengthScale: numberplateFitValue(fit.lengthScale, DEFAULT_NUMBERPLATE_FIT.lengthScale, -5, 5),
    heightScale: numberplateFitValue(fit.heightScale, DEFAULT_NUMBERPLATE_FIT.heightScale, -5, 5),
    plateScale: numberplateFitValue(fit.plateScale, DEFAULT_NUMBERPLATE_FIT.plateScale, 0.1, 3),
    lengthDirection: numberplateFitValue(fit.lengthDirection, DEFAULT_NUMBERPLATE_FIT.lengthDirection, -1, 1) < 0 ? -1 : 1,
    xOffset: numberplateFitValue(fit.xOffset, DEFAULT_NUMBERPLATE_FIT.xOffset, -10, 10),
    yOffset: numberplateFitValue(fit.yOffset, DEFAULT_NUMBERPLATE_FIT.yOffset, -10, 10),
    zOffset: numberplateFitValue(fit.zOffset, DEFAULT_NUMBERPLATE_FIT.zOffset, -10, 10),
    hoodYOffset: numberplateFitValue(fit.hoodYOffset, DEFAULT_NUMBERPLATE_FIT.hoodYOffset, -10, 10),
    hoodZOffset: numberplateFitValue(fit.hoodZOffset, DEFAULT_NUMBERPLATE_FIT.hoodZOffset, -10, 10),
    hoodScale: numberplateFitValue(fit.hoodScale, DEFAULT_NUMBERPLATE_FIT.hoodScale, 0.1, 3),
    hoodPitch: numberplateFitValue(fit.hoodPitch, DEFAULT_NUMBERPLATE_FIT.hoodPitch, -180, 180),
    hoodYaw: numberplateFitValue(fit.hoodYaw, DEFAULT_NUMBERPLATE_FIT.hoodYaw, -180, 180),
    hoodRoll: numberplateFitValue(fit.hoodRoll, DEFAULT_NUMBERPLATE_FIT.hoodRoll, -180, 180),
    doorPitch: numberplateFitValue(fit.doorPitch, DEFAULT_NUMBERPLATE_FIT.doorPitch, -180, 180),
    doorYaw: numberplateFitValue(fit.doorYaw, DEFAULT_NUMBERPLATE_FIT.doorYaw, -180, 180),
    doorRoll: numberplateFitValue(fit.doorRoll, DEFAULT_NUMBERPLATE_FIT.doorRoll, -180, 180),
    doorPlacementMode: numberplateDoorPlacementMode(fit.doorPlacementMode || DEFAULT_NUMBERPLATE_FIT.doorPlacementMode),
  };
}

function numberplateAxisSign(value) {
  const numeric = Number(value);
  return Number.isFinite(numeric) && numeric < 0 ? -1 : 1;
}

function numberplateAxisMagnitude(value, fallback = 1) {
  const numeric = Math.abs(Number(value));
  return Number.isFinite(numeric) && numeric > 0.001 ? numeric : Math.abs(Number(fallback)) || 1;
}

function currentNumberplateFit() {
  state.numberplateFit = normalizeNumberplateFit(state.numberplateFit || DEFAULT_NUMBERPLATE_FIT);
  return { ...state.numberplateFit };
}

function setNumberplateFit(fit = {}) {
  state.numberplateFit = normalizeNumberplateFit({ ...currentNumberplateFit(), ...(fit || {}) });
  return currentNumberplateFit();
}

function resetNumberplateFit() {
  state.numberplateFit = normalizeNumberplateFit(DEFAULT_NUMBERPLATE_FIT);
  return currentNumberplateFit();
}

function carAxisInfo() {
  const baseSize = state.bounds?.size || new THREE.Vector3(2, 1, 4);
  const baseCenter = state.bounds?.center || new THREE.Vector3();
  const fit = currentNumberplateFit();
  const lengthAxis = baseSize.x >= baseSize.z ? "x" : "z";
  const widthAxis = lengthAxis === "x" ? "z" : "x";
  const boundsMagnitude = numberplateAxisMagnitude(fit.boundsScale, DEFAULT_NUMBERPLATE_FIT.boundsScale);
  const widthMagnitude = numberplateAxisMagnitude(fit.widthScale, DEFAULT_NUMBERPLATE_FIT.widthScale);
  const lengthMagnitude = numberplateAxisMagnitude(fit.lengthScale, DEFAULT_NUMBERPLATE_FIT.lengthScale);
  const heightMagnitude = numberplateAxisMagnitude(fit.heightScale, DEFAULT_NUMBERPLATE_FIT.heightScale);
  const boundsDirection = numberplateAxisSign(fit.boundsScale);
  const axisScale = {
    x: boundsMagnitude,
    y: boundsMagnitude * heightMagnitude,
    z: boundsMagnitude,
  };
  axisScale[widthAxis] = boundsMagnitude * widthMagnitude;
  axisScale[lengthAxis] = boundsMagnitude * lengthMagnitude;
  const axisDirection = {
    x: boundsDirection,
    y: boundsDirection * numberplateAxisSign(fit.heightScale),
    z: boundsDirection,
  };
  axisDirection[widthAxis] = boundsDirection * numberplateAxisSign(fit.widthScale);
  axisDirection[lengthAxis] = boundsDirection * numberplateAxisSign(fit.lengthScale) * fit.lengthDirection;
  const size = new THREE.Vector3(
    baseSize.x * axisScale.x,
    baseSize.y * axisScale.y,
    baseSize.z * axisScale.z,
  );
  const center = baseCenter.clone().add(new THREE.Vector3(fit.xOffset, fit.yOffset, fit.zOffset));
  const box = new THREE.Box3(
    center.clone().sub(size.clone().multiplyScalar(0.5)),
    center.clone().add(size.clone().multiplyScalar(0.5)),
  );
  box.min.copy(center).sub(size.clone().multiplyScalar(0.5));
  box.max.copy(center).add(size.clone().multiplyScalar(0.5));
  return { size, center, box, lengthAxis, widthAxis, axisDirection };
}

function mappedPosition(widthValue, heightValue, lengthValue) {
  const { center, lengthAxis, widthAxis, axisDirection } = carAxisInfo();
  const position = center.clone();
  position[widthAxis] += widthValue * axisDirection[widthAxis];
  position.y = center.y + (heightValue - center.y) * axisDirection.y;
  position[lengthAxis] += lengthValue * axisDirection[lengthAxis];
  return position;
}

function actorPlacementOptionalNumber(value) {
  if (value == null) return null;
  const numeric = Number(value);
  return Number.isFinite(numeric) ? numeric : null;
}

function numberplateProjectionOffsetVector(raw = {}) {
  const offset = raw && typeof raw === "object" ? raw : {};
  return new THREE.Vector3(
    actorPlacementOptionalNumber(offset.x) ?? 0,
    actorPlacementOptionalNumber(offset.y) ?? 0,
    actorPlacementOptionalNumber(offset.z) ?? 0,
  );
}

function numberplateSurfaceKind(placement = {}) {
  const label = String(placement.label || "").toUpperCase();
  const surface = Number(placement.surface ?? placement.outer_field_number ?? 0);
  if (label.includes("HOOD") || surface === 1) return "hood";
  if (label.includes("DOOR") || surface === 3 || surface === 4) return "door";
  return "other";
}

function numberplateExportFactors(calibration = {}, surfaceKind = "other") {
  const defaults = surfaceKind === "hood"
    ? {
      position_x: 0.5,
      position_y: 0.5,
      position_z: 0.5,
      rotation_x: 1,
      rotation_y: 1,
      scale_x: 1,
      scale_z: 1,
    }
    : surfaceKind === "door"
      ? {
        position_y: 0.5,
        position_z: 0.5,
        rotation_y: 1,
        scale_x: 1,
        scale_z: 1,
      }
      : {};
  const raw = calibration?.exportFactors && typeof calibration.exportFactors === "object" ? calibration.exportFactors : {};
  for (const key of Object.keys(defaults)) {
    const numeric = Number(raw[key]);
    if (Number.isFinite(numeric) && Math.abs(numeric) > 0.000001) defaults[key] = numeric;
  }
  return defaults;
}

function actorBaselineValue(baseline = {}, key = "", fallback = null) {
  const numeric = Number(baseline?.[key]);
  return Number.isFinite(numeric) ? numeric : fallback;
}

function hoodPreviewRotationTargetKey(calibration = {}) {
  const key = String(calibration?.previewRotationKey || calibration?.preview_rotation_key || "").trim().toLowerCase();
  return ["rx", "ry", "rz"].includes(key) ? key : "ry";
}

function applyActorDeltaToCalibratedTransform(placement = {}, calibrated = {}) {
  const transform = calibrated?.transform || calibrated || {};
  const baseline = transform.actorBaseline && typeof transform.actorBaseline === "object" ? transform.actorBaseline : null;
  if (!baseline) return mappedFromTransform(transform);

  const surfaceKind = numberplateSurfaceKind(placement);
  const factors = numberplateExportFactors(transform, surfaceKind);
  const rawPosition = Array.isArray(placement.position) ? placement.position : [];
  const rawRotation = Array.isArray(placement.rotation) ? placement.rotation : [];
  const rawScale = Array.isArray(placement.scale) ? placement.scale : [];
  const next = {
    x: Number(transform.x ?? 0),
    y: Number(transform.y ?? 0),
    z: Number(transform.z ?? 0),
    rx: Number(transform.rx ?? 0),
    ry: Number(transform.ry ?? 0),
    rz: Number(transform.rz ?? 0),
    sx: Number(transform.sx ?? 1),
    sy: Number(transform.sy ?? 1),
  };
  const addDelta = (targetKey, actorValue, baselineKey, factorKey) => {
    const current = actorPlacementOptionalNumber(actorValue);
    const base = actorBaselineValue(baseline, baselineKey, null);
    const factor = Number(factors[factorKey]);
    if (current == null || base == null || !Number.isFinite(factor) || Math.abs(factor) <= 0.000001) return;
    next[targetKey] += (current - base) / factor;
  };

  if (surfaceKind === "hood") {
    const rawExport = transform.exportFactors && typeof transform.exportFactors === "object" ? transform.exportFactors : {};
    const hoodRotationKey = rawExport.rotation_y != null && rawExport.rotation_y !== "" ? "rotation_y" : "rotation_x";
    const hoodRotationIndex = hoodRotationKey === "rotation_y" ? 1 : 0;
    const hoodPreviewRotationKey = hoodPreviewRotationTargetKey(transform);
    addDelta("x", rawPosition[0], "position_x", "position_x");
    addDelta("y", rawPosition[1], "position_y", "position_y");
    addDelta("z", rawPosition[2], "position_z", "position_z");
    addDelta(hoodPreviewRotationKey, rawRotation[hoodRotationIndex], hoodRotationKey, hoodRotationKey);
  } else if (surfaceKind === "door") {
    addDelta("y", rawPosition[1], "position_y", "position_y");
    addDelta("z", rawPosition[2], "position_z", "position_z");
    addDelta("rx", rawRotation[1], "rotation_y", "rotation_y");
  }
  addDelta("sx", rawScale[0], "scale_x", "scale_x");
  addDelta("sy", rawScale[2], "scale_z", "scale_z");
  return mappedFromTransform(next);
}

function mapActorPlacementTransform(placement = {}, index = 0, options = {}) {
  const calibrated = calibrationForPlacement(placement, options);
  if (calibrated) return mappedFromTransform(calibrated);

  const { size, center, lengthAxis, widthAxis } = carAxisInfo();
  const fit = currentNumberplateFit();
  const label = String(placement.label || "").toUpperCase();
  const rawPosition = placement.position || [0, 0, 0];
  const rawRotation = placement.rotation || [0, 0, 0];
  const rawScale = placement.scale || [1, 0.28, 1];
  const rawX = actorPlacementOptionalNumber(rawPosition[0]) ?? 0;
  const rawY = actorPlacementOptionalNumber(rawPosition[1]);
  const rawZ = actorPlacementOptionalNumber(rawPosition[2]) ?? 0;
  const rawRotationY = actorPlacementOptionalNumber(rawRotation[1]);
  const width = Math.max(size[widthAxis], 1);
  const height = Math.max(size.y, 1);
  const plateScaleFactor = fit.plateScale;
  const surfaceOffset = Math.max(height * 0.015, 0.015);
  const sideGap = width * 0.43;
  const heightValue = (fallback) => (rawY == null ? fallback : center.y + rawY);
  const transform = {
    position: mappedPosition(rawX, heightValue(center.y + height * 0.18), rawZ),
    rotation: new THREE.Euler(0, 0, 0),
    scale: new THREE.Vector3(1, 1, 1),
  };

  if (label.includes("HOOD")) {
    const hoodActorWidth = numberplateAxisMagnitude(rawScale[0], 0.36);
    const hoodActorHeight = numberplateAxisMagnitude(rawScale[2], rawScale[0] ?? 0.36);
    transform.position = mappedPosition(
      rawX,
      heightValue(center.y + height * 0.11 + surfaceOffset) + fit.hoodYOffset,
      rawZ + fit.hoodZOffset,
    );
    transform.rotation = new THREE.Euler(
      -Math.PI / 2 + THREE.MathUtils.degToRad(fit.hoodPitch),
      THREE.MathUtils.degToRad(fit.hoodYaw),
      THREE.MathUtils.degToRad((rawRotationY ?? 0) + fit.hoodRoll),
    );
    transform.scale.set(
      hoodActorWidth * plateScaleFactor * fit.hoodScale,
      hoodActorHeight * plateScaleFactor * fit.hoodScale,
      1,
    );
    return transform;
  }

  const isLeft = label.includes("_L") || label.endsWith("L");
  const isRight = label.includes("_R") || label.endsWith("R");
  const sideSign = isLeft ? 1 : isRight ? -1 : index % 2 ? -1 : 1;
  const doorActorWidth = numberplateAxisMagnitude(rawScale[0], 0.36);
  const doorActorHeight = numberplateAxisMagnitude(rawScale[2], rawScale[0] ?? 0.36);
  const useDoorFallback = fit.doorPlacementMode === "fallback";
  const doorHeightValue = useDoorFallback ? center.y - height * 0.13 : heightValue(center.y - height * 0.13);
  const doorLengthValue = useDoorFallback ? -0.25 : rawZ;
  transform.position = mappedPosition(sideSign * (sideGap + surfaceOffset), doorHeightValue, doorLengthValue);
  const fallbackDoorYaw = sideSign > 0 ? Math.PI / 2 : -Math.PI / 2;
  const doorYaw = useDoorFallback || rawRotationY == null ? fallbackDoorYaw : THREE.MathUtils.degToRad(rawRotationY);
  const doorPitchOffset = THREE.MathUtils.degToRad(fit.doorPitch);
  const doorYawOffset = THREE.MathUtils.degToRad(fit.doorYaw);
  const doorRollOffset = THREE.MathUtils.degToRad(fit.doorRoll * sideSign);
  if (widthAxis === "x") {
    transform.rotation = new THREE.Euler(doorPitchOffset, doorYaw + doorYawOffset, doorRollOffset);
  } else {
    transform.rotation = new THREE.Euler(doorPitchOffset, (sideSign > 0 ? 0 : Math.PI) + doorYawOffset, doorRollOffset);
  }
  transform.scale.set(doorActorWidth * plateScaleFactor, doorActorHeight * plateScaleFactor, 1);
  return transform;
}

function numberOrFallback(value, fallback = 0) {
  const numeric = Number(value);
  return Number.isFinite(numeric) ? numeric : fallback;
}

function actorPlacementRawTransform(placement = {}) {
  const position = Array.isArray(placement.position) ? placement.position : [];
  const rotation = Array.isArray(placement.rotation) ? placement.rotation : [];
  const scale = Array.isArray(placement.scale) ? placement.scale : [];
  return {
    position: new THREE.Vector3(
      numberOrFallback(position[0], 0),
      numberOrFallback(position[1], 0),
      numberOrFallback(position[2], 0),
    ),
    rotation: new THREE.Euler(
      THREE.MathUtils.degToRad(numberOrFallback(rotation[0], 0)),
      THREE.MathUtils.degToRad(numberOrFallback(rotation[1], 0)),
      THREE.MathUtils.degToRad(numberOrFallback(rotation[2], 0)),
    ),
    scale: new THREE.Vector3(
      numberOrFallback(scale[0], 1),
      numberOrFallback(scale[1], 0.28),
      1,
    ),
  };
}

function transformSummary(transform) {
  const position = transform.position;
  const rotation = transform.rotation;
  const scale = transform.scale;
  const fmt = (value) => Math.round(Number(value || 0) * 1000) / 1000;
  return {
    position: [fmt(position.x), fmt(position.y), fmt(position.z)],
    rotation: [
      fmt(THREE.MathUtils.radToDeg(rotation.x)),
      fmt(THREE.MathUtils.radToDeg(rotation.y)),
      fmt(THREE.MathUtils.radToDeg(rotation.z)),
    ],
    scale: [fmt(scale.x), fmt(scale.y), fmt(scale.z)],
  };
}

function placementDiagnosticsRow(placement, index, rawTransform, mappedTransform) {
  return {
    index: placement.index ?? index,
    label: placement.label || `Placement ${index}`,
    surface: placement.surface ?? placement.outer_field_number ?? "",
    numberplateIndex: placement.numberplate_index ?? placement.field_2 ?? "",
    actor: {
      position: placement.position || [],
      rotation: placement.rotation || [],
      scale: placement.scale || [],
    },
    rawPreview: transformSummary(rawTransform),
    mappedPreview: transformSummary(mappedTransform),
  };
}

function normalizedOpacity(value) {
  const numeric = Number(value);
  if (!Number.isFinite(numeric)) return 1;
  return Math.max(0.05, Math.min(1, numeric));
}

async function showNumberplateOverlay(options = {}) {
  if (!state.scene || !state.model) throw new Error("3D model is not loaded");
  const backgroundPath = String(options.backgroundPath || "").trim();
  if (!backgroundPath) throw new Error("Numberplate background PNG is missing");
  removeNumberplateOverlay();
  const requestId = state.numberplateRequestId;
  const texture = (await loadNumberplateTexture(backgroundPath, options.text || "", {
    fontPath: options.fontPath || "",
    fontKey: options.fontKey || options.fontPath || "",
    textLayer: options.textLayer || options.textPlacement || {},
    textColor: options.textColor || options.fontColor || options.overlay || null,
    forceOpaque: Boolean(options.forceOpaque),
  })).texture;
  if (requestId !== state.numberplateRequestId) {
    texture.dispose();
    return { visible: false, cancelled: true };
  }
  const geometry = new THREE.PlaneGeometry(1, 1);
  const material = createNumberplateMaterial(texture, options);
  const mesh = new THREE.Mesh(geometry, material);
  mesh.name = "LiveryLab_Numberplate_Overlay";
  mesh.castShadow = true;
  mesh.receiveShadow = true;
  mesh.userData.numberplateAspect = texture.image?.width && texture.image?.height ? texture.image.width / Math.max(1, texture.image.height) : 1;
  mesh.renderOrder = options.occludeByModel ? 10 : 999;
  setObjectTransform(mesh, options.transform || getDefaultNumberplateTransform());
  normalizeNumberplateMeshScale(mesh);
  state.scene.add(mesh);
  state.numberplate = { mesh, backgroundPath, fontPath: options.fontPath || "", text: options.text || "", mode: "single" };
  applyNumberplateSceneVisibility();
  if (state.numberplateTuner.enabled) updateNumberplateTunerTarget();
  return { visible: true, backgroundPath, text: options.text || "", mode: "single" };
}

function updateNumberplateOverlay(options = {}) {
  if (!state.numberplate?.mesh) return { visible: false };
  if (state.numberplate.mode !== "single") {
    return { visible: true, mode: state.numberplate.mode || "group", updated: false };
  }
  if (options.transform) {
    setObjectTransform(state.numberplate.mesh, options.transform);
    normalizeNumberplateMeshScale(state.numberplate.mesh);
  }
  if (state.numberplate.mesh.material && options.opacity != null) {
    state.numberplate.mesh.material.opacity = normalizedOpacity(options.opacity);
    state.numberplate.mesh.material.needsUpdate = true;
  }
  if (state.numberplateTuner.enabled) updateNumberplateTunerTarget();
  return { visible: true, mode: "single", updated: true };
}

function hasNumberplateOverlay() {
  return {
    visible: Boolean(state.numberplate?.mesh) && state.numberplate.mesh.visible !== false,
    mode: state.numberplate?.mode || "",
    single: state.numberplate?.mode === "single",
  };
}

async function showNumberplatePlacementDiagnostics(options = {}) {
  if (!state.scene || !state.model) throw new Error("3D model is not loaded");
  const placements = Array.isArray(options.placements) ? options.placements : [];
  if (!placements.length) throw new Error("No numberplate placements are available");
  const mode = ["raw", "mapped", "compare"].includes(String(options.mode || "")) ? String(options.mode) : "compare";
  const backgroundPath = String(options.backgroundPath || "").trim();
  let backgroundAspect = null;
  if (backgroundPath) {
    try {
      const loaded = await loadNumberplateTexture(backgroundPath, "", {
        forceOpaque: Boolean(options.forceOpaque),
      });
      backgroundAspect = loaded.aspect;
      loaded.texture.dispose();
    } catch (_error) {
      backgroundAspect = null;
    }
  }
  removeNumberplateOverlay();
  const requestId = state.numberplateRequestId;
  await ensureNumberplateCalibrations();
  if (requestId !== state.numberplateRequestId) return { visible: false, cancelled: true, placements: 0, mode, rows: [] };
  const rawColor = "#ff3d48";
  const mappedColor = "#17c3b2";
  const group = new THREE.Group();
  group.name = "LiveryLab_Numberplate_Placement_Diagnostics";
  const rows = [];

  const addDiagnosticMesh = (placement, index, transform, kind, color, offset = 0) => {
    const label = String(placement.label || `Placement ${index}`);
    const texture = makePlacementDiagnosticTexture(
      `${kind.toUpperCase()} ${label}`,
      color,
      `actor #${placement.index ?? index} surface ${placement.surface ?? placement.outer_field_number ?? "-"}`,
    );
    const material = new THREE.MeshBasicMaterial({
      map: texture,
      transparent: true,
      opacity: mode === "compare" ? 0.72 : 0.94,
      side: THREE.DoubleSide,
      depthWrite: false,
      depthTest: false,
    });
    const mesh = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), material);
    mesh.name = `LiveryLab_Numberplate_Diagnostic_${kind}_${label}`;
    mesh.renderOrder = 1000 + index * 4 + (kind === "mapped" ? 2 : 1);
    mesh.position.copy(transform.position);
    mesh.rotation.copy(transform.rotation);
    mesh.scale.copy(transform.scale);
    if (offset) {
      const normal = new THREE.Vector3(0, 0, 1).applyEuler(mesh.rotation).normalize();
      mesh.position.add(normal.multiplyScalar(offset));
    }
    group.add(mesh);
  };

  placements.forEach((placement, index) => {
    if (requestId !== state.numberplateRequestId) return;
    const raw = actorPlacementRawTransform(placement);
    const mapped = mapActorPlacementTransform(placement, index, options);
    if (backgroundAspect) {
      const width = Math.max(mapped.scale.x, mapped.scale.y * backgroundAspect);
      mapped.scale.set(width, width / Math.max(0.1, backgroundAspect), 1);
    }
    rows.push(placementDiagnosticsRow(placement, index, raw, mapped));
    if (mode === "raw" || mode === "compare") addDiagnosticMesh(placement, index, raw, "raw", rawColor, mode === "compare" ? 0.01 : 0);
    if (mode === "mapped" || mode === "compare") addDiagnosticMesh(placement, index, mapped, "mapped", mappedColor, mode === "compare" ? 0.025 : 0);
  });
  if (requestId !== state.numberplateRequestId) {
    disposeNumberplateRoot(group);
    return { visible: false, cancelled: true, placements: 0, mode, rows: [] };
  }
  state.scene.add(group);
  state.numberplate = { mesh: group, diagnosticPlacements: placements.length, mode: `diagnostics-${mode}` };
  return { visible: true, placements: placements.length, mode, rows };
}

async function getMappedNumberplateTransform(options = {}) {
  const placement = options.placement || {};
  const index = Number(options.index || 0);
  await ensureNumberplateCalibrations();
  const mapped = mapActorPlacementTransform(placement, index, options);
  const backgroundPath = String(options.backgroundPath || "").trim();
  if (backgroundPath) {
    const loaded = await loadNumberplateTexture(backgroundPath, "", {
      forceOpaque: Boolean(options.forceOpaque),
    });
    const width = Math.max(mapped.scale.x, mapped.scale.y * loaded.aspect);
    mapped.scale.set(width, width / Math.max(0.1, loaded.aspect), 1);
  }
  return transformFromMapped(mapped);
}

async function showMappedNumberplates(options = {}) {
  if (!state.scene || !state.model) throw new Error("3D model is not loaded");
  const placements = Array.isArray(options.placements) ? options.placements : [];
  if (!placements.length) throw new Error("No numberplate placements are available");
  const backgroundPath = String(options.backgroundPath || "").trim();
  if (!backgroundPath && !placements.some((placement) => String(placement?.backgroundPng || "").trim())) {
    throw new Error("Numberplate background PNG is missing");
  }
  removeNumberplateOverlay();
  const requestId = state.numberplateRequestId;
  await ensureNumberplateCalibrations();
  if (requestId !== state.numberplateRequestId) return { visible: false, cancelled: true, placements: 0, backgroundPath, backgroundLabel: "" };
  const group = new THREE.Group();
  group.name = "LiveryLab_Numberplate_Mapped";
  const usedBackgrounds = new Set();
  for (const [index, placement] of placements.entries()) {
    const placementBackgroundPath = String(placement?.backgroundPng || backgroundPath || "").trim();
    if (!placementBackgroundPath) continue;
    usedBackgrounds.add(String(placement?.backgroundRel || placementBackgroundPath));
    const loaded = await loadNumberplateTexture(placementBackgroundPath, options.text || "", {
      fontPath: placement?.fontPng || options.fontPath || "",
      fontKey: placement?.fontRel || options.fontKey || placement?.fontPng || options.fontPath || "",
      textLayer: options.textLayer || options.textPlacement || {},
      textColor: placement?.textColor || placement?.overlay || options.textColor || options.fontColor || options.overlay || null,
      forceOpaque: Boolean(options.forceOpaque),
      debugLabel: Boolean(options.debugLabels),
      debugPlacement: placement,
      debugPlacementIndex: index,
    });
    if (requestId !== state.numberplateRequestId) {
      loaded.texture?.dispose?.();
      disposeNumberplateRoot(group);
      return { visible: false, cancelled: true, placements: 0, backgroundPath, backgroundLabel: "" };
    }
    const texture = loaded.texture;
    const material = createNumberplateMaterial(texture, options);
    const mesh = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), material);
    mesh.name = `LiveryLab_Numberplate_Mapped_${placement.label || index}`;
    mesh.castShadow = true;
    mesh.receiveShadow = true;
    mesh.userData.numberplatePickable = true;
    mesh.userData.numberplatePlacementIndex = index;
    mesh.userData.numberplateActorIndex = placement.index ?? index;
    mesh.userData.numberplateLabel = placement.label || "";
    mesh.userData.numberplateSurface = placement.surface ?? placement.outer_field_number ?? "";
    mesh.userData.numberplateIndex = placement.numberplate_index ?? placement.field_2 ?? "";
    mesh.renderOrder = options.occludeByModel ? 10 + index : 1000 + index;
    const mapped = mapActorPlacementTransform(placement, index, options);
    const width = Math.max(mapped.scale.x, mapped.scale.y * loaded.aspect);
    mapped.scale.set(width, width / Math.max(0.1, loaded.aspect), 1);
    mesh.position.copy(mapped.position);
    mesh.rotation.copy(mapped.rotation);
    mesh.scale.copy(mapped.scale);
    group.add(mesh);
  }
  if (requestId !== state.numberplateRequestId) {
    disposeNumberplateRoot(group);
    return { visible: false, cancelled: true, placements: 0, backgroundPath, backgroundLabel: "" };
  }
  state.scene.add(group);
  state.numberplate = { mesh: group, backgroundPath, fontPath: options.fontPath || "", text: options.text || "", mappedPlacements: group.children.length, mode: "mapped-group" };
  applyNumberplateSceneVisibility();
  return {
    visible: true,
    placements: group.children.length,
    backgroundPath,
    backgroundLabel: Array.from(usedBackgrounds).join(" | "),
  };
}

async function showNumberplatePreviewActorOverlay(options = {}) {
  if (!state.scene || !state.model) throw new Error("3D model is not loaded");
  const placements = Array.isArray(options.placements) ? options.placements : [];
  if (!placements.length) throw new Error("No numberplate placements are available");
  const backgroundPath = String(options.backgroundPath || "").trim();
  if (!backgroundPath && !placements.some((placement) => String(placement?.backgroundPng || "").trim())) {
    throw new Error("Numberplate background PNG is missing");
  }
  removeNumberplateOverlay();
  const requestId = state.numberplateRequestId;
  await ensureNumberplateCalibrations();
  if (requestId !== state.numberplateRequestId) return { visible: false, cancelled: true, placements: 0, backgroundPath, backgroundLabel: "" };

  const group = new THREE.Group();
  group.name = "LiveryLab_Numberplate_Preview_Actor_Overlay";
  const usedBackgrounds = new Set();
  const actorProjectionOffset = numberplateProjectionOffsetVector(options.actorProjectionOffset);
  const actorCalibrations = options.actorCalibrations && typeof options.actorCalibrations === "object" ? options.actorCalibrations : null;
  const actorOptions = actorCalibrations ? { ...options, calibrations: actorCalibrations } : options;
  const actorGhostOffsets = options.actorGhostOffsets && typeof options.actorGhostOffsets === "object" ? options.actorGhostOffsets : {};
  const actorGhostRows = [];
  let rendered = 0;

  const applyTextureAspect = (mapped, aspect) => {
    const width = Math.max(mapped.scale.x, mapped.scale.y * aspect);
    mapped.scale.set(width, width / Math.max(0.1, aspect), 1);
    return mapped;
  };
  const roundValue = (value) => {
    const numeric = Number(value);
    return Number.isFinite(numeric) ? Math.round(numeric * 1000000) / 1000000 : null;
  };
  const actorGhostOffsetValue = (value, fallback = 0) => {
    const numeric = Number(value);
    return Number.isFinite(numeric) ? numeric : fallback;
  };
  const normalizeActorGhostOffset = (raw = {}) => {
    const offset = raw && typeof raw === "object" ? raw : {};
    return {
      x: actorGhostOffsetValue(offset.x),
      y: actorGhostOffsetValue(offset.y),
      z: actorGhostOffsetValue(offset.z),
      rx: actorGhostOffsetValue(offset.rx),
      ry: actorGhostOffsetValue(offset.ry),
      rz: actorGhostOffsetValue(offset.rz),
      sx: actorGhostOffsetValue(offset.sx),
      sy: actorGhostOffsetValue(offset.sy),
    };
  };
  const actorGhostOffsetHasValue = (offset = {}) => Object.values(normalizeActorGhostOffset(offset)).some((value) => Math.abs(Number(value) || 0) > 0.000001);
  const actorGhostOffsetForPlacement = (placement = {}, index = 0) => {
    const label = String(placement.label || "").trim();
    const upperLabel = label.toUpperCase();
    const actorIndex = placement.index ?? index;
    const surface = placement.surface ?? placement.outer_field_number ?? "";
    const keys = [
      label && actorIndex !== "" && actorIndex != null ? `${label}:${actorIndex}` : "",
      upperLabel && actorIndex !== "" && actorIndex != null ? `${upperLabel}:${actorIndex}` : "",
      label,
      upperLabel,
      surface !== "" ? `SURFACE_${surface}` : "",
      surface !== "" ? `SURFACE:${surface}` : "",
    ].filter(Boolean);
    for (const key of keys) {
      const offset = actorGhostOffsets[key] || actorGhostOffsets[String(key).toUpperCase()];
      if (offset && typeof offset === "object") return normalizeActorGhostOffset(offset);
    }
    return normalizeActorGhostOffset();
  };
  const applyActorGhostOffset = (mapped, offset = {}) => {
    const normalized = normalizeActorGhostOffset(offset);
    const next = {
      position: mapped.position.clone(),
      rotation: mapped.rotation.clone(),
      scale: mapped.scale.clone(),
    };
    next.position.add(new THREE.Vector3(normalized.x, normalized.y, normalized.z));
    next.rotation.x += THREE.MathUtils.degToRad(normalized.rx);
    next.rotation.y += THREE.MathUtils.degToRad(normalized.ry);
    next.rotation.z += THREE.MathUtils.degToRad(normalized.rz);
    next.scale.x = Math.max(0.001, next.scale.x + normalized.sx);
    next.scale.y = Math.max(0.001, next.scale.y + normalized.sy);
    return next;
  };
  const roundTransform = (transform = {}) => Object.fromEntries(
    ["x", "y", "z", "rx", "ry", "rz", "sx", "sy"].map((key) => [key, roundValue(transform[key])]),
  );
  const transformDelta = (preview = {}, actor = {}) => Object.fromEntries(
    ["x", "y", "z", "rx", "ry", "rz", "sx", "sy"].map((key) => [key, roundValue((actor[key] ?? 0) - (preview[key] ?? 0))]),
  );
  const maxAbs = (values = []) => values.reduce((max, value) => Math.max(max, Math.abs(Number(value) || 0)), 0);
  const rotationAngleDeltaDegrees = (previewRotation, actorRotation) => {
    const previewQuaternion = new THREE.Quaternion().setFromEuler(previewRotation);
    const actorQuaternion = new THREE.Quaternion().setFromEuler(actorRotation);
    return roundValue(THREE.MathUtils.radToDeg(previewQuaternion.angleTo(actorQuaternion)));
  };
  const actorGhostRoundtripRow = (placement, index, previewMapped, actorMapped, actorBaseMapped, calibration, ghostOffset) => {
    const preview = roundTransform(transformFromMapped(previewMapped));
    const actor = roundTransform(transformFromMapped(actorMapped));
    const actorBase = roundTransform(transformFromMapped(actorBaseMapped));
    const delta = transformDelta(preview, actor);
    const rotationDeltaDegrees = rotationAngleDeltaDegrees(previewMapped.rotation, actorMapped.rotation);
    const maxDelta = {
      position: roundValue(maxAbs([delta.x, delta.y, delta.z])),
      rotation: rotationDeltaDegrees,
      scale: roundValue(maxAbs([delta.sx, delta.sy])),
    };
    return {
      label: placement.label || "",
      actorIndex: placement.index ?? index,
      surface: placement.surface ?? placement.outer_field_number ?? "",
      calibration: calibration ? "matched" : "fallback",
      rotationCompare: "quaternion",
      roundtripMatch: maxDelta.position <= 0.0005 && maxDelta.rotation <= 0.05 && maxDelta.scale <= 0.0005,
      maxDelta,
      delta,
      preview,
      actor,
      actorBase,
      actorGhostOffset: roundTransform(ghostOffset),
      actorGhostOffsetActive: actorGhostOffsetHasValue(ghostOffset),
      actorRaw: {
        position: Array.isArray(placement.position) ? placement.position.map(roundValue) : [],
        rotation: Array.isArray(placement.rotation) ? placement.rotation.map(roundValue) : [],
        scale: Array.isArray(placement.scale) ? placement.scale.map(roundValue) : [],
      },
    };
  };
  const offsetAlongNormal = (mesh, amount) => {
    const normal = new THREE.Vector3(0, 0, 1).applyEuler(mesh.rotation).normalize();
    mesh.position.add(normal.multiplyScalar(amount));
  };

  for (const [index, placement] of placements.entries()) {
    const placementBackgroundPath = String(placement?.backgroundPng || backgroundPath || "").trim();
    if (!placementBackgroundPath) continue;
    usedBackgrounds.add(String(placement?.backgroundRel || placementBackgroundPath));
    const loaded = await loadNumberplateTexture(placementBackgroundPath, options.text || "", {
      fontPath: placement?.fontPng || options.fontPath || "",
      fontKey: placement?.fontRel || options.fontKey || placement?.fontPng || options.fontPath || "",
      textLayer: options.textLayer || options.textPlacement || {},
      textColor: placement?.textColor || placement?.overlay || options.textColor || options.fontColor || options.overlay || null,
      forceOpaque: Boolean(options.forceOpaque),
      debugLabel: Boolean(options.debugLabels),
      debugPlacement: placement,
      debugPlacementIndex: index,
    });
    if (requestId !== state.numberplateRequestId) {
      loaded.texture?.dispose?.();
      disposeNumberplateRoot(group);
      return { visible: false, cancelled: true, placements: 0, backgroundPath, backgroundLabel: "" };
    }

    const previewMaterial = createNumberplateMaterial(loaded.texture, { ...options, opacity: options.previewOpacity ?? 0.84, occludeByModel: false });
    previewMaterial.color.set("#ffffff");
    const previewMesh = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), previewMaterial);
    previewMesh.name = `LiveryLab_Numberplate_Preview_Target_${placement.label || index}`;
    previewMesh.userData.numberplatePickable = true;
    previewMesh.userData.numberplatePlacementIndex = index;
    previewMesh.userData.numberplateActorIndex = placement.index ?? index;
    previewMesh.userData.numberplateLabel = placement.label || "";
    previewMesh.userData.numberplateSurface = placement.surface ?? placement.outer_field_number ?? "";
    previewMesh.renderOrder = 1200 + index * 4;
    const previewMapped = applyTextureAspect(mapActorPlacementTransform(placement, index, options), loaded.aspect);
    previewMesh.position.copy(previewMapped.position);
    previewMesh.rotation.copy(previewMapped.rotation);
    previewMesh.scale.copy(previewMapped.scale);
    offsetAlongNormal(previewMesh, 0.003);
    group.add(previewMesh);

    const actorTexture = loaded.texture.clone();
    actorTexture.needsUpdate = true;
    const actorMaterial = createNumberplateMaterial(actorTexture, { ...options, opacity: options.actorOpacity ?? 0.48, occludeByModel: false });
    actorMaterial.color.set("#59c7ff");
    const actorMesh = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), actorMaterial);
    actorMesh.name = `LiveryLab_Numberplate_Actor_Ghost_${placement.label || index}`;
    actorMesh.userData.numberplatePickable = false;
    actorMesh.userData.numberplatePlacementIndex = index;
    actorMesh.userData.numberplateActorIndex = placement.index ?? index;
    actorMesh.userData.numberplateLabel = placement.label || "";
    actorMesh.userData.numberplateSurface = placement.surface ?? placement.outer_field_number ?? "";
    actorMesh.renderOrder = 1202 + index * 4;
    const calibration = calibrationForPlacement(placement, actorOptions);
    const actorBaseMapped = applyTextureAspect(
      calibration
        ? applyActorDeltaToCalibratedTransform(placement, calibration)
        : mapActorPlacementTransform(placement, index, { ...options, calibrations: {}, ignoreCalibrations: true }),
      loaded.aspect,
    );
    const actorGhostOffset = actorGhostOffsetForPlacement(placement, index);
    const actorMapped = applyActorGhostOffset(actorBaseMapped, actorGhostOffset);
    actorGhostRows.push(actorGhostRoundtripRow(placement, index, previewMapped, actorMapped, actorBaseMapped, calibration, actorGhostOffset));
    actorMapped.position.add(actorProjectionOffset);
    actorMesh.position.copy(actorMapped.position);
    actorMesh.rotation.copy(actorMapped.rotation);
    actorMesh.scale.copy(actorMapped.scale);
    offsetAlongNormal(actorMesh, 0.006);
    group.add(actorMesh);
    rendered += 1;
  }

  if (requestId !== state.numberplateRequestId) {
    disposeNumberplateRoot(group);
    return { visible: false, cancelled: true, placements: 0, backgroundPath, backgroundLabel: "" };
  }
  state.scene.add(group);
  state.numberplate = { mesh: group, backgroundPath, fontPath: options.fontPath || "", text: options.text || "", mappedPlacements: rendered, mode: "preview-actor-overlay" };
  applyNumberplateSceneVisibility();
  return {
    visible: true,
    placements: rendered,
    backgroundPath,
    backgroundLabel: Array.from(usedBackgrounds).join(" | "),
    actorCalibrationSource: actorCalibrations ? "built-in" : "combined",
    actorGhostDiagnostics: {
      actorCalibrationSource: actorCalibrations ? "built-in" : "combined",
      roundtripMatch: actorGhostRows.length > 0 && actorGhostRows.every((row) => row.roundtripMatch),
      tolerance: {
        position: 0.0005,
        rotationDegrees: 0.05,
        scale: 0.0005,
      },
      note: "Roundtrip comparison includes Actor Ghost Tuner offsets and ignores Actor Projection Offset.",
      actorGhostOffsetCount: actorGhostRows.filter((row) => row.actorGhostOffsetActive).length,
      rows: actorGhostRows,
    },
    actorProjectionOffset: {
      x: actorProjectionOffset.x,
      y: actorProjectionOffset.y,
      z: actorProjectionOffset.z,
    },
  };
}

function setModelVisible(visible = true) {
  if (state.exteriorModel) state.exteriorModel.visible = Boolean(visible);
  return { visible: Boolean(state.exteriorModel?.visible) };
}

function setAuxiliaryVisible(visible = true) {
  const next = Boolean(visible);
  state.auxiliaryModels.forEach((object) => {
    object.visible = next;
  });
  return { visible: next, count: state.auxiliaryModels.length };
}

function wheelObjectMatchesTarget(object, target) {
  const label = String(object?.userData?.wheelLabel || "").toLowerCase();
  if (!label) return false;
  if (target === "all") return true;
  if (target === "front") return label.endsWith("_lf") || label.endsWith("_rf");
  if (target === "rear") return label.endsWith("_lr") || label.endsWith("_rr");
  return label.endsWith(`_${target}`);
}

function wheelTunerSelectedObjects() {
  const target = String(state.wheelTuner.target || "all");
  return state.auxiliaryModels.filter((object) => wheelObjectMatchesTarget(object, target));
}

function ensureWheelTransformControls() {
  if (state.wheelTuner.controls) {
    const helper = state.wheelTuner.helper || state.wheelTuner.controls.getHelper?.();
    if (helper && helper.parent !== state.scene) {
      state.scene?.add(helper);
      state.wheelTuner.helper = helper;
    }
    return state.wheelTuner.controls;
  }
  if (!state.camera || !state.renderer) return null;
  const controls = new TransformControls(state.camera, state.renderer.domElement);
  controls.setMode("translate");
  controls.setSpace("world");
  controls.setSize(0.8);
  controls.addEventListener("dragging-changed", (event) => {
    if (state.controls) state.controls.enabled = !event.value;
  });
  controls.addEventListener("objectChange", () => {
    const anchor = state.wheelTuner.anchor;
    if (!anchor) return;
    const delta = anchor.position.clone().sub(state.wheelTuner.lastPosition);
    if (delta.lengthSq() < 1e-12) return;
    state.wheelTuner.selected.forEach((object) => object.position.add(delta));
    state.wheelTuner.lastPosition.copy(anchor.position);
    state.wheelTuner.dirty = true;
    refreshGroundedSceneElements();
    dispatchWheelTunerChange();
  });
  const helper = controls.getHelper?.();
  if (helper) {
    state.scene?.add(helper);
    state.wheelTuner.helper = helper;
  }
  state.wheelTuner.controls = controls;
  return controls;
}

function dispatchWheelTunerChange() {
  window.dispatchEvent(new CustomEvent("liverylab-wheel-tuner-change", { detail: getWheelTunerState() }));
}

function removeWheelTunerAnchor() {
  if (state.wheelTuner.controls) {
    state.wheelTuner.controls.detach();
  }
  if (state.wheelTuner.anchor) {
    state.scene?.remove(state.wheelTuner.anchor);
  }
  state.wheelTuner.anchor = null;
  state.wheelTuner.selected = [];
}

function updateWheelTunerSelection() {
  removeWheelTunerAnchor();
  if (!state.wheelTuner.enabled || !state.scene || !state.auxiliaryModels.length) {
    return getWheelTunerState();
  }
  const selected = wheelTunerSelectedObjects();
  state.wheelTuner.selected = selected;
  if (!selected.length) return getWheelTunerState();
  const center = new THREE.Vector3();
  selected.forEach((object) => center.add(object.position));
  center.multiplyScalar(1 / selected.length);
  const anchor = new THREE.Object3D();
  anchor.name = "LiveryLab_Wheel_Tuner_Anchor";
  anchor.position.copy(center);
  state.scene.add(anchor);
  state.wheelTuner.anchor = anchor;
  state.wheelTuner.lastPosition.copy(center);
  ensureWheelTransformControls()?.attach(anchor);
  return getWheelTunerState();
}

function setWheelTunerTarget(target = "all") {
  state.wheelTuner.target = String(target || "all");
  return updateWheelTunerSelection();
}

function setWheelTunerEnabled(enabled = true, target = state.wheelTuner.target) {
  state.wheelTuner.enabled = Boolean(enabled);
  state.wheelTuner.target = String(target || state.wheelTuner.target || "all");
  if (!state.wheelTuner.enabled) {
    removeWheelTunerAnchor();
    return getWheelTunerState();
  }
  return updateWheelTunerSelection();
}

function wheelObjectOffset(object) {
  const base = object?.userData?.wheelBaseTransform || {};
  return {
    x: Number(object.position.x) - Number(base.x || 0),
    y: Number(object.position.y) - Number(base.y || 0),
    z: Number(object.position.z) - Number(base.z || 0),
  };
}

function getWheelCalibrationOverrides() {
  const overrides = {};
  state.auxiliaryModels.forEach((object) => {
    const label = String(object?.userData?.wheelLabel || "").trim();
    if (!label) return;
    const offset = wheelObjectOffset(object);
    const base = object?.userData?.wheelCalibrationBaseOverride || {};
    const row = {};
    ["x", "y", "z"].forEach((axis) => {
      const baseValue = Number(base[axis] || 0);
      const value = baseValue + Number(offset[axis] || 0);
      if (Math.abs(value) >= 0.0001 || Math.abs(baseValue) >= 0.0001 || Math.abs(offset[axis]) >= 0.0001) {
        row[axis] = Number(value.toFixed(5));
      }
    });
    if (Object.keys(row).length) overrides[label] = row;
  });
  return overrides;
}

function getWheelTunerState() {
  return {
    enabled: Boolean(state.wheelTuner.enabled),
    target: state.wheelTuner.target || "all",
    selectedLabels: state.wheelTuner.selected.map((object) => object.userData?.wheelLabel || "").filter(Boolean),
    selectedCount: state.wheelTuner.selected.length,
    dirty: Boolean(state.wheelTuner.dirty),
    overrides: getWheelCalibrationOverrides(),
  };
}

function markWheelCalibrationSaved() {
  state.wheelTuner.dirty = false;
  dispatchWheelTunerChange();
  return getWheelTunerState();
}

function createFlatTextureMaterial(texture, sourceMaterial, materialName = "EXT_SKIN") {
  const material = new THREE.MeshBasicMaterial({
    map: texture,
    color: 0xffffff,
    side: sourceMaterial?.side ?? THREE.FrontSide,
    transparent: Boolean(sourceMaterial?.transparent),
    opacity: Number.isFinite(sourceMaterial?.opacity) ? sourceMaterial.opacity : 1,
    alphaTest: Number.isFinite(sourceMaterial?.alphaTest) ? sourceMaterial.alphaTest : 0,
  });
  material.name = `${sourceMaterial?.name || materialName}_TEXTURE_PREVIEW`;
  material.toneMapped = false;
  return material;
}

function materialFinishConfig(finish = null) {
  const data = finish && typeof finish === "object" ? finish : {};
  return {
    metalness: numericValue(data.metalness, 0, 0, 1),
    roughness: numericValue(data.roughness, 0.3, 0, 1),
    clearCoat: numericValue(data.clearCoat, 1, 0, 1),
    clearCoatRoughness: numericValue(data.clearCoatRoughness, 0.3, 0, 1),
  };
}

function officialMaterialChannelConfig(value = null) {
  const data = value && typeof value === "object" ? value : {};
  const channel = (name, fallbackColor) => {
    const row = data[name] && typeof data[name] === "object" ? data[name] : {};
    return {
      baseColor: colorValue(row.baseColor, fallbackColor),
      metalness: numericValue(row.metalness, 0, 0, 1),
      roughness: numericValue(row.roughness, 0.4, 0, 1),
      clearCoat: numericValue(row.clearCoat, 0, 0, 1),
      clearCoatRoughness: numericValue(row.clearCoatRoughness, 0, 0, 1),
    };
  };
  return {
    enabled: data.available === true,
    red: channel("red", "#b2b2b2"),
    green: channel("green", "#373737"),
    blue: channel("blue", "#000000"),
  };
}

function materialViewSceneLookValues(look = null) {
  const data = materialLookConfig(look);
  return {
    environmentIntensity: numericValue(data.environmentIntensity, 0, 0, 4),
    paintReflectionStrength: numericValue(data.paintReflectionStrength, 0.32, 0, 2),
    paintStripStrength: numericValue(data.paintStripStrength, 0.72, 0, 3),
    paintStripSharpness: numericValue(data.paintStripSharpness, 24, 2, 96),
  };
}

function updateMaterialViewSceneLook(look = null) {
  const values = materialViewSceneLookValues(look);
  let updated = 0;
  state.model?.traverse?.((child) => {
    if (!child.isMesh || !child.material) return;
    const materials = Array.isArray(child.material) ? child.material : [child.material];
    materials.forEach((material) => {
      if (!material?.userData?.liveryLabMaterialView || !material.uniforms) return;
      Object.entries(values).forEach(([key, value]) => {
        if (material.uniforms[key]) material.uniforms[key].value = value;
      });
      material.needsUpdate = true;
      updated += 1;
    });
  });
  return { updated, sceneLook: values };
}

function createMaterialViewMaterial(colorTexture, aoTexture, detailTexture, sourceMaterial, materialName = "EXT_SKIN", finish = null, bodyBaseColor = "#7a00ff", bodyBaseColorEnabled = false, officialMaterialChannels = null, uvOverlayTexture = null, carbonTexture = null) {
  const values = materialFinishConfig(finish);
  const channels = officialMaterialChannelConfig(officialMaterialChannels);
  const sceneLook = materialViewSceneLookValues();
  const material = new THREE.ShaderMaterial({
    uniforms: {
      colorMap: { value: colorTexture },
      uvOverlayMap: { value: uvOverlayTexture || colorTexture },
      useUvOverlay: { value: uvOverlayTexture ? 1 : 0 },
      aoMap: { value: aoTexture || colorTexture },
      detailMap: { value: detailTexture || colorTexture },
      carbonMap: { value: carbonTexture || colorTexture },
      useCarbonMap: { value: carbonTexture ? 1 : 0 },
      carbonScale: { value: 44.5 },
      useAoMap: { value: aoTexture ? 1 : 0 },
      useDetailMap: { value: detailTexture ? 1 : 0 },
      metalness: { value: values.metalness },
      roughness: { value: values.roughness },
      clearCoat: { value: values.clearCoat },
      clearCoatRoughness: { value: values.clearCoatRoughness },
      environmentIntensity: { value: sceneLook.environmentIntensity },
      paintReflectionStrength: { value: sceneLook.paintReflectionStrength },
      paintStripStrength: { value: sceneLook.paintStripStrength },
      paintStripSharpness: { value: sceneLook.paintStripSharpness },
      bodyBaseColor: { value: new THREE.Color(colorValue(bodyBaseColor, "#7a00ff")) },
      bodyBaseColorEnabled: { value: bodyBaseColorEnabled ? 1 : 0 },
      useOfficialChannels: { value: channels.enabled ? 1 : 0 },
      redChannelColor: { value: new THREE.Color(channels.red.baseColor) },
      greenChannelColor: { value: new THREE.Color(channels.green.baseColor) },
      blueChannelColor: { value: new THREE.Color(channels.blue.baseColor) },
      redChannelPbr: { value: new THREE.Vector4(channels.red.metalness, channels.red.roughness, channels.red.clearCoat, channels.red.clearCoatRoughness) },
      greenChannelPbr: { value: new THREE.Vector4(channels.green.metalness, channels.green.roughness, channels.green.clearCoat, channels.green.clearCoatRoughness) },
      blueChannelPbr: { value: new THREE.Vector4(channels.blue.metalness, channels.blue.roughness, channels.blue.clearCoat, channels.blue.clearCoatRoughness) },
      maskThreshold: { value: 0.08 },
      opacity: { value: 1 },
    },
    vertexShader: `
      varying vec2 vUv;
      varying vec3 vNormalView;
      varying vec3 vViewPosition;
      void main() {
        vUv = uv;
        vNormalView = normalize(normalMatrix * normal);
        vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
        vViewPosition = -mvPosition.xyz;
        gl_Position = projectionMatrix * mvPosition;
      }
    `,
    fragmentShader: `
      uniform sampler2D colorMap;
      uniform sampler2D uvOverlayMap;
      uniform float useUvOverlay;
      uniform sampler2D aoMap;
      uniform sampler2D detailMap;
      uniform sampler2D carbonMap;
      uniform float useCarbonMap;
      uniform float carbonScale;
      uniform float useAoMap;
      uniform float useDetailMap;
      uniform float metalness;
      uniform float roughness;
      uniform float clearCoat;
      uniform float clearCoatRoughness;
      uniform float environmentIntensity;
      uniform float paintReflectionStrength;
      uniform float paintStripStrength;
      uniform float paintStripSharpness;
      uniform vec3 bodyBaseColor;
      uniform float bodyBaseColorEnabled;
      uniform float useOfficialChannels;
      uniform vec3 redChannelColor;
      uniform vec3 greenChannelColor;
      uniform vec3 blueChannelColor;
      uniform vec4 redChannelPbr;
      uniform vec4 greenChannelPbr;
      uniform vec4 blueChannelPbr;
      uniform float maskThreshold;
      uniform float opacity;
      varying vec2 vUv;
      varying vec3 vNormalView;
      varying vec3 vViewPosition;
      float studioStrip(float value, float center, float sharpness) {
        float distance = abs(value - center);
        float width = 2.0 / max(8.0, sharpness);
        return exp(-(distance * distance) / (width * width));
      }
      void main() {
        vec4 sampleColor = texture2D(colorMap, vUv);
        vec4 uvOverlay = texture2D(uvOverlayMap, vUv);
        float uvOverlayAmount = useUvOverlay * uvOverlay.a;
        vec3 mask = texture2D(detailMap, vUv).rgb;
        float redMask = smoothstep(maskThreshold, maskThreshold + 0.035, mask.r);
        float greenMask = smoothstep(maskThreshold, maskThreshold + 0.035, mask.g);
        float blueMask = smoothstep(maskThreshold, maskThreshold + 0.035, mask.b);
        float redDominance = smoothstep(0.04, 0.12, mask.r - max(mask.g, mask.b));
        float blueDominance = smoothstep(0.04, 0.12, mask.b - max(mask.r, mask.g));
        float bodyColorArea = bodyBaseColorEnabled * useDetailMap * redMask * redDominance;
        float carbonArea = useCarbonMap * useDetailMap * blueMask * blueDominance;
        float greenOnlyArea = greenMask * (1.0 - redMask);
        float nonBodyMaskMax = max(greenOnlyArea, blueMask);
        vec3 skin = mix(sampleColor.rgb, bodyBaseColor, bodyColorArea);
        float baseArea = mix(1.0, 1.0 - nonBodyMaskMax, useDetailMap);
        float channelSum = max(mask.r + mask.g + mask.b, 0.0001);
        vec3 channelWeights = mask / channelSum;
        float channelArea = useOfficialChannels * useDetailMap * clamp(mask.r + mask.g + mask.b, 0.0, 1.0);
        vec3 officialRedColor = mix(redChannelColor, bodyBaseColor, bodyBaseColorEnabled);
        vec3 carbonColor = texture2D(carbonMap, vUv * carbonScale).rgb;
        vec3 officialBlueColor = mix(blueChannelColor, carbonColor, useCarbonMap);
        vec3 officialColor = officialRedColor * channelWeights.r
          + greenChannelColor * channelWeights.g
          + officialBlueColor * channelWeights.b;
        vec4 officialPbr = redChannelPbr * channelWeights.r
          + greenChannelPbr * channelWeights.g
          + blueChannelPbr * channelWeights.b;
        skin = mix(skin, officialColor, channelArea);
        baseArea = mix(baseArea, 1.0, channelArea);
        skin = mix(skin, carbonColor, carbonArea);
        baseArea = mix(baseArea, 1.0, carbonArea);
        float effectiveMetalness = mix(metalness, officialPbr.x, channelArea);
        float effectiveRoughness = mix(roughness, officialPbr.y, channelArea);
        float effectiveClearCoat = mix(clearCoat, officialPbr.z, channelArea);
        float effectiveClearCoatRoughness = mix(clearCoatRoughness, officialPbr.w, channelArea);
        effectiveMetalness = mix(effectiveMetalness, blueChannelPbr.x, carbonArea);
        effectiveRoughness = mix(effectiveRoughness, blueChannelPbr.y, carbonArea);
        effectiveClearCoat = mix(effectiveClearCoat, blueChannelPbr.z, carbonArea);
        effectiveClearCoatRoughness = mix(effectiveClearCoatRoughness, blueChannelPbr.w, carbonArea);
        float ao = mix(1.0, clamp(texture2D(aoMap, vUv).r, 0.0, 1.0), useAoMap);
        vec3 normal = normalize(vNormalView);
        vec3 keyDir = normalize(vec3(0.3, 0.72, 0.62));
        vec3 fillDir = normalize(vec3(-0.56, 0.28, -0.42));
        float keyLight = max(dot(normal, keyDir), 0.0);
        float fillLight = max(dot(normal, fillDir), 0.0);
        float roughCurve = pow(clamp(effectiveRoughness, 0.0, 1.0), 0.5);
        float smoothCurve = pow(clamp(1.0 - effectiveRoughness, 0.0, 1.0), 1.08);
        float coatRoughCurve = pow(clamp(effectiveClearCoatRoughness, 0.0, 1.0), 0.46);
        float coatSmoothCurve = pow(clamp(1.0 - effectiveClearCoatRoughness, 0.0, 1.0), 1.16);
        float metalCurve = pow(clamp(effectiveMetalness, 0.0, 1.0), 0.62);
        float rim = pow(1.0 - abs(normal.z), mix(3.85, 0.58, roughCurve));
        float gloss = pow(max(dot(normal, normalize(vec3(0.18, 0.64, 0.74))), 0.0), mix(260.0, 5.2, roughCurve));
        float coatGloss = pow(max(dot(normal, normalize(vec3(-0.22, 0.58, 0.78))), 0.0), mix(320.0, 6.4, coatRoughCurve));
        vec3 viewDir = normalize(vViewPosition);
        vec3 reflectionDir = normalize(reflect(-viewDir, normal));
        float stripSharpness = max(8.0, paintStripSharpness * mix(2.25, 0.26, roughCurve));
        float clearStripSharpness = max(8.0, paintStripSharpness * mix(2.8, 0.32, coatRoughCurve));
        float sideGate = smoothstep(0.08, 0.48, abs(normal.x));
        float topGate = smoothstep(0.05, 0.62, normal.y);
        float leftStrip = studioStrip(reflectionDir.x, -0.58, stripSharpness) * sideGate;
        float rightStrip = studioStrip(reflectionDir.x, 0.62, stripSharpness * 0.92) * sideGate * 0.86;
        float topStrip = studioStrip(reflectionDir.y, 0.74, clearStripSharpness * 0.72) * (0.32 + topGate * 0.68);
        float horizonStrip = studioStrip(reflectionDir.y, -0.18, stripSharpness * 0.55) * sideGate * 0.22;
        float studioReflectionMask = clamp(leftStrip + rightStrip + topStrip + horizonStrip, 0.0, 1.15);
        float glossiness = pow(clamp(1.0 - effectiveRoughness, 0.0, 1.0), 1.55);
        float coatGlossiness = pow(clamp(1.0 - effectiveClearCoatRoughness, 0.0, 1.0), 1.38);
        float reflectionGate = clamp(
          (0.04 + effectiveClearCoat * 1.35) * max(glossiness * 0.92, coatGlossiness * effectiveClearCoat)
          + metalCurve * (0.44 + smoothCurve * 0.72),
          0.0,
          1.85
        );
        studioReflectionMask = pow(studioReflectionMask, mix(0.42, 3.15, roughCurve));
        float diffuseEnergy = mix(1.0, 0.22, metalCurve);
        vec3 diffuse = skin * diffuseEnergy * clamp(0.66 + keyLight * 0.24 + fillLight * 0.06, 0.36, 1.02);
        vec3 metalTint = mix(vec3(1.0), skin * mix(1.8, 2.65, smoothCurve), metalCurve);
        vec3 coatTint = mix(vec3(1.0), skin * 0.36 + vec3(0.86), metalCurve * 0.38);
        vec3 studioReflection = mix(vec3(1.0), skin * 2.05, metalCurve * 0.68)
          * studioReflectionMask
          * paintReflectionStrength
          * paintStripStrength
          * environmentIntensity
          * reflectionGate;
        vec3 metalReflection = metalTint * gloss * (0.045 + metalCurve * mix(0.54, 1.08, smoothCurve));
        vec3 clearReflection = coatTint * coatGloss * effectiveClearCoat * mix(0.18, 0.68, coatSmoothCurve);
        vec3 broadCoat = coatTint * pow(max(dot(normal, normalize(vec3(-0.48, 0.48, 0.72))), 0.0), mix(22.0, 4.5, coatRoughCurve))
          * effectiveClearCoat
          * mix(0.035, 0.22, coatRoughCurve);
        vec3 reflection = metalReflection + clearReflection + broadCoat + studioReflection;
        vec3 satinSheen = metalTint * rim * (0.014 + effectiveClearCoat * 0.072 + metalCurve * 0.24) * (0.32 + roughCurve * 0.9);
        vec3 finished = diffuse + reflection + satinSheen;
        vec3 plain = skin * clamp(0.94 + keyLight * 0.1 + fillLight * 0.05, 0.78, 1.08);
        vec3 color = mix(plain, finished, baseArea) * ao;
        color = mix(color, uvOverlay.rgb, uvOverlayAmount);
        gl_FragColor = vec4(max(color, vec3(0.0)), 1.0);
        #include <tonemapping_fragment>
        #include <colorspace_fragment>
      }
    `,
    side: sourceMaterial?.side ?? THREE.FrontSide,
    transparent: false,
    depthWrite: true,
    depthTest: true,
    colorWrite: true,
    blending: THREE.NoBlending,
    alphaTest: 0,
  });
  material.name = `${sourceMaterial?.name || materialName}_MATERIAL_VIEW`;
  material.toneMapped = true;
  material.userData.liveryLabMaterialView = true;
  return material;
}

function updateMaterialViewFinish(finish = null) {
  const values = materialFinishConfig(finish);
  let updated = 0;
  state.model?.traverse?.((child) => {
    if (!child.isMesh || !child.material) return;
    const materials = Array.isArray(child.material) ? child.material : [child.material];
    materials.forEach((material) => {
      if (!material?.userData?.liveryLabMaterialView || !material.uniforms) return;
      Object.entries(values).forEach(([key, value]) => {
        if (material.uniforms[key]) material.uniforms[key].value = value;
      });
      material.needsUpdate = true;
      updated += 1;
    });
  });
  return { updated, materialFinish: values };
}

function updateMaterialViewBodyBaseColor(color = "#7a00ff") {
  const nextColor = new THREE.Color(colorValue(color, "#7a00ff"));
  let updated = 0;
  state.model?.traverse?.((child) => {
    if (!child.isMesh || !child.material) return;
    const materials = Array.isArray(child.material) ? child.material : [child.material];
    materials.forEach((material) => {
      if (!material?.userData?.liveryLabMaterialView || !material.uniforms?.bodyBaseColor) return;
      material.uniforms.bodyBaseColor.value.copy(nextColor);
      material.needsUpdate = true;
      updated += 1;
    });
  });
  return { color, updated };
}

function updateMaterialViewBodyBaseColorEnabled(enabled = false) {
  const value = enabled ? 1 : 0;
  let updated = 0;
  state.model?.traverse?.((child) => {
    if (!child.isMesh || !child.material) return;
    const materials = Array.isArray(child.material) ? child.material : [child.material];
    materials.forEach((material) => {
      if (!material?.userData?.liveryLabMaterialView || !material.uniforms?.bodyBaseColorEnabled) return;
      material.uniforms.bodyBaseColorEnabled.value = value;
      material.needsUpdate = true;
      updated += 1;
    });
  });
  return { enabled: Boolean(enabled), updated };
}

function ensureAoUvSet(mesh) {
  const geometry = mesh?.geometry;
  if (!geometry?.attributes?.uv || geometry.attributes.uv2) return false;
  geometry.setAttribute("uv2", geometry.attributes.uv);
  return true;
}

function applyTexture(model, texture, materialName, options = {}) {
  let applied = 0;
  const target = String(materialName || "").trim();
  const flat = Boolean(options.flat);
  const materialView = Boolean(options.materialView);
  const forceOpaque = Boolean(options.forceOpaque);
  const aoMap = options.aoMap || null;
  const detailMap = options.detailMap || null;
  const materialFinish = options.materialFinish || null;
  const bodyBaseColor = options.bodyBaseColor || "#7a00ff";
  const bodyBaseColorEnabled = options.bodyBaseColorEnabled === true;
  const officialMaterialChannels = options.officialMaterialChannels || null;
  const materialSurfaceDetails = (Array.isArray(options.materialSurfaceDetails) ? options.materialSurfaceDetails : [])
    .filter((row) => row?.batchName)
    .sort((a, b) => String(b.batchName).length - String(a.batchName).length);
  const hiddenNames = hiddenMaterialNameSet(options.hiddenMaterialNames || []);
  const aoMapIntensity = Number.isFinite(Number(options.aoMapIntensity)) ? Number(options.aoMapIntensity) : 1.25;
  let aoApplied = 0;
  let uv2Fallbacks = 0;
  model.traverse((child) => {
    if (!child.isMesh || !child.material) return;
    const original = child.material;
    const materials = Array.isArray(original) ? original : [original];
    let meshChanged = false;
    const nextMaterials = materials.map((material) => {
      const name = String(material.name || "");
      if (materialNameIsHidden(name, hiddenNames)) return material;
      const matches = !target || name === target || name.startsWith(`${target}_`);
      if (!matches) return material;
      const surfaceDetail = materialSurfaceDetails.find((row) => {
        const batch = String(row.batchName || "").trim();
        return batch && (name === batch || name.startsWith(`${batch}_`));
      });
      const activeDetailMap = surfaceDetail?.detailMap || detailMap;
      if (aoMap && ensureAoUvSet(child)) uv2Fallbacks += 1;
      if (materialView) {
        meshChanged = true;
        applied += 1;
        if (aoMap) aoApplied += 1;
        return createMaterialViewMaterial(texture, aoMap, activeDetailMap, material, target, materialFinish, bodyBaseColor, bodyBaseColorEnabled, officialMaterialChannels, options.uvOverlayMap || null, options.carbonMap || null);
      }
      if (flat && !name.includes("_TEXTURE_PREVIEW")) {
        meshChanged = true;
        applied += 1;
        const nextMaterial = createFlatTextureMaterial(texture, material, target);
        nextMaterial.aoMap = aoMap;
        nextMaterial.aoMapIntensity = aoMap ? aoMapIntensity : 0;
        if (aoMap) aoApplied += 1;
        return nextMaterial;
      }
      material.map = texture;
      material.aoMap = aoMap;
      material.aoMapIntensity = aoMap ? aoMapIntensity : 0;
      material.color?.set?.(0xffffff);
      if (forceOpaque) {
        material.opacity = 1;
        material.transparent = false;
        material.alphaTest = 0;
        material.depthWrite = true;
        material.blending = THREE.NoBlending;
      }
      material.needsUpdate = true;
      if (aoMap) aoApplied += 1;
      applied += 1;
      return material;
    });
    if (meshChanged) child.material = Array.isArray(original) ? nextMaterials : nextMaterials[0];
  });
  return { applied, aoApplied, uv2Fallbacks };
}

function applyDefaultSpecialMaterials(model, materialName = "EXT_SKIN", excludedMaterialNames = [], hiddenMaterialNames = []) {
  if (!model) return 0;
  const target = String(materialName || "EXT_SKIN").trim();
  const excluded = new Set((Array.isArray(excludedMaterialNames) ? excludedMaterialNames : [])
    .map((name) => String(name || "").trim())
    .filter(Boolean));
  const hiddenNames = hiddenMaterialNameSet(hiddenMaterialNames);
  let applied = 0;
  model.traverse((child) => {
    if (!child.isMesh || !child.material) return;
    const original = child.material;
    const materials = Array.isArray(original) ? original : [original];
    let meshApplied = false;
    const nextMaterials = materials.map((material) => {
      const name = String(material?.name || "");
      if (materialNameIsHidden(name, hiddenNames)) return material;
      if (name.includes("_EVO_APPROX_")) return material;
      if (target && (name === target || name.startsWith(`${target}_`))) return material;
      if ([...excluded].some((excludedName) => name === excludedName || name.startsWith(`${excludedName}_`))) return material;
      const specialKind = approxSpecialMaterialKind(name);
      if (!specialKind) return material;
      const previewMaterial = createApproxSpecialMaterial(specialKind, material);
      applied += 1;
      meshApplied = true;
      return previewMaterial;
    });
    if (!meshApplied) return;
    child.material = Array.isArray(original) ? nextMaterials : nextMaterials[0];
  });
  return applied;
}

function ensurePreviewLoadActive(loadId) {
  if (loadId && loadId !== state.loadId) throw new Error("Preview load was superseded");
  if (loadId && !state.model) throw new Error("Preview load was cleared");
}

async function applyProjectTextures(project, loadId = 0) {
  const texturePath = String(project?.texturePath || "").trim();
  const textureDataUrl = String(project?.textureDataUrl || "").trim();
  const aoTexturePath = String(project?.aoTexturePath || "").trim();
  const detailTexturePath = String(project?.detailTexturePath || "").trim();
  const materialName = String(project?.materialName || "EXT_SKIN").trim();
  const bannerTexturePath = String(project?.bannerTexturePath || "").trim();
  const bannerTextureDataUrl = String(project?.bannerTextureDataUrl || "").trim();
  const bannerMaterialName = String(project?.bannerMaterialName || "EXT_BANNER").trim();
  const materialView = String(project?.previewMode || "") === "materialView";
  const hiddenMaterialNames = Array.isArray(project?.hiddenMaterialNames) ? project.hiddenMaterialNames : [];
  const applied = [];
  let aoTexture = null;
  let detailTexture = null;
  let uvOverlayTexture = null;
  let carbonTexture = null;
  const materialSurfaceDetails = [];

  if (aoTexturePath) {
    try {
      const aoTextureUrl = await loadTextureDataUrl(aoTexturePath, { cacheNamespace: "preview3d-ao" });
      ensurePreviewLoadActive(loadId);
      aoTexture = await loadTexture(aoTextureUrl);
      try {
        ensurePreviewLoadActive(loadId);
      } catch (error) {
        aoTexture.dispose?.();
        throw error;
      }
      aoTexture.colorSpace = THREE.NoColorSpace;
      aoTexture.flipY = false;
      aoTexture.wrapS = THREE.RepeatWrapping;
      aoTexture.wrapT = THREE.RepeatWrapping;
      aoTexture.anisotropy = Math.min(state.renderer?.capabilities?.getMaxAnisotropy?.() || 1, 8);
    } catch (error) {
      if (!/not found|cannot find|not exist|no such file|errno 2|filenotfound/i.test(String(error?.message || error))) throw error;
      aoTexture = null;
    }
  }

  if (materialView && detailTexturePath) {
    try {
      const detailTextureUrl = await loadTextureDataUrl(detailTexturePath, { cacheNamespace: "preview3d-detail" });
      ensurePreviewLoadActive(loadId);
      detailTexture = await loadTexture(detailTextureUrl);
      try {
        ensurePreviewLoadActive(loadId);
      } catch (error) {
        detailTexture.dispose?.();
        throw error;
      }
      detailTexture.colorSpace = THREE.NoColorSpace || "";
      detailTexture.flipY = false;
      detailTexture.wrapS = THREE.RepeatWrapping;
      detailTexture.wrapT = THREE.RepeatWrapping;
    } catch (error) {
      if (!/not found|cannot find|not exist|no such file|errno 2|filenotfound/i.test(String(error?.message || error))) throw error;
      detailTexture = null;
    }
  }

  if (materialView && Array.isArray(project?.officialPreviewMaterialSurfaces)) {
    for (const surface of project.officialPreviewMaterialSurfaces) {
      const batchName = String(surface?.batchName || "").trim();
      const surfacePath = String(surface?.detailTexturePath || "").trim();
      if (!batchName || !surfacePath || surfacePath === detailTexturePath) continue;
      try {
        const surfaceUrl = await loadTextureDataUrl(surfacePath, { cacheNamespace: `preview3d-detail-${batchName.toLowerCase()}` });
        ensurePreviewLoadActive(loadId);
        const surfaceTexture = await loadTexture(surfaceUrl);
        surfaceTexture.colorSpace = THREE.NoColorSpace || "";
        surfaceTexture.flipY = false;
        surfaceTexture.wrapS = THREE.RepeatWrapping;
        surfaceTexture.wrapT = THREE.RepeatWrapping;
        materialSurfaceDetails.push({ batchName, detailMap: surfaceTexture, detailTexturePath: surfacePath });
      } catch (error) {
        if (!/not found|cannot find|not exist|no such file|errno 2|filenotfound/i.test(String(error?.message || error))) throw error;
      }
    }
  }

  if (materialView && project?.uvMeshOverlayDataUrl) {
    const uvOverlayUrl = String(project.uvMeshOverlayDataUrl).trim();
    if (uvOverlayUrl) {
      ensurePreviewLoadActive(loadId);
      uvOverlayTexture = await loadTexture(uvOverlayUrl);
      uvOverlayTexture.colorSpace = THREE.SRGBColorSpace;
      uvOverlayTexture.flipY = false;
      uvOverlayTexture.wrapS = THREE.RepeatWrapping;
      uvOverlayTexture.wrapT = THREE.RepeatWrapping;
    }
  }

  if (materialView && detailTexture) {
    try {
      carbonTexture = await loadTexture("./assets/carbon_preview.png");
      ensurePreviewLoadActive(loadId);
      carbonTexture.colorSpace = THREE.SRGBColorSpace;
      carbonTexture.flipY = false;
      carbonTexture.wrapS = THREE.RepeatWrapping;
      carbonTexture.wrapT = THREE.RepeatWrapping;
      carbonTexture.anisotropy = Math.min(state.renderer?.capabilities?.getMaxAnisotropy?.() || 1, 8);
    } catch (_error) {
      carbonTexture = null;
    }
  }

  if (texturePath || textureDataUrl) {
    const textureUrl = textureDataUrl || await loadTextureDataUrl(texturePath);
    ensurePreviewLoadActive(loadId);
    const texture = await loadTexture(textureUrl);
    try {
      ensurePreviewLoadActive(loadId);
    } catch (error) {
      texture.dispose?.();
      throw error;
    }
    texture.colorSpace = THREE.SRGBColorSpace;
    texture.flipY = false;
    texture.wrapS = THREE.RepeatWrapping;
    texture.wrapT = THREE.RepeatWrapping;
    state.lastTextureUrl = textureUrl;
    const textureResult = applyTexture(state.model, texture, materialName, {
      flat: !materialView,
      materialView,
      aoMap: aoTexture,
      detailMap: detailTexture,
      materialFinish: project?.materialFinish || null,
      bodyBaseColor: project?.bodyBaseColor || "#7a00ff",
      bodyBaseColorEnabled: project?.bodyBaseColorEnabled === true,
      officialMaterialChannels: project?.officialMaterialChannels || null,
      uvOverlayMap: uvOverlayTexture,
      carbonMap: carbonTexture,
      materialSurfaceDetails,
      aoMapIntensity: 1.25,
      hiddenMaterialNames,
    });
    applied.push({
      texturePath,
      textureDataUrl: textureDataUrl ? "inline" : "",
      aoTexturePath,
      detailTexturePath: materialView ? detailTexturePath : "",
      materialName,
      appliedMaterials: textureResult.applied,
      aoAppliedMaterials: textureResult.aoApplied,
      uv2Fallbacks: textureResult.uv2Fallbacks,
    });
  }
  if (bannerTexturePath || bannerTextureDataUrl) {
    try {
      const bannerTextureUrl = bannerTextureDataUrl || await loadTextureDataUrl(bannerTexturePath);
      ensurePreviewLoadActive(loadId);
      const bannerTexture = await loadTexture(bannerTextureUrl);
      try {
        ensurePreviewLoadActive(loadId);
      } catch (error) {
        bannerTexture.dispose?.();
        throw error;
      }
      bannerTexture.colorSpace = THREE.SRGBColorSpace;
      bannerTexture.flipY = false;
      bannerTexture.wrapS = THREE.ClampToEdgeWrapping;
      bannerTexture.wrapT = THREE.ClampToEdgeWrapping;
      const bannerResult = applyTexture(state.model, bannerTexture, bannerMaterialName, { hiddenMaterialNames });
      applied.push({
        texturePath: bannerTexturePath,
        textureDataUrl: bannerTextureDataUrl ? "inline" : "",
        materialName: bannerMaterialName,
        appliedMaterials: bannerResult.applied,
      });
    } catch (error) {
      if (!/not found|cannot find|not exist|no such file|errno 2|filenotfound/i.test(String(error?.message || error))) throw error;
    }
  }
  return applied;
}

async function setMaterialChannelOverlay(options = {}) {
  const enabled = Boolean(options.enabled);
  if (!state.model) throw new Error("No 3D model loaded");
  const requestId = state.materialOverlay.requestId + 1;
  state.materialOverlay.requestId = requestId;
  if (!enabled) return restoreMaterialChannelOverlay();

  restoreMaterialChannelOverlay();
  const detailTexturePath = String(options.detailTexturePath || "").trim();
  if (!detailTexturePath) throw new Error("Selected material texture path is missing");
  const materialName = String(options.materialName || "EXT_SKIN").trim();
  const displayMode = String(options.displayMode || "").trim() === "grayscale" ? "grayscale" : "channels";
  const selectedTextureKey = String(options.selectedTextureKey || "").trim();
  const hiddenNames = hiddenMaterialNameSet(options.hiddenMaterialNames || []);
  const maskTextureUrl = await loadTextureDataUrl(detailTexturePath);
  const maskTexture = await loadTexture(maskTextureUrl);
  maskTexture.colorSpace = THREE.NoColorSpace || "";
  maskTexture.flipY = false;
  maskTexture.wrapS = THREE.RepeatWrapping;
  maskTexture.wrapT = THREE.RepeatWrapping;
  if (requestId !== state.materialOverlay.requestId) {
    maskTexture.dispose?.();
    return { enabled: false, appliedMaterials: 0, stale: true };
  }
  state.materialOverlay.texture = maskTexture;

  let applied = 0;
  state.model.traverse((child) => {
    if (!child.isMesh || !child.material) return;
    const original = child.material;
    const materials = Array.isArray(original) ? original : [original];
    let meshApplied = false;
    const nextMaterials = materials.map((material) => {
      const name = String(material?.name || "");
      if (materialNameIsHidden(name, hiddenNames)) return material;
      const matches = !materialName || name === materialName || name.startsWith(`${materialName}_`);
      if (!matches) return material;
      const overlay = createChannelOverlayMaterial(maskTexture, material, { displayMode });
      state.materialOverlay.materials.push(overlay);
      applied += 1;
      meshApplied = true;
      return overlay;
    });
    if (!meshApplied) return;
    state.materialOverlay.originals.set(child, original);
    child.material = Array.isArray(original) ? nextMaterials : nextMaterials[0];
  });
  state.materialOverlay.enabled = applied > 0;
  if (!applied) restoreMaterialChannelOverlay();
  return { enabled: state.materialOverlay.enabled, appliedMaterials: applied, detailTexturePath, materialName, displayMode, selectedTextureKey };
}

async function setApproxMaterialOverlay(options = {}) {
  const enabled = Boolean(options.enabled);
  if (!state.model) throw new Error("No 3D model loaded");
  const requestId = state.materialOverlay.requestId + 1;
  state.materialOverlay.requestId = requestId;
  if (!enabled) return restoreMaterialChannelOverlay();

  restoreMaterialChannelOverlay();
  const colorTexturePath = String(options.colorTexturePath || "").trim();
  const detailTexturePath = String(options.detailTexturePath || "").trim();
  const aoTexturePath = String(options.aoTexturePath || "").trim();
  if (!colorTexturePath) throw new Error("EXT_SKIN_C texture path is missing");
  if (!detailTexturePath) throw new Error("EXT_Skin_D texture path is missing");
  const materialName = String(options.materialName || "EXT_SKIN").trim();
  const materialLook = materialLookConfig(options.materialLook || null);
  const hiddenNames = hiddenMaterialNameSet(options.hiddenMaterialNames || []);

  const [colorTextureUrl, maskTextureUrl] = await Promise.all([
    loadTextureDataUrl(colorTexturePath),
    loadTextureDataUrl(detailTexturePath),
  ]);
  const [colorTexture, maskTexture] = await Promise.all([
    loadTexture(colorTextureUrl),
    loadTexture(maskTextureUrl),
  ]);
  colorTexture.colorSpace = THREE.SRGBColorSpace;
  colorTexture.flipY = false;
  colorTexture.wrapS = THREE.RepeatWrapping;
  colorTexture.wrapT = THREE.RepeatWrapping;
  maskTexture.colorSpace = THREE.NoColorSpace || "";
  maskTexture.flipY = false;
  maskTexture.wrapS = THREE.RepeatWrapping;
  maskTexture.wrapT = THREE.RepeatWrapping;
  state.materialOverlay.colorTexture = colorTexture;
  state.materialOverlay.texture = maskTexture;

  let aoTexture = null;
  if (aoTexturePath) {
    try {
      const aoTextureUrl = await loadTextureDataUrl(aoTexturePath, { cacheNamespace: "preview3d-ao" });
      aoTexture = await loadTexture(aoTextureUrl);
      aoTexture.colorSpace = THREE.NoColorSpace || "";
      aoTexture.flipY = false;
      aoTexture.wrapS = THREE.RepeatWrapping;
      aoTexture.wrapT = THREE.RepeatWrapping;
      aoTexture.anisotropy = Math.min(state.renderer?.capabilities?.getMaxAnisotropy?.() || 1, 8);
      state.materialOverlay.aoTexture = aoTexture;
    } catch (error) {
      if (!/not found|cannot find|not exist|no such file|errno 2|filenotfound/i.test(String(error?.message || error))) throw error;
      aoTexture = null;
    }
  }

  let carbonTexture = null;
  const carbonTextureUrl = String(options.carbonTextureUrl || "./assets/carbon_preview.png").trim();
  try {
    carbonTexture = await loadTexture(carbonTextureUrl);
    carbonTexture.colorSpace = THREE.SRGBColorSpace;
    carbonTexture.flipY = false;
    carbonTexture.wrapS = THREE.RepeatWrapping;
    carbonTexture.wrapT = THREE.RepeatWrapping;
    carbonTexture.anisotropy = Math.min(state.renderer?.capabilities?.getMaxAnisotropy?.() || 1, 8);
    state.materialOverlay.carbonTexture = carbonTexture;
  } catch (_error) {
    carbonTexture = null;
  }
  if (requestId !== state.materialOverlay.requestId) {
    colorTexture.dispose?.();
    maskTexture.dispose?.();
    aoTexture?.dispose?.();
    if (state.materialOverlay.aoTexture === aoTexture) state.materialOverlay.aoTexture = null;
    carbonTexture?.dispose?.();
    return {
      enabled: false,
      appliedMaterials: 0,
      specialMaterials: 0,
      stale: true,
      colorTexturePath,
      detailTexturePath,
      aoTexturePath,
      aoTextureLoaded: Boolean(aoTexture),
      carbonTextureLoaded: Boolean(carbonTexture),
      materialName,
    };
  }

  let applied = 0;
  let specialApplied = 0;
  state.model.traverse((child) => {
    if (!child.isMesh || !child.material) return;
    const original = child.material;
    const materials = Array.isArray(original) ? original : [original];
    let meshApplied = false;
    const nextMaterials = materials.map((material) => {
      const name = String(material?.name || "");
      if (materialNameIsHidden(name, hiddenNames)) return material;
      const matches = !materialName || name === materialName || name.startsWith(`${materialName}_`);
      const specialKind = matches ? "" : approxSpecialMaterialKind(name);
      if (!matches && !specialKind) return material;
      const overlay = matches
        ? createApproxMaterial(maskTexture, colorTexture, carbonTexture, aoTexture, material, materialLook)
        : createApproxSpecialMaterial(specialKind, material, materialLook);
      state.materialOverlay.materials.push(overlay);
      applied += 1;
      if (specialKind) specialApplied += 1;
      meshApplied = true;
      return overlay;
    });
    if (!meshApplied) return;
    state.materialOverlay.originals.set(child, original);
    child.material = Array.isArray(original) ? nextMaterials : nextMaterials[0];
  });
  state.materialOverlay.enabled = applied > 0;
  if (!applied) restoreMaterialChannelOverlay();
  return {
    enabled: state.materialOverlay.enabled,
    appliedMaterials: applied,
    specialMaterials: specialApplied,
    colorTexturePath,
    detailTexturePath,
    aoTexturePath,
    aoTextureLoaded: Boolean(aoTexture),
    carbonTextureLoaded: Boolean(carbonTexture),
    materialName,
  };
}

async function loadPreview(project) {
  const loadId = state.loadId + 1;
  state.loadId = loadId;
  const canvas = document.getElementById(project?.canvasId || "preview3dCanvas");
  const empty = document.getElementById(project?.emptyId || "preview3dViewerEmpty");
  if (!canvas) throw new Error("3D canvas is missing");
  const preservedCameraView = project?.preserveCameraView === true ? captureCameraViewState() : null;
  setPreviewLoadStatus(project, "Initializing renderer...");
  ensureViewer(canvas);
  setPreviewLoadStatus(project, "Loading scene settings...");
  await ensureSceneConfigs();
  if (loadId !== state.loadId) throw new Error("Preview load was superseded");
  setSceneMode(project?.sceneMode || state.sceneMode || "light");

  const modelPath = String(project?.modelPath || "").trim();
  const texturePath = String(project?.texturePath || "").trim();
  const aoTexturePath = String(project?.aoTexturePath || "").trim();
  const materialName = String(project?.materialName || "EXT_SKIN").trim();
  const bannerTexturePath = String(project?.bannerTexturePath || "").trim();
  const hiddenMaterialNames = Array.isArray(project?.hiddenMaterialNames) ? project.hiddenMaterialNames : [];
  const hiddenExteriorMaterialNames = Array.isArray(project?.hiddenExteriorMaterialNames)
    ? project.hiddenExteriorMaterialNames
    : [];
  const auxiliaryModels = Array.isArray(project?.auxiliaryModels)
    ? project.auxiliaryModels
      .map((row) => (row && typeof row === "object" ? row : { path: row }))
      .filter((row) => String(row.path || "").trim())
    : [];
  const auxiliaryModelPaths = Array.isArray(project?.auxiliaryModelPaths)
    ? project.auxiliaryModelPaths.map((row) => String(row || "").trim()).filter(Boolean)
    : [];
  const auxiliaryEntries = auxiliaryModels.length
    ? auxiliaryModels
    : auxiliaryModelPaths.map((path) => ({ path }));
  const auxiliaryTimeoutMs = Number(project?.auxiliaryTimeoutMs || 8000);
  if (!modelPath) throw new Error("Preview model path is empty");

  setPreviewLoadStatus(project, `Loading exterior GLB...\n${modelPath}`);
  const modelUrl = await loadModelDataUrl(modelPath);
  setPreviewLoadStatus(project, "Parsing exterior GLB...");
  const gltf = await parseGltf(modelUrl);
  const auxiliaryScenes = [];
  const skippedAuxiliaryModels = [];
  for (const auxiliaryInfo of auxiliaryEntries) {
    const auxiliaryPath = String(auxiliaryInfo.path || "").trim();
    const auxiliaryLabel = auxiliaryInfo.label || auxiliaryPath.split(/[\\/]/).pop() || auxiliaryPath;
    try {
      setPreviewLoadStatus(project, `Loading auxiliary GLB...\n${auxiliaryLabel}`);
      const auxiliaryUrl = await withPreviewAssetTimeout(
        loadModelDataUrl(auxiliaryPath),
        auxiliaryTimeoutMs,
        `Auxiliary GLB load timed out: ${auxiliaryLabel}`,
      );
      setPreviewLoadStatus(project, `Parsing auxiliary GLB...\n${auxiliaryLabel}`);
      const auxiliaryGltf = await withPreviewAssetTimeout(
        parseGltf(auxiliaryUrl),
        auxiliaryTimeoutMs,
        `Auxiliary GLB parse timed out: ${auxiliaryLabel}`,
      );
      auxiliaryGltf.scene.name = `Auxiliary: ${auxiliaryLabel}`;
      applyAuxiliaryTransform(auxiliaryGltf.scene, auxiliaryInfo);
      auxiliaryScenes.push(auxiliaryGltf.scene);
    } catch (error) {
      const message = error?.message || String(error);
      skippedAuxiliaryModels.push({
        label: auxiliaryLabel,
        path: auxiliaryPath,
        message,
      });
      console.warn("Skipping auxiliary preview model", auxiliaryLabel, message);
      setPreviewLoadStatus(project, `Skipping auxiliary GLB...\n${auxiliaryLabel}\n${message}`);
    }
  }
  if (loadId !== state.loadId) throw new Error("Preview load was superseded");
  if (state.model) {
    restoreMaterialChannelOverlay();
    removeWheelTunerAnchor();
    state.scene.remove(state.model);
    disposeObject(state.model);
    removeNumberplateOverlay();
  }
  state.wheelTuner.dirty = false;
  state.model = new THREE.Group();
  state.exteriorModel = gltf.scene;
  state.auxiliaryModels = auxiliaryScenes;
  state.exteriorModel.visible = project?.hideExteriorModel === true ? false : true;
  state.model.name = "LiveryLabPreviewModel";
  gltf.scene.name = "Exterior";
  state.model.add(gltf.scene);
  auxiliaryScenes.forEach((scene) => state.model.add(scene));
  applySceneShadowFlags(state.model);
  state.scene.add(state.model);
  if (state.wheelTuner.enabled) updateWheelTunerSelection();

  setPreviewLoadStatus(project, "Applying livery textures...");
  const appliedTextures = await applyProjectTextures(project, loadId);
  ensurePreviewLoadActive(loadId);
  const appliedMaterials = appliedTextures.reduce((sum, item) => sum + Number(item.appliedMaterials || 0), 0);
  setPreviewLoadStatus(project, "Preparing camera and materials...");
  const specialMaterials = applyDefaultSpecialMaterials(state.model, materialName, [], hiddenMaterialNames);
  const hiddenMaterials = applyHiddenPreviewMaterials(state.model, hiddenMaterialNames);
  const hiddenExteriorMaterials = applyHiddenPreviewMaterials(state.exteriorModel, hiddenExteriorMaterialNames);
  const environment = sceneConfig(state.sceneMode).environment || {};
  updateEnvironmentIntensity(numericValue(environment.intensity, materialLookConfig().environmentIntensity, 0, 4));

  fitCamera(state.model);
  refreshGroundedSceneElements();
  setCameraPreset(project?.cameraPreset || state.cameraPreset || "free");
  const cameraViewPreserved = restoreCameraViewState(preservedCameraView);
  if (empty) empty.style.display = "none";
  let meshCount = 0;
  state.model.traverse((child) => {
    if (child.isMesh) meshCount += 1;
  });
  return {
    modelPath,
    auxiliaryModelPaths: auxiliaryEntries.map((row) => row.path),
    auxiliaryModelCount: auxiliaryScenes.length,
    skippedAuxiliaryModels,
    texturePath,
    aoTexturePath,
    bannerTexturePath,
    materialName,
    appliedMaterials,
    appliedTextures,
    specialMaterials,
    hiddenMaterials,
    hiddenExteriorMaterials,
    wheelMaterials: 0,
    wheelTexturedMaterials: 0,
    wheelFallbackMaterials: 0,
    wheelDetailMaps: 0,
    loadId,
    meshCount,
    cameraViewPreserved,
    canvasSize: canvasSize(),
    health: previewHealth(),
  };
}

async function applyWheelMaterials(wheelMaterials = [], options = {}) {
  if (!state.model) throw new Error("No 3D model loaded");
  const loadId = Number(options.loadId || state.loadId || 0);
  if (loadId && loadId !== state.loadId) {
    return { enabled: false, wheelMaterials: 0, wheelTexturedMaterials: 0, wheelFallbackMaterials: 0, wheelDetailMaps: 0 };
  }
  const result = await applyWheelPreviewMaterials(wheelMaterials, { loadId, materialLook: options.materialLook || null });
  return {
    enabled: result.applied > 0,
    wheelMaterials: result.applied,
    wheelTexturedMaterials: result.textured,
    wheelFallbackMaterials: result.fallback,
    wheelDetailMaps: result.detailMaps,
  };
}

async function reloadTexture(project) {
  if (!state.model) return loadPreview(project);
  const texturePath = String(project?.texturePath || "").trim();
  const aoTexturePath = String(project?.aoTexturePath || "").trim();
  const materialName = String(project?.materialName || "EXT_SKIN").trim();
  const hiddenMaterialNames = Array.isArray(project?.hiddenMaterialNames) ? project.hiddenMaterialNames : [];
  const hiddenExteriorMaterialNames = Array.isArray(project?.hiddenExteriorMaterialNames)
    ? project.hiddenExteriorMaterialNames
    : [];
  const appliedTextures = await applyProjectTextures(project);
  const appliedMaterials = appliedTextures.reduce((sum, item) => sum + Number(item.appliedMaterials || 0), 0);
  const hiddenMaterials = applyHiddenPreviewMaterials(state.model, hiddenMaterialNames);
  const hiddenExteriorMaterials = applyHiddenPreviewMaterials(state.exteriorModel, hiddenExteriorMaterialNames);
  return {
    texturePath,
    aoTexturePath,
    materialName,
    appliedMaterials,
    appliedTextures,
    hiddenMaterials,
    hiddenExteriorMaterials,
  };
}

window.LiveryLabPreview3d = {
  load: loadPreview,
  reloadTexture,
  applyWheelMaterials,
  clear: clearPreview,
  setWheelTunerEnabled,
  setWheelTunerTarget,
  getWheelTunerState,
  getWheelCalibrationOverrides,
  markWheelCalibrationSaved,
  setCameraPreset,
  getCameraPreset,
  getCameraPresetConfig,
  setCameraPresetConfig,
  resetCameraPresetConfig,
  captureCameraPresetFromView,
  hasLoadedModel,
  getHealth: renderHealth,
  setSceneMode,
  setFloorVisible,
  getSceneConfigs,
  getDefaultSceneConfigs,
  setSceneConfigs,
  setSceneConfig,
  getDefaultNumberplateTransform,
  getNumberplateFit: currentNumberplateFit,
  setNumberplateFit,
  resetNumberplateFit,
  showNumberplateOverlay,
  updateNumberplateOverlay,
  hasNumberplateOverlay,
  removeNumberplateOverlay,
  showNumberplatePlacementDiagnostics,
  getMappedNumberplateTransform,
  showMappedNumberplates,
  showNumberplatePreviewActorOverlay,
  selectMappedNumberplate,
  setNumberplateTunerEnabled,
  setNumberplateTunerMode,
  getNumberplateTunerState,
  markNumberplateTunerSaved,
  updateSelectedNumberplateTransform,
  deselectNumberplate: () => {
    setNumberplateTunerEnabled(false);
    setActiveNumberplateMesh(null);
    return getNumberplateTunerState();
  },
  setModelVisible,
  setAuxiliaryVisible,
  setMaterialChannelOverlay,
  setApproxMaterialOverlay,
  setApproxBodyBaseColor,
  setMaterialViewFinish: updateMaterialViewFinish,
  setMaterialViewBodyBaseColor: updateMaterialViewBodyBaseColor,
  setMaterialViewBodyBaseColorEnabled: updateMaterialViewBodyBaseColorEnabled,
  resize,
};
