import { ActionManager } from '@babylonjs/core/Actions/actionManager';
import { ExecuteCodeAction } from '@babylonjs/core/Actions/directActions';
import { ArcRotateCamera } from '@babylonjs/core/Cameras/arcRotateCamera';
import { AssetContainer } from '@babylonjs/core/assetContainer';
import { Color3, Color4 } from '@babylonjs/core/Maths/math.color';
import { Matrix, Quaternion, Vector3, Vector4 } from '@babylonjs/core/Maths/math.vector';
import { Plane } from '@babylonjs/core/Maths/math.plane';
import { Ray } from '@babylonjs/core/Culling/ray';
import { PointerEventTypes } from '@babylonjs/core/Events/pointerEvents';
import { DynamicTexture } from '@babylonjs/core/Materials/Textures/dynamicTexture';
import { Texture } from '@babylonjs/core/Materials/Textures/texture';
import { Effect } from '@babylonjs/core/Materials/effect';
import { PBRMaterial } from '@babylonjs/core/Materials/PBR/pbrMaterial';
import { ShaderMaterial } from '@babylonjs/core/Materials/shaderMaterial';
import { StandardMaterial } from '@babylonjs/core/Materials/standardMaterial';
import { Engine } from '@babylonjs/core/Engines/engine';
import { HemisphericLight } from '@babylonjs/core/Lights/hemisphericLight';
import { SceneLoader } from '@babylonjs/core/Loading/sceneLoader';
import { Mesh } from '@babylonjs/core/Meshes/mesh';
import { CreateBox } from '@babylonjs/core/Meshes/Builders/boxBuilder';
import { CreateDecal } from '@babylonjs/core/Meshes/Builders/decalBuilder';
import { CreateLineSystem } from '@babylonjs/core/Meshes/Builders/linesBuilder';
import { CreatePlane } from '@babylonjs/core/Meshes/Builders/planeBuilder';
import { CreateSphere } from '@babylonjs/core/Meshes/Builders/sphereBuilder';
import { CreateTorus } from '@babylonjs/core/Meshes/Builders/torusBuilder';
import { VertexData } from '@babylonjs/core/Meshes/mesh.vertexData';
import { Scene } from '@babylonjs/core/scene';
import earcut from 'earcut';

const MeshBuilder = { CreateBox, CreateDecal, CreateLineSystem, CreatePlane, CreateSphere, CreateTorus };
const WEBGL_CONTEXT_RELEASE_DELAY_MS = 50;
const WEBGL_RETRY_FRAME_TIMEOUT_MS = 120;
let lastBabylonEngineDisposeAt = 0;

import { asukaHistoryDebug, isAsukaHistoryDebugEnabled, summarize3DScene, summarizeHistoryInfo, summarizeHistoryTrack } from '../core/historyDebug';

import type {
  Asuka3DDecoration,
  Asuka3DEngineOptions,
  Asuka3DMaterialPatch,
  Asuka3DModel,
  Asuka3DMaterialSummary,
  Asuka3DHistoryTrackState,
  Asuka3DMeshSummary,
  Asuka3DPrintArea,
  Asuka3DProduct,
  Asuka3DSceneState,
  AsukaBoundsBox,
  AsukaCoordinateFrame,
  AsukaDesignInput,
  AsukaElementInput,
  AsukaExportOptions,
  AsukaImageInput,
  AsukaHistoryInfo,
  AsukaHistoryOptions,
  AsukaObjectDuplicateOptions,
  AsukaObjectListOptions,
  AsukaObjectOrderOptions,
  AsukaObjectPatch,
  AsukaObjectPreviewOptions,
  AsukaObjectRef,
  AsukaPrintAreaConstraintOptions,
  AsukaPrintAreaConstraintStatus,
  Asuka3DPart,
  Asuka3DPartFocusOptions,
  Asuka3DPartVariation,
  Asuka3DPartVariationApplyOptions,
  AsukaObjectSummary,
  AsukaObjectTransform,
  AsukaPlacementStatus,
  AsukaRenderEngine,
  AsukaSceneGraphNode,
  AsukaSelectionAnchorBox,
  AsukaSurfaceInteractionDebug,
  AsukaSurfacePickOptions,
  AsukaSurfacePickTarget,
  AsukaTextInput,
} from '../core/renderEngine';
import type {
  AsukaAssemblyBounds,
  AsukaAssemblyBuildState,
  AsukaAssemblyBuildTool,
  AsukaAssemblyComponent,
  AsukaAssemblyComponentDefinition,
  AsukaAssemblyConnection,
  AsukaAssemblyExportKind,
  AsukaAssemblyExportOptions,
  AsukaAssemblyFloorSurface,
  AsukaAssemblyFloorEdgeConstraint,
  AsukaAssemblyInteractionState,
  AsukaAssemblyMaterialApplyOptions,
  AsukaAssemblyMaterialPreset,
  AsukaAssemblyMutationOptions,
  AsukaAssemblyPlacementInput,
  AsukaAssemblyPlacementResult,
  AsukaAssemblyPlanPoint,
  AsukaAssemblyQuoteOptions,
  AsukaAssemblyStackLink,
  AsukaAssemblyStartDrawingInput,
  AsukaAssemblyState,
  AsukaAssemblySurface,
  AsukaAssemblySurfaceMaterial,
  AsukaAssemblySurfacePatch,
  AsukaAssemblySurfaceTransformInput,
  AsukaAssemblyTransform,
  AsukaAssemblyWallSurface,
} from '../types/assembly3d';
import {
  AsukaRendererError,
  assertCompatibleCoordinateFrame,
  createRenderEngineEvents,
  emitRenderEngineEvent,
  runAsukaRendererMutation,
} from '../core/renderEngine';
import { emitAsukaEvent } from '../core/events';
import { DEFAULT_PERMISSIONS, getEffectivePermissions } from '../core/permissions';
import { assertCanonical3DSceneState, clear3DTransientSelection, create3DSceneState, select3DPart, select3DProduct } from '../core/scene3d';
import { resolveOrCreate3DModelRuntimeUrl } from '../core/modelAsset3d';
import { beginAsuka3DPerfSpan, ensureAsuka3DPerfDebugApi, isAsuka3DPerfDebugEnabled, logAsuka3DPerf } from '../core/perf3dDebug';
import { resolveProduct3DVariationTargetMeshIds } from '../core/product3dVariationTarget';
import { getProduct3DMeshInstanceId } from '../core/product3dIdentity';
import { resolveProduct3DPartFocusCamera, resolveProduct3DRootFocusCamera } from '../core/product3dFocusCamera';
import {
  resolveProduct3DHoverLabel,
  resolveProduct3DPartDisplayLabel,
  resolveProduct3DPartVariationGroupLabel,
} from '../core/product3dHoverLabel';
import { resolveTextureLayerRotationDegrees, textureLayerDegreesToRadians } from '../core/product3dLayerRotation';
import { resolve3DStickerProjectionStrategy, TARGET_BOUND_STICKER_RENDER_POLICY } from '../core/product3dStickerProjection';
import { getElementPriceBreakdown, normalizePriceInput, sumPriceInputs } from '../core/pricingValue';
import { collectDeclaredPricingColors, collectPricingColorsFromSvg } from '../core/pricingColors';
import { createAsukaLayerMeasurement, type AsukaLayerMeasurement, type AsukaLayerMeasurementOptions, type AsukaPhysicalSize } from '../core/physicalMeasurement';
import {
  constrainFixedSlotTextInput,
  constrainFixedSlotTransformPatch,
  normalizeFixedSlotConstraints,
  type AsukaFixedSlotConstraintsLike,
} from '../core/fixedSlotConstraints';
import { normalizeCameraLimits, normalizeCameraView } from '../core/camera';
import { createAsukaPlacementSessionController } from '../core/placementSession';
import type { AsukaCameraLimits, AsukaCameraMoveOptions, AsukaCameraView } from '../types/camera';
import type {
  AsukaParametricDefinition,
  AsukaParametricMutationOptions,
  AsukaParametricProductDescriptor,
  AsukaParametricRuntimeMeasurements,
  AsukaParametricValue,
} from '../types/parametric3d';
import {
  createParametricDefaultValues,
  evaluateParametricDefinition,
  isParametricExpressionTruthy,
  resolveParametricAttachmentOffset,
  resolveParametricNumber,
} from '../core/parametric3d';
import { withInferredParametricMeasurements } from '../core/parametricMeasurement';
import {
  ASUKA_ASSEMBLY_DEFAULT_PLAN,
  createAssemblyBom,
  createAssemblyExportBlob,
  createAssemblyQuote,
  createAssemblyWorldAnchors,
  findAssemblyInteriorPlacement,
  applyAssemblyMaterialPreset,
  assemblyWorldToDisplay,
  getAssemblyComponentSupport,
  getAssemblyFloorEdgeId,
  getAssemblyFloorElevation,
  getAssemblyFloorMetrics,
  getAssemblyMaterialPresets,
  getAssemblyPolygonSignedArea,
  getAssemblyPlanBounds,
  getAssemblyRoomWallFootprint,
  getAssemblyStackAlignmentDelta,
  getAssemblyStackTargetWidth,
  getAssemblyStackWidthAxis,
  getCompatibleAssemblyConnectorType,
  isAssemblyPolygonSimple,
  normalizeAssemblyComponentDefinition,
  normalizeAssemblySettings,
  normalizeAssemblyStackLink,
  normalizeAssemblyState,
  reconcileAssemblyRoomWalls,
  removeAssemblyMaterialPreset,
  rotateAssemblyVector,
  resolveAssemblyPlacement,
  setAssemblyFloorEdgeLength,
  setAssemblyFloorEdgeAngle,
  setAssemblyFloorEdgeConstraint,
  setAssemblyRoomEdgeWallEnabled,
  saveAssemblyMaterialPreset,
  transformAssemblySurface,
  getAssemblySurfaceTransform,
  validateAssembly,
  validateAssemblyConnection,
} from '../core/assembly3d';
import {
  collect3DHistoryAssets,
  hydrate3DHistoryScene,
  hydrate3DHistorySceneFromRuntime,
  normalize3DHistoryAssets,
  type Asuka3DHistoryAssets,
} from '../core/history3d';
import {
  getRuntimeStoreState,
  hasRuntimeStoreBridge,
  setRuntimeStoreBridge,
  subscribeRuntimeStore,
} from '../core/runtimeStoreBridge';

type BabylonEngineOptions = Asuka3DEngineOptions;

let gltfLoaderPromise: Promise<unknown> | null = null;
const ensureGltfLoader = (): Promise<unknown> => {
  if (!gltfLoaderPromise) {
    gltfLoaderPromise = import('@babylonjs/loaders/glTF').catch((error) => {
      gltfLoaderPromise = null;
      throw error;
    });
  }
  return gltfLoaderPromise;
};

// The shell provides the store bridge; standalone consumers load it at mount time.
const ensureRuntimeStoreBridge = async (): Promise<void> => {
  if (hasRuntimeStoreBridge()) return;
  try {
    const module = await import('../store/asukaStore');
    setRuntimeStoreBridge(module.useAsukaStore);
  } catch {}
};
const useAsukaStore: any = {
  getState: () => getRuntimeStoreState(),
  subscribe: (fn: any) => subscribeRuntimeStore(fn),
};

type DecorationRecord = Asuka3DDecoration & {
  nativeMeshId?: string;
};

const makeId = (prefix: string) => `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;

const get3DDeclaredPricingColors = (source: any, metadata?: any): string[] => {
  const colors = new Set(collectDeclaredPricingColors(
    source?.color,
    source?.colors,
    metadata?.color,
    metadata?.colors,
    metadata?.pricingColors,
  ));
  collectPricingColorsFromSvg(source?.svg).forEach((color) => colors.add(color));
  return [...colors];
};

const toVec3 = (value: unknown, fallback: [number, number, number]): Vector3 => {
  if (Array.isArray(value) && value.length >= 3) {
    return new Vector3(Number(value[0]) || 0, Number(value[1]) || 0, Number(value[2]) || 0);
  }
  return new Vector3(fallback[0], fallback[1], fallback[2]);
};

const fromVec3 = (value: Vector3 | null | undefined): [number, number, number] => [
  Number(value?.x ?? 0) || 0,
  Number(value?.y ?? 0) || 0,
  Number(value?.z ?? 0) || 0,
];

const color3FromInput = (value: unknown, fallback = '#ffffff'): Color3 => {
  const raw = String(value ?? fallback).trim() || fallback;
  try { return Color3.FromHexString(raw); } catch { return Color3.FromHexString(fallback); }
};

const color4FromInput = (value: unknown, fallback = '#ffffff00'): Color4 => {
  const raw = String(value ?? fallback).trim() || fallback;
  try { return Color4.FromHexString(raw); } catch { return Color4.FromHexString(fallback); }
};

const color3ToCssHex = (value: any, fallback = '#ffffff'): string => {
  try {
    if (value && typeof value.toHexString === 'function') return value.toHexString();
    if (value && typeof value === 'object' && ['r', 'g', 'b'].every((key) => typeof value[key] === 'number')) {
      const r = Math.max(0, Math.min(255, Math.round(Number(value.r) * 255)));
      const g = Math.max(0, Math.min(255, Math.round(Number(value.g) * 255)));
      const b = Math.max(0, Math.min(255, Math.round(Number(value.b) * 255)));
      return `#${[r, g, b].map((item) => item.toString(16).padStart(2, '0')).join('')}`;
    }
  } catch {}
  return fallback;
};

const getMaterialBaseColorCss = (material: any, fallback = '#ffffff'): string => {
  const color = material?.albedoColor ?? material?.diffuseColor ?? material?.baseColor ?? null;
  return color3ToCssHex(color, fallback);
};

const clonePlain = <T,>(value: T): T => {
  try { return JSON.parse(JSON.stringify(value)); } catch { return value; }
};

const fileToDataUrl = async (file: File): Promise<string> => await new Promise((resolve, reject) => {
  try {
    const reader = new FileReader();
    reader.onload = () => resolve(String(reader.result ?? ''));
    reader.onerror = () => reject(reader.error ?? new Error('Unable to read file.'));
    reader.readAsDataURL(file);
  } catch (error) {
    reject(error);
  }
});

const createObjectUrlFromSvg = (svg: string): string => {
  const blob = new Blob([svg], { type: 'image/svg+xml' });
  const url = URL.createObjectURL(blob);
  return url;
};

const looksLikeInlineSvg = (value: unknown): boolean => /^\s*(?:<\?xml[\s\S]*?>\s*)?<svg[\s>]/i.test(String(value ?? ''));

const svgToDataUrl = (svg: string): string => `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;

const looksLikeSvgUrl = (value: unknown): boolean => {
  const raw = String(value ?? '').trim();
  return /^data:image\/svg\+xml/i.test(raw) || /^blob:/i.test(raw) || /\.svg(?:[?#].*)?$/i.test(raw);
};

const decodeSvgDataUrl = (url: string): string => {
  const raw = String(url ?? '').trim();
  if (!/^data:image\/svg\+xml/i.test(raw)) return '';
  const comma = raw.indexOf(',');
  if (comma < 0) return '';
  const meta = raw.slice(0, comma).toLowerCase();
  const payload = raw.slice(comma + 1);
  try {
    if (meta.includes(';base64')) return atob(payload);
    return decodeURIComponent(payload);
  } catch {
    return '';
  }
};

const getSvgRasterSize = (svg: string): { width: number; height: number } => {
  const source = String(svg ?? '');
  const attr = (name: string): number => {
    const match = source.match(new RegExp(`${name}\\s*=\\s*["']?([0-9.]+)`, 'i'));
    const value = match ? Number(match[1]) : 0;
    return Number.isFinite(value) && value > 0 ? value : 0;
  };
  let width = attr('width');
  let height = attr('height');
  if (!width || !height) {
    const viewBox = source.match(/viewBox\s*=\s*["']([^"']+)["']/i)?.[1]
      ?.trim()
      .split(/[\s,]+/)
      .map((part) => Number(part));
    if (Array.isArray(viewBox) && viewBox.length >= 4) {
      width = width || Math.abs(Number(viewBox[2]) || 0);
      height = height || Math.abs(Number(viewBox[3]) || 0);
    }
  }
  const fallback = 1024;
  width = Math.max(16, Math.min(4096, width || fallback));
  height = Math.max(16, Math.min(4096, height || fallback));
  return { width, height };
};

const rasterizeSvgToPngDataUrl = async (svg: string): Promise<string> => {
  const source = String(svg ?? '').trim();
  if (!source || typeof document === 'undefined' || typeof Image === 'undefined') return '';
  const { width, height } = getSvgRasterSize(source);
  const image = new Image();
  const dataUrl = svgToDataUrl(source);
  await new Promise<void>((resolve, reject) => {
    image.onload = () => resolve();
    image.onerror = () => reject(new Error('Unable to rasterize SVG design.'));
    image.src = dataUrl;
  });
  const canvas = document.createElement('canvas');
  canvas.width = Math.round(width);
  canvas.height = Math.round(height);
  const ctx = canvas.getContext('2d');
  if (!ctx) return '';
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
  try { return canvas.toDataURL('image/png'); } catch { return ''; }
};

const fetchSvgTextIfPossible = async (url: string): Promise<string> => {
  const raw = String(url ?? '').trim();
  if (!raw) return '';
  if (looksLikeInlineSvg(raw)) return raw;
  const dataSvg = decodeSvgDataUrl(raw);
  if (dataSvg) return dataSvg;
  if (!looksLikeSvgUrl(raw) || typeof fetch !== 'function') return '';
  try {
    const response = await fetch(raw, { credentials: 'same-origin' as RequestCredentials });
    if (!response.ok) return '';
    const text = await response.text();
    return looksLikeInlineSvg(text) ? text : '';
  } catch {
    return '';
  }
};

const prepareTransparent3DImageSource = async (input: { url?: unknown; src?: unknown; svg?: unknown; svgText?: unknown } = {}): Promise<{ url: string; svg: string; rasterized?: boolean }> => {
  const normalized = normalize3DImageSource(input);
  let { url, svg } = normalized;
  if (!svg && url) svg = await fetchSvgTextIfPossible(url);
  if (svg) {
    const rasterUrl = await rasterizeSvgToPngDataUrl(svg).catch(() => '');
    if (rasterUrl) return { url: rasterUrl, svg, rasterized: true };
    return { url: svgToDataUrl(svg), svg, rasterized: false };
  }
  return { url, svg: '', rasterized: false };
};

const normalize3DImageSource = (input: { url?: unknown; src?: unknown; svg?: unknown; svgText?: unknown } = {}): { url: string; svg: string } => {
  let url = String(input.url ?? input.src ?? '').trim();
  let svg = String(input.svgText ?? input.svg ?? '').trim();
  if (!svg && looksLikeInlineSvg(url)) {
    svg = url;
    url = '';
  }
  if (!url && svg) url = createObjectUrlFromSvg(svg);
  return { url, svg };
};



const isProduct3DElementInput = (element: any): boolean => {
  const type = String(element?.type ?? element?.kind ?? '').trim().toLowerCase();
  return type === 'product-3d' || !!element?.product3d;
};

const findElementVariationById = (variations: any[], variationId?: string): any | null => {
  const path = findElementVariationPathById(variations, variationId);
  return path?.[path.length - 1] ?? null;
};

const findElementVariationPathById = (variations: any[], variationId?: string, trail: any[] = []): any[] | null => {
  const needle = String(variationId ?? '').trim();
  if (!needle) return null;
  for (const variation of Array.isArray(variations) ? variations : []) {
    if (!variation) continue;
    const nextTrail = [...trail, variation];
    if (String(variation.id ?? '').trim() === needle) return nextTrail;
    const found = findElementVariationPathById(Array.isArray(variation.variations) ? variation.variations : [], needle, nextTrail);
    if (found) return found;
  }
  return null;
};

const getFirstLeafVariationPath = (variations: any[]): any[] => {
  const list = Array.isArray(variations) ? variations : [];
  if (!list.length) return [];
  const path: any[] = [];
  let cursor: any = list[0];
  while (cursor) {
    path.push(cursor);
    const children = Array.isArray(cursor.variations) ? cursor.variations : [];
    if (!children.length) break;
    cursor = children[0];
  }
  return path;
};

const getActiveVariationPath = (variations: any[], trail: any[] = []): any[] => {
  for (const variation of Array.isArray(variations) ? variations : []) {
    if (!variation) continue;
    const nextTrail = [...trail, variation];
    if (variation.active && variation.id) return nextTrail;
    const found = getActiveVariationPath(Array.isArray(variation.variations) ? variation.variations : [], nextTrail);
    if (found.length) return found;
  }
  return trail.length ? [] : getFirstLeafVariationPath(variations);
};

const mergeMaterialMaps = (base: any, incoming: any): Record<string, any> => {
  const next: Record<string, any> = { ...(base && typeof base === 'object' ? base : {}) };
  for (const [key, value] of Object.entries(incoming && typeof incoming === 'object' ? incoming : {})) {
    const id = String(key ?? '').trim();
    if (!id || !value || typeof value !== 'object') continue;
    next[id] = { ...((next[id] && typeof next[id] === 'object') ? next[id] : {}), ...(value as any) };
  }
  return next;
};

const mergeProduct3DVariationPath = (variationPath: any[]): any => {
  const merged: any = {};
  for (const variation of Array.isArray(variationPath) ? variationPath : []) {
    const product3d = variation?.product3d && typeof variation.product3d === 'object' ? variation.product3d : null;
    if (!product3d) continue;
    const { materials, model, ...rest } = product3d;
    Object.assign(merged, rest);
    if (model && typeof model === 'object') merged.model = { ...(merged.model ?? {}), ...model };
    if (materials && typeof materials === 'object') merged.materials = mergeMaterialMaps(merged.materials, materials);
  }
  const materialKeys = Object.keys(merged.materials ?? {});
  if (!String(merged.materialTarget ?? '').trim()) merged.materialTarget = materialKeys[0] ?? 'product';
  return merged;
};

const normalizeVariationPathInput = (element: any, input: AsukaElementInput): any[] => {
  const explicitPath = Array.isArray((input as any)?.variationPath) ? (input as any).variationPath.filter(Boolean) : [];
  if (explicitPath.length) return explicitPath;
  const variations = Array.isArray(element?.variations) ? element.variations : [];
  const byId = findElementVariationPathById(variations, String(input?.variationId ?? input?.variation?.id ?? ''));
  if (byId?.length) return byId;
  if (input?.variation && typeof input.variation === 'object') return [input.variation];
  return getActiveVariationPath(variations);
};


const normalize3DPrintAreaCanvasSize = (value: any): { width: number; height: number } => ({
  width: Math.max(16, Number(value?.width ?? 1024) || 1024),
  height: Math.max(16, Number(value?.height ?? 1024) || 1024),
});


const split3DList = (value: any): string[] => String(Array.isArray(value) ? value.join(',') : (value ?? ''))
  .split(',')
  .map((item) => item.trim())
  .filter(Boolean);



const normalize3DParts = (value: any): any[] => {
  const source = Array.isArray(value) ? value : [];
  return source.map((part: any, index: number) => {
    const id = String(part?.id ?? `part-${index + 1}`).trim() || `part-${index + 1}`;
    const normalized = {
      ...(part && typeof part === 'object' ? part : {}),
      id,
      label: String(part?.label ?? part?.name ?? id).trim() || id,
      meshNames: split3DList(part?.meshNames ?? part?.meshName ?? part?.targetMeshIds ?? part?.targetMeshId),
      materialNames: split3DList(part?.materialNames ?? part?.materialName),
      materialTarget: String(part?.materialTarget ?? id).trim() || id,
      editable: part?.editable !== false,
      printable: part?.printable === true,
      visible: part?.visible !== false,
      locked: part?.locked === true,
      parentPartId: String(part?.parentPartId ?? '').trim() || undefined,
    };
    try { delete (normalized as any).variations; } catch {}
    return normalized;
  });
};

const compact3DPartMetadataForScene = (metadata: any): Record<string, unknown> | undefined => {
  if (!metadata || typeof metadata !== 'object') return undefined;
  const allowed = new Set([
    'engine',
    'viewId',
    'partId',
    'asukaProduct3DPartId',
    'asukaProduct3DPartLabel',
    'sourceMeshId',
    'sourceMeshName',
    'nativeId',
    'nativeName',
    'detectedFrom',
    'detectionKind',
    'componentIndex',
    'componentRank',
    'componentVertices',
    'componentTriangles',
    'componentBboxMin',
    'componentBboxMax',
  ]);
  const out: Record<string, unknown> = {};
  for (const key of Object.keys(metadata)) {
    if (!allowed.has(key)) continue;
    const value = metadata[key];
    if (value == null) continue;
    if (typeof value === 'string' && value.length > 1024) continue;
    if (Array.isArray(value) && value.length > 12) continue;
    if (typeof value === 'object' && !Array.isArray(value)) {
      try { out[key] = clonePlain(value); } catch {}
    } else {
      out[key] = value;
    }
  }
  return Object.keys(out).length ? out : undefined;
};

const compact3DPartForScene = (part: any, index = 0): Record<string, unknown> => {
  const id = String(part?.id ?? `part-${index + 1}`).trim() || `part-${index + 1}`;
  const meshNames = split3DList(part?.meshNames ?? part?.meshName ?? part?.targetMeshIds ?? part?.targetMeshId);
  const materialNames = split3DList(part?.materialNames ?? part?.materialName);
  const metadata = compact3DPartMetadataForScene(part?.metadata);
  return {
    id,
    name: String(part?.name ?? '').trim() || undefined,
    label: String(part?.label ?? part?.name ?? id).trim() || id,
    meshNames,
    materialNames,
    materialTarget: String(part?.materialTarget ?? id).trim() || id,
    editable: part?.editable !== false,
    printable: part?.printable === true,
    visible: part?.visible !== false,
    locked: part?.locked === true,
    parentPartId: String(part?.parentPartId ?? '').trim() || undefined,
    ...(metadata ? { metadata } : {}),
  };
};

const compact3DPartsForScene = (parts: any): Record<string, unknown>[] => (Array.isArray(parts) ? parts : [])
  .map((part, index) => compact3DPartForScene(part, index));

const isHeavy3DDataUrl = (value: unknown): boolean => {
  const raw = String(value ?? '').trim();
  if (!raw) return false;
  if (/^data:(?:model\/|application\/(?:octet-stream|gltf|glb|x-binary))/i.test(raw)) return true;
  return /^data:/i.test(raw) && raw.length > 8_000_000;
};

const compact3DMetadataForState = (metadata: any): Record<string, unknown> | undefined => {
  if (!metadata || typeof metadata !== 'object') return undefined;
  const out: Record<string, unknown> = {};
  for (const key of Object.keys(metadata)) {
    if (/^(native|__native|runtime|product3d|model|file|files|blob|data|texture|imageElement|canvas|mesh|material)$/i.test(key)) continue;
    const value = metadata[key];
    if (value == null) continue;
    if (typeof value === 'string') {
      if (isHeavy3DDataUrl(value) || value.length > 4096) continue;
      out[key] = value;
      continue;
    }
    if (typeof value === 'number' || typeof value === 'boolean') {
      out[key] = value;
      continue;
    }
    if (Array.isArray(value)) {
      if (value.length > 32) continue;
      try { out[key] = clonePlain(value); } catch {}
      continue;
    }
    if (typeof value === 'object') {
      try {
        const serialized = JSON.stringify(value);
        if (serialized.length <= 4096 && !serialized.match(/data:(?:model\/|application\/(?:octet-stream|gltf|glb|x-binary))/i)) out[key] = clonePlain(value);
      } catch {}
    }
  }
  return Object.keys(out).length ? out : undefined;
};

const compact3DMaterialConfigForState = (material: any, options: { history?: boolean } = {}): Record<string, unknown> => {
  if (!material || typeof material !== 'object') return {};
  const out: Record<string, unknown> = {};
  const allowed = new Set([
    'id', 'name', 'label', 'color', 'diffuseColor', 'emissiveColor', 'specularColor',
    'opacity', 'alpha', 'metallic', 'roughness', 'textureUrl', 'normalTextureUrl', 'useNativeMaterial', 'useTextureAlpha', 'alphaFromTexture', 'opacityTextureUrl',
    'textureRepeatX', 'textureRepeatY', 'repeatX', 'repeatY', 'textureScaleX', 'textureScaleY', 'uScale', 'vScale',
    'textureOffsetX', 'textureOffsetY', 'uOffset', 'vOffset', 'targetMeshIds',
    'targetMeshes', 'materialTarget', 'metadata',
  ]);
  for (const key of Object.keys(material)) {
    if (!allowed.has(key)) continue;
    const value = material[key];
    if (value == null) continue;
    if (key === 'metadata') {
      const metadata = compact3DMetadataForState(value);
      if (metadata) out.metadata = metadata;
      continue;
    }
    if (typeof value === 'string') {
      if (isHeavy3DDataUrl(value)) continue;
      if (options.history && /^data:/i.test(value) && value.length > 524_288) continue;
      if (value.length > 8_000_000) continue;
      out[key] = value;
      continue;
    }
    if (typeof value === 'number' || typeof value === 'boolean') {
      out[key] = value;
      continue;
    }
    if (Array.isArray(value)) {
      if (value.length <= 64) { try { out[key] = clonePlain(value); } catch {} }
      continue;
    }
    if (typeof value === 'object') {
      try {
        const serialized = JSON.stringify(value);
        if (serialized.length <= (options.history ? 2048 : 8192)) out[key] = clonePlain(value);
      } catch {}
    }
  }
  return out;
};

const compact3DMaterialStateForPersistence = (materials: any, options: { history?: boolean } = {}): Record<string, Record<string, unknown>> => {
  const out: Record<string, Record<string, unknown>> = {};
  for (const [key, value] of Object.entries(materials && typeof materials === 'object' ? materials : {})) {
    const id = String(key ?? '').trim();
    if (!id || !value || typeof value !== 'object') continue;
    out[id] = compact3DMaterialConfigForState(value, options);
  }
  return out;
};

const compact3DModelForState = (model: any): Asuka3DModel | undefined => {
  if (!model || typeof model !== 'object') return undefined;
  const url = String(model?.url ?? '').trim();
  if (!url) return undefined;
  const format = String(model?.format ?? '').trim();
  const metadata = compact3DMetadataForState(model?.metadata);
  return {
    url,
    ...(format ? { format } : {}),
    ...(metadata ? { metadata } : {}),
  };
};

const compact3DModelForHistory = (model: any): Asuka3DModel | undefined => {
  if (!model || typeof model !== 'object') return undefined;
  const url = String(model?.url ?? '').trim();
  if (!url || isHeavy3DDataUrl(url) || url.length > 4096) return undefined;
  const format = String(model?.format ?? '').trim();
  const metadata = compact3DMetadataForState(model?.metadata);
  return {
    url,
    ...(format ? { format } : {}),
    ...(metadata ? { metadata } : {}),
  };
};

const is3DPartVariationGroup = (variation: any): boolean => Boolean(
  variation?.metadata?.asukaProduct3DPartGroup === true
  || variation?.product3d?.partGroup === true,
);

const find3DPartById = (parts: any[], partId: string): any | null => {
  const needle = String(partId ?? '').trim();
  if (!needle) return null;
  return (Array.isArray(parts) ? parts : []).find((part: any) => String(part?.id ?? '').trim() === needle) ?? null;
};

const get3DPartIdFromVariationNode = (variation: any): string => String(
  variation?.product3d?.partId
  ?? variation?.metadata?.asukaProduct3DPartId
  ?? variation?.metadata?.partId
  ?? '',
).trim();

const find3DPartGroupInVariationPath = (variationPath: any[]): any | null => (Array.isArray(variationPath) ? variationPath : [])
  .find((variation: any) => is3DPartVariationGroup(variation) && get3DPartIdFromVariationNode(variation)) ?? null;

const getPartMaterialTarget = (part: any): string => {
  const meshNames = split3DList(part?.meshNames ?? part?.meshName ?? part?.targetMeshIds ?? part?.targetMeshId);
  return String(part?.materialTarget ?? part?.id ?? meshNames[0] ?? '').trim();
};

const materialFromVariationTreeForPart = (variationPath: any[], part: any): any | null => {
  const merged = mergeProduct3DVariationPath(variationPath);
  const meshNames = split3DList(part?.meshNames ?? part?.meshName ?? part?.targetMeshIds ?? part?.targetMeshId);
  const target = String(merged?.materialTarget ?? getPartMaterialTarget(part)).trim();
  if (!target) return null;
  const materials = merged?.materials && typeof merged.materials === 'object' ? merged.materials : {};
  const material =
    (materials[target] && typeof materials[target] === 'object' ? materials[target] : null)
    ?? Object.values(materials).find((item: any) => item && typeof item === 'object')
    ?? null;
  if (!material) return null;
  return {
    ...(material as any),
    label: String((material as any)?.label ?? part?.label ?? part?.id ?? target),
    targetMeshIds: meshNames,
    metadata: {
      ...(((material as any)?.metadata && typeof (material as any).metadata === 'object') ? (material as any).metadata : {}),
      partId: String(part?.id ?? ''),
      partLabel: String(part?.label ?? part?.id ?? ''),
      materialTarget: target,
      targetMeshIds: meshNames,
    },
  };
};

const collectActive3DPartMaterialsFromVariationTree = (variations: any[], partsInput: any): Record<string, any> => {
  const parts = normalize3DParts(partsInput);
  const out: Record<string, any> = {};
  const walk = (list: any[], trail: any[] = []) => {
    for (const variation of Array.isArray(list) ? list : []) {
      if (!variation || typeof variation !== 'object') continue;
      const nextTrail = [...trail, variation];
      if (is3DPartVariationGroup(variation)) {
        const partId = String(variation?.product3d?.partId ?? variation?.metadata?.asukaProduct3DPartId ?? '').trim();
        const part = find3DPartById(parts, partId);
        if (!part || part?.editable === false || part?.visible === false) continue;
        const activePath = getActiveVariationPath(Array.isArray(variation?.variations) ? variation.variations : []);
        const material = materialFromVariationTreeForPart([...nextTrail, ...activePath], part);
        const materialTarget = String(material?.metadata?.materialTarget ?? variation?.product3d?.materialTarget ?? part?.materialTarget ?? part?.id ?? '').trim();
        if (material && materialTarget) out[materialTarget] = material;
        continue;
      }
      if (Array.isArray(variation?.variations) && variation.variations.length) walk(variation.variations, nextTrail);
    }
  };
  walk(variations);
  if (Object.keys(out).length === 0 && parts.length === 1) {
    const part = parts[0];
    if (part?.editable !== false && part?.visible !== false) {
      const activePath = getActiveVariationPath(Array.isArray(variations) ? variations : []);
      const material = materialFromVariationTreeForPart(activePath, part);
      const materialTarget = String(material?.metadata?.materialTarget ?? part?.materialTarget ?? part?.id ?? '').trim();
      if (material && materialTarget) out[materialTarget] = material;
    }
  }
  return out;
};

const collectActive3DPartVariationSelectionsFromVariationTree = (variations: any[], partsInput: any): Record<string, { variationId: string; variationName: string; variationPath: any[]; variationPathLabel: string; variationPrice: number }> => {
  const parts = normalize3DParts(partsInput);
  const out: Record<string, { variationId: string; variationName: string; variationPath: any[]; variationPathLabel: string; variationPrice: number }> = {};
  const walk = (list: any[], trail: any[] = []) => {
    for (const variation of Array.isArray(list) ? list : []) {
      if (!variation || typeof variation !== 'object') continue;
      const nextTrail = [...trail, variation];
      if (is3DPartVariationGroup(variation)) {
        const partId = String(variation?.product3d?.partId ?? variation?.metadata?.asukaProduct3DPartId ?? '').trim();
        const part = find3DPartById(parts, partId);
        if (!part || part?.editable === false || part?.visible === false) continue;
        const activePath = getActiveVariationPath(Array.isArray(variation?.variations) ? variation.variations : []);
        const activeVariation = activePath[activePath.length - 1];
        const variationId = String(activeVariation?.id ?? '').trim();
        if (!variationId) continue;
        const fullPath = [...nextTrail, ...activePath];
        const records = fullPath.map((node: any) => ({
          id: String(node?.id ?? '').trim() || undefined,
          name: String(node?.name ?? node?.label ?? '').trim() || undefined,
        }));
        out[partId] = {
          variationId,
          variationName: String(activeVariation?.name ?? activeVariation?.label ?? variationId).trim() || variationId,
          variationPath: records,
          variationPathLabel: records.map((node: any) => String(node?.name ?? node?.id ?? '').trim()).filter(Boolean).join(' / '),
          variationPrice: sumPriceInputs(fullPath.map((node: any) => node?.price)),
        };
        continue;
      }
      if (Array.isArray(variation?.variations) && variation.variations.length) walk(variation.variations, nextTrail);
    }
  };
  walk(variations);
  if (Object.keys(out).length === 0 && parts.length === 1) {
    const part = parts[0];
    if (part?.editable !== false && part?.visible !== false) {
      const activePath = getActiveVariationPath(Array.isArray(variations) ? variations : []);
      const activeVariation = activePath[activePath.length - 1];
      const variationId = String(activeVariation?.id ?? '').trim();
      const partId = String(part?.id ?? '').trim();
      if (variationId && partId) {
        const records = activePath.map((node: any) => ({
          id: String(node?.id ?? '').trim() || undefined,
          name: String(node?.name ?? node?.label ?? '').trim() || undefined,
        }));
        out[partId] = {
          variationId,
          variationName: String(activeVariation?.name ?? activeVariation?.label ?? variationId).trim() || variationId,
          variationPath: records,
          variationPathLabel: records.map((node: any) => String(node?.name ?? node?.id ?? '').trim()).filter(Boolean).join(' / '),
          variationPrice: sumPriceInputs(activePath.map((node: any) => node?.price)),
        };
      }
    }
  }
  return out;
};

const collectActive3DPartVariationApplyTargetsFromVariationTree = (variations: any[], partsInput: any): Record<string, { variationId: string; variationName: string; variationPath: any[]; variationPathLabel: string; variation: any }> => {
  const parts = normalize3DParts(partsInput);
  const out: Record<string, { variationId: string; variationName: string; variationPath: any[]; variationPathLabel: string; variation: any }> = {};
  const walk = (list: any[], trail: any[] = []) => {
    for (const variation of Array.isArray(list) ? list : []) {
      if (!variation || typeof variation !== 'object') continue;
      const nextTrail = [...trail, variation];
      if (is3DPartVariationGroup(variation)) {
        const partId = String(variation?.product3d?.partId ?? variation?.metadata?.asukaProduct3DPartId ?? '').trim();
        const part = find3DPartById(parts, partId);
        if (!part || part?.editable === false || part?.visible === false) continue;
        const activePath = getActiveVariationPath(Array.isArray(variation?.variations) ? variation.variations : []);
        const activeVariation = activePath[activePath.length - 1];
        const variationId = String(activeVariation?.id ?? '').trim();
        if (!variationId) continue;
        const fullPath = [...nextTrail, ...activePath];
        const records = fullPath.map((node: any) => ({
          id: String(node?.id ?? '').trim() || undefined,
          name: String(node?.name ?? node?.label ?? '').trim() || undefined,
        }));
        out[partId] = {
          variationId,
          variationName: String(activeVariation?.name ?? activeVariation?.label ?? variationId).trim() || variationId,
          variationPath: fullPath,
          variationPathLabel: records.map((node: any) => String(node?.name ?? node?.id ?? '').trim()).filter(Boolean).join(' / '),
          variation: activeVariation,
        };
        continue;
      }
      if (Array.isArray(variation?.variations) && variation.variations.length) walk(variation.variations, nextTrail);
    }
  };
  walk(variations);
  if (Object.keys(out).length === 0 && parts.length === 1) {
    const part = parts[0];
    if (part?.editable !== false && part?.visible !== false) {
      const activePath = getActiveVariationPath(Array.isArray(variations) ? variations : []);
      const activeVariation = activePath[activePath.length - 1];
      const variationId = String(activeVariation?.id ?? '').trim();
      const partId = String(part?.id ?? '').trim();
      if (variationId && partId) {
        const records = activePath.map((node: any) => ({
          id: String(node?.id ?? '').trim() || undefined,
          name: String(node?.name ?? node?.label ?? '').trim() || undefined,
        }));
        out[partId] = {
          variationId,
          variationName: String(activeVariation?.name ?? activeVariation?.label ?? variationId).trim() || variationId,
          variationPath: activePath,
          variationPathLabel: records.map((node: any) => String(node?.name ?? node?.id ?? '').trim()).filter(Boolean).join(' / '),
          variation: activeVariation,
        };
      }
    }
  }
  return out;
};


const get3DPrintAreaCoordinateFrame = (mode: unknown): AsukaCoordinateFrame => (
  String(mode ?? '').trim().toLowerCase() === 'surface'
    ? { space: 'surface-tangent', unit: 'world-unit' }
    : { space: 'texture', unit: 'texture-px' }
);

const with3DPrintAreaCoordinateFrame = (area: Asuka3DPrintArea): Asuka3DPrintArea => {
  const frame = get3DPrintAreaCoordinateFrame(area?.mode);
  if (area?.space === frame.space && area?.unit === frame.unit) return area;
  return { ...(area as any), ...frame } as Asuka3DPrintArea;
};

const normalize3DPrintAreas = (value: any): Asuka3DPrintArea[] => {
  const source = Array.isArray(value) ? value : [];
  return source.map((area: any, index: number) => {
    const id = String(area?.id ?? `print-area-${index + 1}`).trim() || `print-area-${index + 1}`;
    const mode = String(area?.mode ?? 'texture').trim() === 'surface' ? 'surface' : 'texture';
    return {
      ...(area && typeof area === 'object' ? area : {}),
      id,
      label: String(area?.label ?? `Print area ${index + 1}`).trim() || `Print area ${index + 1}`,
      targetMeshId: String(area?.targetMeshId ?? 'product').trim() || 'product',
      mode,
      ...get3DPrintAreaCoordinateFrame(mode),
      textureSlot: String(area?.textureSlot ?? 'diffuseTexture').trim() || 'diffuseTexture',
      canvasSize: normalize3DPrintAreaCanvasSize(area?.canvasSize),
      objects: Array.isArray(area?.objects) ? clonePlain(area.objects) : [],
      visible: area?.visible !== false,
      locked: area?.locked === true,
    } as Asuka3DPrintArea;
  });
};

const has3DMaterialVisualOverride = (material: any): boolean => {
  if (!material || typeof material !== 'object') return false;
  const visualKeys = [
    'color', 'diffuseColor', 'emissiveColor', 'specularColor',
    'opacity', 'alpha', 'metallic', 'roughness',
    'textureUrl', 'normalTextureUrl', 'opacityTextureUrl', 'useTextureAlpha', 'alphaFromTexture',
    'textureRepeatX', 'textureRepeatY', 'repeatX', 'repeatY',
    'textureScaleX', 'textureScaleY', 'uScale', 'vScale',
    'textureOffsetX', 'textureOffsetY', 'uOffset', 'vOffset',
  ];
  return visualKeys.some((key) => {
    if (!Object.prototype.hasOwnProperty.call(material, key)) return false;
    const value = material[key];
    return typeof value === 'string' ? value.trim().length > 0 : value != null;
  });
};

const normalizeProduct3DConfig = (element: any, variation?: any, variationPathInput?: any[]) => {
  const product = { ...((element?.product3d && typeof element.product3d === 'object') ? element.product3d : {}) } as any;
  const variationPath = Array.isArray(variationPathInput) && variationPathInput.length ? variationPathInput : (variation ? [variation] : []);
  const mergedVariationProduct = mergeProduct3DVariationPath(variationPath);
  const modelUrl = String(mergedVariationProduct?.model?.url ?? product?.model?.url ?? '').trim();
  const modelFormat = String(mergedVariationProduct?.model?.format ?? product?.model?.format ?? 'glb').trim() || 'glb';
  const previewUrl = String(mergedVariationProduct?.preview?.url ?? mergedVariationProduct?.previewUrl ?? variation?.image ?? variation?.icon ?? element?.image ?? element?.icon ?? '').trim();
  const parts = normalize3DParts(
    Array.isArray(mergedVariationProduct?.parts)
      ? mergedVariationProduct.parts
      : (Array.isArray(product?.parts) ? product.parts : []),
  );
  const printAreas = normalize3DPrintAreas(
    Array.isArray(mergedVariationProduct?.printAreas)
      ? mergedVariationProduct.printAreas
      : (Array.isArray(product?.printAreas) ? product.printAreas : []),
  );

  const partMaterials = collectActive3DPartMaterialsFromVariationTree(Array.isArray(element?.variations) ? element.variations : [], parts);
  const rawVariationMaterials = mergedVariationProduct?.materials && typeof mergedVariationProduct.materials === 'object' ? { ...(mergedVariationProduct.materials as any) } : {};
  const selectedPartGroup = find3DPartGroupInVariationPath(variationPath);
  const selectedPartId = get3DPartIdFromVariationNode(selectedPartGroup);
  const selectedPart = selectedPartId ? find3DPartById(parts, selectedPartId) : null;
  const selectedPartTarget = selectedPart ? getPartMaterialTarget(selectedPart) : '';
  let variationMaterials = rawVariationMaterials;
  if (selectedPartTarget) {
    const rawTarget = String(mergedVariationProduct?.materialTarget ?? '').trim();
    const sourceMaterial =
      (rawVariationMaterials[selectedPartTarget] && typeof rawVariationMaterials[selectedPartTarget] === 'object' ? rawVariationMaterials[selectedPartTarget] : null)
      ?? (rawTarget && rawVariationMaterials[rawTarget] && typeof rawVariationMaterials[rawTarget] === 'object' ? rawVariationMaterials[rawTarget] : null)
      ?? (rawVariationMaterials.product && typeof rawVariationMaterials.product === 'object' ? rawVariationMaterials.product : null)
      ?? Object.values(rawVariationMaterials).find((item: any) => item && typeof item === 'object')
      ?? (partMaterials[selectedPartTarget] && typeof partMaterials[selectedPartTarget] === 'object' ? partMaterials[selectedPartTarget] : null);
    const targetMeshIds = split3DList(selectedPart?.meshNames ?? selectedPart?.meshName ?? selectedPart?.targetMeshIds ?? selectedPart?.targetMeshId);
    variationMaterials = sourceMaterial ? {
      [selectedPartTarget]: {
        ...(sourceMaterial as any),
        label: String((sourceMaterial as any)?.label ?? selectedPart?.label ?? selectedPartTarget),
        targetMeshIds,
        metadata: {
          ...(((sourceMaterial as any)?.metadata && typeof (sourceMaterial as any).metadata === 'object') ? (sourceMaterial as any).metadata : {}),
          partId: selectedPartId,
          partLabel: String(selectedPart?.label ?? selectedPartId),
          materialTarget: selectedPartTarget,
          targetMeshIds,
        },
      },
    } : {};
  }
  const firstVariationTarget = Object.keys(variationMaterials)[0];
  const materialTarget = String(selectedPartTarget || mergedVariationProduct?.materialTarget || firstVariationTarget || 'product').trim() || 'product';
  const materials = {
    ...partMaterials,
    ...variationMaterials,
  } as Record<string, any>;
  const targetMaterial = { ...((materials[materialTarget] && typeof materials[materialTarget] === 'object') ? materials[materialTarget] : {}) };
  if (!targetMaterial.label) targetMaterial.label = materialTarget;
  if (typeof targetMaterial.useNativeMaterial !== 'boolean' && !has3DMaterialVisualOverride(targetMaterial)) {
    targetMaterial.useNativeMaterial = true;
  }
  materials[materialTarget] = targetMaterial;

  return {
    model: modelUrl ? { url: modelUrl, format: modelFormat } : undefined,
    materials,
    materialTarget,
    previewUrl,
    parts,
    printAreas,
  };
};

const getVariationPathLabel = (variationPath: any[]): string => (Array.isArray(variationPath) ? variationPath : [])
  .map((node: any) => String(node?.name ?? node?.id ?? '').trim())
  .filter(Boolean)
  .join(' / ');

const BABYLON_PRODUCT3D_DEBUG_PREFIX = '[AsukaDesigner:Babylon3D]';

const isBabylonProduct3DDebugEnabled = (): boolean => {
  try {
    const g = globalThis as any;
    if (g?.__ASUKA_3D_PRODUCT_DEBUG__ === true) return true;
    if (g?.__ASUKA_3D_PRODUCT_DEBUG__ === false || g?.__ASUKA_DISABLE_3D_PRODUCT_DEBUG__ === true) return false;
    const search = typeof window !== 'undefined' ? String(window.location?.search ?? '') : '';
    if (/(?:^|[?&])asuka3ddebug=1(?:&|$)/i.test(search)) return true;
    if (/(?:^|[?&])asuka3ddebug=0(?:&|$)/i.test(search)) return false;
    const stored = typeof window !== 'undefined' ? window.localStorage?.getItem('asuka:3d-product-debug') : null;
    if (stored === '1' || stored === 'true' || stored === 'on') return true;
    if (stored === '0' || stored === 'false' || stored === 'off') return false;
  } catch {}
  return false;
};

const summarizeBabylonDebugValue = (value: any, depth = 0): any => {
  if (value == null) return value;
  if (value instanceof Error) return { name: value.name, message: value.message, stack: value.stack };
  if (typeof value === 'string') {
    if (/^data:/i.test(value)) {
      const mime = value.match(/^data:([^;,]+)/i)?.[1] ?? 'unknown';
      return { kind: 'data-url', mime, length: value.length, prefix: value.slice(0, 96) };
    }
    if (/^blob:/i.test(value)) return { kind: 'blob-url', length: value.length, prefix: value.slice(0, 120) };
    if (value.length > 260) return `${value.slice(0, 160)}… (${value.length} chars)`;
    return value;
  }
  if (typeof value !== 'object') return value;
  try {
    if (typeof Blob !== 'undefined' && value instanceof Blob) return { kind: 'blob', type: value.type, size: value.size };
  } catch {}
  if (Array.isArray(value)) {
    const max = depth > 1 ? 12 : 28;
    const out = value.slice(0, max).map((item) => summarizeBabylonDebugValue(item, depth + 1));
    if (value.length > max) out.push(`… ${value.length - max} more`);
    return out;
  }
  try {
    if (typeof value.getTotalVertices === 'function' || typeof value.getTotalIndices === 'function') {
      let vertices: any = undefined;
      let indices: any = undefined;
      try { vertices = value.getTotalVertices?.(); } catch {}
      try { indices = value.getTotalIndices?.(); } catch {}
      return {
        kind: 'mesh',
        id: String(value.id ?? ''),
        name: String(value.name ?? ''),
        vertices,
        indices,
        material: String(value.material?.name ?? value.material?.id ?? ''),
        enabled: typeof value.isEnabled === 'function' ? value.isEnabled() : undefined,
      };
    }
  } catch {}
  if (depth > 3) return '[Object]';
  const out: Record<string, any> = {};
  for (const key of Object.keys(value).slice(0, 45)) out[key] = summarizeBabylonDebugValue(value[key], depth + 1);
  const remaining = Object.keys(value).length - Object.keys(out).length;
  if (remaining > 0) out.__remainingKeys = remaining;
  return out;
};

const debugBabylonProduct3D = (phase: string, payload?: Record<string, any>, level: 'info' | 'warn' | 'error' = 'info') => {
  if (!isBabylonProduct3DDebugEnabled()) return;
  try {
    const entry = { at: new Date().toISOString(), phase, payload: summarizeBabylonDebugValue(payload ?? {}) };
    const g = globalThis as any;
    const logs = Array.isArray(g.__ASUKA_BABYLON_3D_LOGS__) ? g.__ASUKA_BABYLON_3D_LOGS__ : [];
    logs.push(entry);
    g.__ASUKA_BABYLON_3D_LOGS__ = logs.slice(-200);
    const logger = level === 'error' ? console.error : (level === 'warn' ? console.warn : console.info);
    logger(`${BABYLON_PRODUCT3D_DEBUG_PREFIX} ${phase}`, entry.payload);
  } catch {}
};


const isBabylon3DLayerDebugEnabled = (): boolean => {
  try {
    const g = globalThis as any;
    if (g?.__ASUKA_3D_LAYER_DEBUG_ENABLED__ === true || g?.__ASUKA_3D_LAYER_DEBUG__ === true) return true;
    if (g?.__ASUKA_3D_LAYER_DEBUG_ENABLED__ === false || g?.__ASUKA_3D_LAYER_DEBUG__ === false) return false;
    const search = typeof window !== 'undefined' ? String(window.location?.search ?? '') : '';
    if (/(?:^|[?&])asuka3dlayerdebug=1(?:&|$)/i.test(search)) return true;
    if (/(?:^|[?&])asuka3dlayerdebug=0(?:&|$)/i.test(search)) return false;
    const stored = typeof window !== 'undefined' ? window.localStorage?.getItem('asuka:3d-layer-debug') : null;
    if (stored === '1' || stored === 'true' || stored === 'on') return true;
    if (stored === '0' || stored === 'false' || stored === 'off') return false;
  } catch {}
  return false;
};

const pushBabylon3DLayerDebug = (phase: string, payload?: Record<string, any>, level: 'info' | 'warn' | 'error' = 'info'): void => {
  try {
    const entry = { at: new Date().toISOString(), phase, payload: summarizeBabylonDebugValue(payload ?? {}) };
    const g = globalThis as any;
    const logs = Array.isArray(g.__ASUKA_3D_LAYER_LOGS__) ? g.__ASUKA_3D_LAYER_LOGS__ : [];
    logs.push(entry);
    g.__ASUKA_3D_LAYER_LOGS__ = logs.slice(-500);
    if (!isBabylon3DLayerDebugEnabled()) return;
    if (level === 'error') console.error(`[AsukaDesigner:3DLayer] ${phase}`, entry.payload);
  } catch {}
};

const splitSceneUrl = (url: string): { rootUrl: string; sceneFilename: string } => {
  const trimmed = String(url ?? '').trim();
  if (!trimmed) return { rootUrl: '', sceneFilename: '' };
  if (/^(blob:|data:)/i.test(trimmed)) return { rootUrl: '', sceneFilename: trimmed };
  const index = trimmed.lastIndexOf('/');
  if (index < 0) return { rootUrl: '', sceneFilename: trimmed };
  return { rootUrl: trimmed.slice(0, index + 1), sceneFilename: trimmed.slice(index + 1) };
};

const getModelMimeType = (model: Asuka3DModel | undefined, url: string): string => {
  const format = String((model as any)?.format ?? '').trim().toLowerCase();
  const value = String(url ?? '').trim().toLowerCase();
  if (format === 'gltf' || value.startsWith('data:model/gltf+json') || /\.gltf(?:[?#].*)?$/.test(value)) return 'model/gltf+json';
  return 'model/gltf-binary';
};

const getSceneLoaderUrl = async (url: string, model?: Asuka3DModel): Promise<string> => {
  const trimmed = String(url ?? '').trim();
  if (!/^data:/i.test(trimmed)) {
    debugBabylonProduct3D('loader-url:direct', { url: trimmed, model });
    return trimmed;
  }
  const runtimeUrl = await resolveOrCreate3DModelRuntimeUrl(trimmed, getModelMimeType(model, trimmed));
  debugBabylonProduct3D('loader-url:data-url', { originalUrl: trimmed, model, runtimeUrl, usingFallbackDataUrl: runtimeUrl === trimmed });
  return runtimeUrl || trimmed;
};

const getModelPluginExtension = (model: Asuka3DModel | undefined, url: string): '.glb' | '.gltf' | undefined => {
  const format = String((model as any)?.format ?? '').trim().toLowerCase();
  if (format === 'gltf') return '.gltf';
  if (format === 'glb') return '.glb';
  const value = String(url ?? '').trim().toLowerCase();
  if (value.startsWith('data:model/gltf+json') || /\.gltf(?:[?#].*)?$/.test(value)) return '.gltf';
  if (value.startsWith('data:model/gltf-binary') || /\.glb(?:[?#].*)?$/.test(value)) return '.glb';
  return undefined;
};

export function createAsukaBabylonRenderEngine(options: BabylonEngineOptions = {}): AsukaRenderEngine {
  const emitCameraEvent = (payload: Record<string, unknown>) => {
    try { emitAsukaEvent('engine3d.camera.changed', payload); } catch {}
    emitRenderEngineEvent({
      type: 'viewport',
      action: String(payload?.action ?? 'camera.changed'),
      viewId: String(payload?.viewId ?? '').trim() || null,
      engine: '3d',
      payload,
    });
  };
  const emitHistoryEvent = (action: string, payload: Record<string, unknown>) => {
    emitRenderEngineEvent({
      type: 'history',
      action,
      viewId: String(payload?.viewId ?? '').trim() || null,
      engine: '3d',
      payload,
    });
  };
  ensureAsuka3DPerfDebugApi();
  logAsuka3DPerf('engine', 'create', { antialias: options.antialias !== false, historyMaxSnapshots: options.history?.maxSnapshots ?? 8 });
  let canvas: HTMLCanvasElement | null = null;
  let engine: Engine | null = null;
  let scene: Scene | null = null;
  const preloadedModelAssets = new Map<string, { scene: Scene; promise: Promise<AssetContainer>; container?: AssetContainer }>();
  let camera: ArcRotateCamera | null = null;
  let resizeObserver: ResizeObserver | null = null;
  let resizeRaf1 = 0;
  let resizeRaf2 = 0;
  let resizeTimer: number | null = null;
  let currentViewId: string | null = null;
  let currentState: Asuka3DSceneState = create3DSceneState(options.scene);
  let cachedCurrentProductPartsSource: any[] | null = null;
  let cachedCurrentProductParts: any[] | null = null;
  let cachedProductPartLookup: { parts: any[]; componentRank: Map<number, any>; alias: Map<string, any>; looseAlias: Map<string, any[]> } | null = null;
  type Asuka3DHistoryTrack = { snapshots: Asuka3DSceneState[]; index: number; lastSignature?: string; assets: Asuka3DHistoryAssets };
  const historyTracksByView = new Map<string, Asuka3DHistoryTrack>();
  let isRestoringHistory = false;
  let historyMutationSuppressDepth = 0;
  const runWithoutIntermediateHistory = <T,>(callback: () => T): T => {
    historyMutationSuppressDepth += 1;
    try {
      return callback();
    } finally {
      historyMutationSuppressDepth = Math.max(0, historyMutationSuppressDepth - 1);
    }
  };
  const commitFinal3DHistorySnapshot = (source: string, id?: string | null): void => {
    // Final commits are valid only after the mutation has fully settled. Several
    // high-level operations call lower-level transform helpers internally; without
    // this guard those helpers can push an intermediate snapshot before clamp /
    // decal recreation finishes, which makes undo/redo appear random in 3D.
    if (isRestoringHistory || historyMutationSuppressDepth > 0) return;
    try { commit3DHistorySnapshot({ source, reason: source, id: id ?? undefined } as any); } catch {}
  };
  const historyMaxSnapshots = Math.max(2, Math.min(100, Number(options.history?.maxSnapshots ?? 8) || 8));
  const cameraConfigSource = ((options as any)?.camera && typeof (options as any).camera === 'object') ? (options as any).camera as Record<string, unknown> : {};
  const readCameraNumberOption = (...keys: string[]): number | null => {
    for (const key of keys) {
      const direct = Number((cameraConfigSource as any)[key]);
      if (Number.isFinite(direct)) return direct;
    }
    return null;
  };
  const readCameraBooleanOption = (...keys: string[]): boolean | null => {
    for (const key of keys) {
      if (Object.prototype.hasOwnProperty.call(cameraConfigSource, key)) {
        const direct = (cameraConfigSource as any)[key];
        if (typeof direct === 'boolean') return direct;
        if (typeof direct === 'number' && Number.isFinite(direct)) return direct !== 0;
        if (typeof direct === 'string') {
          const normalized = direct.trim().toLowerCase();
          if (['true', '1', 'yes', 'on'].includes(normalized)) return true;
          if (['false', '0', 'no', 'off'].includes(normalized)) return false;
        }
      }
    }
    return null;
  };
  const productConfigSource = ((options as any)?.products && typeof (options as any).products === 'object') ? (options as any).products as Record<string, unknown> : {};
  const readProductBooleanOption = (...keys: string[]): boolean | null => {
    for (const key of keys) {
      if (Object.prototype.hasOwnProperty.call(productConfigSource, key)) {
        const direct = (productConfigSource as any)[key];
        if (typeof direct === 'boolean') return direct;
        if (typeof direct === 'number' && Number.isFinite(direct)) return direct !== 0;
        if (typeof direct === 'string') {
          const normalized = direct.trim().toLowerCase();
          if (['true', '1', 'yes', 'on'].includes(normalized)) return true;
          if (['false', '0', 'no', 'off'].includes(normalized)) return false;
        }
      }
    }
    return null;
  };
  const degToRad = (value: number): number => (value * Math.PI) / 180;
  const radToDeg = (value: number): number => (value * 180) / Math.PI;
  const clampNumber = (value: number, min: number, max: number): number => Math.max(min, Math.min(max, value));
  const configuredCameraLimits = normalizeCameraLimits({
    minTilt: readCameraNumberOption('minTilt') ?? 2,
    maxTilt: readCameraNumberOption('maxTilt') ?? 85,
    maxTurn: readCameraNumberOption('maxTurn') ?? 180,
    turnCenter: readCameraNumberOption('turnCenter') ?? 90,
  });
  let cameraLimits: AsukaCameraLimits = normalizeCameraLimits(currentState.cameraLimits, configuredCameraLimits);
  let cameraMinBetaLimit = degToRad(90 - cameraLimits.maxTilt);
  let cameraMaxBetaLimit = degToRad(90 - cameraLimits.minTilt);
  const syncCameraBetaLimits = (): void => {
    cameraMinBetaLimit = clampNumber(degToRad(90 - cameraLimits.maxTilt), 0.02, Math.PI - 0.05);
    cameraMaxBetaLimit = clampNumber(degToRad(90 - cameraLimits.minTilt), cameraMinBetaLimit + 0.02, Math.PI - 0.02);
  };
  // Camera part-focus must stay responsive. Earlier versions evaluated every
  // candidate angle with many raycasts, which produced a visible delay before
  // the camera started moving on complex GLB products. Keep the search cone, but
  // use a cheap projected-area pre-pass and raycast only the best few candidates.
  const focusOcclusionSamples = Math.max(3, Math.min(15, Math.trunc(Number(readCameraNumberOption('focusOcclusionSamples') ?? 7) || 7)));
  const focusViewCandidateSamples = Math.max(5, Math.min(31, Math.trunc(Number(readCameraNumberOption('focusViewCandidateSamples') ?? 17) || 17)));
  const focusOcclusionTopCandidates = Math.max(1, Math.min(8, Math.trunc(Number(readCameraNumberOption('focusOcclusionTopCandidates') ?? 4) || 4)));
  const focusMultiProductTargetBlend = Math.max(0, Math.min(1, Number(readCameraNumberOption('focusMultiProductTargetBlend') ?? 0.58) || 0));
  const focusOcclusionScoreWeight = Math.max(18, Math.min(110, Number(readCameraNumberOption('focusOcclusionWeight') ?? 48) || 48));
  const focusVisibleAreaScoreWeight = Math.max(0, Math.min(80, Number(readCameraNumberOption('focusVisibleAreaWeight') ?? 18) || 18));
  const focusTargetCoverageScoreWeight = Math.max(0, Math.min(80, Number(readCameraNumberOption('focusTargetCoverageWeight') ?? 24) || 24));
  const cameraFitViewRadiusRatio = Math.max(0.25, Math.min(12, Number(readCameraNumberOption('fitViewRadiusRatio') ?? 1.35) || 1.35));
  const cameraFitOnAddElement = readCameraBooleanOption('fitOnAdd') === true;
  const cameraAddElementFitRadiusRatio = Math.max(0.25, Math.min(12, Number(readCameraNumberOption('addFitRadiusRatio') ?? cameraFitViewRadiusRatio) || cameraFitViewRadiusRatio));
  const centerProductGroup = readProductBooleanOption('centerGroup') !== false;
  const centerProductGroupOnAdd = centerProductGroup && readProductBooleanOption('centerOnAdd') !== false;
  const centerProductGroupOnTransform = centerProductGroup && readProductBooleanOption('centerOnTransform') !== false;
  const centerProductGroupInView = centerProductGroup && readProductBooleanOption('centerInView') !== false;
  const lockProductCameraTarget = centerProductGroupInView && readProductBooleanOption('lockCameraTarget') !== false;
  const centerProductGroupDuringFocus = centerProductGroupInView && readProductBooleanOption('centerDuringFocus') !== false;
  const isAssemblyModeEnabled = (): boolean => currentState.assembly?.enabled === true;
  const canEditAssemblyRuntime = (): boolean => {
    const store = useAsukaStore.getState() as any;
    const permissions = getEffectivePermissions(
      store.permissions ?? DEFAULT_PERMISSIONS,
      store.permissionsByView?.[store.activeViewId ?? ''] ?? null,
    );
    return permissions.canEditAssembly !== false;
  };
  const assertAssemblyEditPermission = (operation: string): void => {
    if (canEditAssemblyRuntime()) return;
    throw new AsukaRendererError(
      'UNSUPPORTED_OPERATION',
      'Spatial Assembly editing is disabled by the current permissions.',
      {
        engine: '3d',
        operation: `assembly.${operation}`,
        details: { permission: 'canEditAssembly' },
      },
    );
  };
  let modelMeshes: Mesh[] = [];
  let modelMeshLookupCache: { source: Mesh[]; length: number; objects: Set<Mesh>; ids: Set<string>; names: Set<string> } | null = null;
  let decorationRecords: DecorationRecord[] = [];
  const objectMeshes = new Map<string, Mesh>();
  const printAreaMeshes = new Map<string, Mesh>();
  const printAreaTextures = new Map<string, { texture: DynamicTexture; mesh: Mesh; material: any; slot: string; originalTexture: any }>();
  const printAreaImageCache = new Map<string, HTMLImageElement>();
  const printAreaImageLoadAreaIds = new Map<string, Set<string>>();
  const printAreaTextureRenderRafs = new Map<string, number>();
  const printAreaTextureRenderTimers = new Map<string, number>();
  const objectKinds = new Map<string, string>();
  const decalFragmentsByObjectId = new Map<string, Mesh[]>();
  let scheduleProjectedDecorationRefresh: ((ownerId: string, reason: string) => void) | null = null;
  let scheduleProjectedDecorationRefreshForTargets: ((targets: Mesh[], reason: string) => void) | null = null;
  const assemblyEnvironmentMeshes: Mesh[] = [];
  const assemblyEnvironmentMaterials: StandardMaterial[] = [];
  const assemblyBuildPreviewMeshes: Mesh[] = [];
  const assemblyBuildPreviewMaterials: StandardMaterial[] = [];
  let assemblyBuildSelectedSurfaceId: string | null = null;
  let assemblyBuildState: {
    input: AsukaAssemblyStartDrawingInput;
    tool: AsukaAssemblyBuildTool;
    pointerId: number | null;
    start: Vector3 | null;
    current: Vector3 | null;
    points: Vector3[];
    dragging: boolean;
    replaceOnFirstCommit: boolean;
  } | null = null;
  let assemblySurfacePointDragState: {
    floorId: string;
    pointIndex: number;
    pointerId: number | null;
    originalPoints: Array<{ x: number; z: number }>;
    currentPoints: Array<{ x: number; z: number }>;
    changed: boolean;
  } | null = null;
  let assemblySurfaceGizmoDragState: {
    surfaceId: string;
    handle: 'edge' | 'move' | 'rotate' | 'wall-start' | 'wall-end';
    edgeIndex: number | null;
    pointerId: number | null;
    originalSurface: AsukaAssemblySurface;
    currentSurface: AsukaAssemblySurface;
    startPointerWorld: Vector3;
    pivot: { x: number; z: number };
    startAngle: number;
    changed: boolean;
  } | null = null;
  let assemblyBuildDomCleanup: (() => void) | null = null;
  let assemblyBuildPreviousCursor = '';
  let assemblyBuildPreviousTouchAction = '';
  let assemblyBuildPreviousCameraButtons: number[] | null = null;
  let assemblyConstructionModeActive = false;
  let assemblyMoveToolActive = false;
  let assemblyMoveToolPreviousCursor = '';
  let assemblyMoveToolDomCleanup: (() => void) | null = null;
  let assemblyComponentDragState: {
    componentId: string;
    mode: 'move' | 'rotate';
    pointerId: number | null;
    startClientX: number;
    startClientY: number;
    startPointerWorld: Vector3;
    rotationPivot: Vector3;
    startRotationAngle: number;
    screenSpaceFallback: boolean;
    verticalMode: boolean;
    supportOnHorizontal: boolean;
    originalTransform: AsukaAssemblyTransform;
    originalBounds: AsukaAssemblyBounds;
    lastPlacementInput: AsukaAssemblyPlacementInput | null;
    lastResult: AsukaAssemblyPlacementResult | null;
    lastAcceptedPlacementInput: AsukaAssemblyPlacementInput | null;
    lastAcceptedResult: AsukaAssemblyPlacementResult | null;
    moved: boolean;
    previousCursor: string;
    source: string;
    startCameraState?: Asuka3DSceneState['camera'];
  } | null = null;

  const summarizeLayerMaterialForDebug = (material: any): Record<string, any> | null => {
    if (!material) return null;
    let texture = material?.diffuseTexture ?? material?.opacityTexture ?? null;
    if (!texture) {
      try { texture = typeof material?.getTexture === 'function' ? material.getTexture('uSampler') : null; } catch {}
    }
    if (!texture) {
      try {
        const activeTextures = typeof material?.getActiveTextures === 'function' ? material.getActiveTextures() : null;
        texture = Array.isArray(activeTextures) ? activeTextures[0] ?? null : null;
      } catch {}
    }
    let textureReady: any = undefined;
    let materialReady: any = undefined;
    try { textureReady = typeof texture?.isReady === 'function' ? texture.isReady() : undefined; } catch {}
    try { materialReady = typeof material?.isReady === 'function' ? material.isReady() : undefined; } catch {}
    return {
      name: String(material?.name ?? material?.id ?? ''),
      className: String(material?.getClassName?.() ?? material?.constructor?.name ?? ''),
      alpha: material?.alpha,
      backFaceCulling: material?.backFaceCulling,
      disableLighting: material?.disableLighting,
      renderingGroupId: material?.renderingGroupId,
      transparencyMode: (material as any)?.transparencyMode,
      zOffset: (material as any)?.zOffset,
      zOffsetUnits: (material as any)?.zOffsetUnits,
      depthFunction: (material as any)?.depthFunction,
      targetBoundProjectedSticker: (material as any)?.__asukaTargetBoundProjectedStickerMaterial === true,
      surfaceShellOffset: Number((material as any)?.__asukaSurfaceShellOffset ?? 0),
      ready: materialReady,
      texture: texture ? {
        name: String(texture?.name ?? texture?.url ?? ''),
        url: String(texture?.url ?? '').slice(0, 160),
        hasAlpha: texture?.hasAlpha,
        ready: textureReady,
        coordinatesMode: texture?.coordinatesMode,
      } : null,
    };
  };

  const summarizeLayerMeshForDebug = (mesh: any): Record<string, any> | null => {
    if (!mesh) return null;
    let vertices: any = undefined;
    let indices: any = undefined;
    let enabled: any = undefined;
    let visible: any = undefined;
    let absolutePosition: any = undefined;
    try { vertices = mesh.getTotalVertices?.(); } catch {}
    try { indices = mesh.getTotalIndices?.(); } catch {}
    try { enabled = mesh.isEnabled?.(); } catch {}
    try { visible = mesh.isVisible; } catch {}
    try { absolutePosition = fromVec3(mesh.getAbsolutePosition?.() ?? mesh.position); } catch {}
    return {
      id: String(mesh.id ?? ''),
      name: String(mesh.name ?? ''),
      kind: objectKinds.get(String(mesh.id ?? '')),
      className: String(mesh.getClassName?.() ?? mesh.constructor?.name ?? ''),
      vertices,
      indices,
      enabled,
      visible,
      visibility: Number(mesh.visibility ?? 1),
      renderOutline: mesh.renderOutline === true,
      isPickable: mesh.isPickable,
      renderingGroupId: mesh.renderingGroupId,
      position: fromVec3(mesh.position ?? Vector3.Zero()),
      absolutePosition,
      scaling: fromVec3(mesh.scaling ?? Vector3.One()),
      projectionMode: (mesh as any).__asukaProjectionMode,
      targetMeshId: (mesh as any).__asukaTargetMeshId,
      projectedStickerOverlay: (mesh as any).__asukaProjectedStickerOverlay === true,
      targetBoundProjectedSticker: (mesh as any).__asukaTargetBoundProjectedSticker === true,
      surfaceShellOffset: Number((mesh as any).__asukaSurfaceShellOffset ?? 0),
      broadMeshTargetProjector: (mesh as any).__asukaBroadMeshTargetProjector === true,
      decalSize: (mesh as any).__asukaDecalSize,
      decalPosition: (mesh as any).__asukaDecalPosition,
      surfaceNormal: (mesh as any).__asukaSurfaceNormal,
      material: summarizeLayerMaterialForDebug(mesh.material),
    };
  };

  const get3DLayerDebugSnapshot = (idInput?: string | null): Record<string, any> => {
    const wanted = String(idInput ?? '').trim();
    const objectIds = wanted ? [wanted] : Array.from(new Set([
      ...decorationRecords.map((record: any) => String(record?.id ?? '').trim()).filter(Boolean),
      ...Array.from(objectMeshes.keys()).filter((id) => id && id !== 'product'),
    ]));
    return {
      currentViewId,
      selectedObjectId,
      modelMeshes: modelMeshes.map((mesh) => ({
        id: String(mesh?.id ?? ''),
        name: String(mesh?.name ?? ''),
        enabled: (() => { try { return mesh.isEnabled?.(); } catch { return undefined; } })(),
        visible: (mesh as any)?.isVisible,
        vertices: (() => { try { return mesh.getTotalVertices?.(); } catch { return undefined; } })(),
        renderingGroupId: (mesh as any)?.renderingGroupId,
      })).slice(0, 80),
      printAreas: clonePlain(getPrintAreas().map((area: any) => ({
        id: area?.id,
        label: area?.label,
        mode: area?.mode,
        targetMeshId: area?.targetMeshId,
        metadata: area?.metadata,
        worldBounds: area?.worldBounds,
      }))),
      printAreaVisuals: Array.from(printAreaMeshes.entries()).map(([id, mesh]) => ({
        id,
        outline: (mesh as any)?.__asukaPrintAreaOutline === true,
        screenBox: getMeshScreenBox(mesh),
        mesh: summarizeLayerMeshForDebug(mesh),
      })),
      printAreaDrawing: {
        active: printAreaDrawState != null,
        targetMeshId: printAreaDrawState?.targetMeshId ?? null,
        isDragging: printAreaDrawState?.isDragging === true,
        hasStartSurface: printAreaDrawState?.startSurface != null,
        last: clonePlain(lastPrintAreaDrawDiagnostic),
      },
      printAreaAddPick: {
        active: printAreaAddPickState != null,
        candidates: getPickablePrintAreasForAdd().map((area) => String(area?.id ?? '')).filter(Boolean),
        last: clonePlain(lastPrintAreaAddPickDiagnostic),
      },
      assemblyBuild: {
        active: assemblyBuildState != null,
        tool: assemblyBuildState?.tool ?? null,
        dragging: assemblyBuildState?.dragging === true,
        draftPointCount: assemblyBuildState?.points.length ?? 0,
        start: assemblyBuildState?.start ? fromVec3(assemblyBuildState.start) : null,
        current: assemblyBuildState?.current ? fromVec3(assemblyBuildState.current) : null,
        selectedSurfaceId: assemblyBuildSelectedSurfaceId,
        surfaces: [
          ...(currentState.assembly?.plan?.floors ?? []),
          ...(currentState.assembly?.plan?.walls ?? []),
        ].map((surface: any) => ({ id: surface?.id, kind: surface?.kind })),
      },
      objects: objectIds.map((id) => {
        const mesh = objectMeshes.get(id) ?? null;
        const fragments = decalFragmentsByObjectId.get(id) ?? [];
        const record = decorationRecords.find((item: any) => String(item?.id ?? '') === id) ?? null;
        return {
          id,
          kind: objectKinds.get(id) ?? record?.type,
          record: record ? summarizeBabylonDebugValue(record) : null,
          mesh: summarizeLayerMeshForDebug(mesh),
          fragments: fragments.map((fragment) => summarizeLayerMeshForDebug(fragment)),
        };
      }),
      maps: {
        objectMeshes: Array.from(objectMeshes.keys()),
        objectKinds: Array.from(objectKinds.entries()),
        decalFragments: Array.from(decalFragmentsByObjectId.entries()).map(([id, fragments]) => [id, fragments.length]),
      },
    };
  };

  const install3DLayerDebugApi = (): void => {
    try {
      const g = globalThis as any;
      g.__ASUKA_3D_LAYER_DEBUG__ = {
        enable: () => { g.__ASUKA_3D_LAYER_DEBUG_ENABLED__ = true; try { window.localStorage?.setItem('asuka:3d-layer-debug', '1'); } catch {} return true; },
        disable: () => { g.__ASUKA_3D_LAYER_DEBUG_ENABLED__ = false; try { window.localStorage?.removeItem('asuka:3d-layer-debug'); } catch {} return true; },
        clear: () => { g.__ASUKA_3D_LAYER_LOGS__ = []; return true; },
        logs: () => Array.isArray(g.__ASUKA_3D_LAYER_LOGS__) ? g.__ASUKA_3D_LAYER_LOGS__ : [],
        dump: (id?: string) => get3DLayerDebugSnapshot(id),
        report: (id?: string) => {
          const report = { logs: Array.isArray(g.__ASUKA_3D_LAYER_LOGS__) ? g.__ASUKA_3D_LAYER_LOGS__ : [], snapshot: get3DLayerDebugSnapshot(id) };
          try { console.info('[AsukaDesigner:3DLayer] report', report); } catch {}
          return report;
        },
        copy: async (id?: string) => {
          const text = JSON.stringify((g.__ASUKA_3D_LAYER_DEBUG__ as any).report(id), null, 2);
          try { await navigator.clipboard?.writeText?.(text); return { ok: true, copied: text.length }; } catch { return text; }
        },
      };
    } catch {}
  };

  const debug3DLayer = (phase: string, payload?: Record<string, any>, level: 'info' | 'warn' | 'error' = 'info') => {
    pushBabylon3DLayerDebug(phase, payload, level);
  };

  const schedule3DLayerRenderCheck = (id: string, phase = 'post-add-render-check') => {
    try {
      setTimeout(() => {
        try {
          const snapshot = get3DLayerDebugSnapshot(id);
          const item = Array.isArray((snapshot as any).objects) ? (snapshot as any).objects[0] : null;
          debug3DLayer(phase, { id, item }, item?.mesh || item?.fragments?.length ? 'info' : 'warn');
        } catch {}
      }, 450);
      setTimeout(() => {
        try {
          const snapshot = get3DLayerDebugSnapshot(id);
          const item = Array.isArray((snapshot as any).objects) ? (snapshot as any).objects[0] : null;
          debug3DLayer(`${phase}:late`, { id, item }, item?.mesh || item?.fragments?.length ? 'info' : 'warn');
        } catch {}
      }, 1600);
    } catch {}
  };

  const materialState: NonNullable<Asuka3DSceneState['materials']> = { ...(options.scene?.materials ?? {}) };
  install3DLayerDebugApi();
  // Keep the full variation catalog out of exported 3D scene snapshots. Large
  // 3D products can contain GLB data URLs and deep part-variation trees;
  // duplicating that tree into currentState/export/history on every variation
  // change quickly exhausts the browser heap. The engine only needs the full tree
  // at runtime to resolve part variation choices, so it is stored separately and
  // never serialized with the view scene.
  type RuntimeProductCatalog = {
    id?: string;
    name?: string;
    familyId?: string;
    variations?: any[];
    parts?: any[];
    variationsRef?: any[];
    partsRef?: any[];
    labelSignature?: string;
  };
  let runtimeProductCatalog: RuntimeProductCatalog | null = null;
  const runtimeProductCatalogsByIdentity = new Map<string, RuntimeProductCatalog>();
  let runtimeProductCatalogRevision = 0;
  // Product recipes are catalog data, while only per-instance values are persisted in scenes/history.
  // Keep a lightweight runtime registry keyed by catalog id/name and by placed product id.
  const runtimeParametricCatalog = new Map<string, any>();
  const runtimeParametricElementByProductId = new Map<string, any>();
  const runtimeAssemblyCatalog = new Map<string, any>();
  const runtimeAssemblyElementByProductId = new Map<string, any>();
  type ParametricMeshBaseline = {
    position: Vector3;
    rotation: Vector3;
    rotationQuaternion: Quaternion | null;
    scaling: Vector3;
    enabled: boolean;
    localSize: Vector3;
  };
  const parametricBaselinesByProductId = new Map<string, Map<Mesh, ParametricMeshBaseline>>();
  const parametricMeasurementsByProductId = new Map<string, AsukaParametricRuntimeMeasurements>();
  type RuntimeProductTextureSlots = {
    baseColor?: { url: string; texture: Texture };
    normal?: { url: string; texture: Texture };
  };
  const materialTexturesByMaterial = new WeakMap<object, RuntimeProductTextureSlots>();
  // Keep the authored material for every imported mesh. Variations are patches
  // over that baseline, so untargeted PBR textures and normal maps survive.
  const nativeProductMaterialByMesh = new WeakMap<Mesh, any>();
  const objectUrls = new Set<string>();
  asukaHistoryDebug('engine3d.created', {
    initialViewId: currentViewId,
    initialScene: summarize3DScene(currentState),
    historyMaxSnapshots,
    note: 'Babylon 3D engine diagnostic history build is active.',
  });

  let selectedObjectId: string | null = null;
  // Materials are no longer selectable from a standalone module, but the engine
  // still exposes readonly material summaries for the 3D scene graph. Keep this
  // Internal selection state is always defined before selection helpers run.
  let selectedMaterialId: string | null = null;
  let selectedPartId: string | null = String((currentState as any)?.selectedPartId ?? '').trim() || null;
  let hoveredProductMesh: Mesh | null = null;
  let hoveredProductMeshPreviousVisual: {
    renderOutline: boolean;
    outlineWidth: number;
    outlineColor: Color3 | null;
    showBoundingBox: boolean;
    visibility: number;
  } | null = null;
  let lastPrintAreaDrawDiagnostic: Record<string, unknown> | null = null;
  let lastPrintAreaAddPickDiagnostic: Record<string, unknown> | null = null;
  let hoveredProductKey = '';
  let pendingProductHoverPick: { mesh: Mesh; rawMesh: Mesh | null; pick: any; key: string } | null = null;
  let pendingProductHoverEvent: PointerEvent | MouseEvent | null = null;
  let productHoverRaf = 0;
  let lastProductHoverHitKey = '';
  const productHoverActionManagedMeshes = new WeakSet<Mesh>();
  let productHoverActionsInstalled = false;
  const productHoverMeshTargetCache = new WeakMap<Mesh, { parts: any[]; materialId: string; materialName: string; catalogRevision: number; target: AsukaSurfacePickTarget; key: string }>();
  let productHoverPointerPickingSuspended = false;
  let productHoverPreviousSkipPointerMovePicking: boolean | null = null;
  let productHoverRestoreTimer: number | null = null;
  let productHoverSuspendedUntil = 0;
  let lastProductHoverSuspendDebugAt = 0;
  let hoverPointerMoveCount = 0;
  let hoverTransitionCount = 0;
  let hoverClearCount = 0;
  let hoverLastRollupAt = 0;
  let productCanvasPointerDown: {
    x: number;
    y: number;
    moved: boolean;
    targetKind?: 'product' | 'empty';
    target?: AsukaSurfacePickTarget | null;
  } | null = null;
  let consumedProductPointerUpClick: { at: number; x: number; y: number } | null = null;
  let lastProductPick: {
    asukaProductId?: string;
    productId?: string;
    productPartId?: string;
    productPartLabel?: string;
    productPartMeshId?: string;
    productPartMaterialId?: string;
    productPartPoint?: [number, number, number];
    productPartNormal?: [number, number, number];
    productPartFaceId?: number;
  } | null = null;
  let cameraFocusRaf = 0;
  let pointerObserver: any = null;
  let canvasPointerLeaveHandler: ((event: PointerEvent) => void) | null = null;
  let cameraViewObserver: any = null;
  let cameraPersistTimer: number | null = null;
  let lastPersistedCameraSignature = '';
  let cameraPersistenceSuppressedUntil = 0;
  // Camera target locking must stay cheap while the user rotates the 3D view.
  // Computing product-group bounds on every ArcRotateCamera view-matrix change
  // is expensive on detailed GLB products and makes orbit controls feel laggy.
  // The group center is therefore cached and only recomputed when products are
  // added/removed/transformed or when a caller explicitly asks for fresh bounds.
  let cachedProductGroupViewTarget: Vector3 | null = null;
  let productGroupViewTargetDirty = true;
  let productGroupCenterTimer: number | null = null;
  let pendingProductGroupCenterEmitKey = '';
  let pendingProductGroupCenterOnDeselect = false;
  let pendingProductGroupCenterSource = 'productGroup.center.onDeselect';
  let cameraInputDiagnosticsCleanup: (() => void) | null = null;
  let variationPerfSeq = 0;
  let lastVariationPerfAt = 0;
  let lastVariationPerfLabel = '';
  let pendingCameraInputPerf: { seq: number; type: string; at: number; x?: number; y?: number; cameraBefore?: Asuka3DSceneState['camera']; variationSeq: number; variationLabel: string; variationAgeMs: number } | null = null;
  let cameraInputPerfSeq = 0;
  let renderPerfStats: { startedAt: number; lastLogAt: number; frames: number; totalMs: number; maxMs: number; longFrames: number; afterVariationFrames: number } | null = null;
  let renderPerfLastFrameAt = 0;
  let dragState: {
    id: string;
    mode: 'mesh' | 'texture';
    pointerId: number | null;
    startClientX: number;
    startClientY: number;
    startPosition: Vector3;
    startPointerWorld: Vector3 | null;
    dragPlane: Plane | null;
    moved: boolean;
    surfaceConforming: boolean;
    lastSurfaceDragRenderAt?: number;
    lastSurfaceDragPoint?: Vector3 | null;
    lastSurfacePreviewPoint?: Vector3 | null;
    pendingSurface?: SurfacePickResult | null;
    areaId?: string;
    startTexturePoint?: { x: number; y: number } | null;
    startTextureObject?: any;
    startCameraState?: Asuka3DSceneState['camera'];
    previousCursor?: string;
  } | null = null;
  const placementController = createAsukaPlacementSessionController<AsukaObjectTransform>('3d');
  const emit3DPlacement = (action: string, status: AsukaPlacementStatus | null) => {
    emitRenderEngineEvent({
      type: 'placement',
      engine: '3d',
      action,
      id: status?.objectId ?? null,
      viewId: currentViewId,
      payload: status,
    });
    return status;
  };

  type Asuka3DDragPerfStats = {
    id: string;
    mode: 'mesh' | 'texture';
    startedAt: number;
    lastLogAt: number;
    moves: number;
    textureMoves: number;
    surfacePicks: number;
    decalRecreates: number;
    emitChangedCalls: number;
    syncPlacementCalls: number;
    totalMoveMs: number;
    maxMoveMs: number;
    surfacePickMs: number;
    decalRecreateMs: number;
    textureMoveMs: number;
    finishMs: number;
    commitMs: number;
  };
  let dragPerfStats: Asuka3DDragPerfStats | null = null;
  const perfNow = (): number => {
    try { return (typeof performance !== 'undefined' && typeof performance.now === 'function') ? performance.now() : Date.now(); } catch { return Date.now(); }
  };
  const roundPerf = (value: number): number => Math.round(Number(value || 0) * 100) / 100;
  const get3DHeapSnapshot = (): Record<string, number | null> | null => {
    try {
      const memory = (performance as any)?.memory;
      if (!memory) return null;
      const toMb = (value: any) => Number.isFinite(Number(value)) ? Math.round((Number(value) / 1024 / 1024) * 10) / 10 : null;
      return {
        usedMB: toMb(memory.usedJSHeapSize),
        totalMB: toMb(memory.totalJSHeapSize),
        limitMB: toMb(memory.jsHeapSizeLimit),
      };
    } catch {
      return null;
    }
  };
  const getCameraPerfState = () => {
    const cam = camera;
    if (!cam) return null;
    return {
      alpha: roundPerf(Number((cam as any).alpha ?? 0)),
      beta: roundPerf(Number((cam as any).beta ?? 0)),
      radius: roundPerf(Number((cam as any).radius ?? 0)),
      position: fromVec3(cam.position),
      target: fromVec3((cam as any).target ?? Vector3.Zero()),
    };
  };
  const get3DPerfCounters = () => ({
    objectMeshes: objectMeshes.size,
    decorationRecords: decorationRecords.length,
    decalObjectIds: decalFragmentsByObjectId.size,
    decalFragments: Array.from(decalFragmentsByObjectId.values()).reduce((total, fragments) => total + (Array.isArray(fragments) ? fragments.length : 0), 0),
    printAreaMeshes: printAreaMeshes.size,
    printAreaTextures: printAreaTextures.size,
    imageCache: printAreaImageCache.size,
    textureRenderRafs: printAreaTextureRenderRafs.size,
    textureRenderTimers: printAreaTextureRenderTimers.size,
    historyViews: historyTracksByView.size,
    historyCurrent: summarize3DHistoryTrackForDebug(currentViewId),
    sceneMeshes: Array.isArray(scene?.meshes) ? scene!.meshes.length : 0,
    sceneMaterials: Array.isArray(scene?.materials) ? scene!.materials.length : 0,
    sceneTextures: Array.isArray(scene?.textures) ? scene!.textures.length : 0,
    runtimeProductMaterials: Array.isArray(scene?.materials) ? scene!.materials.filter((material: any) => isRuntimeProductMaterial(material)).length : 0,
    runtimeStickerMaterials: Array.isArray(scene?.materials) ? scene!.materials.filter((material: any) => isRuntimeProjectedStickerMaterial(material)).length : 0,
    cameraFocusRaf: Boolean(cameraFocusRaf),
    heap: get3DHeapSnapshot(),
  });
  const noteCameraRawInputPerf = (type: string, event?: PointerEvent | MouseEvent | WheelEvent | TouchEvent | null) => {
    if (!isAsuka3DPerfDebugEnabled()) return;
    const now = perfNow();
    const seq = ++cameraInputPerfSeq;
    const pointerLike: any = event ?? {};
    const x = typeof pointerLike.clientX === 'number' ? Number(pointerLike.clientX) : undefined;
    const y = typeof pointerLike.clientY === 'number' ? Number(pointerLike.clientY) : undefined;
    const variationAgeMs = lastVariationPerfAt > 0 ? now - lastVariationPerfAt : -1;
    pendingCameraInputPerf = {
      seq,
      type,
      at: now,
      x,
      y,
      cameraBefore: readCameraState(),
      variationSeq: variationPerfSeq,
      variationLabel: lastVariationPerfLabel,
      variationAgeMs,
    };
  };
  const noteCameraFirstRenderAfterInput = (source: string, renderMs = 0) => {
    const pending = pendingCameraInputPerf;
    if (!pending || !isAsuka3DPerfDebugEnabled()) return;
    const now = perfNow();
    pendingCameraInputPerf = null;
    logAsuka3DPerf('camera', 'first-render-after-input', {
      seq: pending.seq,
      inputType: pending.type,
      source,
      inputToRenderMs: now - pending.at,
      renderMs,
      x: pending.x,
      y: pending.y,
      variationSeq: pending.variationSeq,
      variationLabel: pending.variationLabel,
      variationAgeMs: pending.variationAgeMs,
      cameraBefore: pending.cameraBefore,
      cameraAfter: readCameraState(),
    }, { durationMs: now - pending.at });
  };
  const noteRenderLoopPerf = (renderMs: number) => {
    if (!isAsuka3DPerfDebugEnabled()) return;
    const now = perfNow();
    const frameGapMs = renderPerfLastFrameAt > 0 ? now - renderPerfLastFrameAt : 0;
    renderPerfLastFrameAt = now;
    if (frameGapMs >= 50) {
      logAsuka3DPerf('render', 'frame-gap', {
        frameGapMs,
        renderMs,
        camera: getCameraPerfState(),
        counters: get3DPerfCounters(),
      }, { durationMs: frameGapMs, level: frameGapMs >= 120 ? 'warn' : 'info' });
    }
    if (!renderPerfStats) renderPerfStats = { startedAt: now, lastLogAt: now, frames: 0, totalMs: 0, maxMs: 0, longFrames: 0, afterVariationFrames: 0 };
    renderPerfStats.frames += 1;
    renderPerfStats.totalMs += renderMs;
    renderPerfStats.maxMs = Math.max(renderPerfStats.maxMs, renderMs);
    if (renderMs >= 24) renderPerfStats.longFrames += 1;
    const variationAgeMs = lastVariationPerfAt > 0 ? now - lastVariationPerfAt : Number.POSITIVE_INFINITY;
    if (variationAgeMs < 10000) renderPerfStats.afterVariationFrames += 1;
    if (renderMs >= 40) {
      logAsuka3DPerf('render', 'long-frame', { renderMs, camera: getCameraPerfState(), counters: get3DPerfCounters() }, { durationMs: renderMs, level: 'warn' });
    }
    if (now - renderPerfStats.lastLogAt >= 1500) {
      const stats = renderPerfStats;
      const windowMs = Math.max(1, now - stats.startedAt);
      logAsuka3DPerf('render', 'window', {
        windowMs,
        frames: stats.frames,
        fps: Math.round((stats.frames / windowMs) * 100000) / 100,
        avgRenderMs: stats.frames ? stats.totalMs / stats.frames : 0,
        maxRenderMs: stats.maxMs,
        longFrames: stats.longFrames,
        afterVariationFrames: stats.afterVariationFrames,
        camera: getCameraPerfState(),
        counters: get3DPerfCounters(),
      }, { durationMs: windowMs, console: true });
      renderPerfStats = { startedAt: now, lastLogAt: now, frames: 0, totalMs: 0, maxMs: 0, longFrames: 0, afterVariationFrames: 0 };
    }
  };
  const attachCameraInputDiagnostics = () => {
    if (!canvas || cameraInputDiagnosticsCleanup) return;
    const pointerDown = (event: PointerEvent) => noteCameraRawInputPerf('pointerdown', event);
    const pointerMove = (event: PointerEvent) => { if (pendingCameraInputPerf) return; if (event.buttons) noteCameraRawInputPerf('pointermove', event); };
    const wheel = (event: WheelEvent) => noteCameraRawInputPerf('wheel', event);
    const touchStart = (event: TouchEvent) => noteCameraRawInputPerf('touchstart', event);
    try { canvas.addEventListener('pointerdown', pointerDown, { passive: true, capture: true } as any); } catch {}
    try { canvas.addEventListener('pointermove', pointerMove, { passive: true, capture: true } as any); } catch {}
    try { canvas.addEventListener('wheel', wheel, { passive: true, capture: true } as any); } catch {}
    try { canvas.addEventListener('touchstart', touchStart, { passive: true, capture: true } as any); } catch {}
    cameraInputDiagnosticsCleanup = () => {
      try { canvas?.removeEventListener('pointerdown', pointerDown, { capture: true } as any); } catch {}
      try { canvas?.removeEventListener('pointermove', pointerMove, { capture: true } as any); } catch {}
      try { canvas?.removeEventListener('wheel', wheel, { capture: true } as any); } catch {}
      try { canvas?.removeEventListener('touchstart', touchStart, { capture: true } as any); } catch {}
    };
  };
  const startDragPerf = (id: string, mode: 'mesh' | 'texture') => {
    if (!false) return;
    const now = perfNow();
    dragPerfStats = {
      id,
      mode,
      startedAt: now,
      lastLogAt: now,
      moves: 0,
      textureMoves: 0,
      surfacePicks: 0,
      decalRecreates: 0,
      emitChangedCalls: 0,
      syncPlacementCalls: 0,
      totalMoveMs: 0,
      maxMoveMs: 0,
      surfacePickMs: 0,
      decalRecreateMs: 0,
      textureMoveMs: 0,
      finishMs: 0,
      commitMs: 0,
    };
  };
  const noteDragMovePerf = (elapsedMs: number, extra: Partial<Asuka3DDragPerfStats> = {}) => {
    const stats = dragPerfStats;
    if (!stats || !false) return;
    stats.moves += 1;
    stats.totalMoveMs += elapsedMs;
    stats.maxMoveMs = Math.max(stats.maxMoveMs, elapsedMs);
    stats.textureMoves += Number(extra.textureMoves ?? 0) || 0;
    stats.surfacePicks += Number(extra.surfacePicks ?? 0) || 0;
    stats.decalRecreates += Number(extra.decalRecreates ?? 0) || 0;
    stats.textureMoveMs += Number(extra.textureMoveMs ?? 0) || 0;
    stats.surfacePickMs += Number(extra.surfacePickMs ?? 0) || 0;
    stats.decalRecreateMs += Number(extra.decalRecreateMs ?? 0) || 0;
    const now = perfNow();
    if (elapsedMs >= 12 || now - stats.lastLogAt >= 750) {
      stats.lastLogAt = now;
    }
  };
  const finishDragPerf = (_reason: string, _extra: Record<string, unknown> = {}) => {
    const stats = dragPerfStats;
    if (!stats || !false) return;
    dragPerfStats = null;
  };

  type SurfacePickResult = { targetMesh: Mesh; position: Vector3; normal: Vector3; uv?: { x: number; y: number }; faceId?: number | null; rawPosition?: Vector3 | null; surfaceRight?: Vector3 | null; surfaceUp?: Vector3 | null };
  let printAreaDrawState: {
    id?: string;
    label?: string;
    targetMeshId?: string;
    physicalSize?: AsukaPhysicalSize;
    metadata?: Record<string, unknown>;
    onCreate?: (area: Asuka3DPrintArea) => void;
    onCancel?: () => void;
    pointerId: number | null;
    startClientX: number;
    startClientY: number;
    startSurface: SurfacePickResult | null;
    lastSurface: SurfacePickResult | null;
    lastSurfaceClientX: number;
    lastSurfaceClientY: number;
    previewMesh: Mesh | null;
    hoverMesh: Mesh | null;
    isDragging: boolean;
  } | null = null;
  let printAreaDrawDomCleanup: (() => void) | null = null;
  let printAreaDrawPreviousCursor = '';
  let printAreaDrawPreviousTouchAction = '';
  let printAreaAddPickState: {
    resolve: (id: string | null) => void;
    previousCursor: string;
    cleanup: () => void;
  } | null = null;
  let printSurfaceFacePickState: {
    targetMeshId?: string;
    primaryFace?: string;
    faces: string[];
    selectedTriangles?: FaceNetTriangleRef[];
    primaryTriangleIndex?: number;
    onChange?: (selection: { pickedFace: string; primaryFace: string; faces: string[]; meshId?: string; point?: [number, number, number]; normal?: [number, number, number]; faceId?: number | null; pickedTriangleIndex?: number | null; primaryTriangleIndex?: number | null; selectedTriangles?: FaceNetTriangleRef[] }) => void;
    onCancel?: () => void;
  } | null = null;
  let printSurfaceFacePickPreviousCursor = '';
  let printSurfaceFacePickPreviousTouchAction = '';
  let lastSurfaceInteraction: AsukaSurfaceInteractionDebug = {
    engine: '3d',
    selectedObjectId: null,
    activePrintAreaId: null,
    lastPick: null,
    occlusion: {
      depthTest: true,
      depthWrite: false,
      renderingGroupId: null,
      textureSlotsOpaque: true,
      overlaysMatchProductRenderingGroup: true,
    },
  };

  const AUTO_PRODUCT_SURFACE_AREA_ID = '__asuka-auto-product-surface';
  const PRINT_SURFACE_MESH_PREFIX = 'asuka_surface_';
  let listDetectedPrintSurfaceAreas = (): Asuka3DPrintArea[] => [];

  const requireScene = (): Scene => {
    if (!scene) throw new Error('Asuka 3D engine is not mounted.');
    return scene;
  };

  const getModelLoaderDescriptor = async (model?: Asuka3DModel): Promise<{
    scene: Scene;
    loaderUrl: string;
    rootUrl: string;
    sceneFilename: string;
    pluginExtension: '.glb' | '.gltf' | undefined;
    key: string;
  } | null> => {
    const sc = requireScene();
    const url = getResolvedModelUrl(model);
    if (!url) return null;
    const loaderUrl = await getSceneLoaderUrl(url, model);
    const { rootUrl, sceneFilename } = splitSceneUrl(loaderUrl);
    const pluginExtension = getModelPluginExtension(model, url);
    return { scene: sc, loaderUrl, rootUrl, sceneFilename, pluginExtension, key: `${pluginExtension ?? ''}:${loaderUrl}` };
  };

  const preloadModelAsset = async (input?: { model?: Asuka3DModel }): Promise<boolean> => {
    const endSpan = beginAsuka3DPerfSpan('model', 'preload', { model: input?.model });
    const descriptorStart = perfNow();
    const descriptor = await getModelLoaderDescriptor(input?.model);
    const descriptorMs = perfNow() - descriptorStart;
    if (!descriptor) { endSpan({ ok: false, reason: 'no-descriptor', descriptorMs }, { level: 'warn' }); return false; }
    const existing = preloadedModelAssets.get(descriptor.key);
    if (existing && existing.scene === descriptor.scene) {
      const waitStart = perfNow();
      try {
        const asset = await existing.promise;
        endSpan({ ok: true, cache: 'hit', descriptorMs, waitMs: perfNow() - waitStart, meshes: asset.meshes?.length ?? null, materials: asset.materials?.length ?? null });
        return true;
      } catch (error) {
        endSpan({ ok: false, cache: 'hit-error', descriptorMs, waitMs: perfNow() - waitStart, error }, { level: 'error' });
        return false;
      }
    }
    if (existing) {
      try { existing.container?.dispose(); } catch {}
      preloadedModelAssets.delete(descriptor.key);
    }
    const entry: { scene: Scene; promise: Promise<AssetContainer>; container?: AssetContainer } = {
      scene: descriptor.scene,
      promise: Promise.resolve(null as unknown as AssetContainer),
    };
    const loaderStarted = perfNow();
    entry.promise = ensureGltfLoader().then(() => SceneLoader.LoadAssetContainerAsync(
      descriptor.rootUrl,
      descriptor.sceneFilename,
      descriptor.scene,
      undefined,
      descriptor.pluginExtension,
    )).then((assetContainer) => {
      entry.container = assetContainer;
      logAsuka3DPerf('model', 'preload-loader-resolved', {
        loaderMs: perfNow() - loaderStarted,
        meshes: assetContainer.meshes?.length ?? 0,
        materials: assetContainer.materials?.length ?? 0,
        textures: assetContainer.textures?.length ?? 0,
        animationGroups: assetContainer.animationGroups?.length ?? 0,
      });
      return assetContainer;
    }).catch((error) => {
      if (preloadedModelAssets.get(descriptor.key) === entry) preloadedModelAssets.delete(descriptor.key);
      logAsuka3DPerf('model', 'preload-loader-error', { loaderMs: perfNow() - loaderStarted, error }, { level: 'error' });
      throw error;
    });
    preloadedModelAssets.set(descriptor.key, entry);
    try {
      const asset = await entry.promise;
      endSpan({ ok: true, cache: 'miss', descriptorMs, loaderMs: perfNow() - loaderStarted, meshes: asset.meshes?.length ?? null });
      return true;
    } catch (error) {
      endSpan({ ok: false, cache: 'miss-error', descriptorMs, loaderMs: perfNow() - loaderStarted, error }, { level: 'error' });
      return false;
    }
  };

  const instantiatePreloadedModelMeshes = async (model?: Asuka3DModel): Promise<Mesh[] | null> => {
    const endSpan = beginAsuka3DPerfSpan('model', 'instantiate-preloaded', { model });
    const descriptor = await getModelLoaderDescriptor(model);
    if (!descriptor) { endSpan({ ok: false, reason: 'no-descriptor' }); return null; }
    const entry = preloadedModelAssets.get(descriptor.key);
    if (!entry || entry.scene !== descriptor.scene) { endSpan({ ok: false, reason: 'cache-miss' }); return null; }
    let assetContainer: AssetContainer;
    const waitStart = perfNow();
    try { assetContainer = await entry.promise; } catch (error) { endSpan({ ok: false, reason: 'preload-rejected', waitMs: perfNow() - waitStart, error }, { level: 'error' }); return null; }
    const before = new Set(descriptor.scene.meshes);
    const instantiateStart = perfNow();
    try { assetContainer.instantiateModelsToScene(undefined, true, { doNotInstantiate: true }); } catch (error) { endSpan({ ok: false, reason: 'instantiate-error', waitMs: perfNow() - waitStart, instantiateMs: perfNow() - instantiateStart, error }, { level: 'error' }); return null; }
    const meshes = descriptor.scene.meshes.filter((mesh: any): mesh is Mesh => mesh instanceof Mesh && !before.has(mesh));
    endSpan({ ok: true, waitMs: waitStart ? instantiateStart - waitStart : 0, instantiateMs: perfNow() - instantiateStart, meshCount: meshes.length });
    return meshes;
  };

  const disposePreloadedModelAssets = (): void => {
    for (const entry of preloadedModelAssets.values()) {
      try { entry.container?.dispose(); } catch {}
    }
    preloadedModelAssets.clear();
  };

  const requireCamera = (): ArcRotateCamera => {
    if (!camera) throw new Error('Asuka 3D camera is not mounted.');
    return camera;
  };

  const resizeEngineNow = () => {
    try { engine?.resize(); } catch {}
    try { scene?.render(); } catch {}
  };

  const cancelScheduledEngineResize = () => {
    if (resizeRaf1) {
      try { cancelAnimationFrame(resizeRaf1); } catch {}
      resizeRaf1 = 0;
    }
    if (resizeRaf2) {
      try { cancelAnimationFrame(resizeRaf2); } catch {}
      resizeRaf2 = 0;
    }
    if (resizeTimer != null) {
      try { window.clearTimeout(resizeTimer); } catch {}
      resizeTimer = null;
    }
  };

  const normalizeAngleNear = (angle: number, center: number): number => {
    const full = Math.PI * 2;
    let next = angle;
    while (next - center > Math.PI) next -= full;
    while (next - center < -Math.PI) next += full;
    return next;
  };

  const applyCameraAngleLimitsToCamera = (cam: ArcRotateCamera | null = camera): void => {
    if (!cam) return;
    syncCameraBetaLimits();
    try { cam.lowerBetaLimit = cameraMinBetaLimit; } catch {}
    try { cam.upperBetaLimit = cameraMaxBetaLimit; } catch {}
    try {
      if (Number.isFinite(Number(cam.beta))) cam.beta = clampNumber(Number(cam.beta), cameraMinBetaLimit, cameraMaxBetaLimit);
    } catch {}
    const center = degToRad(cameraLimits.turnCenter);
    const maxTurn = degToRad(cameraLimits.maxTurn);
    try {
      if (cameraLimits.maxTurn >= 179.999) {
        cam.lowerAlphaLimit = null;
        cam.upperAlphaLimit = null;
      } else {
        const alpha = normalizeAngleNear(Number(cam.alpha) || center, center);
        cam.lowerAlphaLimit = center - maxTurn;
        cam.upperAlphaLimit = center + maxTurn;
        cam.alpha = clampNumber(alpha, center - maxTurn, center + maxTurn);
      }
    } catch {}
  };

  const clampCameraPositionToBetaLimits = (target: Vector3, position: Vector3): Vector3 => {
    const offset = position.subtract(target);
    const radius = offset.length();
    if (!Number.isFinite(radius) || radius <= 0.0001) return position;
    const rawBeta = Math.acos(clampNumber(offset.y / radius, -1, 1));
    const beta = clampNumber(rawBeta, cameraMinBetaLimit, cameraMaxBetaLimit);
    if (Math.abs(beta - rawBeta) < 0.00001) return position;
    let horizontal = new Vector3(offset.x, 0, offset.z);
    if (horizontal.lengthSquared() < 0.000001) {
      const alpha = Number((camera as any)?.alpha);
      horizontal = Number.isFinite(alpha) ? new Vector3(Math.cos(alpha), 0, Math.sin(alpha)) : new Vector3(0, 0, 1);
    }
    horizontal.normalize();
    const horizontalDistance = Math.sin(beta) * radius;
    return target.add(horizontal.scale(horizontalDistance)).add(new Vector3(0, Math.cos(beta) * radius, 0));
  };

  const scheduleEngineResize = () => {
    resizeEngineNow();
    if (typeof requestAnimationFrame === 'function') {
      if (resizeRaf1) {
        try { cancelAnimationFrame(resizeRaf1); } catch {}
      }
      resizeRaf1 = requestAnimationFrame(() => {
        resizeRaf1 = 0;
        resizeEngineNow();
        if (resizeRaf2) {
          try { cancelAnimationFrame(resizeRaf2); } catch {}
        }
        resizeRaf2 = requestAnimationFrame(() => {
          resizeRaf2 = 0;
          resizeEngineNow();
        });
      });
    }
    if (typeof window !== 'undefined') {
      if (resizeTimer != null) {
        try { window.clearTimeout(resizeTimer); } catch {}
      }
      resizeTimer = window.setTimeout(() => {
        resizeTimer = null;
        resizeEngineNow();
      }, 80);
    }
  };

  const ensureBaseScene = () => {
    const sc = requireScene();
    if (!camera) {
      camera = new ArcRotateCamera('asuka-camera', Math.PI / 2, Math.PI / 2.35, 4.5, Vector3.Zero(), sc);
      camera.minZ = 0.01;
      camera.wheelPrecision = 45;
      camera.pinchPrecision = 90;
      applyCameraAngleLimitsToCamera(camera);
      applyProductCameraInputLock(camera);
      camera.attachControl(canvas, false);
      applyProductCameraInputLock(camera);
      try {
        cameraViewObserver = (camera as any).onViewMatrixChangedObservable?.add?.(() => {
          noteCameraFirstRenderAfterInput('camera.viewMatrix');
          // Keep the camera target lock out of the hot orbit path. Panning is
          // disabled by applyProductCameraInputLock(), and product/group
          // recenter commits call centerCameraTargetOnProductGroup() once.
          // Re-locking the target from every view-matrix change was expensive on
          // scenes with multiple Product 3D instances and made rotation laggy.
          if (isArcRotateCameraInertiaActive() || productCanvasPointerDown?.moved === true) {
            suspendProductHoverForCameraMotion('camera.viewMatrix', 140);
          }
          scheduleCameraStatePersist('camera.viewMatrix');
        });
      } catch {}
      debugBabylonProduct3D('camera:target-lock-hotpath-disabled', {
        lockProductCameraTarget,
        centerProductGroupInView,
        reason: 'Target is centered only on add/deselect/focus commits, not on every orbit frame.',
      });
      attachCameraInputDiagnostics();
    }
    if (!sc.lights.length) {
      const light = new HemisphericLight('asuka-light', new Vector3(0.2, 1, 0.35), sc);
      light.intensity = 0.92;
    }
  };

  const isRuntimeProjectedStickerMaterial = (material: any): boolean => {
    if (!material) return false;
    if ((material as any).__asukaRuntimeProjectedStickerMaterial === true) return true;
    const name = String((material as any).name ?? (material as any).id ?? '').trim();
    return /-projected-sticker-(shader|control-material)$/i.test(name);
  };

  const disposeRuntimeProjectedStickerMaterial = (material: any): void => {
    if (!isRuntimeProjectedStickerMaterial(material)) return;
    try { material.dispose(false, false); } catch { try { material.dispose(); } catch {} }
  };

  const disposeDecalFragments = (ownerIdInput: string, preserveMaterial = true) => {
    const ownerId = String(ownerIdInput ?? '').trim();
    if (!ownerId) return;
    const fragments = decalFragmentsByObjectId.get(ownerId) ?? [];
    const runtimeMaterials = new Set<any>();
    for (const fragment of fragments) {
      const material = (fragment as any)?.material ?? null;
      const runtimeMaterial = isRuntimeProjectedStickerMaterial(material);
      if (runtimeMaterial) runtimeMaterials.add(material);
      try { if (preserveMaterial || runtimeMaterial) (fragment as any).material = null; } catch {}
      try { fragment.dispose(false, runtimeMaterial ? false : !preserveMaterial); } catch {}
    }
    for (const material of runtimeMaterials) disposeRuntimeProjectedStickerMaterial(material);
    decalFragmentsByObjectId.delete(ownerId);
  };

  const disposeSpecificDecalFragments = (fragmentsInput: Array<Mesh | null | undefined>, preserveMaterial = true) => {
    const fragments = Array.isArray(fragmentsInput) ? fragmentsInput : [];
    const runtimeMaterials = new Set<any>();
    for (const fragment of fragments) {
      if (!fragment || fragment.isDisposed?.()) continue;
      const material = (fragment as any)?.material ?? null;
      const runtimeMaterial = isRuntimeProjectedStickerMaterial(material);
      if (runtimeMaterial) runtimeMaterials.add(material);
      try { if (preserveMaterial || runtimeMaterial) (fragment as any).material = null; } catch {}
      try { fragment.dispose(false, runtimeMaterial ? false : !preserveMaterial); } catch {}
    }
    for (const material of runtimeMaterials) disposeRuntimeProjectedStickerMaterial(material);
  };

  const getRenderableVertexCount = (mesh?: Mesh | null): number => {
    if (!mesh || mesh.isDisposed?.()) return 0;
    try {
      const total = Number(mesh.getTotalVertices?.() ?? 0);
      return Number.isFinite(total) && total > 0 ? total : 0;
    } catch { return 0; }
  };

  const getDecalRenderableVertexCount = (primary?: Mesh | null, fragmentsInput?: Array<Mesh | null | undefined> | null): number => {
    const fragments = Array.isArray(fragmentsInput) ? fragmentsInput : [];
    const seen = new Set<Mesh>();
    let total = 0;
    if (primary && !primary.isDisposed?.()) {
      seen.add(primary);
      total += getRenderableVertexCount(primary);
    }
    for (const fragment of fragments) {
      if (!fragment || fragment.isDisposed?.() || seen.has(fragment)) continue;
      seen.add(fragment);
      total += getRenderableVertexCount(fragment);
    }
    return total;
  };

  const setDecorationFragmentMetadata = (ownerId: string, fragment: Mesh) => {
    (fragment as any).__asukaDecorationProxyFor = ownerId;
    (fragment as any).__asukaDecalFragment = true;
    fragment.isPickable = true;
  };

  const setDecalFragmentsSelectionState = (ownerId: string, selected: boolean) => {
    for (const fragment of decalFragmentsByObjectId.get(ownerId) ?? []) {
      try {
        if ((fragment as any).__asukaProjectedStickerOverlay === true) {
          fragment.renderOutline = false;
          fragment.outlineWidth = 0;
          (fragment as any).showBoundingBox = false;
          continue;
        }
        fragment.renderOutline = selected;
        fragment.outlineColor = color3FromInput('#2563eb');
        fragment.outlineWidth = selected ? 0.035 : 0;
        (fragment as any).showBoundingBox = selected && (fragment as any).__asukaExactFaceNetPrintSurface3D !== true;
      } catch {}
    }
  };

  const compactVariationPath = (input: any): Array<{ id?: string; name?: string }> => (Array.isArray(input) ? input : [])
    .slice(0, 32)
    .map((node: any) => ({
      id: String(node?.id ?? '').trim() || undefined,
      name: String(node?.name ?? node?.label ?? '').trim() || undefined,
    }))
    .filter((node: any) => !!node.id || !!node.name);

  const compactProductTransform = (input: any): Record<string, unknown> | undefined => {
    const source = input && typeof input === 'object' ? input : null;
    if (!source) return undefined;
    const raw = ((source as any).transform && typeof (source as any).transform === 'object') ? (source as any).transform : source;
    const rawPosition = ((raw as any).position && typeof (raw as any).position === 'object') ? (raw as any).position : null;
    const rawRotation = ((raw as any).rotation && typeof (raw as any).rotation === 'object') ? (raw as any).rotation : null;
    const rawScale = ((raw as any).scale && typeof (raw as any).scale === 'object') ? (raw as any).scale : null;
    const readVec = (value: any, defaults: { x: number; y: number; z: number }) => {
      const x = Number(value?.x ?? defaults.x);
      const y = Number(value?.y ?? defaults.y);
      const z = Number(value?.z ?? defaults.z);
      return {
        x: Number.isFinite(x) ? x : defaults.x,
        y: Number.isFinite(y) ? y : defaults.y,
        z: Number.isFinite(z) ? z : defaults.z,
      };
    };
    const position = rawPosition ? readVec(rawPosition, { x: 0, y: 0, z: 0 }) : undefined;
    const rotation = rawRotation ? readVec(rawRotation, { x: 0, y: 0, z: 0 }) : undefined;
    const scale = rawScale ? readVec(rawScale, { x: 1, y: 1, z: 1 }) : undefined;
    const hasPosition = !!position && (Math.abs(position.x) > 0.000001 || Math.abs(position.y) > 0.000001 || Math.abs(position.z) > 0.000001);
    const hasRotation = !!rotation && (Math.abs(rotation.x) > 0.000001 || Math.abs(rotation.y) > 0.000001 || Math.abs(rotation.z) > 0.000001);
    const hasScale = !!scale && (Math.abs(scale.x - 1) > 0.000001 || Math.abs(scale.y - 1) > 0.000001 || Math.abs(scale.z - 1) > 0.000001);
    if (!hasPosition && !hasRotation && !hasScale) return undefined;
    return {
      ...(hasPosition ? { position } : {}),
      ...(hasRotation ? { rotation } : {}),
      ...(hasScale ? { scale } : {}),
    };
  };

  const compactProduct = (input: any, index = 0, options: { history?: boolean } = {}): Asuka3DProduct | undefined => {
    if (!input || typeof input !== 'object') return undefined;
    const id = String((input as any).id ?? `product-${index + 1}`).trim() || `product-${index + 1}`;
    const catalogId = String((input as any).catalogId ?? '').trim() || undefined;
    const name = String((input as any).name ?? catalogId ?? id).trim() || id;
    const model = (input as any).model && typeof (input as any).model === 'object'
      ? (options.history === true ? compact3DModelForHistory((input as any).model) : compact3DModelForState((input as any).model))
      : undefined;
    const parts = compact3DPartsForScene((input as any).parts ?? []);
    const meshIds = split3DList((input as any).meshIds);
    const variationPath = compactVariationPath((input as any).variationPath);
    const partVariationIds = ((input as any).partVariationIds && typeof (input as any).partVariationIds === 'object')
      ? { ...((input as any).partVariationIds as Record<string, string>) }
      : undefined;
    const partVariationPrices = ((input as any).partVariationPrices && typeof (input as any).partVariationPrices === 'object')
      ? Object.fromEntries(Object.entries((input as any).partVariationPrices)
        .map(([key, value]) => [String(key), normalizePriceInput(value)])
        .filter((entry): entry is [string, number] => Boolean(entry[0]) && entry[1] !== undefined))
      : undefined;
    const price = normalizePriceInput((input as any).price);
    const basePrice = normalizePriceInput((input as any).basePrice);
    const variationPrice = normalizePriceInput((input as any).variationPrice);
    const rawParametricValues = ((input as any).parametric?.values && typeof (input as any).parametric.values === 'object')
      ? (input as any).parametric.values
      : {};
    const parametricValues: Record<string, AsukaParametricValue> = {};
    for (const [key, value] of Object.entries(rawParametricValues)) {
      const normalizedKey = String(key ?? '').trim();
      if (!normalizedKey) continue;
      if (value == null || typeof value === 'string' || typeof value === 'boolean' || (typeof value === 'number' && Number.isFinite(value))) {
        parametricValues[normalizedKey] = value as AsukaParametricValue;
      }
    }
    const transform = compactProductTransform(input);
    const metadata = compact3DMetadataForState((input as any).metadata);
    return {
      id,
      ...(catalogId ? { catalogId } : {}),
      name,
      ...(model ? { model } : {}),
      ...(parts.length ? { parts: parts as Asuka3DPart[] } : {}),
      ...(String((input as any).variationId ?? '').trim() ? { variationId: String((input as any).variationId).trim() } : {}),
      ...(String((input as any).variationName ?? '').trim() ? { variationName: String((input as any).variationName).trim() } : {}),
      ...(variationPath.length ? { variationPath } : {}),
      ...(String((input as any).variationPathLabel ?? '').trim() ? { variationPathLabel: String((input as any).variationPathLabel).trim() } : {}),
      ...(partVariationIds && Object.keys(partVariationIds as any).length ? { partVariationIds } : {}),
      ...(partVariationPrices && Object.keys(partVariationPrices).length ? { partVariationPrices } : {}),
      ...(price !== undefined ? { price } : {}),
      ...(basePrice !== undefined ? { basePrice } : {}),
      ...(variationPrice !== undefined ? { variationPrice } : {}),
      ...(Object.keys(parametricValues).length ? { parametric: { values: parametricValues } } : {}),
      ...(transform ? { transform: transform as any } : {}),
      ...(meshIds.length ? { meshIds } : {}),
      ...(metadata ? { metadata } : {}),
    } as Asuka3DProduct;
  };

  const compactProducts = (input: any, options: { history?: boolean } = {}): Asuka3DProduct[] => (Array.isArray(input) ? input : [])
    .map((item, index) => compactProduct(item, index, options))
    .filter((item): item is Asuka3DProduct => Boolean(item));

  const compactProductForUi = (input: any): Record<string, unknown> | null => {
    if (!input || typeof input !== 'object') return null;
    const id = String(input.id ?? '').trim();
    if (!id) return null;
    const catalogId = String(input.catalogId ?? '').trim();
    const name = String(input.name ?? catalogId ?? id).trim() || id;
    const variationId = String(input.variationId ?? '').trim();
    const variationName = String(input.variationName ?? '').trim();
    const variationPathLabel = String(input.variationPathLabel ?? '').trim();
    return {
      id, ...(catalogId ? { catalogId } : {}), name,
      ...(variationId ? { variationId } : {}),
      ...(variationName ? { variationName } : {}),
      ...(variationPathLabel ? { variationPathLabel } : {}),
      ...(input.partVariationIds && typeof input.partVariationIds === 'object' ? { partVariationIds: { ...input.partVariationIds } } : {}),
      ...(input.partVariationPrices && typeof input.partVariationPrices === 'object' ? { partVariationPrices: { ...input.partVariationPrices } } : {}),
      ...(normalizePriceInput(input.price) !== undefined ? { price: normalizePriceInput(input.price) } : {}),
      ...(normalizePriceInput(input.basePrice) !== undefined ? { basePrice: normalizePriceInput(input.basePrice) } : {}),
      ...(normalizePriceInput(input.variationPrice) !== undefined ? { variationPrice: normalizePriceInput(input.variationPrice) } : {}),
      ...(input.parametric?.values && typeof input.parametric.values === 'object' ? { parametric: { values: { ...input.parametric.values } } } : {}),
      ...(Array.isArray(input.meshIds) ? { meshIds: [...input.meshIds] } : {}),
      hasModel: Boolean(String(input?.model?.url ?? '').trim()),
    };
  };

  const getProductsFromState = (stateInput: any): any[] => compactProducts(stateInput?.products);

  const getProductId = (product: any): string => String(product?.id ?? '').trim();

  const hydrateHistoryProductsFromRuntime = (stateInput: Asuka3DSceneState, runtimeStateInput: Asuka3DSceneState): Asuka3DSceneState =>
    hydrate3DHistorySceneFromRuntime(stateInput, runtimeStateInput);

  const getSelectedProductId = (): string => {
    const selected = String(selectedObjectId ?? '').trim();
    if (selected.startsWith('product:')) return selected.slice('product:'.length).trim();
    const picked = String((lastProductPick as any)?.asukaProductId ?? (lastProductPick as any)?.productId ?? '').trim();
    if (picked) return picked;
    const stateId = String((currentState as any)?.selectedProductId ?? '').trim();
    if (stateId) return stateId;
    const products = getProductsFromState(currentState);
    return getProductId(products[products.length - 1] ?? null);
  };

  const getKnownProductIds = (): string[] => {
    const ids = new Set<string>();
    for (const instance of getProductsFromState(currentState)) {
      const id = getProductId(instance);
      if (id) ids.add(id);
    }
    // Runtime meshes are the ground truth after imports, duplicated elements and
    // history restores. Keep them in the group-center set so centering never
    // silently uses only the currently active Product 3D instance.
    for (const mesh of getRenderableModelMeshes(modelMeshes)) {
      const metadata: any = (mesh as any)?.metadata ?? {};
      const id = String(metadata?.asukaProductId ?? metadata?.productId ?? '').trim();
      if (id) ids.add(id);
    }
    return Array.from(ids);
  };

  const meshBelongsToProductInstance = (mesh: Mesh | null | undefined, instanceIdInput?: string | null): boolean => {
    if (!mesh) return false;
    const instanceId = String(instanceIdInput ?? '').trim();
    if (!instanceId) return true;
    const metadata: any = (mesh as any).metadata ?? {};
    const own = String(metadata?.asukaProductId ?? metadata?.productId ?? '').trim();
    return own === instanceId;
  };

  const getProductInstanceMeshes = (instanceIdInput?: string | null): Mesh[] => {
    const instanceId = String(instanceIdInput ?? '').trim();
    const renderable = getRenderableModelMeshes(modelMeshes).filter((mesh) => meshBelongsToProductInstance(mesh, instanceId));
    if (renderable.length || instanceId) return renderable;
    return getRenderableModelMeshes(modelMeshes);
  };

  const getProductById = (instanceIdInput?: string | null): any | null => {
    const instanceId = String(instanceIdInput ?? '').trim();
    if (!instanceId) return null;
    return getProductsFromState(currentState).find((instance: any) => getProductId(instance) === instanceId) ?? null;
  };


  const flattenCatalogElements = (itemsInput: any[]): any[] => {
    const out: any[] = [];
    const walk = (items: any[]) => {
      for (const item of Array.isArray(items) ? items : []) {
        if (!item || typeof item !== 'object') continue;
        out.push(item);
        walk(Array.isArray(item.children) ? item.children : []);
      }
    };
    walk(itemsInput);
    return out;
  };

  const syncRuntimeParametricCatalog = (elementsInput: any[]): void => {
    runtimeParametricCatalog.clear();
    for (const element of flattenCatalogElements(elementsInput)) {
      if (!element?.parametric || element?.parametric?.schemaVersion !== 1) continue;
      for (const key of [element?.id, element?.name, element?.familyId]) {
        const normalized = String(key ?? '').trim();
        if (normalized) runtimeParametricCatalog.set(normalized, element);
      }
    }
    for (const product of getProductsFromState(currentState)) {
      const productId = getProductId(product);
      const element = runtimeParametricCatalog.get(String(product?.catalogId ?? '').trim())
        ?? runtimeParametricCatalog.get(String(product?.name ?? '').trim())
        ?? null;
      if (productId && element) runtimeParametricElementByProductId.set(productId, element);
    }
  };

  const syncRuntimeAssemblyCatalog = (elementsInput: any[]): void => {
    runtimeAssemblyCatalog.clear();
    runtimeAssemblyElementByProductId.clear();
    for (const element of flattenCatalogElements(elementsInput)) {
      if (!element?.assembly || element?.assembly?.schemaVersion !== 1) continue;
      for (const key of [element?.id, element?.name, element?.familyId]) {
        const normalized = String(key ?? '').trim();
        if (normalized) runtimeAssemblyCatalog.set(normalized, element);
      }
    }
    for (const product of getProductsFromState(currentState)) {
      const productId = getProductId(product);
      const element = runtimeAssemblyCatalog.get(String(product?.catalogId ?? '').trim())
        ?? runtimeAssemblyCatalog.get(String(product?.name ?? '').trim())
        ?? null;
      if (productId && element) runtimeAssemblyElementByProductId.set(productId, element);
    }
  };

  const resolveAssemblyElementForProduct = (productInput: any): any | null => {
    const product = productInput && typeof productInput === 'object' ? productInput : null;
    const productId = getProductId(product);
    return (productId ? runtimeAssemblyElementByProductId.get(productId) : null)
      ?? runtimeAssemblyCatalog.get(String(product?.catalogId ?? '').trim())
      ?? runtimeAssemblyCatalog.get(String(product?.name ?? '').trim())
      ?? null;
  };

  const getAssemblyDefinitionForProduct = (productInput: any): AsukaAssemblyComponentDefinition | null => {
    const definition = resolveAssemblyElementForProduct(productInput)?.assembly;
    return definition?.schemaVersion === 1
      ? normalizeAssemblyComponentDefinition(definition)
      : null;
  };

  const resolveParametricElementForProduct = (productInput: any): any | null => {
    const product = productInput && typeof productInput === 'object' ? productInput : null;
    const productId = getProductId(product);
    return (productId ? runtimeParametricElementByProductId.get(productId) : null)
      ?? runtimeParametricCatalog.get(String(product?.catalogId ?? '').trim())
      ?? runtimeParametricCatalog.get(String(product?.name ?? '').trim())
      ?? null;
  };

  const getParametricDefinitionForProduct = (productInput: any): AsukaParametricDefinition | null => {
    const definition = resolveParametricElementForProduct(productInput)?.parametric;
    return definition && definition.schemaVersion === 1 ? definition as AsukaParametricDefinition : null;
  };

  const meshMatchesParametricSelector = (mesh: Mesh, selectorInput: unknown): boolean => {
    const selector = String(selectorInput ?? '').trim().toLowerCase();
    if (!selector) return false;
    const metadata: any = (mesh as any)?.metadata ?? {};
    const candidates = [
      mesh.id,
      mesh.name,
      metadata?.asukaOriginalMeshId,
      metadata?.asukaOriginalMeshName,
      metadata?.asukaOriginalPartId,
      metadata?.partId,
      metadata?.productPartId,
    ].map((value) => String(value ?? '').trim().toLowerCase()).filter(Boolean);
    if (selector === '*') return true;
    return candidates.some((candidate) => candidate === selector || candidate.endsWith(`::${selector}`));
  };

  const getParametricPartMeshes = (
    productIdInput: string,
    definition: AsukaParametricDefinition,
    partIdInput: string,
  ): Mesh[] => {
    const productId = String(productIdInput ?? '').trim();
    const partId = String(partIdInput ?? '').trim();
    const part = definition?.parts?.[partId];
    if (!productId || !part) return [];
    const productMeshes = getProductInstanceMeshes(productId);
    const direct = productMeshes.filter((mesh) => (part.selectors ?? []).some((selector) => meshMatchesParametricSelector(mesh, selector)));
    if (part.includeDescendants === false || !direct.length) return direct;
    const included = new Set<Mesh>(direct);
    for (const candidate of productMeshes) {
      let parent: any = candidate.parent;
      while (parent) {
        if (included.has(parent as Mesh)) { included.add(candidate); break; }
        parent = parent.parent;
      }
    }
    return Array.from(included);
  };

  const getParametricTransformRoots = (meshesInput: Mesh[]): Mesh[] => {
    const meshes = (Array.isArray(meshesInput) ? meshesInput : []).filter((mesh) => !!mesh && !mesh.isDisposed?.());
    const selected = new Set<Mesh>(meshes);
    return meshes.filter((mesh) => {
      let parent: any = mesh.parent;
      while (parent) {
        if (selected.has(parent as Mesh)) return false;
        parent = parent.parent;
      }
      return true;
    });
  };

  const parametricWorldOffsetToLocal = (mesh: Mesh, offset: Vector3): Vector3 => {
    const parent: any = mesh?.parent ?? null;
    if (!parent || typeof parent.getWorldMatrix !== 'function') return offset.clone();
    try {
      parent.computeWorldMatrix?.(true);
      return Vector3.TransformNormal(offset, Matrix.Invert(parent.getWorldMatrix()));
    } catch { return offset.clone(); }
  };

  const measureParametricMeshes = (meshesInput: Mesh[]): { width: number; height: number; depth: number; centerX: number; centerY: number; centerZ: number } => {
    const meshes = (Array.isArray(meshesInput) ? meshesInput : []).filter((mesh) => !!mesh && !mesh.isDisposed?.());
    let min = new Vector3(Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY);
    let max = new Vector3(Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY);
    for (const mesh of meshes) {
      try {
        mesh.computeWorldMatrix(true);
        const box = mesh.getBoundingInfo().boundingBox;
        min = Vector3.Minimize(min, box.minimumWorld);
        max = Vector3.Maximize(max, box.maximumWorld);
      } catch {}
    }
    if (!Number.isFinite(min.x) || !Number.isFinite(max.x)) {
      return { width: 0, height: 0, depth: 0, centerX: 0, centerY: 0, centerZ: 0 };
    }
    const size = max.subtract(min);
    const center = min.add(max).scale(0.5);
    return {
      width: Math.abs(size.x),
      height: Math.abs(size.y),
      depth: Math.abs(size.z),
      centerX: center.x,
      centerY: center.y,
      centerZ: center.z,
    };
  };

  const restoreParametricMeshBaseline = (mesh: Mesh, state: ParametricMeshBaseline): void => {
    mesh.position.copyFrom(state.position);
    if (state.rotationQuaternion) {
      mesh.rotationQuaternion = state.rotationQuaternion.clone();
      mesh.rotation.copyFromFloats(0, 0, 0);
    } else {
      mesh.rotationQuaternion = null;
      mesh.rotation.copyFrom(state.rotation);
    }
    mesh.scaling.copyFrom(state.scaling);
    mesh.setEnabled(state.enabled);
    try { mesh.computeWorldMatrix(true); } catch {}
  };

  const isParametricBaselineCurrent = (
    productIdInput: string,
    baselineInput?: Map<Mesh, ParametricMeshBaseline> | null,
  ): boolean => {
    const productId = String(productIdInput ?? '').trim();
    const baseline = baselineInput ?? null;
    if (!productId || !baseline?.size) return false;
    const meshes = getProductInstanceMeshes(productId).filter((mesh) => !!mesh && !mesh.isDisposed?.());
    if (!meshes.length || meshes.length !== baseline.size) return false;
    const current = new Set(meshes);
    return meshes.every((mesh) => baseline.has(mesh))
      && Array.from(baseline.keys()).every((mesh) => current.has(mesh) && !mesh.isDisposed?.());
  };

  const captureParametricBaseline = (
    productIdInput: string,
    definition: AsukaParametricDefinition,
    force = false,
  ): AsukaParametricRuntimeMeasurements => {
    const productId = String(productIdInput ?? '').trim();
    if (!productId) return {};
    if (force) {
      parametricBaselinesByProductId.delete(productId);
      parametricMeasurementsByProductId.delete(productId);
    }
    let baseline = parametricBaselinesByProductId.get(productId);
    if (baseline && !isParametricBaselineCurrent(productId, baseline)) {
      parametricBaselinesByProductId.delete(productId);
      parametricMeasurementsByProductId.delete(productId);
      baseline = undefined;
    }
    const existingMeasurements = parametricMeasurementsByProductId.get(productId);
    if (baseline && existingMeasurements) return clonePlain(existingMeasurements);

    const productMeshes = getProductInstanceMeshes(productId).filter((mesh) => !!mesh && !mesh.isDisposed?.());
    // Never cache an empty baseline. During view restoration the catalog can be
    // synchronized a few frames before Babylon has finished instantiating the GLB.
    // An empty cached map made every later parameter mutation update only JSON
    // while no mesh received the transformation.
    if (!productMeshes.length) return {};

    if (!baseline) {
      baseline = new Map<Mesh, ParametricMeshBaseline>();
      for (const mesh of productMeshes) {
        let localSize = new Vector3(1, 1, 1);
        try { localSize = mesh.getBoundingInfo().boundingBox.extendSize.scale(2); } catch {}
        baseline.set(mesh, {
          position: mesh.position.clone(),
          rotation: mesh.rotation.clone(),
          rotationQuaternion: mesh.rotationQuaternion?.clone() ?? null,
          scaling: mesh.scaling.clone(),
          enabled: mesh.isEnabled(),
          localSize,
        });
      }
      if (baseline.size) parametricBaselinesByProductId.set(productId, baseline);
    }
    for (const [mesh, state] of baseline.entries()) {
      if (!mesh || mesh.isDisposed?.()) continue;
      restoreParametricMeshBaseline(mesh, state);
    }
    const parts: Record<string, any> = {};
    for (const partId of Object.keys(definition.parts ?? {})) {
      parts[partId] = measureParametricMeshes(getParametricPartMeshes(productId, definition, partId));
    }
    const measurements = { product: measureParametricMeshes(productMeshes), parts };
    parametricMeasurementsByProductId.set(productId, clonePlain(measurements));
    return measurements;
  };

  const sanitizeParametricStateValues = (input: any): Record<string, AsukaParametricValue> => {
    const source = input && typeof input === 'object' ? input : {};
    const values = source?.values && typeof source.values === 'object' ? source.values : source;
    const out: Record<string, AsukaParametricValue> = {};
    for (const [key, value] of Object.entries(values ?? {})) {
      const id = String(key ?? '').trim();
      if (!id) continue;
      if (value == null || typeof value === 'string' || typeof value === 'boolean' || (typeof value === 'number' && Number.isFinite(value))) {
        out[id] = value as AsukaParametricValue;
      }
    }
    return out;
  };

  const buildParametricDescriptor = (
    productIdInput?: string | null,
    patchValues: Record<string, unknown> = {},
  ): AsukaParametricProductDescriptor | null => {
    const productId = String(productIdInput ?? getSelectedProductId() ?? '').trim();
    const product = getProductById(productId);
    if (!product) return null;
    const rawDefinition = getParametricDefinitionForProduct(product);
    if (!rawDefinition) return null;
    const definition = withInferredParametricMeasurements(rawDefinition);
    const measurements = captureParametricBaseline(productId, definition, false);
    const existing = sanitizeParametricStateValues(product?.parametric);
    const defaults = createParametricDefaultValues(definition);
    const evaluation = evaluateParametricDefinition(definition, { ...defaults, ...existing, ...patchValues }, measurements);
    return {
      productId,
      catalogId: String(product?.catalogId ?? '').trim() || undefined,
      name: String(product?.name ?? '').trim() || undefined,
      definition: clonePlain(definition),
      state: { values: clonePlain(evaluation.values) },
      evaluation: clonePlain(evaluation),
    };
  };

  type InternalParametricMutationOptions = AsukaParametricMutationOptions & {
    skipAssemblyCollisionGuard?: boolean;
    skipCameraReframe?: boolean;
    propagateAssemblyConnections?: boolean;
    reconcileAssemblyPlacement?: boolean;
  };

  const getAssemblyCollisionKey = (
    collision: { componentId: string; otherComponentId: string; clearanceId?: string },
  ): string => {
    const pair = [
      String(collision?.componentId ?? '').trim(),
      String(collision?.otherComponentId ?? '').trim(),
    ].sort();
    return `${pair[0]}|${pair[1]}|${String(collision?.clearanceId ?? '').trim()}`;
  };

  const applyParametricProductState = (
    productIdInput?: string | null,
    valuesPatch: Record<string, unknown> = {},
    optionsInput: InternalParametricMutationOptions = {},
  ): AsukaParametricProductDescriptor | null => {
    const productId = String(productIdInput ?? getSelectedProductId() ?? '').trim();
    const descriptor = buildParametricDescriptor(productId, valuesPatch);
    if (!descriptor) return null;
    if (optionsInput.validate !== false && !descriptor.evaluation.valid) return descriptor;
    const productBeforeMutation = getProductById(productId);
    const previousParametricValues = sanitizeParametricStateValues(productBeforeMutation?.parametric);
    const assemblyBeforeMutation = isAssemblyModeEnabled()
      ? clonePlain(getAssemblyState())
      : null;
    const componentsBeforeMutation = assemblyBeforeMutation ? listAssemblyComponents() : [];
    const transformsBeforeMutation = assemblyBeforeMutation
      ? new Map(getProductsFromState(currentState).map((product: any) => [
          getProductId(product),
          normalizeAssemblyTransform(product?.transform),
        ] as const))
      : new Map<string, AsukaAssemblyTransform>();
    const collisionKeysBeforeMutation = assemblyBeforeMutation
      ? new Set(validateAssembly(assemblyBeforeMutation, componentsBeforeMutation).collisions.map(getAssemblyCollisionKey))
      : new Set<string>();
    const boundsBeforeMutation = getProductInstanceBounds(productId);
    const assemblyComponentBefore = isAssemblyModeEnabled()
      ? listAssemblyComponents().find((component) => component.id === productId) ?? null
      : null;
    const definition = descriptor.definition;
    const baseline = parametricBaselinesByProductId.get(productId) ?? new Map<Mesh, ParametricMeshBaseline>();
    for (const [mesh, state] of baseline.entries()) {
      if (!mesh || mesh.isDisposed?.()) continue;
      restoreParametricMeshBaseline(mesh, state);
    }
    const plans = new Map<Mesh, { offset: Vector3; rotation: Vector3; scale: Vector3; enabled: boolean }>();
    for (const [mesh, state] of baseline.entries()) {
      if (!mesh || mesh.isDisposed?.()) continue;
      plans.set(mesh, { offset: Vector3.Zero(), rotation: Vector3.Zero(), scale: new Vector3(1, 1, 1), enabled: state.enabled });
    }
    const context = descriptor.evaluation.context;
    const attachmentRules: Extract<AsukaParametricDefinition['rules'][number], { type: 'attach' }>[] = [];
    const partOffsets = new Map<string, Vector3>();
    const finalPartSizes = new Map<string, Vector3>();
    const getPartOffset = (partId: string): Vector3 => partOffsets.get(partId)?.clone() ?? Vector3.Zero();
    const addPartOffset = (partId: string, delta: Vector3): void => {
      partOffsets.set(partId, getPartOffset(partId).add(delta));
    };
    for (const [partId, measurementInput] of Object.entries((context as any)?.part ?? {})) {
      const measurement = measurementInput as any;
      finalPartSizes.set(partId, new Vector3(
        Math.max(0, Number(measurement?.width) || 0),
        Math.max(0, Number(measurement?.height) || 0),
        Math.max(0, Number(measurement?.depth) || 0),
      ));
    }
    for (const rule of definition.rules ?? []) {
      if (!rule || !isParametricExpressionTruthy(rule.when, context, true)) continue;
      if (rule.type === 'attach') {
        attachmentRules.push(rule);
        continue;
      }
      const targetMeshes = getParametricPartMeshes(productId, definition, rule.target);
      if (!targetMeshes.length) continue;
      if (rule.type === 'visibility') {
        const visible = isParametricExpressionTruthy(rule.visible, context, true);
        for (const mesh of targetMeshes) {
          const plan = plans.get(mesh);
          if (plan) plan.enabled = visible;
        }
        continue;
      }
      if (rule.type === 'transform') {
        const offset = new Vector3(
          resolveParametricNumber(rule.positionOffset?.x, context, 0),
          resolveParametricNumber(rule.positionOffset?.y, context, 0),
          resolveParametricNumber(rule.positionOffset?.z, context, 0),
        );
        addPartOffset(rule.target, offset);
        for (const mesh of getParametricTransformRoots(targetMeshes)) {
          const plan = plans.get(mesh);
          if (!plan) continue;
          plan.offset.addInPlace(offset);
          plan.rotation.x += (resolveParametricNumber(rule.rotationOffset?.x, context, 0) * Math.PI) / 180;
          plan.rotation.y += (resolveParametricNumber(rule.rotationOffset?.y, context, 0) * Math.PI) / 180;
          plan.rotation.z += (resolveParametricNumber(rule.rotationOffset?.z, context, 0) * Math.PI) / 180;
          plan.scale.x *= resolveParametricNumber(rule.scale?.x, context, 1);
          plan.scale.y *= resolveParametricNumber(rule.scale?.y, context, 1);
          plan.scale.z *= resolveParametricNumber(rule.scale?.z, context, 1);
        }
        continue;
      }
      if (rule.type === 'resize') {
        const measurement = (descriptor.evaluation.context as any)?.part?.[rule.target] ?? {};
        const currentMeasurement = measureParametricMeshes(targetMeshes);
        const wanted = {
          x: resolveParametricNumber(rule.size?.x, context, Number(measurement.width) || 0),
          y: resolveParametricNumber(rule.size?.y, context, Number(measurement.height) || 0),
          z: resolveParametricNumber(rule.size?.z, context, Number(measurement.depth) || 0),
        };
        const ratios = {
          x: Number(measurement.width) > 0.000001 ? wanted.x / Number(measurement.width) : 1,
          y: Number(measurement.height) > 0.000001 ? wanted.y / Number(measurement.height) : 1,
          z: Number(measurement.depth) > 0.000001 ? wanted.z / Number(measurement.depth) : 1,
        };
        const finalSize = finalPartSizes.get(rule.target) ?? new Vector3(
          Number(measurement.width) || 0,
          Number(measurement.height) || 0,
          Number(measurement.depth) || 0,
        );
        if (rule.size?.x != null) finalSize.x = wanted.x;
        if (rule.size?.y != null) finalSize.y = wanted.y;
        if (rule.size?.z != null) finalSize.z = wanted.z;
        finalPartSizes.set(rule.target, finalSize);
        for (const mesh of getParametricTransformRoots(targetMeshes)) {
          const plan = plans.get(mesh);
          if (!plan) continue;
          // Babylon scales a mesh around its own pivot. Offset every root so a
          // multipart semantic part instead grows around the measured part
          // center, independently from exporter-specific Blender pivots.
          try {
            const rootPosition = mesh.getAbsolutePosition();
            plan.offset.x += (rootPosition.x - currentMeasurement.centerX) * (ratios.x - 1);
            plan.offset.y += (rootPosition.y - currentMeasurement.centerY) * (ratios.y - 1);
            plan.offset.z += (rootPosition.z - currentMeasurement.centerZ) * (ratios.z - 1);
          } catch {}
          if (rule.size?.x != null) plan.scale.x *= ratios.x;
          if (rule.size?.y != null) plan.scale.y *= ratios.y;
          if (rule.size?.z != null) plan.scale.z *= ratios.z;
        }
      }
    }

    const pendingAttachments = [...attachmentRules];
    const attachmentTargets = new Set(pendingAttachments.map((rule) => rule.target));
    const resolvedAttachmentTargets = new Set<string>();
    const applyAttachment = (rule: typeof pendingAttachments[number]): void => {
      const supportMeasurement = (context as any)?.part?.[rule.to] ?? {};
      const targetMeasurement = (context as any)?.part?.[rule.target] ?? {};
      const supportSize = finalPartSizes.get(rule.to);
      const targetSize = finalPartSizes.get(rule.target);
      const resolved = resolveParametricAttachmentOffset({
        support: supportMeasurement,
        target: targetMeasurement,
        supportSize: supportSize ? { x: supportSize.x, y: supportSize.y, z: supportSize.z } : undefined,
        targetSize: targetSize ? { x: targetSize.x, y: targetSize.y, z: targetSize.z } : undefined,
        supportOffset: getPartOffset(rule.to),
        supportAnchor: rule.supportAnchor,
        targetAnchor: rule.targetAnchor,
        positionOffset: rule.positionOffset,
        context,
      });
      const delta = new Vector3(resolved.x, resolved.y, resolved.z);
      for (const mesh of getParametricTransformRoots(getParametricPartMeshes(productId, definition, rule.target))) {
        plans.get(mesh)?.offset.addInPlace(delta);
      }
      addPartOffset(rule.target, delta);
      resolvedAttachmentTargets.add(rule.target);
    };
    for (let pass = 0; pendingAttachments.length && pass <= attachmentRules.length; pass += 1) {
      let changed = false;
      for (let index = pendingAttachments.length - 1; index >= 0; index -= 1) {
        const rule = pendingAttachments[index];
        if (attachmentTargets.has(rule.to) && !resolvedAttachmentTargets.has(rule.to)) continue;
        applyAttachment(rule);
        pendingAttachments.splice(index, 1);
        changed = true;
      }
      if (!changed) break;
    }
    // Cyclic or malformed recipes still receive deterministic baseline-relative
    // offsets instead of silently dropping their attachment rules.
    for (const rule of pendingAttachments) applyAttachment(rule);

    for (const [mesh, state] of baseline.entries()) {
      const plan = plans.get(mesh);
      if (!plan || !mesh || mesh.isDisposed?.()) continue;
      mesh.position.copyFrom(state.position.add(parametricWorldOffsetToLocal(mesh, plan.offset)));
      if (state.rotationQuaternion) {
        const deltaRotation = Quaternion.FromEulerAngles(plan.rotation.x, plan.rotation.y, plan.rotation.z);
        mesh.rotationQuaternion = state.rotationQuaternion.multiply(deltaRotation);
        mesh.rotation.copyFromFloats(0, 0, 0);
      } else {
        mesh.rotationQuaternion = null;
        mesh.rotation.copyFrom(state.rotation.add(plan.rotation));
      }
      mesh.scaling.copyFrom(new Vector3(
        state.scaling.x * plan.scale.x,
        state.scaling.y * plan.scale.y,
        state.scaling.z * plan.scale.z,
      ));
      mesh.setEnabled(plan.enabled);
      try { mesh.computeWorldMatrix(true); } catch {}
    }
    const parametricBounds = getProductInstanceBounds(productId);
    if (parametricBounds) {
      applyProductTransformDeltaToMeshes(
        getProductInstanceMeshes(productId),
        normalizeAssemblyTransform(null),
        normalizeAssemblyTransform(productBeforeMutation?.transform),
        parametricBounds.center,
      );
    }
    patchProductInState(productId, (product) => ({ ...product, parametric: { values: clonePlain(descriptor.state.values) } }));
    if (optionsInput.reconcileAssemblyPlacement !== false) {
      reconcileAssemblyAfterParametricResize(productId, boundsBeforeMutation, assemblyComponentBefore, {
        propagateConnections: optionsInput.propagateAssemblyConnections !== false
          && Object.keys(valuesPatch ?? {}).some((key) => String(key ?? '').trim().length > 0),
      });
    } else if (isAssemblyModeEnabled()) {
      renderAssemblyEnvironment();
    } else {
      try { scene?.render(); } catch {}
    }
    const assemblyAfterMutation = assemblyBeforeMutation ? getAssemblyState() : null;
    const collisionSettings = assemblyAfterMutation
      ? normalizeAssemblySettings(assemblyAfterMutation.settings).collision
      : null;
    if (
      assemblyBeforeMutation
      && assemblyAfterMutation
      && optionsInput.skipAssemblyCollisionGuard !== true
      && collisionSettings?.enabled !== false
      && collisionSettings?.policy === 'prevent'
    ) {
      const affectedComponentIds = new Set(getAssemblyConnectedComponentIds(productId));
      const introducedCollisions = validateAssembly(assemblyAfterMutation, listAssemblyComponents()).collisions
        .filter((collision) => (
          affectedComponentIds.has(collision.componentId)
          || affectedComponentIds.has(collision.otherComponentId)
        ))
        .filter((collision) => !collisionKeysBeforeMutation.has(getAssemblyCollisionKey(collision)));
      if (introducedCollisions.length) {
        patchProductInState(productId, (product) => ({
          ...product,
          parametric: { values: clonePlain(previousParametricValues) },
        }));
        applyParametricProductState(productId, {}, {
          source: 'parametric.assembly-collision-rollback',
          commitHistory: false,
          emitChange: false,
          validate: false,
          skipAssemblyCollisionGuard: true,
          propagateAssemblyConnections: false,
        });
        for (const [componentId, transform] of transformsBeforeMutation.entries()) {
          applyAssemblyProductTransform(componentId, transform, { render: false });
        }
        currentState = {
          ...currentState,
          assembly: normalizeAssemblyState(assemblyBeforeMutation),
        };
        try {
          if (canvas) {
            canvas.dataset.asukaAssemblyResize = JSON.stringify({
              accepted: false,
              reason: 'collision',
              componentId: productId,
              collisions: introducedCollisions,
            });
          }
        } catch {}
        renderAssemblyEnvironment();
        return buildParametricDescriptor(productId);
      }
    }
    try {
      if (canvas) {
        canvas.dataset.asukaAssemblyResize = JSON.stringify({
          accepted: true,
          componentId: productId,
          collisions: [],
        });
      }
    } catch {}
    invalidateProductGroupViewTarget();
    if (optionsInput.skipCameraReframe !== true) {
      centerCameraTargetOnProductGroup('parametricProduct.cameraTarget', { force: centerProductGroupInView, render: false, compute: true });
    }
    try { scene?.render(); } catch {}
    if (optionsInput.emitChange !== false) emitChanged('setProductParameters', productId, { commitHistory: optionsInput.commitHistory !== false });
    else if (optionsInput.commitHistory !== false) commitFinal3DHistorySnapshot(String(optionsInput.source ?? 'setProductParameters'), productId);
    return buildParametricDescriptor(productId) ?? descriptor;
  };

  const initializeParametricProduct = (productIdInput: string, elementInput?: any, forceBaseline = true): AsukaParametricProductDescriptor | null => {
    const productId = String(productIdInput ?? '').trim();
    if (!productId) return null;
    const element = elementInput && typeof elementInput === 'object'
      ? elementInput
      : resolveParametricElementForProduct(getProductById(productId));
    if (!element?.parametric) return null;
    runtimeParametricElementByProductId.set(productId, element);
    captureParametricBaseline(productId, element.parametric as AsukaParametricDefinition, forceBaseline);
    return applyParametricProductState(productId, {}, {
      source: 'parametric.initialize',
      commitHistory: false,
      emitChange: false,
      validate: false,
      reconcileAssemblyPlacement: false,
    });
  };

  const synchronizeParametricCatalogAndScene = (elementsInput: any[]): void => {
    syncRuntimeParametricCatalog(elementsInput);
    for (const product of getProductsFromState(currentState)) {
      const productId = getProductId(product);
      if (!productId || !getProductInstanceMeshes(productId).length) continue;
      const element = resolveParametricElementForProduct(product);
      if (!element?.parametric) continue;
      runtimeParametricElementByProductId.set(productId, element);
      if (!parametricBaselinesByProductId.has(productId)) initializeParametricProduct(productId, element, true);
    }
  };

  const getProductIdFromMesh = (mesh: Mesh | null | undefined): string => getProduct3DMeshInstanceId(mesh);

  const getProductHoverLabel = (mesh: Mesh | null | undefined, fallback?: any): string => {
    const metadata = mesh && (mesh as any).metadata && typeof (mesh as any).metadata === 'object' ? (mesh as any).metadata : {};
    const fallbackMetadata = fallback?.metadata && typeof fallback.metadata === 'object' ? fallback.metadata : {};
    const instanceId = getProductIdFromMesh(mesh) || String(fallbackMetadata?.asukaProductId ?? fallbackMetadata?.productId ?? '').trim();
    const instance = getProductById(instanceId);
    const product = instance && typeof instance === 'object' ? instance : null;
    const catalogProduct = getCurrentProductForParts(instanceId);
    const configuredPartLabel = resolveProduct3DPartVariationGroupLabel(
      catalogProduct?.variations,
      [
        fallback?.productPartId,
        fallback?.meshId,
        fallbackMetadata?.productPartId,
        fallbackMetadata?.productPartMeshId,
        metadata?.productPartId,
        metadata?.asukaProduct3DPartId,
        metadata?.asukaOriginalPartId,
        metadata?.partId,
        metadata?.sourceMeshId,
        metadata?.sourceMeshName,
        mesh?.id,
        mesh?.name,
      ],
    );
    const runtimeRenderableMeshCount = instanceId ? getRenderableModelMeshes(getProductInstanceMeshes(instanceId)).length : 0;
    const persistedRenderableMeshCount = Number(product?.metadata?.renderableMeshCount);
    const renderableMeshCount = runtimeRenderableMeshCount > 0
      ? runtimeRenderableMeshCount
      : (Number.isFinite(persistedRenderableMeshCount) && persistedRenderableMeshCount > 0 ? persistedRenderableMeshCount : 0);

    return resolveProduct3DHoverLabel({
      meshCount: renderableMeshCount,
      // Variation group names are editable labels. Mesh/part names remain stable
      // technical identifiers and are only used when no configured label exists.
      partLabels: [
        configuredPartLabel,
        fallback?.productPartLabel,
        fallbackMetadata?.productPartLabel,
        fallback?.label,
        metadata?.productPartLabel,
        metadata?.asukaProduct3DPartLabel,
        metadata?.partLabel,
        fallback?.productPartId,
        fallbackMetadata?.productPartId,
        metadata?.productPartId,
        metadata?.asukaProduct3DPartId,
        metadata?.asukaOriginalPartId,
        metadata?.partId,
      ],
      // A one-mesh product has no useful part distinction. Prefer the catalog /
      // runtime product name instead of exporter-generated node ids.
      elementLabels: [
        product?.name,
        metadata?.elementName,
        fallbackMetadata?.elementName,
        metadata?.catalogName,
        fallbackMetadata?.catalogName,
        product?.catalogId,
      ],
      fallbackLabels: [mesh?.name, mesh?.id],
    });
  };

  const getProductInstanceRootMeshes = (meshesInput: Mesh[]): Mesh[] => {
    const meshes = (Array.isArray(meshesInput) ? meshesInput : []).filter((mesh) => !!mesh && !mesh.isDisposed?.());
    const meshSet = new Set(meshes);
    const roots = meshes.filter((mesh) => {
      let parent: any = mesh.parent;
      while (parent) {
        if (meshSet.has(parent as Mesh)) return false;
        parent = parent.parent;
      }
      return true;
    });
    return roots.length ? roots : meshes;
  };

  const getMeshCollectionBounds = (meshesInput: Mesh[]) => {
    const meshes = (Array.isArray(meshesInput) ? meshesInput : []).filter((mesh) => !!mesh && !mesh.isDisposed?.());
    if (!meshes.length) return null;
    let min = new Vector3(Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY);
    let max = new Vector3(Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY);
    for (const mesh of meshes) {
      try {
        mesh.computeWorldMatrix(true);
        const box = mesh.getBoundingInfo().boundingBox;
        min = Vector3.Minimize(min, box.minimumWorld);
        max = Vector3.Maximize(max, box.maximumWorld);
      } catch {}
    }
    if (!Number.isFinite(min.x) || !Number.isFinite(max.x)) return null;
    return { min, max, center: min.add(max).scale(0.5), size: max.subtract(min) };
  };

  const getProductTransformVectors = (input: any) => {
    const transform = (compactProductTransform(input) ?? {}) as any;
    return {
      position: new Vector3(
        Number(transform?.position?.x ?? 0) || 0,
        Number(transform?.position?.y ?? 0) || 0,
        Number(transform?.position?.z ?? 0) || 0,
      ),
      rotation: new Vector3(
        Number(transform?.rotation?.x ?? 0) || 0,
        Number(transform?.rotation?.y ?? 0) || 0,
        Number(transform?.rotation?.z ?? 0) || 0,
      ),
      scale: new Vector3(
        Math.max(0.0001, Number(transform?.scale?.x ?? 1) || 1),
        Math.max(0.0001, Number(transform?.scale?.y ?? 1) || 1),
        Math.max(0.0001, Number(transform?.scale?.z ?? 1) || 1),
      ),
    };
  };

  const createProductWorldTransformDelta = (
    pivot: Vector3,
    previousInput: any,
    nextInput: any,
  ): Matrix => {
    const previous = getProductTransformVectors(previousInput);
    const next = getProductTransformVectors(nextInput);
    const positionDelta = next.position.subtract(previous.position);
    const rotationDelta = next.rotation.subtract(previous.rotation);
    const scaleRatio = new Vector3(
      next.scale.x / Math.max(0.0001, previous.scale.x),
      next.scale.y / Math.max(0.0001, previous.scale.y),
      next.scale.z / Math.max(0.0001, previous.scale.z),
    );
    return Matrix.Translation(-pivot.x, -pivot.y, -pivot.z)
      .multiply(Matrix.Scaling(scaleRatio.x, scaleRatio.y, scaleRatio.z))
      .multiply(Matrix.RotationYawPitchRoll(rotationDelta.y, rotationDelta.x, rotationDelta.z))
      .multiply(Matrix.Translation(
        pivot.x + positionDelta.x,
        pivot.y + positionDelta.y,
        pivot.z + positionDelta.z,
      ));
  };

  const applyProductTransformDeltaToMeshes = (
    meshesInput: Mesh[],
    previousInput: any,
    nextInput: any,
    pivotInput?: Vector3 | null,
  ): boolean => {
    const meshes = (Array.isArray(meshesInput) ? meshesInput : []).filter((mesh) => !!mesh && !mesh.isDisposed?.());
    const roots = getProductInstanceRootMeshes(meshes);
    const pivot = pivotInput?.clone?.() ?? getMeshCollectionBounds(meshes)?.center?.clone?.() ?? null;
    if (!roots.length || !pivot) return false;
    const worldDelta = createProductWorldTransformDelta(pivot, previousInput, nextInput);
    let changed = false;
    for (const mesh of roots) {
      try {
        mesh.computeWorldMatrix(true);
        const nextWorld = mesh.getWorldMatrix().clone().multiply(worldDelta);
        let nextLocal = nextWorld;
        const parent: any = mesh.parent ?? null;
        if (parent && typeof parent.getWorldMatrix === 'function') {
          parent.computeWorldMatrix?.(true);
          nextLocal = nextWorld.multiply(Matrix.Invert(parent.getWorldMatrix()));
        }
        const nextScale = Vector3.One();
        const nextRotation = Quaternion.Identity();
        const nextPosition = Vector3.Zero();
        if (!nextLocal.decompose(nextScale, nextRotation, nextPosition)) continue;
        mesh.position.copyFrom(nextPosition);
        mesh.scaling.copyFrom(nextScale);
        mesh.rotationQuaternion = nextRotation;
        mesh.rotation.copyFromFloats(0, 0, 0);
        mesh.computeWorldMatrix(true);
        changed = true;
      } catch {}
    }
    for (const mesh of meshes) {
      try { mesh.computeWorldMatrix(true); } catch {}
    }
    return changed;
  };

  const applyProductInstanceTransformToMeshes = (meshesInput: Mesh[], transformInput: any): void => {
    const transform = compactProductTransform(transformInput);
    if (!transform) return;
    applyProductTransformDeltaToMeshes(meshesInput, null, transform);
  };

  const getProductInstanceBounds = (instanceIdInput?: string | null) => {
    return getMeshCollectionBounds(getProductInstanceMeshes(instanceIdInput));
  };

  const getAssemblyFloorElevationForScene = (
    points?: AsukaAssemblyPlanPoint[] | null,
    fallbackInput?: number,
  ): number => {
    const assembly = getAssemblyState();
    const fallback = Number(fallbackInput ?? assembly.plan?.floorY ?? 0) || 0;
    const bounds = getProductsFromState(currentState)
      .map((product: any) => getProductInstanceBounds(getProductId(product)))
      .filter(Boolean);
    return getAssemblyFloorElevation(bounds, points, fallback);
  };

  const normalizeAssemblyTransform = (input: any): AsukaAssemblyTransform => ({
    position: {
      x: Number(input?.position?.x ?? 0) || 0,
      y: Number(input?.position?.y ?? 0) || 0,
      z: Number(input?.position?.z ?? 0) || 0,
    },
    rotation: {
      x: Number(input?.rotation?.x ?? 0) || 0,
      y: Number(input?.rotation?.y ?? 0) || 0,
      z: Number(input?.rotation?.z ?? 0) || 0,
    },
    scale: {
      x: Math.max(0.0001, Number(input?.scale?.x ?? 1) || 1),
      y: Math.max(0.0001, Number(input?.scale?.y ?? 1) || 1),
      z: Math.max(0.0001, Number(input?.scale?.z ?? 1) || 1),
    },
  });

  const getAssemblyState = (): AsukaAssemblyState => normalizeAssemblyState(currentState.assembly);

  const listAssemblyComponents = (): AsukaAssemblyComponent[] => {
    const assembly = getAssemblyState();
    const connections = assembly.connections ?? [];
    const out: AsukaAssemblyComponent[] = [];
    for (const product of getProductsFromState(currentState)) {
      const id = getProductId(product);
      const definition = getAssemblyDefinitionForProduct(product);
      const bounds = getProductInstanceBounds(id);
      if (!id || !definition || !bounds) continue;
      const transform = normalizeAssemblyTransform(product?.transform);
      const neutralProduct = (parametricMeasurementsByProductId.get(id) as any)?.product ?? null;
      const geometryScale = {
        x: Number(neutralProduct?.width) > 0.000001
          ? Math.max(0.0001, bounds.size.x / (Number(neutralProduct.width) * Math.max(0.0001, transform.scale?.x ?? 1)))
          : 1,
        y: Number(neutralProduct?.height) > 0.000001
          ? Math.max(0.0001, bounds.size.y / (Number(neutralProduct.height) * Math.max(0.0001, transform.scale?.y ?? 1)))
          : 1,
        z: Number(neutralProduct?.depth) > 0.000001
          ? Math.max(0.0001, bounds.size.z / (Number(neutralProduct.depth) * Math.max(0.0001, transform.scale?.z ?? 1)))
          : 1,
      };
      const geometrySize = {
        x: Number(neutralProduct?.width) > 0.000001
          ? Number(neutralProduct.width) * geometryScale.x
          : bounds.size.x / Math.max(0.0001, transform.scale?.x ?? 1),
        y: Number(neutralProduct?.height) > 0.000001
          ? Number(neutralProduct.height) * geometryScale.y
          : bounds.size.y / Math.max(0.0001, transform.scale?.y ?? 1),
        z: Number(neutralProduct?.depth) > 0.000001
          ? Number(neutralProduct.depth) * geometryScale.z
          : bounds.size.z / Math.max(0.0001, transform.scale?.z ?? 1),
      };
      out.push({
        id,
        catalogId: String(product?.catalogId ?? '').trim() || undefined,
        label: String(product?.name ?? id).trim() || id,
        category: definition.category,
        tags: Array.isArray(definition.tags) ? definition.tags.slice() : [],
        transform,
        geometryScale,
        bounds: {
          min: { x: bounds.min.x, y: bounds.min.y, z: bounds.min.z },
          max: { x: bounds.max.x, y: bounds.max.y, z: bounds.max.z },
          center: { x: bounds.center.x, y: bounds.center.y, z: bounds.center.z },
          size: { x: bounds.size.x, y: bounds.size.y, z: bounds.size.z },
        },
        anchors: createAssemblyWorldAnchors(
          id,
          definition,
          transform,
          { x: bounds.center.x, y: bounds.center.y, z: bounds.center.z },
          connections,
          geometryScale,
          geometrySize,
        ),
        connectionIds: connections
          .filter((connection) => connection.source.componentId === id || connection.target.componentId === id)
          .map((connection) => connection.id),
        definition,
        metadata: {
          productId: id,
          variationId: String(product?.variationId ?? '').trim() || undefined,
          price: normalizePriceInput(product?.price) ?? 0,
          basePrice: normalizePriceInput(product?.basePrice) ?? 0,
          variationPrice: normalizePriceInput(product?.variationPrice) ?? 0,
          sku: String(product?.metadata?.sku ?? '').trim() || undefined,
        },
      });
    }
    return out;
  };

  const clearAssemblyEnvironment = (): void => {
    while (assemblyEnvironmentMeshes.length) {
      const mesh = assemblyEnvironmentMeshes.pop();
      try { mesh?.dispose(false, false); } catch {}
    }
    while (assemblyEnvironmentMaterials.length) {
      const material = assemblyEnvironmentMaterials.pop();
      try {
        (material as any)?.metadata?.asukaAssemblyTexture?.release?.();
      } catch {}
      try { material?.dispose(false, true); } catch {}
    }
    try {
      if (canvas) delete canvas.dataset.asukaAssemblyTextures;
    } catch {}
  };

  const clearAssemblyBuildPreview = (): void => {
    while (assemblyBuildPreviewMeshes.length) {
      const mesh = assemblyBuildPreviewMeshes.pop();
      try { mesh?.dispose(false, false); } catch {}
    }
    while (assemblyBuildPreviewMaterials.length) {
      const material = assemblyBuildPreviewMaterials.pop();
      try { material?.dispose(false, true); } catch {}
    }
  };

  const getAssemblyGuideBounds = (component: AsukaAssemblyComponent): AsukaAssemblyBounds => {
    const custom = component.definition?.collision?.bounds;
    if (!custom?.size) return component.bounds;
    const localCenter = custom.center ?? { x: 0, y: 0, z: 0 };
    const rotatedCenter = rotateAssemblyVector(localCenter, component.transform.rotation);
    const center = {
      x: component.bounds.center.x + rotatedCenter.x,
      y: component.bounds.center.y + rotatedCenter.y,
      z: component.bounds.center.z + rotatedCenter.z,
    };
    const size = {
      x: Math.max(0.001, Number(custom.size.x) || 0.001),
      y: Math.max(0.001, Number(custom.size.y) || 0.001),
      z: Math.max(0.001, Number(custom.size.z) || 0.001),
    };
    return {
      min: {
        x: center.x - size.x * 0.5,
        y: center.y - size.y * 0.5,
        z: center.z - size.z * 0.5,
      },
      max: {
        x: center.x + size.x * 0.5,
        y: center.y + size.y * 0.5,
        z: center.z + size.z * 0.5,
      },
      center,
      size,
    };
  };

  const getAssemblyClearanceGuideBounds = (
    component: AsukaAssemblyComponent,
    clearance: NonNullable<AsukaAssemblyComponentDefinition['clearances']>[number],
  ): AsukaAssemblyBounds => {
    const localCenter = clearance.center ?? { x: 0, y: 0, z: 0 };
    const geometryScale = component.geometryScale ?? { x: 1, y: 1, z: 1 };
    const scaledCenter = clearance.positionMode === 'scaled'
      ? {
          x: localCenter.x * geometryScale.x,
          y: localCenter.y * geometryScale.y,
          z: localCenter.z * geometryScale.z,
        }
      : localCenter;
    const rotatedCenter = rotateAssemblyVector(scaledCenter, component.transform.rotation);
    const center = {
      x: component.bounds.center.x + rotatedCenter.x,
      y: component.bounds.center.y + rotatedCenter.y,
      z: component.bounds.center.z + rotatedCenter.z,
    };
    const sizeScale = clearance.sizeMode === 'scaled' ? geometryScale : { x: 1, y: 1, z: 1 };
    const size = {
      x: Math.max(0.001, (Number(clearance.size.x) || 0.001) * sizeScale.x),
      y: Math.max(0.001, (Number(clearance.size.y) || 0.001) * sizeScale.y),
      z: Math.max(0.001, (Number(clearance.size.z) || 0.001) * sizeScale.z),
    };
    return {
      min: {
        x: center.x - size.x * 0.5,
        y: center.y - size.y * 0.5,
        z: center.z - size.z * 0.5,
      },
      max: {
        x: center.x + size.x * 0.5,
        y: center.y + size.y * 0.5,
        z: center.z + size.z * 0.5,
      },
      center,
      size,
    };
  };

  const createAssemblyPolygonMesh = (
    name: string,
    pointsInput: Array<{ x: number; z: number }>,
    thicknessInput: number,
  ): Mesh | null => {
    if (!scene || pointsInput.length < 3) return null;
    const points = pointsInput.map((point) => ({
      x: Number(point.x) || 0,
      z: Number(point.z) || 0,
    }));
    const flatPoints = points.flatMap((point) => [point.x, point.z]);
    const triangulated = earcut(flatPoints, undefined, 2);
    if (triangulated.length < 3) return null;
    const thickness = Math.max(0.003, Number(thicknessInput) || 0.012);
    const minX = Math.min(...points.map((point) => point.x));
    const maxX = Math.max(...points.map((point) => point.x));
    const minZ = Math.min(...points.map((point) => point.z));
    const maxZ = Math.max(...points.map((point) => point.z));
    const width = Math.max(0.0001, maxX - minX);
    const depth = Math.max(0.0001, maxZ - minZ);
    const positions: number[] = [];
    const uvs: number[] = [];
    const indices: number[] = [];
    const normals: number[] = [];

    for (const point of points) {
      positions.push(point.x, 0, point.z);
      uvs.push((point.x - minX) / width, (point.z - minZ) / depth);
    }
    const bottomOffset = points.length;
    for (const point of points) {
      positions.push(point.x, -thickness, point.z);
      uvs.push((point.x - minX) / width, 1 - (point.z - minZ) / depth);
    }
    for (let index = 0; index < triangulated.length; index += 3) {
      const a = triangulated[index];
      const b = triangulated[index + 1];
      const c = triangulated[index + 2];
      const pointA = points[a];
      const pointB = points[b];
      const pointC = points[c];
      const normalY = (
        (pointB.z - pointA.z) * (pointC.x - pointA.x)
        - (pointB.x - pointA.x) * (pointC.z - pointA.z)
      );
      const top = normalY >= 0 ? [a, b, c] : [a, c, b];
      indices.push(...top);
      indices.push(
        top[2] + bottomOffset,
        top[1] + bottomOffset,
        top[0] + bottomOffset,
      );
    }

    const counterClockwise = getAssemblyPolygonSignedArea(points) >= 0;
    for (let index = 0; index < points.length; index += 1) {
      const nextIndex = (index + 1) % points.length;
      const start = points[index];
      const end = points[nextIndex];
      const sideOffset = positions.length / 3;
      positions.push(
        start.x, 0, start.z,
        start.x, -thickness, start.z,
        end.x, 0, end.z,
        end.x, -thickness, end.z,
      );
      uvs.push(0, 0, 0, 1, 1, 0, 1, 1);
      if (counterClockwise) {
        indices.push(
          sideOffset, sideOffset + 2, sideOffset + 1,
          sideOffset + 2, sideOffset + 3, sideOffset + 1,
        );
      } else {
        indices.push(
          sideOffset, sideOffset + 1, sideOffset + 2,
          sideOffset + 2, sideOffset + 1, sideOffset + 3,
        );
      }
    }
    VertexData.ComputeNormals(positions, indices, normals);
    const vertexData = new VertexData();
    vertexData.positions = positions;
    vertexData.indices = indices;
    vertexData.normals = normals;
    vertexData.uvs = uvs;
    const mesh = new Mesh(name, scene);
    vertexData.applyToMesh(mesh, false);
    return mesh;
  };

  const createAssemblyPolygonTopMesh = (
    name: string,
    pointsInput: Array<{ x: number; z: number }>,
  ): Mesh | null => {
    if (!scene || pointsInput.length < 3) return null;
    const points = pointsInput.map((point) => ({
      x: Number(point.x) || 0,
      z: Number(point.z) || 0,
    }));
    const triangulated = earcut(
      points.flatMap((point) => [point.x, point.z]),
      undefined,
      2,
    );
    if (triangulated.length < 3) return null;
    const minX = Math.min(...points.map((point) => point.x));
    const maxX = Math.max(...points.map((point) => point.x));
    const minZ = Math.min(...points.map((point) => point.z));
    const maxZ = Math.max(...points.map((point) => point.z));
    const width = Math.max(0.0001, maxX - minX);
    const depth = Math.max(0.0001, maxZ - minZ);
    const positions = points.flatMap((point) => [point.x, 0, point.z]);
    const uvs = points.flatMap((point) => [
      (point.x - minX) / width,
      (point.z - minZ) / depth,
    ]);
    const indices: number[] = [];
    const normals = points.flatMap(() => [0, 1, 0]);
    for (let index = 0; index < triangulated.length; index += 3) {
      const a = triangulated[index];
      const b = triangulated[index + 1];
      const c = triangulated[index + 2];
      const pointA = points[a];
      const pointB = points[b];
      const pointC = points[c];
      const normalY = (
        (pointB.z - pointA.z) * (pointC.x - pointA.x)
        - (pointB.x - pointA.x) * (pointC.z - pointA.z)
      );
      // Babylon's left-handed renderer treats clockwise winding as the front
      // face. Keep normals pointing upward, but wind the finish so it is visible
      // only from above when back-face culling is enabled.
      indices.push(...(normalY >= 0 ? [a, c, b] : [a, b, c]));
    }
    const vertexData = new VertexData();
    vertexData.positions = positions;
    vertexData.indices = indices;
    vertexData.normals = normals;
    vertexData.uvs = uvs;
    const mesh = new Mesh(name, scene);
    vertexData.applyToMesh(mesh, false);
    return mesh;
  };

  const createAssemblyWallPrism = (
    name: string,
    footprint: ReturnType<typeof getAssemblyRoomWallFootprint>,
    heightInput: number,
  ): Mesh | null => {
    if (!scene) return null;
    const points = [
      footprint.interiorStart,
      footprint.interiorEnd,
      footprint.exteriorEnd,
      footprint.exteriorStart,
    ].map((point) => ({
      x: Number(point.x) || 0,
      z: Number(point.z) || 0,
    }));
    const triangulated = earcut(points.flatMap((point) => [point.x, point.z]), undefined, 2);
    if (triangulated.length < 3) return null;
    const height = Math.max(0.05, Number(heightInput) || 0.05);
    const minX = Math.min(...points.map((point) => point.x));
    const maxX = Math.max(...points.map((point) => point.x));
    const minZ = Math.min(...points.map((point) => point.z));
    const maxZ = Math.max(...points.map((point) => point.z));
    const width = Math.max(0.0001, maxX - minX);
    const depth = Math.max(0.0001, maxZ - minZ);
    const positions: number[] = [];
    const uvs: number[] = [];
    const indices: number[] = [];
    const normals: number[] = [];

    for (const point of points) {
      positions.push(point.x, 0, point.z);
      uvs.push((point.x - minX) / width, (point.z - minZ) / depth);
    }
    const topOffset = points.length;
    for (const point of points) {
      positions.push(point.x, height, point.z);
      uvs.push((point.x - minX) / width, (point.z - minZ) / depth);
    }
    for (let index = 0; index < triangulated.length; index += 3) {
      const a = triangulated[index];
      const b = triangulated[index + 1];
      const c = triangulated[index + 2];
      const pointA = points[a];
      const pointB = points[b];
      const pointC = points[c];
      const normalY = (
        (pointB.z - pointA.z) * (pointC.x - pointA.x)
        - (pointB.x - pointA.x) * (pointC.z - pointA.z)
      );
      const top = normalY >= 0 ? [a, b, c] : [a, c, b];
      indices.push(
        top[2],
        top[1],
        top[0],
        top[0] + topOffset,
        top[1] + topOffset,
        top[2] + topOffset,
      );
    }
    for (let index = 0; index < points.length; index += 1) {
      const nextIndex = (index + 1) % points.length;
      const start = points[index];
      const end = points[nextIndex];
      const sideOffset = positions.length / 3;
      positions.push(
        start.x, 0, start.z,
        end.x, 0, end.z,
        end.x, height, end.z,
        start.x, height, start.z,
      );
      uvs.push(0, 1, 1, 1, 1, 0, 0, 0);
      indices.push(
        sideOffset,
        sideOffset + 1,
        sideOffset + 2,
        sideOffset,
        sideOffset + 2,
        sideOffset + 3,
      );
    }
    VertexData.ComputeNormals(positions, indices, normals);
    const vertexData = new VertexData();
    vertexData.positions = positions;
    vertexData.indices = indices;
    vertexData.normals = normals;
    vertexData.uvs = uvs;
    const mesh = new Mesh(name, scene);
    vertexData.applyToMesh(mesh, false);
    return mesh;
  };

  const createAssemblyWallFinishMesh = (
    name: string,
    start: { x: number; z: number },
    end: { x: number; z: number },
    heightInput: number,
    normalSide: 'left' | 'right',
  ): Mesh | null => {
    if (!scene) return null;
    const height = Math.max(0.05, Number(heightInput) || 0.05);
    const positions = [
      start.x, 0, start.z,
      end.x, 0, end.z,
      end.x, height, end.z,
      start.x, height, start.z,
    ];
    const indices = normalSide === 'left'
      ? [0, 1, 2, 0, 2, 3]
      : [0, 2, 1, 0, 3, 2];
    const normals: number[] = [];
    VertexData.ComputeNormals(positions, indices, normals);
    const vertexData = new VertexData();
    vertexData.positions = positions;
    vertexData.indices = indices;
    vertexData.normals = normals;
    vertexData.uvs = [0, 1, 1, 1, 1, 0, 0, 0];
    const mesh = new Mesh(name, scene);
    vertexData.applyToMesh(mesh, false);
    return mesh;
  };

  const renderAssemblyEnvironment = (): void => {
    clearAssemblyEnvironment();
    if (!scene || !isAssemblyModeEnabled()) return;
    const assembly = getAssemblyState();
    const plan = assembly.plan;
    if (!plan?.points?.length) return;
    const settings = normalizeAssemblySettings(assembly.settings);
    const visualization = settings.visualization;
    const explicitSurfaces = Array.isArray(plan.floors) || Array.isArray(plan.walls);
    const floorY = Number(plan.floorY ?? 0) || 0;
    const wallHeight = Math.max(0.05, Number(plan.height ?? 2.6) || 2.6);
    const wallThickness = Math.max(0.005, Number(plan.wallThickness ?? 0.08) || 0.08);
    const bounds = getAssemblyPlanBounds(plan);

    const makeEnvironmentMaterial = (name: string, color: string, alpha: number): StandardMaterial => {
      const material = new StandardMaterial(name, scene!);
      material.diffuseColor = color3FromInput(color);
      material.emissiveColor = color3FromInput(color).scale(0.08);
      material.alpha = alpha;
      material.backFaceCulling = false;
      material.disableDepthWrite = alpha < 0.99;
      assemblyEnvironmentMaterials.push(material);
      return material;
    };

    const resolveAssemblyTextureLoaderHints = (
      textureUrl: string,
    ): { mimeType?: string; forcedExtension?: string } => {
      const dataMime = textureUrl.match(/^data:([^;,]+)/i)?.[1]?.toLowerCase();
      const pathname = textureUrl.split(/[?#]/, 1)[0].toLowerCase();
      const mimeByExtension: Record<string, string> = {
        '.svg': 'image/svg+xml',
        '.png': 'image/png',
        '.jpg': 'image/jpeg',
        '.jpeg': 'image/jpeg',
        '.webp': 'image/webp',
        '.gif': 'image/gif',
        '.bmp': 'image/bmp',
        '.avif': 'image/avif',
      };
      const extensionByMime: Record<string, string> = {
        'image/svg+xml': '.svg',
        'image/png': '.png',
        'image/jpeg': '.jpg',
        'image/webp': '.webp',
        'image/gif': '.gif',
        'image/bmp': '.bmp',
        'image/avif': '.avif',
      };
      const forcedExtension = Object.keys(mimeByExtension).find((extension) => (
        pathname.endsWith(extension)
      )) ?? (dataMime ? extensionByMime[dataMime] : undefined);
      const mimeType = dataMime ?? (forcedExtension ? mimeByExtension[forcedExtension] : undefined);
      return { mimeType, forcedExtension };
    };

    const createAssemblyTextureRuntimeSource = (
      textureUrl: string,
    ): { url: string; release: (() => void) | null } => {
      if (!/^data:image\/svg\+xml(?:[;,])/i.test(textureUrl)) {
        return { url: textureUrl, release: null };
      }
      try {
        const separator = textureUrl.indexOf(',');
        if (separator < 0) return { url: textureUrl, release: null };
        const header = textureUrl.slice(0, separator);
        const payload = textureUrl.slice(separator + 1);
        let source = '';
        if (/;base64(?:;|$)/i.test(header)) {
          const binary = atob(payload);
          const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
          source = new TextDecoder().decode(bytes);
        } else {
          source = decodeURIComponent(payload);
        }
        const objectUrl = URL.createObjectURL(new Blob([source], { type: 'image/svg+xml' }));
        let active = true;
        return {
          url: objectUrl,
          release: () => {
            if (!active) return;
            active = false;
            URL.revokeObjectURL(objectUrl);
          },
        };
      } catch {
        return { url: textureUrl, release: null };
      }
    };

    const syncAssemblyTextureDiagnostic = (): void => {
      try {
        if (!canvas) return;
        const surfaces = assemblyEnvironmentMaterials.flatMap((material) => {
          const diagnostic = (material as any).metadata?.asukaAssemblyTexture;
          if (!diagnostic) return [];
          const texture = material.diffuseTexture as Texture | null;
          const ready = diagnostic.status === 'ready'
            || (diagnostic.status !== 'failed' && texture?.isReady?.() === true);
          return [{
            material: material.name,
            url: diagnostic.url,
            status: ready ? 'ready' : diagnostic.status,
            attached: texture != null,
            backFaceCulling: material.backFaceCulling,
            error: diagnostic.error ?? null,
          }];
        });
        canvas.dataset.asukaAssemblyTextures = JSON.stringify({
          requested: surfaces.length,
          attached: surfaces.filter((entry) => entry.attached).length,
          ready: surfaces.filter((entry) => entry.status === 'ready').length,
          failed: surfaces.filter((entry) => entry.status === 'failed').length,
          surfaces,
        });
      } catch {}
    };

    const makeSurfaceMaterial = (
      name: string,
      input: AsukaAssemblySurfaceMaterial | null | undefined,
      fallbackColor: string,
      fallbackAlpha = 1,
      surfaceSize?: { width: number; height: number },
    ): StandardMaterial => {
      const alpha = Math.max(0, Math.min(1, Number(input?.opacity ?? fallbackAlpha) || fallbackAlpha));
      const material = makeEnvironmentMaterial(name, String(input?.color ?? fallbackColor), alpha);
      const textureUrl = String(input?.textureUrl ?? '').trim();
      if (textureUrl) {
        const diagnostic = {
          url: textureUrl,
          status: 'loading' as 'loading' | 'ready' | 'failed',
          error: null as string | null,
          release: null as (() => void) | null,
        };
        (material as any).metadata = {
          ...((material as any).metadata ?? {}),
          asukaAssemblyTexture: diagnostic,
        };
        try {
          const hints = resolveAssemblyTextureLoaderHints(textureUrl);
          const runtimeSource = createAssemblyTextureRuntimeSource(textureUrl);
          diagnostic.release = runtimeSource.release;
          const releaseRuntimeSource = () => {
            diagnostic.release?.();
            diagnostic.release = null;
          };
          const texture = new Texture(runtimeSource.url, scene!, {
            noMipmap: false,
            invertY: false,
            samplingMode: Texture.TRILINEAR_SAMPLINGMODE,
            mimeType: hints.mimeType,
            forcedExtension: hints.forcedExtension,
            gammaSpace: true,
            onLoad: () => {
              diagnostic.status = 'ready';
              diagnostic.error = null;
              // Pattern assets already contain their intended base color.
              // Keeping the fallback tint would multiply and darken them.
              material.diffuseColor = Color3.White();
              releaseRuntimeSource();
              syncAssemblyTextureDiagnostic();
            },
            onError: (message?: string, exception?: unknown) => {
              diagnostic.status = 'failed';
              diagnostic.error = String(
                message
                || (exception as any)?.message
                || 'Texture loading failed.',
              );
              material.diffuseTexture = null;
              releaseRuntimeSource();
              syncAssemblyTextureDiagnostic();
            },
          });
          const physicalWidth = Math.max(0, Number(input?.textureSize?.width ?? 0) || 0);
          const physicalHeight = Math.max(0, Number(input?.textureSize?.height ?? 0) || 0);
          texture.uScale = physicalWidth > 0 && surfaceSize
            ? Math.max(0.01, surfaceSize.width / physicalWidth)
            : Math.max(0.01, Number(input?.textureScale?.x ?? 1) || 1);
          texture.vScale = physicalHeight > 0 && surfaceSize
            ? Math.max(0.01, surfaceSize.height / physicalHeight)
            : Math.max(0.01, Number(input?.textureScale?.y ?? 1) || 1);
          texture.wAng = ((Number(input?.textureRotation ?? 0) || 0) * Math.PI) / 180;
          texture.wrapU = Texture.WRAP_ADDRESSMODE;
          texture.wrapV = Texture.WRAP_ADDRESSMODE;
          texture.isBlocking = false;
          material.diffuseTexture = texture;
          material.emissiveColor = Color3.Black();
          syncAssemblyTextureDiagnostic();
        } catch (error) {
          diagnostic.status = 'failed';
          diagnostic.error = String((error as Error)?.message || error || 'Texture loading failed.');
          diagnostic.release?.();
          diagnostic.release = null;
          syncAssemblyTextureDiagnostic();
        }
      }
      return material;
    };
    let assemblyDimensionCount = 0;
    const formatAssemblyDimension = (value: number): string => {
      const unit = settings.measurement.displayUnit;
      const converted = assemblyWorldToDisplay(value, assembly.settings);
      const decimals = unit === 'm' ? 2 : unit === 'cm' ? 1 : 0;
      return `${Number(converted.toFixed(decimals))} ${unit}`;
    };
    const addAssemblyDimensionLabel = (
      text: string,
      position: Vector3,
    ): void => {
      const texture = new DynamicTexture(
        `asuka-assembly-dimension-texture-${assemblyDimensionCount}`,
        { width: 512, height: 128 },
        scene!,
        false,
      );
      texture.hasAlpha = true;
      // DynamicTexture is rendered through the back side of a billboard plane
      // in Babylon's left-handed scene. Rotate the UVs once so labels remain
      // readable from every camera angle.
      texture.uScale = -1;
      texture.uOffset = 1;
      texture.vScale = -1;
      texture.vOffset = 1;
      const context = texture.getContext() as any;
      context.clearRect(0, 0, 512, 128);
      context.fillStyle = 'rgba(15, 23, 42, 0.9)';
      context.beginPath();
      context.roundRect?.(8, 14, 496, 100, 24);
      if (typeof context.roundRect !== 'function') context.rect(8, 14, 496, 100);
      context.fill();
      context.fillStyle = '#ffffff';
      context.font = '600 38px Arial, sans-serif';
      context.textAlign = 'center';
      context.textBaseline = 'middle';
      context.fillText(text, 256, 64);
      texture.update(false);

      const material = makeEnvironmentMaterial(
        `asuka-assembly-dimension-material-${assemblyDimensionCount}`,
        '#ffffff',
        1,
      );
      material.disableLighting = true;
      material.emissiveColor = Color3.White();
      material.diffuseTexture = texture;
      material.opacityTexture = texture;
      const plane = MeshBuilder.CreatePlane(
        `asuka-assembly-dimension-label-${assemblyDimensionCount}`,
        {
          width: Math.max(0.48, Math.min(1.2, 0.24 + text.length * 0.052)),
          height: 0.22,
          sideOrientation: Mesh.DOUBLESIDE,
        },
        scene!,
      );
      plane.position.copyFrom(position);
      plane.scaling.x = -1;
      plane.billboardMode = Mesh.BILLBOARDMODE_ALL;
      plane.material = material;
      plane.isPickable = false;
      plane.renderingGroupId = 3;
      plane.metadata = {
        ...(plane.metadata ?? {}),
        asukaAssemblyEnvironment: true,
        asukaAssemblyDimension: true,
        text,
      };
      assemblyEnvironmentMeshes.push(plane);
      assemblyDimensionCount += 1;
    };
    const addAssemblyDimensionLines = (
      name: string,
      lines: Vector3[][],
    ): void => {
      const lineSystem = MeshBuilder.CreateLineSystem(name, { lines }, scene!);
      lineSystem.color = color3FromInput('#1d4ed8');
      lineSystem.alpha = 0.92;
      lineSystem.isPickable = false;
      lineSystem.renderingGroupId = 3;
      lineSystem.metadata = {
        ...(lineSystem.metadata ?? {}),
        asukaAssemblyEnvironment: true,
        asukaAssemblyDimension: true,
      };
      assemblyEnvironmentMeshes.push(lineSystem);
    };
    const addFloorDimensions = (surface: AsukaAssemblyFloorSurface): void => {
      if (
        settings.visualization.dimensions === false
        || surface.id !== assemblyBuildSelectedSurfaceId
        || assemblyBuildState?.tool !== 'select'
      ) return;
      const points = surface.points;
      const outwardSign = getAssemblyPolygonSignedArea(points) >= 0 ? -1 : 1;
      const offset = Math.max(0.14, settings.grid.size * 0.9);
      const y = (Number(surface.y ?? floorY) || floorY) + 0.07;
      points.forEach((start, index) => {
        const end = points[(index + 1) % points.length];
        const dx = Number(end.x) - Number(start.x);
        const dz = Number(end.z) - Number(start.z);
        const length = Math.hypot(dx, dz);
        if (length <= 0.0001) return;
        const normal = {
          x: (-dz / length) * outwardSign,
          z: (dx / length) * outwardSign,
        };
        const offsetStart = new Vector3(
          Number(start.x) + normal.x * offset,
          y,
          Number(start.z) + normal.z * offset,
        );
        const offsetEnd = new Vector3(
          Number(end.x) + normal.x * offset,
          y,
          Number(end.z) + normal.z * offset,
        );
        addAssemblyDimensionLines(
          `asuka-assembly-floor-dimension-${surface.id}-${index}`,
          [
            [new Vector3(Number(start.x), y, Number(start.z)), offsetStart],
            [offsetStart, offsetEnd],
            [new Vector3(Number(end.x), y, Number(end.z)), offsetEnd],
          ],
        );
        addAssemblyDimensionLabel(
          formatAssemblyDimension(length),
          new Vector3(
            (offsetStart.x + offsetEnd.x) * 0.5,
            y + 0.13,
            (offsetStart.z + offsetEnd.z) * 0.5,
          ),
        );
      });
    };
    const addWallDimensions = (surface: AsukaAssemblyWallSurface): void => {
      if (
        settings.visualization.dimensions === false
        || surface.id !== assemblyBuildSelectedSurfaceId
        || assemblyBuildState?.tool !== 'select'
      ) return;
      const start = surface.start;
      const end = surface.end;
      const dx = Number(end.x) - Number(start.x);
      const dz = Number(end.z) - Number(start.z);
      const length = Math.hypot(dx, dz);
      if (length <= 0.0001) return;
      const height = Math.max(0.05, Number(surface.height ?? wallHeight) || wallHeight);
      const base = Number(surface.baseY ?? floorY) || floorY;
      const tangent = { x: dx / length, z: dz / length };
      const normal = { x: -tangent.z, z: tangent.x };
      const lineY = base + height + 0.14;
      const startTop = new Vector3(Number(start.x), lineY, Number(start.z));
      const endTop = new Vector3(Number(end.x), lineY, Number(end.z));
      addAssemblyDimensionLines(
        `asuka-assembly-wall-length-${surface.id}`,
        [[startTop, endTop]],
      );
      addAssemblyDimensionLabel(
        formatAssemblyDimension(length),
        new Vector3(
          (startTop.x + endTop.x) * 0.5,
          lineY + 0.13,
          (startTop.z + endTop.z) * 0.5,
        ),
      );
      const heightX = Number(end.x) + normal.x * 0.18;
      const heightZ = Number(end.z) + normal.z * 0.18;
      addAssemblyDimensionLines(
        `asuka-assembly-wall-height-${surface.id}`,
        [[
          new Vector3(heightX, base, heightZ),
          new Vector3(heightX, base + height, heightZ),
        ]],
      );
      addAssemblyDimensionLabel(
        formatAssemblyDimension(height),
        new Vector3(heightX, base + height * 0.5, heightZ),
      );
    };

    // Drawn floors and walls are physical product surfaces. The generated
    // legacy room stays out of a fresh construction workspace.
    const showGeneratedEnvironment = visualization.environment
      && !assemblyConstructionModeActive;
    const surfaceHandleScale = (() => {
      try {
        if (window.matchMedia?.('(pointer: coarse)').matches) return 1.6;
        if (Number(navigator.maxTouchPoints) > 0) return 1.25;
      } catch {}
      return 1;
    })();
    if (showGeneratedEnvironment || explicitSurfaces) {
      const surfacePickable = assemblyBuildState?.tool === 'select' || assemblyBuildState?.tool === 'erase';
      const finishSurfaceMesh = (mesh: Mesh, surface: AsukaAssemblySurface): void => {
        mesh.isPickable = surfacePickable;
        mesh.metadata = {
          ...(mesh.metadata ?? {}),
          asukaAssemblyEnvironment: true,
          asukaAssemblySurface: true,
          kind: surface.kind,
          surfaceId: surface.id,
        };
        if (surface.id === assemblyBuildSelectedSurfaceId) {
          mesh.renderOutline = true;
          mesh.outlineColor = color3FromInput('#2563eb');
          mesh.outlineWidth = 0.035;
        }
        assemblyEnvironmentMeshes.push(mesh);
      };
      let surfaceHandleMaterial: StandardMaterial | null = null;
      let surfaceEdgeHandleMaterial: StandardMaterial | null = null;
      let surfaceMoveHandleMaterial: StandardMaterial | null = null;
      let surfaceRotateHandleMaterial: StandardMaterial | null = null;
      const getSurfaceHandleMaterial = (
        kind: 'point' | 'edge' | 'move' | 'rotate',
      ): StandardMaterial => {
        const create = (name: string, color: string): StandardMaterial => {
          const material = makeEnvironmentMaterial(name, color, 0.96);
          material.disableLighting = true;
          material.emissiveColor = color3FromInput(color);
          return material;
        };
        if (kind === 'point') {
          surfaceHandleMaterial ??= create(
            'asuka-assembly-floor-point-handle-material',
            '#2563eb',
          );
          return surfaceHandleMaterial;
        }
        if (kind === 'edge') {
          surfaceEdgeHandleMaterial ??= create(
            'asuka-assembly-surface-edge-handle-material',
            '#60a5fa',
          );
          return surfaceEdgeHandleMaterial;
        }
        if (kind === 'move') {
          surfaceMoveHandleMaterial ??= create(
            'asuka-assembly-surface-move-handle-material',
            '#0f9f8f',
          );
          return surfaceMoveHandleMaterial;
        }
        surfaceRotateHandleMaterial ??= create(
          'asuka-assembly-surface-rotate-handle-material',
          '#7c3aed',
        );
        return surfaceRotateHandleMaterial;
      };
      const markSurfaceGizmo = (
        mesh: Mesh,
        surface: AsukaAssemblySurface,
        handle: 'edge' | 'move' | 'rotate' | 'wall-start' | 'wall-end',
        edgeIndex: number | null = null,
      ): void => {
        mesh.isPickable = true;
        mesh.renderingGroupId = 3;
        mesh.metadata = {
          ...(mesh.metadata ?? {}),
          asukaAssemblyEnvironment: true,
          asukaAssemblySurfaceGizmo: true,
          surfaceId: surface.id,
          handle,
          edgeIndex,
        };
        assemblyEnvironmentMeshes.push(mesh);
      };
      const addSurfaceTransformHandles = (
        surface: AsukaAssemblySurface,
        center: { x: number; z: number },
        radius: number,
        y: number,
      ): void => {
        const gridSize = Math.max(0.01, Number(settings.grid.size) || 0.1);
        const moveDiameter = Math.max(0.09, Math.min(0.2, gridSize)) * surfaceHandleScale;
        const move = MeshBuilder.CreateSphere(
          `asuka-assembly-surface-move-${surface.id}`,
          { diameter: moveDiameter, segments: 12 },
          scene!,
        );
        move.position = new Vector3(center.x, y + moveDiameter * 0.65, center.z);
        move.material = getSurfaceHandleMaterial('move');
        markSurfaceGizmo(move, surface, 'move');

        const ringDiameter = Math.max(0.45, radius * 0.85);
        const ring = MeshBuilder.CreateTorus(
          `asuka-assembly-surface-rotate-${surface.id}`,
          {
            diameter: ringDiameter,
            thickness: Math.max(0.018, Math.min(0.05, ringDiameter * 0.035))
              * surfaceHandleScale,
            tessellation: 48,
          },
          scene!,
        );
        ring.position = new Vector3(center.x, y + 0.035, center.z);
        ring.material = getSurfaceHandleMaterial('rotate');
        markSurfaceGizmo(ring, surface, 'rotate');
      };
      const addFloorPointHandles = (surface: AsukaAssemblyFloorSurface): void => {
        if (
          surface.id !== assemblyBuildSelectedSurfaceId
          || assemblyBuildState?.tool !== 'select'
        ) return;
        const gridSize = Math.max(0.01, Number(settings.grid.size) || 0.1);
        const diameter = Math.max(0.07, Math.min(0.18, gridSize * 0.85))
          * surfaceHandleScale;
        const transform = getAssemblySurfaceTransform(surface);
        const floorYValue = Number(surface.y ?? floorY) || floorY;
        for (const [pointIndex, point] of surface.points.entries()) {
          const handle = MeshBuilder.CreateSphere(
            `asuka-assembly-floor-point-${surface.id}-${pointIndex}`,
            { diameter, segments: 12 },
            scene!,
          );
          handle.position = new Vector3(
            Number(point.x) || 0,
            floorYValue + diameter * 0.58,
            Number(point.z) || 0,
          );
          handle.material = getSurfaceHandleMaterial('point');
          handle.isPickable = true;
          handle.renderingGroupId = 3;
          handle.metadata = {
            ...(handle.metadata ?? {}),
            asukaAssemblyEnvironment: true,
            asukaAssemblySurfaceHandle: true,
            surfaceId: surface.id,
            pointIndex,
          };
          assemblyEnvironmentMeshes.push(handle);

          const next = surface.points[(pointIndex + 1) % surface.points.length];
          const edgeHandle = MeshBuilder.CreateBox(
            `asuka-assembly-floor-edge-${surface.id}-${pointIndex}`,
            { size: diameter * 0.72 },
            scene!,
          );
          edgeHandle.position = new Vector3(
            (Number(point.x) + Number(next.x)) * 0.5,
            floorYValue + diameter * 0.48,
            (Number(point.z) + Number(next.z)) * 0.5,
          );
          edgeHandle.rotation.y = -Math.atan2(
            Number(next.z) - Number(point.z),
            Number(next.x) - Number(point.x),
          );
          edgeHandle.material = getSurfaceHandleMaterial('edge');
          markSurfaceGizmo(edgeHandle, surface, 'edge', pointIndex);
        }
        if (transform) {
          const radius = Math.max(
            ...surface.points.map((point) => Math.hypot(
              Number(point.x) - transform.center.x,
              Number(point.z) - transform.center.z,
            )),
            0.25,
          );
          addSurfaceTransformHandles(surface, transform.center, radius, floorYValue);
        }
      };

      const addWallHandles = (surface: AsukaAssemblyWallSurface): void => {
        if (
          surface.id !== assemblyBuildSelectedSurfaceId
          || assemblyBuildState?.tool !== 'select'
        ) return;
        const gridSize = Math.max(0.01, Number(settings.grid.size) || 0.1);
        const diameter = Math.max(0.08, Math.min(0.18, gridSize * 0.9))
          * surfaceHandleScale;
        const baseY = Number(surface.baseY ?? floorY) || floorY;
        const transform = getAssemblySurfaceTransform(surface);
        for (const [handle, point] of [
          ['wall-start', surface.start],
          ['wall-end', surface.end],
        ] as const) {
          const marker = MeshBuilder.CreateSphere(
            `asuka-assembly-${handle}-${surface.id}`,
            { diameter, segments: 12 },
            scene!,
          );
          marker.position = new Vector3(
            Number(point.x) || 0,
            baseY + diameter * 0.58,
            Number(point.z) || 0,
          );
          marker.material = getSurfaceHandleMaterial('point');
          markSurfaceGizmo(marker, surface, handle);
        }
        if (transform) {
          const radius = Math.max(
            0.25,
            Math.hypot(
              Number(surface.end.x) - Number(surface.start.x),
              Number(surface.end.z) - Number(surface.start.z),
            ) * 0.65,
          );
          addSurfaceTransformHandles(surface, transform.center, radius, baseY);
        }
      };

      if (explicitSurfaces) {
      for (const [index, surface] of (plan.floors ?? []).entries()) {
        const thickness = Math.max(0.003, Number(surface.thickness ?? 0.012) || 0.012);
        const floor = createAssemblyPolygonMesh(
          `asuka-assembly-floor-${surface.id || index}`,
          surface.points,
          thickness,
        );
        if (!floor) continue;
        floor.position.y = Number(surface.y ?? floorY) || floorY;
        const floorMinX = Math.min(...surface.points.map((point) => Number(point.x) || 0));
        const floorMaxX = Math.max(...surface.points.map((point) => Number(point.x) || 0));
        const floorMinZ = Math.min(...surface.points.map((point) => Number(point.z) || 0));
        const floorMaxZ = Math.max(...surface.points.map((point) => Number(point.z) || 0));
        const floorSize = {
          width: Math.max(0.001, floorMaxX - floorMinX),
          height: Math.max(0.001, floorMaxZ - floorMinZ),
        };
        const floorStructureMaterial = surface.material
          ? { ...surface.material, textureUrl: undefined }
          : surface.material;
        floor.material = makeSurfaceMaterial(
          `asuka-assembly-floor-material-${surface.id || index}`,
          floorStructureMaterial,
          String(plan.floorColor ?? '#f6f8fb'),
          1,
          floorSize,
        );
        finishSurfaceMesh(floor, surface);
        if (String(surface.material?.textureUrl ?? '').trim()) {
          const topFinish = createAssemblyPolygonTopMesh(
            `asuka-assembly-floor-top-finish-${surface.id || index}`,
            surface.points,
          );
          if (topFinish) {
            topFinish.position.y = floor.position.y
              + Math.max(0.0015, Math.min(0.006, thickness * 0.08));
            const topMaterial = makeSurfaceMaterial(
              `asuka-assembly-floor-top-material-${surface.id || index}`,
              surface.material,
              String(plan.floorColor ?? '#f6f8fb'),
              1,
              floorSize,
            );
            topMaterial.backFaceCulling = true;
            topFinish.material = topMaterial;
            topFinish.isPickable = false;
            topFinish.metadata = {
              ...(topFinish.metadata ?? {}),
              asukaAssemblyEnvironment: true,
              asukaAssemblySurfaceFinish: true,
              surfaceId: surface.id,
              kind: surface.kind,
              face: 'top',
            };
            assemblyEnvironmentMeshes.push(topFinish);
          }
        }
        addFloorPointHandles(surface);
        addFloorDimensions(surface);
      }
      for (const [index, surface] of (plan.walls ?? []).entries()) {
        const dx = Number(surface.end?.x ?? 0) - Number(surface.start?.x ?? 0);
        const dz = Number(surface.end?.z ?? 0) - Number(surface.start?.z ?? 0);
        const length = Math.sqrt(dx * dx + dz * dz);
        if (length <= 0.0001) continue;
        const height = Math.max(0.05, Number(surface.height ?? wallHeight) || wallHeight);
        const thickness = Math.max(0.005, Number(surface.thickness ?? wallThickness) || wallThickness);
        const leftNormal = { x: -dz / length, z: dx / length };
        const interiorSide = surface.metadata?.interiorSide === 'right' ? 'right' : 'left';
        const interiorSign = interiorSide === 'left' ? 1 : -1;
        const alignToFloorEdge = surface.metadata?.alignToFloorEdge === true;
        const centerOffset = alignToFloorEdge ? -interiorSign * thickness * 0.5 : 0;
        const centerX = (
          Number(surface.start?.x ?? 0)
          + Number(surface.end?.x ?? 0)
        ) * 0.5 + leftNormal.x * centerOffset;
        const centerZ = (
          Number(surface.start?.z ?? 0)
          + Number(surface.end?.z ?? 0)
        ) * 0.5 + leftNormal.z * centerOffset;
        const renderedLength = length + (alignToFloorEdge ? thickness : 0);
        const rotationY = -Math.atan2(dz, dx);
        const baseY = Number(surface.baseY ?? floorY) || floorY;
        const linkedFloorId = String(surface.metadata?.roomSurfaceId ?? '').trim();
        const linkedFloor = alignToFloorEdge && linkedFloorId
          ? (plan.floors ?? []).find((floorSurface) => floorSurface.id === linkedFloorId) ?? null
          : null;
        const footprint = linkedFloor
          ? getAssemblyRoomWallFootprint(linkedFloor, surface, plan.walls ?? [])
          : null;
        const wall = footprint
          ? createAssemblyWallPrism(
              `asuka-assembly-wall-${surface.id || index}`,
              footprint,
              height,
            )
          : MeshBuilder.CreateBox(`asuka-assembly-wall-${surface.id || index}`, {
              width: renderedLength,
              height,
              depth: thickness,
            }, scene);
        if (!wall) continue;
        if (footprint) {
          wall.position.y = baseY;
        } else {
          wall.position = new Vector3(
            centerX,
            baseY + height * 0.5,
            centerZ,
          );
          wall.rotation.y = rotationY;
        }
        wall.material = makeSurfaceMaterial(
          `asuka-assembly-wall-material-${surface.id || index}`,
          surface.material,
          String(plan.wallColor ?? '#e7ebf1'),
          1,
          {
            width: footprint
              ? Math.max(
                  length,
                  Math.hypot(
                    footprint.exteriorEnd.x - footprint.exteriorStart.x,
                    footprint.exteriorEnd.z - footprint.exteriorStart.z,
                  ),
                )
              : renderedLength,
            height,
          },
        );
        wall.metadata = {
          ...(wall.metadata ?? {}),
          wallFace: 'structure',
          miteredStart: footprint?.miteredStart === true,
          miteredEnd: footprint?.miteredEnd === true,
        };
        finishSurfaceMesh(wall, surface);

        const addWallFinish = (
          face: 'interior' | 'exterior',
          sideSign: number,
          materialInput: AsukaAssemblySurfaceMaterial | null | undefined,
        ): void => {
          const isInterior = face === 'interior';
          const normalSide = sideSign > 0 ? 'left' : 'right';
          const normal = {
            x: leftNormal.x * sideSign,
            z: leftNormal.z * sideSign,
          };
          const finishStart = footprint
            ? (isInterior ? footprint.interiorStart : footprint.exteriorStart)
            : null;
          const finishEnd = footprint
            ? (isInterior ? footprint.interiorEnd : footprint.exteriorEnd)
            : null;
          const finishOffset = Math.max(0.0015, Math.min(0.006, thickness * 0.04));
          const finishLength = finishStart && finishEnd
            ? Math.max(0.0001, Math.hypot(
                finishEnd.x - finishStart.x,
                finishEnd.z - finishStart.z,
              ))
            : renderedLength;
          const plane = finishStart && finishEnd
            ? createAssemblyWallFinishMesh(
                `asuka-assembly-wall-${face}-${surface.id || index}`,
                {
                  x: finishStart.x + normal.x * finishOffset,
                  z: finishStart.z + normal.z * finishOffset,
                },
                {
                  x: finishEnd.x + normal.x * finishOffset,
                  z: finishEnd.z + normal.z * finishOffset,
                },
                height,
                normalSide,
              )
            : MeshBuilder.CreatePlane(
                `asuka-assembly-wall-${face}-${surface.id || index}`,
                {
                  width: renderedLength,
                  height,
                  sideOrientation: Mesh.DOUBLESIDE,
                },
                scene,
              );
          if (!plane) return;
          if (finishStart && finishEnd) {
            plane.position.y = baseY;
          } else {
            const faceOffset = sideSign * (thickness * 0.5 + finishOffset);
            plane.position = new Vector3(
              centerX + leftNormal.x * faceOffset,
              baseY + height * 0.5,
              centerZ + leftNormal.z * faceOffset,
            );
            // Babylon planes face local -Z. Turn the left-side finish so each
            // wall face presents its front UVs to the side it represents.
            plane.rotation.y = rotationY + (sideSign > 0 ? Math.PI : 0);
          }
          plane.material = makeSurfaceMaterial(
            `asuka-assembly-wall-${face}-material-${surface.id || index}`,
            materialInput ?? surface.material,
            String(plan.wallColor ?? '#e7ebf1'),
            1,
            { width: finishLength, height },
          );
          plane.metadata = { ...(plane.metadata ?? {}), wallFace: face };
          finishSurfaceMesh(plane, surface);
        };

        addWallFinish('interior', interiorSign, surface.interiorMaterial);
        addWallFinish('exterior', -interiorSign, surface.exteriorMaterial);
        addWallHandles(surface);
        addWallDimensions(surface);
      }
      } else {
      const floorMaterial = makeEnvironmentMaterial(
        'asuka-assembly-floor-material',
        String(plan.floorColor ?? '#f6f8fb'),
        0.62,
      );
      const wallMaterial = makeEnvironmentMaterial(
        'asuka-assembly-wall-material',
        String(plan.wallColor ?? '#e7ebf1'),
        0.28,
      );
      const floor = MeshBuilder.CreateBox('asuka-assembly-floor', {
        width: Math.max(0.01, bounds.size.x),
        height: 0.012,
        depth: Math.max(0.01, bounds.size.z),
      }, scene);
      floor.position = new Vector3(bounds.center.x, floorY - 0.006, bounds.center.z);
      floor.material = floorMaterial;
      floor.isPickable = false;
      floor.metadata = { ...(floor.metadata ?? {}), asukaAssemblyEnvironment: true, kind: 'floor' };
      assemblyEnvironmentMeshes.push(floor);

      const points = plan.points;
      const segmentCount = plan.closed === false ? Math.max(0, points.length - 1) : points.length;
      for (let index = 0; index < segmentCount; index += 1) {
        const start = points[index];
        const end = points[(index + 1) % points.length];
        const dx = Number(end?.x ?? 0) - Number(start?.x ?? 0);
        const dz = Number(end?.z ?? 0) - Number(start?.z ?? 0);
        const length = Math.sqrt(dx * dx + dz * dz);
        if (length <= 0.0001) continue;
        const wall = MeshBuilder.CreateBox(`asuka-assembly-wall-${index}`, {
          width: length,
          height: wallHeight,
          depth: wallThickness,
        }, scene);
        wall.position = new Vector3(
          (Number(start?.x ?? 0) + Number(end?.x ?? 0)) * 0.5,
          floorY + wallHeight * 0.5,
          (Number(start?.z ?? 0) + Number(end?.z ?? 0)) * 0.5,
        );
        wall.rotation.y = -Math.atan2(dz, dx);
        wall.material = wallMaterial;
        wall.isPickable = false;
        wall.metadata = { ...(wall.metadata ?? {}), asukaAssemblyEnvironment: true, kind: 'wall', segmentIndex: index };
        assemblyEnvironmentMeshes.push(wall);
      }
      }

    }

    if (
      assemblyConstructionModeActive
      && assemblyMoveToolActive
      && canEditAssemblyRuntime()
    ) {
      const selectedComponentId = getSelectedProductId();
      const selectedComponent = listAssemblyComponents().find(
        (component) => component.id === selectedComponentId,
      ) ?? null;
      if (selectedComponent) {
        const radius = Math.max(
          0.28,
          Math.max(selectedComponent.bounds.size.x, selectedComponent.bounds.size.z) * 0.68,
        );
        const rotationMaterial = makeEnvironmentMaterial(
          'asuka-assembly-component-rotation-gizmo-material',
          '#7c3aed',
          0.92,
        );
        rotationMaterial.disableLighting = true;
        rotationMaterial.emissiveColor = color3FromInput('#7c3aed');
        const ring = MeshBuilder.CreateTorus(
          `asuka-assembly-component-rotation-gizmo-${selectedComponent.id}`,
          {
            diameter: radius * 2,
            thickness: Math.max(0.025, Math.min(0.07, radius * 0.09)),
            tessellation: 56,
          },
          scene,
        );
        ring.position = new Vector3(
          selectedComponent.bounds.center.x,
          selectedComponent.bounds.min.y + 0.035,
          selectedComponent.bounds.center.z,
        );
        ring.material = rotationMaterial;
        ring.isPickable = true;
        ring.renderingGroupId = 3;
        ring.metadata = {
          ...(ring.metadata ?? {}),
          asukaAssemblyEnvironment: true,
          asukaAssemblyComponentRotationGizmo: true,
          componentId: selectedComponent.id,
        };
        assemblyEnvironmentMeshes.push(ring);
      }
    }

    if (assemblyComponentDragState?.lastResult) {
      const drag = assemblyComponentDragState;
      const result = drag.lastResult;
      if (!result) return;
      const currentComponent = listAssemblyComponents().find(
        (component) => component.id === drag.componentId,
      ) ?? null;
      const originalPosition = drag.originalTransform.position ?? { x: 0, y: 0, z: 0 };
      const requestedPosition = result.requested.position ?? originalPosition;
      const originalRotation = drag.originalTransform.rotation ?? { x: 0, y: 0, z: 0 };
      const requestedRotation = result.requested.rotation ?? originalRotation;
      const yawDelta = Number(requestedRotation.y ?? 0) - Number(originalRotation.y ?? 0);
      const halfX = drag.originalBounds.size.x * 0.5;
      const halfZ = drag.originalBounds.size.z * 0.5;
      const cosine = Math.abs(Math.cos(yawDelta));
      const sine = Math.abs(Math.sin(yawDelta));
      const previewSize = result.accepted && currentComponent
        ? currentComponent.bounds.size
        : {
            x: halfX * cosine * 2 + halfZ * sine * 2,
            y: drag.originalBounds.size.y,
            z: halfX * sine * 2 + halfZ * cosine * 2,
          };
      const previewCenter = result.accepted && currentComponent
        ? currentComponent.bounds.center
        : {
            x: drag.originalBounds.center.x
              + Number(requestedPosition.x ?? 0)
              - Number(originalPosition.x ?? 0),
            y: drag.originalBounds.center.y
              + Number(requestedPosition.y ?? 0)
              - Number(originalPosition.y ?? 0),
            z: drag.originalBounds.center.z
              + Number(requestedPosition.z ?? 0)
              - Number(originalPosition.z ?? 0),
          };
      const ghostColor = result.accepted ? '#10b981' : '#ef4444';
      const ghostMaterial = makeEnvironmentMaterial(
        'asuka-assembly-placement-ghost-material',
        ghostColor,
        0.2,
      );
      ghostMaterial.disableLighting = true;
      ghostMaterial.emissiveColor = color3FromInput(ghostColor);
      ghostMaterial.wireframe = true;
      ghostMaterial.disableDepthWrite = true;
      const ghost = MeshBuilder.CreateBox(
        `asuka-assembly-placement-ghost-${drag.componentId}`,
        {
          width: Math.max(0.001, previewSize.x),
          height: Math.max(0.001, previewSize.y),
          depth: Math.max(0.001, previewSize.z),
        },
        scene,
      );
      ghost.position = new Vector3(previewCenter.x, previewCenter.y, previewCenter.z);
      ghost.material = ghostMaterial;
      ghost.isPickable = false;
      ghost.renderingGroupId = 3;
      ghost.metadata = {
        ...(ghost.metadata ?? {}),
        asukaAssemblyEnvironment: true,
        asukaAssemblyPlacementGhost: true,
        componentId: drag.componentId,
        accepted: result.accepted,
        snapKind: result.snap?.kind ?? null,
      };
      assemblyEnvironmentMeshes.push(ghost);
    }

    if (assembly.settings?.grid?.enabled !== false) {
      const size = Math.max(0.02, Number(assembly.settings?.grid?.size ?? 0.1) || 0.1);
      const maxLines = 900;
      const lines: Vector3[][] = [];
      const pushLine = (start: Vector3, end: Vector3): boolean => {
        if (lines.length >= maxLines) return false;
        lines.push([start, end]);
        return true;
      };

      const floorRegions = (plan.floors?.length ?? 0) > 0
        ? (plan.floors ?? []).map((surface) => ({
            minX: Math.min(...surface.points.map((point) => Number(point.x) || 0)),
            maxX: Math.max(...surface.points.map((point) => Number(point.x) || 0)),
            minZ: Math.min(...surface.points.map((point) => Number(point.z) || 0)),
            maxZ: Math.max(...surface.points.map((point) => Number(point.z) || 0)),
            y: Number(surface.y ?? floorY) || floorY,
          }))
        : [{
            minX: bounds.min.x,
            maxX: bounds.max.x,
            minZ: bounds.min.z,
            maxZ: bounds.max.z,
            y: floorY,
          }];
      for (const region of floorRegions) {
        for (let x = region.minX; x <= region.maxX + 0.0001 && lines.length < maxLines; x += size) {
          pushLine(
            new Vector3(x, region.y + 0.008, region.minZ),
            new Vector3(x, region.y + 0.008, region.maxZ),
          );
        }
        for (let z = region.minZ; z <= region.maxZ + 0.0001 && lines.length < maxLines; z += size) {
          pushLine(
            new Vector3(region.minX, region.y + 0.008, z),
            new Vector3(region.maxX, region.y + 0.008, z),
          );
        }
      }

      const wallGridSurfaces = Array.isArray(plan.walls)
        ? plan.walls.map((wall) => ({
            start: wall.start,
            end: wall.end,
            baseY: Number(wall.baseY ?? floorY) || floorY,
            height: Math.max(0.05, Number(wall.height ?? wallHeight) || wallHeight),
            thickness: Math.max(0.005, Number(wall.thickness ?? wallThickness) || wallThickness),
          }))
        : assemblyConstructionModeActive
          ? []
          : Array.from({
            length: plan.closed === false
              ? Math.max(0, plan.points.length - 1)
              : plan.points.length,
            }, (_, index) => ({
              start: plan.points[index],
              end: plan.points[(index + 1) % plan.points.length],
              baseY: floorY,
              height: wallHeight,
              thickness: wallThickness,
            }));
      for (const wall of wallGridSurfaces) {
        const dx = Number(wall.end.x) - Number(wall.start.x);
        const dz = Number(wall.end.z) - Number(wall.start.z);
        const length = Math.hypot(dx, dz);
        if (length <= 0.0001) continue;
        const tangentX = dx / length;
        const tangentZ = dz / length;
        const normalX = -tangentZ;
        const normalZ = tangentX;
        const faceOffset = wall.thickness * 0.5 + 0.003;
        for (const side of [-1, 1]) {
          const offsetX = normalX * faceOffset * side;
          const offsetZ = normalZ * faceOffset * side;
          for (let along = 0; along <= length + 0.0001 && lines.length < maxLines; along += size) {
            const x = Number(wall.start.x) + tangentX * Math.min(along, length) + offsetX;
            const z = Number(wall.start.z) + tangentZ * Math.min(along, length) + offsetZ;
            pushLine(
              new Vector3(x, wall.baseY, z),
              new Vector3(x, wall.baseY + wall.height, z),
            );
          }
          for (let height = 0; height <= wall.height + 0.0001 && lines.length < maxLines; height += size) {
            const y = wall.baseY + Math.min(height, wall.height);
            pushLine(
              new Vector3(Number(wall.start.x) + offsetX, y, Number(wall.start.z) + offsetZ),
              new Vector3(Number(wall.end.x) + offsetX, y, Number(wall.end.z) + offsetZ),
            );
          }
        }
      }

      if (lines.length) {
        const grid = MeshBuilder.CreateLineSystem('asuka-assembly-grid', { lines }, scene);
        grid.color = color3FromInput(String(plan.gridColor ?? '#cbd5e1')).scale(0.72);
        grid.alpha = 0.26;
        grid.isPickable = false;
        grid.metadata = {
          ...(grid.metadata ?? {}),
          asukaAssemblyEnvironment: true,
          kind: 'grid',
          floorGrid: true,
          wallGrid: true,
        };
        assemblyEnvironmentMeshes.push(grid);
      }
    }

    if (visualization.enabled) {
      const components = listAssemblyComponents();
      const validation = validateAssembly(assembly, components);
      const selectedComponentId = String(getSelectedProductId() ?? '').trim();
      const selectedComponent = components.find((component) => component.id === selectedComponentId) ?? null;
      const detailedComponents = visualization.selectedOnly && selectedComponent
        ? [selectedComponent]
        : components;
      const collidingIds = new Set(validation.collisions.flatMap((entry) => [
        entry.componentId,
        entry.otherComponentId,
      ]));
      const clearanceIssueIds = new Set(
        validation.issues
          .filter((entry) => entry.code === 'clearance')
          .flatMap((entry) => entry.componentIds),
      );

      const makeGuideMaterial = (name: string, color: string, alpha: number): StandardMaterial => {
        const material = makeEnvironmentMaterial(name, color, alpha);
        material.disableLighting = true;
        material.emissiveColor = color3FromInput(color);
        material.disableDepthWrite = true;
        return material;
      };
      const anchorAvailableMaterial = makeGuideMaterial('asuka-assembly-anchor-available', '#0f9f8f', 0.96);
      const anchorCompatibleMaterial = makeGuideMaterial('asuka-assembly-anchor-compatible', '#f59e0b', 0.98);
      const anchorConnectedMaterial = makeGuideMaterial('asuka-assembly-anchor-connected', '#2563eb', 0.98);
      const collisionMaterial = makeGuideMaterial('asuka-assembly-collision-volume', '#14b8a6', 0.12);
      const collisionInvalidMaterial = makeGuideMaterial('asuka-assembly-collision-invalid', '#ef4444', 0.2);
      const clearanceMaterial = makeGuideMaterial('asuka-assembly-clearance-volume', '#f59e0b', 0.13);
      const clearanceInvalidMaterial = makeGuideMaterial('asuka-assembly-clearance-invalid', '#ef4444', 0.18);
      collisionMaterial.wireframe = true;
      collisionInvalidMaterial.wireframe = true;
      clearanceMaterial.wireframe = true;
      clearanceInvalidMaterial.wireframe = true;

      const addGuideBox = (
        name: string,
        guideBounds: AsukaAssemblyBounds,
        material: StandardMaterial,
        kind: string,
        componentId: string,
        detailId?: string,
      ): void => {
        const box = MeshBuilder.CreateBox(name, {
          width: Math.max(0.001, guideBounds.size.x),
          height: Math.max(0.001, guideBounds.size.y),
          depth: Math.max(0.001, guideBounds.size.z),
        }, scene!);
        box.position = new Vector3(guideBounds.center.x, guideBounds.center.y, guideBounds.center.z);
        box.material = material;
        box.isPickable = false;
        box.renderingGroupId = 3;
        box.metadata = {
          ...(box.metadata ?? {}),
          asukaAssemblyEnvironment: true,
          asukaAssemblyGuide: true,
          kind,
          componentId,
          detailId,
        };
        assemblyEnvironmentMeshes.push(box);
      };

      if (visualization.collisions) {
        for (const component of detailedComponents) {
          if (component.definition?.collision?.enabled === false) continue;
          addGuideBox(
            `asuka-assembly-collision-${component.id}`,
            getAssemblyGuideBounds(component),
            collidingIds.has(component.id) ? collisionInvalidMaterial : collisionMaterial,
            'collision',
            component.id,
          );
        }
      }

      if (visualization.clearances) {
        for (const component of detailedComponents) {
          for (const clearance of component.definition?.clearances ?? []) {
            addGuideBox(
              `asuka-assembly-clearance-${component.id}-${clearance.id}`,
              getAssemblyClearanceGuideBounds(component, clearance),
              clearanceIssueIds.has(component.id) ? clearanceInvalidMaterial : clearanceMaterial,
              'clearance',
              component.id,
              clearance.id,
            );
          }
        }
      }

      if (visualization.anchors) {
        const selectedAnchors = selectedComponent?.anchors ?? [];
        for (const component of components) {
          const isDetailed = detailedComponents.some((entry) => entry.id === component.id);
          for (const anchor of component.anchors) {
            const compatibleTarget = !isDetailed
              && selectedAnchors.some((source) => getCompatibleAssemblyConnectorType(source, anchor) != null);
            if (!isDetailed && !compatibleTarget) continue;
            const connected = anchor.connectionCount > 0;
            const material = connected
              ? anchorConnectedMaterial
              : (compatibleTarget ? anchorCompatibleMaterial : anchorAvailableMaterial);
            const componentExtent = Math.max(
              component.bounds.size.x,
              component.bounds.size.y,
              component.bounds.size.z,
            );
            const diameter = Math.min(0.14, Math.max(0.045, componentExtent * (compatibleTarget ? 0.035 : 0.05)));
            const sphere = MeshBuilder.CreateSphere(
              `asuka-assembly-anchor-${component.id}-${anchor.id}`,
              { diameter, segments: 12 },
              scene!,
            );
            sphere.position = new Vector3(anchor.position.x, anchor.position.y, anchor.position.z);
            sphere.material = material;
            sphere.isPickable = false;
            sphere.renderingGroupId = 3;
            sphere.metadata = {
              ...(sphere.metadata ?? {}),
              asukaAssemblyEnvironment: true,
              asukaAssemblyGuide: true,
              kind: 'anchor',
              componentId: component.id,
              anchorId: anchor.id,
              compatibleTarget,
              connected,
            };
            assemblyEnvironmentMeshes.push(sphere);

            const normalLength = diameter * 2.4;
            const normalEnd = new Vector3(
              anchor.position.x + anchor.normal.x * normalLength,
              anchor.position.y + anchor.normal.y * normalLength,
              anchor.position.z + anchor.normal.z * normalLength,
            );
            const normal = MeshBuilder.CreateLineSystem(
              `asuka-assembly-anchor-normal-${component.id}-${anchor.id}`,
              {
                lines: [[
                  new Vector3(anchor.position.x, anchor.position.y, anchor.position.z),
                  normalEnd,
                ]],
              },
              scene!,
            );
            normal.color = connected
              ? color3FromInput('#2563eb')
              : color3FromInput(compatibleTarget ? '#f59e0b' : '#0f9f8f');
            normal.alpha = 0.95;
            normal.isPickable = false;
            normal.renderingGroupId = 3;
            normal.metadata = {
              ...(normal.metadata ?? {}),
              asukaAssemblyEnvironment: true,
              asukaAssemblyGuide: true,
              kind: 'anchor-normal',
              componentId: component.id,
              anchorId: anchor.id,
            };
            assemblyEnvironmentMeshes.push(normal);
          }
        }
      }

      if (visualization.connections && (assembly.connections?.length ?? 0) > 0) {
        const anchorsByKey = new Map<string, AsukaAssemblyComponent['anchors'][number]>();
        for (const component of components) {
          for (const anchor of component.anchors) {
            anchorsByKey.set(`${component.id}:${anchor.id}`, anchor);
          }
        }
        const connectionLines = (assembly.connections ?? []).flatMap((connection) => {
          const source = anchorsByKey.get(`${connection.source.componentId}:${connection.source.anchorId}`);
          const target = anchorsByKey.get(`${connection.target.componentId}:${connection.target.anchorId}`);
          return source && target
            ? [[
                new Vector3(source.position.x, source.position.y, source.position.z),
                new Vector3(target.position.x, target.position.y, target.position.z),
              ]]
            : [];
        });
        if (connectionLines.length) {
          const connections = MeshBuilder.CreateLineSystem(
            'asuka-assembly-connections',
            { lines: connectionLines },
            scene!,
          );
          connections.color = color3FromInput('#2563eb');
          connections.alpha = 0.9;
          connections.isPickable = false;
          connections.renderingGroupId = 3;
          connections.metadata = {
            ...(connections.metadata ?? {}),
            asukaAssemblyEnvironment: true,
            asukaAssemblyGuide: true,
            kind: 'connections',
          };
          assemblyEnvironmentMeshes.push(connections);
        }
      }
    }
    try {
      if (canvas) {
        syncAssemblyTextureDiagnostic();
        const handles: Record<string, number> = {};
        const surfaceGizmos = assemblyEnvironmentMeshes.filter((mesh) => (
          mesh.metadata?.asukaAssemblySurfaceHandle === true
          || mesh.metadata?.asukaAssemblySurfaceGizmo === true
        ));
        for (const mesh of surfaceGizmos) {
          const handle = mesh.metadata?.asukaAssemblySurfaceHandle === true
            ? 'point'
            : String(mesh.metadata?.handle ?? 'unknown');
          handles[handle] = (handles[handle] ?? 0) + 1;
        }
        canvas.dataset.asukaAssemblySurfaceGizmos = JSON.stringify({
          surfaceId: assemblyBuildSelectedSurfaceId || null,
          total: surfaceGizmos.length,
          pointerScale: surfaceHandleScale,
          handles,
        });
        canvas.dataset.asukaAssemblyDimensions = JSON.stringify({
          surfaceId: assemblyBuildSelectedSurfaceId || null,
          labels: assemblyDimensionCount,
        });
        canvas.dataset.asukaAssemblyWallJoints = JSON.stringify({
          miteredEnds: assemblyEnvironmentMeshes.filter((mesh) => (
            mesh.metadata?.wallFace === 'structure'
            && (
              mesh.metadata?.miteredStart === true
              || mesh.metadata?.miteredEnd === true
            )
          )).reduce((count, mesh) => (
            count
            + (mesh.metadata?.miteredStart === true ? 1 : 0)
            + (mesh.metadata?.miteredEnd === true ? 1 : 0)
          ), 0),
        });
        const placementGhost = assemblyEnvironmentMeshes.find(
          (mesh) => mesh.metadata?.asukaAssemblyPlacementGhost === true,
        ) ?? null;
        canvas.dataset.asukaAssemblyPlacementGhost = JSON.stringify({
          visible: placementGhost != null,
          componentId: placementGhost?.metadata?.componentId ?? null,
          accepted: placementGhost?.metadata?.accepted ?? null,
          snapKind: placementGhost?.metadata?.snapKind ?? null,
        });
      }
    } catch {}
    try { scene.render(); } catch {}
  };

  const applyAssemblyProductTransform = (
    componentIdInput: string,
    transformInput: AsukaAssemblyTransform,
    optionsInput: { render?: boolean } = {},
  ): boolean => {
    const componentId = String(componentIdInput ?? '').trim();
    const product = getProductById(componentId);
    const meshes = getProductInstanceMeshes(componentId);
    const roots = getProductInstanceRootMeshes(meshes);
    const bounds = getProductInstanceBounds(componentId);
    if (!componentId || !product || !roots.length || !bounds) return false;
    const previous = normalizeAssemblyTransform(product?.transform);
    const next = normalizeAssemblyTransform(transformInput);
    const pivot = bounds.center.clone();
    applyProductTransformDeltaToMeshes(meshes, previous, next, pivot);
    patchProductInState(componentId, (instance: any) => ({
      ...(instance ?? {}),
      transform: next,
    }));
    invalidateProductGroupViewTarget();
    try {
      renderPrintAreaVisuals();
      if (optionsInput.render !== false) renderAssemblyEnvironment();
      else scene?.render();
    } catch {}
    return true;
  };

  const getAssemblyLocalScaleAxisForWorldAxis = (
    component: AsukaAssemblyComponent,
    worldAxis: 'x' | 'z',
  ): 'x' | 'z' => {
    const yaw = Number(component.transform.rotation?.y ?? 0) || 0;
    const localXMostlyFollowsWorldX = Math.abs(Math.cos(yaw)) >= Math.abs(Math.sin(yaw));
    if (worldAxis === 'x') return localXMostlyFollowsWorldX ? 'x' : 'z';
    return localXMostlyFollowsWorldX ? 'z' : 'x';
  };

  const resizeAssemblyStackUpperComponent = (
    upperComponentIdInput: string,
    targetWidthInput: number,
    worldAxis: 'x' | 'z',
  ): boolean => {
    const upperComponentId = String(upperComponentIdInput ?? '').trim();
    const before = listAssemblyComponents().find((component) => component.id === upperComponentId) ?? null;
    const currentWidth = Number(before?.bounds?.size?.[worldAxis] ?? 0) || 0;
    const targetWidth = Math.max(0.0001, Number(targetWidthInput) || currentWidth);
    if (!before || currentWidth <= 0.000001 || Math.abs(currentWidth - targetWidth) <= 0.00001) return false;

    const descriptor = buildParametricDescriptor(upperComponentId);
    const widthParameter = descriptor?.definition.parameters.find((parameter) => (
      (parameter.type === 'number' || parameter.type === 'integer')
      && parameter.measurement?.kind === 'length'
      && parameter.measurement.dimension === 'width'
    )) ?? null;
    if (descriptor && widthParameter) {
      const storedValue = Number(
        descriptor.state.values?.[widthParameter.id]
        ?? widthParameter.default,
      );
      if (Number.isFinite(storedValue) && Math.abs(storedValue) > 0.000001) {
        const nextStoredValue = storedValue * targetWidth / currentWidth;
        const applied = applyParametricProductState(upperComponentId, {
          [widthParameter.id]: widthParameter.type === 'integer'
            ? Math.round(nextStoredValue)
            : nextStoredValue,
        }, {
          source: 'assembly.stack.width',
          commitHistory: false,
          emitChange: false,
          validate: true,
          skipAssemblyCollisionGuard: true,
          skipCameraReframe: true,
          propagateAssemblyConnections: false,
          reconcileAssemblyPlacement: false,
        });
        const afterParametric = listAssemblyComponents().find(
          (component) => component.id === upperComponentId,
        ) ?? null;
        const measuredWidth = Number(afterParametric?.bounds.size?.[worldAxis] ?? 0) || 0;
        const widthTolerance = Math.max(0.0005, targetWidth * 0.005);
        if (applied && afterParametric && Math.abs(measuredWidth - targetWidth) <= widthTolerance) {
          return true;
        }
      }
    }

    const refreshed = listAssemblyComponents().find((component) => component.id === upperComponentId) ?? before;
    const refreshedWidth = Math.max(0.0001, Number(refreshed.bounds.size?.[worldAxis] ?? currentWidth));
    const localAxis = getAssemblyLocalScaleAxisForWorldAxis(refreshed, worldAxis);
    const currentScale = Math.max(0.0001, Number(refreshed.transform.scale?.[localAxis] ?? 1) || 1);
    return applyAssemblyProductTransform(upperComponentId, {
      ...refreshed.transform,
      scale: {
        x: Number(refreshed.transform.scale?.x ?? 1) || 1,
        y: Number(refreshed.transform.scale?.y ?? 1) || 1,
        z: Number(refreshed.transform.scale?.z ?? 1) || 1,
        [localAxis]: Math.max(0.0001, currentScale * targetWidth / refreshedWidth),
      },
    }, { render: false });
  };

  const applyAssemblyStackLink = (link: AsukaAssemblyStackLink): string | null => {
    let components = listAssemblyComponents();
    const lower = components.find((component) => component.id === link.lowerComponentId) ?? null;
    let upper = components.find((component) => component.id === link.upperComponentId) ?? null;
    if (!lower || !upper) return null;

    const target = getAssemblyStackTargetWidth(lower, upper, link);
    if (link.syncWidth !== false) {
      resizeAssemblyStackUpperComponent(upper.id, target.width, target.axis);
      components = listAssemblyComponents();
      upper = components.find((component) => component.id === link.upperComponentId) ?? upper;
    }

    const delta = getAssemblyStackAlignmentDelta(lower, upper, link.gap);
    if (Math.hypot(delta.x, delta.y, delta.z) > 0.000001) {
      const position = upper.transform.position ?? { x: 0, y: 0, z: 0 };
      applyAssemblyProductTransform(upper.id, {
        ...upper.transform,
        position: {
          x: position.x + delta.x,
          y: position.y + delta.y,
          z: position.z + delta.z,
        },
      }, { render: false });
    }
    return upper.id;
  };

  const reflowAssemblyStackLinksFrom = (
    rootComponentIdInput: string,
    optionsInput: { includeIncoming?: boolean } = {},
  ): string[] => {
    const rootComponentId = String(rootComponentIdInput ?? '').trim();
    const links = getAssemblyState().stackLinks ?? [];
    if (!rootComponentId || !links.length) return [];
    const moved: string[] = [];
    const visitedLinks = new Set<string>();

    if (optionsInput.includeIncoming !== false) {
      const incoming = links.find((link) => link.upperComponentId === rootComponentId) ?? null;
      if (incoming) {
        const upperId = applyAssemblyStackLink(incoming);
        visitedLinks.add(incoming.id);
        if (upperId) moved.push(upperId);
      }
    }

    const queue = [rootComponentId];
    const visitedComponents = new Set<string>();
    while (queue.length) {
      const lowerComponentId = queue.shift()!;
      if (visitedComponents.has(lowerComponentId)) continue;
      visitedComponents.add(lowerComponentId);
      const link = links.find((entry) => (
        entry.lowerComponentId === lowerComponentId && !visitedLinks.has(entry.id)
      )) ?? null;
      if (!link) continue;
      visitedLinks.add(link.id);
      const upperId = applyAssemblyStackLink(link);
      if (!upperId) continue;
      moved.push(upperId);
      queue.push(upperId);
    }
    return Array.from(new Set(moved));
  };

  const reflowAssemblyConnectionsFrom = (rootComponentIdInput: string): string[] => {
    const rootComponentId = String(rootComponentIdInput ?? '').trim();
    const assembly = getAssemblyState();
    const settings = normalizeAssemblySettings(assembly.settings);
    const connections = assembly.connections ?? [];
    if (!rootComponentId || settings.adjacency.reflowOnResize === false || !connections.length) return [];

    const moved: string[] = [];
    const visited = new Set<string>([rootComponentId]);
    const queue = [rootComponentId];
    while (queue.length) {
      const fixedComponentId = queue.shift()!;
      for (const connection of connections) {
        const sourceIsFixed = connection.source.componentId === fixedComponentId;
        const targetIsFixed = connection.target.componentId === fixedComponentId;
        if (!sourceIsFixed && !targetIsFixed) continue;
        const fixedEndpoint = sourceIsFixed ? connection.source : connection.target;
        const movingEndpoint = sourceIsFixed ? connection.target : connection.source;
        if (visited.has(movingEndpoint.componentId)) continue;

        const components = listAssemblyComponents();
        const fixedComponent = components.find((component) => component.id === fixedEndpoint.componentId);
        const movingComponent = components.find((component) => component.id === movingEndpoint.componentId);
        const fixedAnchor = fixedComponent?.anchors.find((anchor) => anchor.id === fixedEndpoint.anchorId);
        const movingAnchor = movingComponent?.anchors.find((anchor) => anchor.id === movingEndpoint.anchorId);
        if (!fixedComponent || !movingComponent || !fixedAnchor || !movingAnchor) continue;

        const delta = {
          x: fixedAnchor.position.x - movingAnchor.position.x,
          y: fixedAnchor.position.y - movingAnchor.position.y,
          z: fixedAnchor.position.z - movingAnchor.position.z,
        };
        const distance = Math.hypot(delta.x, delta.y, delta.z);
        if (distance > 0.000001) {
          const currentPosition = movingComponent.transform.position ?? { x: 0, y: 0, z: 0 };
          const nextTransform = normalizeAssemblyTransform({
            ...movingComponent.transform,
            position: {
              x: currentPosition.x + delta.x,
              y: currentPosition.y + delta.y,
              z: currentPosition.z + delta.z,
            },
          });
          if (applyAssemblyProductTransform(movingComponent.id, nextTransform, { render: false })) {
            moved.push(movingComponent.id);
          }
        }
        visited.add(movingEndpoint.componentId);
        queue.push(movingEndpoint.componentId);
      }
    }
    return moved;
  };

  const getAssemblyConnectedComponentIds = (rootComponentIdInput: string): string[] => {
    const rootComponentId = String(rootComponentIdInput ?? '').trim();
    if (!rootComponentId) return [];
    const assembly = getAssemblyState();
    const connections = assembly.connections ?? [];
    const stackLinks = assembly.stackLinks ?? [];
    const visited = new Set<string>([rootComponentId]);
    const queue = [rootComponentId];
    while (queue.length) {
      const componentId = queue.shift()!;
      for (const connection of connections) {
        const neighborId = connection.source.componentId === componentId
          ? connection.target.componentId
          : connection.target.componentId === componentId
            ? connection.source.componentId
            : '';
        if (!neighborId || visited.has(neighborId)) continue;
        visited.add(neighborId);
        queue.push(neighborId);
      }
      for (const link of stackLinks) {
        const neighborId = link.lowerComponentId === componentId
          ? link.upperComponentId
          : link.upperComponentId === componentId
            ? link.lowerComponentId
            : '';
        if (!neighborId || visited.has(neighborId)) continue;
        visited.add(neighborId);
        queue.push(neighborId);
      }
    }
    return Array.from(visited);
  };

  const getAssemblyComponentGroupBounds = (
    componentsInput: AsukaAssemblyComponent[],
  ): AsukaAssemblyBounds | null => {
    const components = Array.isArray(componentsInput) ? componentsInput : [];
    if (!components.length) return null;
    const min = {
      x: Math.min(...components.map((component) => component.bounds.min.x)),
      y: Math.min(...components.map((component) => component.bounds.min.y)),
      z: Math.min(...components.map((component) => component.bounds.min.z)),
    };
    const max = {
      x: Math.max(...components.map((component) => component.bounds.max.x)),
      y: Math.max(...components.map((component) => component.bounds.max.y)),
      z: Math.max(...components.map((component) => component.bounds.max.z)),
    };
    return {
      min,
      max,
      center: {
        x: (min.x + max.x) * 0.5,
        y: (min.y + max.y) * 0.5,
        z: (min.z + max.z) * 0.5,
      },
      size: {
        x: max.x - min.x,
        y: max.y - min.y,
        z: max.z - min.z,
      },
    };
  };

  const getAssemblyPointBounds = (
    pointsInput: Array<{ x: number; z: number }>,
  ): { minX: number; maxX: number; minZ: number; maxZ: number; width: number; depth: number } | null => {
    const points = Array.isArray(pointsInput) ? pointsInput : [];
    if (points.length < 3) return null;
    const minX = Math.min(...points.map((point) => Number(point.x) || 0));
    const maxX = Math.max(...points.map((point) => Number(point.x) || 0));
    const minZ = Math.min(...points.map((point) => Number(point.z) || 0));
    const maxZ = Math.max(...points.map((point) => Number(point.z) || 0));
    return {
      minX,
      maxX,
      minZ,
      maxZ,
      width: Math.max(0.000001, maxX - minX),
      depth: Math.max(0.000001, maxZ - minZ),
    };
  };

  const getAssemblySupportingFloor = (
    plan: NonNullable<AsukaAssemblyState['plan']>,
    component: AsukaAssemblyComponent | null,
  ): AsukaAssemblyFloorSurface | null => {
    const floors = (plan.floors ?? []).filter((floor) => floor.points.length >= 3);
    if (!floors.length) return null;
    const center = component?.bounds.center ?? { x: 0, z: 0 };
    const ranked = floors
      .map((floor) => {
        const bounds = getAssemblyPointBounds(floor.points);
        if (!bounds) return null;
        const dx = center.x < bounds.minX
          ? bounds.minX - center.x
          : center.x > bounds.maxX
            ? center.x - bounds.maxX
            : 0;
        const dz = center.z < bounds.minZ
          ? bounds.minZ - center.z
          : center.z > bounds.maxZ
            ? center.z - bounds.maxZ
            : 0;
        return { floor, distanceSquared: dx * dx + dz * dz };
      })
      .filter(Boolean) as Array<{ floor: AsukaAssemblyFloorSurface; distanceSquared: number }>;
    ranked.sort((left, right) => left.distanceSquared - right.distanceSquared);
    return ranked[0]?.floor ?? floors[0] ?? null;
  };

  const updateAssemblyAutoFitScales = (
    componentIdsInput: string[],
    scaleInput: number,
  ): void => {
    const componentIds = new Set(componentIdsInput.map((id) => String(id ?? '').trim()).filter(Boolean));
    if (!componentIds.size) return;
    const assembly = getAssemblyState();
    const nextScales = { ...(assembly.componentAutoFitScales ?? {}) };
    const scale = Math.min(1, Math.max(0.01, Number(scaleInput) || 1));
    for (const componentId of componentIds) {
      if (scale < 0.999999) nextScales[componentId] = scale;
      else delete nextScales[componentId];
    }
    currentState = {
      ...currentState,
      assembly: normalizeAssemblyState({
        ...assembly,
        componentAutoFitScales: nextScales,
      }),
    };
  };

  const applyAssemblyComponentScale = (
    component: AsukaAssemblyComponent,
    scaleRatioInput: number,
    targetCenterInput?: Partial<AsukaAssemblyTransform['position']>,
  ): boolean => {
    const scaleRatio = Math.max(0.0001, Number(scaleRatioInput) || 1);
    const transform = normalizeAssemblyTransform(component.transform);
    const support = getAssemblyComponentSupport(getAssemblyState(), component);
    const targetCenter = {
      x: Number(targetCenterInput?.x ?? component.bounds.center.x) || 0,
      y: Number(targetCenterInput?.y ?? (
        support === 'floor'
          ? component.bounds.min.y + component.bounds.size.y * scaleRatio * 0.5
          : support === 'ceiling'
            ? component.bounds.max.y - component.bounds.size.y * scaleRatio * 0.5
            : component.bounds.center.y
      )) || 0,
      z: Number(targetCenterInput?.z ?? component.bounds.center.z) || 0,
    };
    return applyAssemblyProductTransform(component.id, {
      ...transform,
      position: {
        x: (transform.position?.x ?? 0) + targetCenter.x - component.bounds.center.x,
        y: (transform.position?.y ?? 0) + targetCenter.y - component.bounds.center.y,
        z: (transform.position?.z ?? 0) + targetCenter.z - component.bounds.center.z,
      },
      scale: {
        x: (transform.scale?.x ?? 1) * scaleRatio,
        y: (transform.scale?.y ?? 1) * scaleRatio,
        z: (transform.scale?.z ?? 1) * scaleRatio,
      },
    }, { render: false });
  };

  const restoreAssemblyComponentGroupNaturalScale = (
    rootComponentIdInput: string,
  ): string[] => {
    const rootComponentId = String(rootComponentIdInput ?? '').trim();
    const componentIds = getAssemblyConnectedComponentIds(rootComponentId);
    const assembly = getAssemblyState();
    const scaleMap = assembly.componentAutoFitScales ?? {};
    const components = listAssemblyComponents();
    let restored = false;
    for (const componentId of componentIds) {
      const previousFitScale = Math.min(1, Math.max(0.01, Number(scaleMap[componentId]) || 1));
      if (previousFitScale >= 0.999999) continue;
      const component = components.find((entry) => entry.id === componentId);
      if (!component) continue;
      restored = applyAssemblyComponentScale(component, 1 / previousFitScale) || restored;
    }
    updateAssemblyAutoFitScales(componentIds, 1);
    if (restored) reflowAssemblyConnectionsFrom(rootComponentId);
    return componentIds;
  };

  const translateAssemblyComponentGroupIntoBounds = (
    componentIdsInput: string[],
    bounds: { minX: number; maxX: number; minZ: number; maxZ: number },
  ): boolean => {
    const componentIds = new Set(componentIdsInput);
    const components = listAssemblyComponents().filter((component) => componentIds.has(component.id));
    const groupBounds = getAssemblyComponentGroupBounds(components);
    if (!groupBounds) return false;
    let deltaX = 0;
    let deltaZ = 0;
    if (groupBounds.min.x < bounds.minX) deltaX = bounds.minX - groupBounds.min.x;
    else if (groupBounds.max.x > bounds.maxX) deltaX = bounds.maxX - groupBounds.max.x;
    if (groupBounds.min.z < bounds.minZ) deltaZ = bounds.minZ - groupBounds.min.z;
    else if (groupBounds.max.z > bounds.maxZ) deltaZ = bounds.maxZ - groupBounds.max.z;
    if (Math.hypot(deltaX, deltaZ) <= 0.000001) return false;
    let changed = false;
    for (const component of components) {
      const transform = normalizeAssemblyTransform(component.transform);
      changed = applyAssemblyProductTransform(component.id, {
        ...transform,
        position: {
          x: (transform.position?.x ?? 0) + deltaX,
          y: transform.position?.y ?? 0,
          z: (transform.position?.z ?? 0) + deltaZ,
        },
      }, { render: false }) || changed;
    }
    return changed;
  };

  const shrinkAssemblyComponentGroupToFit = (
    rootComponentIdInput: string,
    componentIdsInput: string[],
  ): boolean => {
    const rootComponentId = String(rootComponentIdInput ?? '').trim();
    const componentIds = new Set(componentIdsInput);
    const assembly = getAssemblyState();
    const plan = assembly.plan;
    let components = listAssemblyComponents().filter((component) => componentIds.has(component.id));
    const rootComponent = components.find((component) => component.id === rootComponentId) ?? components[0] ?? null;
    const groupBounds = getAssemblyComponentGroupBounds(components);
    if (!plan || !rootComponent || !groupBounds) return false;
    const supportingFloor = getAssemblySupportingFloor(plan, rootComponent);
    const areaPoints = supportingFloor?.points?.length
      ? supportingFloor.points
      : plan.points;
    const areaBounds = getAssemblyPointBounds(areaPoints);
    if (!areaBounds) return false;
    const padding = Math.max(
      0,
      Number(plan.autoExpandPadding ?? Math.max(0.2, Number(assembly.settings?.grid?.size ?? 0.1) * 2)) || 0,
    );
    const usableBounds = {
      minX: areaBounds.minX + padding,
      maxX: areaBounds.maxX - padding,
      minZ: areaBounds.minZ + padding,
      maxZ: areaBounds.maxZ - padding,
    };
    const usableWidth = Math.max(0.000001, usableBounds.maxX - usableBounds.minX);
    const usableDepth = Math.max(0.000001, usableBounds.maxZ - usableBounds.minZ);
    const usableHeight = Math.max(
      0.000001,
      (Number(plan.height ?? 2.6) || 2.6) - padding * 2,
    );
    const rawScale = Math.min(
      1,
      usableWidth / Math.max(0.000001, groupBounds.size.x),
      usableDepth / Math.max(0.000001, groupBounds.size.z),
      usableHeight / Math.max(0.000001, groupBounds.size.y),
    );
    const fitScale = rawScale < 0.999999
      ? Math.max(0.01, rawScale * 0.995)
      : 1;
    if (fitScale < 0.999999) {
      const pivot = groupBounds.center;
      for (const component of components) {
        applyAssemblyComponentScale(component, fitScale, {
          x: pivot.x + (component.bounds.center.x - pivot.x) * fitScale,
          z: pivot.z + (component.bounds.center.z - pivot.z) * fitScale,
        });
      }
      reflowAssemblyConnectionsFrom(rootComponentId);
    }
    updateAssemblyAutoFitScales(Array.from(componentIds), fitScale);
    translateAssemblyComponentGroupIntoBounds(Array.from(componentIds), usableBounds);
    components = listAssemblyComponents().filter((component) => componentIds.has(component.id));
    return fitScale < 0.999999 || Boolean(getAssemblyComponentGroupBounds(components));
  };

  const expandAssemblyPlanToFit = (
    componentsInput: AsukaAssemblyComponent[],
    rootComponentIdInput?: string,
  ): boolean => {
    const components = Array.isArray(componentsInput) ? componentsInput : [];
    const assembly = getAssemblyState();
    const plan = assembly.plan;
    if (!plan || plan.autoExpand === false || !components.length) return false;
    const rootComponentId = String(rootComponentIdInput ?? '').trim();
    const rootComponent = components.find((component) => component.id === rootComponentId)
      ?? components[0]
      ?? null;
    const supportingFloor = getAssemblySupportingFloor(plan, rootComponent);
    const currentBounds = getAssemblyPointBounds(
      supportingFloor?.points?.length ? supportingFloor.points : plan.points,
    );
    if (!currentBounds) return false;
    const padding = Math.max(
      0,
      Number(plan.autoExpandPadding ?? Math.max(0.2, Number(assembly.settings?.grid?.size ?? 0.1) * 2)) || 0,
    );
    const componentMinX = Math.min(...components.map((component) => component.bounds.min.x));
    const componentMaxX = Math.max(...components.map((component) => component.bounds.max.x));
    const componentMinZ = Math.min(...components.map((component) => component.bounds.min.z));
    const componentMaxZ = Math.max(...components.map((component) => component.bounds.max.z));
    const nextMinX = Math.min(currentBounds.minX, componentMinX - padding);
    const nextMaxX = Math.max(currentBounds.maxX, componentMaxX + padding);
    const nextMinZ = Math.min(currentBounds.minZ, componentMinZ - padding);
    const nextMaxZ = Math.max(currentBounds.maxZ, componentMaxZ + padding);
    const floorY = Number(plan.floorY ?? 0) || 0;
    const componentMaxY = Math.max(...components.map((component) => component.bounds.max.y));
    const nextHeight = Math.max(Number(plan.height ?? 2.6) || 2.6, componentMaxY - floorY + padding);
    const changed = Math.abs(nextMinX - currentBounds.minX) > 0.000001
      || Math.abs(nextMaxX - currentBounds.maxX) > 0.000001
      || Math.abs(nextMinZ - currentBounds.minZ) > 0.000001
      || Math.abs(nextMaxZ - currentBounds.maxZ) > 0.000001
      || Math.abs(nextHeight - (Number(plan.height ?? 2.6) || 2.6)) > 0.000001;
    if (!changed) return false;

    const sourceWidth = currentBounds.width;
    const sourceDepth = currentBounds.depth;
    const nextWidth = Math.max(0.000001, nextMaxX - nextMinX);
    const nextDepth = Math.max(0.000001, nextMaxZ - nextMinZ);
    const mapPoint = (point: { x: number; z: number }) => ({
      x: nextMinX + ((point.x - currentBounds.minX) / sourceWidth) * nextWidth,
      z: nextMinZ + ((point.z - currentBounds.minZ) / sourceDepth) * nextDepth,
    });
    let nextPoints = supportingFloor ? plan.points : plan.points.map(mapPoint);
    let nextFloors = plan.floors;
    let nextWalls = plan.walls;
    if (supportingFloor) {
      const expandedFloor: AsukaAssemblyFloorSurface = {
        ...supportingFloor,
        points: supportingFloor.points.map(mapPoint),
      };
      nextFloors = (plan.floors ?? []).map((floor) => (
        floor.id === supportingFloor.id ? expandedFloor : floor
      ));
      nextWalls = reconcileAssemblyRoomWalls(expandedFloor, plan.walls, {
        height: nextHeight,
        thickness: plan.wallThickness,
      }).map((wall) => (
        String(wall.metadata?.roomSurfaceId ?? '').trim() === expandedFloor.id
          ? { ...wall, height: Math.max(Number(wall.height ?? 0) || 0, nextHeight) }
          : wall
      ));
      if ((plan.floors ?? []).length === 1) nextPoints = expandedFloor.points;
    } else if (plan.walls?.length) {
      nextWalls = plan.walls.map((wall) => ({
        ...wall,
        start: mapPoint(wall.start),
        end: mapPoint(wall.end),
        height: Math.max(Number(wall.height ?? 0) || 0, nextHeight),
      }));
    }
    const nextPlan = {
      ...plan,
      points: nextPoints,
      height: nextHeight,
      ...(nextFloors !== undefined ? { floors: nextFloors } : {}),
      ...(nextWalls !== undefined ? { walls: nextWalls } : {}),
    };
    currentState = {
      ...currentState,
      assembly: normalizeAssemblyState({
        ...assembly,
        plan: nextPlan,
      }),
    };
    return true;
  };

  const reconcileAssemblyAfterParametricResize = (
    productIdInput: string,
    boundsBefore: ReturnType<typeof getProductInstanceBounds>,
    componentBefore: AsukaAssemblyComponent | null,
    optionsInput: { propagateConnections?: boolean } = {},
  ): void => {
    const productId = String(productIdInput ?? '').trim();
    const boundsAfterResize = getProductInstanceBounds(productId);
    const product = getProductById(productId);
    if (!productId || !boundsBefore || !boundsAfterResize || !product) {
      if (isAssemblyModeEnabled()) renderAssemblyEnvironment();
      return;
    }

    const assemblyBeforeResize = getAssemblyState();
    const floorSupported = componentBefore
      ? getAssemblyComponentSupport(assemblyBeforeResize, componentBefore) === 'floor'
      : false;
    const positionDelta = {
      x: boundsBefore.center.x - boundsAfterResize.center.x,
      y: floorSupported
        ? boundsBefore.min.y - boundsAfterResize.min.y
        : boundsBefore.center.y - boundsAfterResize.center.y,
      z: boundsBefore.center.z - boundsAfterResize.center.z,
    };
    if (Math.hypot(positionDelta.x, positionDelta.y, positionDelta.z) > 0.000001) {
      const currentTransform = normalizeAssemblyTransform(product?.transform);
      applyAssemblyProductTransform(productId, {
        ...currentTransform,
        position: {
          x: (currentTransform.position?.x ?? 0) + positionDelta.x,
          y: (currentTransform.position?.y ?? 0) + positionDelta.y,
          z: (currentTransform.position?.z ?? 0) + positionDelta.z,
        },
      }, { render: false });
    }

    if (componentBefore && isAssemblyModeEnabled()) {
      if (optionsInput.propagateConnections === true) {
        const componentIds = restoreAssemblyComponentGroupNaturalScale(productId);
        reflowAssemblyConnectionsFrom(productId);
        reflowAssemblyStackLinksFrom(productId);
        const settings = normalizeAssemblySettings(getAssemblyState().settings);
        if (settings.adjacency.resizeOverflowPolicy === 'shrink-components') {
          shrinkAssemblyComponentGroupToFit(productId, componentIds);
        } else {
          const componentIdSet = new Set(componentIds);
          expandAssemblyPlanToFit(
            listAssemblyComponents().filter((component) => componentIdSet.has(component.id)),
            productId,
          );
        }
      }
      renderAssemblyEnvironment();
    } else {
      try { scene?.render(); } catch {}
    }
  };

  const recordAssemblyMutation = (
    action: string,
    id: string | null,
    options: AsukaAssemblyMutationOptions = {},
  ): void => {
    if (options.emitChange !== false) {
      emitChanged(action, id, { commitHistory: options.commitHistory !== false });
    } else if (options.commitHistory !== false) {
      commitFinal3DHistorySnapshot(action, id);
    }
  };

  const configureAssemblyInternal = (
    patch: Partial<AsukaAssemblyState>,
    options: AsukaAssemblyMutationOptions = {},
  ): AsukaAssemblyState => {
    const previous = getAssemblyState();
    const next = normalizeAssemblyState({
      ...previous,
      ...(patch ?? {}),
      plan: {
        ...(previous.plan ?? {}),
        ...(patch?.plan ?? {}),
        points: patch?.plan?.points ?? previous.plan?.points ?? [],
      },
      settings: {
        ...(previous.settings ?? {}),
        ...(patch?.settings ?? {}),
        grid: { ...(previous.settings?.grid ?? {}), ...(patch?.settings?.grid ?? {}) },
        measurement: {
          ...(previous.settings?.measurement ?? {}),
          ...(patch?.settings?.measurement ?? {}),
        },
        snap: { ...(previous.settings?.snap ?? {}), ...(patch?.settings?.snap ?? {}) },
        collision: { ...(previous.settings?.collision ?? {}), ...(patch?.settings?.collision ?? {}) },
        adjacency: { ...(previous.settings?.adjacency ?? {}), ...(patch?.settings?.adjacency ?? {}) },
        visualization: { ...(previous.settings?.visualization ?? {}), ...(patch?.settings?.visualization ?? {}) },
      },
      connections: patch?.connections ?? previous.connections,
      stackLinks: patch?.stackLinks ?? previous.stackLinks,
    });
    currentState = { ...currentState, assembly: next };
    renderAssemblyEnvironment();
    recordAssemblyMutation('setAssemblyConfiguration', currentViewId, options);
    return clonePlain(next);
  };

  const listAssemblySurfacesInternal = (): AsukaAssemblySurface[] => {
    const plan = getAssemblyState().plan;
    return clonePlain([
      ...(plan?.floors ?? []),
      ...(plan?.walls ?? []),
    ]);
  };

  const getAssemblyBuildStateInternal = (): AsukaAssemblyBuildState => ({
    active: assemblyBuildState != null,
    tool: assemblyBuildState?.tool ?? null,
    selectedSurfaceId: assemblyBuildSelectedSurfaceId,
    dragging: assemblyBuildState?.dragging === true
      || assemblySurfacePointDragState != null
      || assemblySurfaceGizmoDragState != null,
    draftPointCount: assemblyBuildState?.points.length ?? 0,
  });

  const selectAssemblySurfaceInternal = (idInput: string | null): AsukaAssemblySurface | null => {
    const id = String(idInput ?? '').trim();
    const surface = id
      ? listAssemblySurfacesInternal().find((entry) => entry.id === id) ?? null
      : null;
    assemblyBuildSelectedSurfaceId = surface?.id ?? null;
    renderAssemblyEnvironment();
    emitChanged('assemblySurface:select', surface?.id ?? null, { commitHistory: false });
    return clonePlain(surface);
  };

  const getAssemblySurfaceEnvelope = (
    floors: NonNullable<AsukaAssemblyState['plan']>['floors'] = [],
    walls: NonNullable<AsukaAssemblyState['plan']>['walls'] = [],
    fallback: Array<{ x: number; z: number }> = [],
  ): Array<{ x: number; z: number }> => {
    const points = [
      ...(floors ?? []).flatMap((surface) => surface.points ?? []),
      ...(walls ?? []).flatMap((surface) => [surface.start, surface.end]),
    ];
    if (!points.length) return clonePlain(fallback);
    const minX = Math.min(...points.map((point) => Number(point.x) || 0));
    const maxX = Math.max(...points.map((point) => Number(point.x) || 0));
    const minZ = Math.min(...points.map((point) => Number(point.z) || 0));
    const maxZ = Math.max(...points.map((point) => Number(point.z) || 0));
    const padding = Math.max(0.02, Number(getAssemblyState().settings?.grid?.size ?? 0.1) || 0.1);
    return [
      { x: minX - padding, z: minZ - padding },
      { x: maxX + padding, z: minZ - padding },
      { x: maxX + padding, z: maxZ + padding },
      { x: minX - padding, z: maxZ + padding },
    ];
  };

  const updateAssemblySurfaceInternal = (
    idInput: string,
    patch: AsukaAssemblySurfacePatch,
    options: AsukaAssemblyMutationOptions = {},
  ): AsukaAssemblySurface | null => {
    const id = String(idInput ?? '').trim();
    if (!id) return null;
    const assembly = getAssemblyState();
    const plan = assembly.plan;
    if (!plan) return null;
    const previousFloor = (plan.floors ?? []).find((surface) => surface.id === id) ?? null;
    if (
      previousFloor
      && Array.isArray(patch.points)
      && !isAssemblyPolygonSimple(patch.points)
    ) return null;
    const mergeSurfaceMaterial = (
      current: AsukaAssemblySurfaceMaterial | null | undefined,
      next: AsukaAssemblySurfaceMaterial | null | undefined,
    ): AsukaAssemblySurfaceMaterial => ({
      ...(current ?? {}),
      ...(next ?? {}),
      textureScale: {
        ...(current?.textureScale ?? {}),
        ...(next?.textureScale ?? {}),
      },
      textureSize: next?.textureSize
        ? {
            ...(current?.textureSize ?? {}),
            ...next.textureSize,
          }
        : current?.textureSize,
    });
    const floors = (plan.floors ?? []).map((surface) => surface.id === id
      ? {
          ...surface,
          ...clonePlain(patch),
          id: surface.id,
          kind: 'floor' as const,
          material: mergeSurfaceMaterial(surface.material, patch.material),
        }
      : surface);
    let walls = (plan.walls ?? []).map((surface) => surface.id === id
      ? {
          ...surface,
          ...clonePlain(patch),
          id: surface.id,
          kind: 'wall' as const,
          material: mergeSurfaceMaterial(surface.material, patch.material),
          interiorMaterial: mergeSurfaceMaterial(
            surface.interiorMaterial ?? surface.material,
            patch.interiorMaterial ?? patch.material,
          ),
          exteriorMaterial: mergeSurfaceMaterial(
            surface.exteriorMaterial ?? surface.material,
            patch.exteriorMaterial ?? patch.material,
          ),
      }
      : surface);
    const updatedFloor = floors.find((surface) => surface.id === id) ?? null;
    if (
      updatedFloor
      && (
        updatedFloor.metadata?.roomSurface === true
        || walls.some((wall) => String(wall.metadata?.roomSurfaceId ?? '') === updatedFloor.id)
      )
    ) {
      walls = reconcileAssemblyRoomWalls(updatedFloor, walls, {
        height: Number(plan.height ?? 2.6) || 2.6,
        thickness: Number(plan.wallThickness ?? 0.08) || 0.08,
        material: { color: String(plan.wallColor ?? '#e7ebf1') },
        createId: () => makeId('assembly-wall'),
      });
    }
    if (![...floors, ...walls].some((surface) => surface.id === id)) return null;
    configureAssemblyInternal({
      plan: {
        ...plan,
        floors,
        walls,
        points: getAssemblySurfaceEnvelope(floors, walls, plan.points),
      },
    }, {
      ...options,
      source: options.source ?? 'assembly.surface.update',
    });
    return clonePlain(listAssemblySurfacesInternal().find((surface) => surface.id === id) ?? null);
  };

  const removeAssemblySurfaceInternal = (
    idInput: string,
    options: AsukaAssemblyMutationOptions = {},
  ): AsukaAssemblySurface | null => {
    const id = String(idInput ?? '').trim();
    const assembly = getAssemblyState();
    const plan = assembly.plan;
    if (!id || !plan) return null;
    const previous = listAssemblySurfacesInternal().find((surface) => surface.id === id) ?? null;
    if (!previous) return null;
    const linkedRoomSurfaceId = previous.kind === 'wall'
      ? String(previous.metadata?.roomSurfaceId ?? '').trim()
      : '';
    const removedRoomEdgeId = previous.kind === 'wall'
      ? String(previous.metadata?.roomEdgeId ?? '').trim()
      : '';
    const removedRoomWallTemplate = previous.kind === 'wall'
      ? clonePlain(previous)
      : null;
    const floors = (plan.floors ?? [])
      .filter((surface) => surface.id !== id)
      .map((surface) => {
        if (surface.id !== linkedRoomSurfaceId || !removedRoomEdgeId) return surface;
        const openWallEdgeIds = Array.from(new Set([
          ...(Array.isArray(surface.metadata?.openWallEdgeIds)
            ? surface.metadata!.openWallEdgeIds as string[]
            : []),
          removedRoomEdgeId,
        ]));
        const roomWallTemplates = (
          surface.metadata?.roomWallTemplates
          && typeof surface.metadata.roomWallTemplates === 'object'
          && !Array.isArray(surface.metadata.roomWallTemplates)
        )
          ? clonePlain(surface.metadata.roomWallTemplates as Record<string, unknown>)
          : {};
        if (removedRoomWallTemplate) {
          roomWallTemplates[removedRoomEdgeId] = removedRoomWallTemplate;
        }
        return {
          ...surface,
          metadata: {
            ...(surface.metadata ?? {}),
            openWallEdgeIds,
            roomWallTemplates,
          },
        };
      });
    const walls = (plan.walls ?? []).filter((surface) => (
      surface.id !== id
      && (
        previous.kind !== 'floor'
        || String(surface.metadata?.roomSurfaceId ?? '').trim() !== previous.id
      )
    ));
    if (assemblyBuildSelectedSurfaceId === id) assemblyBuildSelectedSurfaceId = null;
    configureAssemblyInternal({
      plan: {
        ...plan,
        floors,
        walls,
        points: getAssemblySurfaceEnvelope(floors, walls, plan.points),
      },
    }, {
      ...options,
      source: options.source ?? 'assembly.surface.remove',
    });
    return clonePlain(previous);
  };

  const clearAssemblySurfacesInternal = (
    options: AsukaAssemblyMutationOptions = {},
  ): AsukaAssemblyState => {
    const assembly = getAssemblyState();
    assemblyBuildSelectedSurfaceId = null;
    return configureAssemblyInternal({
      enabled: true,
      plan: {
        ...(assembly.plan ?? ASUKA_ASSEMBLY_DEFAULT_PLAN),
        floors: [],
        walls: [],
      },
    }, {
      ...options,
      source: options.source ?? 'assembly.surface.clear',
    });
  };

  const placeAssemblyComponentInternal = (
    componentId: string,
    input: AsukaAssemblyPlacementInput = {},
    options: AsukaAssemblyMutationOptions = {},
  ): AsukaAssemblyPlacementResult => {
    const assembly = getAssemblyState();
    const components = listAssemblyComponents();
    const result = resolveAssemblyPlacement(assembly, components, componentId, input);
    const requestedSupport = input.support;
    const component = components.find((entry) => entry.id === componentId) ?? null;
    const previousPlacement = assembly.componentPlacements?.[componentId] ?? null;
    const supportChanged = requestedSupport != null
      && requestedSupport !== previousPlacement?.support;
    if (!result.accepted || (!result.changed && !supportChanged)) return clonePlain(result);
    if (result.changed && !applyAssemblyProductTransform(componentId, result.applied)) {
      return {
        ...result,
        accepted: false,
        changed: false,
        applied: components.find((component) => component.id === componentId)?.transform ?? {},
      };
    }
    if (result.changed) reflowAssemblyStackLinksFrom(componentId);
    const placedComponents = listAssemblyComponents();
    const retainedConnections = (assembly.connections ?? []).filter((connection) => {
      const involvesPlacedComponent = connection.source.componentId === componentId
        || connection.target.componentId === componentId;
      if (!involvesPlacedComponent) return true;
      const sourceComponent = placedComponents.find((component) =>
        component.id === connection.source.componentId);
      const targetComponent = placedComponents.find((component) =>
        component.id === connection.target.componentId);
      const sourceAnchor = sourceComponent?.anchors.find((anchor) =>
        anchor.id === connection.source.anchorId);
      const targetAnchor = targetComponent?.anchors.find((anchor) =>
        anchor.id === connection.target.anchorId);
      if (!sourceAnchor || !targetAnchor) return false;
      const tolerance = Math.max(
        0.002,
        Math.min(sourceAnchor.snapDistance, targetAnchor.snapDistance) * 0.05,
      );
      return Math.hypot(
        sourceAnchor.position.x - targetAnchor.position.x,
        sourceAnchor.position.y - targetAnchor.position.y,
        sourceAnchor.position.z - targetAnchor.position.z,
      ) <= tolerance;
    });
    const sameConnectionEndpoints = (
      left: AsukaAssemblyConnection,
      right: AsukaAssemblyConnection,
    ): boolean => (
      left.source.componentId === right.source.componentId
      && left.source.anchorId === right.source.anchorId
      && left.target.componentId === right.target.componentId
      && left.target.anchorId === right.target.anchorId
    ) || (
      left.source.componentId === right.target.componentId
      && left.source.anchorId === right.target.anchorId
      && left.target.componentId === right.source.componentId
      && left.target.anchorId === right.source.anchorId
    );
    const connections = result.createdConnection
      ? [
          ...retainedConnections.filter((entry) =>
            !sameConnectionEndpoints(entry, result.createdConnection!)),
          result.createdConnection,
        ]
      : retainedConnections;
    if (
      result.createdConnection
      || connections.length !== (assembly.connections ?? []).length
      || supportChanged
    ) {
      const componentPlacements = supportChanged
        ? {
            ...(assembly.componentPlacements ?? {}),
            [componentId]: {
              ...(previousPlacement ?? {}),
              support: requestedSupport,
              supportOffset: previousPlacement?.supportOffset
                ?? component?.definition?.placement?.supportOffset
                ?? 0,
            },
          }
        : assembly.componentPlacements;
      currentState = {
        ...currentState,
        assembly: normalizeAssemblyState({
          ...assembly,
          componentPlacements,
          connections,
        }),
      };
    }
    recordAssemblyMutation('updateAssemblyPlacement', componentId, options);
    const appliedComponent = listAssemblyComponents().find((entry) => entry.id === componentId) ?? null;
    return clonePlain({
      ...result,
      changed: result.changed || supportChanged,
      applied: appliedComponent?.transform ?? result.applied,
    });
  };

  const autoPlaceAssemblyComponentInternal = (
    componentId: string,
    options: AsukaAssemblyMutationOptions = {},
  ): AsukaAssemblyPlacementResult => {
    const components = listAssemblyComponents();
    const component = components.find((entry) => entry.id === String(componentId ?? '').trim());
    if (!component) return resolveAssemblyPlacement(getAssemblyState(), components, componentId, {});
    const assembly = getAssemblyState();
    const defaultToFloor = (assembly.plan?.floors?.length ?? 0) > 0
      && getAssemblyComponentSupport(assembly, component) === 'free';
    let best: {
      sourceAnchorId: string;
      targetComponentId: string;
      targetAnchorId: string;
      distance: number;
      delta: { x: number; y: number; z: number };
    } | null = null;
    for (const source of component.anchors) {
      for (const targetComponent of components) {
        if (targetComponent.id === component.id) continue;
        for (const target of targetComponent.anchors) {
          if (!getCompatibleAssemblyConnectorType(source, target)) continue;
          const dx = target.position.x - source.position.x;
          const dy = target.position.y - source.position.y;
          const dz = target.position.z - source.position.z;
          const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
          if (!best || distance < best.distance) {
            best = {
              sourceAnchorId: source.id,
              targetComponentId: targetComponent.id,
              targetAnchorId: target.id,
              distance,
              delta: { x: dx, y: dy, z: dz },
            };
          }
        }
      }
    }
    if (best) {
      const anchorInput: AsukaAssemblyPlacementInput = {
        position: {
          x: (component.transform.position?.x ?? 0) + best.delta.x,
          y: (component.transform.position?.y ?? 0) + best.delta.y,
          z: (component.transform.position?.z ?? 0) + best.delta.z,
        },
        sourceAnchorId: best.sourceAnchorId,
        targetComponentId: best.targetComponentId,
        targetAnchorId: best.targetAnchorId,
        snapBoundaries: false,
        snapGrid: false,
      };
      const anchorResult = resolveAssemblyPlacement(
        getAssemblyState(),
        components,
        component.id,
        anchorInput,
      );
      if (anchorResult.accepted) {
        return placeAssemblyComponentInternal(component.id, anchorInput, options);
      }
    }

    const interior = findAssemblyInteriorPlacement(
      getAssemblyState(),
      components,
      component.id,
    );
    if (!interior.accepted || (!interior.changed && !defaultToFloor)) return clonePlain(interior);
    return placeAssemblyComponentInternal(component.id, {
      position: clonePlain(interior.applied.position),
      rotation: clonePlain(interior.applied.rotation),
      snap: false,
      snapSupport: defaultToFloor,
      ...(defaultToFloor ? { support: 'floor' as const } : {}),
      allowInvalid: false,
      axis: 'free',
      metadata: {
        source: 'assembly.auto-place.inside',
        ...(defaultToFloor ? { automaticFloorSupport: true } : {}),
      },
    }, options);
  };

  const connectAssemblyInternal = (
    connectionInput: Partial<AsukaAssemblyConnection>,
    options: AsukaAssemblyMutationOptions = {},
  ): AsukaAssemblyConnection | null => {
    const assembly = getAssemblyState();
    const result = validateAssemblyConnection(assembly, listAssemblyComponents(), connectionInput);
    if (!result.valid || !result.connection) return null;
    currentState = {
      ...currentState,
      assembly: normalizeAssemblyState({
        ...assembly,
        connections: [
          ...(assembly.connections ?? []).filter((entry) => entry.id !== result.connection!.id),
          result.connection,
        ],
      }),
    };
    recordAssemblyMutation('setAssemblyConnection', result.connection.id, options);
    return clonePlain(result.connection);
  };

  const disconnectAssemblyInternal = (
    connectionIdInput: string,
    options: AsukaAssemblyMutationOptions = {},
  ): AsukaAssemblyConnection | null => {
    const connectionId = String(connectionIdInput ?? '').trim();
    const assembly = getAssemblyState();
    const previous = (assembly.connections ?? []).find((entry) => entry.id === connectionId) ?? null;
    if (!previous) return null;
    currentState = {
      ...currentState,
      assembly: normalizeAssemblyState({
        ...assembly,
        connections: (assembly.connections ?? []).filter((entry) => entry.id !== connectionId),
      }),
    };
    recordAssemblyMutation('removeAssemblyConnection', connectionId, options);
    return clonePlain(previous);
  };

  const linkAssemblyStackInternal = (
    linkInput: Partial<AsukaAssemblyStackLink>,
    options: AsukaAssemblyMutationOptions = {},
  ): AsukaAssemblyStackLink | null => {
    const components = listAssemblyComponents();
    const lowerComponentId = String(linkInput?.lowerComponentId ?? '').trim();
    const upperComponentId = String(linkInput?.upperComponentId ?? '').trim();
    const lower = components.find((component) => component.id === lowerComponentId) ?? null;
    const upper = components.find((component) => component.id === upperComponentId) ?? null;
    if (!lower || !upper || lower.id === upper.id) return null;

    const widthAxis = getAssemblyStackWidthAxis(
      lower,
      upper,
      linkInput.widthAxis === 'x' || linkInput.widthAxis === 'z' ? linkInput.widthAxis : 'auto',
    );
    const maximumInput = Number(linkInput.upperMaxWidth);
    const gapInput = Number(linkInput.gap);
    const preservedGap = Math.max(0, upper.bounds.min.y - lower.bounds.max.y);
    const link = normalizeAssemblyStackLink({
      ...linkInput,
      lowerComponentId: lower.id,
      upperComponentId: upper.id,
      widthAxis,
      gap: Number.isFinite(gapInput) ? Math.max(0, gapInput) : preservedGap,
      upperMaxWidth: Number.isFinite(maximumInput) && maximumInput > 0
        ? maximumInput
        : Math.max(0.0001, Number(upper.bounds.size?.[widthAxis] ?? 0) || 0.0001),
      createdAt: String(linkInput.createdAt ?? '').trim() || new Date().toISOString(),
      metadata: {
        ...(linkInput.metadata ?? {}),
        source: String(options.source ?? linkInput.metadata?.source ?? 'assembly.stack'),
      },
    });
    if (!link) return null;

    const assembly = getAssemblyState();
    const retained = (assembly.stackLinks ?? []).filter((entry) => (
      entry.id !== link.id
      && entry.lowerComponentId !== link.lowerComponentId
      && entry.upperComponentId !== link.upperComponentId
    ));
    const byLower = new Map(retained.map((entry) => [entry.lowerComponentId, entry.upperComponentId] as const));
    let cursor = link.upperComponentId;
    const visited = new Set<string>();
    while (!visited.has(cursor)) {
      if (cursor === link.lowerComponentId) return null;
      visited.add(cursor);
      const next = byLower.get(cursor);
      if (!next) break;
      cursor = next;
    }

    currentState = {
      ...currentState,
      assembly: normalizeAssemblyState({
        ...assembly,
        stackLinks: [link, ...retained],
      }),
    };
    reflowAssemblyStackLinksFrom(link.lowerComponentId, { includeIncoming: false });
    renderAssemblyEnvironment();
    recordAssemblyMutation('setAssemblyStackLink', link.id, options);
    return clonePlain(link);
  };

  const unlinkAssemblyStackInternal = (
    linkIdInput: string,
    options: AsukaAssemblyMutationOptions = {},
  ): AsukaAssemblyStackLink | null => {
    const linkId = String(linkIdInput ?? '').trim();
    const assembly = getAssemblyState();
    const previous = (assembly.stackLinks ?? []).find((entry) => entry.id === linkId) ?? null;
    if (!previous) return null;
    currentState = {
      ...currentState,
      assembly: normalizeAssemblyState({
        ...assembly,
        stackLinks: (assembly.stackLinks ?? []).filter((entry) => entry.id !== linkId),
      }),
    };
    renderAssemblyEnvironment();
    recordAssemblyMutation('removeAssemblyStackLink', linkId, options);
    return clonePlain(previous);
  };

  const getProductGroupBounds = (instanceIdsInput?: string[] | null) => {
    const wanted = new Set((Array.isArray(instanceIdsInput) ? instanceIdsInput : getKnownProductIds())
      .map((id) => String(id ?? '').trim())
      .filter(Boolean));
    const meshes = getRenderableModelMeshes(modelMeshes).filter((mesh) => {
      if (!mesh || mesh.isDisposed?.()) return false;
      const instanceId = getProductIdFromMesh(mesh);
      return wanted.size ? wanted.has(instanceId) : true;
    });
    if (!meshes.length) return null;
    let min = new Vector3(Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY);
    let max = new Vector3(Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY);
    for (const mesh of meshes) {
      try {
        mesh.computeWorldMatrix(true);
        const box = mesh.getBoundingInfo().boundingBox;
        min = Vector3.Minimize(min, box.minimumWorld);
        max = Vector3.Maximize(max, box.maximumWorld);
      } catch {}
    }
    if (!Number.isFinite(min.x) || !Number.isFinite(max.x)) return null;
    return { min, max, center: min.add(max).scale(0.5), size: max.subtract(min) };
  };

  const invalidateProductGroupViewTarget = (): void => {
    productGroupViewTargetDirty = true;
  };

  const setCachedProductGroupViewTarget = (target: Vector3 | null): void => {
    cachedProductGroupViewTarget = target?.clone?.() ?? null;
    productGroupViewTargetDirty = false;
  };

  const getProductGroupViewTarget = (options: { compute?: boolean } = {}): Vector3 | null => {
    if (!productGroupViewTargetDirty && cachedProductGroupViewTarget) {
      return cachedProductGroupViewTarget.clone();
    }
    if (options.compute === false) {
      return cachedProductGroupViewTarget?.clone?.() ?? null;
    }
    const bounds = getProductGroupBounds();
    const target = bounds?.center?.clone?.() ?? null;
    setCachedProductGroupViewTarget(target);
    return target?.clone?.() ?? null;
  };

  const applyProductCameraInputLock = (cam: ArcRotateCamera | null = camera): void => {
    if (!cam || !lockProductCameraTarget) return;
    try { (cam as any).panningSensibility = 0; } catch {}
    try { (cam as any).panningDistanceLimit = 0; } catch {}
    try { (cam as any).panningAxis = Vector3.Zero(); } catch {}
    try {
      const pointers = (cam as any)?.inputs?.attached?.pointers;
      if (pointers) {
        pointers.panningSensibility = 0;
        pointers.panningMouseButton = -1;
      }
    } catch {}
  };

  const getArcRotatePointerInput = (): any => {
    try { return (camera as any)?.inputs?.attached?.pointers ?? null; } catch { return null; }
  };

  const applyAssemblyBuildCameraControls = (): void => {
    const pointers = getArcRotatePointerInput();
    if (!pointers) return;
    if (assemblyBuildPreviousCameraButtons == null) {
      assemblyBuildPreviousCameraButtons = Array.isArray(pointers.buttons)
        ? pointers.buttons.slice()
        : [0, 1, 2];
    }
    // Left click belongs to the construction tool. Middle/right drag and the
    // wheel remain attached to Babylon so the room can still be inspected.
    pointers.buttons = [1, 2];
  };

  const restoreAssemblyBuildCameraControls = (): void => {
    const pointers = getArcRotatePointerInput();
    if (pointers && assemblyBuildPreviousCameraButtons) {
      pointers.buttons = assemblyBuildPreviousCameraButtons.slice();
    }
    assemblyBuildPreviousCameraButtons = null;
  };

  const centerCameraTargetOnProductGroup = (source = 'productGroup.cameraTarget', options: { force?: boolean; render?: boolean; compute?: boolean } = {}): boolean => {
    const cam = camera;
    if (!cam) return false;
    if (!options.force && !centerProductGroupInView) return false;
    const target = getProductGroupViewTarget({ compute: options.compute !== false }) ?? Vector3.Zero();
    const currentTarget = cam.target?.clone?.() ?? Vector3.Zero();
    const shouldUpdate = Vector3.DistanceSquared(currentTarget, target) > 0.00000001;
    applyProductCameraInputLock(cam);
    if (!shouldUpdate) return false;
    const alpha = Number((cam as any).alpha);
    const beta = Number((cam as any).beta);
    const radius = Number((cam as any).radius);
    cameraPersistenceSuppressedUntil = Math.max(cameraPersistenceSuppressedUntil, Date.now() + 80);
    try { cam.setTarget(target); } catch { try { cam.target = target; } catch {} }
    if (Number.isFinite(alpha)) (cam as any).alpha = alpha;
    if (Number.isFinite(beta)) (cam as any).beta = clampNumber(beta, cameraMinBetaLimit, cameraMaxBetaLimit);
    if (Number.isFinite(radius) && radius > 0) (cam as any).radius = radius;
    applyCameraAngleLimitsToCamera(cam);
    debugBabylonProduct3D('product-group:camera-target-centered', { source, target: fromVec3(target) });
    if (options.render !== false) {
      try { scene?.render(); } catch {}
    }
    lastPersistedCameraSignature = '';
    scheduleCameraStatePersist(source, 80);
    return true;
  };

  const translateMeshRootByWorldDelta = (mesh: Mesh | null | undefined, delta: Vector3): boolean => {
    if (!mesh || mesh.isDisposed?.() || !delta || delta.lengthSquared() < 0.0000000001) return false;
    try {
      mesh.computeWorldMatrix(true);
      const current = typeof (mesh as any).getAbsolutePosition === 'function'
        ? (mesh as any).getAbsolutePosition()
        : ((mesh as any).absolutePosition ?? mesh.position);
      const absolute = new Vector3(Number(current?.x ?? 0), Number(current?.y ?? 0), Number(current?.z ?? 0));
      const next = absolute.add(delta);
      if (typeof (mesh as any).setAbsolutePosition === 'function') {
        (mesh as any).setAbsolutePosition(next);
      } else if (mesh.parent && typeof (mesh.parent as any).getWorldMatrix === 'function') {
        const parentWorld = (mesh.parent as any).getWorldMatrix();
        const inverse = parentWorld.clone().invert();
        mesh.position = Vector3.TransformCoordinates(next, inverse);
      } else {
        mesh.position = mesh.position.add(delta);
      }
      mesh.computeWorldMatrix(true);
      return true;
    } catch {
      try {
        mesh.position = mesh.position.add(delta);
        mesh.computeWorldMatrix(true);
        return true;
      } catch {}
    }
    return false;
  };

  const translateProductInstanceMeshesByDelta = (instanceIdInput: string, delta: Vector3): boolean => {
    const instanceId = String(instanceIdInput ?? '').trim();
    if (!instanceId || !delta || delta.lengthSquared() < 0.0000000001) return false;
    const meshes = getProductInstanceMeshes(instanceId);
    if (!meshes.length) return false;
    let moved = false;
    for (const mesh of getProductInstanceRootMeshes(meshes)) {
      moved = translateMeshRootByWorldDelta(mesh, delta) || moved;
    }
    for (const mesh of meshes) {
      try { mesh.computeWorldMatrix(true); } catch {}
    }
    return moved;
  };

  const patchProductPositionDeltasInState = (deltasByProductId: Map<string, Vector3>): void => {
    if (!deltasByProductId.size) return;
    const products = getProductsFromState(currentState);
    if (!products.length) return;
    const previousSelectedProductId = String(currentState.selectedProductId ?? '').trim();
    let changed = false;
    const nextProducts = products.map((product: any, index: number) => {
      const productId = getProductId(product);
      const delta = deltasByProductId.get(productId);
      if (!delta || delta.lengthSquared() < 0.0000000001) return product;
      const transform = (compactProductTransform(product?.transform) ?? {}) as any;
      const position = transform?.position ?? { x: 0, y: 0, z: 0 };
      changed = true;
      return compactProduct({
        ...product,
        transform: {
          ...transform,
          position: {
            x: (Number(position.x ?? 0) || 0) + delta.x,
            y: (Number(position.y ?? 0) || 0) + delta.y,
            z: (Number(position.z ?? 0) || 0) + delta.z,
          },
        },
      }, index) ?? product;
    });
    if (!changed) return;
    const selectedProductId = previousSelectedProductId && nextProducts.some((product: any) => getProductId(product) === previousSelectedProductId)
      ? previousSelectedProductId
      : getProductId(nextProducts[nextProducts.length - 1] ?? null);
    currentState = {
      ...currentState,
      products: compactProducts(nextProducts),
      selectedProductId: selectedProductId || null,
    };
  };

  const clearScheduledProductGroupCenter = (): void => {
    if (productGroupCenterTimer != null) {
      try { clearTimeout(productGroupCenterTimer); } catch {}
      productGroupCenterTimer = null;
    }
  };

  const markProductGroupCenterPendingOnDeselect = (source = 'productGroup.center.onDeselect', emitKeyInput = ''): void => {
    if (!centerProductGroupOnTransform || isAssemblyModeEnabled()) return;
    clearScheduledProductGroupCenter();
    pendingProductGroupCenterOnDeselect = true;
    pendingProductGroupCenterSource = String(source || 'productGroup.center.onDeselect');
    const emitKey = String(emitKeyInput || pendingProductGroupCenterEmitKey || '').trim();
    if (emitKey) pendingProductGroupCenterEmitKey = emitKey;
    // While a Product 3D is selected and being nudged, keep the view locked to
    // the world origin. The actual product/group recenter is intentionally
    // delayed until the product is deselected, so keyboard repeat does not
    // trigger repeated bounds computations or scene-wide translations.
    if (centerProductGroupInView) setCachedProductGroupViewTarget(Vector3.Zero());
    else invalidateProductGroupViewTarget();
  };

  const runPendingProductGroupCenterOnDeselect = (source = 'productGroup.center.onDeselect'): boolean => {
    if (!pendingProductGroupCenterOnDeselect) return false;
    const emitKey = String(pendingProductGroupCenterEmitKey || '').trim();
    const resolvedSource = String(source || pendingProductGroupCenterSource || 'productGroup.center.onDeselect');
    pendingProductGroupCenterOnDeselect = false;
    pendingProductGroupCenterSource = 'productGroup.center.onDeselect';
    pendingProductGroupCenterEmitKey = '';
    const changed = recenterProductGroup(resolvedSource);
    if (changed && emitKey) emitChanged('updateProductTransform', emitKey);
    return changed;
  };

  const recenterProductGroup = (source = 'productGroup.center'): boolean => {
    // A manual/commit recenter supersedes any delayed recenter queued by
    // transient keyboard movement. This avoids double recenter/emit cycles after
    // the keyup commit and makes the final single-element position deterministic.
    clearScheduledProductGroupCenter();
    if (!centerProductGroup || isAssemblyModeEnabled()) return false;
    const instanceIds = getKnownProductIds();
    if (!instanceIds.length) return false;
    const elementCenter = getProductGroupCenterFromInstanceCenters(instanceIds);
    const placementCenter = getProductGroupPlacementCenter(instanceIds);
    const bounds = getProductGroupBounds(instanceIds);

    // Important: the group center is the average of 3D product centers,
    // not the bounding-box center of the whole scene and not the camera target.
    // For two products this is exactly the midpoint between both element centers.
    // Keep Y untouched for placement so vertical keyboard moves are not
    // cancelled by a horizontal recenter commit.
    const center = elementCenter ?? placementCenter ?? bounds?.center ?? null;
    if (!center) return false;
    const delta = new Vector3(-center.x, 0, -center.z);
    if (!Number.isFinite(delta.x) || !Number.isFinite(delta.y) || !Number.isFinite(delta.z)) return false;

    // The scene placement center is X/Z=0. For the camera target, keep the Y of
    // the visible group so the object does not appear too high/low in the view,
    // but never use the camera target as the source of truth for placement.
    const targetY = bounds?.center?.y ?? elementCenter?.y ?? placementCenter?.y ?? 0;
    const centeredViewTarget = new Vector3(0, targetY, 0);
    const applied = new Map<string, Vector3>();
    if (delta.lengthSquared() >= 0.0000000001) {
      for (const instanceId of instanceIds) {
        if (translateProductInstanceMeshesByDelta(instanceId, delta)) applied.set(instanceId, delta.clone());
      }
      if (applied.size) patchProductPositionDeltasInState(applied);
    }
    setCachedProductGroupViewTarget(centeredViewTarget);
    debugBabylonProduct3D('product-group:centered', {
      source,
      instanceIds,
      mode: elementCenter ? 'average-individual-visual-centers-xz' : (placementCenter ? 'placement-average-fallback' : 'bounds-fallback'),
      delta: fromVec3(delta),
      centerBefore: fromVec3(center),
      viewTargetAfter: fromVec3(centeredViewTarget),
      ...(elementCenter ? { elementCenterBefore: fromVec3(elementCenter) } : null),
      ...(placementCenter ? { placementCenterBefore: fromVec3(placementCenter) } : null),
      ...(bounds ? { boundsCenterBefore: fromVec3(bounds.center), size: fromVec3(bounds.size) } : null),
    });
    const targetChanged = centerCameraTargetOnProductGroup(`${source}.cameraTarget`, { force: true, render: false, compute: false });
    if (applied.size || targetChanged) {
      try { scene?.render(); } catch {}
    }
    return applied.size > 0 || targetChanged;
  };


  const getProductInstanceByIdFromState = (instanceIdInput: string): any | null => {
    const instanceId = String(instanceIdInput ?? '').trim();
    if (!instanceId) return null;
    return getProductsFromState(currentState).find((item: any) => getProductId(item) === instanceId) ?? null;
  };

  const vectorFromLoose3DValue = (value: any): Vector3 | null => {
    if (Array.isArray(value) && value.length >= 3) {
      const x = Number(value[0]);
      const y = Number(value[1]);
      const z = Number(value[2]);
      if (Number.isFinite(x) && Number.isFinite(y) && Number.isFinite(z)) return new Vector3(x, y, z);
      return null;
    }
    if (value && typeof value === 'object') {
      const x = Number(value.x ?? value[0] ?? 0);
      const y = Number(value.y ?? value[1] ?? 0);
      const z = Number(value.z ?? value[2] ?? 0);
      if (Number.isFinite(x) && Number.isFinite(y) && Number.isFinite(z)) return new Vector3(x, y, z);
    }
    return null;
  };

  const getProductInstanceTransformOffset = (instanceIdInput: string): any => {
    const instance = getProductInstanceByIdFromState(instanceIdInput);
    const transform = compactProductTransform(instance?.transform ?? instance) as any;
    return transform?.position ?? { x: 0, y: 0, z: 0 };
  };

  const vectorFromProductInstanceTransformPosition = (instanceIdInput: string): Vector3 | null => {
    const position = getProductInstanceTransformOffset(instanceIdInput);
    const x = Number(position?.x ?? 0) || 0;
    const y = Number(position?.y ?? 0) || 0;
    const z = Number(position?.z ?? 0) || 0;
    if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z)) return null;
    return new Vector3(x, y, z);
  };

  const getProductInstanceBasePlacementOffset = (instanceIdInput: string): Vector3 => {
    const instance = getProductInstanceByIdFromState(instanceIdInput);
    const metadata = instance?.metadata && typeof instance.metadata === 'object' ? instance.metadata : {};
    const candidates = [
      metadata?.asukaProductBaseOffset,
      metadata?.asukaProductInitialPlacementOffset,
      metadata?.placementBaseOffset,
      metadata?.baseOffset,
      instance?.asukaProductBaseOffset,
      instance?.baseOffset,
    ];
    for (const candidate of candidates) {
      const vector = vectorFromLoose3DValue(candidate);
      if (vector) return vector;
    }
    const mesh = getProductInstanceMeshes(instanceIdInput)[0] ?? null;
    const meshMetadata = mesh && (mesh as any).metadata && typeof (mesh as any).metadata === 'object' ? (mesh as any).metadata : {};
    for (const candidate of [meshMetadata?.asukaProductBaseOffset, meshMetadata?.asukaProductInitialPlacementOffset]) {
      const vector = vectorFromLoose3DValue(candidate);
      if (vector) return vector;
    }
    return Vector3.Zero();
  };

  const vectorFromProductInstancePlacementPosition = (instanceIdInput: string): Vector3 | null => {
    const transformPosition = vectorFromProductInstanceTransformPosition(instanceIdInput);
    if (!transformPosition) return null;
    return getProductInstanceBasePlacementOffset(instanceIdInput).add(transformPosition);
  };

  const getProductGroupPlacementCenter = (instanceIdsInput?: string[] | null): Vector3 | null => {
    const ids = (Array.isArray(instanceIdsInput) ? instanceIdsInput : getKnownProductIds())
      .map((id) => String(id ?? '').trim())
      .filter(Boolean);
    if (!ids.length) return null;

    // Product 3D placement center is the average of the instance placement
    // positions. Additional Product 3D instances receive an automatic runtime
    // offset so they do not overlap on creation; that offset is part of the
    // current placement and must be included as a fallback.
    let total = Vector3.Zero();
    let count = 0;
    for (const id of ids) {
      const position = vectorFromProductInstancePlacementPosition(id);
      if (!position) continue;
      total = total.add(position);
      count += 1;
    }
    if (!count) return null;
    return total.scale(1 / count);
  };

  const getProductRuntimeRootCenter = (instanceIdInput: string): Vector3 | null => {
    const instanceId = String(instanceIdInput ?? '').trim();
    if (!instanceId) return null;
    const roots = getProductInstanceRootMeshes(getProductInstanceMeshes(instanceId));
    if (!roots.length) return null;
    let total = Vector3.Zero();
    let count = 0;
    for (const root of roots) {
      try {
        root.computeWorldMatrix(true);
        const absolute = typeof (root as any).getAbsolutePosition === 'function'
          ? (root as any).getAbsolutePosition()
          : root.position;
        if (!absolute) continue;
        const center = new Vector3(Number(absolute.x), Number(absolute.y), Number(absolute.z));
        if (!Number.isFinite(center.x) || !Number.isFinite(center.y) || !Number.isFinite(center.z)) continue;
        total = total.add(center);
        count += 1;
      } catch {}
    }
    if (!count) return null;
    return total.scale(1 / count);
  };

  const getProductVisualCenter = (instanceIdInput: string): Vector3 | null => {
    const instanceId = String(instanceIdInput ?? '').trim();
    if (!instanceId) return null;
    const bounds = getProductInstanceBounds(instanceId);
    if (bounds?.center) return bounds.center.clone();
    return null;
  };

  const getProductCenter = (instanceIdInput: string): Vector3 | null => {
    const instanceId = String(instanceIdInput ?? '').trim();
    if (!instanceId) return null;

    // Editor centering must use each 3D product center, then average
    // those centers. For real products, the reliable center users see is the
    // individual instance visual bounds center, not the model origin nor the
    // combined group bounds. With two objects this gives the midpoint between
    // the two visible objects, exactly as expected in the canvas.
    const visualCenter = getProductVisualCenter(instanceId);
    if (visualCenter) return visualCenter.clone();
    const placement = vectorFromProductInstancePlacementPosition(instanceId);
    if (placement) return placement.clone();
    const rootCenter = getProductRuntimeRootCenter(instanceId);
    if (rootCenter) return rootCenter.clone();
    return null;
  };

  const getProductGroupCenterFromInstanceCenters = (instanceIdsInput?: string[] | null): Vector3 | null => {
    const ids = (Array.isArray(instanceIdsInput) ? instanceIdsInput : getKnownProductIds())
      .map((id) => String(id ?? '').trim())
      .filter(Boolean);
    if (!ids.length) return null;

    // This is the centering rule expected by the editor: take the placement
    // center of each 3D product, then center the midpoint/average of
    // those centers on the scene origin. With two objects this is exactly the
    // middle of the line joining both element centers.
    let total = Vector3.Zero();
    let count = 0;
    const debugCenters: any[] = [];
    for (const id of ids) {
      const center = getProductCenter(id);
      if (!center) continue;
      total = total.add(center);
      count += 1;
      debugCenters.push({ id, center: fromVec3(center) });
    }
    if (!count) return null;
    const average = total.scale(1 / count);
    debugBabylonProduct3D('product-group:center-source', { ids, centers: debugCenters, average: fromVec3(average) });
    return average;
  };

  const patchProductInState = (productIdInput: string, updater: (product: any) => any): void => {
    const productId = String(productIdInput ?? '').trim();
    if (!productId) return;
    const products = getProductsFromState(currentState);
    if (!products.length) return;
    let changed = false;
    const nextProducts = products.map((product: any, index: number) => {
      if (getProductId(product) !== productId) return product;
      changed = true;
      const draft = compactProduct(product, index) ?? product;
      return compactProduct(updater(draft), index) ?? product;
    });
    if (!changed) return;
    currentState = {
      ...currentState,
      products: compactProducts(nextProducts),
      selectedProductId: productId,
    };
  };

  const normalizeRuntimeProductCatalogIdentity = (value: unknown): string => String(value ?? '').trim().toLowerCase();

  const getRuntimeProductCatalogIdentityKeys = (input: any): string[] => Array.from(new Set([
    input?.id,
    input?.catalogId,
    input?.name,
    input?.familyId,
  ].map(normalizeRuntimeProductCatalogIdentity).filter(Boolean)));

  const findRuntimeProductCatalog = (product: any): RuntimeProductCatalog | null => {
    for (const key of getRuntimeProductCatalogIdentityKeys(product)) {
      const catalog = runtimeProductCatalogsByIdentity.get(key);
      if (catalog) return catalog;
    }
    return runtimeProductCatalog && productCatalogMatchesStateProduct(runtimeProductCatalog, product)
      ? runtimeProductCatalog
      : null;
  };

  const getRuntimeProductCatalogLabelSignature = (variations: any[], parts: any[]): string => {
    const labels: string[] = [];
    const visit = (items: any[]) => {
      for (const item of Array.isArray(items) ? items : []) {
        labels.push([
          item?.id,
          item?.name,
          item?.label,
          item?.product3d?.partId,
          item?.metadata?.asukaProduct3DPartId,
        ].map((value) => String(value ?? '').trim()).join(':'));
        visit(item?.variations);
      }
    };
    visit(variations);
    for (const part of Array.isArray(parts) ? parts : []) {
      labels.push([part?.id, part?.name, part?.label].map((value) => String(value ?? '').trim()).join(':'));
    }
    return labels.join('|');
  };

  const rememberRuntimeProductCatalog = (element: any): void => {
    if (!element || typeof element !== 'object') return;
    const variations = Array.isArray(element?.variations) ? element.variations : [];
    const product3d = element?.product3d && typeof element.product3d === 'object' ? element.product3d : {};
    const parts = Array.isArray(product3d?.parts)
      ? product3d.parts
      : (Array.isArray(element?.parts) ? element.parts : []);
    const id = String(element?.id ?? element?.catalogId ?? '').trim() || undefined;
    const name = String(element?.name ?? '').trim() || undefined;
    const familyId = String(element?.familyId ?? '').trim() || undefined;
    const labelSignature = getRuntimeProductCatalogLabelSignature(variations, parts);
    const keys = getRuntimeProductCatalogIdentityKeys({ id, name, familyId });
    const previous = keys.map((key) => runtimeProductCatalogsByIdentity.get(key)).find(Boolean) ?? null;
    const sameCatalog = previous
      && previous.variationsRef === variations
      && previous.partsRef === parts
      && previous.labelSignature === labelSignature
      && String(previous.id ?? '') === String(id ?? '')
      && String(previous.name ?? '') === String(name ?? '')
      && String(previous.familyId ?? '') === String(familyId ?? '');
    if (sameCatalog) {
      runtimeProductCatalog = previous;
      return;
    }
    // Runtime-only read model: keep references instead of cloning the full tree.
    // Scene/export/history stay compact while every placed product can resolve
    // the latest editable variation labels immediately after catalog sync.
    const catalog: RuntimeProductCatalog = {
      id,
      name,
      familyId,
      variations,
      parts,
      variationsRef: variations,
      partsRef: parts,
      labelSignature,
    };
    if (previous) {
      for (const [key, value] of runtimeProductCatalogsByIdentity) {
        if (value === previous) runtimeProductCatalogsByIdentity.delete(key);
      }
    }
    for (const key of keys) runtimeProductCatalogsByIdentity.set(key, catalog);
    runtimeProductCatalog = catalog;
    runtimeProductCatalogRevision += 1;
    cachedCurrentProductPartsSource = null;
    cachedCurrentProductParts = null;
    cachedProductPartLookup = null;
    lastProductHoverHitKey = '';
    if (hoveredProductKey) clearProductHover(true);
  };

  const syncRuntimeProductCatalog = (elementsInput: any[]): void => {
    for (const element of Array.isArray(elementsInput) ? elementsInput : []) rememberRuntimeProductCatalog(element);
    const selectedProduct = getProductById(getSelectedProductId()) ?? getProductsFromState(currentState)[0] ?? null;
    runtimeProductCatalog = selectedProduct ? findRuntimeProductCatalog(selectedProduct) : runtimeProductCatalog;
  };

  const productCatalogMatchesStateProduct = (catalog: any, product: any): boolean => {
    if (!catalog || !product) return false;
    const catalogIds = [catalog.id, catalog.name].map((value) => String(value ?? '').trim()).filter(Boolean);
    const productIds = [product.id, product.name, product.catalogId].map((value) => String(value ?? '').trim()).filter(Boolean);
    if (!catalogIds.length || !productIds.length) return false;
    return catalogIds.some((left) => productIds.includes(left));
  };

  const shouldUseProductTextureAlpha = (config: any): boolean => {
    const direct = config?.useTextureAlpha ?? config?.alphaFromTexture ?? config?.useAlphaFromDiffuseTexture;
    const metadata = config?.metadata && typeof config.metadata === 'object' ? config.metadata : null;
    const metaValue = metadata?.useTextureAlpha ?? metadata?.alphaFromTexture ?? metadata?.useAlphaFromDiffuseTexture;
    const value = direct ?? metaValue;
    if (typeof value === 'boolean') return value;
    if (typeof value === 'string') return /^(1|true|yes|on)$/i.test(value.trim());
    return false;
  };

  const getMaterialOpacityValue = (config: any): number => {
    const raw = typeof config?.opacity === 'number' ? config.opacity : (typeof config?.alpha === 'number' ? config.alpha : 1);
    if (!Number.isFinite(raw)) return 1;
    return Math.max(0, Math.min(1, raw));
  };

  const getTextureNumberConfig = (config: any, keys: string[], fallback: number, options: { min?: number; max?: number } = {}): number => {
    const metadata = config?.metadata && typeof config.metadata === 'object' ? config.metadata : null;
    for (const key of keys) {
      const direct = Number(config?.[key]);
      if (Number.isFinite(direct)) return Math.max(options.min ?? -Infinity, Math.min(options.max ?? Infinity, direct));
      const meta = Number(metadata?.[key]);
      if (Number.isFinite(meta)) return Math.max(options.min ?? -Infinity, Math.min(options.max ?? Infinity, meta));
    }
    return fallback;
  };

  const applyProductTextureTiling = (texture: Texture | null, config: any): void => {
    if (!texture) return;
    const repeatX = getTextureNumberConfig(config, ['textureRepeatX', 'repeatX', 'textureScaleX', 'uScale', 'textureRepeat'], 1, { min: 0.01, max: 100 });
    const repeatY = getTextureNumberConfig(config, ['textureRepeatY', 'repeatY', 'textureScaleY', 'vScale', 'textureRepeat'], 1, { min: 0.01, max: 100 });
    const offsetX = getTextureNumberConfig(config, ['textureOffsetX', 'offsetX', 'uOffset'], 0, { min: -100, max: 100 });
    const offsetY = getTextureNumberConfig(config, ['textureOffsetY', 'offsetY', 'vOffset'], 0, { min: -100, max: 100 });
    try { texture.uScale = repeatX; } catch {}
    try { texture.vScale = repeatY; } catch {}
    try { texture.uOffset = offsetX; } catch {}
    try { texture.vOffset = offsetY; } catch {}
    // Babylon only repeats a scaled texture when the wrap mode is repeat. Keep it
    // forced for Product 3D variations so textureRepeatX/Y can make patterns
    // smaller without requiring custom UVs in the GLB.
    try { texture.wrapU = Texture.WRAP_ADDRESSMODE; } catch {}
    try { texture.wrapV = Texture.WRAP_ADDRESSMODE; } catch {}
  };

  const applyProductTextureOpacityMode = (mat: StandardMaterial | PBRMaterial, texture: Texture | null, config: any): void => {
    const hasExplicitOpacity = typeof config?.opacity === 'number' || typeof config?.alpha === 'number';
    const hasExplicitAlphaMode = config?.useTextureAlpha != null
      || config?.alphaFromTexture != null
      || config?.useAlphaFromDiffuseTexture != null
      || config?.metadata?.useTextureAlpha != null
      || config?.metadata?.alphaFromTexture != null
      || config?.metadata?.useAlphaFromDiffuseTexture != null;
    // A color-only variation is a patch over the imported material. Preserve
    // its authored transparency settings unless the variation explicitly
    // replaces the base texture or alpha configuration.
    if (!texture && !hasExplicitOpacity && !hasExplicitAlphaMode) return;
    const opacity = hasExplicitOpacity ? getMaterialOpacityValue(config) : Math.max(0, Math.min(1, Number(mat.alpha ?? 1) || 1));
    const useTextureAlpha = Boolean(texture) && shouldUseProductTextureAlpha(config);
    try { mat.alpha = opacity; } catch {}
    if (texture) {
      try { texture.hasAlpha = useTextureAlpha; } catch {}
    }
    try {
      if (mat instanceof PBRMaterial) mat.useAlphaFromAlbedoTexture = useTextureAlpha;
      else mat.useAlphaFromDiffuseTexture = useTextureAlpha;
    } catch {}
    try {
      if (mat instanceof StandardMaterial && useTextureAlpha && texture) {
        (mat as any).opacityTexture = texture;
      } else if (mat instanceof StandardMaterial && ((mat as any).opacityTexture === texture || !String(config?.opacityTextureUrl ?? '').trim())) {
        (mat as any).opacityTexture = null;
      }
    } catch {}
    try {
      (mat as any).transparencyMode = useTextureAlpha || opacity < 1 ? 2 : 0;
    } catch {}
  };

  const getProductBaseColorTexture = (material: any): Texture | null => {
    if (!material) return null;
    return ((material instanceof PBRMaterial || 'albedoTexture' in material)
      ? material.albedoTexture
      : material.diffuseTexture) as Texture | null;
  };

  const setProductBaseColorTexture = (material: any, texture: Texture | null): void => {
    if (!material) return;
    try {
      if (material instanceof PBRMaterial || 'albedoTexture' in material) material.albedoTexture = texture;
      else material.diffuseTexture = texture;
    } catch {}
  };

  const disposeOwnedMaterialTextures = (material: StandardMaterial | PBRMaterial | null | undefined): number => {
    if (!material) return 0;
    const slots = materialTexturesByMaterial.get(material);
    if (!slots) return 0;
    let disposed = 0;
    if (slots.baseColor) {
      try { if (getProductBaseColorTexture(material) === slots.baseColor.texture) setProductBaseColorTexture(material, null); } catch {}
      try { if ((material as any).opacityTexture === slots.baseColor.texture) (material as any).opacityTexture = null; } catch {}
      try { slots.baseColor.texture.dispose(); disposed += 1; } catch {}
    }
    if (slots.normal) {
      try { if ((material as any).bumpTexture === slots.normal.texture) (material as any).bumpTexture = null; } catch {}
      try { slots.normal.texture.dispose(); disposed += 1; } catch {}
    }
    materialTexturesByMaterial.delete(material);
    return disposed;
  };

  const isRuntimeProductMaterial = (material: any): material is StandardMaterial | PBRMaterial => Boolean(
    (material instanceof StandardMaterial || material instanceof PBRMaterial)
    && material?.metadata
    && typeof material.metadata === 'object'
    && material.metadata.asukaProductMaterialRuntime === true,
  );

  const disposeRuntimeProductMaterialIfUnused = (material: any, nextOwner?: Mesh | null): void => {
    if (!isRuntimeProductMaterial(material)) return;
    try {
      const stillUsed = scene?.meshes?.some((mesh) => !!mesh && mesh !== nextOwner && (mesh as any).material === material);
      if (stillUsed) return;
    } catch {}
    disposeOwnedMaterialTextures(material);
    try { material.dispose(false, false); } catch { try { material.dispose(); } catch {} }
  };

  const clearRuntimeProductMaterials = (): void => {
    const seen = new Set<any>();
    try {
      for (const material of scene?.materials ?? []) {
        if (!isRuntimeProductMaterial(material) || seen.has(material)) continue;
        seen.add(material);
        disposeRuntimeProductMaterialIfUnused(material, null);
      }
    } catch {}
  };

  const clearModel = () => {
    clearPrintAreaTextures();
    clearRuntimeProductMaterials();
    for (const [id, kind] of Array.from(objectKinds.entries())) {
      if (kind === 'mesh') {
        objectKinds.delete(id);
        objectMeshes.delete(id);
      }
    }
    for (const mesh of modelMeshes) {
      try { mesh.dispose(false, true); } catch {}
    }
    modelMeshes = [];
    runtimeParametricElementByProductId.clear();
    parametricBaselinesByProductId.clear();
    parametricMeasurementsByProductId.clear();
  };

  const clearDecorations = () => {
    for (const [id, mesh] of objectMeshes.entries()) {
      if (objectKinds.get(id) !== 'mesh') {
        disposeDecalFragments(id);
        try { mesh.dispose(false, true); } catch {}
        objectMeshes.delete(id);
        objectKinds.delete(id);
      }
    }
    for (const id of Array.from(decalFragmentsByObjectId.keys())) disposeDecalFragments(id);
    decorationRecords = [];
  };

  const clearPrintAreaVisuals = () => {
    for (const mesh of printAreaMeshes.values()) {
      try { mesh.dispose(false, true); } catch {}
    }
    printAreaMeshes.clear();
  };

  const clearPrintAreaTextures = () => {
    for (const runtime of printAreaTextures.values()) {
      try { if (runtime?.material && runtime.slot) runtime.material[runtime.slot] = runtime.originalTexture ?? null; } catch {}
      try { runtime?.texture?.dispose?.(); } catch {}
    }
    printAreaTextures.clear();
  };

  const clearPrintAreaDrawPreview = () => {
    const preview = printAreaDrawState?.previewMesh ?? null;
    const hover = printAreaDrawState?.hoverMesh ?? null;
    if (preview) {
      try { preview.dispose(false, true); } catch {}
    }
    if (hover && hover !== preview) {
      try { hover.dispose(false, true); } catch {}
    }
    if (printAreaDrawState) {
      printAreaDrawState.previewMesh = null;
      printAreaDrawState.hoverMesh = null;
    }
  };

  const fitView = (fitOptions: { radiusRatio?: number } = {}) => {
    const cam = requireCamera();
    const meshes = modelMeshes.filter((mesh) => {
      if (!mesh || mesh.isDisposed()) return false;
      try { return mesh.getTotalVertices() > 0; } catch { return true; }
    });
    if (!meshes.length) {
      cam.target = Vector3.Zero();
      cam.radius = 4.5;
      return;
    }

    let min = new Vector3(Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY);
    let max = new Vector3(Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY);
    for (const mesh of meshes) {
      try {
        mesh.computeWorldMatrix(true);
        const box = mesh.getBoundingInfo().boundingBox;
        min = Vector3.Minimize(min, box.minimumWorld);
        max = Vector3.Maximize(max, box.maximumWorld);
      } catch {}
    }

    if (!Number.isFinite(min.x) || !Number.isFinite(max.x)) return;
    const center = centerProductGroupInView ? (getProductGroupViewTarget() ?? min.add(max).scale(0.5)) : min.add(max).scale(0.5);
    const size = max.subtract(min).length();
    const radiusRatio = Math.max(0.25, Math.min(12, Number(fitOptions?.radiusRatio ?? cameraFitViewRadiusRatio) || cameraFitViewRadiusRatio));
    cam.target = center;
    cam.radius = Math.max(1.6, size * radiusRatio);
    applyProductCameraInputLock(cam);
  };

  const getRenderableModelMeshes = (meshes: Mesh[] = modelMeshes): Mesh[] => meshes.filter((mesh) => {
    if (!mesh || mesh.isDisposed()) return false;
    try {
      return mesh.getTotalVertices() > 0;
    } catch {
      return true;
    }
  });

  const GEOMETRY_COMPONENT_PART_PREFIX = '__component_';
  const MAX_RUNTIME_GEOMETRY_COMPONENT_PARTS = 14;
  const MIN_RUNTIME_GEOMETRY_COMPONENT_RATIO = 0.008;
  const MIN_RUNTIME_GEOMETRY_COMPONENT_VERTICES = 96;
  const MIN_RUNTIME_GEOMETRY_COMPONENT_TRIANGLES = 32;

  type RuntimeGeometryComponent = {
    sourceIndex: number;
    rank: number;
    vertexCount: number;
    triangleCount: number;
    vertexIndices: number[];
    triangleStarts: number[];
    bboxMin: [number, number, number];
    bboxMax: [number, number, number];
  };

  type RuntimeWorldBounds = { min: Vector3; max: Vector3; center: Vector3; size: Vector3 };

  type RuntimeComponentFocusTarget = {
    component?: RuntimeGeometryComponent;
    bounds: RuntimeWorldBounds;
    normal: Vector3;
    distanceScore: number;
    facingScore: number;
    sizeScore: number;
  };

  type RuntimeFocusViewVisibility = {
    occlusionScore: number;
    targetCoverage: number;
    projectedAreaScore: number;
    visibleAreaScore: number;
  };

  const runtimeGeometryComponentCache = new WeakMap<Mesh, RuntimeGeometryComponent[]>();
  const runtimeGeometryComponentTriangleRankCache = new WeakMap<Mesh, Map<number, number>>();

  const getMeshFloatData = (mesh: Mesh, kind: string): number[] => {
    try {
      const values = mesh.getVerticesData(kind) as number[] | Float32Array | null;
      return values ? Array.from(values as any) : [];
    } catch {
      return [];
    }
  };

  const getMeshIndicesArray = (mesh: Mesh, vertexCount: number): number[] => {
    try {
      const indices = mesh.getIndices();
      if (indices && indices.length) return Array.from(indices as any).map((value) => Number(value)).filter((value) => Number.isFinite(value) && value >= 0);
    } catch {}
    return Array.from({ length: Math.max(0, vertexCount) }, (_, index) => index);
  };

  const computeMeshConnectedComponents = (mesh: Mesh): RuntimeGeometryComponent[] => {
    const positions = getMeshFloatData(mesh, 'position');
    const vertexCount = Math.floor(positions.length / 3);
    const indices = getMeshIndicesArray(mesh, vertexCount);
    if (vertexCount <= 0 || indices.length < 3) return [];

    // Important performance detail for GLB files exported as logical part meshes:
    // each mesh can reuse the full product POSITION accessor while its indices
    // only reference a small subset of vertices. The previous implementation
    // walked every unreferenced vertex as an isolated component, which made the
    // first hover/focus on detailed products very expensive. Work only on
    // vertices referenced by this primitive's indices.
    const referencedVertices = new Set<number>();
    for (const index of indices) {
      const value = Number(index);
      if (Number.isFinite(value) && value >= 0 && value < vertexCount) referencedVertices.add(value);
    }
    if (!referencedVertices.size) return [];

    const parent = Array.from({ length: vertexCount }, (_, index) => index);
    const rank = Array.from({ length: vertexCount }, () => 0);
    const find = (input: number): number => {
      let value = input;
      while (parent[value] !== value) {
        parent[value] = parent[parent[value]];
        value = parent[value];
      }
      return value;
    };
    const union = (aInput: number, bInput: number) => {
      const a = Number(aInput);
      const b = Number(bInput);
      if (!Number.isFinite(a) || !Number.isFinite(b) || a < 0 || b < 0 || a >= vertexCount || b >= vertexCount) return;
      let rootA = find(a);
      let rootB = find(b);
      if (rootA === rootB) return;
      if (rank[rootA] < rank[rootB]) [rootA, rootB] = [rootB, rootA];
      parent[rootB] = rootA;
      if (rank[rootA] === rank[rootB]) rank[rootA] += 1;
    };

    for (let i = 0; i + 2 < indices.length; i += 3) {
      union(indices[i], indices[i + 1]);
      union(indices[i + 1], indices[i + 2]);
      union(indices[i + 2], indices[i]);
    }

    const byPosition = new Map<string, number>();
    for (const i of referencedVertices) {
      const x = positions[i * 3];
      const y = positions[i * 3 + 1];
      const z = positions[i * 3 + 2];
      const key = `${Math.round(x * 100000)}:${Math.round(y * 100000)}:${Math.round(z * 100000)}`;
      const previous = byPosition.get(key);
      if (previous == null) byPosition.set(key, i);
      else union(i, previous);
    }

    const byRoot = new Map<number, { vertices: Set<number>; triangleStarts: number[] }>();
    for (const i of referencedVertices) {
      const root = find(i);
      const current = byRoot.get(root) ?? { vertices: new Set<number>(), triangleStarts: [] };
      current.vertices.add(i);
      byRoot.set(root, current);
    }
    for (let i = 0; i + 2 < indices.length; i += 3) {
      const root = find(indices[i]);
      const current = byRoot.get(root);
      if (current) current.triangleStarts.push(i);
    }

    const components = Array.from(byRoot.values()).map((item, sourceIndex) => {
      const vertexIndices = Array.from(item.vertices.values());
      const bboxMin: [number, number, number] = [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY];
      const bboxMax: [number, number, number] = [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY];
      for (const vertexIndex of vertexIndices) {
        const offset = vertexIndex * 3;
        const x = positions[offset];
        const y = positions[offset + 1];
        const z = positions[offset + 2];
        bboxMin[0] = Math.min(bboxMin[0], x);
        bboxMin[1] = Math.min(bboxMin[1], y);
        bboxMin[2] = Math.min(bboxMin[2], z);
        bboxMax[0] = Math.max(bboxMax[0], x);
        bboxMax[1] = Math.max(bboxMax[1], y);
        bboxMax[2] = Math.max(bboxMax[2], z);
      }
      return {
        sourceIndex,
        rank: 0,
        vertexCount: vertexIndices.length,
        triangleCount: item.triangleStarts.length,
        vertexIndices,
        triangleStarts: item.triangleStarts,
        bboxMin,
        bboxMax,
      } as RuntimeGeometryComponent;
    }).sort((a, b) => (b.triangleCount - a.triangleCount) || (b.vertexCount - a.vertexCount));

    return components.map((component, rankIndex) => ({ ...component, rank: rankIndex }));
  };

  const getUsableGeometryComponents = (mesh: Mesh): RuntimeGeometryComponent[] => {
    const cached = runtimeGeometryComponentCache.get(mesh);
    if (cached) return cached;
    const totalVertices = (() => { try { return mesh.getTotalVertices(); } catch { return 0; } })();
    const minVertices = Math.max(MIN_RUNTIME_GEOMETRY_COMPONENT_VERTICES, Math.round(totalVertices * MIN_RUNTIME_GEOMETRY_COMPONENT_RATIO));
    const components = computeMeshConnectedComponents(mesh)
      .filter((component) => component.vertexCount >= minVertices && component.triangleCount >= MIN_RUNTIME_GEOMETRY_COMPONENT_TRIANGLES)
      .slice(0, MAX_RUNTIME_GEOMETRY_COMPONENT_PARTS);
    runtimeGeometryComponentCache.set(mesh, components);
    return components;
  };

  const getGeometryComponentTriangleRankMap = (mesh: Mesh): Map<number, number> => {
    const cached = runtimeGeometryComponentTriangleRankCache.get(mesh);
    if (cached) return cached;
    const map = new Map<number, number>();
    for (const component of getUsableGeometryComponents(mesh)) {
      for (const triangleStart of component.triangleStarts) map.set(triangleStart, component.rank);
    }
    runtimeGeometryComponentTriangleRankCache.set(mesh, map);
    return map;
  };

  const summarizeGeometryComponentsForMesh = (mesh: Mesh, options: { viewId?: string | null } = {}): Asuka3DMeshSummary[] => {
    const components = getUsableGeometryComponents(mesh);
    if (components.length <= 1) return [];
    const meshName = String(mesh.name || mesh.id || 'mesh').trim() || 'mesh';
    const material: any = mesh.material ?? null;
    const materialId = String(material?.id ?? '').trim();
    const materialName = String(material?.name ?? '').trim();
    return components.map((component) => {
      const id = `${meshName}${GEOMETRY_COMPONENT_PART_PREFIX}${component.rank + 1}`;
      return {
        id,
        label: `${meshName} / Part ${component.rank + 1}`,
        name: id,
        materialId: materialId || undefined,
        materialName: materialName || undefined,
        vertices: component.vertexCount,
        visible: mesh.isEnabled(),
        metadata: {
          engine: 'babylon',
          viewId: options.viewId ?? currentViewId,
          nativeId: id,
          nativeName: id,
          sourceMeshId: String(mesh.id ?? '').trim() || undefined,
          sourceMeshName: meshName,
          componentIndex: component.sourceIndex,
          componentRank: component.rank,
          componentVertices: component.vertexCount,
          componentTriangles: component.triangleCount,
          componentBboxMin: component.bboxMin,
          componentBboxMax: component.bboxMax,
          detectedFrom: 'runtime-geometry-components',
          detectionKind: 'geometry-component',
        },
      } as Asuka3DMeshSummary;
    });
  };

  const cloneMeshMaterial = (mesh: Mesh, id: string): any => {
    const material: any = mesh.material ?? null;
    if (!material) return null;
    try {
      const cloned = typeof material.clone === 'function' ? material.clone(`${String(material.name || material.id || 'material')}-${id}`) : null;
      if (cloned) return cloned;
    } catch {}
    return material;
  };

  const copyMeshTransform = (source: Mesh, target: Mesh) => {
    try { target.parent = source.parent; } catch {}
    try { target.position.copyFrom(source.position); } catch {}
    try { target.rotation.copyFrom(source.rotation); } catch {}
    try { target.scaling.copyFrom(source.scaling); } catch {}
    try { if (source.rotationQuaternion) target.rotationQuaternion = source.rotationQuaternion.clone(); } catch {}
    try { target.isVisible = source.isVisible; } catch {}
    try { target.setEnabled(source.isEnabled()); } catch {}
    try { target.checkCollisions = source.checkCollisions; } catch {}
    try { target.receiveShadows = source.receiveShadows; } catch {}
  };

  const createComponentMeshFromTriangles = (source: Mesh, id: string, triangleStarts: number[]): Mesh | null => {
    if (!scene || !triangleStarts.length) return null;
    const positions = getMeshFloatData(source, 'position');
    const normals = getMeshFloatData(source, 'normal');
    const uvs = getMeshFloatData(source, 'uv');
    const uvs2 = getMeshFloatData(source, 'uv2');
    const colors = getMeshFloatData(source, 'color');
    const vertexCount = Math.floor(positions.length / 3);
    const indices = getMeshIndicesArray(source, vertexCount);
    if (!positions.length || !indices.length) return null;

    const vertexMap = new Map<number, number>();
    const nextPositions: number[] = [];
    const nextNormals: number[] = [];
    const nextUvs: number[] = [];
    const nextUvs2: number[] = [];
    const nextColors: number[] = [];
    const nextIndices: number[] = [];
    const addVertex = (sourceIndex: number): number => {
      const existing = vertexMap.get(sourceIndex);
      if (existing != null) return existing;
      const nextIndex = vertexMap.size;
      vertexMap.set(sourceIndex, nextIndex);
      const p = sourceIndex * 3;
      nextPositions.push(positions[p] ?? 0, positions[p + 1] ?? 0, positions[p + 2] ?? 0);
      if (normals.length >= p + 3) nextNormals.push(normals[p] ?? 0, normals[p + 1] ?? 0, normals[p + 2] ?? 0);
      const uv = sourceIndex * 2;
      if (uvs.length >= uv + 2) nextUvs.push(uvs[uv] ?? 0, uvs[uv + 1] ?? 0);
      if (uvs2.length >= uv + 2) nextUvs2.push(uvs2[uv] ?? 0, uvs2[uv + 1] ?? 0);
      const c = sourceIndex * 4;
      if (colors.length >= c + 4) nextColors.push(colors[c] ?? 1, colors[c + 1] ?? 1, colors[c + 2] ?? 1, colors[c + 3] ?? 1);
      return nextIndex;
    };

    for (const start of triangleStarts) {
      if (start < 0 || start + 2 >= indices.length) continue;
      nextIndices.push(addVertex(indices[start]), addVertex(indices[start + 1]), addVertex(indices[start + 2]));
    }
    if (nextIndices.length < 3 || nextPositions.length < 9) return null;

    const mesh = new Mesh(id, scene);
    mesh.id = id;
    mesh.name = id;
    copyMeshTransform(source, mesh);
    const vertexData = new VertexData();
    vertexData.positions = nextPositions;
    vertexData.indices = nextIndices;
    if (nextNormals.length === nextPositions.length) vertexData.normals = nextNormals;
    if (nextUvs.length === (nextPositions.length / 3) * 2) vertexData.uvs = nextUvs;
    if (nextUvs2.length === (nextPositions.length / 3) * 2) vertexData.uvs2 = nextUvs2;
    if (nextColors.length === (nextPositions.length / 3) * 4) vertexData.colors = nextColors;
    try {
      if (!vertexData.normals || !vertexData.normals.length) {
        const generatedNormals: number[] = [];
        VertexData.ComputeNormals(nextPositions, nextIndices, generatedNormals);
        vertexData.normals = generatedNormals;
      }
    } catch {}
    vertexData.applyToMesh(mesh);
    mesh.material = cloneMeshMaterial(source, id);
    try { mesh.computeWorldMatrix(true); } catch {}
    return mesh;
  };

  const isGeometryComponentPart = (part: any): boolean => Boolean(
    part?.metadata?.detectionKind === 'geometry-component'
    || part?.metadata?.detectedFrom === 'runtime-geometry-components'
    || (part?.metadata?.detectedFrom === 'model-file-upload' && part?.metadata?.componentRank != null)
    || split3DList(part?.meshNames).some((name) => name.includes(GEOMETRY_COMPONENT_PART_PREFIX)),
  );

  const splitConfiguredGeometryComponentMeshes = (meshes: Mesh[], partsInput: any): Mesh[] => {
    const parts = normalize3DParts(partsInput).filter(isGeometryComponentPart);
    if (!parts.length) return meshes;
    const renderable = getRenderableModelMeshes(meshes);
    if (renderable.length !== 1) return meshes;
    const source = renderable[0];
    const components = getUsableGeometryComponents(source);
    if (components.length <= 1) return meshes;
    const componentsByRank = new Map(components.map((component) => [component.rank, component]));
    const usedTriangleStarts = new Set<number>();
    const newMeshes: Mesh[] = [];
    const meshName = String(source.name || source.id || 'mesh').trim() || 'mesh';

    for (const part of parts) {
      const meshNameHint = String(split3DList(part?.meshNames)[0] ?? '').trim();
      const componentMatch = meshNameHint.match(/__component_(\d+)$/i);
      let rankIndex = Number(part?.metadata?.componentRank);
      if (!Number.isFinite(rankIndex)) rankIndex = componentMatch ? Number(componentMatch[1]) - 1 : NaN;
      const component = componentsByRank.get(rankIndex);
      if (!component) continue;
      const meshId = split3DList(part?.meshNames)[0] || `${meshName}${GEOMETRY_COMPONENT_PART_PREFIX}${component.rank + 1}`;
      const partMesh = createComponentMeshFromTriangles(source, meshId, component.triangleStarts);
      if (!partMesh) continue;
      partMesh.metadata = {
        ...((partMesh.metadata && typeof partMesh.metadata === 'object') ? partMesh.metadata : {}),
        asukaProduct3DPartId: String(part?.id ?? ''),
        asukaGeometryComponent: true,
        sourceMeshId: String(source.id ?? ''),
        sourceMeshName: meshName,
        componentRank: component.rank,
      };
      for (const triangleStart of component.triangleStarts) usedTriangleStarts.add(triangleStart);
      newMeshes.push(partMesh);
    }

    if (!newMeshes.length) return meshes;
    const sourceIndices = getMeshIndicesArray(source, (() => { try { return source.getTotalVertices(); } catch { return 0; } })());
    const restTriangleStarts: number[] = [];
    for (let i = 0; i + 2 < sourceIndices.length; i += 3) {
      if (!usedTriangleStarts.has(i)) restTriangleStarts.push(i);
    }
    const restMesh = createComponentMeshFromTriangles(source, `${meshName}__rest`, restTriangleStarts);
    if (restMesh) {
      restMesh.metadata = {
        ...((restMesh.metadata && typeof restMesh.metadata === 'object') ? restMesh.metadata : {}),
        asukaGeometryComponentRest: true,
        sourceMeshId: String(source.id ?? ''),
        sourceMeshName: meshName,
      };
      newMeshes.push(restMesh);
    }

    try { source.setEnabled(false); } catch {}
    try { source.dispose(false, false); } catch {}
    return meshes.filter((mesh) => mesh !== source).concat(newMeshes);
  };


  const summarizeModelMeshes = (meshes: Mesh[], options: { includeProductAlias?: boolean; includeGeometryComponents?: boolean; viewId?: string | null } = {}): Asuka3DMeshSummary[] => {
    const summaries: Asuka3DMeshSummary[] = [];
    const seen = new Set<string>();
    const addMesh = (mesh: Mesh | null | undefined, aliasId?: string, aliasLabel?: string) => {
      if (!mesh || mesh.isDisposed()) return;
      const id = String(aliasId ?? mesh.id ?? mesh.name ?? '').trim();
      if (!id || seen.has(id)) return;
      seen.add(id);
      let vertices: number | undefined;
      try { vertices = mesh.getTotalVertices?.(); } catch {}
      const material = (mesh as any).material ?? null;
      const materialId = String(material?.id ?? '').trim();
      const materialName = String(material?.name ?? '').trim();
      const meshName = String(mesh.name ?? '').trim();
      const meshId = String(mesh.id ?? '').trim();
      summaries.push({
        id,
        label: aliasLabel ?? meshName ?? meshId ?? id,
        name: meshName || meshId || id,
        materialId: materialId || undefined,
        materialName: materialName || undefined,
        vertices,
        visible: mesh.isEnabled(),
        metadata: {
          engine: 'babylon',
          viewId: options.viewId ?? currentViewId,
          nativeId: meshId || undefined,
          nativeName: meshName || undefined,
          isProductAlias: aliasId === 'product',
        },
      });
    };

    const renderable = getRenderableModelMeshes(meshes);
    if (options.includeProductAlias) {
      const productMesh = objectMeshes.get('product');
      if (productMesh) addMesh(productMesh, 'product', 'Product (auto)');
    }
    for (const mesh of renderable) addMesh(mesh);
    if (options.includeGeometryComponents && renderable.length === 1 && summaries.length <= 1) {
      const componentSummaries = summarizeGeometryComponentsForMesh(renderable[0], { viewId: options.viewId });
      if (componentSummaries.length > 1) return componentSummaries;
    }
    return summaries;
  };

  const getModelMeshSummaries = (): Asuka3DMeshSummary[] => summarizeModelMeshes(modelMeshes, { includeProductAlias: true, viewId: currentViewId });

  const inspectModelParts = async (input: { model: Asuka3DModel }): Promise<Asuka3DMeshSummary[]> => {
    debugBabylonProduct3D('inspect-model-parts:start', { input });
    const model = {
      ...input.model,
      url: String(input.model.url ?? '').trim(),
      format: String(input.model.format ?? '').trim() || undefined,
    };
    const url = String(model.url ?? '').trim();
    if (!url || !engine) {
      debugBabylonProduct3D('inspect-model-parts:skip', { hasUrl: Boolean(url), hasEngine: Boolean(engine), model }, 'warn');
      return [];
    }

    let inspectScene: Scene | null = null;
    try {
      inspectScene = new Scene(engine);
      inspectScene.autoClear = false;
      const loaderUrl = await getSceneLoaderUrl(url, model as any);
      const { rootUrl, sceneFilename } = splitSceneUrl(loaderUrl);
      const pluginExtension = getModelPluginExtension(model as any, url);
      debugBabylonProduct3D('inspect-model-parts:import-start', { model, loaderUrl, rootUrl, sceneFilename, pluginExtension });
      await ensureGltfLoader();
      const result = await SceneLoader.ImportMeshAsync('', rootUrl, sceneFilename, inspectScene, undefined, pluginExtension);
      const importedMeshes = result.meshes.filter((mesh: any): mesh is Mesh => mesh instanceof Mesh);
      const summaries = summarizeModelMeshes(importedMeshes, { includeProductAlias: false, includeGeometryComponents: true, viewId: currentViewId });
      debugBabylonProduct3D('inspect-model-parts:import-success', { meshCount: importedMeshes.length, meshes: importedMeshes, summaryCount: summaries.length, summaries });
      return summaries;
    } catch (error) {
      debugBabylonProduct3D('inspect-model-parts:import-error', { input, model, error }, 'error');
      return [];
    } finally {
      try { inspectScene?.dispose(); } catch {}
    }
  };


  const normalizeMaterialId = (id: unknown): string => String(id ?? '').replace(/^material:/, '').trim();

  const materialConfigFromNative = (material: any): Record<string, unknown> => {
    const out: Record<string, unknown> = {};
    try {
      const color = material?.albedoColor ?? material?.diffuseColor;
      if (color) out.color = color.toHexString?.() ?? undefined;
    } catch {}
    try {
      const baseColorTexture = material?.albedoTexture ?? material?.diffuseTexture ?? null;
      const textureUrl = String(baseColorTexture?.url ?? baseColorTexture?.name ?? '').trim();
      if (textureUrl && (!/^data:/i.test(textureUrl) || textureUrl.length <= 4096)) out.textureUrl = textureUrl;
      out.hasBaseColorTexture = Boolean(baseColorTexture);
    } catch {}
    try {
      const normalTexture = material?.bumpTexture ?? null;
      const normalTextureUrl = String(normalTexture?.url ?? normalTexture?.name ?? '').trim();
      if (normalTextureUrl && (!/^data:/i.test(normalTextureUrl) || normalTextureUrl.length <= 4096)) out.normalTextureUrl = normalTextureUrl;
      out.hasNormalTexture = Boolean(normalTexture);
    } catch {}
    try { out.nativeMaterialClass = String(material?.metadata?.asukaNativeMaterialClass ?? material?.getClassName?.() ?? material?.constructor?.name ?? ''); } catch {}
    return out;
  };

  const getMaterialSummaries = (): Asuka3DMaterialSummary[] => {
    const summaries: Asuka3DMaterialSummary[] = [];
    const byId = new Map<string, Asuka3DMaterialSummary>();
    const add = (idInput: unknown, configInput?: any, meshId?: string, materialLabel?: string) => {
      const id = normalizeMaterialId(idInput);
      if (!id) return;
      const config = configInput && typeof configInput === 'object' ? configInput : {};
      const current = byId.get(id) ?? {
        id,
        label: String(config?.label ?? materialLabel ?? id),
        selected: selectedMaterialId === id,
        targetMeshIds: split3DList(config?.targetMeshIds ?? config?.metadata?.targetMeshIds),
        metadata: { engine: 'babylon', viewId: currentViewId },
      } as Asuka3DMaterialSummary;
      if (config?.color && !current.color) current.color = String(config.color);
      if (config?.textureUrl && !current.textureUrl) current.textureUrl = String(config.textureUrl);
      if (config?.normalTextureUrl && !current.normalTextureUrl) current.normalTextureUrl = String(config.normalTextureUrl);
      if (typeof config?.metallic === 'number') current.metallic = Number(config.metallic);
      if (typeof config?.roughness === 'number') current.roughness = Number(config.roughness);
      if (config?.label) current.label = String(config.label);
      for (const targetMeshId of split3DList(config?.targetMeshIds ?? config?.metadata?.targetMeshIds)) {
        if (targetMeshId && !current.targetMeshIds?.includes(targetMeshId)) current.targetMeshIds = [...(current.targetMeshIds ?? []), targetMeshId];
      }
      if (meshId && !current.targetMeshIds?.includes(meshId)) current.targetMeshIds = [...(current.targetMeshIds ?? []), meshId];
      current.selected = selectedMaterialId === id;
      current.metadata = { ...(current.metadata ?? {}), material: clonePlain(config), engine: 'babylon', viewId: currentViewId };
      byId.set(id, current);
    };

    for (const [id, config] of Object.entries(materialState ?? {})) add(id, config);
    for (const mesh of getRenderableModelMeshes(modelMeshes)) {
      const material: any = (mesh as any).material ?? null;
      const materialId = String(material?.id ?? material?.name ?? '').trim();
      const meshId = String(mesh?.id ?? mesh?.name ?? '').trim();
      if (materialId) add(materialId, { ...materialConfigFromNative(material), ...((materialState as any)[materialId] ?? {}) }, meshId, String(material?.name ?? materialId));
      const meshAlias = String(mesh?.id ?? mesh?.name ?? '').trim();
      if (meshAlias && (materialState as any)[meshAlias]) add(meshAlias, (materialState as any)[meshAlias], meshId);
    }
    if ((materialState as any).product) {
      const meshIds = getRenderableModelMeshes(modelMeshes).map((mesh) => String(mesh.id || mesh.name || '')).filter(Boolean);
      add('product', (materialState as any).product, meshIds[0]);
      const product = byId.get('product');
      if (product) product.targetMeshIds = Array.from(new Set([...(product.targetMeshIds ?? []), ...meshIds]));
    }

    for (const item of byId.values()) summaries.push(item);
    summaries.sort((a, b) => String(a.label ?? a.id).localeCompare(String(b.label ?? b.id)));
    return summaries;
  };

  const selectMaterial = (id: string | null) => {
    const next = normalizeMaterialId(id);
    selectedMaterialId = next || null;
    if (selectedMaterialId) setSelectedObject(null, false);
    emitChanged('selectMaterial', selectedMaterialId);
  };

  const updateMaterial = (idInput: string, patch: Asuka3DMaterialPatch = {}) => {
    const id = normalizeMaterialId(idInput);
    if (!id) return;
    const current = (materialState as any)[id] && typeof (materialState as any)[id] === 'object' ? (materialState as any)[id] : {};
    const next: Record<string, unknown> = { ...current };
    if (typeof patch.color === 'string') {
      const value = patch.color.trim();
      if (value) next.color = value;
      else delete next.color;
    }
    if (typeof patch.textureUrl === 'string') {
      const value = patch.textureUrl.trim();
      if (value) next.textureUrl = value;
      else delete next.textureUrl;
    }
    if (typeof patch.normalTextureUrl === 'string') {
      const value = patch.normalTextureUrl.trim();
      if (value) next.normalTextureUrl = value;
      else delete next.normalTextureUrl;
    }
    if (typeof patch.label === 'string') {
      const value = patch.label.trim();
      if (value) next.label = value;
      else delete next.label;
    }
    if (typeof patch.metallic === 'number' && Number.isFinite(patch.metallic)) next.metallic = Math.max(0, Math.min(1, patch.metallic));
    if (typeof patch.roughness === 'number' && Number.isFinite(patch.roughness)) next.roughness = Math.max(0, Math.min(1, patch.roughness));
    if (patch.metadata && typeof patch.metadata === 'object') next.metadata = { ...((next as any).metadata ?? {}), ...(patch.metadata as any) };
    (materialState as any)[id] = next;
    currentState = { ...((currentState ?? {}) as any), materials: compact3DMaterialStateForPersistence(materialState) } as Asuka3DSceneState;
    applyMaterialState();
    emitChanged('updateMaterial', id);
  };

  const getEngineCapabilities = () => ({
    scene3d: true,
    objects: true,
    meshes: true,
    materials: true,
    parts: true,
    partVariations: true,
    parametricProducts: true,
    assembly3d: true,
    printAreas: true,
    surfaceConstraints: true,
    surfacePicking: true,
    surfaceVisibility: true,
    decorations: true,
    addImage: true,
    addText: true,
    addDesign: true,
    addElement: true,
    selectObject: true,
    transform: true,
    color: true,
    visibility: true,
    locking: true,
    removeObject: true,
    duplicateObject: true,
    objectTransform: true,
    reorderObjects: true,
    zOrder: true,
    previewObject: true,
    selectionToolbar: true,
    exportPng: true,
    exportJson: true,
    exportState: true,
    download: true,
    history: true,
  });

  const getSceneGraphNodes = (): AsukaSceneGraphNode[] => {
    const nodes: AsukaSceneGraphNode[] = [];
    const viewId = currentViewId;
    const product = (currentState as any)?.product;
    const hasProduct = modelMeshes.some((mesh) => !!mesh && !mesh.isDisposed());
    const productId = product && typeof product === 'object'
      ? `product:${String(product.id ?? 'current')}`
      : (hasProduct ? 'product:current' : null);

    if (productId) {
      nodes.push({
        id: productId,
        kind: 'product-3d',
        category: 'product',
        label: String(product?.name ?? 'Product 3D'),
        visible: hasProduct && modelMeshes.some((mesh) => mesh.isEnabled()),
        locked: true,
        readonly: true,
        selectable: false,
        depth: 0,
        metadata: { engine: 'babylon', viewId, itemGroup: 'Product', product: compactProductForUi(product), ...(lastProductPick ? clonePlain(lastProductPick) : {}) },
        actions: { select: false, hide: true, lock: false, delete: false, transform: false, color: false },
      });
    }

    for (const mesh of getModelMeshSummaries()) {
      nodes.push({
        id: mesh.id,
        kind: 'mesh',
        category: 'mesh',
        label: mesh.label ?? mesh.name ?? mesh.id,
        visible: mesh.visible !== false,
        locked: true,
        readonly: true,
        selectable: false,
        selected: false,
        parentId: productId,
        depth: productId ? 1 : 0,
        metadata: { ...(mesh.metadata ?? {}), materialId: mesh.materialId, materialName: mesh.materialName, vertices: mesh.vertices },
        actions: { select: false, hide: true, lock: false, delete: false, transform: false, color: false },
      });
    }

    const seenMaterials = new Set<string>();
    const addMaterialNode = (id: string, label?: string, metadata?: Record<string, unknown>) => {
      const materialId = String(id ?? '').trim();
      if (!materialId || seenMaterials.has(materialId)) return;
      seenMaterials.add(materialId);
      nodes.push({
        id: `material:${materialId}`,
        kind: 'material',
        category: 'material',
        label: label || materialId,
        visible: true,
        locked: false,
        readonly: true,
        selectable: false,
        selected: false,
        parentId: productId,
        depth: productId ? 1 : 0,
        metadata: { engine: 'babylon', viewId, materialId, ...(metadata ?? {}) },
        actions: { select: false, hide: false, lock: false, delete: false, transform: false, color: false },
      });
    };

    for (const mesh of getModelMeshSummaries()) {
      if (mesh.materialId) addMaterialNode(mesh.materialId, mesh.materialName, { sourceMeshId: mesh.id });
    }
    Object.entries(materialState ?? {}).forEach(([id, value]) => addMaterialNode(id, id, { material: clonePlain(value) }));

    const printAreas = Array.isArray((currentState as any)?.printAreas) ? (currentState as any).printAreas as Asuka3DPrintArea[] : [];
    for (const area of printAreas) {
      const id = String(area?.id ?? '').trim();
      if (!id) continue;
      nodes.push({
        id,
        kind: 'print-area',
        category: 'print-area',
        label: String(area?.label ?? id),
        visible: printAreaMeshes.get(id)?.isEnabled?.() !== false,
        locked: (area as any)?.locked === true,
        readonly: false,
        selectable: true,
        selected: getActivePrintAreaId() === id,
        parentId: area.targetMeshId || productId || null,
        depth: 2,
        metadata: { engine: 'babylon', viewId, printArea: clonePlain(area), targetMeshId: area.targetMeshId },
        actions: { select: true, hide: true, lock: true, delete: true, transform: false, color: false },
      });
    }

    for (const area of printAreas) {
      if (!isTextureSlotPrintArea(area)) continue;
      const areaId = String(area?.id ?? '').trim();
      for (const object of Array.isArray((area as any)?.objects) ? (area as any).objects : []) {
        const id = String(object?.id ?? '').trim();
        if (!id) continue;
        const kind = String(object?.type ?? 'object');
        nodes.push({
          id,
          kind,
          category: 'decoration',
          label: String(object?.label ?? object?.source?.text ?? id),
          visible: object?.visible !== false,
          locked: object?.locked === true || (area as any)?.locked === true,
          readonly: false,
          selectable: true,
          selected: selectedObjectId === id,
          parentId: areaId || productId || null,
          depth: 3,
          metadata: {
            engine: 'babylon',
            viewId,
            source: clonePlain(object?.source ?? {}),
            placement: 'texture',
            textureObject: clonePlain(object),
            targetMeshId: area.targetMeshId,
            targetPrintAreaId: areaId,
          },
          actions: { select: true, hide: true, lock: true, delete: true, transform: true, color: true },
        });
      }
    }

    for (const deco of decorationRecords) {
      const id = String(deco?.id ?? '').trim();
      if (!id) continue;
      const mesh = objectMeshes.get(id);
      const kind = String(deco?.type ?? objectKinds.get(id) ?? 'object');
      nodes.push({
        id,
        kind,
        category: 'decoration',
        label: String((deco as any)?.label ?? deco?.source?.text ?? mesh?.name ?? id),
        visible: (deco as any)?.visible !== false && mesh?.isEnabled?.() !== false,
        locked: (deco as any)?.locked === true,
        readonly: false,
        selectable: true,
        selected: selectedObjectId === id,
        parentId: deco.targetPrintAreaId || deco.targetMeshId || productId || null,
        depth: deco.targetPrintAreaId ? 3 : 2,
        metadata: {
          engine: 'babylon',
          viewId,
          source: clonePlain(deco.source ?? {}),
          placement: clonePlain(deco.placement ?? {}),
          price: deco.price,
          targetMeshId: deco.targetMeshId,
          targetPrintAreaId: (deco as any).targetPrintAreaId,
          screenBox: getMeshScreenBox(mesh ?? null),
        },
        actions: { select: true, hide: true, lock: true, delete: true, transform: true, color: true },
      });
    }

    return nodes;
  };


  const normalizeImportedModel = (meshes: Mesh[]) => {
    const renderable = getRenderableModelMeshes(meshes);
    if (!renderable.length) return;

    let min = new Vector3(Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY);
    let max = new Vector3(Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY);
    for (const mesh of renderable) {
      try {
        mesh.computeWorldMatrix(true);
        const box = mesh.getBoundingInfo().boundingBox;
        min = Vector3.Minimize(min, box.minimumWorld);
        max = Vector3.Maximize(max, box.maximumWorld);
      } catch {}
    }

    if (!Number.isFinite(min.x) || !Number.isFinite(max.x)) return;
    const center = min.add(max).scale(0.5);
    const size = max.subtract(min);
    const maxAxis = Math.max(Math.abs(size.x), Math.abs(size.y), Math.abs(size.z));
    const scale = Number.isFinite(maxAxis) && maxAxis > 0.0001 ? Math.min(100, Math.max(0.001, 2.35 / maxAxis)) : 1;
    const targets = getProductInstanceRootMeshes(meshes);

    for (const mesh of targets) {
      try {
        mesh.position = mesh.position.subtract(center);
        mesh.scaling = mesh.scaling.scale(scale);
      } catch {}
    }
    for (const mesh of meshes) {
      try { mesh.computeWorldMatrix(true); } catch {}
    }
  };

  const findMaterialTargets = (key: string, configInput?: any): Mesh[] => {
    const normalized = String(key ?? '').trim();
    if (!scene || !normalized) return [];
    const meshes = scene.meshes.filter((mesh): mesh is Mesh => mesh instanceof Mesh);
    const explicitTargets = split3DList(configInput?.targetMeshIds ?? configInput?.targetMeshes ?? configInput?.metadata?.targetMeshIds);
    if (explicitTargets.length) {
      const wanted = new Set(explicitTargets);
      const exactTargets = meshes.filter((mesh) => {
        const materialName = String((mesh.material as any)?.name ?? '').trim();
        return wanted.has(String(mesh.id ?? '')) || wanted.has(String(mesh.name ?? '')) || wanted.has(materialName);
      });
      if (exactTargets.length) return exactTargets;
    }
    if (normalized === '*') return meshes;
    const exact = meshes.filter((mesh) => {
      const materialName = String((mesh.material as any)?.name ?? '').trim();
      return mesh.id === normalized || mesh.name === normalized || materialName === normalized;
    });
    if (exact.length) return exact;
    // In product-3d variations, the default material target is usually "product".
    // Real GLB/glTF files rarely have a mesh literally named product, so map that
    // alias to the imported renderable product meshes instead of silently applying
    // the material only to the fallback placeholder.
    if (/^(product|main|default)$/i.test(normalized)) return getRenderableModelMeshes(modelMeshes);
    return [];
  };

  const getProductMaterialPatchSignature = (key: string, config: any): string => {
    const summarizeString = (value: unknown): string => {
      const raw = String(value ?? '').trim();
      if (raw.length <= 512) return raw;
      return `${raw.length}:${raw.slice(0, 192)}:${raw.slice(-192)}`;
    };
    return JSON.stringify([
      key,
      summarizeString(config?.color ?? config?.diffuseColor),
      summarizeString(config?.emissiveColor),
      summarizeString(config?.specularColor),
      summarizeString(config?.textureUrl),
      summarizeString(config?.normalTextureUrl),
      config?.opacity ?? null,
      config?.alpha ?? null,
      config?.metallic ?? null,
      config?.roughness ?? null,
      shouldUseProductTextureAlpha(config),
      getTextureNumberConfig(config, ['textureRepeatX', 'repeatX', 'textureScaleX', 'uScale', 'textureRepeat'], 1),
      getTextureNumberConfig(config, ['textureRepeatY', 'repeatY', 'textureScaleY', 'vScale', 'textureRepeat'], 1),
      getTextureNumberConfig(config, ['textureOffsetX', 'offsetX', 'uOffset'], 0),
      getTextureNumberConfig(config, ['textureOffsetY', 'offsetY', 'vOffset'], 0),
    ]);
  };

  const applyMaterialState = () => {
    const endSpan = beginAsuka3DPerfSpan('material', 'apply-state', {
      configuredTargets: Object.keys(materialState).length,
    });
    const sc = requireScene();
    let keys = 0;
    let targetMeshes = 0;
    let clonedMaterials = 0;
    let createdMaterials = 0;
    let reusedMaterials = 0;
    let assignedMaterials = 0;
    let createdTextures = 0;
    let disposedTextures = 0;
    const changedTargetMeshes = new Set<Mesh>();
    for (const [key, config] of Object.entries(materialState)) {
      keys += 1;
      const targets = findMaterialTargets(key, config);
      targetMeshes += targets.length;
      for (const mesh of targets) {
        const currentMaterial: any = mesh.material ?? null;
        if ((config as any)?.useNativeMaterial === true) {
          const hasNativeMaterial = nativeProductMaterialByMesh.has(mesh);
          const nativeMaterial = hasNativeMaterial ? nativeProductMaterialByMesh.get(mesh) : null;
          if (hasNativeMaterial && mesh.material !== nativeMaterial) {
            mesh.material = nativeMaterial ?? null;
            assignedMaterials += 1;
            changedTargetMeshes.add(mesh);
            disposeRuntimeProductMaterialIfUnused(currentMaterial, mesh);
          }
          continue;
        }
        const patchSignature = getProductMaterialPatchSignature(key, config);
        let mat: StandardMaterial | PBRMaterial | null = null;
        const currentPatchKey = String(currentMaterial?.metadata?.asukaMaterialPatchKey ?? '');
        const currentPatchSignature = String(currentMaterial?.metadata?.asukaMaterialPatchSignature ?? '');
        if (isRuntimeProductMaterial(currentMaterial) && currentPatchKey === key && currentPatchSignature === patchSignature) {
          mat = currentMaterial;
          reusedMaterials += 1;
        } else {
          const baselineMaterial = nativeProductMaterialByMesh.has(mesh)
            ? nativeProductMaterialByMesh.get(mesh)
            : (!isRuntimeProductMaterial(currentMaterial) ? currentMaterial : null);
          try {
            const cloned = baselineMaterial?.clone?.(`asuka-material-${key}-${String(mesh.id || mesh.name || makeId('mesh'))}`);
            if (cloned instanceof StandardMaterial || cloned instanceof PBRMaterial) {
              mat = cloned;
              clonedMaterials += 1;
            }
          } catch {}
        }
        if (!mat) {
          mat = new StandardMaterial(`asuka-material-${key}-${String(mesh.id || mesh.name || makeId('mesh'))}`, sc);
          createdMaterials += 1;
        }
        mat.metadata = {
          ...((mat.metadata && typeof mat.metadata === 'object') ? mat.metadata : {}),
          asukaMaterialPatchKey: key,
          asukaMaterialPatchSignature: patchSignature,
          asukaProductMaterialRuntime: true,
          asukaNativeMaterialClass: nativeProductMaterialByMesh.get(mesh)?.getClassName?.() ?? null,
        };
        const color = config?.color ?? config?.diffuseColor;
        if (color) {
          try {
            if (mat instanceof PBRMaterial) mat.albedoColor = color3FromInput(color);
            else mat.diffuseColor = color3FromInput(color);
          } catch {}
        }
        if (config?.emissiveColor) {
          try { mat.emissiveColor = color3FromInput(config.emissiveColor, '#000000'); } catch {}
        }
        if (mat instanceof StandardMaterial && config?.specularColor) {
          try { mat.specularColor = color3FromInput(config.specularColor); } catch {}
        }

        const ensureOwnedTexture = (slot: keyof RuntimeProductTextureSlots, url: string): Texture => {
          const slots = materialTexturesByMaterial.get(mat as object) ?? {};
          const existing = slots[slot];
          if (existing && existing.url === url && !(existing.texture as any).isDisposed?.()) return existing.texture;
          if (existing) {
            try { existing.texture.dispose(); disposedTextures += 1; } catch {}
          }
          const texture = new Texture(url, sc, false, true, Texture.TRILINEAR_SAMPLINGMODE);
          createdTextures += 1;
          try {
            (texture as any).metadata = {
              ...(((texture as any).metadata && typeof (texture as any).metadata === 'object') ? (texture as any).metadata : {}),
              asukaProductMaterialTexture: true,
              slot,
              url,
            };
          } catch {}
          slots[slot] = { url, texture };
          materialTexturesByMaterial.set(mat as object, slots);
          return texture;
        };

        const textureUrl = String(config?.textureUrl ?? '').trim();
        if (textureUrl) {
          const texture = ensureOwnedTexture('baseColor', textureUrl);
          setProductBaseColorTexture(mat, texture);
          applyProductTextureTiling(texture, config);
          applyProductTextureOpacityMode(mat, texture, config);
        } else {
          // No custom texture means "keep the GLB texture". The material was
          // cloned from the native baseline above, so albedo and UV data remain.
          applyProductTextureOpacityMode(mat, null, config);
        }

        const normalTextureUrl = String(config?.normalTextureUrl ?? '').trim();
        if (normalTextureUrl) {
          const normalTexture = ensureOwnedTexture('normal', normalTextureUrl);
          try { mat.bumpTexture = normalTexture; } catch {}
          applyProductTextureTiling(normalTexture, config);
        }

        if (mat instanceof PBRMaterial) {
          if (typeof config?.metallic === 'number') mat.metallic = Math.max(0, Math.min(1, Number(config.metallic)));
          if (typeof config?.roughness === 'number') mat.roughness = Math.max(0, Math.min(1, Number(config.roughness)));
        } else {
          if (typeof config?.metallic === 'number') mat.specularPower = Math.max(1, 16 + Number(config.metallic) * 80);
          if (typeof config?.roughness === 'number') mat.specularPower = Math.max(1, 96 - Number(config.roughness) * 80);
        }
        if (mesh.material !== mat) {
          mesh.material = mat;
          assignedMaterials += 1;
          changedTargetMeshes.add(mesh);
          disposeRuntimeProductMaterialIfUnused(currentMaterial, mesh);
        }
      }
    }
    if (changedTargetMeshes.size) {
      scheduleProjectedDecorationRefreshForTargets?.(
        Array.from(changedTargetMeshes),
        'product-material-changed',
      );
    }
    endSpan({
      keys,
      targetMeshes,
      clonedMaterials,
      createdMaterials,
      reusedMaterials,
      assignedMaterials,
      createdTextures,
      disposedTextures,
    });
  };

  const getCameraStateSignature = (cameraState?: Asuka3DSceneState['camera'] | null): string => {
    if (!cameraState || typeof cameraState !== 'object') return '';
    const nums = [
      ...(Array.isArray(cameraState.position) ? cameraState.position : []),
      ...(Array.isArray(cameraState.target) ? cameraState.target : []),
      cameraState.fov,
      (cameraState as any).alpha,
      (cameraState as any).beta,
      (cameraState as any).radius,
    ].map((value) => {
      const num = Number(value);
      return Number.isFinite(num) ? num.toFixed(5) : '';
    });
    return nums.join('|');
  };

  const applyCameraState = (cameraState: Asuka3DSceneState['camera'] | undefined) => {
    if (!cameraState) return;
    const cam = requireCamera();
    cameraPersistenceSuppressedUntil = Date.now() + 120;
    applyCameraAngleLimitsToCamera(cam);
    const target = toVec3(cameraState.target, fromVec3(cam.target));
    try { cam.setTarget(target); } catch { cam.target = target; }
    if (typeof cameraState.fov === 'number' && Number.isFinite(cameraState.fov)) cam.fov = cameraState.fov;
    const alpha = Number((cameraState as any).alpha);
    const beta = Number((cameraState as any).beta);
    const radius = Number((cameraState as any).radius);
    const hasSphericalPose = Number.isFinite(alpha) && Number.isFinite(beta) && Number.isFinite(radius) && radius > 0;
    if (hasSphericalPose) {
      cam.alpha = alpha;
      cam.beta = clampNumber(beta, cameraMinBetaLimit, cameraMaxBetaLimit);
      cam.radius = radius;
    } else {
      const position = clampCameraPositionToBetaLimits(target, toVec3(cameraState.position, fromVec3(cam.position)));
      try { cam.setPosition(position); } catch { cam.position = position; }
    }
    applyCameraAngleLimitsToCamera(cam);
    applyProductCameraInputLock(cam);
    if (centerProductGroupInView) {
      centerCameraTargetOnProductGroup('applyCameraState.cameraTarget', { force: true, render: false });
    }
    lastPersistedCameraSignature = getCameraStateSignature(readCameraState());
  };

  const readCameraState = (): Asuka3DSceneState['camera'] | undefined => {
    const cam = camera;
    if (!cam) return undefined;
    return {
      position: fromVec3(cam.position),
      target: fromVec3(cam.target),
      fov: cam.fov,
      alpha: cam.alpha,
      beta: cam.beta,
      radius: cam.radius,
    } as Asuka3DSceneState['camera'];
  };

  /** Convert the Babylon orbit camera into the small public degree-based contract. */
  const readCameraView = (): AsukaCameraView | null => {
    const cam = camera;
    if (!cam) return null;
    return normalizeCameraView({
      turn: radToDeg(Number(cam.alpha) || 0),
      tilt: 90 - radToDeg(Number(cam.beta) || Math.PI / 2),
      distance: Math.max(0.01, Number(cam.radius) || Vector3.Distance(cam.position, cam.target)),
      target: fromVec3(cam.target),
      fov: radToDeg(Number(cam.fov) || degToRad(45)),
    });
  };

  const setCameraViewInternal = (input: Partial<AsukaCameraView>, moveOptions: AsukaCameraMoveOptions = {}): AsukaCameraView | null => {
    const cam = camera;
    if (!cam) return null;
    const current = readCameraView() ?? { turn: 90, tilt: 20, distance: 4, target: [0, 0, 0] as [number, number, number], fov: 45 };
    const next = normalizeCameraView({ ...current, ...(input ?? {}), target: Array.isArray(input?.target) ? input.target : current.target });
    if (!next) return current;
    const target = toVec3(next.target, current.target);
    const alpha = degToRad(next.turn);
    const beta = clampNumber(degToRad(90 - next.tilt), cameraMinBetaLimit, cameraMaxBetaLimit);
    const distance = Math.max(0.01, next.distance);
    const horizontal = Math.sin(beta) * distance;
    const position = new Vector3(
      target.x + Math.cos(alpha) * horizontal,
      target.y + Math.cos(beta) * distance,
      target.z + Math.sin(alpha) * horizontal,
    );
    try { cam.fov = degToRad(next.fov ?? current.fov ?? 45); } catch {}
    animateCameraTo(target, position, moveOptions.animate !== false, {
      duration: moveOptions.duration,
      action: moveOptions.source ?? 'camera.set',
      preserveTarget: true,
    });
    return next;
  };

  const getCameraLimitsInternal = (): AsukaCameraLimits => ({ ...cameraLimits });

  const setCameraLimitsInternal = (input: Partial<AsukaCameraLimits>): AsukaCameraLimits => {
    cameraLimits = normalizeCameraLimits({ ...cameraLimits, ...(input ?? {}) }, configuredCameraLimits);
    currentState = { ...(currentState ?? {}), cameraLimits: { ...cameraLimits } } as Asuka3DSceneState;
    applyCameraAngleLimitsToCamera(camera);
    try { scene?.render(); } catch {}
    try {
      emitCameraEvent({
        action: 'camera.limits',
        viewId: currentViewId,
        camera: readCameraState(),
        cameraLimits: { ...cameraLimits },
      });
    } catch {}
    return { ...cameraLimits };
  };

  const persistCameraStateFromRuntime = (source = 'camera.viewMatrix'): void => {
    const viewId = String(currentViewId ?? '').trim();
    const cameraState = readCameraState();
    if (!viewId || !cameraState) return;
    const signature = getCameraStateSignature(cameraState);
    if (!signature || signature === lastPersistedCameraSignature) return;
    lastPersistedCameraSignature = signature;
    currentState = { ...(currentState ?? {}), camera: cameraState } as Asuka3DSceneState;
    emitCameraEvent({ action: 'camera.changed', source, viewId, camera: cameraState, cameraLimits: { ...cameraLimits } });
  };

  const scheduleCameraStatePersist = (source = 'camera.viewMatrix', delay = 160): void => {
    if (!camera || !currentViewId) return;
    try { if (cameraPersistTimer) window.clearTimeout(cameraPersistTimer); } catch {}
    cameraPersistTimer = null;
    const run = () => {
      cameraPersistTimer = null;
      const now = Date.now();
      if (now < cameraPersistenceSuppressedUntil) {
        scheduleCameraStatePersist(source, Math.max(60, cameraPersistenceSuppressedUntil - now + 40));
        return;
      }
      persistCameraStateFromRuntime(source);
    };
    try {
      cameraPersistTimer = window.setTimeout(run, Math.max(40, delay)) as unknown as number;
    } catch {
      run();
    }
  };

  const flushCameraStatePersist = (source = 'camera.flush'): void => {
    try { if (cameraPersistTimer) window.clearTimeout(cameraPersistTimer); } catch {}
    cameraPersistTimer = null;
    if (Date.now() < cameraPersistenceSuppressedUntil) return;
    persistCameraStateFromRuntime(source);
  };

  const getResolvedModelUrl = (model?: Asuka3DModel | null): string => String(model?.url ?? '').trim();

  const makeProductId = (catalogIdInput: string, stateInput: any): string => {
    const base = String(catalogIdInput || 'product-3d').trim().replace(/[^a-z0-9_-]+/gi, '-').replace(/^-+|-+$/g, '') || 'product-3d';
    const existing = new Set(getProductsFromState(stateInput).map((item: any) => String(item?.id ?? '').trim()).filter(Boolean));
    let id = `${base}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`;
    let guard = 0;
    while (existing.has(id) && guard < 50) {
      guard += 1;
      id = `${base}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}-${guard}`;
    }
    return id;
  };

  const getMeshInstanceAliases = (mesh?: Mesh | null): string[] => {
    if (!mesh) return [];
    const metadata: any = (mesh as any).metadata ?? {};
    const material: any = (mesh as any).material ?? null;
    return Array.from(new Set([
      mesh.id,
      mesh.name,
      metadata?.asukaOriginalMeshId,
      metadata?.asukaOriginalMeshName,
      metadata?.asukaOriginalPartId,
      metadata?.asukaCatalogId,
      metadata?.sourceMeshId,
      metadata?.sourceMeshName,
      metadata?.nativeId,
      metadata?.nativeName,
      metadata?.partId,
      metadata?.asuka?.partId,
      material?.id,
      material?.name,
    ].map((value) => String(value ?? '').trim()).filter(Boolean)));
  };

  const meshMatchesInstanceTarget = (mesh: Mesh, targetInput: any): boolean => {
    const target = String(targetInput ?? '').trim();
    if (!target) return false;
    const normalizedTarget = normalizeLoosePartAlias(target);
    return getMeshInstanceAliases(mesh).some((alias) => {
      const normalizedAlias = normalizeLoosePartAlias(alias);
      return alias === target
        || (!!normalizedAlias && !!normalizedTarget && (
          normalizedAlias === normalizedTarget
          || normalizedAlias.includes(normalizedTarget)
          || normalizedTarget.includes(normalizedAlias)
        ));
    });
  };

  const getImportedMeshIdsForTarget = (meshes: Mesh[], targetInput: any, configInput?: any): string[] => {
    const renderable = getRenderableModelMeshes(meshes);
    if (!renderable.length) return [];
    const explicitTargets = split3DList(configInput?.targetMeshIds ?? configInput?.targetMeshes ?? configInput?.metadata?.targetMeshIds);
    const target = String(targetInput ?? configInput?.materialTarget ?? configInput?.metadata?.materialTarget ?? '').trim();
    const wantsWholeProduct = !explicitTargets.length && /^(product|main|default|\*)$/i.test(target || 'product');
    if (wantsWholeProduct) return renderable.map((mesh) => String(mesh.id ?? '').trim()).filter(Boolean);
    const wanted = explicitTargets.length ? explicitTargets : [target].filter(Boolean);
    const ids = renderable
      .filter((mesh) => wanted.some((item) => meshMatchesInstanceTarget(mesh, item)))
      .map((mesh) => String(mesh.id ?? '').trim())
      .filter(Boolean);
    return Array.from(new Set(ids));
  };

  const scopeProductMaterials = (materialsInput: any, meshes: Mesh[], instanceId: string): Record<string, any> => {
    const scoped: Record<string, any> = {};
    const entries = Object.entries(materialsInput && typeof materialsInput === 'object' ? materialsInput : {});
    for (const [key, value] of entries) {
      if (!value || typeof value !== 'object') continue;
      const originalTarget = String(key ?? '').trim() || 'product';
      const targetMeshIds = getImportedMeshIdsForTarget(meshes, originalTarget, value);
      if (!targetMeshIds.length) continue;
      const scopedKey = `${instanceId}::${originalTarget}`;
      scoped[scopedKey] = {
        ...(clonePlain(value) as any),
        label: String((value as any)?.label ?? originalTarget),
        materialTarget: scopedKey,
        targetMeshIds,
        metadata: {
          ...((((value as any)?.metadata && typeof (value as any).metadata === 'object') ? clonePlain((value as any).metadata) : {}) as any),
          asukaProductId: instanceId,
          originalMaterialTarget: originalTarget,
          materialTarget: scopedKey,
          targetMeshIds,
        },
      };
    }
    if (!Object.keys(scoped).length && meshes.length) {
      const targetMeshIds = getRenderableModelMeshes(meshes).map((mesh) => String(mesh.id ?? '').trim()).filter(Boolean);
      scoped[`${instanceId}::product`] = {
        label: 'Product',
        // A GLB without an explicit material configuration must retain the
        // authored PBR materials and embedded textures it was imported with.
        useNativeMaterial: true,
        targetMeshIds,
        metadata: { asukaProductId: instanceId, originalMaterialTarget: 'product', materialTarget: `${instanceId}::product`, targetMeshIds },
      };
    }
    return scoped;
  };

  const scopeProductParts = (partsInput: any, meshes: Mesh[], instanceId: string): any[] => normalize3DParts(partsInput).map((part: any) => {
    const originalTarget = String(part?.materialTarget ?? part?.id ?? 'product').trim() || 'product';
    const meshNames = split3DList(part?.meshNames ?? part?.meshName ?? part?.targetMeshIds ?? part?.targetMeshId);
    const targetMeshIds = Array.from(new Set(
      (meshNames.length ? meshNames : [originalTarget])
        .flatMap((target) => getImportedMeshIdsForTarget(meshes, target, { targetMeshIds: meshNames })),
    ));
    return {
      ...(clonePlain(part) as any),
      meshNames: targetMeshIds.length ? targetMeshIds : meshNames,
      targetMeshIds: targetMeshIds.length ? targetMeshIds : meshNames,
      materialTarget: `${instanceId}::${originalTarget}`,
      metadata: {
        ...(((part?.metadata && typeof part.metadata === 'object') ? clonePlain(part.metadata) : {}) as any),
        asukaProductId: instanceId,
        originalMaterialTarget: originalTarget,
        originalMeshNames: meshNames,
      },
    };
  });

  const scopeProductPrintAreas = (areasInput: any, meshes: Mesh[], productId: string): Asuka3DPrintArea[] => normalize3DPrintAreas(areasInput).map((area: any, index: number) => {
    const originalId = String(area?.id ?? `print-area-${index + 1}`).trim() || `print-area-${index + 1}`;
    const originalTarget = String(area?.targetMeshId ?? 'product').trim() || 'product';
    const targetMeshIds = getImportedMeshIdsForTarget(meshes, originalTarget, { targetMeshIds: split3DList((area as any)?.targetMeshIds ?? originalTarget) });
    const targetMeshId = targetMeshIds[0] ?? originalTarget;
    return {
      ...(clonePlain(area) as any),
      id: `${productId}::${originalId}`,
      label: String(area?.label ?? originalId).trim() || originalId,
      targetMeshId,
      targetMeshIds,
      metadata: {
        ...(((area?.metadata && typeof area.metadata === 'object') ? clonePlain(area.metadata) : {}) as any),
        asukaProductId: productId,
        originalPrintAreaId: originalId,
        originalTargetMeshId: originalTarget,
      },
    } as Asuka3DPrintArea;
  });

  const offsetImportedProductInstanceMeshes = (meshes: Mesh[], instanceIndex: number): Vector3 => {
    if (!meshes.length || instanceIndex <= 0) return Vector3.Zero();
    const renderable = getRenderableModelMeshes(meshes);
    if (!renderable.length) return Vector3.Zero();
    let min = new Vector3(Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY);
    let max = new Vector3(Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY);
    for (const mesh of renderable) {
      try {
        mesh.computeWorldMatrix(true);
        const box = mesh.getBoundingInfo().boundingBox;
        min = Vector3.Minimize(min, box.minimumWorld);
        max = Vector3.Maximize(max, box.maximumWorld);
      } catch {}
    }
    const width = Number.isFinite(max.x - min.x) ? Math.max(1, max.x - min.x) : 2;
    const offset = new Vector3(width * 1.18 * instanceIndex, 0, 0);
    const targets = getProductInstanceRootMeshes(meshes);
    for (const mesh of targets) {
      translateMeshRootByWorldDelta(mesh, offset);
    }
    const serializedOffset = fromVec3(offset);
    for (const mesh of meshes) {
      try {
        const metadata = ((mesh as any).metadata && typeof (mesh as any).metadata === 'object') ? { ...((mesh as any).metadata as any) } : {};
        (mesh as any).metadata = {
          ...metadata,
          asukaProductBaseOffset: serializedOffset,
          asukaProductInitialPlacementOffset: serializedOffset,
        };
      } catch {}
      try { mesh.computeWorldMatrix(true); } catch {}
    }
    return offset;
  };

  const importProductMeshes = async (input: {
    productId: string;
    catalogId?: string;
    model?: Asuka3DModel;
    parts?: any[];
    productIndex?: number;
    refit?: boolean;
  }): Promise<{ meshes: Mesh[]; placementOffset: Vector3 }> => {
    const endSpan = beginAsuka3DPerfSpan('add', 'import-product-meshes', {
      productId: input.productId,
      catalogId: input.catalogId,
      partCount: Array.isArray(input.parts) ? input.parts.length : 0,
      productIndex: input.productIndex ?? 0,
      model: input.model,
    });
    const sc = requireScene();
    const productId = String(input.productId ?? '').trim() || makeId('product');
    const url = getResolvedModelUrl(input.model);
    if (!url) {
      endSpan({ ok: false, reason: 'no-url' }, { level: 'warn' });
      return { meshes: [], placementOffset: Vector3.Zero() };
    }
    const sourceStart = perfNow();
    let importedMeshes = await instantiatePreloadedModelMeshes(input.model);
    const preloadedAttemptMs = perfNow() - sourceStart;
    if (importedMeshes) {
      debugBabylonProduct3D('product:import-preloaded', { productId, catalogId: input.catalogId, model: input.model, meshCount: importedMeshes.length });
    } else {
      const loaderUrl = await getSceneLoaderUrl(url, input.model);
      const { rootUrl, sceneFilename } = splitSceneUrl(loaderUrl);
      const pluginExtension = getModelPluginExtension(input.model, url);
      debugBabylonProduct3D('product:import-start', { productId, catalogId: input.catalogId, model: input.model, rootUrl, sceneFilename, pluginExtension });
      const directImportStart = perfNow();
      await ensureGltfLoader();
      const result = await SceneLoader.ImportMeshAsync('', rootUrl, sceneFilename, sc, undefined, pluginExtension);
      importedMeshes = result.meshes.filter((mesh: any): mesh is Mesh => mesh instanceof Mesh);
      logAsuka3DPerf('add', 'direct-import-resolved', { productId, importMs: perfNow() - directImportStart, meshCount: importedMeshes.length });
    }
    const splitStart = perfNow();
    importedMeshes = splitConfiguredGeometryComponentMeshes(importedMeshes, input.parts ?? []);
    const splitMs = perfNow() - splitStart;
    const normalizeStart = perfNow();
    normalizeImportedModel(importedMeshes);
    const normalizeMs = perfNow() - normalizeStart;
    // Keep the authored material for every imported mesh. Multi-part products
    // need the same restoration path as flat products when "Default" is chosen.
    for (const mesh of getRenderableModelMeshes(importedMeshes)) {
      if (!nativeProductMaterialByMesh.has(mesh)) nativeProductMaterialByMesh.set(mesh, (mesh as any).material ?? null);
    }

    const seenIds = new Set<string>();
    importedMeshes.forEach((mesh: Mesh, index: number) => {
      const originalId = String(mesh.id || mesh.name || `mesh-${index + 1}`).trim() || `mesh-${index + 1}`;
      const originalName = String(mesh.name || originalId).trim() || originalId;
      let runtimeId = `${productId}::${originalId}`;
      let suffix = 1;
      while (seenIds.has(runtimeId) || objectMeshes.has(runtimeId)) {
        suffix += 1;
        runtimeId = `${productId}::${originalId}::${suffix}`;
      }
      seenIds.add(runtimeId);
      const metadata = ((mesh as any).metadata && typeof (mesh as any).metadata === 'object') ? { ...((mesh as any).metadata as any) } : {};
      const extras = metadata?.gltf?.extras ?? (mesh as any)?._gltf?.extras ?? null;
      const partId = String(metadata?.asuka?.partId ?? metadata?.partId ?? extras?.asuka?.partId ?? extras?.partId ?? (/^asuka_part_/i.test(originalId) ? originalId : '')).trim();
      mesh.id = runtimeId;
      mesh.name = originalName;
      (mesh as any).metadata = {
        ...metadata,
        asukaProductId: productId,
        asukaCatalogId: String(input.catalogId ?? '').trim() || undefined,
        asukaOriginalMeshId: originalId,
        asukaOriginalMeshName: originalName,
        asukaOriginalPartId: partId || undefined,
      };
      objectMeshes.set(runtimeId, mesh);
      objectKinds.set(runtimeId, 'mesh');
    });

    const placementOffset = offsetImportedProductInstanceMeshes(importedMeshes, Math.max(0, Number(input.productIndex ?? 0) || 0));
    modelMeshes = [...modelMeshes, ...importedMeshes];
    if (!objectMeshes.has('product')) {
      const firstRenderable = getRenderableModelMeshes(importedMeshes)[0];
      if (firstRenderable) {
        objectMeshes.set('product', firstRenderable);
        objectKinds.set('product', 'mesh');
      }
    }
    prepareNativePrintSurfaceMeshes();
    if (input.refit !== false) fitView();
    debugBabylonProduct3D('product:import-done', { productId, meshCount: importedMeshes.length, totalModelMeshes: modelMeshes.length, placementOffset: fromVec3(placementOffset) });
    endSpan({
      ok: true,
      source: preloadedAttemptMs >= 0 && preloadedModelAssets.size > 0 ? 'preload-or-direct' : 'direct',
      preloadedAttemptMs,
      splitMs,
      normalizeMs,
      meshCount: importedMeshes.length,
      renderableMeshCount: getRenderableModelMeshes(importedMeshes).length,
      totalModelMeshes: modelMeshes.length,
      sceneMeshes: scene?.meshes?.length ?? 0,
      sceneMaterials: scene?.materials?.length ?? 0,
      placementOffset: fromVec3(placementOffset),
    });
    return { meshes: importedMeshes, placementOffset };
  };

  const loadProducts = async (productsInput: any[]): Promise<void> => {
    const products = compactProducts(productsInput);
    clearModel();
    for (const [index, product] of products.entries()) {
      const productId = String((product as any).id ?? `product-${index + 1}`);
      const importedResult = await importProductMeshes({
        productId,
        catalogId: String((product as any).catalogId ?? ''),
        model: ((product as any).model && typeof (product as any).model === 'object') ? (product as any).model : undefined,
        parts: Array.isArray((product as any).parts) ? (product as any).parts : [],
        productIndex: index,
        refit: false,
      });
      const imported = importedResult.meshes;
      const element = resolveParametricElementForProduct(product);
      if (element?.parametric?.schemaVersion === 1) {
        initializeParametricProduct(productId, element, true);
      } else {
        applyProductInstanceTransformToMeshes(imported, (product as any).transform);
      }
    }
    invalidateProductGroupViewTarget();
    if (centerProductGroup) recenterProductGroup('loadProducts');
    if (products.length) fitView();
  };

  const getModelBounds = () => {
    const meshes = modelMeshes.filter((mesh) => {
      if (!mesh || mesh.isDisposed()) return false;
      try { return mesh.getTotalVertices() > 0; } catch { return true; }
    });
    if (!meshes.length) {
      return { center: Vector3.Zero(), min: new Vector3(-1, -1, -0.1), max: new Vector3(1, 1, 0.1), size: new Vector3(2, 2, 0.2) };
    }

    let min = new Vector3(Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY);
    let max = new Vector3(Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY);
    for (const mesh of meshes) {
      try {
        mesh.computeWorldMatrix(true);
        const box = mesh.getBoundingInfo().boundingBox;
        min = Vector3.Minimize(min, box.minimumWorld);
        max = Vector3.Maximize(max, box.maximumWorld);
      } catch {}
    }
    if (!Number.isFinite(min.x) || !Number.isFinite(max.x)) {
      min = new Vector3(-1, -1, -0.1);
      max = new Vector3(1, 1, 0.1);
    }
    return { center: min.add(max).scale(0.5), min, max, size: max.subtract(min) };
  };

  const getDefaultDecorationTransform = () => {
    const cam = camera;
    const bounds = getModelBounds();
    const size = bounds.max.subtract(bounds.min);
    let dir = cam ? cam.position.subtract(bounds.center) : new Vector3(0, 0, 1);
    if (dir.lengthSquared() < 0.0001) dir = new Vector3(0, 0, 1);
    dir.normalize();
    const half = size.scale(0.5);
    const projectedExtent = Math.abs(dir.x) * Math.max(half.x, 0.01)
      + Math.abs(dir.y) * Math.max(half.y, 0.01)
      + Math.abs(dir.z) * Math.max(half.z, 0.01);
    const offset = Math.max(0.045, Math.min(0.18, size.length() * 0.035));
    const position = bounds.center.add(dir.scale(projectedExtent + offset));
    return { position, direction: dir };
  };

  const orientDecorationTowardCamera = (mesh: Mesh) => {
    try {
      if (camera) mesh.lookAt(camera.position);
    } catch {}
  };

  const getExplicitPrintAreas = (): Asuka3DPrintArea[] => Array.isArray(currentState.printAreas)
    ? (currentState.printAreas as Asuka3DPrintArea[])
    : [];

  const getPrintAreas = (): Asuka3DPrintArea[] => {
    const byId = new Map<string, Asuka3DPrintArea>();
    for (const area of getExplicitPrintAreas()) {
      const id = String(area?.id ?? '').trim();
      if (id) byId.set(id, with3DPrintAreaCoordinateFrame(area));
    }
    try {
      for (const area of listDetectedPrintSurfaceAreas()) {
        const id = String(area?.id ?? '').trim();
        if (id && !byId.has(id)) byId.set(id, with3DPrintAreaCoordinateFrame(area));
      }
    } catch {}
    return Array.from(byId.values());
  };

  const getPrintAreaById = (id: string | null | undefined): Asuka3DPrintArea | null => {
    const key = String(id ?? '').trim();
    if (!key) return null;
    return getPrintAreas().find((area) => String(area?.id ?? '').trim() === key) ?? null;
  };

  const getActivePrintAreaId = (): string => String((currentState as any).activePrintAreaId ?? '').trim();

  const getPrintAreaNormal = (area?: Asuka3DPrintArea | null): Vector3 | null => {
    const raw = (area as any)?.metadata?.surfaceNormal
      ?? (area as any)?.metadata?.normal
      ?? (area as any)?.worldBounds?.normal
      ?? (area as any)?.placement?.normal;
    if (!Array.isArray(raw) || raw.length < 3) return null;
    const normal = toVec3(raw, [0, 0, 1]);
    if (normal.lengthSquared() < 0.0001) return null;
    normal.normalize();
    return normal;
  };

  const setPrintAreasState = (areasInput: Asuka3DPrintArea[], activeIdInput?: string | null, action = 'printAreas') => {
    const areas = Array.isArray(areasInput)
      ? clonePlain(areasInput.map((area) => with3DPrintAreaCoordinateFrame(area)))
      : [];
    const ids = new Set(areas.map((area) => String(area?.id ?? '').trim()).filter(Boolean));
    const requestedActiveId = String(activeIdInput ?? '').trim();
    const previousActiveId = getActivePrintAreaId();
    const activeId = requestedActiveId && ids.has(requestedActiveId)
      ? requestedActiveId
      : (previousActiveId && ids.has(previousActiveId) ? previousActiveId : (String(areas[0]?.id ?? '').trim() || null));
    currentState = {
      ...(clonePlain(currentState) ?? {}),
      printAreas: areas,
      activePrintAreaId: activeId,
    } as Asuka3DSceneState;
    renderPrintAreaTextures();
    renderPrintAreaVisuals();
    emitChanged(action, activeId ?? null);
  };

  const selectPrintAreaInternal = (id: string | null, shouldEmit = true) => {
    const key = String(id ?? '').trim();
    const areas = getPrintAreas();
    const nextId = key && areas.some((area) => String(area?.id ?? '') === key)
      ? key
      : null;
    currentState = {
      ...(clonePlain(currentState) ?? {}),
      activePrintAreaId: nextId,
    } as Asuka3DSceneState;
    setSelectedObject(null, false);
    renderPrintAreaVisuals();
    if (shouldEmit) emitChanged('selectPrintArea', nextId);
  };

  const getPreferredProductSurfaceMeshForNewDecoration = (input?: any): { mesh: Mesh | null; partId?: string; partLabel?: string; point?: Vector3 | null; normal?: Vector3 | null } => {
    const explicitMeshId = String(input?.targetMeshId
      ?? input?.meshId
      ?? input?.metadata?.targetMeshId
      ?? input?.metadata?.meshId
      ?? '').trim();
    const metadataPick = (input?.metadata?.productPick && typeof input.metadata.productPick === 'object') ? input.metadata.productPick : null;
    const pick = metadataPick ?? lastProductPick ?? null;
    const pickedMeshId = String(pick?.productPartMeshId ?? '').trim();
    const selectedPart = getSelectedPartInternal();
    const selectedMeshId = String(selectedPart?.meshIds?.[0] ?? selectedPart?.meshNames?.[0] ?? '').trim();
    const partMeshId = explicitMeshId || pickedMeshId || selectedMeshId;
    let mesh = partMeshId ? findModelMesh(partMeshId) : null;
    if (!mesh && selectedPart) mesh = findMeshForProductPart(selectedPart as any, pickedMeshId, String(pick?.productPartMaterialId ?? '').trim());
    if (!mesh) mesh = findModelMesh('product') ?? findModelMesh(null);
    const point = Array.isArray(pick?.productPartPoint) && pick.productPartPoint.length >= 3
      ? toVec3(pick.productPartPoint, [0, 0, 0])
      : null;
    let normal = Array.isArray(pick?.productPartNormal) && pick.productPartNormal.length >= 3
      ? toVec3(pick.productPartNormal, [0, 0, 1])
      : null;
    if (normal && normal.lengthSquared() < 0.0001) normal = null;
    if (normal) normal.normalize();
    return {
      mesh,
      partId: String(pick?.productPartId ?? selectedPart?.id ?? '').trim() || undefined,
      partLabel: String(pick?.productPartLabel ?? selectedPart?.label ?? selectedPart?.name ?? '').trim() || undefined,
      point,
      normal,
    };
  };

  const getAutomaticProductSurfacePrintArea = (input?: any): Asuka3DPrintArea | null => {
    const preferred = getPreferredProductSurfaceMeshForNewDecoration(input);
    const targetMesh = preferred.mesh;
    if (!targetMesh) return null;
    const meshBounds = getMeshWorldBounds(targetMesh);
    const transform = getDefaultDecorationTransform();
    const basis = getViewTransformBasis();
    const size = meshBounds.size;
    const surfaceNormal = preferred.normal ?? transform.direction;
    if (surfaceNormal.lengthSquared() < 0.0001) surfaceNormal.copyFrom(new Vector3(0, 0, 1));
    surfaceNormal.normalize();
    const half = size.scale(0.5);
    const projectedExtent = Math.abs(surfaceNormal.x) * Math.max(half.x, 0.01)
      + Math.abs(surfaceNormal.y) * Math.max(half.y, 0.01)
      + Math.abs(surfaceNormal.z) * Math.max(half.z, 0.01);
    const surfaceCenter = preferred.point
      ? preferred.point.add(surfaceNormal.scale(0.006))
      : meshBounds.center.add(surfaceNormal.scale(projectedExtent + 0.006));
    const tangent = getSurfaceTangentBasis(surfaceNormal);
    const right = basis?.right && basis.right.lengthSquared?.() > 0.0001 ? basis.right : tangent.right;
    const up = basis?.up && basis.up.lengthSquared?.() > 0.0001 ? basis.up : tangent.up;
    const projectedWidth = Math.abs(right.x) * Math.max(size.x, 0.01)
      + Math.abs(right.y) * Math.max(size.y, 0.01)
      + Math.abs(right.z) * Math.max(size.z, 0.01);
    const projectedHeight = Math.abs(up.x) * Math.max(size.x, 0.01)
      + Math.abs(up.y) * Math.max(size.y, 0.01)
      + Math.abs(up.z) * Math.max(size.z, 0.01);
    const modelBounds = getModelBounds();
    const modelSize = modelBounds.size;
    const maxModelFace = Math.max(modelSize.x, modelSize.y, modelSize.z, 0.01);
    const surfaceWidth = Math.max(0.08, Math.min(maxModelFace * 0.9, projectedWidth * 0.62));
    const surfaceHeight = Math.max(0.08, Math.min(maxModelFace * 0.9, projectedHeight * 0.62));
    // Automatic product-surface placement must be deep enough to project onto curved
    // products (mugs, balls, rounded packaging) instead of behaving like a shallow
    // front sticker. Babylon clips decal geometry to this projection box.
    const surfaceDepth = Math.max(0.06, Math.min(Math.max(surfaceWidth, surfaceHeight), Math.min(surfaceWidth, surfaceHeight) * 0.45));
    debug3DLayer('auto-surface-area:resolved', {
      targetMeshId: String(targetMesh.id || targetMesh.name || 'product'),
      preferredPartId: preferred.partId,
      preferredPartLabel: preferred.partLabel,
      hasPreferredPoint: Boolean(preferred.point),
      hasPreferredNormal: Boolean(preferred.normal),
      surfaceCenter: fromVec3(surfaceCenter),
      surfaceNormal: fromVec3(surfaceNormal),
      surfaceSize: [surfaceWidth, surfaceHeight, surfaceDepth],
    });
    return {
      id: AUTO_PRODUCT_SURFACE_AREA_ID,
      label: preferred.partLabel || 'Product surface',
      targetMeshId: String(targetMesh.id || targetMesh.name || 'product'),
      mode: 'surface',
      space: 'surface-tangent',
      unit: 'world-unit',
      worldBounds: {
        center: fromVec3(surfaceCenter),
        size: [surfaceWidth, surfaceHeight, surfaceDepth],
      },
      metadata: {
        autoProductSurface: true,
        surfaceConforming: true,
        targetMeshId: String(targetMesh.id || targetMesh.name || 'product'),
        ...(preferred.partId ? { productPartId: preferred.partId } : null),
        ...(preferred.partLabel ? { productPartLabel: preferred.partLabel } : null),
        surfacePosition: fromVec3(surfaceCenter),
        surfaceNormal: fromVec3(surfaceNormal),
        surfaceFromPreferredPick: Boolean(preferred.point && preferred.normal),
      },
    } as Asuka3DPrintArea;
  };

  const isAutomaticProductSurfaceArea = (area?: Asuka3DPrintArea | null): boolean => {
    if (!area) return false;
    return String(area?.id ?? '').trim() === AUTO_PRODUCT_SURFACE_AREA_ID || (area as any)?.metadata?.autoProductSurface === true;
  };

  const isNativePrintSurfaceArea = (area?: Asuka3DPrintArea | null): boolean => {
    if (!area) return false;
    const metadata = ((area as any)?.metadata && typeof (area as any).metadata === 'object') ? (area as any).metadata : {};
    if (metadata.nativePrintSurface === true || metadata.detectedFromMeshName === true) return true;
    const id = String((area as any)?.id ?? '').trim().toLowerCase();
    const target = String((area as any)?.targetMeshId ?? metadata.sourceMeshId ?? metadata.sourceMeshName ?? '').trim().toLowerCase();
    return id.startsWith(PRINT_SURFACE_MESH_PREFIX) || target.startsWith(PRINT_SURFACE_MESH_PREFIX);
  };

  const isTextureSlotPrintArea = (area?: Asuka3DPrintArea | null): boolean => {
    if (!area) return false;
    // GLB meshes named asuka_surface_* are real helper surfaces, not material
    // texture slots. Treating them as texture slots paints a full rectangular
    // DynamicTexture on the helper mesh; on many GLBs the transparent pixels are
    // rendered as black. Native surfaces must therefore use the decal/surface path.
    if (isNativePrintSurfaceArea(area)) return false;
    const mode = String((area as any)?.mode ?? '').trim().toLowerCase();
    return mode === 'texture';
  };

  const shouldUseDecalForPrintArea = (area?: Asuka3DPrintArea | null): boolean => {
    if (!area || isTextureSlotPrintArea(area)) return false;
    // Both explicit print areas and the automatic product surface use real Babylon
    // decals. This keeps 3D text/images/designs conforming to curved products
    // instead of floating on a flat camera-facing plane. Plane placement remains
    // only as a safety fallback when decal creation is impossible for a model.
    return true;
  };

  const shouldUseTextureSlotForNewObject = (area: Asuka3DPrintArea | null | undefined): boolean => isTextureSlotPrintArea(area);


  const getActivePrintArea = (input?: any): Asuka3DPrintArea | null => {
    const areas = getPrintAreas();
    const explicitId = String(input?.printAreaId
      ?? input?.targetPrintAreaId
      ?? input?.metadata?.printAreaId
      ?? input?.metadata?.targetPrintAreaId
      ?? '').trim();
    const activeId = getActivePrintAreaId();
    const wanted = explicitId || activeId;
    if (wanted) {
      if (wanted === AUTO_PRODUCT_SURFACE_AREA_ID) return getAutomaticProductSurfacePrintArea(input);
      const found = areas.find((area) => String(area?.id ?? '') === wanted);
      if (found) return found;
      if (explicitId) return getAutomaticProductSurfacePrintArea(input);
    }
    if (areas.length === 1) return areas[0];
    return getAutomaticProductSurfacePrintArea(input);
  };

  const getDeclaredBoxPrintSurfacePrimaryBasis = (primaryFaceInput: unknown): { right: Vector3; up: Vector3; normal: Vector3 } | null => {
    try {
      const primaryFace = normalizeDeclaredBoxFaceId(primaryFaceInput, 'front');
      if (primaryFace === 'front') return { right: new Vector3(1, 0, 0), up: new Vector3(0, 1, 0), normal: new Vector3(0, 0, 1) };
      if (primaryFace === 'back') return { right: new Vector3(-1, 0, 0), up: new Vector3(0, 1, 0), normal: new Vector3(0, 0, -1) };
      if (primaryFace === 'right') return { right: new Vector3(0, 0, -1), up: new Vector3(0, 1, 0), normal: new Vector3(1, 0, 0) };
      if (primaryFace === 'left') return { right: new Vector3(0, 0, 1), up: new Vector3(0, 1, 0), normal: new Vector3(-1, 0, 0) };
      if (primaryFace === 'top') return { right: new Vector3(1, 0, 0), up: new Vector3(0, 0, -1), normal: new Vector3(0, 1, 0) };
      if (primaryFace === 'bottom') return { right: new Vector3(1, 0, 0), up: new Vector3(0, 0, 1), normal: new Vector3(0, -1, 0) };
    } catch {}
    return null;
  };

  const getDeclaredBoxPrintSurfaceNetBounds = (area: Asuka3DPrintArea | null | undefined): { center: Vector3; size: Vector3; direction: Vector3; basis: { right: Vector3; up: Vector3; normal: Vector3 }; metadata: Record<string, unknown> } | null => {
    if (!area) return null;
    const rawDeclaredSurface = (area as any)?.metadata?.printSurface3D ?? (area as any)?.printSurface3D ?? (area as any)?.metadata?.printSurface ?? null;
    const rawDeclaredSurfaceType = String((rawDeclaredSurface as any)?.type ?? (rawDeclaredSurface as any)?.unwrapMode ?? '').trim().toLowerCase();
    const rawSelectedTriangles = Array.isArray((rawDeclaredSurface as any)?.selectedTriangles)
      ? (rawDeclaredSurface as any).selectedTriangles
      : (Array.isArray((rawDeclaredSurface as any)?.triangles)
        ? (rawDeclaredSurface as any).triangles
        : (Array.isArray((rawDeclaredSurface as any)?.faceNetTriangles) ? (rawDeclaredSurface as any).faceNetTriangles : []));
    if (rawDeclaredSurfaceType === 'face-net' && rawSelectedTriangles.length > 0) {
      // Exact Drawing Areas face-net surfaces already persist their real triangle
      // bounds in worldBounds/placement. Reusing the old box-net helper here
      // reintroduced a fake cuboid rectangle, which then moved the sticker center
      // outside the selected triangles and produced either a detached plane or no
      // clipped face-net fragments at all. Let getPrintAreaBounds fall back to the
      // exact persisted bounds for these surfaces.
      return null;
    }
    const declared = getDeclaredBoxPrintSurfaceMetadata({ __asukaPrintSurfaceArea: area } as any);
    if (!declared) return null;
    const targetMeshId = String((area as any)?.targetMeshId ?? (area as any)?.metadata?.targetMeshId ?? '').trim();
    const targetMesh = findModelMesh(targetMeshId || 'product') ?? findModelMesh(null);
    if (!targetMesh) return null;
    const rawPositions = targetMesh.getVerticesData?.('position' as any) as number[] | null;
    if (!rawPositions || rawPositions.length < 9) return null;
    const world = targetMesh.getWorldMatrix?.() ?? Matrix.Identity();
    const localPoints: Vector3[] = [];
    for (let i = 0; i + 2 < rawPositions.length; i += 3) {
      localPoints.push(new Vector3(Number(rawPositions[i] ?? 0) || 0, Number(rawPositions[i + 1] ?? 0) || 0, Number(rawPositions[i + 2] ?? 0) || 0));
    }
    if (!localPoints.length) return null;
    const min = localPoints.reduce((acc, point) => Vector3.Minimize(acc, point), localPoints[0].clone());
    const max = localPoints.reduce((acc, point) => Vector3.Maximize(acc, point), localPoints[0].clone());
    const widthX = Math.max(0.001, max.x - min.x);
    const heightY = Math.max(0.001, max.y - min.y);
    const depthZ = Math.max(0.001, max.z - min.z);
    const faceSet = new Set(declared.faces);
    const rects: Array<{ face: string; u0: number; u1: number; v0: number; v1: number }> = [];
    const addNetRect = (face: string, u0: number, u1: number, v0: number, v1: number) => {
      if (!faceSet.has(face as any)) return;
      if (u1 <= u0 || v1 <= v0) return;
      rects.push({ face, u0, u1, v0, v1 });
    };
    const primaryFace = declared.primaryFace;
    if (primaryFace === 'front') {
      addNetRect('front', 0, widthX, 0, heightY);
      addNetRect('left', -depthZ, 0, 0, heightY);
      addNetRect('right', widthX, widthX + depthZ, 0, heightY);
      addNetRect('top', 0, widthX, heightY, heightY + depthZ);
      addNetRect('bottom', 0, widthX, -depthZ, 0);
      addNetRect('back', widthX + depthZ, widthX + depthZ + widthX, 0, heightY);
    } else if (primaryFace === 'back') {
      addNetRect('back', 0, widthX, 0, heightY);
      addNetRect('right', -depthZ, 0, 0, heightY);
      addNetRect('left', widthX, widthX + depthZ, 0, heightY);
      addNetRect('top', 0, widthX, heightY, heightY + depthZ);
      addNetRect('bottom', 0, widthX, -depthZ, 0);
      addNetRect('front', widthX + depthZ, widthX + depthZ + widthX, 0, heightY);
    } else if (primaryFace === 'right') {
      addNetRect('right', 0, depthZ, 0, heightY);
      addNetRect('front', -widthX, 0, 0, heightY);
      addNetRect('back', depthZ, depthZ + widthX, 0, heightY);
      addNetRect('top', 0, depthZ, heightY, heightY + widthX);
      addNetRect('bottom', 0, depthZ, -widthX, 0);
      addNetRect('left', depthZ + widthX, depthZ + widthX + depthZ, 0, heightY);
    } else if (primaryFace === 'left') {
      addNetRect('left', 0, depthZ, 0, heightY);
      addNetRect('back', -widthX, 0, 0, heightY);
      addNetRect('front', depthZ, depthZ + widthX, 0, heightY);
      addNetRect('top', 0, depthZ, heightY, heightY + widthX);
      addNetRect('bottom', 0, depthZ, -widthX, 0);
      addNetRect('right', depthZ + widthX, depthZ + widthX + depthZ, 0, heightY);
    } else if (primaryFace === 'top') {
      addNetRect('top', 0, widthX, 0, depthZ);
      addNetRect('left', -depthZ, 0, 0, depthZ);
      addNetRect('right', widthX, widthX + depthZ, 0, depthZ);
      addNetRect('front', 0, widthX, -heightY, 0);
      addNetRect('back', 0, widthX, depthZ, depthZ + heightY);
      addNetRect('bottom', widthX + depthZ, widthX + depthZ + widthX, 0, depthZ);
    } else {
      addNetRect('bottom', 0, widthX, 0, depthZ);
      addNetRect('left', -depthZ, 0, 0, depthZ);
      addNetRect('right', widthX, widthX + depthZ, 0, depthZ);
      addNetRect('back', 0, widthX, -heightY, 0);
      addNetRect('front', 0, widthX, depthZ, depthZ + heightY);
      addNetRect('top', widthX + depthZ, widthX + depthZ + widthX, 0, depthZ);
    }
    if (!rects.length) return null;
    const minU = Math.min(...rects.map((rect) => rect.u0));
    const maxU = Math.max(...rects.map((rect) => rect.u1));
    const minV = Math.min(...rects.map((rect) => rect.v0));
    const maxV = Math.max(...rects.map((rect) => rect.v1));
    const netWidth = Math.max(0.001, maxU - minU);
    const netHeight = Math.max(0.001, maxV - minV);
    const basis = getDeclaredBoxPrintSurfacePrimaryBasis(primaryFace);
    if (!basis) return null;
    const origin = (() => {
      if (primaryFace === 'front') return new Vector3(min.x, min.y, max.z);
      if (primaryFace === 'back') return new Vector3(max.x, min.y, min.z);
      if (primaryFace === 'right') return new Vector3(max.x, min.y, max.z);
      if (primaryFace === 'left') return new Vector3(min.x, min.y, min.z);
      if (primaryFace === 'top') return new Vector3(min.x, max.y, max.z);
      return new Vector3(min.x, min.y, min.z);
    })();
    const centerU = (minU + maxU) / 2;
    const centerV = (minV + maxV) / 2;
    const localCenter = origin.add(basis.right.scale(centerU)).add(basis.up.scale(centerV));
    const center = Vector3.TransformCoordinates(localCenter, world);
    const worldRightSpan = Vector3.TransformNormal(basis.right.scale(netWidth), world);
    const worldUpSpan = Vector3.TransformNormal(basis.up.scale(netHeight), world);
    const worldNormalSpan = Vector3.TransformNormal(basis.normal, world);
    try { worldNormalSpan.normalize(); } catch {}
    const sizeDepth = Math.max(0.01, Vector3.TransformNormal(basis.normal.scale(Math.max(widthX, heightY, depthZ) * 0.08), world).length());
    return {
      center,
      size: new Vector3(Math.max(0.001, worldRightSpan.length()), Math.max(0.001, worldUpSpan.length()), sizeDepth),
      direction: worldNormalSpan.clone(),
      basis: { right: worldRightSpan.normalize(), up: worldUpSpan.normalize(), normal: worldNormalSpan.clone() },
      metadata: { mode: 'face-net', primaryFace, faces: [...declared.faces], netBounds: { minU, maxU, minV, maxV }, targetMeshId: String(targetMesh.id || targetMesh.name || targetMeshId || 'product') },
    };
  };

  const getPrintAreaBounds = (area: Asuka3DPrintArea | null | undefined) => {
    const declaredBoxBounds = getDeclaredBoxPrintSurfaceNetBounds(area);
    if (declaredBoxBounds) {
      return {
        center: declaredBoxBounds.center,
        size: declaredBoxBounds.size,
        direction: declaredBoxBounds.direction,
      };
    }
    const transform = getDefaultDecorationTransform();
    const rawCenter = (area as any)?.worldBounds?.center ?? (area as any)?.placement?.position;
    const rawSize = (area as any)?.worldBounds?.size ?? (area as any)?.placement?.scale;
    const center = Array.isArray(rawCenter)
      ? toVec3(rawCenter, fromVec3(transform.position))
      : transform.position;
    const size = Array.isArray(rawSize)
      ? toVec3(rawSize, [1.2, 0.8, 0.02])
      : new Vector3(1.2, 0.8, 0.02);
    const areaNormal = getPrintAreaNormal(area);
    return {
      center,
      size: new Vector3(Math.max(0.05, Math.abs(size.x)), Math.max(0.05, Math.abs(size.y)), Math.max(0.005, Math.abs(size.z))),
      direction: areaNormal ?? transform.direction,
    };
  };

  const orientMeshForPrintArea = (mesh: Mesh, area?: Asuka3DPrintArea | null) => {
    const rotation = (area as any)?.placement?.rotation;
    if (Array.isArray(rotation) && rotation.length >= 3) {
      mesh.rotation = toVec3(rotation, fromVec3(mesh.rotation));
      return;
    }
    const bounds = getPrintAreaBounds(area);
    const normal = bounds.direction;
    if (normal.lengthSquared() > 0.0001) {
      try { mesh.lookAt(bounds.center.add(normal)); return; } catch {}
    }
    orientDecorationTowardCamera(mesh);
  };

  const placeDecorationMesh = (mesh: Mesh, area?: Asuka3DPrintArea | null, baseSize: { width: number; height: number } = { width: 1, height: 1 }) => {
    if (area) {
      const bounds = getPrintAreaBounds(area);
      mesh.position = bounds.center.add(bounds.direction.scale(0.018));
      orientMeshForPrintArea(mesh, area);
      const fit = Math.min(bounds.size.x / Math.max(baseSize.width, 0.01), bounds.size.y / Math.max(baseSize.height, 0.01));
      if (Number.isFinite(fit) && fit > 0) {
        const scale = Math.max(0.05, Math.min(1.25, fit * 0.82));
        mesh.scaling = new Vector3(scale, scale, scale);
      }
      return mesh;
    }
    const transform = getDefaultDecorationTransform();
    mesh.position = transform.position;
    orientDecorationTowardCamera(mesh);
    return mesh;
  };

  const createSurfacePrintAreaOutlineMesh = (area: Asuka3DPrintArea, visualId: string, options: { active?: boolean; preview?: boolean } = {}): Mesh | null => {
    const sc = requireScene();
    const id = String((area as any)?.id ?? visualId).trim() || visualId;
    const bounds = getPrintAreaBounds(area);
    const basis = getPrintAreaSurfaceBasis(area);
    const targetMesh = findModelMesh(String((area as any)?.targetMeshId ?? '').trim() || 'product') ?? findModelMesh(null);
    if (!targetMesh) return null;

    const halfWidth = Math.max(0.025, bounds.size.x / 2);
    const halfHeight = Math.max(0.025, bounds.size.y / 2);
    const corners = [
      [-halfWidth, -halfHeight],
      [halfWidth, -halfHeight],
      [halfWidth, halfHeight],
      [-halfWidth, halfHeight],
    ] as Array<[number, number]>;
    const lines: Vector3[][] = [];

    if (options.preview) {
      const offset = basis.normal.scale(0.014);
      lines.push([...corners, corners[0]].map(([x, y]) => bounds.center
        .add(basis.right.scale(x))
        .add(basis.up.scale(y))
        .add(offset)));
    } else {
      const samplesPerEdge = 8;
      for (let edgeIndex = 0; edgeIndex < corners.length; edgeIndex += 1) {
        const from = corners[edgeIndex];
        const to = corners[(edgeIndex + 1) % corners.length];
        let segment: Vector3[] = [];
        for (let step = 0; step <= samplesPerEdge; step += 1) {
          const amount = step / samplesPerEdge;
          const x = from[0] + (to[0] - from[0]) * amount;
          const y = from[1] + (to[1] - from[1]) * amount;
          const desired = bounds.center.add(basis.right.scale(x)).add(basis.up.scale(y));
          const projected = projectPointToModelSurface(desired, basis.normal, targetMesh);
          if (!projected) {
            if (segment.length >= 2) lines.push(segment);
            segment = [];
            continue;
          }
          segment.push(projected.position.add(projected.normal.scale(0.006)));
        }
        if (segment.length >= 2) lines.push(segment);
      }
    }

    if (!lines.length) return null;
    const mesh = MeshBuilder.CreateLineSystem(visualId, { lines, useVertexAlpha: false }, sc);
    const color = options.preview ? '#f97316' : (options.active ? '#22c55e' : '#3b82f6');
    mesh.id = options.preview ? `print-area-preview:${id}` : `print-area:${id}`;
    mesh.name = String((area as any)?.label ?? id);
    mesh.color = color3FromInput(color);
    mesh.alpha = options.preview ? 0.96 : (options.active ? 0.9 : 0.7);
    mesh.isPickable = false;
    mesh.renderingGroupId = targetMesh.renderingGroupId;
    (mesh as any).alwaysSelectAsActiveMesh = true;
    (mesh as any).__asukaPrintAreaId = id;
    (mesh as any).__asukaPrintAreaPreview = !!options.preview;
    (mesh as any).__asukaPrintAreaOutline = true;
    return mesh;
  };

  const createPrintAreaVisualMesh = (area: Asuka3DPrintArea, visualId: string, options: { active?: boolean; preview?: boolean } = {}): Mesh | null => {
    const authoredPrintSurface = (area as any)?.metadata?.printSurface3D ?? (area as any)?.printSurface3D ?? (area as any)?.metadata?.printSurface ?? null;
    const createdFrom = String((area as any)?.metadata?.createdFrom ?? '').trim();
    if (area?.mode === 'surface' && createdFrom === 'surface-draw' && !authoredPrintSurface) {
      return createSurfacePrintAreaOutlineMesh(area, visualId, options);
    }
    if (area?.mode === 'surface' || authoredPrintSurface) {
      // A filled translucent decal over a 3D print surface makes the product
      // itself look darker/selected. Keep authored 3D areas as engine
      // constraints only; drawing previews use outline/selection UI instead.
      debug3DLayer('print-area:visual-fill-blocked', { visualId, areaId: String((area as any)?.id ?? ''), active: !!options.active, preview: !!options.preview });
      return null;
    }
    const declaredPrintSurface = getDeclaredBoxPrintSurfaceMetadata({ __asukaPrintSurfaceArea: area } as any);
    if (declaredPrintSurface) {
      // Authored face-net surfaces must never display a global decal/plane visual.
      // That helper was the source of the floating rectangle seen around the body.
      debug3DLayer('face-net:visual-proxy-blocked', { visualId, areaId: String((area as any)?.id ?? ''), faces: declaredPrintSurface.faces, active: !!options.active, preview: !!options.preview });
      return null;
    }
    const sc = requireScene();
    const id = String((area as any)?.id ?? visualId).trim() || visualId;
    const bounds = getPrintAreaBounds(area);
    const normal = getPrintAreaNormal(area) ?? bounds.direction;
    const color = options.preview ? '#f97316' : (options.active ? '#22c55e' : '#3b82f6');
    const alpha = options.preview ? 0.46 : (options.active ? 0.38 : 0.24);
    const width = Math.max(0.04, bounds.size.x);
    const height = Math.max(0.04, bounds.size.y);
    const depth = Math.max(0.02, Math.min(Math.max(width, height) * 0.25, 0.08));
    let mesh: Mesh | null = null;
    const targetMesh = findModelMesh(String((area as any)?.targetMeshId ?? '').trim() || 'product') ?? findModelMesh(null);
    const isSurfaceArea = (area as any)?.mode === 'surface' && !!targetMesh;
    if (isSurfaceArea && targetMesh) {
      try {
        mesh = (MeshBuilder as any).CreateDecal(visualId, targetMesh, {
          position: bounds.center,
          normal,
          size: new Vector3(width, height, depth),
          angle: 0,
        }) as Mesh;
      } catch { mesh = null; }
    }
    if (!mesh) {
      mesh = MeshBuilder.CreatePlane(visualId, { width, height }, sc);
      mesh.position = bounds.center.add(normal.scale(options.preview ? 0.02 : 0.016));
      orientMeshForPrintArea(mesh, area);
    }
    mesh.id = options.preview ? `print-area-preview:${id}` : `print-area:${id}`;
    mesh.name = String((area as any)?.label ?? id);
    mesh.isPickable = false;
    (mesh as any).__asukaPrintAreaId = id;
    (mesh as any).__asukaPrintAreaPreview = !!options.preview;
    const mat = new StandardMaterial(`${mesh.id}-material`, sc);
    mat.diffuseColor = color3FromInput(color);
    mat.emissiveColor = color3FromInput(color);
    mat.alpha = alpha;
    mat.backFaceCulling = false;
    mat.forceDepthWrite = false;
    try { (mat as any).zOffset = -8; } catch {}
    try { (mat as any).zOffsetUnits = -8; } catch {}
    mesh.material = mat;
    try {
      mesh.renderOutline = true;
      mesh.outlineColor = color3FromInput(color);
      mesh.outlineWidth = options.preview ? 0.06 : 0.04;
      (mesh as any).showBoundingBox = true;
    } catch {}
    return mesh;
  };

  const renderPrintAreaVisuals = () => {
    clearPrintAreaVisuals();
    const activeId = getActivePrintAreaId();
    const selectedDecoration = selectedObjectId
      ? decorationRecords.find((record: any) => String(record?.id ?? '') === selectedObjectId) ?? null
      : null;
    const selectedTextureArea = selectedObjectId
      ? getPrintAreas().find((area: any) => Array.isArray(area?.objects) && area.objects.some((object: any) => String(object?.id ?? '') === selectedObjectId)) ?? null
      : null;
    const selectedAreaId = String(
      (selectedDecoration as any)?.targetPrintAreaId
      ?? (selectedDecoration as any)?.metadata?.printAreaId
      ?? (selectedTextureArea as any)?.id
      ?? '',
    ).trim();
    const revealAllForPick = printAreaAddPickState != null;
    for (const area of getPrintAreas()) {
      const id = String(area?.id ?? '').trim();
      if (!id || isTextureSlotPrintArea(area)) continue;
      if (!revealAllForPick && id !== selectedAreaId) continue;
      const isActive = activeId === id || (!activeId && getPrintAreas().length === 1);
      const mesh = createPrintAreaVisualMesh(area, `asuka-print-area-${id}`, { active: isActive });
      if (mesh) printAreaMeshes.set(id, mesh);
    }
  };

  const getDecalRuntimePosition = (mesh?: Mesh | null): Vector3 | null => {
    const value = (mesh as any)?.__asukaDecalPosition;
    if (!Array.isArray(value)) return null;
    const position = toVec3(value, [0, 0, 0]);
    return position;
  };

  type RuntimeProjectionMode = 'decal' | 'surface' | 'texture' | 'plane';
  const toDecorationPlacementMode = (mode: RuntimeProjectionMode): NonNullable<Asuka3DDecoration['placement']>['mode'] => mode === 'decal' ? 'surface' : mode;

  const decorationPlacementFromMesh = (mesh: Mesh, mode: RuntimeProjectionMode = 'plane'): Asuka3DDecoration['placement'] => {
    const runtimeDecalPosition = mode === 'decal' || mode === 'surface' ? getDecalRuntimePosition(mesh) : null;
    return {
      mode: toDecorationPlacementMode(mode),
      // Projected/decal stickers often use an invisible control mesh whose
      // geometry is authored directly in world space while mesh.position stays
      // at [0,0,0]. Saving mesh.position made 3D undo/redo restore stickers at
      // the model origin, so they disappeared until the next drag rebuilt the
      // decal from a fresh surface pick. Persist the actual projection center.
      position: fromVec3(runtimeDecalPosition ?? mesh.position),
      rotation: fromVec3(mesh.rotation),
      scale: fromVec3(mesh.scaling),
    };
  };

  const getMeshScreenBox = (mesh: Mesh | null | undefined): { left: number; top: number; width: number; height: number } | null => {
    const sceneRef = scene;
    const cameraRef = camera;
    const canvasRef = canvas;
    if (!mesh || !canvasRef || !sceneRef || !cameraRef) return null;
    try {
      mesh.computeWorldMatrix(true);
      const vectors = mesh.getBoundingInfo().boundingBox.vectorsWorld;
      const renderEngine = sceneRef.getEngine();
      const renderWidth = Math.max(1, renderEngine.getRenderWidth());
      const renderHeight = Math.max(1, renderEngine.getRenderHeight());
      const viewport = cameraRef.viewport.toGlobal(renderWidth, renderHeight);
      const rect = canvasRef.getBoundingClientRect();
      const transformMatrix = sceneRef.getTransformMatrix();
      const projected = vectors.map((point) => Vector3.Project(point, Matrix.Identity(), transformMatrix, viewport));
      const xs = projected.map((point) => Number(point.x)).filter(Number.isFinite);
      const ys = projected.map((point) => Number(point.y)).filter(Number.isFinite);
      if (!xs.length || !ys.length) return null;
      const minX = Math.min(...xs);
      const maxX = Math.max(...xs);
      const minY = Math.min(...ys);
      const maxY = Math.max(...ys);
      const scaleX = rect.width / renderWidth;
      const scaleY = rect.height / renderHeight;
      return {
        left: rect.left + minX * scaleX,
        top: rect.top + minY * scaleY,
        width: Math.max(24, (maxX - minX) * scaleX),
        height: Math.max(24, (maxY - minY) * scaleY),
      };
    } catch {
      return null;
    }
  };


  const normalizeMeshLookupKey = (value: any): string => String(value ?? '')
    .trim()
    .toLowerCase()
    .replace(/^asuka_(part|surface)_/i, '')
    .replace(/_print$/i, '')
    .replace(/^part[_-]/i, '')
    .replace(/^surface[_-]/i, '')
    .replace(/[^a-z0-9]+/g, '_')
    .replace(/^_+|_+$/g, '');

  const meshMatchesLookupKey = (mesh: Mesh, wanted: string): boolean => {
    const rawWanted = String(wanted ?? '').trim();
    if (!rawWanted) return false;
    const wantedLookup = normalizeMeshLookupKey(rawWanted);
    const suffix = `::${rawWanted}`;
    const values = [mesh.id, mesh.name].map((value) => String(value ?? '').trim()).filter(Boolean);
    for (const value of values) {
      if (value === rawWanted || value.endsWith(suffix)) return true;
      const segments = value.split('::').map((segment) => segment.trim()).filter(Boolean);
      if (segments.some((segment) => segment === rawWanted)) return true;
      if (wantedLookup && segments.some((segment) => normalizeMeshLookupKey(segment) === wantedLookup)) return true;
    }
    return false;
  };

  const findModelMesh = (id?: string | null): Mesh | null => {
    const wanted = String(id ?? '').trim();
    const meshes = modelMeshes.filter((mesh) => !!mesh && !mesh.isDisposed());
    if (wanted) {
      const found = meshes.find((mesh) => mesh.id === wanted || mesh.name === wanted);
      if (found) return found;
      const byRuntime = objectMeshes.get(wanted);
      if (byRuntime && objectKinds.get(wanted) === 'mesh' && !byRuntime.isDisposed()) return byRuntime;
      const sceneMesh = scene?.getMeshById?.(wanted) ?? scene?.getMeshByName?.(wanted) ?? null;
      if (sceneMesh && !(sceneMesh as Mesh).isDisposed?.()) return sceneMesh as Mesh;
      const byInstancedPartName = meshes.find((mesh) => meshMatchesLookupKey(mesh, wanted));
      if (byInstancedPartName) return byInstancedPartName;
    }
    return meshes[0] ?? null;
  };


  const getPrintAreaCanvasSize = (area?: Asuka3DPrintArea | null): { width: number; height: number } => ({
    width: Math.max(16, Number((area as any)?.canvasSize?.width ?? 1024) || 1024),
    height: Math.max(16, Number((area as any)?.canvasSize?.height ?? 1024) || 1024),
  });

  const normalizeTextureUvPoint = (value: any): { x: number; y: number } | null => {
    const rawX = Array.isArray(value) ? value[0] : (value?.x ?? value?.u);
    const rawY = Array.isArray(value) ? value[1] : (value?.y ?? value?.v);
    const x = Number(rawX);
    const y = Number(rawY);
    if (!Number.isFinite(x) || !Number.isFinite(y)) return null;
    return { x: clampNumber(x, 0, 1), y: clampNumber(y, 0, 1) };
  };

  const getPickTextureUv = (pick: any): { x: number; y: number } | null => {
    try {
      const coords = typeof pick?.getTextureCoordinates === 'function'
        ? pick.getTextureCoordinates()
        : (pick?.textureCoordinates ?? pick?.uv ?? null);
      return normalizeTextureUvPoint(coords);
    } catch {
      return null;
    }
  };

  const normalizeTextureUvBounds = (value: any): { x: number; y: number; width: number; height: number } | null => {
    if (!value || typeof value !== 'object') return null;
    const x = Number(value.x);
    const y = Number(value.y);
    const width = Number(value.width);
    const height = Number(value.height);
    if (![x, y, width, height].every(Number.isFinite)) return null;
    if (width <= 0 || height <= 0) return null;
    const safeWidth = clampNumber(width, 0.01, 1);
    const safeHeight = clampNumber(height, 0.01, 1);
    return {
      x: clampNumber(x, 0, Math.max(0, 1 - safeWidth)),
      y: clampNumber(y, 0, Math.max(0, 1 - safeHeight)),
      width: safeWidth,
      height: safeHeight,
    };
  };

  const makeUvBoundsFromCenter = (center: { x: number; y: number }, width = 0.46, height = 0.46) => {
    const safeWidth = clampNumber(Number(width) || 0.46, 0.04, 1);
    const safeHeight = clampNumber(Number(height) || 0.46, 0.04, 1);
    return {
      x: clampNumber(center.x - safeWidth / 2, 0, Math.max(0, 1 - safeWidth)),
      y: clampNumber(center.y - safeHeight / 2, 0, Math.max(0, 1 - safeHeight)),
      width: safeWidth,
      height: safeHeight,
    };
  };

  const getTexturePrintAreaUvBounds = (area?: Asuka3DPrintArea | null) => normalizeTextureUvBounds(
    (area as any)?.uvBounds
      ?? (area as any)?.metadata?.uvBounds
      ?? (area as any)?.metadata?.visibleUvBounds
      ?? null,
  );

  const getTexturePrintAreaVisibleUvCenter = (area?: Asuka3DPrintArea | null): { x: number; y: number } | null => {
    const explicit = normalizeTextureUvPoint(
      (area as any)?.metadata?.visibleUvCenter
        ?? (area as any)?.metadata?.uvCenter
        ?? (area as any)?.uvCenter
        ?? null,
    );
    if (explicit) return explicit;
    if (!area || !isTextureSlotPrintArea(area)) return null;
    const targetMesh = getTexturePrintAreaTargetMesh(area);
    const surface = pickVisibleModelSurface(targetMesh);
    return surface?.uv ?? null;
  };

  const getTexturePrintAreaContentRect = (area: Asuka3DPrintArea, size = getPrintAreaCanvasSize(area)) => {
    const uvBounds = getTexturePrintAreaUvBounds(area);
    if (uvBounds) {
      return {
        x: uvBounds.x * size.width,
        y: uvBounds.y * size.height,
        width: Math.max(1, uvBounds.width * size.width),
        height: Math.max(1, uvBounds.height * size.height),
        constrained: true,
      };
    }
    const center = getTexturePrintAreaVisibleUvCenter(area);
    if (center) {
      const metadata = (area as any)?.metadata && typeof (area as any).metadata === 'object' ? (area as any).metadata : {};
      const bounds = makeUvBoundsFromCenter(
        center,
        Number(metadata.visibleUvWidth ?? metadata.uvWidth ?? 0.46) || 0.46,
        Number(metadata.visibleUvHeight ?? metadata.uvHeight ?? 0.46) || 0.46,
      );
      return {
        x: bounds.x * size.width,
        y: bounds.y * size.height,
        width: Math.max(1, bounds.width * size.width),
        height: Math.max(1, bounds.height * size.height),
        constrained: true,
      };
    }
    return { x: 0, y: 0, width: size.width, height: size.height, constrained: false };
  };

  const normalizeTexturePrintAreaObjectPlacement = (area: Asuka3DPrintArea, object: any, contentRect = getTexturePrintAreaContentRect(area)) => {
    const size = getPrintAreaCanvasSize(area);
    const maxWidth = contentRect.constrained ? Math.max(1, contentRect.width) : size.width;
    const maxHeight = contentRect.constrained ? Math.max(1, contentRect.height) : size.height;
    const width = Math.max(1, Math.min(Number(object?.width ?? contentRect.width) || contentRect.width, maxWidth));
    const height = Math.max(1, Math.min(Number(object?.height ?? contentRect.height) || contentRect.height, maxHeight));
    const minX = contentRect.constrained ? contentRect.x : 0;
    const minY = contentRect.constrained ? contentRect.y : 0;
    const maxX = contentRect.constrained ? contentRect.x + contentRect.width - width : size.width - width;
    const maxY = contentRect.constrained ? contentRect.y + contentRect.height - height : size.height - height;
    return {
      ...object,
      width,
      height,
      x: clampNumber(Number(object?.x ?? minX) || minX, Math.min(minX, maxX), Math.max(minX, maxX)),
      y: clampNumber(Number(object?.y ?? minY) || minY, Math.min(minY, maxY), Math.max(minY, maxY)),
    };
  };

  const normalizeTextureSlotName = (material: any, area?: Asuka3DPrintArea | null): string => {
    const requested = String((area as any)?.textureSlot ?? '').trim();
    const raw = requested || (material && 'albedoTexture' in material ? 'albedoTexture' : 'diffuseTexture');
    const aliases: Record<string, string> = {
      albedo: 'albedoTexture',
      basecolor: 'albedoTexture',
      baseColor: 'albedoTexture',
      baseColorTexture: 'albedoTexture',
      diffuse: 'diffuseTexture',
      color: material && 'albedoTexture' in material ? 'albedoTexture' : 'diffuseTexture',
      map: material && 'albedoTexture' in material ? 'albedoTexture' : 'diffuseTexture',
    };
    const normalized = aliases[raw] ?? raw;
    if (normalized === 'diffuseTexture' && material && !('diffuseTexture' in material) && 'albedoTexture' in material) return 'albedoTexture';
    if (normalized === 'albedoTexture' && material && !('albedoTexture' in material) && 'diffuseTexture' in material) return 'diffuseTexture';
    return normalized;
  };

  const getTexturePrintAreaTargetMesh = (area: Asuka3DPrintArea): Mesh | null => {
    const wanted = String((area as any)?.targetMeshId ?? 'product').trim() || 'product';
    return findModelMesh(wanted) ?? findModelMesh(null);
  };

  const getOrCreateTexturePrintAreaRuntime = (area: Asuka3DPrintArea) => {
    const sc = requireScene();
    const id = String((area as any)?.id ?? '').trim();
    if (!id || !isTextureSlotPrintArea(area)) return null;
    const targetMesh = getTexturePrintAreaTargetMesh(area);
    if (!targetMesh) return null;
    let material: any = targetMesh.material;
    if (!material) {
      material = new StandardMaterial(`asuka-print-area-material-${id}`, sc);
      targetMesh.material = material;
    }
    const slot = normalizeTextureSlotName(material, area);
    const size = getPrintAreaCanvasSize(area);
    const existing = printAreaTextures.get(id);
    const existingSize = existing ? (existing.texture as any).getSize?.() : null;
    const sameRuntime = existing
      && existing.mesh === targetMesh
      && existing.material === material
      && existing.slot === slot
      && Number(existingSize?.width ?? 0) === size.width
      && Number(existingSize?.height ?? 0) === size.height;
    if (sameRuntime) return existing;

    if (existing) {
      try { if (existing.material && existing.slot) existing.material[existing.slot] = existing.originalTexture ?? null; } catch {}
      try { existing.texture.dispose(); } catch {}
      printAreaTextures.delete(id);
    }

    const texture = new DynamicTexture(`asuka-print-area-texture-${id}`, { width: size.width, height: size.height }, sc, false);
    const metadata = (area as any)?.metadata && typeof (area as any).metadata === 'object' ? (area as any).metadata : {};
    // Drawing Areas on complex meshes: force transparent so the DynamicTexture
    // does NOT discolour the original material. Images are painted via a separate
    // surface-patch overlay mesh and never touch this DynamicTexture.
    // Classify the mesh regardless of whether the area has printSurface3D, so
    // legacy texture-slot areas (created before the face-net flow) also stay
    // transparent on complex/organic meshes (shoe, shirt, etc.).
    const isDrawingAreaComplexMesh = (() => {
      const m = getTexturePrintAreaTargetMesh(area);
      return m ? classifyMeshForPrintArea(String(m.id ?? m.name ?? '')).type === 'complex' : false;
    })();
    const transparentOverlay = (area as any)?.transparentBackground === true
      || metadata.transparentBackground === true
      || metadata.textureSlotComposite === 'transparent-overlay'
      || metadata.nativePrintSurface === true
      || isDrawingAreaComplexMesh;
    // Product material texture slots must stay opaque to avoid see-through faces.
    // GLB helper meshes named asuka_surface_* are different: they are overlay
    // print surfaces, so transparent pixels must remain transparent. Otherwise
    // adding a design paints a black rectangle over the product.
    texture.hasAlpha = transparentOverlay;
    const originalTexture = material ? material[slot] : null;
    try { material[slot] = texture; } catch {}
    try { if ('useAlphaFromDiffuseTexture' in material) material.useAlphaFromDiffuseTexture = transparentOverlay; } catch {}
    try { if ('useAlphaFromAlbedoTexture' in material) material.useAlphaFromAlbedoTexture = transparentOverlay; } catch {}
    try { if (typeof material.alpha === 'number' && material.alpha <= 0) material.alpha = 1; } catch {}
    try { material.transparencyMode = transparentOverlay ? 2 : 0; } catch {}
    const runtime = { texture, mesh: targetMesh, material, slot, originalTexture };
    printAreaTextures.set(id, runtime);
    return runtime;
  };

  const paintPrintAreaTextureBackground = (ctx: CanvasRenderingContext2D, width: number, height: number, area: Asuka3DPrintArea, runtime?: { material?: any } | null) => {
    ctx.clearRect(0, 0, width, height);
    const metadata = (area as any)?.metadata && typeof (area as any).metadata === 'object' ? (area as any).metadata : {};
    const transparent = (area as any)?.transparentBackground === true
      || metadata.transparentBackground === true
      || metadata.nativePrintSurface === true
      || metadata.textureSlotComposite === 'transparent-overlay';
    if (transparent) {
      try { ctx.globalCompositeOperation = 'copy'; ctx.fillStyle = 'rgba(0,0,0,0)'; ctx.fillRect(0, 0, width, height); ctx.globalCompositeOperation = 'source-over'; } catch {}
      return;
    }
    const background = String((area as any)?.background ?? metadata.background ?? '').trim()
      || getMaterialBaseColorCss(runtime?.material, '#ffffff');
    ctx.save();
    ctx.fillStyle = background;
    ctx.fillRect(0, 0, width, height);
    ctx.restore();
  };

  const notifyTextureImageLoaded = (url: string) => {
    const key = String(url ?? '').trim();
    if (!key) return;
    const areaIds = Array.from(printAreaImageLoadAreaIds.get(key) ?? []);
    printAreaImageLoadAreaIds.delete(key);
    for (const areaId of areaIds) requestPrintAreaTextureRender(areaId);
  };

  const trackTextureImageLoad = (url: string, areaIdInput?: string | null) => {
    const key = String(url ?? '').trim();
    const areaId = String(areaIdInput ?? '').trim();
    if (!key || !areaId) return;
    let areaIds = printAreaImageLoadAreaIds.get(key);
    if (!areaIds) {
      areaIds = new Set<string>();
      printAreaImageLoadAreaIds.set(key, areaIds);
    }
    areaIds.add(areaId);
  };

  const loadTexturePrintAreaImage = (url: string, areaIdInput?: string | null): HTMLImageElement | null => {
    const key = String(url ?? '').trim();
    if (!key || typeof Image === 'undefined') return null;
    const areaId = String(areaIdInput ?? '').trim();
    const cached = printAreaImageCache.get(key);
    if (cached) {
      // If the image is already usable, do not schedule another render from
      // inside renderPrintAreaTexture(). The previous implementation scheduled
      // a new render for every cached draw, causing an unbounded render loop
      // after repeated undo/redo operations.
      if (!(cached.complete && Number((cached as any).naturalWidth ?? 0) > 0)) trackTextureImageLoad(key, areaId);
      return cached;
    }
    const image = new Image();
    if (!/^data:|^blob:/i.test(key)) {
      try { image.crossOrigin = 'anonymous'; } catch {}
    }
    trackTextureImageLoad(key, areaId);
    let settled = false;
    const notifyLoaded = () => {
      if (settled) return;
      settled = true;
      notifyTextureImageLoaded(key);
    };
    image.onload = () => notifyLoaded();
    image.onerror = () => notifyLoaded();
    image.src = key;
    printAreaImageCache.set(key, image);
    try { void (image as any).decode?.().then(notifyLoaded).catch(() => notifyLoaded()); } catch {}
    return image;
  };

  const drawTexturePrintAreaObject = (ctx: CanvasRenderingContext2D, object: any, area: Asuka3DPrintArea, width: number, height: number) => {
    if (!object || object.visible === false) return;
    const type = String(object.type ?? '').trim().toLowerCase();
    const source = object.source && typeof object.source === 'object' ? object.source : {};
    const x = Number(object.x ?? width * 0.25) || 0;
    const y = Number(object.y ?? height * 0.25) || 0;
    const w = Math.max(1, Number(object.width ?? width * 0.5) || width * 0.5);
    const h = Math.max(1, Number(object.height ?? (type === 'text' ? height * 0.16 : height * 0.5)) || height * 0.5);
    const rotation = (Number(object.rotation ?? 0) || 0) * Math.PI / 180;
    const opacity = Math.max(0, Math.min(1, Number(object.opacity ?? 1) || 1));
    ctx.save();
    ctx.beginPath();
    ctx.rect(0, 0, width, height);
    ctx.clip();
    ctx.globalAlpha = opacity;
    ctx.translate(x + w / 2, y + h / 2);
    if (rotation) ctx.rotate(rotation);
    if (type === 'text') {
      const text = String(source.text ?? object.text ?? '').trim();
      if (text) {
        const fontSize = Math.max(8, Number(source.fontSize ?? object.fontSize ?? Math.min(h * 0.72, width * 0.08)) || 48);
        const fontFamily = String(source.fontFamily ?? object.fontFamily ?? 'Arial').trim() || 'Arial';
        ctx.fillStyle = String(source.color ?? object.color ?? '#111827');
        ctx.textAlign = 'center';
        ctx.textBaseline = 'middle';
        ctx.font = `${fontSize}px ${fontFamily}`;
        const lines = text.split(/\r?\n/);
        const lineHeight = fontSize * 1.18;
        const startY = -((lines.length - 1) * lineHeight) / 2;
        lines.forEach((line, index) => ctx.fillText(line, 0, startY + index * lineHeight, w));
      }
      ctx.restore();
      return;
    }
    const svgSource = String(source.svg ?? object.svg ?? '').trim();
    let url = String(source.url ?? object.url ?? '').trim();
    if (svgSource && (!url || /^blob:/i.test(url) || looksLikeInlineSvg(url))) url = svgToDataUrl(svgSource);
    const image = loadTexturePrintAreaImage(url, String((area as any)?.id ?? ''));
    if (image && Number((image as any).naturalWidth ?? 0) > 0) {
      try { ctx.drawImage(image, -w / 2, -h / 2, w, h); } catch {}
    }
    ctx.restore();
  };

  function renderPrintAreaTextureById(idInput: string) {
    const id = String(idInput ?? '').trim();
    if (!id) return;
    const area = getPrintAreas().find((candidate) => String(candidate?.id ?? '') === id);
    if (!area) return;
    renderPrintAreaTexture(area);
  }

  const requestPrintAreaTextureRender = (idInput: string) => {
    const id = String(idInput ?? '').trim();
    if (!id) return;
    if (!printAreaTextureRenderRafs.has(id)) {
      try {
        const raf = requestAnimationFrame(() => {
          printAreaTextureRenderRafs.delete(id);
          try { renderPrintAreaTextureById(id); } catch {}
        });
        printAreaTextureRenderRafs.set(id, raf);
      } catch {
        try { renderPrintAreaTextureById(id); } catch {}
      }
    }
    const previousTimer = printAreaTextureRenderTimers.get(id);
    if (previousTimer) {
      try { clearTimeout(previousTimer); } catch {}
    }
    try {
      const timer = setTimeout(() => {
        printAreaTextureRenderTimers.delete(id);
        try { renderPrintAreaTextureById(id); } catch {}
      }, 80) as unknown as number;
      printAreaTextureRenderTimers.set(id, timer);
    } catch {}
  };

  function renderPrintAreaTexture(area: Asuka3DPrintArea) {
    if (!isTextureSlotPrintArea(area)) return;
    const runtime = getOrCreateTexturePrintAreaRuntime(area);
    if (!runtime) return;
    const size = getPrintAreaCanvasSize(area);
    const ctx = runtime.texture.getContext() as CanvasRenderingContext2D;
    paintPrintAreaTextureBackground(ctx, size.width, size.height, area, runtime);
    const contentRect = getTexturePrintAreaContentRect(area, size);
    ctx.save();
    ctx.beginPath();
    ctx.rect(contentRect.x, contentRect.y, contentRect.width, contentRect.height);
    ctx.clip();
    for (const rawObject of Array.isArray((area as any)?.objects) ? (area as any).objects : []) {
      const object = normalizeTexturePrintAreaObjectPlacement(area, rawObject, contentRect);
      drawTexturePrintAreaObject(ctx, object, area, size.width, size.height);
    }
    ctx.restore();
    try { runtime.texture.update(false); } catch { try { runtime.texture.update(); } catch {} }
  }

  function renderPrintAreaTextures() {
    const areas = getPrintAreas().filter((area) => isTextureSlotPrintArea(area));
    const ids = new Set(areas.map((area) => String(area?.id ?? '').trim()).filter(Boolean));
    for (const [id, runtime] of Array.from(printAreaTextures.entries())) {
      if (ids.has(id)) continue;
      try { if (runtime.material && runtime.slot) runtime.material[runtime.slot] = runtime.originalTexture ?? null; } catch {}
      try { runtime.texture.dispose(); } catch {}
      printAreaTextures.delete(id);
    }
    for (const area of areas) renderPrintAreaTexture(area);
  }

  const updateTexturePrintAreaObjects = (
    areaIdInput: string,
    updater: (objects: any[], area: Asuka3DPrintArea) => any[],
    options: { render?: 'immediate' | 'scheduled' | 'none'; cloneObjects?: boolean } = {},
  ) => {
    const areaId = String(areaIdInput ?? '').trim();
    if (!areaId) return null;
    let updatedArea: Asuka3DPrintArea | null = null;
    const nextAreas = getPrintAreas().map((area) => {
      if (String(area?.id ?? '') !== areaId) return area;
      const rawObjects = Array.isArray((area as any).objects) ? (area as any).objects as any[] : [];
      const objects = options.cloneObjects === false ? rawObjects.slice() : clonePlain(rawObjects) as any[];
      const contentRect = getTexturePrintAreaContentRect(area);
      const nextObjects = (updater(objects, area) ?? []).map((object) => normalizeTexturePrintAreaObjectPlacement(area, object, contentRect));
      updatedArea = { ...(area as any), objects: nextObjects } as Asuka3DPrintArea;
      return updatedArea;
    });
    if (!updatedArea) return null;
    currentState = { ...((currentState ?? {}) as any), printAreas: nextAreas } as Asuka3DSceneState;
    const renderMode = options.render ?? 'immediate';
    if (renderMode === 'scheduled') requestPrintAreaTextureRender(areaId);
    else if (renderMode !== 'none') renderPrintAreaTexture(updatedArea);
    return updatedArea;
  };

  const findTexturePrintAreaObject = (objectIdInput: string): { area: Asuka3DPrintArea; object: any } | null => {
    const objectId = String(objectIdInput ?? '').trim();
    if (!objectId) return null;
    for (const area of getPrintAreas()) {
      const object = (Array.isArray((area as any)?.objects) ? (area as any).objects : []).find((item: any) => String(item?.id ?? '') === objectId);
      if (object) return { area, object };
    }
    return null;
  };


  const getTexturePrintAreaPointerPoint = (area: Asuka3DPrintArea, event?: PointerEvent | MouseEvent | null, pickedMesh?: any): { x: number; y: number; uv: { x: number; y: number } } | null => {
    if (!scene || !isTextureSlotPrintArea(area)) return null;
    const targetMesh = getTexturePrintAreaTargetMesh(area);
    if (!targetMesh) return null;
    const isTargetMesh = (mesh: any) => !!mesh && (mesh === targetMesh || mesh.id === targetMesh.id || mesh.name === targetMesh.name);
    let pick: any = null;
    const directMesh = pickedMesh as Mesh | null;
    if (directMesh && isTargetMesh(directMesh)) {
      pick = { hit: true, pickedMesh: directMesh, textureCoordinates: null };
    }
    const ray = getPointerPickingRay(event ?? null);
    if (ray) {
      try {
        const rayPick = scene.pickWithRay(ray, (mesh: any) => isTargetMesh(mesh), false);
        if (rayPick?.hit && rayPick.pickedMesh) pick = rayPick;
      } catch {}
    }
    if (!pick?.hit && !pick?.pickedMesh) return null;
    const uv = getPickTextureUv(pick);
    if (!uv) return null;
    const size = getPrintAreaCanvasSize(area);
    return { x: uv.x * size.width, y: uv.y * size.height, uv };
  };

  const findTexturePrintAreaObjectAtPointer = (event?: PointerEvent | MouseEvent | null, pickedMesh?: any): { area: Asuka3DPrintArea; object: any; point: { x: number; y: number; uv: { x: number; y: number } }; contentRect: { x: number; y: number; width: number; height: number; constrained: boolean } } | null => {
    for (const area of getPrintAreas().filter((candidate) => isTextureSlotPrintArea(candidate))) {
      const point = getTexturePrintAreaPointerPoint(area, event ?? null, pickedMesh);
      if (!point) continue;
      const contentRect = getTexturePrintAreaContentRect(area);
      const pad = Math.max(6, Math.min(24, Math.min(contentRect.width, contentRect.height) * 0.035));
      const insideContent = point.x >= contentRect.x - pad
        && point.x <= contentRect.x + contentRect.width + pad
        && point.y >= contentRect.y - pad
        && point.y <= contentRect.y + contentRect.height + pad;
      if (!insideContent) continue;
      const objects = Array.isArray((area as any)?.objects) ? (area as any).objects : [];
      for (let index = objects.length - 1; index >= 0; index -= 1) {
        const object = normalizeTexturePrintAreaObjectPlacement(area, objects[index], contentRect);
        if (!object || object.visible === false) continue;
        const hitPad = Math.max(10, Math.min(28, Math.min(Number(object.width ?? 0) || 0, Number(object.height ?? 0) || 0) * 0.18));
        const left = Number(object.x ?? 0) - hitPad;
        const top = Number(object.y ?? 0) - hitPad;
        const right = Number(object.x ?? 0) + Math.max(1, Number(object.width ?? 1) || 1) + hitPad;
        const bottom = Number(object.y ?? 0) + Math.max(1, Number(object.height ?? 1) || 1) + hitPad;
        if (point.x >= left && point.x <= right && point.y >= top && point.y <= bottom) {
          return { area, object, point, contentRect };
        }
      }
    }
    return null;
  };

  const addTexturePrintAreaObject = (area: Asuka3DPrintArea, object: any) => {
    const id = String((area as any)?.id ?? '').trim();
    updateTexturePrintAreaObjects(id, (objects) => [...objects, object]);
    requestPrintAreaTextureRender(id);
    setSelectedObject(String(object?.id ?? '') || null, false);
    emitChanged(`addTexturePrintAreaObject:${String(object?.type ?? 'object')}`, object?.id ?? id);
    requestSelectionMenuForAdded3DLayer(String(object?.id ?? id), `add-${String(object?.type ?? 'object')}`);
    return objectRef(String(object?.id ?? id), String(object?.type ?? 'object'));
  };

  const makeTexturePrintAreaObject = (area: Asuka3DPrintArea, id: string, type: string, source: Record<string, unknown>, input: Record<string, unknown> = {}) => {
    const size = getPrintAreaCanvasSize(area);
    const contentRect = getTexturePrintAreaContentRect(area, size);
    const isText = type === 'text';
    const requestedWidth = Number((input as any).width);
    const requestedHeight = Number((input as any).height);
    const defaultWidth = isText
      ? Math.min(size.width * 0.72, contentRect.width * 0.9)
      : Math.min(size.width * 0.42, contentRect.width * 0.74);
    const defaultHeight = isText
      ? Math.min(size.height * 0.18, contentRect.height * 0.72)
      : Math.min(size.height * 0.42, contentRect.height * 0.74);
    const width = Math.max(16, Math.min(Number.isFinite(requestedWidth) && requestedWidth > 0 ? requestedWidth : defaultWidth, contentRect.constrained ? contentRect.width * 0.94 : size.width));
    const height = Math.max(16, Math.min(Number.isFinite(requestedHeight) && requestedHeight > 0 ? requestedHeight : defaultHeight, contentRect.constrained ? contentRect.height * 0.94 : size.height));
    const object = {
      id,
      type,
      x: contentRect.x + Math.max(0, (contentRect.width - width) / 2),
      y: contentRect.y + Math.max(0, (contentRect.height - height) / 2),
      width,
      height,
      rotation: Number((input as any).rotation ?? 0) || 0,
      opacity: Number((input as any).opacity ?? 1) || 1,
      visible: true,
      locked: false,
      source,
      price: (input as any).price,
      printingMethodId: (input as any).printingMethodId ?? null,
      metadata: {
        ...(((input as any).metadata && typeof (input as any).metadata === 'object') ? (input as any).metadata : {}),
        printAreaId: area.id,
        placement: 'texture',
        initialPlacement: { x: contentRect.x + Math.max(0, (contentRect.width - width) / 2), y: contentRect.y + Math.max(0, (contentRect.height - height) / 2), width, height, rotation: Number((input as any).rotation ?? 0) || 0 },
        initialContentRect: { x: Math.round(contentRect.x), y: Math.round(contentRect.y), width: Math.round(contentRect.width), height: Math.round(contentRect.height) },
      },
    };
    return normalizeTexturePrintAreaObjectPlacement(area, object, contentRect);
  };

  const getMeshWorldBounds = (mesh: Mesh) => {
    try {
      mesh.computeWorldMatrix(true);
      const box = mesh.getBoundingInfo().boundingBox;
      const min = box.minimumWorld.clone();
      const max = box.maximumWorld.clone();
      return { min, max, center: min.add(max).scale(0.5), size: max.subtract(min) };
    } catch {
      return { min: new Vector3(-1, -1, -0.1), max: new Vector3(1, 1, 0.1), center: Vector3.Zero(), size: new Vector3(2, 2, 0.2) };
    }
  };

  const getMeshIndexedWorldBounds = (mesh: Mesh): { min: Vector3; max: Vector3; center: Vector3; size: Vector3 } | null => {
    try {
      const positions = getMeshFloatData(mesh, 'position');
      const vertexCount = Math.floor(positions.length / 3);
      if (vertexCount <= 0) return null;
      const indices = getMeshIndicesArray(mesh, vertexCount);
      if (!indices.length) return null;
      mesh.computeWorldMatrix(true);
      const world = mesh.getWorldMatrix();
      const seen = new Set<number>();
      let min = new Vector3(Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY);
      let max = new Vector3(Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY);
      let used = 0;
      for (const rawIndex of indices) {
        const index = Math.trunc(Number(rawIndex));
        if (!Number.isFinite(index) || index < 0 || index >= vertexCount || seen.has(index)) continue;
        seen.add(index);
        const offset = index * 3;
        const point = Vector3.TransformCoordinates(new Vector3(positions[offset] || 0, positions[offset + 1] || 0, positions[offset + 2] || 0), world);
        min = Vector3.Minimize(min, point);
        max = Vector3.Maximize(max, point);
        used += 1;
      }
      if (!used || !Number.isFinite(min.x) || !Number.isFinite(max.x)) return null;
      const size = max.subtract(min);
      if (size.lengthSquared() <= 0.000001) return null;
      return { min, max, center: min.add(max).scale(0.5), size };
    } catch {
      return null;
    }
  };

  const getGeometryComponentWorldBounds = (mesh: Mesh, component: RuntimeGeometryComponent): { min: Vector3; max: Vector3; center: Vector3; size: Vector3 } | null => {
    try {
      const positions = getMeshFloatData(mesh, 'position');
      const vertexCount = Math.floor(positions.length / 3);
      const vertexIndices = Array.isArray(component?.vertexIndices) ? component.vertexIndices : [];
      if (vertexCount <= 0 || !vertexIndices.length) return null;
      mesh.computeWorldMatrix(true);
      const world = mesh.getWorldMatrix();
      let min = new Vector3(Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY);
      let max = new Vector3(Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY);
      let used = 0;
      for (const rawIndex of vertexIndices) {
        const index = Math.trunc(Number(rawIndex));
        if (!Number.isFinite(index) || index < 0 || index >= vertexCount) continue;
        const offset = index * 3;
        const point = Vector3.TransformCoordinates(new Vector3(positions[offset] || 0, positions[offset + 1] || 0, positions[offset + 2] || 0), world);
        min = Vector3.Minimize(min, point);
        max = Vector3.Maximize(max, point);
        used += 1;
      }
      if (!used || !Number.isFinite(min.x) || !Number.isFinite(max.x)) return null;
      const size = max.subtract(min);
      if (size.lengthSquared() <= 0.000001) return null;
      return { min, max, center: min.add(max).scale(0.5), size };
    } catch {
      return null;
    }
  };


  const getGeometryComponentWorldSamplePoints = (mesh: Mesh, component: RuntimeGeometryComponent, limit = Math.max(18, focusOcclusionSamples * 3)): Vector3[] => {
    try {
      const positions = getMeshFloatData(mesh, 'position');
      const vertexCount = Math.floor(positions.length / 3);
      const vertexIndices = Array.isArray(component?.vertexIndices) ? component.vertexIndices : [];
      if (vertexCount <= 0 || !vertexIndices.length) return [];
      mesh.computeWorldMatrix(true);
      const world = mesh.getWorldMatrix();
      const maxSamples = Math.max(6, Math.min(96, Math.trunc(limit) || 24));
      const step = Math.max(1, Math.floor(vertexIndices.length / maxSamples));
      const samples: Vector3[] = [];
      const seen = new Set<number>();
      for (let i = 0; i < vertexIndices.length && samples.length < maxSamples; i += step) {
        const index = Math.trunc(Number(vertexIndices[i]));
        if (!Number.isFinite(index) || index < 0 || index >= vertexCount || seen.has(index)) continue;
        seen.add(index);
        const offset = index * 3;
        samples.push(Vector3.TransformCoordinates(new Vector3(positions[offset] || 0, positions[offset + 1] || 0, positions[offset + 2] || 0), world));
      }
      if (samples.length < Math.min(maxSamples, vertexIndices.length)) {
        for (const rawIndex of vertexIndices) {
          if (samples.length >= maxSamples) break;
          const index = Math.trunc(Number(rawIndex));
          if (!Number.isFinite(index) || index < 0 || index >= vertexCount || seen.has(index)) continue;
          seen.add(index);
          const offset = index * 3;
          samples.push(Vector3.TransformCoordinates(new Vector3(positions[offset] || 0, positions[offset + 1] || 0, positions[offset + 2] || 0), world));
        }
      }
      return samples;
    } catch {
      return [];
    }
  };

  const getMeshWorldSamplePointsInsideBounds = (mesh: Mesh, bounds: RuntimeWorldBounds, limit = Math.max(18, focusOcclusionSamples * 3)): Vector3[] => {
    try {
      const positions = getMeshFloatData(mesh, 'position');
      const vertexCount = Math.floor(positions.length / 3);
      if (vertexCount <= 0) return [];
      mesh.computeWorldMatrix(true);
      const world = mesh.getWorldMatrix();
      const maxSamples = Math.max(6, Math.min(96, Math.trunc(limit) || 24));
      const margin = Math.max(0.01, Math.min(0.18, bounds.size.length() * 0.055));
      const step = Math.max(1, Math.floor(vertexCount / Math.max(maxSamples * 2, 1)));
      const samples: Vector3[] = [];
      const addPoint = (index: number): void => {
        if (samples.length >= maxSamples) return;
        const offset = index * 3;
        const point = Vector3.TransformCoordinates(new Vector3(positions[offset] || 0, positions[offset + 1] || 0, positions[offset + 2] || 0), world);
        if (isPointInsideExpandedBounds(point, bounds, margin)) samples.push(point);
      };
      for (let index = 0; index < vertexCount && samples.length < maxSamples; index += step) addPoint(index);
      if (samples.length < Math.min(maxSamples, 10)) {
        for (let index = 0; index < vertexCount && samples.length < maxSamples; index += 1) addPoint(index);
      }
      return samples;
    } catch {
      return [];
    }
  };

  const getBoundsOutwardFocusNormal = (bounds: RuntimeWorldBounds, modelBounds: RuntimeWorldBounds): Vector3 => {
    let normal = bounds.center.subtract(modelBounds.center);
    if (normal.lengthSquared() < 0.0001 && camera) normal = camera.position.subtract(bounds.center);
    if (normal.lengthSquared() < 0.0001) normal = new Vector3(0, 0, 1);
    normal.normalize();
    return normal;
  };

  const getGeometryComponentWorldNormal = (mesh: Mesh, component: RuntimeGeometryComponent, bounds: RuntimeWorldBounds, modelBounds: RuntimeWorldBounds): Vector3 => {
    const fallback = getBoundsOutwardFocusNormal(bounds, modelBounds);
    try {
      const normals = getMeshFloatData(mesh, 'normal');
      const normalCount = Math.floor(normals.length / 3);
      const vertexIndices = Array.isArray(component?.vertexIndices) ? component.vertexIndices : [];
      if (!normalCount || !vertexIndices.length) return fallback;
      mesh.computeWorldMatrix(true);
      const world = mesh.getWorldMatrix();
      const sum = Vector3.Zero();
      let used = 0;
      const seen = new Set<number>();
      for (const rawIndex of vertexIndices) {
        const index = Math.trunc(Number(rawIndex));
        if (!Number.isFinite(index) || index < 0 || index >= normalCount || seen.has(index)) continue;
        seen.add(index);
        const offset = index * 3;
        const localNormal = new Vector3(normals[offset] || 0, normals[offset + 1] || 0, normals[offset + 2] || 0);
        if (localNormal.lengthSquared() < 0.000001) continue;
        const worldNormal = Vector3.TransformNormal(localNormal, world);
        if (worldNormal.lengthSquared() < 0.000001) continue;
        worldNormal.normalize();
        sum.addInPlace(worldNormal);
        used += 1;
      }
      if (!used || sum.lengthSquared() < 0.000001) return fallback;
      sum.normalize();
      // Normals imported from GLB parts can be mirrored or double-sided. For
      // camera focus we need the outside-facing side of the chosen island, so
      // orient the averaged normal away from the product center.
      if (Vector3.Dot(sum, fallback) < 0) sum.scaleInPlace(-1);
      return sum.lengthSquared() > 0.000001 ? sum : fallback;
    } catch {
      return fallback;
    }
  };

  const getGeometryComponentFocusTarget = (mesh: Mesh, component: RuntimeGeometryComponent, modelBounds: RuntimeWorldBounds): RuntimeComponentFocusTarget | null => {
    const bounds = getGeometryComponentWorldBounds(mesh, component);
    if (!bounds) return null;
    const normal = getGeometryComponentWorldNormal(mesh, component, bounds, modelBounds);
    const toCamera = camera ? camera.position.subtract(bounds.center) : normal.clone();
    if (toCamera.lengthSquared() > 0.000001) toCamera.normalize();
    const facingScore = camera && toCamera.lengthSquared() > 0.000001 ? Vector3.Dot(normal, toCamera) : 0;
    return {
      component,
      bounds,
      normal,
      distanceScore: camera ? Vector3.DistanceSquared(camera.position, bounds.center) : 0,
      facingScore,
      sizeScore: bounds.size.lengthSquared(),
    };
  };

  const getClosestVisibleGeometryComponentFocusTarget = (mesh: Mesh, modelBounds: RuntimeWorldBounds): RuntimeComponentFocusTarget | null => {
    try {
      const components = getUsableGeometryComponents(mesh);
      if (components.length <= 1) return null;
      const ranked = components
        .map((component) => getGeometryComponentFocusTarget(mesh, component, modelBounds))
        .filter((item): item is RuntimeComponentFocusTarget => !!item)
        .sort((a, b) => {
          const facingDelta = b.facingScore - a.facingScore;
          // Prefer the island that is actually visible from the current camera,
          // then fall back to proximity. This avoids framing the opposite twin
          // or centering between two separated components such as left/right swooshes.
          if (Math.abs(facingDelta) > 0.08) return facingDelta;
          return (a.distanceScore - b.distanceScore) || (b.sizeScore - a.sizeScore);
        });
      return ranked[0] ?? null;
    } catch {
      return null;
    }
  };

  const getNamedPrintSurfaceMeshKey = (mesh?: Mesh | null): string => {
    if (!mesh) return '';
    const candidates = [mesh.name, mesh.id].map((value) => String(value ?? '').trim()).filter(Boolean);
    return candidates.find((value) => value.toLowerCase().startsWith(PRINT_SURFACE_MESH_PREFIX)) ?? '';
  };

  const isNamedPrintSurfaceMesh = (mesh?: Mesh | null): boolean => Boolean(getNamedPrintSurfaceMeshKey(mesh));

  const makeNativePrintSurfaceMeshInvisible = (mesh?: Mesh | null): void => {
    if (!mesh || !scene) return;
    try { mesh.isPickable = true; } catch {}
    try { (mesh as any).__asukaNativePrintSurface = true; } catch {}
    // Named GLB print surfaces are only picking/placement helpers. They must not
    // contribute any color to the final render. Using a tiny alpha was not enough
    // on some Babylon/material paths and could still appear as a black rectangle.
    try { mesh.visibility = 0; } catch {}
    try { mesh.renderingGroupId = 1; } catch {}
    try { (mesh as any).alwaysSelectAsActiveMesh = true; } catch {}
    try { (mesh as any).__asukaColorWriteDisabled = true; } catch {}
    try {
      const mat = new StandardMaterial(`${mesh.id || mesh.name || 'asuka-surface'}-transparent-print-surface`, scene);
      mat.alpha = 0;
      mat.diffuseColor = Color3.White();
      mat.emissiveColor = Color3.White();
      mat.backFaceCulling = false;
      mat.disableLighting = true;
      mat.forceDepthWrite = false;
      try { (mat as any).transparencyMode = 2; } catch {}
      try { (mat as any).disableColorWrite = true; } catch {}
      try { (mat as any).needDepthPrePass = false; } catch {}
      mesh.material = mat;
    } catch {}
  };

  const prepareNativePrintSurfaceMeshes = (): void => {
    for (const mesh of modelMeshes) {
      if (isNamedPrintSurfaceMesh(mesh)) makeNativePrintSurfaceMeshInvisible(mesh);
    }
    ensureProductHoverActions();
  };

  const labelFromPrintSurfaceMeshName = (name: string): string => {
    const cleaned = String(name ?? '')
      .replace(/^asuka_surface_/i, '')
      .replace(/_?print$/i, '')
      .replace(/[_-]+/g, ' ')
      .trim();
    if (!cleaned) return 'Print surface';
    return cleaned.replace(/\b\w/g, (char) => char.toUpperCase());
  };

  const makePrintAreaFromNamedSurfaceMesh = (mesh: Mesh, index = 0): Asuka3DPrintArea | null => {
    const key = getNamedPrintSurfaceMeshKey(mesh);
    if (!key) return null;
    const bounds = getMeshWorldBounds(mesh);
    const size = bounds.size;
    const width = Math.max(0.08, Math.abs(size.x) || 0.64);
    const height = Math.max(0.08, Math.abs(size.y) || Math.abs(size.z) || 0.64);
    const depth = Math.max(0.01, Math.min(Math.max(width, height) * 0.08, 0.08));
    const nativeId = String(mesh.id || mesh.name || key).trim() || key;
    return {
      id: key,
      label: labelFromPrintSurfaceMeshName(key) || `Print surface ${index + 1}`,
      targetMeshId: nativeId,
      targetMeshIds: [nativeId],
      mode: 'surface',
      space: 'surface-tangent',
      unit: 'world-unit',
      uvBounds: { x: 0, y: 0, width: 1, height: 1 },
      canvasSize: { width: 1024, height: 1024 },
      objects: [],
      transparentBackground: true,
      visible: mesh.isEnabled?.() !== false,
      locked: false,
      worldBounds: { center: fromVec3(bounds.center), size: [width, height, depth] },
      placement: { position: fromVec3(bounds.center), scale: [width, height, depth] },
      metadata: {
        engine: 'babylon',
        detectedFromMeshName: true,
        nativePrintSurface: true,
        transparentBackground: true,
        textureSlotComposite: 'transparent-overlay',
        sourceMeshId: String(mesh.id ?? '').trim() || undefined,
        sourceMeshName: String(mesh.name ?? '').trim() || undefined,
      },
    } as Asuka3DPrintArea;
  };

  listDetectedPrintSurfaceAreas = () => getRenderableModelMeshes(modelMeshes)
    .filter((mesh) => isNamedPrintSurfaceMesh(mesh))
    .map((mesh, index) => makePrintAreaFromNamedSurfaceMesh(mesh, index))
    .filter(Boolean) as Asuka3DPrintArea[];

  const getCurrentProductParts = (productIdInput?: string | null): any[] => {
    const productId = String(productIdInput ?? '').trim();
    const selectedProduct = getProductById(productId || getSelectedProductId()) ?? getProductsFromState(currentState)[0] ?? null;
    const runtimeCatalog = selectedProduct ? findRuntimeProductCatalog(selectedProduct) : runtimeProductCatalog;
    const source = Array.isArray(runtimeCatalog?.parts) && runtimeCatalog.parts.length
      ? runtimeCatalog.parts
      : (Array.isArray(selectedProduct?.parts) ? selectedProduct.parts : []);
    if (cachedCurrentProductPartsSource === source && cachedCurrentProductParts) return cachedCurrentProductParts;
    cachedCurrentProductPartsSource = source;
    cachedCurrentProductParts = normalize3DParts(source).map((part: any) => {
      const label = resolveProduct3DPartDisplayLabel(runtimeCatalog?.variations, part);
      if (!label || label === String(part?.label ?? '').trim()) return part;
      return {
        ...part,
        label,
        metadata: {
          ...(part?.metadata && typeof part.metadata === 'object' ? part.metadata : {}),
          configuredPartLabel: label,
        },
      };
    });
    cachedProductPartLookup = null;
    return cachedCurrentProductParts;
  };

  const normalizePartAlias = (value: any): string => String(value ?? '').trim();

  const normalizeLoosePartAlias = (value: any): string => String(value ?? '')
    .trim()
    .toLowerCase()
    .replace(/^asuka_(part|surface)_/i, '')
    .replace(/_print$/i, '')
    .replace(/^part[_-]/i, '')
    .replace(/^surface[_-]/i, '')
    .replace(/[^a-z0-9]+/g, '_')
    .replace(/^_+|_+$/g, '');

  const partAliasMatchesLoosely = (left: any, right: any): boolean => {
    const rawLeft = String(left ?? '').trim().toLowerCase();
    const rawRight = String(right ?? '').trim().toLowerCase();
    const normalizedLeft = normalizeLoosePartAlias(left);
    const normalizedRight = normalizeLoosePartAlias(right);
    if (!rawLeft || !rawRight) return false;
    return rawLeft === rawRight
      || rawLeft.includes(rawRight)
      || rawRight.includes(rawLeft)
      || (!!normalizedLeft && !!normalizedRight && (
        normalizedLeft === normalizedRight
        || normalizedLeft.includes(normalizedRight)
        || normalizedRight.includes(normalizedLeft)
      ));
  };

  const getFocusPartAliases = (part: any): string[] => Array.from(new Set([
    part?.id,
    part?.label,
    part?.name,
    part?.materialTarget,
    part?.metadata?.sourceMeshId,
    part?.metadata?.sourceMeshName,
    part?.metadata?.partId,
    ...split3DList(part?.meshIds ?? part?.meshNames ?? part?.meshName ?? part?.targetMeshIds ?? part?.targetMeshId),
    ...split3DList(part?.materialIds ?? part?.materialNames ?? part?.materialName),
  ].map(normalizePartAlias).filter(Boolean)));

  const getMeshPartAliases = (mesh?: Mesh | null): string[] => {
    if (!mesh) return [];
    const material: any = (mesh as any).material ?? null;
    const metadata: any = (mesh as any).metadata ?? {};
    return Array.from(new Set([
      mesh.id,
      mesh.name,
      material?.id,
      material?.name,
      metadata?.nativeId,
      metadata?.nativeName,
      metadata?.sourceMeshId,
      metadata?.sourceMeshName,
      metadata?.asukaOriginalMeshId,
      metadata?.asukaOriginalMeshName,
      metadata?.asukaOriginalPartId,
      metadata?.partId,
      metadata?.asukaProduct3DPartId,
      metadata?.asuka?.partId,
    ].map(normalizePartAlias).filter(Boolean)));
  };

  const getPartComponentRank = (part: any): number | null => {
    let rank = Number(part?.metadata?.componentRank ?? part?.metadata?.componentIndexRank);
    if (Number.isFinite(rank)) return rank;
    const meshName = split3DList(part?.meshNames ?? part?.meshName ?? part?.targetMeshIds ?? part?.targetMeshId).find((name) => name.includes(GEOMETRY_COMPONENT_PART_PREFIX));
    const match = String(meshName ?? '').match(/__component_(\d+)$/i);
    if (match) {
      rank = Number(match[1]) - 1;
      if (Number.isFinite(rank)) return rank;
    }
    return null;
  };

  const findGeometryComponentRankForPick = (mesh: Mesh | null | undefined, faceIdInput: any): number | null => {
    if (!mesh) return null;
    const faceId = Number(faceIdInput);
    if (!Number.isFinite(faceId) || faceId < 0) return null;
    const triangleStart = Math.floor(faceId) * 3;
    try {
      const rank = getGeometryComponentTriangleRankMap(mesh).get(triangleStart);
      return Number.isFinite(Number(rank)) ? Number(rank) : null;
    } catch {}
    return null;
  };

  const partMatchesMeshOrMaterial = (part: any, aliases: string[], materialAliases: string[] = []): boolean => {
    const wanted = [...aliases, ...materialAliases].map(normalizePartAlias).filter(Boolean);
    if (!wanted.length) return false;
    const candidates = getFocusPartAliases(part);
    return wanted.some((target) => candidates.some((candidate) => partAliasMatchesLoosely(candidate, target)));
  };

  const getPartLookup = (productIdInput?: string | null) => {
    const parts = getCurrentProductParts(productIdInput);
    if (cachedProductPartLookup?.parts === parts) return cachedProductPartLookup;
    const componentRank = new Map<number, any>();
    const alias = new Map<string, any>();
    const looseAlias = new Map<string, any[]>();
    for (const part of parts) {
      const rank = getPartComponentRank(part);
      if (rank != null && !componentRank.has(rank)) componentRank.set(rank, part);
      for (const partAlias of getFocusPartAliases(part)) {
        if (!alias.has(partAlias)) alias.set(partAlias, part);
        const loose = normalizeLoosePartAlias(partAlias);
        if (loose) {
          const list = looseAlias.get(loose) ?? [];
          if (!list.includes(part)) list.push(part);
          looseAlias.set(loose, list);
        }
      }
    }
    cachedProductPartLookup = { parts, componentRank, alias, looseAlias };
    return cachedProductPartLookup;
  };

  const findProductPartFromLookup = (aliases: string[], materialAliases: string[] = [], productIdInput?: string | null): any | null => {
    const lookup = getPartLookup(productIdInput);
    const wanted = [...aliases, ...materialAliases].map(normalizePartAlias).filter(Boolean);
    for (const target of wanted) {
      const exact = lookup.alias.get(target);
      if (exact) return exact;
    }
    for (const target of wanted) {
      const loose = normalizeLoosePartAlias(target);
      if (!loose) continue;
      const exactLoose = lookup.looseAlias.get(loose);
      if (exactLoose?.length) return exactLoose[0];
    }
    return null;
  };


  const getPickGeometricWorldFrame = (pickInfo?: any, meshInput?: Mesh | null, pointInput?: Vector3 | null): { normal: Vector3; right: Vector3; up: Vector3 } | null => {
    const mesh = (meshInput ?? pickInfo?.pickedMesh ?? null) as Mesh | null;
    const faceId = Number(pickInfo?.faceId);
    if (!mesh || !Number.isFinite(faceId) || faceId < 0) return null;
    try {
      const indices = mesh.getIndices?.() as number[] | null;
      const positions = mesh.getVerticesData?.('position' as any) as number[] | null;
      if (!indices || !positions || !indices.length || !positions.length) return null;
      const base = Math.floor(faceId) * 3;
      if (base + 2 >= indices.length) return null;
      const ia = Number(indices[base]);
      const ib = Number(indices[base + 1]);
      const ic = Number(indices[base + 2]);
      if (!Number.isFinite(ia) || !Number.isFinite(ib) || !Number.isFinite(ic)) return null;
      const read = (index: number) => {
        const offset = index * 3;
        if (offset + 2 >= positions.length) return null;
        return new Vector3(
          Number(positions[offset] ?? 0) || 0,
          Number(positions[offset + 1] ?? 0) || 0,
          Number(positions[offset + 2] ?? 0) || 0,
        );
      };
      const aLocal = read(ia);
      const bLocal = read(ib);
      const cLocal = read(ic);
      if (!aLocal || !bLocal || !cLocal) return null;
      const world = mesh.getWorldMatrix?.() ?? Matrix.Identity();
      const a = Vector3.TransformCoordinates(aLocal, world);
      const b = Vector3.TransformCoordinates(bLocal, world);
      const c = Vector3.TransformCoordinates(cLocal, world);
      let normal = Vector3.Cross(b.subtract(a), c.subtract(a));
      if (normal.lengthSquared() < 0.0000001) {
        normal = Vector3.Cross(c.subtract(a), b.subtract(a));
      }
      if (normal.lengthSquared() < 0.0000001) return null;
      normal.normalize();
      const point = pointInput ?? ((pickInfo?.pickedPoint ?? null) as Vector3 | null);
      if (point && camera) {
        const toCamera = camera.position.subtract(point);
        if (toCamera.lengthSquared() > 0.0001 && Vector3.Dot(normal, toCamera) < 0) normal = normal.scale(-1);
      }
      const projectTangentCandidate = (axis: Vector3): Vector3 | null => {
        const tangent = axis.subtract(normal.scale(Vector3.Dot(axis, normal)));
        if (tangent.lengthSquared() < 0.0000001) return null;
        tangent.normalize();
        return tangent;
      };
      const meshAxisCandidates = [
        Vector3.TransformNormal(new Vector3(1, 0, 0), world),
        Vector3.TransformNormal(new Vector3(0, 0, 1), world),
        Vector3.TransformNormal(new Vector3(0, 1, 0), world),
      ]
        .map(projectTangentCandidate)
        .filter((axis): axis is Vector3 => !!axis);
      const edgeCandidates = [b.subtract(a), c.subtract(b), a.subtract(c)]
        .map(projectTangentCandidate)
        .filter((edge): edge is Vector3 => !!edge);
      const worldUp = new Vector3(0, 1, 0);
      const chooseMostHorizontal = (candidates: Vector3[]): Vector3 | null => {
        return candidates
          .map((axis, index) => ({ axis, index, vertical: Math.abs(Vector3.Dot(axis, worldUp)) }))
          .sort((left, right) => (left.vertical - right.vertical) || (left.index - right.index))[0]?.axis ?? null;
      };
      let right = chooseMostHorizontal(meshAxisCandidates) ?? chooseMostHorizontal(edgeCandidates);
      if (!right || right.lengthSquared() < 0.0000001) return null;
      right.normalize();
      let up = Vector3.Cross(normal, right);
      if (up.lengthSquared() < 0.0000001) return null;
      up.normalize();
      if (camera) {
        const viewBasis = getViewTransformBasis();
        const viewUp = viewBasis.up.subtract(normal.scale(Vector3.Dot(viewBasis.up, normal)));
        if (viewUp.lengthSquared() > 0.0001) {
          viewUp.normalize();
          if (Vector3.Dot(up, viewUp) < 0) {
            right = right.scale(-1);
            up = up.scale(-1);
          }
        }
      }
      return { normal, right, up };
    } catch {
      return null;
    }
  };

  const getPickGeometricWorldNormal = (pickInfo?: any, meshInput?: Mesh | null): Vector3 | null => {
    return getPickGeometricWorldFrame(pickInfo, meshInput)?.normal ?? null;
  };

  const getBestPickWorldNormal = (pickInfo?: any, meshInput?: Mesh | null, pointInput?: Vector3 | null): { normal: Vector3 | null; source: string } => {
    const geometric = getPickGeometricWorldNormal(pickInfo, meshInput);
    let normal = geometric && geometric.lengthSquared() > 0.0001 ? geometric.clone() : null;
    let source = normal ? 'face-geometry' : '';
    if (!normal) {
      try {
        const raw = (typeof pickInfo?.getNormal === 'function'
          ? (pickInfo as any).getNormal(true, true) ?? (pickInfo as any).getNormal(true) ?? (pickInfo as any).getNormal()
          : null) as Vector3 | null;
        if (raw && raw.lengthSquared?.() > 0.0001) {
          normal = raw.clone().normalize();
          source = 'pick-normal';
        }
      } catch {}
    }
    const point = pointInput ?? ((pickInfo?.pickedPoint ?? null) as Vector3 | null);
    if (normal && point && camera) {
      try {
        const toCamera = camera.position.subtract(point);
        if (toCamera.lengthSquared() > 0.0001 && Vector3.Dot(normal, toCamera) < 0) normal = normal.scale(-1);
      } catch {}
    }
    if (normal && normal.lengthSquared() > 0.0001) {
      normal.normalize();
      return { normal, source: source || 'unknown' };
    }
    return { normal: null, source: 'none' };
  };

  const findProductPartForPick = (pickedMesh?: Mesh | null, pickInfo?: any): any | null => {
    const mesh = pickedMesh ?? null;
    const productId = getProductIdFromMesh(mesh);
    const lookup = getPartLookup(productId);
    if (!lookup.parts.length) return null;
    const aliases = getMeshPartAliases(mesh);
    const material: any = (mesh as any)?.material ?? null;
    const materialAliases = [material?.id, material?.name].map(normalizePartAlias).filter(Boolean);

    // Fast path: most production GLBs expose one mesh/material per logical part
    // (asuka_part_upper, asuka_part_swoosh, ...). Resolve those by alias first.
    // Computing connected geometry components needs walking indices/vertices and
    // is only useful for synthetic __component_N parts.
    const cachedMatch = findProductPartFromLookup(aliases, materialAliases, productId);
    if (cachedMatch && !lookup.componentRank.size) return cachedMatch;

    if (lookup.componentRank.size) {
      const componentRank = findGeometryComponentRankForPick(mesh, pickInfo?.faceId);
      if (componentRank != null) {
        const byComponent = lookup.componentRank.get(componentRank);
        if (byComponent) return byComponent;
      }
    }

    if (cachedMatch) return cachedMatch;

    // Fallback kept for rare loose aliases that intentionally match by substring.
    return lookup.parts.find((part) => partMatchesMeshOrMaterial(part, aliases, materialAliases)) ?? null;
  };

  const getProductPickFromPickInfo = (pickInfo?: any): typeof lastProductPick => {
    const pickedMesh = (pickInfo?.pickedMesh ?? null) as Mesh | null;
    const productId = getProductIdFromMesh(pickedMesh);
    const part = findProductPartForPick(pickedMesh, pickInfo);
    const material: any = (pickedMesh as any)?.material ?? null;
    const meshId = normalizePartAlias(pickedMesh?.id ?? pickedMesh?.name);
    const materialId = normalizePartAlias(material?.id ?? material?.name);
    const point = pickInfo?.pickedPoint ? fromVec3(pickInfo.pickedPoint as Vector3) : undefined;
    let normal: [number, number, number] | undefined;
    const bestPickNormal = getBestPickWorldNormal(pickInfo, pickedMesh, (pickInfo?.pickedPoint ?? null) as Vector3 | null);
    if (bestPickNormal.normal) normal = fromVec3(bestPickNormal.normal);
    if (!part && !meshId && !materialId) return null;
    return {
      asukaProductId: productId || undefined,
      productId: productId || undefined,
      productPartId: normalizePartAlias(part?.id),
      productPartLabel: normalizePartAlias(part?.label ?? part?.name ?? part?.id),
      productPartMeshId: meshId || split3DList(part?.meshNames)[0] || undefined,
      productPartMaterialId: materialId || split3DList(part?.materialNames)[0] || undefined,
      productPartPoint: point,
      productPartNormal: normal,
      productPartFaceId: Number.isFinite(Number(pickInfo?.faceId)) ? Number(pickInfo.faceId) : undefined,
    };
  };

  const findMeshesForProductPart = (part?: any | null, fallbackMeshId?: string, fallbackMaterialId?: string, productIdInput?: string | null): Mesh[] => {
    const out: Mesh[] = [];
    const productId = String(productIdInput ?? '').replace(/^product:/i, '').trim();
    const belongsToProduct = (mesh?: Mesh | null): mesh is Mesh => !!mesh && (!productId || meshBelongsToProductInstance(mesh, productId));
    const add = (mesh?: Mesh | null) => {
      if (!belongsToProduct(mesh) || mesh.isDisposed?.()) return;
      if (out.some((candidate) => candidate === mesh || candidate.id === mesh.id || candidate.name === mesh.name)) return;
      out.push(mesh);
    };
    const findScopedMesh = (aliasInput: string): Mesh | null => {
      const alias = normalizePartAlias(aliasInput);
      if (!alias) return null;
      const direct = findModelMesh(alias);
      if (belongsToProduct(direct)) return direct;
      return getRenderableModelMeshes(modelMeshes).find((mesh) => {
        if (!belongsToProduct(mesh)) return false;
        return getMeshPartAliases(mesh).some((candidate) => partAliasMatchesLoosely(candidate, alias));
      }) ?? null;
    };

    const meshIds = [
      fallbackMeshId,
      ...split3DList(part?.meshIds ?? part?.meshNames ?? part?.meshName ?? part?.targetMeshIds ?? part?.targetMeshId),
    ].map(normalizePartAlias).filter(Boolean);
    for (const meshId of meshIds) add(findScopedMesh(meshId));

    const materialNames = new Set([
      ...split3DList(part?.materialIds ?? part?.materialNames ?? part?.materialName),
      fallbackMaterialId,
    ].map(normalizePartAlias).filter(Boolean));
    if (materialNames.size) {
      for (const mesh of getRenderableModelMeshes(modelMeshes)) {
        if (!belongsToProduct(mesh)) continue;
        const material: any = mesh?.material ?? null;
        if (materialNames.has(normalizePartAlias(material?.id)) || materialNames.has(normalizePartAlias(material?.name))) add(mesh);
      }
    }

    const sourceMeshIds = split3DList(part?.metadata?.sourceMeshIds ?? part?.metadata?.sourceMeshId ?? part?.metadata?.sourceMeshName)
      .map(normalizePartAlias)
      .filter(Boolean);
    for (const sourceMeshId of sourceMeshIds) add(findScopedMesh(sourceMeshId));

    if (!out.length) add(getRenderableModelMeshes(modelMeshes).find(belongsToProduct) ?? null);
    return out;
  };

  const findMeshForProductPart = (part?: any | null, fallbackMeshId?: string, fallbackMaterialId?: string, productIdInput?: string | null): Mesh | null => {
    return findMeshesForProductPart(part, fallbackMeshId, fallbackMaterialId, productIdInput)[0] ?? null;
  };

  const getComponentPartWorldBounds = (mesh: Mesh, part: any) => {
    const minRaw = part?.metadata?.componentBboxMin;
    const maxRaw = part?.metadata?.componentBboxMax;
    if (!Array.isArray(minRaw) || !Array.isArray(maxRaw) || minRaw.length < 3 || maxRaw.length < 3) return null;
    try {
      mesh.computeWorldMatrix(true);
      const world = mesh.getWorldMatrix();
      const min = toVec3(minRaw, [0, 0, 0]);
      const max = toVec3(maxRaw, [0, 0, 0]);
      const corners = [
        new Vector3(min.x, min.y, min.z), new Vector3(max.x, min.y, min.z),
        new Vector3(min.x, max.y, min.z), new Vector3(max.x, max.y, min.z),
        new Vector3(min.x, min.y, max.z), new Vector3(max.x, min.y, max.z),
        new Vector3(min.x, max.y, max.z), new Vector3(max.x, max.y, max.z),
      ].map((corner) => Vector3.TransformCoordinates(corner, world));
      let outMin = new Vector3(Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY);
      let outMax = new Vector3(Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY);
      for (const corner of corners) {
        outMin = Vector3.Minimize(outMin, corner);
        outMax = Vector3.Maximize(outMax, corner);
      }
      if (!Number.isFinite(outMin.x) || !Number.isFinite(outMax.x)) return null;
      return { min: outMin, max: outMax, center: outMin.add(outMax).scale(0.5), size: outMax.subtract(outMin) };
    } catch {
      return null;
    }
  };

  const isSameRuntimeMesh = (left?: Mesh | null, right?: Mesh | null): boolean => {
    if (!left || !right) return false;
    if (left === right) return true;
    const leftId = String(left.id ?? '').trim();
    const rightId = String(right.id ?? '').trim();
    if (leftId && rightId && leftId === rightId) return true;
    const leftName = String(left.name ?? '').trim();
    const rightName = String(right.name ?? '').trim();
    return !!leftName && !!rightName && leftName === rightName;
  };

  const getFocusTangentAxes = (normal: Vector3): { right: Vector3; up: Vector3 } => {
    const forward = normal.clone();
    if (forward.lengthSquared() < 0.000001) forward.copyFrom(new Vector3(0, 0, 1));
    forward.normalize();
    let right = Vector3.Cross(Vector3.Up(), forward);
    if (right.lengthSquared() < 0.000001) right = Vector3.Cross(new Vector3(1, 0, 0), forward);
    if (right.lengthSquared() < 0.000001) right = new Vector3(1, 0, 0);
    right.normalize();
    let up = Vector3.Cross(forward, right);
    if (up.lengthSquared() < 0.000001) up = Vector3.Up();
    up.normalize();
    return { right, up };
  };

  const getFocusBoundsCornerPoints = (bounds: RuntimeWorldBounds): Vector3[] => {
    const min = bounds.min;
    const max = bounds.max;
    return [
      new Vector3(min.x, min.y, min.z), new Vector3(max.x, min.y, min.z),
      new Vector3(min.x, max.y, min.z), new Vector3(max.x, max.y, min.z),
      new Vector3(min.x, min.y, max.z), new Vector3(max.x, min.y, max.z),
      new Vector3(min.x, max.y, max.z), new Vector3(max.x, max.y, max.z),
    ];
  };

  const getFocusBoundsSamplePoints = (bounds: RuntimeWorldBounds, cameraPosition?: Vector3 | null): Vector3[] => {
    const center = bounds.center.clone();
    const diagonal = Math.max(0.001, bounds.size.length());
    const viewDirection = cameraPosition ? center.subtract(cameraPosition) : null;
    if (viewDirection && viewDirection.lengthSquared() > 0.000001) viewDirection.normalize();
    const axes = getFocusTangentAxes(viewDirection && viewDirection.lengthSquared() > 0.000001 ? viewDirection : new Vector3(0, 0, 1));
    const radius = Math.max(0.015, Math.min(diagonal * 0.32, Math.max(bounds.size.x, bounds.size.y, bounds.size.z) * 0.48));
    const candidates = [
      center,
      ...getFocusBoundsCornerPoints(bounds),
      center.add(axes.right.scale(radius)),
      center.add(axes.right.scale(-radius)),
      center.add(axes.up.scale(radius)),
      center.add(axes.up.scale(-radius)),
      center.add(axes.right.scale(radius * 0.72)).add(axes.up.scale(radius * 0.72)),
      center.add(axes.right.scale(-radius * 0.72)).add(axes.up.scale(radius * 0.72)),
      center.add(axes.right.scale(radius * 0.72)).add(axes.up.scale(-radius * 0.72)),
      center.add(axes.right.scale(-radius * 0.72)).add(axes.up.scale(-radius * 0.72)),
      center.add(viewDirection && viewDirection.lengthSquared() > 0.000001 ? viewDirection.scale(-Math.min(radius * 0.35, diagonal * 0.08)) : Vector3.Zero()),
    ];
    return candidates.slice(0, Math.max(1, Math.min(candidates.length, focusOcclusionSamples)));
  };

  const measureFocusProjectedAreaScore = (cameraPosition: Vector3, focusPoint: Vector3, bounds: RuntimeWorldBounds, targetSamplePoints: Vector3[] = []): number => {
    const viewDirection = focusPoint.subtract(cameraPosition);
    if (viewDirection.lengthSquared() < 0.000001) return 0;
    viewDirection.normalize();
    const axes = getFocusTangentAxes(viewDirection);
    const points = targetSamplePoints.length >= 3 ? targetSamplePoints : getFocusBoundsCornerPoints(bounds);
    let minX = Number.POSITIVE_INFINITY;
    let maxX = Number.NEGATIVE_INFINITY;
    let minY = Number.POSITIVE_INFINITY;
    let maxY = Number.NEGATIVE_INFINITY;
    for (const point of points) {
      const relative = point.subtract(focusPoint);
      const x = Vector3.Dot(relative, axes.right);
      const y = Vector3.Dot(relative, axes.up);
      if (!Number.isFinite(x) || !Number.isFinite(y)) continue;
      minX = Math.min(minX, x);
      maxX = Math.max(maxX, x);
      minY = Math.min(minY, y);
      maxY = Math.max(maxY, y);
    }
    if (!Number.isFinite(minX) || !Number.isFinite(maxX) || !Number.isFinite(minY) || !Number.isFinite(maxY)) return 0;
    const projectedArea = Math.max(0, maxX - minX) * Math.max(0, maxY - minY);
    const sx = Math.max(0.001, Math.abs(bounds.size.x));
    const sy = Math.max(0.001, Math.abs(bounds.size.y));
    const sz = Math.max(0.001, Math.abs(bounds.size.z));
    const maxFaceArea = Math.max(sx * sy, sx * sz, sy * sz, 0.0001);
    return clampNumber(projectedArea / maxFaceArea, 0, 1.35);
  };

  const isPointInsideExpandedBounds = (point: Vector3 | null | undefined, bounds: RuntimeWorldBounds, margin: number): boolean => {
    if (!point) return false;
    return point.x >= bounds.min.x - margin && point.x <= bounds.max.x + margin
      && point.y >= bounds.min.y - margin && point.y <= bounds.max.y + margin
      && point.z >= bounds.min.z - margin && point.z <= bounds.max.z + margin;
  };

  const isFocusOcclusionCandidateMeshFromSet = (mesh: any, focusMeshes: Set<Mesh>, targetMesh?: Mesh | null, includeTargetMesh = false): boolean => {
    const candidate = mesh as Mesh | null;
    if (!candidate || candidate.isDisposed?.()) return false;
    if (!includeTargetMesh && targetMesh && isSameRuntimeMesh(candidate, targetMesh)) return false;
    try { if (Number((candidate as any).visibility ?? 1) <= 0.001) return false; } catch {}
    const metadata: any = (candidate as any).metadata ?? {};
    if (metadata?.nativePrintSurface || (candidate as any).__asukaNativePrintSurface) return false;
    if ((candidate as any).__asukaColorWriteDisabled) return false;
    if (isRuntimeProjectedStickerMaterial((candidate as any).material)) return false;
    return focusMeshes.has(candidate);
  };

  const estimateFocusViewVisibility = (cameraPosition: Vector3, focusPoint: Vector3, bounds: RuntimeWorldBounds, targetSamplePoints: Vector3[] = []): RuntimeFocusViewVisibility => {
    const projectedAreaScore = measureFocusProjectedAreaScore(cameraPosition, focusPoint, bounds, targetSamplePoints);
    const visibleAreaScore = clampNumber(Math.min(1, projectedAreaScore), 0, 1);
    return {
      occlusionScore: 0.35,
      targetCoverage: visibleAreaScore,
      projectedAreaScore,
      visibleAreaScore,
    };
  };

  const measureFocusViewVisibility = (cameraPosition: Vector3, focusPoint: Vector3, bounds: RuntimeWorldBounds, targetMesh?: Mesh | null, targetSamplePoints: Vector3[] = []): RuntimeFocusViewVisibility => {
    const projectedAreaScore = measureFocusProjectedAreaScore(cameraPosition, focusPoint, bounds, targetSamplePoints);
    if (!scene) {
      return {
        occlusionScore: 0,
        targetCoverage: 1,
        projectedAreaScore,
        visibleAreaScore: projectedAreaScore,
      };
    }
    let score = 0;
    let visibleTargetSamples = 0;
    const maxRaySamples = Math.max(1, focusOcclusionSamples);
    const geometrySampleLimit = targetSamplePoints.length
      ? Math.max(1, Math.min(targetSamplePoints.length, Math.ceil(maxRaySamples * 0.55)))
      : 0;
    const geometrySamples = geometrySampleLimit > 0 ? targetSamplePoints.slice(0, geometrySampleLimit) : [];
    const boundsSampleLimit = Math.max(1, maxRaySamples - geometrySamples.length);
    const boundsSamples = getFocusBoundsSamplePoints(bounds, cameraPosition).slice(0, boundsSampleLimit);
    const samples = (geometrySamples.length ? [...geometrySamples, ...boundsSamples] : boundsSamples).slice(0, maxRaySamples);
    const diagonal = Math.max(0.001, bounds.size.length());
    const boundsMargin = Math.max(0.012, Math.min(0.22, diagonal * 0.09));
    const focusMeshes = new Set<Mesh>(getRenderableModelMeshes(modelMeshes));
    for (const sample of samples) {
      const direction = sample.subtract(cameraPosition);
      const distance = direction.length();
      if (!Number.isFinite(distance) || distance <= 0.001) continue;
      direction.normalize();
      try {
        // Include the target mesh in the ray test. When a logical part contains
        // several disconnected islands in the same mesh, excluding the target
        // mesh hides the case where another island of that same part blocks the
        // chosen island. A hit on the target is accepted only when it lands inside
        // the focused component bounds.
        const ray = new Ray(cameraPosition, direction, Math.max(0.001, distance + boundsMargin));
        const pick = scene.pickWithRay(ray, (mesh: any) => isFocusOcclusionCandidateMeshFromSet(mesh, focusMeshes, targetMesh, true), false);
        if (pick?.hit && pick.pickedMesh) {
          const pickedMesh = pick.pickedMesh as Mesh;
          const sameTarget = !!targetMesh && isSameRuntimeMesh(pickedMesh, targetMesh);
          const pickedPoint = (pick.pickedPoint ?? null) as Vector3 | null;
          if (sameTarget && isPointInsideExpandedBounds(pickedPoint, bounds, boundsMargin)) {
            visibleTargetSamples += 1;
            continue;
          }
          const pickedDistance = Number(pick.distance ?? distance);
          const closeness = Number.isFinite(pickedDistance) ? clampNumber(1 - (pickedDistance / Math.max(distance, 0.001)), 0, 1) : 0.5;
          // Same-mesh blockers are especially confusing visually: they usually
          // mean the camera is aimed at the wrong island of a multi-component
          // part, so penalize them harder than unrelated meshes.
          score += (sameTarget ? 7.5 : 4.4) + (closeness * (sameTarget ? 5.2 : 2.9));
        } else {
          // No hit means no blocker, but also no confirmed visible target surface.
          // Keep a tiny penalty so confirmed target visibility wins ties.
          score += 0.22;
        }
      } catch {}
    }
    const targetCoverage = visibleTargetSamples / Math.max(1, samples.length);
    const missingTargetPenalty = Math.max(0, 0.72 - targetCoverage) * 2.7;
    const occlusionScore = (score / Math.max(1, samples.length)) + missingTargetPenalty;
    const visibleAreaScore = clampNumber(targetCoverage * Math.min(1, projectedAreaScore), 0, 1);
    return {
      occlusionScore,
      targetCoverage,
      projectedAreaScore,
      visibleAreaScore,
    };
  };

  const rotateFocusNormalAroundAxis = (normal: Vector3, axis: Vector3, radians: number): Vector3 => {
    try {
      const safeAxis = axis.clone();
      if (safeAxis.lengthSquared() < 0.000001) return normal.clone();
      safeAxis.normalize();
      const rotated = Vector3.TransformNormal(normal, Matrix.RotationAxis(safeAxis, radians));
      if (rotated.lengthSquared() > 0.000001) {
        rotated.normalize();
        return rotated;
      }
    } catch {}
    return normal.clone();
  };

  const rotateFocusNormalAroundUp = (normal: Vector3, radians: number): Vector3 => {
    return rotateFocusNormalAroundAxis(normal, Vector3.Up(), radians);
  };

  const addUniqueFocusNormal = (normals: Vector3[], input?: Vector3 | null): void => {
    const normal = input?.clone?.() ?? null;
    if (!normal || normal.lengthSquared() < 0.000001) return;
    normal.normalize();
    if (normals.some((candidate) => Vector3.Dot(candidate, normal) > 0.985)) return;
    normals.push(normal);
  };

  const buildFocusNormalCandidates = (baseNormal: Vector3, focusPoint: Vector3, modelBounds: RuntimeWorldBounds): Vector3[] => {
    const normals: Vector3[] = [];
    const outward = getBoundsOutwardFocusNormal({ min: focusPoint, max: focusPoint, center: focusPoint, size: new Vector3(0.01, 0.01, 0.01) }, modelBounds);
    addUniqueFocusNormal(normals, baseNormal);
    addUniqueFocusNormal(normals, outward);
    if (camera) addUniqueFocusNormal(normals, camera.position.subtract(focusPoint));

    const seeds = normals.slice(0, 3);
    for (const seed of seeds) {
      const axes = getFocusTangentAxes(seed);
      // Keep the useful escape angles, but avoid an exhaustive orbit search here.
      // A cheap pre-pass below will shortlist only the few best directions before
      // any raycasts run, so camera movement starts without the previous lag.
      for (const degrees of [-56, -36, -20, 20, 36, 56]) {
        addUniqueFocusNormal(normals, rotateFocusNormalAroundUp(seed, degToRad(degrees)));
        if (normals.length >= focusViewCandidateSamples) return normals.slice(0, focusViewCandidateSamples);
      }
      for (const degrees of [-24, -12, 12, 24]) {
        addUniqueFocusNormal(normals, rotateFocusNormalAroundAxis(seed, axes.right, degToRad(degrees)));
        if (normals.length >= focusViewCandidateSamples) return normals.slice(0, focusViewCandidateSamples);
      }
      for (const yaw of [-32, 32]) {
        const yawed = rotateFocusNormalAroundUp(seed, degToRad(yaw));
        const yawAxes = getFocusTangentAxes(yawed);
        for (const pitch of [-16, 16]) {
          addUniqueFocusNormal(normals, rotateFocusNormalAroundAxis(yawed, yawAxes.right, degToRad(pitch)));
          if (normals.length >= focusViewCandidateSamples) return normals.slice(0, focusViewCandidateSamples);
        }
      }
    }
    return normals.slice(0, focusViewCandidateSamples);
  };

  const resolveLeastOccludedFocusView = (input: { focusPoint: Vector3; bounds: RuntimeWorldBounds; normal: Vector3; distance: number; targetMesh?: Mesh | null; modelBounds: RuntimeWorldBounds; targetSamplePoints?: Vector3[] }): { normal: Vector3; position: Vector3; occlusionScore: number; targetCoverage: number; projectedAreaScore: number; visibleAreaScore: number; viewScore: number } => {
    const baseNormal = input.normal.clone();
    if (baseNormal.lengthSquared() < 0.000001) baseNormal.copyFrom(getBoundsOutwardFocusNormal(input.bounds, input.modelBounds));
    baseNormal.normalize();
    const outward = getBoundsOutwardFocusNormal(input.bounds, input.modelBounds);
    const currentDirection = camera ? camera.position.subtract(input.focusPoint) : null;
    if (currentDirection && currentDirection.lengthSquared() > 0.000001) currentDirection.normalize();
    const buildCandidateScore = (resolvedNormal: Vector3, position: Vector3, visibility: RuntimeFocusViewVisibility, useOcclusionScore: boolean) => {
      const occlusionScore = visibility.occlusionScore;
      const frontDot = clampNumber(Vector3.Dot(resolvedNormal, baseNormal), -1, 1);
      const outwardDot = clampNumber(Vector3.Dot(resolvedNormal, outward), -1, 1);
      const alignmentPenalty = Math.pow(1 - Math.max(0, frontDot), 2) + (Math.max(0, 0.45 - frontDot) * 2.8);
      const outwardPenalty = Math.max(0, 0.28 - outwardDot) * 1.5;
      const continuityPenalty = currentDirection ? (1 - Math.max(-1, Math.min(1, Vector3.Dot(resolvedNormal, currentDirection)))) * 0.035 : 0;
      const belowTargetPenalty = Math.max(0, input.focusPoint.y - position.y) * 2.4;
      const targetCoveragePenalty = (1 - clampNumber(visibility.targetCoverage, 0, 1)) * focusTargetCoverageScoreWeight;
      const visibleAreaPenalty = (1 - clampNumber(visibility.visibleAreaScore, 0, 1)) * focusVisibleAreaScoreWeight;
      const projectedAreaPenalty = (1 - clampNumber(Math.min(1, visibility.projectedAreaScore), 0, 1)) * (focusVisibleAreaScoreWeight * 0.38);
      return (useOcclusionScore ? (occlusionScore * focusOcclusionScoreWeight) : 0)
        + targetCoveragePenalty
        + visibleAreaPenalty
        + projectedAreaPenalty
        + (alignmentPenalty * 2.4)
        + outwardPenalty
        + continuityPenalty
        + belowTargetPenalty;
    };
    const rawCandidates = buildFocusNormalCandidates(baseNormal, input.focusPoint, input.modelBounds).map((candidateNormal) => {
      let position = input.focusPoint.add(candidateNormal.scale(input.distance));
      position = clampCameraPositionToBetaLimits(input.focusPoint, position);
      const resolvedNormal = position.subtract(input.focusPoint);
      if (resolvedNormal.lengthSquared() < 0.000001) resolvedNormal.copyFrom(candidateNormal);
      resolvedNormal.normalize();
      const estimatedVisibility = estimateFocusViewVisibility(position, input.focusPoint, input.bounds, input.targetSamplePoints ?? []);
      return { normal: resolvedNormal, position, preScore: buildCandidateScore(resolvedNormal, position, estimatedVisibility, false), estimatedVisibility };
    }).sort((a, b) => a.preScore - b.preScore);
    const shortlisted = rawCandidates.slice(0, Math.max(1, Math.min(rawCandidates.length, focusOcclusionTopCandidates)));
    const candidates = shortlisted.map((candidate) => {
      const visibility = measureFocusViewVisibility(candidate.position, input.focusPoint, input.bounds, input.targetMesh ?? null, input.targetSamplePoints ?? []);
      const occlusionScore = visibility.occlusionScore;
      const viewScore = buildCandidateScore(candidate.normal, candidate.position, visibility, true);
      return {
        normal: candidate.normal,
        position: candidate.position,
        occlusionScore,
        targetCoverage: visibility.targetCoverage,
        projectedAreaScore: visibility.projectedAreaScore,
        visibleAreaScore: visibility.visibleAreaScore,
        viewScore,
      };
    }).sort((a, b) => (a.viewScore - b.viewScore) || (a.occlusionScore - b.occlusionScore));
    return candidates[0] ?? {
      normal: baseNormal,
      position: clampCameraPositionToBetaLimits(input.focusPoint, input.focusPoint.add(baseNormal.scale(input.distance))),
      occlusionScore: 0,
      targetCoverage: 1,
      projectedAreaScore: 1,
      visibleAreaScore: 1,
      viewScore: 0,
    };
  };

  const animateCameraTo = (
    target: Vector3,
    position: Vector3,
    animate = true,
    moveOptions: { duration?: number; action?: string; preserveTarget?: boolean } = {},
  ) => {
    const cam = camera;
    if (!cam) return;
    if (cameraFocusRaf) {
      try { cancelAnimationFrame(cameraFocusRaf); } catch {}
      cameraFocusRaf = 0;
    }
    const focusResolvedGroupTarget = centerProductGroupDuringFocus && moveOptions.preserveTarget !== true
      ? (getProductGroupViewTarget({ compute: true }) ?? target)
      : null;
    const apply = (nextTarget: Vector3, nextPosition: Vector3) => {
      const resolvedTarget = focusResolvedGroupTarget ? focusResolvedGroupTarget.clone() : nextTarget;
      const targetOffset = resolvedTarget.subtract(nextTarget);
      const resolvedPosition = nextPosition.add(targetOffset);
      const clampedPosition = clampCameraPositionToBetaLimits(resolvedTarget, resolvedPosition);
      try { cam.target = resolvedTarget; } catch {}
      try { cam.setPosition(clampedPosition); } catch { try { cam.position = clampedPosition; } catch {} }
      applyCameraAngleLimitsToCamera(cam);
      applyProductCameraInputLock(cam);
      try { scene?.render(); } catch {}
    };
    if (!animate || typeof requestAnimationFrame !== 'function') {
      apply(target, position);
      emitCameraEvent({ action: moveOptions.action ?? 'focusPart', viewId: currentViewId, camera: readCameraState(), cameraLimits });
      return;
    }
    const startTarget = cam.target.clone();
    const startPosition = cam.position.clone();
    const duration = Math.max(0, Number(moveOptions.duration ?? 420) || 420);
    const start = performance.now();
    const step = (now: number) => {
      const t = Math.min(1, Math.max(0, (now - start) / duration));
      const eased = 1 - Math.pow(1 - t, 3);
      apply(Vector3.Lerp(startTarget, target, eased), Vector3.Lerp(startPosition, position, eased));
      if (t < 1) {
        cameraFocusRaf = requestAnimationFrame(step);
      } else {
        cameraFocusRaf = 0;
        emitCameraEvent({ action: moveOptions.action ?? 'focusPart', viewId: currentViewId, camera: readCameraState(), cameraLimits });
      }
    };
    cameraFocusRaf = requestAnimationFrame(step);
  };

  const focusVariationRoot = (animate = true): void => {
    if (!camera) return;
    const groupCenter = getProductGroupViewTarget({ compute: true }) ?? Vector3.Zero();
    const resolved = resolveProduct3DRootFocusCamera({
      currentTarget: fromVec3(camera.target),
      currentPosition: fromVec3(camera.position),
      groupCenter: fromVec3(groupCenter),
    });
    animateCameraTo(
      toVec3(resolved.target, [0, 0, 0]),
      toVec3(resolved.position, [0, 0, 1]),
      animate,
      { action: 'focusVariationRoot', preserveTarget: true },
    );
  };

  const focusPart = (input: Asuka3DPartFocusOptions & { label?: string } = {}) => {
    if (input.scope === 'root') {
      focusVariationRoot(input.animate !== false);
      return;
    }
    const requestedProductId = String(input.productId ?? getSelectedProductId() ?? '').replace(/^product:/i, '').trim();
    const parts = getAllProductParts(requestedProductId);
    const requestedPartId = normalizePartAlias(input.partId);
    const requestedPartLabel = normalizePartAlias((input as any).partLabel ?? (input as any).label);
    const requestedMeshId = normalizePartAlias(input.meshId);
    const requestedMaterialId = normalizePartAlias(input.materialId);
    const findPartByAlias = (wanted: string): any | null => {
      const target = normalizePartAlias(wanted);
      if (!target) return null;
      return parts.find((item) => getFocusPartAliases(item).some((alias) => partAliasMatchesLoosely(alias, target))) ?? null;
    };
    const partFromExplicitPart = findPartByAlias(requestedPartId) ?? findPartByAlias(requestedPartLabel);
    // Module clicks represent a deliberate part-group selection. In that case,
    // stale mesh/material ids from the previous canvas hover/pick must not drive
    // the camera to the old part. Use part id/label first, then mesh/material only
    // as a fallback when no explicit part was resolved.
    const part = partFromExplicitPart ?? findPartByAlias(requestedMeshId) ?? findPartByAlias(requestedMaterialId);
    const selectedPartAliases = part ? getFocusPartAliases(part) : [];
    const aliasBelongsToSelectedPart = (alias: string): boolean => !!alias && (!selectedPartAliases.length || selectedPartAliases.some((candidate) => partAliasMatchesLoosely(candidate, alias)));
    const explicitPartFallback = requestedPartId || requestedPartLabel;
    const meshFallback = partFromExplicitPart
      ? (aliasBelongsToSelectedPart(requestedMeshId) ? requestedMeshId : explicitPartFallback)
      : (requestedMeshId || explicitPartFallback);
    const materialFallback = partFromExplicitPart && !aliasBelongsToSelectedPart(requestedMaterialId)
      ? ''
      : requestedMaterialId;
    if (!camera) return;
    const requestedPoint = Array.isArray(input.point) && input.point.length >= 3 ? toVec3(input.point, [0, 0, 0]) : null;
    const candidateMeshes = findMeshesForProductPart(part, meshFallback, materialFallback, requestedProductId);
    const modelBounds = getModelBounds();
    const currentCameraDistance = Math.max(0.01, Vector3.Distance(camera.position, camera.target));
    const focusCandidates = candidateMeshes.map((candidateMesh) => {
      const componentBounds = part ? getComponentPartWorldBounds(candidateMesh, part) : null;
      const componentTarget = componentBounds
        ? { bounds: componentBounds, normal: getBoundsOutwardFocusNormal(componentBounds, modelBounds) }
        : getClosestVisibleGeometryComponentFocusTarget(candidateMesh, modelBounds);
      // A logical product part can be one material/mesh made of several separated
      // islands (for example left + right swoosh). Module focus must frame one
      // visible island and place the camera in front of that island, not between
      // every disconnected component of the group.
      const indexedBounds = getMeshIndexedWorldBounds(candidateMesh);
      const bounds = componentTarget?.bounds ?? indexedBounds ?? getMeshWorldBounds(candidateMesh);
      const component = componentTarget && 'component' in componentTarget ? (componentTarget as RuntimeComponentFocusTarget).component : undefined;
      const targetSamplePoints = component
        ? getGeometryComponentWorldSamplePoints(candidateMesh, component)
        : getMeshWorldSamplePointsInsideBounds(candidateMesh, bounds);
      const focusPoint = requestedPoint ? requestedPoint.clone() : bounds.center.clone();
      const distanceScore = requestedPoint
        ? Vector3.DistanceSquared(bounds.center, requestedPoint)
        : Vector3.DistanceSquared(camera!.position, bounds.center);
      const focusNormal = componentTarget?.normal ?? getBoundsOutwardFocusNormal(bounds, modelBounds);
      const distance = currentCameraDistance;
      const resolvedView = resolveLeastOccludedFocusView({ focusPoint, bounds, normal: focusNormal, distance, targetMesh: candidateMesh, modelBounds, targetSamplePoints });
      return {
        mesh: candidateMesh,
        bounds,
        focusPoint,
        distance,
        distanceScore,
        facingScore: componentTarget && 'facingScore' in componentTarget ? Number((componentTarget as RuntimeComponentFocusTarget).facingScore) || 0 : 0,
        sizeScore: bounds.size.lengthSquared(),
        focusNormal: resolvedView.normal,
        focusPosition: resolvedView.position,
        occlusionScore: resolvedView.occlusionScore,
        targetCoverage: resolvedView.targetCoverage,
        projectedAreaScore: resolvedView.projectedAreaScore,
        visibleAreaScore: resolvedView.visibleAreaScore,
        viewScore: resolvedView.viewScore,
        targetSamplePoints,
        usingSingleComponent: Boolean(componentTarget),
      };
    }).sort((a, b) => {
      const viewDelta = a.viewScore - b.viewScore;
      if (Math.abs(viewDelta) > 0.35) return viewDelta;
      if (a.usingSingleComponent || b.usingSingleComponent) {
        const facingDelta = b.facingScore - a.facingScore;
        if (Math.abs(facingDelta) > 0.08) return facingDelta;
      }
      const occlusionDelta = a.occlusionScore - b.occlusionScore;
      if (Math.abs(occlusionDelta) > 0.12) return occlusionDelta;
      return (a.distanceScore - b.distanceScore) || (b.sizeScore - a.sizeScore);
    });
    const focusTarget = focusCandidates[0] ?? null;
    if (!focusTarget) return;
    const bounds = focusTarget.bounds;
    const focusPoint = focusTarget.focusPoint;
    const hasExplicitNormal = Array.isArray(input.normal) && input.normal.length >= 3;
    let normal = hasExplicitNormal
      ? toVec3(input.normal, [0, 0, 1])
      : focusTarget.focusNormal.clone();
    if (normal.lengthSquared() < 0.0001) normal = camera.position.subtract(focusPoint);
    if (normal.lengthSquared() < 0.0001) normal = new Vector3(0, 0, 1);
    normal.normalize();
    const explicitView = hasExplicitNormal
      ? resolveLeastOccludedFocusView({
        focusPoint,
        bounds,
        normal,
        distance: focusTarget.distance ?? Math.max(0.9, bounds.size.length() * 2.8),
        targetMesh: focusTarget.mesh,
        modelBounds,
        targetSamplePoints: focusTarget.targetSamplePoints ?? [],
      })
      : null;
    const focusedProductId = requestedProductId || getProductIdFromMesh(focusTarget.mesh);
    const knownProductIds = getKnownProductIds();
    const groupCenter = getProductGroupViewTarget({ compute: true }) ?? camera.target;
    const productCenter = focusedProductId
      ? (getProductGroupBounds([focusedProductId])?.center ?? null)
      : null;
    const cameraMove = resolveProduct3DPartFocusCamera({
      currentTarget: fromVec3(camera.target),
      currentPosition: fromVec3(camera.position),
      focusDirection: fromVec3(explicitView?.normal ?? normal),
      productCount: knownProductIds.length,
      groupCenter: fromVec3(groupCenter),
      productCenter: productCenter ? fromVec3(productCenter) : null,
      multiProductTargetBlend: focusMultiProductTargetBlend,
    });
    animateCameraTo(
      toVec3(cameraMove.target, fromVec3(camera.target)),
      toVec3(cameraMove.position, fromVec3(camera.position)),
      input.animate !== false,
      { action: 'focusPart', preserveTarget: true },
    );
  };


  const isArcRotateCameraInertiaActive = (): boolean => {
    const cam: any = camera as any;
    if (!cam) return false;
    const values = [
      cam.inertialAlphaOffset,
      cam.inertialBetaOffset,
      cam.inertialRadiusOffset,
      cam.inertialPanningX,
      cam.inertialPanningY,
    ];
    return values.some((value) => Math.abs(Number(value) || 0) > 0.00001);
  };

  const restoreProductHoverPointerMovePickingIfIdle = (): void => {
    productHoverRestoreTimer = null;
    const now = Date.now();
    if (now < productHoverSuspendedUntil || isArcRotateCameraInertiaActive()) {
      productHoverSuspendedUntil = Math.max(productHoverSuspendedUntil, now + 90);
      if (typeof window !== 'undefined') {
        productHoverRestoreTimer = window.setTimeout(restoreProductHoverPointerMovePickingIfIdle, 90);
      }
      return;
    }
    if (scene && productHoverPointerPickingSuspended) {
      try { (scene as any).skipPointerMovePicking = productHoverPreviousSkipPointerMovePicking === true; } catch {}
    }
    productHoverPointerPickingSuspended = false;
    productHoverPreviousSkipPointerMovePicking = null;
  };

  const suspendProductHoverForCameraMotion = (reason = 'camera', durationMs = 140): void => {
    const now = Date.now();
    productHoverSuspendedUntil = Math.max(productHoverSuspendedUntil, now + Math.max(60, durationMs));
    if (scene && !productHoverPointerPickingSuspended) {
      try {
        productHoverPreviousSkipPointerMovePicking = (scene as any).skipPointerMovePicking === true;
        (scene as any).skipPointerMovePicking = true;
        productHoverPointerPickingSuspended = true;
      } catch {}
    }
    if (hoveredProductKey) clearProductHover(true);
    if (isBabylonProduct3DDebugEnabled() && now - lastProductHoverSuspendDebugAt > 500) {
      lastProductHoverSuspendDebugAt = now;
      debugBabylonProduct3D('hover:suspend-during-camera-motion', { reason, untilMs: Math.max(0, productHoverSuspendedUntil - now), skipPointerMovePicking: !!(scene as any)?.skipPointerMovePicking });
    }
    if (typeof window !== 'undefined') {
      if (productHoverRestoreTimer != null) {
        try { window.clearTimeout(productHoverRestoreTimer); } catch {}
      }
      productHoverRestoreTimer = window.setTimeout(restoreProductHoverPointerMovePickingIfIdle, Math.max(60, durationMs));
    }
  };

  const isProductHoverSuspended = (event?: PointerEvent | MouseEvent | null): boolean => {
    const buttons = Number((event as any)?.buttons ?? 0);
    return buttons > 0
      || Date.now() < productHoverSuspendedUntil
      || productCanvasPointerDown?.moved === true
      || isArcRotateCameraInertiaActive();
  };

  const emitProductHoverMove = (event?: PointerEvent | MouseEvent | null): void => {
    if (!hoveredProductKey) return;
    const clientX = Number((event as any)?.clientX);
    const clientY = Number((event as any)?.clientY);
    if (!Number.isFinite(clientX) || !Number.isFinite(clientY)) return;
    const emitStart = isAsuka3DPerfDebugEnabled() ? perfNow() : 0;
    try { emitAsukaEvent('engine3d.hover.move', { active: true, clientX, clientY, viewId: currentViewId, engine: 'babylon' }); } catch {}
    const now = perfNow();
    if (isAsuka3DPerfDebugEnabled() && now - hoverLastRollupAt >= 1000) {
      logAsuka3DPerf('hover', 'pointer-rollup', {
        pointerMoves: hoverPointerMoveCount,
        transitions: hoverTransitionCount,
        clears: hoverClearCount,
        currentKey: hoveredProductKey,
        currentMeshId: hoveredProductMesh?.id ?? null,
        emitMoveMs: emitStart ? now - emitStart : 0,
        sceneMeshes: scene?.meshes?.length ?? 0,
      }, { console: true });
      hoverPointerMoveCount = 0;
      hoverTransitionCount = 0;
      hoverClearCount = 0;
      hoverLastRollupAt = now;
    }
  };

  const getModelMeshLookup = () => {
    if (modelMeshLookupCache?.source === modelMeshes && modelMeshLookupCache.length === modelMeshes.length) return modelMeshLookupCache;
    const objects = new Set<Mesh>();
    const ids = new Set<string>();
    const names = new Set<string>();
    for (const mesh of modelMeshes) {
      if (!mesh || mesh.isDisposed?.()) continue;
      objects.add(mesh);
      const id = String(mesh.id ?? '').trim();
      const name = String(mesh.name ?? '').trim();
      if (id) ids.add(id);
      if (name) names.add(name);
    }
    modelMeshLookupCache = { source: modelMeshes, length: modelMeshes.length, objects, ids, names };
    return modelMeshLookupCache;
  };

  const resolvePickedProductMesh = (input?: Mesh | null): Mesh | null => {
    const lookup = getModelMeshLookup();
    let cursor: any = input ?? null;
    while (cursor) {
      const mesh = cursor as Mesh;
      if (mesh && !mesh.isDisposed?.()) {
        const metadata: any = (mesh as any).metadata ?? {};
        const meshId = String(mesh.id ?? '').trim();
        const meshName = String(mesh.name ?? '').trim();
        const isKnownModelMesh = lookup.objects.has(mesh) || (!!meshId && lookup.ids.has(meshId)) || (!!meshName && lookup.names.has(meshName));
        const hasProductInstance = !!String(metadata?.asukaProductId ?? metadata?.productId ?? '').trim();
        const looksLikeProductPart = /^(.+::)?asuka_part_/i.test(meshId) || /^(.+::)?asuka_part_/i.test(meshName);
        if (isKnownModelMesh || hasProductInstance || looksLikeProductPart) return mesh;
      }
      cursor = cursor?.parent ?? null;
    }
    return null;
  };

  const isProductHoverPickCandidate = (candidate?: Mesh | null): boolean => {
    const mesh = candidate as any;
    if (!mesh || typeof mesh.isDisposed === 'function' && mesh.isDisposed()) return false;
    const resolved = resolvePickedProductMesh(mesh as Mesh);
    return !!resolved && !resolved.isDisposed?.();
  };

  const pickProductHoverMesh = (pointerInfo?: any): { mesh: Mesh; rawMesh: Mesh | null; pick: any } | null => {
    const endSpan = beginAsuka3DPerfSpan('hover', 'pick-product-mesh', {
      hasInitialPick: Boolean(pointerInfo?.pickInfo),
      sceneMeshCount: scene?.meshes?.length ?? 0,
      modelMeshCount: modelMeshes.length,
    });
    const initialPick = pointerInfo?.pickInfo ?? null;
    const initialRawMesh = (initialPick?.pickedMesh ?? null) as Mesh | null;
    const initialMesh = resolvePickedProductMesh(initialRawMesh);
    const initialPart = initialRawMesh ? findProductPartForPick(initialRawMesh, initialPick) : null;
    const initialProductId = getProductIdFromMesh(initialRawMesh ?? initialMesh);
    const needsPrecisePartPick = !!initialMesh && getCurrentProductParts(initialProductId).length > 1 && !initialPart;
    if (initialMesh && !needsPrecisePartPick) {
      endSpan({ mode: 'pointer-info', hit: true, meshId: initialMesh.id, rawMeshId: initialRawMesh?.id ?? null });
      return { mesh: initialMesh, rawMesh: initialRawMesh, pick: initialPick };
    }

    if (!scene) { endSpan({ mode: 'no-scene', hit: false }); return null; }
    const rayStart = perfNow();
    const ray = getPointerPickingRay((pointerInfo?.event as PointerEvent | MouseEvent | undefined) ?? null);
    const rayMs = perfNow() - rayStart;
    if (!ray) { endSpan({ mode: 'no-ray', hit: false, rayMs }); return null; }
    let pick: any = null;
    let pickError: unknown = null;
    const preciseStart = perfNow();
    try {
      pick = scene.pickWithRay(ray, (candidate: any) => isProductHoverPickCandidate(candidate as Mesh), false);
    } catch (error) {
      pickError = error;
      pick = null;
    }
    const preciseMs = perfNow() - preciseStart;
    const rawMesh = (pick?.pickedMesh ?? null) as Mesh | null;
    const mesh = resolvePickedProductMesh(rawMesh);
    endSpan({
      mode: 'fallback-raycast',
      hit: Boolean(mesh),
      rayMs,
      preciseMs,
      meshId: mesh?.id ?? null,
      rawMeshId: rawMesh?.id ?? null,
      faceId: Number.isFinite(Number(pick?.faceId)) ? Number(pick.faceId) : null,
      error: pickError instanceof Error ? pickError.message : pickError ? String(pickError) : null,
    }, { level: pickError ? 'warn' : 'info' });
    return mesh ? { mesh, rawMesh, pick } : null;
  };

  const makeFastProductHoverTargetFromMesh = (mesh: Mesh | null | undefined, rawMesh?: Mesh | null): { target: AsukaSurfacePickTarget; key: string } | null => {
    if (!mesh || mesh.isDisposed?.()) return null;
    const hoverMesh = rawMesh && !rawMesh.isDisposed?.() ? rawMesh : mesh;
    const material: any = (hoverMesh as any)?.material ?? (mesh as any)?.material ?? null;
    const materialId = String(material?.id ?? '').trim();
    const materialName = String(material?.name ?? '').trim();
    const meshMetadata: any = (mesh as any)?.metadata ?? {};
    const rawMetadata: any = (hoverMesh as any)?.metadata ?? {};
    const instanceId = String(meshMetadata?.asukaProductId ?? meshMetadata?.productId ?? rawMetadata?.asukaProductId ?? rawMetadata?.productId ?? '').trim();
    const partsRef = getCurrentProductParts(instanceId);
    const cached = productHoverMeshTargetCache.get(hoverMesh);
    if (cached
      && cached.parts === partsRef
      && cached.materialId === materialId
      && cached.materialName === materialName
      && cached.catalogRevision === runtimeProductCatalogRevision) return cached;

    // Fast hover path: ActionManager already resolved the entered mesh. Do not
    // ask for face/component precision here; on detailed products like the Nike
    // GLB that can rebuild connected-component maps when simply moving the
    // mouse from one part to another. Component precision is still available for
    // explicit focus/pick flows, but hover only needs a stable part label.
    const part = findProductPartFromLookup(
      [...getMeshPartAliases(hoverMesh), ...getMeshPartAliases(mesh)],
      [materialId, materialName],
      instanceId,
    );
    const meshId = String(hoverMesh.id || hoverMesh.name || mesh.id || mesh.name || '').trim() || null;
    const productPartId = String(part?.id ?? '').trim();
    const productPartLabel = String(part?.label ?? part?.name ?? part?.id ?? '').trim();
    const label = getProductHoverLabel(hoverMesh, {
      productPartId: productPartId || undefined,
      productPartLabel: productPartLabel || undefined,
      meshId: meshId || undefined,
      metadata: {
        asukaProductId: instanceId || undefined,
        productId: instanceId || undefined,
      },
    });
    const key = [
      instanceId || 'instance:current',
      String((hoverMesh as any).uniqueId ?? hoverMesh.id ?? hoverMesh.name ?? 'mesh:unknown').trim(),
      materialId || materialName || 'material:none',
      productPartId || label || 'part:none',
    ].join('|');
    const target = makeSurfacePickTarget({
      kind: productPartId ? 'product' : 'mesh',
      meshId,
      label,
      point: null,
      normal: null,
      uv: null,
      priority: 40,
      metadata: {
        productPartId: productPartId || undefined,
        productPartLabel: productPartLabel || undefined,
        productPartMeshId: meshId || undefined,
        productPartMaterialId: materialId || materialName || undefined,
        renderingGroupId: Number((hoverMesh as any).renderingGroupId ?? 0),
        asukaProductId: instanceId || undefined,
        productId: instanceId || undefined,
      },
    });
    const next = { parts: partsRef, materialId, materialName, catalogRevision: runtimeProductCatalogRevision, target, key };
    productHoverMeshTargetCache.set(hoverMesh, next);
    return next;
  };

  const makeLightweightProductHoverTargetFromPick = (hoverPick: { mesh: Mesh; rawMesh: Mesh | null; pick: any } | null): AsukaSurfacePickTarget | null => {
    if (!hoverPick) return makeSurfacePickTarget({ kind: 'none', priority: 0 });
    const { mesh, rawMesh, pick } = hoverPick;
    const part = findProductPartForPick(mesh, pick) ?? findProductPartForPick(rawMesh, pick);
    const material: any = (mesh as any)?.material ?? null;
    const meshId = String(mesh.id || mesh.name || '').trim() || null;
    const productPartId = String(part?.id ?? '').trim();
    const productPartLabel = String(part?.label ?? part?.name ?? part?.id ?? '').trim();
    const materialId = String(material?.id ?? material?.name ?? '').trim();
    const meshMetadata: any = (mesh as any)?.metadata ?? {};
    const rawMetadata: any = (rawMesh as any)?.metadata ?? {};
    const instanceId = String(meshMetadata?.asukaProductId ?? meshMetadata?.productId ?? rawMetadata?.asukaProductId ?? rawMetadata?.productId ?? '').trim();
    return makeSurfacePickTarget({
      kind: productPartId ? 'product' : 'mesh',
      meshId,
      label: getProductHoverLabel(mesh, {
        productPartId: productPartId || undefined,
        productPartLabel: productPartLabel || undefined,
        meshId: meshId || undefined,
        metadata: {
          asukaProductId: instanceId || undefined,
          productId: instanceId || undefined,
        },
      }),
      point: pick?.pickedPoint ? fromVec3(pick.pickedPoint as Vector3) : null,
      normal: null,
      uv: getPickTextureUv(pick),
      priority: 40,
      metadata: {
        productPartId: productPartId || undefined,
        productPartLabel: productPartLabel || undefined,
        productPartMeshId: meshId || undefined,
        productPartMaterialId: materialId || undefined,
        renderingGroupId: Number((mesh as any).renderingGroupId ?? 0),
        asukaProductId: instanceId || undefined,
        productId: instanceId || undefined,
      },
    });
  };

  const makeProductHoverHitFromPick = (hoverPick: { mesh: Mesh; rawMesh: Mesh | null; pick: any } | null): { mesh: Mesh; rawMesh: Mesh | null; pick: any; key: string } | null => {
    if (!hoverPick) return null;
    const { mesh, rawMesh, pick } = hoverPick;
    if (!mesh || mesh.isDisposed?.()) return null;
    const meshMetadata: any = (mesh as any)?.metadata ?? {};
    const rawMetadata: any = (rawMesh as any)?.metadata ?? {};
    const instanceId = String(meshMetadata?.asukaProductId ?? meshMetadata?.productId ?? rawMetadata?.asukaProductId ?? rawMetadata?.productId ?? '').trim();
    const material: any = (mesh as any)?.material ?? null;
    const materialId = String(material?.id ?? material?.name ?? '').trim();
    const shouldUseGeometryComponentHoverKey = getPartLookup(instanceId).componentRank.size > 0;
    const componentRank = shouldUseGeometryComponentHoverKey ? findGeometryComponentRankForPick(mesh, pick?.faceId) : null;
    const meshKey = String((mesh as any).uniqueId ?? mesh.id ?? mesh.name ?? '').trim();
    const rawKey = String((rawMesh as any)?.uniqueId ?? rawMesh?.id ?? rawMesh?.name ?? '').trim();
    const logicalPartKey = componentRank != null ? `component:${componentRank}` : `mesh:${mesh.id || mesh.name || rawMesh?.id || rawMesh?.name || 'unknown'}`;
    const key = [instanceId || 'instance:current', meshKey || rawKey || 'mesh:unknown', materialId || 'material:none', logicalPartKey].join('|');
    return { mesh, rawMesh, pick, key };
  };

  const makeProductHoverHitFromPointerInfo = (pointerInfo?: any): { mesh: Mesh; rawMesh: Mesh | null; pick: any; key: string } | null => makeProductHoverHitFromPick(pickProductHoverMesh(pointerInfo));

  const flushProductHoverTargetUpdate = (): void => {
    if (productHoverRaf) {
      try { cancelAnimationFrame(productHoverRaf); } catch {}
      productHoverRaf = 0;
    }
    const hoverPick = pendingProductHoverPick;
    const event = pendingProductHoverEvent;
    pendingProductHoverPick = null;
    pendingProductHoverEvent = null;
    const started = isBabylonProduct3DDebugEnabled() ? perfNow() : 0;
    const target = makeLightweightProductHoverTargetFromPick(hoverPick);
    setProductHoverTarget(target, event ?? null);
    if (started) {
      const ms = perfNow() - started;
      if (ms > 12) debugBabylonProduct3D('hover:slow-enter', { ms, meshCount: modelMeshes.length, partCount: getCurrentProductParts().length, targetKind: target?.kind });
    }
  };

  const scheduleProductHoverTargetUpdate = (pointerInfo?: any, event?: PointerEvent | MouseEvent | null): void => {
    if (isProductHoverSuspended(event ?? null)) {
      clearProductHover(true);
      return;
    }
    emitProductHoverMove(event ?? null);
    const hoverHit = makeProductHoverHitFromPointerInfo(pointerInfo);
    if (!hoverHit) {
      if (lastProductHoverHitKey || hoveredProductKey) {
        lastProductHoverHitKey = '';
        clearProductHover(true);
      }
      return;
    }
    if (hoverHit.key === lastProductHoverHitKey) return;
    lastProductHoverHitKey = hoverHit.key;
    pendingProductHoverPick = hoverHit;
    pendingProductHoverEvent = event ?? null;
    if (productHoverRaf) return;
    productHoverRaf = requestAnimationFrame(flushProductHoverTargetUpdate);
  };


  const setProductHoverFromMeshEvent = (mesh: Mesh | null | undefined, actionEvent?: any): void => {
    const event = (actionEvent?.sourceEvent ?? actionEvent?.event ?? null) as PointerEvent | MouseEvent | null;
    const endSpan = beginAsuka3DPerfSpan('hover', 'action-enter', {
      sourceMeshId: mesh?.id ?? null,
      sourceMeshName: mesh?.name ?? null,
      meshUnderPointerId: actionEvent?.meshUnderPointer?.id ?? null,
      buttons: Number((event as any)?.buttons ?? 0),
    });
    if (isProductHoverSuspended(event)) {
      clearProductHover(true);
      endSpan({ suspended: true, reason: 'camera-or-buttons' });
      return;
    }
    const rawSource = (actionEvent?.meshUnderPointer ?? actionEvent?.source ?? mesh ?? null) as Mesh | null;
    const resolvedFromAction = resolvePickedProductMesh(rawSource) ?? resolvePickedProductMesh(mesh ?? null);
    const started = isBabylonProduct3DDebugEnabled() ? perfNow() : 0;

    if (resolvedFromAction) {
      const fast = makeFastProductHoverTargetFromMesh(resolvedFromAction, rawSource ?? resolvedFromAction);
      if (!fast) { endSpan({ resolved: true, target: false, mode: 'action-fast-mesh' }, { level: 'warn' }); return; }
      if (fast.key === lastProductHoverHitKey) {
        emitProductHoverMove(event);
        endSpan({ resolved: true, repeated: true, mode: 'action-fast-mesh', key: fast.key });
        return;
      }
      lastProductHoverHitKey = fast.key;
      setProductHoverTarget(fast.target, event, rawSource ?? resolvedFromAction);
      endSpan({ resolved: true, repeated: false, mode: 'action-fast-mesh', key: fast.key, meshId: resolvedFromAction.id, label: fast.target?.label });
      if (started) {
        const ms = perfNow() - started;
        if (ms > 8) debugBabylonProduct3D('hover:slow-enter-event', { ms, meshCount: modelMeshes.length, partCount: getCurrentProductParts().length, mode: 'action-fast-mesh' });
      }
      return;
    }

    // Rare fallback for meshes where ActionManager did not expose the source.
    // This path can raycast, so it should not run during normal product hover.
    const hoverPick = pickProductHoverMesh({ event, pickInfo: actionEvent?.pickInfo ?? null });
    const hoverHit = makeProductHoverHitFromPick(hoverPick);
    if (!hoverHit) { endSpan({ resolved: false, mode: 'fallback-raycast' }); return; }
    if (hoverHit.key === lastProductHoverHitKey) {
      emitProductHoverMove(event);
      endSpan({ resolved: true, repeated: true, mode: 'fallback-raycast', key: hoverHit.key });
      return;
    }
    lastProductHoverHitKey = hoverHit.key;
    setProductHoverTarget(makeLightweightProductHoverTargetFromPick(hoverHit), event, hoverHit.mesh);
    endSpan({ resolved: true, repeated: false, mode: 'fallback-raycast', key: hoverHit.key, meshId: hoverHit.mesh.id });
    if (started) {
      const ms = perfNow() - started;
      if (ms > 8) debugBabylonProduct3D('hover:slow-enter-event', { ms, meshCount: modelMeshes.length, partCount: getCurrentProductParts().length, mode: 'fallback-raycast' });
    }
  };

  const clearProductHoverFromMeshEvent = (mesh: Mesh | null | undefined): void => {
    if (isProductHoverSuspended(null)) return;
    const resolved = resolvePickedProductMesh(mesh ?? null);
    if (!resolved || !hoveredProductMesh) return;
    const sameMesh = hoveredProductMesh === resolved
      || String(hoveredProductMesh.id ?? '') === String(resolved.id ?? '')
      || String(hoveredProductMesh.name ?? '') === String(resolved.name ?? '');
    if (sameMesh) clearProductHover(true);
  };

  const ensureProductHoverActionsForMesh = (mesh: Mesh | null | undefined): void => {
    if (!scene || !mesh || mesh.isDisposed?.() || productHoverActionManagedMeshes.has(mesh)) return;
    productHoverActionManagedMeshes.add(mesh);
    try { mesh.isPickable = true; } catch {}
    try {
      if (!mesh.actionManager) mesh.actionManager = new ActionManager(scene);
      mesh.actionManager.registerAction(new ExecuteCodeAction(ActionManager.OnPointerOverTrigger, (evt: any) => {
        setProductHoverFromMeshEvent(mesh, evt);
      }));
      mesh.actionManager.registerAction(new ExecuteCodeAction(ActionManager.OnPointerOutTrigger, () => {
        clearProductHoverFromMeshEvent(mesh);
      }));
      productHoverActionsInstalled = true;
      logAsuka3DPerf('hover', 'action-installed', { meshId: mesh.id, meshName: mesh.name, totalModelMeshes: modelMeshes.length }, { console: false });
    } catch (error) {
      logAsuka3DPerf('hover', 'action-install-error', { meshId: mesh.id, meshName: mesh.name, error }, { level: 'error' });
      debugBabylonProduct3D('hover:action-manager-register-failed', { meshId: mesh.id, meshName: mesh.name, error }, 'warn');
    }
  };

  const ensureProductHoverActions = (): void => {
    for (const mesh of getRenderableModelMeshes(modelMeshes)) ensureProductHoverActionsForMesh(mesh);
  };


  const clearProductHover = (shouldEmit = true, resetHitKey = true): void => {
    if (productHoverRaf) {
      try { cancelAnimationFrame(productHoverRaf); } catch {}
      productHoverRaf = 0;
    }
    pendingProductHoverPick = null;
    pendingProductHoverEvent = null;
    if (resetHitKey) lastProductHoverHitKey = '';
    const previousHoverMesh = hoveredProductMesh;
    const previousHoverVisual = hoveredProductMeshPreviousVisual;
    const hadHover = !!previousHoverMesh || !!hoveredProductKey;
    if (previousHoverMesh && !previousHoverMesh.isDisposed?.()) {
      try {
        previousHoverMesh.renderOutline = previousHoverVisual?.renderOutline === true;
        previousHoverMesh.outlineWidth = Number(previousHoverVisual?.outlineWidth ?? 0) || 0;
        if (previousHoverVisual?.outlineColor) previousHoverMesh.outlineColor = previousHoverVisual.outlineColor;
        (previousHoverMesh as any).showBoundingBox = previousHoverVisual?.showBoundingBox === true;
        if (previousHoverVisual && Number.isFinite(previousHoverVisual.visibility)) {
          previousHoverMesh.visibility = previousHoverVisual.visibility;
        }
      } catch {}

      // Hover is a temporary visual state. Do not call setSelectedObject() here:
      // on detailed Product 3D scenes it iterates every runtime mesh on every
      // hover leave/enter. Restore only the previous hovered mesh when it belongs
      // to the selected Product 3D instance.
      if (selectedObjectId) {
        try {
          const productSelected = selectedObjectId.startsWith('product:');
          const selectedProductInstanceId = productSelected ? selectedObjectId.slice('product:'.length).trim() : '';
          if (productSelected && meshBelongsToProductInstance(previousHoverMesh, selectedProductInstanceId)) {
            previousHoverMesh.renderOutline = true;
            previousHoverMesh.outlineColor = color3FromInput('#16a34a');
            previousHoverMesh.outlineWidth = 0.035;
            (previousHoverMesh as any).showBoundingBox = true;
          }
        } catch {}
      }
    }
    if (hadHover) {
      hoverClearCount += 1;
      logAsuka3DPerf('hover', 'clear', {
        meshId: previousHoverMesh?.id ?? null,
        meshName: previousHoverMesh?.name ?? null,
        previousKey: hoveredProductKey,
        restoredVisibility: previousHoverVisual?.visibility ?? null,
        shouldEmit,
        resetHitKey,
      });
    }
    hoveredProductMesh = null;
    hoveredProductMeshPreviousVisual = null;
    hoveredProductKey = '';
    try { if (canvas) canvas.style.cursor = ''; } catch {}
    if (shouldEmit && hadHover) {
      try { emitAsukaEvent('engine3d.hover.changed', { active: false, target: null, viewId: currentViewId, engine: 'babylon' }); } catch {}
    }
  };

  const setProductHoverTarget = (target: AsukaSurfacePickTarget | null | undefined, event?: PointerEvent | MouseEvent | null, runtimeMesh?: Mesh | null): void => {
    const kind = String(target?.kind ?? '').trim();
    if (kind !== 'product' && kind !== 'mesh') {
      clearProductHover(true);
      return;
    }
    const metadata: any = target?.metadata && typeof target.metadata === 'object' ? target.metadata : {};
    const meshId = String(target?.meshId ?? metadata?.productPartMeshId ?? metadata?.meshId ?? '').trim();
    const mesh = runtimeMesh && !runtimeMesh.isDisposed?.() ? runtimeMesh : findModelMesh(meshId);
    if (!mesh) {
      clearProductHover(true);
      return;
    }
    const productKey = [mesh.id, mesh.name, metadata?.productPartId, metadata?.asukaProductId, metadata?.productId].map((value) => String(value ?? '').trim()).filter(Boolean).join('|');
    const hoverChanged = productKey !== hoveredProductKey || hoveredProductMesh !== mesh;
    if (hoverChanged) {
      clearProductHover(false, false);
      hoveredProductMesh = mesh;
      hoveredProductKey = productKey;
      hoveredProductMeshPreviousVisual = {
        renderOutline: mesh.renderOutline === true,
        outlineWidth: Number(mesh.outlineWidth ?? 0) || 0,
        outlineColor: mesh.outlineColor?.clone?.() ?? null,
        showBoundingBox: (mesh as any).showBoundingBox === true,
        visibility: Number(mesh.visibility ?? 1),
      };
      try {
        // Keep the hover indication subtle without swapping/cloning the mesh
        // material. Material swapping can race with variation updates and restore
        // the old material after the user picks a new color/texture.
        mesh.renderOutline = false;
        mesh.outlineWidth = Number(hoveredProductMeshPreviousVisual.outlineWidth ?? 0) || 0;
        (mesh as any).showBoundingBox = false;
        mesh.visibility = Number.isFinite(Number(hoveredProductMeshPreviousVisual?.visibility))
          ? Number(hoveredProductMeshPreviousVisual?.visibility)
          : mesh.visibility;
      } catch {}
      hoverTransitionCount += 1;
      logAsuka3DPerf('hover', 'visual-applied', {
        meshId: mesh.id,
        meshName: mesh.name,
        partId: metadata?.productPartId ?? null,
        previousVisibility: hoveredProductMeshPreviousVisual?.visibility ?? null,
        nextVisibility: Number(mesh.visibility ?? 1),
        productKey,
      });
    }
    try { if (canvas) canvas.style.cursor = 'pointer'; } catch {}
    if (!hoverChanged) return;
    const label = getProductHoverLabel(mesh, target);
    const clientX = Number((event as any)?.clientX);
    const clientY = Number((event as any)?.clientY);
    const hoverEmitStart = perfNow();
    try {
      emitAsukaEvent('engine3d.hover.changed', {
        active: true,
        label,
        clientX: Number.isFinite(clientX) ? clientX : undefined,
        clientY: Number.isFinite(clientY) ? clientY : undefined,
        target: {
          kind,
          meshId: mesh.id || mesh.name || meshId,
          label,
          productPartId: metadata?.productPartId,
          productPartLabel: metadata?.productPartLabel,
          productId: getProductIdFromMesh(mesh),
        },
        viewId: currentViewId,
        engine: 'babylon',
      });
    } catch {}
    logAsuka3DPerf('hover', 'changed-emitted', {
      label,
      meshId: mesh.id,
      partId: metadata?.productPartId ?? null,
      productId: getProductIdFromMesh(mesh),
      clientX: Number.isFinite(clientX) ? clientX : null,
      clientY: Number.isFinite(clientY) ? clientY : null,
    }, { durationMs: perfNow() - hoverEmitStart });
  };

  const emitCanvasProductPartClick = (target: AsukaSurfacePickTarget, event?: PointerEvent | MouseEvent | null): void => {
    const startedAt = perfNow();
    const metadata: any = target?.metadata && typeof target.metadata === 'object' ? target.metadata : {};
    const productId = String(metadata?.asukaProductId ?? metadata?.productId ?? '').trim();
    const product = getProductById(productId);
    const objectId = productId ? `product:${productId}` : (getProductSelectionId() ?? 'product:current');
    if (productId) {
      selectProductInternal(productId, { emit: false, selectObject: false, source: 'canvas-product-part-click' });
    }
    const clickedPartId = String(metadata?.productPartId ?? metadata?.partId ?? '').trim();
    if (clickedPartId) selectPartInternal(clickedPartId, { emit: false, focus: false });
    const meshId = String(target?.meshId ?? metadata?.productPartMeshId ?? '').trim();
    const label = getProductHoverLabel(findModelMesh(meshId), target);
    const clientX = Number((event as any)?.clientX);
    const clientY = Number((event as any)?.clientY);
    try {
      const payload = {
        engine: 'babylon',
        viewId: currentViewId,
        active: true,
        label,
        clientX: Number.isFinite(clientX) ? clientX : undefined,
        clientY: Number.isFinite(clientY) ? clientY : undefined,
        selected: {
          id: objectId,
          kind: 'product-3d',
          label: String((product as any)?.name ?? label ?? 'Product 3D'),
          metadata: {
            engine: 'babylon',
            viewId: currentViewId,
            itemGroup: 'Product',
            productId: productId || undefined,
            catalogId: String((product as any)?.catalogId ?? '').trim() || undefined,
            product: compactProductForUi(product),
            ...(clonePlain((target.metadata ?? {}) as any) as any),
            screenBox: getMeshScreenBox(findModelMesh(meshId)),
          },
        },
        target: {
          kind: target.kind,
          meshId: target.meshId,
          label,
          ...(clonePlain((target.metadata ?? {}) as any) as any),
        },
      };
      emitAsukaEvent('engine3d.productPart.clicked', payload);
      emitRenderEngineEvent({ type: 'part-activated', action: 'parts.activate', viewId: currentViewId, engine: '3d', payload });
      logAsuka3DPerf('product-click', 'variation-event-emitted', {
        productId: productId || null,
        catalogId: String((product as any)?.catalogId ?? '').trim() || null,
        partId: clickedPartId || null,
        meshId: meshId || null,
        label,
      }, { durationMs: perfNow() - startedAt, console: false });
    } catch (error) {
      logAsuka3DPerf('product-click', 'variation-event-error', {
        productId: productId || null,
        partId: clickedPartId || null,
        meshId: meshId || null,
        error,
      }, { durationMs: perfNow() - startedAt, level: 'warn' });
    }
  };

  const normalizeProductPartLabel = (value: any, fallback: string): string => String(value ?? fallback ?? '').trim() || fallback;

  const normalizePartLookupValue = (value: any): string => String(value ?? '').trim().toLowerCase();

  const getPartAliases = (part: any): string[] => Array.from(new Set([
    part?.id,
    part?.label,
    part?.name,
    part?.materialTarget,
    ...split3DList(part?.meshNames ?? part?.meshName ?? part?.targetMeshIds ?? part?.targetMeshId),
    ...split3DList(part?.materialNames ?? part?.materialName),
  ].map(normalizePartLookupValue).filter(Boolean)));

  const aliasesMatchLoosely = (left: string, right: string): boolean => {
    const a = normalizePartLookupValue(left);
    const b = normalizePartLookupValue(right);
    if (!a || !b) return false;
    if (a === b) return true;
    if (a.includes(b) || b.includes(a)) return true;
    const compactA = a.replace(/^asuka_(part|surface)_/i, '').replace(/_print$/i, '');
    const compactB = b.replace(/^asuka_(part|surface)_/i, '').replace(/_print$/i, '');
    return !!compactA && !!compactB && (compactA === compactB || compactA.includes(compactB) || compactB.includes(compactA));
  };

  const detectProductPartsFromScene = (productIdInput?: string | null): any[] => {
    const productId = String(productIdInput ?? '').replace(/^product:/i, '').trim();
    const detected = new Map<string, any>();
    const add = (idInput: string, patch: any = {}) => {
      const id = String(idInput ?? '').trim();
      if (!id) return;
      const previous = detected.get(id) ?? {};
      detected.set(id, {
        ...previous,
        ...patch,
        id,
        label: normalizeProductPartLabel(patch?.label ?? previous?.label, id),
        meshNames: Array.from(new Set([...(previous.meshNames ?? []), ...split3DList(patch?.meshNames ?? patch?.meshName ?? patch?.targetMeshIds ?? patch?.targetMeshId)])),
        materialNames: Array.from(new Set([...(previous.materialNames ?? []), ...split3DList(patch?.materialNames ?? patch?.materialName)])),
        editable: patch?.editable !== false,
        visible: patch?.visible !== false,
      });
    };

    for (const mesh of getRenderableModelMeshes(modelMeshes)) {
      if (productId && !meshBelongsToProductInstance(mesh, productId)) continue;
      const meshName = String(mesh?.name ?? mesh?.id ?? '').trim();
      const partMatch = meshName.match(/^asuka_part_(.+)$/i);
      if (partMatch?.[1]) {
        const id = partMatch[1].replace(/_mesh$/i, '').trim();
        add(id, { label: id.replace(/_/g, ' '), meshNames: [mesh.id, mesh.name].filter(Boolean), metadata: { detectedFromMeshName: true } });
      }
    }

    for (const area of getPrintAreas()) {
      const areaProductId = String((area as any)?.metadata?.asukaProductId ?? '').trim();
      if (productId && areaProductId && areaProductId !== productId) continue;
      if (productId && !areaProductId) {
        const areaMesh = findModelMesh(String(area?.targetMeshId ?? '').trim());
        if (areaMesh && !meshBelongsToProductInstance(areaMesh, productId)) continue;
      }
      const areaId = String(area?.id ?? '').trim();
      const source = String((area as any)?.metadata?.sourceMeshName ?? (area as any)?.metadata?.sourceMeshId ?? area?.targetMeshId ?? areaId).trim();
      const match = source.match(/^asuka_surface_(.+?)(?:_print)?$/i) ?? areaId.match(/^asuka_surface_(.+?)(?:_print)?$/i);
      const partId = String((area as any)?.metadata?.partId ?? match?.[1] ?? '').replace(/_print$/i, '').trim();
      if (partId) add(partId, {
        label: partId.replace(/_/g, ' '),
        meshNames: [area?.targetMeshId, (area as any)?.metadata?.sourceMeshId, (area as any)?.metadata?.sourceMeshName].filter(Boolean),
        printable: true,
        metadata: { detectedFromPrintSurface: true },
      });
    }
    return Array.from(detected.values());
  };

  const getAllProductParts = (productIdInput?: string | null): any[] => {
    const configured = getCurrentProductParts(productIdInput);
    const byId = new Map<string, any>();
    for (const part of configured) {
      const id = String(part?.id ?? '').trim();
      if (id) byId.set(id, part);
    }
    for (const part of detectProductPartsFromScene(productIdInput)) {
      const id = String(part?.id ?? '').trim();
      if (!id || byId.has(id)) continue;
      byId.set(id, normalize3DParts([part])[0]);
    }
    return Array.from(byId.values());
  };

  const getPrintAreaIdsForPart = (part: any): string[] => {
    const aliases = getPartAliases(part);
    return getPrintAreas().filter((area) => {
      const areaAliases = [
        area?.id,
        area?.label,
        area?.targetMeshId,
        ...(Array.isArray((area as any)?.targetMeshIds) ? (area as any).targetMeshIds : []),
        (area as any)?.metadata?.partId,
        (area as any)?.metadata?.sourceMeshId,
        (area as any)?.metadata?.sourceMeshName,
      ].map(normalizePartLookupValue).filter(Boolean);
      return aliases.some((alias) => areaAliases.some((candidate) => aliasesMatchLoosely(alias, candidate)));
    }).map((area) => String(area?.id ?? '').trim()).filter(Boolean);
  };

  const getCurrentProductForParts = (productIdInput?: string | null): any => {
    const productId = String(productIdInput ?? getSelectedProductId() ?? '').trim();
    const stateProduct = getProductById(productId) ?? getProductsFromState(currentState)[0] ?? null;
    if (!stateProduct) return runtimeProductCatalog ?? null;
    const catalog = findRuntimeProductCatalog(stateProduct);
    if (catalog) {
      return {
        ...(catalog as any),
        ...(stateProduct as any),
        parts: Array.isArray(catalog.parts) && catalog.parts.length ? catalog.parts : stateProduct.parts,
        variations: catalog.variations ?? [],
      };
    }
    return stateProduct;
  };

  const findPartVariationGroupNode = (partId: string, variations: any[], trail: any[] = []): { group: any; path: any[] } | null => {
    const wanted = normalizePartLookupValue(partId);
    if (!wanted) return null;
    for (const item of Array.isArray(variations) ? variations : []) {
      if (!item || typeof item !== 'object') continue;
      const nextTrail = [...trail, item];
      const itemPartId = normalizePartLookupValue(get3DPartIdFromVariationNode(item));
      if (is3DPartVariationGroup(item) && itemPartId && aliasesMatchLoosely(itemPartId, wanted)) return { group: item, path: nextTrail };
      const found = findPartVariationGroupNode(partId, Array.isArray(item?.variations) ? item.variations : [], nextTrail);
      if (found) return found;
    }
    return null;
  };

  const findVariationPathInList = (variations: any[], variationId: string, trail: any[] = []): any[] | null => {
    const wanted = String(variationId ?? '').trim();
    if (!wanted) return null;
    for (const variation of Array.isArray(variations) ? variations : []) {
      if (!variation || typeof variation !== 'object') continue;
      const nextTrail = [...trail, variation];
      if (String(variation?.id ?? '').trim() === wanted) return nextTrail;
      const found = findVariationPathInList(Array.isArray(variation?.variations) ? variation.variations : [], wanted, nextTrail);
      if (found) return found;
    }
    return null;
  };

  const pathToMeta = (variationPath: any[]): Array<{ id?: string; name?: string }> => (Array.isArray(variationPath) ? variationPath : [])
    .map((node: any) => ({ id: String(node?.id ?? '').trim() || undefined, name: String(node?.name ?? node?.label ?? '').trim() || undefined }))
    .filter((node) => !!node.id || !!node.name);

  const compactPartVariationForApi = (variation: any): Asuka3DPartVariation | null => {
    if (!variation || typeof variation !== 'object') return null;
    const id = String(variation?.id ?? '').trim();
    if (!id) return null;
    const metadata = variation?.metadata && typeof variation.metadata === 'object' ? { ...(variation.metadata as any) } : {};
    delete (metadata as any).native;
    delete (metadata as any).product3d;
    delete (metadata as any).model;
    delete (metadata as any).texture;
    delete (metadata as any).runtime;
    return {
      id,
      label: String(variation?.label ?? variation?.name ?? id).trim() || id,
      name: String(variation?.name ?? '').trim() || undefined,
      partId: String(variation?.partId ?? metadata.partId ?? '').trim() || undefined,
      previewUrl: String(variation?.previewUrl ?? variation?.image ?? variation?.icon ?? '').trim() || undefined,
      image: String(variation?.image ?? '').trim() || undefined,
      icon: String(variation?.icon ?? '').trim() || undefined,
      active: variation?.active === true,
      path: Array.isArray(variation?.path) ? clonePlain(variation.path) : undefined,
      metadata,
    } as Asuka3DPartVariation;
  };

  const listPartsInternal = (): Asuka3DPart[] => getAllProductParts().map((part: any) => {
    const id = String(part?.id ?? '').trim();
    const serializedPart = compact3DPartForScene(part);
    const meshNames = split3DList(part?.meshNames ?? part?.meshName ?? part?.targetMeshIds ?? part?.targetMeshId);
    const materialNames = split3DList(part?.materialNames ?? part?.materialName);
    const printAreaIds = getPrintAreaIdsForPart(part);
    const activeVariation = id ? getActivePartVariationInternal(id) : null;
    return {
      ...serializedPart,
      id,
      label: String(part?.label ?? part?.name ?? id).trim() || id,
      engine: '3d',
      meshIds: meshNames,
      meshNames,
      materialIds: materialNames,
      materialNames,
      materialTarget: getPartMaterialTarget(part),
      printAreaIds,
      editable: part?.editable !== false,
      printable: part?.printable === true || printAreaIds.length > 0,
      visible: part?.visible !== false,
      locked: part?.locked === true,
      selected: String(selectedPartId ?? '').trim() === id,
      activeVariationId: activeVariation?.id ?? null,
      activeVariationLabel: activeVariation?.label ?? activeVariation?.name ?? null,
      metadata: {
        ...(compact3DMetadataForState(part?.metadata) ?? {}),
        printAreaIds,
      },
    } as Asuka3DPart;
  }).filter((part) => !!part.id);

  const getPartInternal = (id: string): Asuka3DPart | null => {
    const wanted = String(id ?? '').trim();
    if (!wanted) return null;
    return listPartsInternal().find((part) => {
      const aliases = getFocusPartAliases(part);
      return aliases.some((alias) => partAliasMatchesLoosely(alias, wanted));
    }) ?? null;
  };

  const selectPartInternal = (id: string | null, options: { focus?: boolean; animate?: boolean; emit?: boolean } = {}) => {
    const wanted = String(id ?? '').trim();
    const part = wanted ? getPartInternal(wanted) : null;
    currentState = select3DPart(currentState, part ? String(part.id) : null);
    selectedPartId = currentState.selectedPartId ?? null;
    if (part) {
      lastProductPick = {
        ...(lastProductPick ? clonePlain(lastProductPick) as any : {}),
        productPartId: String(part.id),
        productPartLabel: String(part.label ?? part.id),
        productPartMeshId: String(part.meshIds?.[0] ?? part.meshNames?.[0] ?? '').trim() || undefined,
        productPartMaterialId: String(part.materialTarget ?? part.materialIds?.[0] ?? '').trim() || undefined,
      };
      if (options.focus) focusPart({ partId: String(part.id), meshId: String(part.meshIds?.[0] ?? ''), materialId: String(part.materialTarget ?? ''), animate: options.animate !== false });
    }
    if (options.emit !== false) emitChanged('selectPart', selectedPartId);
  };

  const getSelectedPartInternal = (): Asuka3DPart | null => {
    const activeId = String(selectedPartId ?? (currentState as any)?.selectedPartId ?? '').trim();
    if (activeId) return getPartInternal(activeId);
    const pickId = String(lastProductPick?.productPartId ?? '').trim();
    return pickId ? getPartInternal(pickId) : null;
  };

  const listPartVariationsInternal = (partId: string): Asuka3DPartVariation[] => {
    const product = getCurrentProductForParts();
    const variations = Array.isArray(product?.variations) ? product.variations : [];
    const groupResult = findPartVariationGroupNode(partId, variations);
    const activeProduct = getProductById(getSelectedProductId()) ?? getProductsFromState(currentState)[0] ?? null;
    const activeMap = ((activeProduct?.partVariationIds && typeof activeProduct.partVariationIds === 'object') ? activeProduct.partVariationIds : {}) as Record<string, unknown>;
    const activeId = String(activeMap[String(partId)] ?? '').trim();

    const toApiVariation = (variation: any, path: any[]): Asuka3DPartVariation => {
      const id = String(variation?.id ?? '').trim();
      const variationPath = pathToMeta(path);
      return {
        id,
        label: String(variation?.name ?? variation?.label ?? id).trim() || id,
        name: String(variation?.name ?? '').trim() || undefined,
        partId: String(partId ?? '').trim(),
        previewUrl: String(variation?.image ?? variation?.icon ?? '').trim() || undefined,
        image: String(variation?.image ?? '').trim() || undefined,
        icon: String(variation?.icon ?? '').trim() || undefined,
        active: activeId ? activeId === id : variation?.active === true,
        path: variationPath,
        product3d: variation?.product3d && typeof variation.product3d === 'object'
          ? {
            partId: variation.product3d.partId,
            partGroup: variation.product3d.partGroup,
            materialTarget: variation.product3d.materialTarget,
            materials: variation.product3d.materials,
          }
          : undefined,
        metadata: {
          ...(((variation?.metadata && typeof variation.metadata === 'object') ? variation.metadata : {}) as any),
          partId: String(partId ?? '').trim(),
          variationPath,
        },
      } as Asuka3DPartVariation;
    };

    if (groupResult) {
      const group = groupResult.group;
      return (Array.isArray(group?.variations) ? group.variations : [])
        .map((variation: any) => toApiVariation(variation, [...groupResult.path, variation]))
        .filter((variation: Asuka3DPartVariation) => !!variation.id);
    }

    // A one-mesh product uses the normal flat variation tree. This fallback is
    // deliberately restricted to exactly one configured part so the established
    // Nike/multi-part behavior remains byte-for-byte conceptually unchanged.
    const configuredParts = normalize3DParts(product?.parts ?? []);
    const singlePart = configuredParts.length === 1 ? configuredParts[0] : null;
    if (!singlePart) return [];
    const singleAliases = [singlePart?.id, singlePart?.label, singlePart?.materialTarget, ...(split3DList(singlePart?.meshNames ?? singlePart?.meshName))]
      .map(normalizePartLookupValue)
      .filter(Boolean);
    const wanted = normalizePartLookupValue(partId);
    if (!singleAliases.some((alias) => aliasesMatchLoosely(alias, wanted))) return [];

    const leafPaths: any[][] = [];
    const collectLeaves = (list: any[], trail: any[] = []) => {
      for (const variation of Array.isArray(list) ? list : []) {
        if (!variation || typeof variation !== 'object' || is3DPartVariationGroup(variation)) continue;
        const nextPath = [...trail, variation];
        const children = Array.isArray(variation?.variations) ? variation.variations : [];
        if (children.length) collectLeaves(children, nextPath);
        else leafPaths.push(nextPath);
      }
    };
    collectLeaves(variations);
    return leafPaths
      .map((path) => toApiVariation(path[path.length - 1], path))
      .filter((variation: Asuka3DPartVariation) => !!variation.id);
  };

  const getActivePartVariationInternal = (partId: string): Asuka3DPartVariation | null => {
    const variations = listPartVariationsInternal(partId);
    return variations.find((variation) => variation.active === true) ?? variations[0] ?? null;
  };

  const applyPartVariationInternal = async (
    partIdInput: string,
    variationIdInput: string,
    optionsInput: Asuka3DPartVariationApplyOptions = {},
  ): Promise<Asuka3DPartVariation | null> => {
    const perfEnabled = isAsuka3DPerfDebugEnabled();
    const perfStart = perfEnabled ? perfNow() : 0;
    const perfSeq = ++variationPerfSeq;
    const partId = String(partIdInput ?? '').trim();
    const variationId = String(variationIdInput ?? '').trim();
    const endSpan = beginAsuka3DPerfSpan('variation', 'apply-part', {
      sequence: perfSeq,
      partId,
      variationId,
      productId: (optionsInput as any)?.productId ?? currentState.selectedProductId ?? null,
      focus: optionsInput?.focus !== false,
      commitHistory: optionsInput?.commitHistory !== false,
      emitChange: optionsInput?.emitChange !== false,
      source: (optionsInput as any)?.source ?? null,
    });
    lastVariationPerfAt = perfStart || perfNow();
    lastVariationPerfLabel = `${partId || '?'}:${variationId || '?'}`;
    if (!partId || !variationId) {
      endSpan({ ok: false, reason: 'missing-id' }, { level: 'warn' });
      return null;
    }
    const targetInstanceId = String((optionsInput as any)?.productId ?? currentState.selectedProductId ?? '').trim();
    const getPartTargetAliasesForInstanceResolution = (candidate: any): string[] => Array.from(new Set([
      candidate?.materialTarget,
      candidate?.targetMeshId,
      ...split3DList(candidate?.meshIds ?? candidate?.meshNames ?? candidate?.meshName ?? candidate?.targetMeshIds ?? candidate?.targetMeshId),
    ].map((value) => String(value ?? '').trim()).filter(Boolean)));
    const partCandidateBelongsToInstance = (candidate: any, instanceId: string): boolean => {
      const wantedInstance = String(instanceId ?? '').trim();
      if (!wantedInstance) return false;
      const metadata: any = candidate?.metadata ?? {};
      const metadataInstanceId = String(metadata?.asukaProductId ?? '').trim();
      if (metadataInstanceId && metadataInstanceId === wantedInstance) return true;
      return getPartTargetAliasesForInstanceResolution(candidate).some((alias) => alias.includes(`${wantedInstance}::`));
    };
    const findPartForInstance = (): Asuka3DPart | null => {
      const allParts = listPartsInternal();
      const partMatches = allParts.filter((candidate: any) => {
        const aliases = getFocusPartAliases(candidate);
        return aliases.some((alias) => partAliasMatchesLoosely(alias, partId));
      });
      if (!targetInstanceId) return partMatches[0] ?? null;
      const scoped = partMatches.find((candidate: any) => partCandidateBelongsToInstance(candidate, targetInstanceId));
      if (scoped) return scoped;
      // The first product loaded by a view can reuse the bootstrap model to keep
      // the stable single-element flow. Its part definitions may therefore be
      // unscoped even though the meshes are tagged with the instance id.
      const unscoped = partMatches.filter((candidate: any) => {
        const metadata: any = candidate?.metadata ?? {};
        return !String(metadata?.asukaProductId ?? '').trim();
      });
      return unscoped[0] ?? partMatches[0] ?? null;
    };
    const resolvePartMaterialTargetForInstance = (candidate: any, fallbackPartId: string): string => {
      const defaultTarget = String(candidate?.materialTarget ?? getPartMaterialTarget(candidate)).trim() || fallbackPartId;
      if (!targetInstanceId) return defaultTarget;
      const explicitScopedTarget = getPartTargetAliasesForInstanceResolution(candidate).find((alias) => alias.includes(`${targetInstanceId}::`) && partAliasMatchesLoosely(alias, fallbackPartId));
      if (explicitScopedTarget) return explicitScopedTarget;
      const originalTarget = String(candidate?.metadata?.originalMaterialTarget ?? defaultTarget.replace(`${targetInstanceId}::`, '') ?? fallbackPartId).trim() || fallbackPartId;
      return `${targetInstanceId}::${originalTarget}`;
    };
    const partLookupStart = perfNow();
    const part = findPartForInstance();
    const partLookupMs = perfNow() - partLookupStart;
    if (!part || part.locked || part.editable === false) {
      endSpan({ ok: false, reason: !part ? 'part-not-found' : part.locked ? 'part-locked' : 'part-not-editable', partLookupMs }, { level: 'warn' });
      return null;
    }

    const variationResolveStart = perfNow();
    const product = (optionsInput?.element && typeof optionsInput.element === 'object')
      ? (optionsInput.element as any)
      : ((getCurrentProductForParts(targetInstanceId) ?? {}) as any);
    const sourceVariations = Array.isArray(product?.variations) ? product.variations : [];
    if (sourceVariations.length) rememberRuntimeProductCatalog(product);
    const sceneProduct = compactProduct({
      ...(product ?? {}),
      variationId,
      variationName: String(optionsInput?.variation?.name ?? variationId),
    });

    const explicitPath = Array.isArray(optionsInput?.variationPath) ? optionsInput.variationPath.filter(Boolean) : [];
    let variationPath = explicitPath.length ? explicitPath : null;
    if (!variationPath) {
      const groupResult = findPartVariationGroupNode(partId, sourceVariations);
      const leafPath = groupResult ? findVariationPathInList(Array.isArray(groupResult.group?.variations) ? groupResult.group.variations : [], variationId, groupResult.path) : null;
      variationPath = leafPath ?? findVariationPathInList(sourceVariations, variationId) ?? [];
    }
    const variation = (optionsInput?.variation && typeof optionsInput.variation === 'object')
      ? optionsInput.variation
      : (variationPath[variationPath.length - 1] ?? null);

    const target = resolvePartMaterialTargetForInstance(part, partId);
    const rawPart = part;
    const materialBase = materialFromVariationTreeForPart(variationPath, rawPart) ?? (() => {
      const merged = mergeProduct3DVariationPath(variationPath);
      const rawMaterials = merged?.materials && typeof merged.materials === 'object' ? merged.materials : {};
      const rawTarget = String(merged?.materialTarget ?? '').trim();
      const sourceMaterial = (rawMaterials[target] && typeof rawMaterials[target] === 'object' ? rawMaterials[target] : null)
        ?? (rawTarget && rawMaterials[rawTarget] && typeof rawMaterials[rawTarget] === 'object' ? rawMaterials[rawTarget] : null)
        ?? Object.values(rawMaterials).find((item: any) => item && typeof item === 'object')
        ?? null;
      if (!sourceMaterial) return null;
      const meshNames = split3DList((rawPart as any)?.meshNames ?? (rawPart as any)?.meshName ?? part.meshIds ?? part.meshNames);
      return {
        ...(sourceMaterial as any),
        label: String((sourceMaterial as any)?.label ?? part.label ?? target),
        targetMeshIds: meshNames,
        metadata: {
          ...(((sourceMaterial as any)?.metadata && typeof (sourceMaterial as any).metadata === 'object') ? (sourceMaterial as any).metadata : {}),
          partId,
          partLabel: String(part.label ?? partId),
          materialTarget: target,
          targetMeshIds: meshNames,
        },
      };
    })();
    // target is a material-state key for many products, not necessarily a mesh id.
    // Keep the real scoped mesh ids so flat one-mesh color variations repaint the
    // Babylon mesh instead of only updating persisted material state.
    const effectiveTargetMeshIds = resolveProduct3DVariationTargetMeshIds({
      material: materialBase,
      targetInstanceId,
      materialTarget: target,
      meshes: scene?.meshes as any[],
    });
    const material = materialBase ? {
      ...(materialBase as any),
      materialTarget: target,
      targetMeshIds: effectiveTargetMeshIds.length ? effectiveTargetMeshIds : (materialBase as any).targetMeshIds,
      metadata: {
        ...((((materialBase as any).metadata && typeof (materialBase as any).metadata === 'object') ? (materialBase as any).metadata : {}) as any),
        partId,
        partLabel: String(part.label ?? partId),
        materialTarget: target,
        ...(targetInstanceId ? { asukaProductId: targetInstanceId } : {}),
        ...(effectiveTargetMeshIds.length ? { targetMeshIds: effectiveTargetMeshIds } : {}),
      },
    } : null;
    if (material && targetInstanceId) {
      const targetOriginal = target.replace(`${targetInstanceId}::`, '');
      for (const [staleKey, staleConfig] of Object.entries(materialState)) {
        if (staleKey === target || staleKey.includes(`${targetInstanceId}::`)) continue;
        const staleMetadata: any = (staleConfig as any)?.metadata ?? {};
        const staleInstanceId = String(staleMetadata?.asukaProductId ?? '').trim();
        const stalePartId = String(staleMetadata?.partId ?? staleMetadata?.originalMaterialTarget ?? staleKey).trim();
        if (staleInstanceId === targetInstanceId && (
          partAliasMatchesLoosely(staleKey, targetOriginal)
          || partAliasMatchesLoosely(stalePartId, partId)
        )) delete (materialState as any)[staleKey];
      }
    }
    const variationResolveMs = perfNow() - variationResolveStart;

    const targetProduct = getProductById(targetInstanceId) ?? getProductsFromState(currentState)[0] ?? null;
    const currentPartVariationIds = ((targetProduct?.partVariationIds && typeof targetProduct.partVariationIds === 'object') ? targetProduct.partVariationIds : {}) as Record<string, unknown>;
    const currentPartVariationPrices = ((targetProduct?.partVariationPrices && typeof targetProduct.partVariationPrices === 'object') ? targetProduct.partVariationPrices : {}) as Record<string, unknown>;
    const previousVariationId = String(currentPartVariationIds[partId] ?? '').trim();
    const selectedPartVariationPrice = sumPriceInputs(variationPath.map((node: any) => node?.price));
    const partVariationPrices: Record<string, number> = {};
    for (const [key, value] of Object.entries(currentPartVariationPrices)) {
      const normalized = normalizePriceInput(value);
      if (String(key).trim() && normalized !== undefined) partVariationPrices[String(key)] = normalized;
    }
    partVariationPrices[partId] = selectedPartVariationPrice;
    const previousVariationPrice = normalizePriceInput(targetProduct?.variationPrice)
      ?? sumPriceInputs(Object.values(currentPartVariationPrices));
    const previousTotal = normalizePriceInput(targetProduct?.price);
    const sourceElementBasePrice = normalizePriceInput((optionsInput as any)?.element?.price);
    const basePrice = normalizePriceInput(targetProduct?.basePrice)
      ?? (previousTotal !== undefined ? previousTotal - previousVariationPrice : undefined)
      ?? sourceElementBasePrice
      ?? 0;
    const variationPrice = sumPriceInputs(Object.values(partVariationPrices));
    const totalPrice = basePrice + variationPrice;
    const pricingChanged = normalizePriceInput(currentPartVariationPrices[partId]) !== selectedPartVariationPrice
      || normalizePriceInput(targetProduct?.basePrice) !== basePrice
      || normalizePriceInput(targetProduct?.variationPrice) !== variationPrice
      || normalizePriceInput(targetProduct?.price) !== totalPrice;
    const materialBeforeSignature = target ? (() => { try { return JSON.stringify((materialState as any)[target] ?? null); } catch { return ''; } })() : '';
    const materialAfterSignature = material ? (() => { try { return JSON.stringify(material ?? null); } catch { return ''; } })() : materialBeforeSignature;
    const materialChanged = !!(material && target && materialBeforeSignature !== materialAfterSignature);


    if (previousVariationId === variationId && !materialChanged && !pricingChanged) {
      if (material && target) {
        (materialState as any)[target] = clonePlain(material);
        applyMaterialState();
        try { scene?.render(); } catch {}
      }
      const active = getActivePartVariationInternal(partId) ?? listPartVariationsInternal(partId).find((item) => item.id === variationId) ?? null;
      endSpan({ ok: true, skipped: true, reason: 'already-active', partLookupMs, variationResolveMs, totalMs: perfNow() - perfStart });
      return compactPartVariationForApi(active);
    }

    let materialApplyMs = 0;
    if (materialChanged && material && target) {
      const materialApplyStart = perfEnabled ? perfNow() : 0;
      (materialState as any)[target] = clonePlain(material);
      applyMaterialState();
      materialApplyMs = perfEnabled ? perfNow() - materialApplyStart : 0;
    }

    const partVariationIds = {
      ...currentPartVariationIds,
      [partId]: variationId,
    };
    const variationPathRecords = pathToMeta(variationPath);
    const variationPathLabel = variationPathRecords.map((node: any) => String(node?.name ?? node?.id ?? '').trim()).filter(Boolean).join(' / ');
    if (targetInstanceId) {
      patchProductInState(targetInstanceId, (product: any) => ({
        ...product,
        ...(sceneProduct ?? {}),
        id: targetInstanceId,
        variationId,
        variationName: String(variation?.name ?? variationId),
        variationPath: variationPathRecords,
        variationPathLabel,
        partVariationIds,
        partVariationPrices,
        basePrice,
        variationPrice,
        price: totalPrice,
      }));
    }
    selectedPartId = partId;
    currentState = {
      ...currentState,
      materials: compact3DMaterialStateForPersistence(materialState),
      selectedProductId: targetInstanceId || currentState.selectedProductId || null,
      selectedPartId: partId,
    };

    if (optionsInput?.focus !== false) {
      focusPart({ partId, meshId: String(part.meshIds?.[0] ?? part.meshNames?.[0] ?? ''), materialId: target, animate: optionsInput?.animate !== false });
    }
    // A product-part material variation does not change print-area geometry or
    // texture objects. Re-rendering every print area on each variation click
    // was one of the remaining sources of progressive lag. Let Babylon redraw the
    // scene with the updated material and keep print-area rebuilds for actual
    // print-area/object mutations.
    const renderStart = perfEnabled ? perfNow() : 0;
    try { scene?.render(); } catch {}
    const renderMs = perfEnabled ? perfNow() - renderStart : 0;
    const emitStart = perfEnabled ? perfNow() : 0;
    if (optionsInput?.emitChange !== false) {
      emitChanged('applyPartVariation', `${partId}:${variationId}`, { commitHistory: optionsInput?.commitHistory !== false });
    } else if (optionsInput?.commitHistory !== false) {
      try { commit3DHistorySnapshot({ source: 'applyPartVariation', reason: 'applyPartVariation' }); } catch {}
    }
    const emitMs = perfEnabled ? perfNow() - emitStart : 0;
    const listStart = perfEnabled ? perfNow() : 0;
    const applied = getActivePartVariationInternal(partId) ?? listPartVariationsInternal(partId).find((item) => item.id === variationId) ?? null;
    const listMs = perfEnabled ? perfNow() - listStart : 0;
    endSpan({
      ok: true,
      skipped: false,
      targetInstanceId,
      target,
      partLookupMs,
      variationResolveMs,
      materialChanged,
      materialApplyMs,
      renderMs,
      emitMs,
      listMs,
      history: get3DHistoryInfo(currentViewId),
      sceneMaterials: scene?.materials?.length ?? 0,
      sceneTextures: scene?.textures?.length ?? 0,
      totalMs: perfNow() - perfStart,
    });
    return compactPartVariationForApi(applied);
  };

  const restorePersistedPartVariations = async (): Promise<void> => {
    for (const product of getProductsFromState(currentState)) {
      const productId = String(product?.id ?? '').trim();
      const selectedVariations = product?.partVariationIds && typeof product.partVariationIds === 'object'
        ? product.partVariationIds as Record<string, unknown>
        : {};
      const element = findRuntimeProductCatalog(product);
      for (const [partId, variationIdInput] of Object.entries(selectedVariations)) {
        const variationId = String(variationIdInput ?? '').trim();
        if (!String(partId).trim() || !variationId) continue;
        try {
          await applyPartVariationInternal(partId, variationId, {
            productId,
            element: element ?? undefined,
            focus: false,
            animate: false,
            commitHistory: false,
            emitChange: false,
          });
        } catch {}
      }
    }
  };


  const isSameModelMesh = (mesh: any, target?: Mesh | null) => {
    if (!mesh || !target) return false;
    const targetId = String(target.id ?? target.name ?? '').trim();
    return mesh === target || (!!targetId && (mesh.id === targetId || mesh.name === targetId));
  };

  const isModelMeshCandidate = (mesh: any, candidates: Mesh[]) => {
    if (!mesh) return false;
    return candidates.some((candidate) => candidate === mesh || candidate.id === mesh.id || candidate.name === mesh.name);
  };

  const isPreferredTargetOccludedOnRay = (ray: any, preferredTarget: Mesh | null | undefined, targetPick: any, candidates: Mesh[]) => {
    if (!scene || !preferredTarget || !targetPick?.hit || !targetPick.pickedMesh) return false;
    try {
      const nearestPick = scene.pickWithRay(ray, (mesh: any) => isModelMeshCandidate(mesh, candidates), false);
      if (!nearestPick?.hit || !nearestPick.pickedMesh) return false;
      if (isSameModelMesh(nearestPick.pickedMesh, preferredTarget)) return false;
      const nearestDistance = Number(nearestPick.distance ?? 0);
      const targetDistance = Number(targetPick.distance ?? 0);
      return Number.isFinite(nearestDistance)
        && Number.isFinite(targetDistance)
        && nearestDistance < targetDistance - 0.002;
    } catch {
      return false;
    }
  };

  const pickVisibleModelSurface = (preferredTarget?: Mesh | null) => {
    if (!scene || !camera) return null;
    const candidates = modelMeshes.filter((mesh) => !!mesh && !mesh.isDisposed());
    if (!candidates.length) return null;
    const preferredId = String(preferredTarget?.id ?? preferredTarget?.name ?? '').trim();
    const predicate = (mesh: any) => {
      if (!mesh) return false;
      if (preferredId) return mesh === preferredTarget || mesh.id === preferredId || mesh.name === preferredId;
      return candidates.some((candidate) => candidate === mesh || candidate.id === mesh.id || candidate.name === mesh.name);
    };

    const rays: { ray: any; offsetScore: number; label: string }[] = [];
    try {
      const renderEngine = scene.getEngine();
      const width = Math.max(1, renderEngine.getRenderWidth());
      const height = Math.max(1, renderEngine.getRenderHeight());
      const cx = width / 2;
      const cy = height / 2;
      const step = Math.max(10, Math.min(width, height) * 0.08);
      const samples = [
        [0, 0, 1],
        [-1, 0, 0.92], [1, 0, 0.92], [0, -1, 0.92], [0, 1, 0.92],
        [-1, -1, 0.84], [1, -1, 0.84], [-1, 1, 0.84], [1, 1, 0.84],
      ];
      for (const [dx, dy, score] of samples) {
        try {
          rays.push({
            ray: scene.createPickingRay(cx + dx * step, cy + dy * step, Matrix.Identity(), camera),
            offsetScore: Number(score) || 0,
            label: `screen:${dx},${dy}`,
          });
        } catch {}
      }
    } catch {}
    try { rays.push({ ray: camera.getForwardRay(), offsetScore: 0.9, label: 'camera-forward' }); } catch {}

    let best: { pick: any; pickedMesh: Mesh; point: Vector3; normal: Vector3; normalSource: string; score: number; label: string; uv?: { x: number; y: number } | null; surfaceRight?: Vector3 | null; surfaceUp?: Vector3 | null } | null = null;
    for (const entry of rays) {
      if (!entry?.ray) continue;
      try {
        const pick = scene.pickWithRay(entry.ray, predicate, false);
        if (!pick?.hit || !pick.pickedMesh || !pick.pickedPoint) continue;
        if (isPreferredTargetOccludedOnRay(entry.ray, preferredTarget, pick, candidates)) continue;
        const pickedMesh = pick.pickedMesh as Mesh;
        const point = pick.pickedPoint as Vector3;
        const geometricFrame = getPickGeometricWorldFrame(pick, pickedMesh, point);
        const bestPickNormal = geometricFrame ? { normal: geometricFrame.normal, source: 'face-frame' } : getBestPickWorldNormal(pick, pickedMesh, point);
        let normal = bestPickNormal.normal;
        if (!normal || normal.lengthSquared() < 0.0001) normal = camera.position.subtract(point);
        if (normal.lengthSquared() < 0.0001) normal = new Vector3(0, 0, 1);
        normal.normalize();
        const toCamera = camera.position.subtract(point);
        if (toCamera.lengthSquared() > 0.0001) {
          if (Vector3.Dot(normal, toCamera) < 0) normal = normal.scale(-1);
        }
        normal.normalize();
        // Prefer hits closest to the camera (front surface of the mesh) over
        // hits farther away (which are back-faces / interior surfaces on GLBs
        // with per-face duplicate vertices and corrupted stored normals).
        // Ray distance naturally discriminates front vs back regardless of normals.
        const distScore = 1.0 / (1.0 + Math.max(0, pick.distance ?? 0));
        const score = distScore * 2.0 + Math.max(0, Math.min(1, entry.offsetScore)) * 0.35;
        if (!best || score > best.score) {
          best = {
            pick,
            pickedMesh,
            point,
            normal,
            normalSource: bestPickNormal.source,
            score,
            label: entry.label,
            uv: getPickTextureUv(pick),
            surfaceRight: geometricFrame?.right ?? null,
            surfaceUp: geometricFrame?.up ?? null,
          };
        }
      } catch {}
    }
    if (!best) return null;
    debug3DLayer('auto-surface-pick:hit', {
      targetMesh: summarizeLayerMeshForDebug(best.pickedMesh),
      faceId: Number.isFinite(Number((best.pick as any)?.faceId)) ? Number((best.pick as any).faceId) : null,
      normalSource: best.normalSource,
      pickRay: best.label,
      score: best.score,
      point: fromVec3(best.point),
      normal: fromVec3(best.normal),
    });
    return {
      targetMesh: best.pickedMesh,
      position: best.point.add(best.normal.scale(0.006)),
      rawPosition: best.point.clone(),
      normal: best.normal,
      normalSource: best.normalSource,
      faceId: Number.isFinite(Number((best.pick as any)?.faceId)) ? Number((best.pick as any).faceId) : null,
      ...(best.uv ? { uv: best.uv } : {}),
      ...(best.surfaceRight ? { surfaceRight: best.surfaceRight } : null),
      ...(best.surfaceUp ? { surfaceUp: best.surfaceUp } : null),
    };
  };

  const resolveDecalProjection = (area?: Asuka3DPrintArea | null) => {
    const faceNetTargetMeshId = getExactFaceNetAreaTargetMeshId(area);
    const requestedTargetMeshId = faceNetTargetMeshId || String(area?.targetMeshId ?? 'product').trim() || 'product';
    const initialTargetMesh = findModelMesh(requestedTargetMeshId);
    if (!initialTargetMesh) return null;
    const autoSurface = isAutomaticProductSurfaceArea(area);
    const explicitSurfaceArea = !!area && !autoSurface && !isTextureSlotPrintArea(area);
    const explicitMeshSurface = !autoSurface
      && isExactFaceNetPrintArea(area)
      && !!faceNetTargetMeshId
      && faceNetTargetMeshId.toLowerCase() !== 'product';
    const metadataSurface = autoSurface && Array.isArray((area as any)?.metadata?.surfacePosition) && Array.isArray((area as any)?.metadata?.surfaceNormal)
      ? {
          targetMesh: initialTargetMesh,
          position: toVec3((area as any).metadata.surfacePosition, fromVec3(initialTargetMesh.position)),
          normal: toVec3((area as any).metadata.surfaceNormal, [0, 0, 1]),
        }
      : null;
    if (metadataSurface?.normal && metadataSurface.normal.lengthSquared() > 0.0001) metadataSurface.normal.normalize();
    // Explicit surface areas are authored from a concrete point on the mesh.
    // Reusing only the target mesh bounds here moved every new layer to an
    // arbitrary extremity of complex meshes (for example the top of a shoe),
    // leaving the recorded area center, selection box and rendered sticker out
    // of sync. Keep the authored world-space center as the projection source.
    const explicitAreaCenterInput = (area as any)?.worldBounds?.center ?? (area as any)?.placement?.position ?? null;
    const explicitAreaNormal = explicitSurfaceArea ? getPrintAreaNormal(area) : null;
    const explicitAreaSurface = explicitSurfaceArea
      && Array.isArray(explicitAreaCenterInput)
      && explicitAreaNormal
      && explicitAreaNormal.lengthSquared() > 0.0001
      ? (() => {
          const normal = explicitAreaNormal.clone().normalize();
          const position = toVec3(explicitAreaCenterInput, fromVec3(initialTargetMesh.position));
          return {
            targetMesh: initialTargetMesh,
            position,
            rawPosition: position.subtract(normal.scale(0.006)),
            normal,
            normalSource: 'print-area-world-bounds',
          };
        })()
      : null;
    const metadataSurfaceIsRealPick = ((area as any)?.metadata?.surfaceFromPreferredPick === true)
      || ((area as any)?.metadata?.preferredSurfacePick === true)
      || ((area as any)?.metadata?.surfacePickSource === 'pointer')
      || ((area as any)?.metadata?.surfacePickSource === 'canvas')
      || Boolean(explicitAreaSurface);
    const shouldPickVisibleSurface = autoSurface || explicitMeshSurface || Boolean(explicitAreaSurface);
    const cameraSurfacePick = shouldPickVisibleSurface && !metadataSurfaceIsRealPick ? pickVisibleModelSurface(initialTargetMesh) : null;
    const surfacePick = shouldPickVisibleSurface
      ? (explicitAreaSurface ?? (metadataSurfaceIsRealPick ? (metadataSurface ?? cameraSurfacePick) : (cameraSurfacePick ?? metadataSurface)))
      : null;
    const targetMesh = surfacePick?.targetMesh ?? initialTargetMesh;
    const meshBounds = getMeshWorldBounds(targetMesh);
    const areaBounds = getPrintAreaBounds(area);
    let normal = surfacePick?.normal ?? getPrintAreaNormal(area) ?? areaBounds.center.subtract(meshBounds.center);
    if (normal.lengthSquared() < 0.0001 && camera) normal = camera.position.subtract(meshBounds.center);
    if (normal.lengthSquared() < 0.0001) normal = new Vector3(0, 0, 1);
    normal.normalize();
    const automaticSurfaceFrame = autoSurface
      ? getProjectedStickerStableBasis(normal, 0)
      : null;

    const half = meshBounds.size.scale(0.5);
    const projectedExtent = Math.abs(normal.x) * Math.max(half.x, 0.01)
      + Math.abs(normal.y) * Math.max(half.y, 0.01)
      + Math.abs(normal.z) * Math.max(half.z, 0.01);
    const position = surfacePick?.position ?? meshBounds.center.add(normal.scale(projectedExtent + 0.006));
    const decalDepth = autoSurface
      ? Math.max(0.08, Math.min(Math.max(areaBounds.size.x, areaBounds.size.y), Math.min(areaBounds.size.x, areaBounds.size.y) * 0.45))
      : Math.max(0.035, areaBounds.size.z || 0.035);
    const size = new Vector3(
      Math.max(0.05, areaBounds.size.x),
      Math.max(0.05, areaBounds.size.y),
      decalDepth,
    );
    debug3DLayer('decal-projection:resolved', {
      areaId: String(area?.id ?? ''),
      areaMode: String((area as any)?.mode ?? ''),
      autoSurface,
      explicitMeshSurface,
      requestedTargetMeshId,
      faceNetTargetMeshId: faceNetTargetMeshId || null,
      targetMesh: summarizeLayerMeshForDebug(targetMesh),
      position: fromVec3(position),
      normal: fromVec3(normal),
      size: fromVec3(size),
      usedMetadataSurface: Boolean(metadataSurface),
      usedCameraSurfacePick: Boolean(cameraSurfacePick),
      usedMetadataAsRealPick: Boolean(metadataSurfaceIsRealPick && surfacePick === metadataSurface),
      usedSurfacePick: Boolean(surfacePick),
      surfaceNormalSource: String((surfacePick as any)?.normalSource ?? ''),
    });
    return {
      targetMesh,
      position,
      normal,
      size,
      ...(Number.isFinite(Number((surfacePick as any)?.faceId)) ? { faceId: Number((surfacePick as any).faceId) } : null),
      ...((surfacePick as any)?.rawPosition ? { rawPosition: (surfacePick as any).rawPosition } : null),
      ...(automaticSurfaceFrame?.right
        ? { surfaceRight: automaticSurfaceFrame.right }
        : ((surfacePick as any)?.surfaceRight ? { surfaceRight: (surfacePick as any).surfaceRight } : null)),
      ...(automaticSurfaceFrame?.up
        ? { surfaceUp: automaticSurfaceFrame.up }
        : ((surfacePick as any)?.surfaceUp ? { surfaceUp: (surfacePick as any).surfaceUp } : null)),
    };
  };

  const createTextTexture = (id: string, input: AsukaTextInput) => {
    const sc = requireScene();
    const text = String(input.text ?? '').trim() || 'Text';
    const width = Math.max(512, Math.min(2048, 80 + text.length * 42));
    const height = 256;
    const texture = new DynamicTexture(`${id}-texture`, { width, height }, sc, false);
    texture.hasAlpha = true;
    const fontSize = Math.max(18, Number(input.fontSize ?? 72) || 72);
    const fontFamily = String(input.fontFamily ?? 'Arial').trim() || 'Arial';
    const ctx = texture.getContext() as any;
    ctx.clearRect(0, 0, width, height);
    const fontStyle = String((input as any).fontStyle ?? 'normal').trim() || 'normal';
    const fontWeight = String((input as any).fontWeight ?? 'normal').trim() || 'normal';
    ctx.font = `${fontStyle} ${fontWeight} ${fontSize}px ${fontFamily}`;
    ctx.fillStyle = String(input.color ?? '#111827');
    ctx.textBaseline = 'middle';
    ctx.textAlign = 'center';
    ctx.fillText(text, width / 2, height / 2);
    if ((input as any).underline === true) {
      const measured = Math.min(width - 24, Math.max(1, Number(ctx.measureText?.(text)?.width ?? width * 0.7) || width * 0.7));
      ctx.strokeStyle = String(input.color ?? '#111827');
      ctx.lineWidth = Math.max(2, fontSize * 0.055);
      ctx.beginPath();
      ctx.moveTo((width - measured) / 2, height / 2 + fontSize * 0.58);
      ctx.lineTo((width + measured) / 2, height / 2 + fontSize * 0.58);
      ctx.stroke();
    }
    texture.update();
    return { texture, text, width, height };
  };

  const orientTextureForCameraFacingPlane = <T extends Texture>(texture: T): T => {
    // lookAt() keeps the plane upright but reverses its horizontal screen axis in
    // Babylon's left-handed scene. Surface decals own their UV frame; only the
    // camera-facing fallback needs this compensation.
    texture.uScale = -1;
    texture.uOffset = 1;
    return texture;
  };

  const applyDecorationMaterialRenderBias = (material: StandardMaterial | null | undefined) => {
    if (!material) return material;
    // Decals sit almost flush with the product mesh. A small depth/render bias
    // reduces z-fighting and the washed-out / clipped look that can appear when
    // Babylon alternates between the product surface and the decal fragments.
    material.backFaceCulling = false;
    material.forceDepthWrite = false;
    try { (material as any).zOffset = -8; } catch {}
    try { (material as any).zOffsetUnits = -8; } catch {}
    return material;
  };

  const createDecorationMaterial = (id: string, texture: Texture | DynamicTexture) => {
    const sc = requireScene();
    texture.hasAlpha = true;
    const mat = new StandardMaterial(`${id}-material`, sc);
    mat.diffuseColor = Color3.White();
    mat.specularColor = Color3.Black();
    mat.alpha = 1;
    mat.diffuseTexture = texture;
    mat.useAlphaFromDiffuseTexture = true;
    try { (mat as any).transparencyMode = 2; } catch {}
    mat.emissiveColor = Color3.White();
    mat.disableLighting = true;
    applyDecorationMaterialRenderBias(mat);
    return mat;
  };


  const readBase64Byte = (binary: string, index: number): number => Number(binary.charCodeAt(index) ?? 0) & 0xff;

  const getImageAspectRatioFromSource = (urlInput: string): number | null => {
    const url = String(urlInput ?? '').trim();
    if (!url) return null;
    try {
      const svgSize = url.match(/<svg[\s\S]*?\swidth=["']?([0-9.]+)[^"'\s>]*["']?[\s\S]*?\sheight=["']?([0-9.]+)[^"'\s>]*["']?/i)
        || url.match(/<svg[\s\S]*?\sheight=["']?([0-9.]+)[^"'\s>]*["']?[\s\S]*?\swidth=["']?([0-9.]+)[^"'\s>]*["']?/i);
      if (svgSize) {
        const first = Math.abs(Number(svgSize[1]) || 0);
        const second = Math.abs(Number(svgSize[2]) || 0);
        const width = /\swidth=/i.test(svgSize[0]) && svgSize[0].indexOf(svgSize[1]) < svgSize[0].indexOf(svgSize[2]) ? first : second;
        const height = width === first ? second : first;
        if (width > 0 && height > 0) return width / height;
      }
      const viewBox = url.match(/viewBox=["']\s*[-0-9.]+\s+[-0-9.]+\s+([0-9.]+)\s+([0-9.]+)\s*["']/i);
      if (viewBox) {
        const width = Math.abs(Number(viewBox[1]) || 0);
        const height = Math.abs(Number(viewBox[2]) || 0);
        if (width > 0 && height > 0) return width / height;
      }
    } catch {}
    const dataMatch = url.match(/^data:([^;,]+)?(?:;[^,]*)?,(.*)$/i);
    if (!dataMatch) return null;
    const mime = String(dataMatch[1] ?? '').toLowerCase();
    const payload = String(dataMatch[2] ?? '');
    try {
      const binary = /;base64,/i.test(url)
        ? atob(payload)
        : decodeURIComponent(payload);
      if (mime.includes('png') && binary.length >= 24
        && readBase64Byte(binary, 0) === 0x89
        && binary.slice(1, 4) === 'PNG') {
        const width = (readBase64Byte(binary, 16) << 24) | (readBase64Byte(binary, 17) << 16) | (readBase64Byte(binary, 18) << 8) | readBase64Byte(binary, 19);
        const height = (readBase64Byte(binary, 20) << 24) | (readBase64Byte(binary, 21) << 16) | (readBase64Byte(binary, 22) << 8) | readBase64Byte(binary, 23);
        if (width > 0 && height > 0) return width / height;
      }
      if ((mime.includes('jpeg') || mime.includes('jpg')) && binary.length > 12) {
        let offset = 2;
        while (offset + 9 < binary.length) {
          if (readBase64Byte(binary, offset) !== 0xff) { offset += 1; continue; }
          const marker = readBase64Byte(binary, offset + 1);
          const length = (readBase64Byte(binary, offset + 2) << 8) | readBase64Byte(binary, offset + 3);
          if (length < 2) break;
          if ((marker >= 0xc0 && marker <= 0xc3) || (marker >= 0xc5 && marker <= 0xc7) || (marker >= 0xc9 && marker <= 0xcb) || (marker >= 0xcd && marker <= 0xcf)) {
            const height = (readBase64Byte(binary, offset + 5) << 8) | readBase64Byte(binary, offset + 6);
            const width = (readBase64Byte(binary, offset + 7) << 8) | readBase64Byte(binary, offset + 8);
            if (width > 0 && height > 0) return width / height;
            break;
          }
          offset += 2 + length;
        }
      }
      if (mime.includes('gif') && binary.length >= 10) {
        const width = readBase64Byte(binary, 6) | (readBase64Byte(binary, 7) << 8);
        const height = readBase64Byte(binary, 8) | (readBase64Byte(binary, 9) << 8);
        if (width > 0 && height > 0) return width / height;
      }
    } catch {}
    return null;
  };

  const normalizeDecalProjectionAspect = <T extends { targetMesh: Mesh; position: Vector3; normal: Vector3; size: Vector3; angle?: number }>(
    projection: T,
    aspectInput?: number | null,
    options: { autoSurface?: boolean; fitRatio?: number } = {},
  ): T => {
    const aspect = Math.abs(Number(aspectInput) || 0);
    if (!Number.isFinite(aspect) || aspect <= 0.001 || !projection?.size) {
      debug3DLayer('decal-projection:aspect-fit:skip', {
        aspectInput,
        autoSurface: Boolean(options.autoSurface),
        projectionSize: projection?.size ? fromVec3(projection.size) : null,
      }, 'warn');
      return projection;
    }
    const safe = getSafeDecalProjectionSize(projection.size);
    const sourceWidth = Math.max(0.025, Math.abs(safe.x));
    const sourceHeight = Math.max(0.025, Math.abs(safe.y));

    // The automatic product surface is the whole available face/part. It must be
    // treated as a container, not as the requested sticker size. If we keep using
    // the full container as the decal size, the surface-patch renderer correctly
    // paints all intersecting triangles, but the image becomes a stretched product
    // skin. Start new/restored automatic layers as a smaller physical sticker and
    // preserve the bitmap aspect ratio inside that box.
    const fitRatio = options.autoSurface
      ? 0.42
      : clampNumber(Number(options.fitRatio ?? 1) || 1, 0.08, 1);
    const explicitMeshFit = !options.autoSurface && fitRatio < 1;
    const maxPhysical = explicitMeshFit ? 0.34 : Number.POSITIVE_INFINITY;
    const maxWidth = Math.min(sourceWidth * fitRatio, maxPhysical);
    const maxHeight = Math.min(sourceHeight * fitRatio, maxPhysical);
    let width = maxWidth;
    let height = maxHeight;
    const availableAspect = width / Math.max(height, 0.0001);
    if (availableAspect > aspect) width = height * aspect;
    else height = width / aspect;

    // Keep a visible, selectable minimum, but never allow the minimum to exceed
    // the fitted container. This is important on small 3D product parts.
    const minSticker = (options.autoSurface || fitRatio < 1) ? 0.08 : 0.025;
    if (width < minSticker && height < minSticker) {
      if (aspect >= 1) {
        width = Math.min(maxWidth, minSticker);
        height = Math.min(maxHeight, width / aspect);
      } else {
        height = Math.min(maxHeight, minSticker);
        width = Math.min(maxWidth, height * aspect);
      }
    }
    const smallest = Math.min(width, height);
    if (fitRatio < 1 && smallest > 0 && smallest < minSticker) {
      const scale = minSticker / smallest;
      const scaledWidth = width * scale;
      const scaledHeight = height * scale;
      if (scaledWidth <= maxWidth && scaledHeight <= maxHeight) {
        width = scaledWidth;
        height = scaledHeight;
      } else if (aspect >= 1) {
        width = maxWidth;
        height = Math.min(maxHeight, width / aspect);
      } else {
        height = maxHeight;
        width = Math.min(maxWidth, height * aspect);
      }
    }

    const next = {
      ...(projection as any),
      size: new Vector3(Math.max(0.025, width), Math.max(0.025, height), safe.z),
    } as T;
    debug3DLayer('decal-projection:aspect-fit', {
      aspect,
      autoSurface: Boolean(options.autoSurface),
      containerSize: fromVec3(safe),
      fitRatio,
      fittedSize: fromVec3(next.size),
    });
    return next;
  };

  type LoadedImageMetrics = { width: number; height: number; aspect: number };

  const getImageMetricsFromLoadedImage = (urlInput: string): Promise<LoadedImageMetrics | null> => new Promise((resolve) => {
    const url = String(urlInput ?? '').trim();
    if (!url || typeof Image === 'undefined') { resolve(null); return; }
    try {
      const image = new Image();
      if (!/^data:|^blob:/i.test(url)) {
        try { image.crossOrigin = 'anonymous'; } catch {}
      }
      let settled = false;
      const finish = (value: LoadedImageMetrics | null) => {
        if (settled) return;
        settled = true;
        resolve(value);
      };
      image.onload = () => {
        const width = Math.abs(Number((image as any).naturalWidth ?? image.width ?? 0) || 0);
        const height = Math.abs(Number((image as any).naturalHeight ?? image.height ?? 0) || 0);
        finish(width > 0 && height > 0 ? { width, height, aspect: width / height } : null);
      };
      image.onerror = () => finish(null);
      image.src = url;
      try {
        void (image as any).decode?.().then(() => {
          const width = Math.abs(Number((image as any).naturalWidth ?? image.width ?? 0) || 0);
          const height = Math.abs(Number((image as any).naturalHeight ?? image.height ?? 0) || 0);
          finish(width > 0 && height > 0 ? { width, height, aspect: width / height } : null);
        }).catch(() => finish(null));
      } catch {}
      try { setTimeout(() => finish(null), 500); } catch {}
    } catch {
      resolve(null);
    }
  });

  const resolveImageAspectRatio = async (urlInput: string): Promise<number | null> => {
    const parsed = getImageAspectRatioFromSource(urlInput);
    if (parsed && Number.isFinite(parsed) && parsed > 0.001) return parsed;
    return (await getImageMetricsFromLoadedImage(urlInput))?.aspect ?? null;
  };

  const getImageAspectRatioFromMetadata = (metadataInput: unknown): number | null => {
    const metadata = metadataInput && typeof metadataInput === 'object'
      ? metadataInput as Record<string, unknown>
      : {};
    const explicit = Math.abs(Number(metadata.sourceAspectRatio) || 0);
    if (Number.isFinite(explicit) && explicit > 0.001) return explicit;
    const width = Math.abs(Number(metadata.sourcePixelWidth) || 0);
    const height = Math.abs(Number(metadata.sourcePixelHeight) || 0);
    return width > 0 && height > 0 ? width / height : null;
  };

  const getCameraFacingImagePlaneSize = (aspectInput?: number | null): { width: number; height: number } => {
    const maxSize = 1.1;
    const aspect = Math.abs(Number(aspectInput) || 0);
    if (!Number.isFinite(aspect) || aspect <= 0.001) return { width: maxSize, height: maxSize };
    return aspect >= 1
      ? { width: maxSize, height: maxSize / aspect }
      : { width: maxSize * aspect, height: maxSize };
  };

  const applyDecorationColor = (mesh: Mesh, color: string, deco?: DecorationRecord | null) => {
    const value = String(color ?? '').trim();
    if (!value) return;
    const kind = String(deco?.type ?? objectKinds.get(mesh.id) ?? '').toLowerCase();
    if (kind === 'text') {
      const text = String(deco?.source?.text ?? mesh.name ?? 'Text').trim() || 'Text';
      try { mesh.material?.dispose?.(false, true); } catch {}
      mesh.material = createDecorationMaterial(mesh.id, createTextTexture(mesh.id, { text, color: value }).texture);
      for (const fragment of decalFragmentsByObjectId.get(mesh.id) ?? []) fragment.material = mesh.material;
      return;
    }
    if (mesh.material instanceof StandardMaterial) {
      mesh.material.diffuseColor = color3FromInput(value);
      applyDecorationMaterialRenderBias(mesh.material);
    }
    for (const fragment of decalFragmentsByObjectId.get(mesh.id) ?? []) fragment.material = mesh.material;
  };

  const setDecalRuntimeMetadata = (
    mesh: Mesh,
    targetMesh: Mesh,
    size: Vector3,
    normal?: Vector3 | null,
    angle = 0,
    position?: Vector3 | null,
    surfaceRight?: Vector3 | null,
    surfaceUp?: Vector3 | null,
  ) => {
    (mesh as any).__asukaProjectionMode = 'decal';
    (mesh as any).__asukaTargetMeshId = String(targetMesh.id || targetMesh.name || 'product');
    (mesh as any).__asukaDecalSize = fromVec3(size);
    (mesh as any).__asukaDecalAngle = Number(angle) || 0;
    if (position && position.lengthSquared() >= 0) (mesh as any).__asukaDecalPosition = fromVec3(position);
    if (normal && normal.lengthSquared() > 0.0001) (mesh as any).__asukaSurfaceNormal = fromVec3(normal.normalize());
    if (surfaceRight && surfaceRight.lengthSquared() > 0.0001) (mesh as any).__asukaSurfaceRight = fromVec3(surfaceRight.normalize());
    if (surfaceUp && surfaceUp.lengthSquared() > 0.0001) (mesh as any).__asukaSurfaceUp = fromVec3(surfaceUp.normalize());
    // Render decals after the base mesh (group 1 > group 0) to avoid z-fighting
    // on organic GLBs where polygon offset alone is insufficient.
    mesh.renderingGroupId = 1;
  };

  const rotateVectorAroundAxis = (vectorInput: Vector3, axisInput: Vector3, angleInput = 0): Vector3 => {
    const angle = Number(angleInput) || 0;
    const vector = vectorInput.clone();
    if (!angle) return vector;
    let axis = axisInput.clone();
    if (axis.lengthSquared() < 0.0001) return vector;
    axis.normalize();
    // Rodrigues' rotation formula keeps the saved decal basis deterministic when
    // history restores a rotated 3D layer. Relying on mesh.rotation alone was not
    // enough because projected sticker overlays are rebuilt from projection data.
    const cos = Math.cos(angle);
    const sin = Math.sin(angle);
    return vector.scale(cos)
      .add(Vector3.Cross(axis, vector).scale(sin))
      .add(axis.scale(Vector3.Dot(axis, vector) * (1 - cos)));
  };

  const getProjectedBoundsInterval = (bounds: { center: Vector3; size: Vector3 }, axisInput: Vector3) => {
    let axis = axisInput.clone();
    if (axis.lengthSquared() < 0.0001) axis = new Vector3(0, 0, 1);
    axis.normalize();
    const half = bounds.size.scale(0.5);
    const extent = Math.abs(axis.x) * Math.max(half.x, 0.0001)
      + Math.abs(axis.y) * Math.max(half.y, 0.0001)
      + Math.abs(axis.z) * Math.max(half.z, 0.0001);
    const centerProjection = Vector3.Dot(bounds.center, axis);
    return { min: centerProjection - extent, max: centerProjection + extent };
  };

  const intervalOverlaps = (a: { min: number; max: number }, b: { min: number; max: number }) => a.max >= b.min && b.max >= a.min;

  const meshIntersectsProjectedDecalVolume = (
    mesh: Mesh,
    projection: { position: Vector3; normal: Vector3; size: Vector3 },
    preferredTarget?: Mesh | null,
  ) => {
    const bounds = getMeshWorldBounds(mesh);
    const basis = getSurfaceTangentBasis(projection.normal);
    const centerDelta = bounds.center.subtract(projection.position);
    const boundsRight = getProjectedBoundsInterval(bounds, basis.right);
    const boundsUp = getProjectedBoundsInterval(bounds, basis.up);
    const boundsNormal = getProjectedBoundsInterval(bounds, projection.normal);
    const halfX = Math.max(0.02, Math.abs(projection.size.x) * 0.5);
    const halfY = Math.max(0.02, Math.abs(projection.size.y) * 0.5);
    const halfZ = Math.max(0.02, Math.abs(projection.size.z) * 0.5);
    const margin = Math.max(0.01, Math.min(Math.max(halfX, halfY) * 0.16, 0.04));
    const boxRight = {
      min: Vector3.Dot(projection.position, basis.right) - halfX - margin,
      max: Vector3.Dot(projection.position, basis.right) + halfX + margin,
    };
    const boxUp = {
      min: Vector3.Dot(projection.position, basis.up) - halfY - margin,
      max: Vector3.Dot(projection.position, basis.up) + halfY + margin,
    };
    const boxNormal = {
      min: Vector3.Dot(projection.position, projection.normal) - halfZ - margin,
      max: Vector3.Dot(projection.position, projection.normal) + halfZ + margin,
    };
    if (!intervalOverlaps(boundsRight, boxRight) || !intervalOverlaps(boundsUp, boxUp) || !intervalOverlaps(boundsNormal, boxNormal)) return false;
    // Reject meshes that are clearly on the opposite side of the product. We
    // want the sticker to continue on adjacent faces, not mirror through the
    // whole object onto the opposite side.
    if (preferredTarget && preferredTarget !== mesh) {
      const preferredBounds = getMeshWorldBounds(preferredTarget);
      const preferredNormal = getProjectedBoundsInterval(preferredBounds, projection.normal);
      const candidateNormal = boundsNormal;
      const behindGap = preferredNormal.min - candidateNormal.max;
      const maxAllowedBehindGap = Math.max(Math.min(halfZ * 0.65, Math.max(halfX, halfY) * 0.22), 0.015);
      if (behindGap > maxAllowedBehindGap) return false;
      const lateralRightDistance = Math.abs(Vector3.Dot(centerDelta, basis.right));
      const lateralUpDistance = Math.abs(Vector3.Dot(centerDelta, basis.up));
      const isNearProjectionWindow = lateralRightDistance <= halfX + Math.max(bounds.size.length() * 0.15, margin)
        && lateralUpDistance <= halfY + Math.max(bounds.size.length() * 0.15, margin);
      if (!isNearProjectionWindow) return false;
    }
    return true;
  };

  const getProjectedDecalTargetMeshes = (
    preferredTarget?: Mesh | null,
    projection?: { position: Vector3; normal: Vector3; size: Vector3 } | null,
  ): Mesh[] => {
    const targets: Mesh[] = [];
    const seen = new Set<string>();
    const add = (mesh?: Mesh | null) => {
      if (!mesh || mesh.isDisposed?.()) return;
      try { if (mesh.getTotalVertices?.() <= 0) return; } catch {}
      if (projection && !meshIntersectsProjectedDecalVolume(mesh, projection, preferredTarget ?? null)) return;
      const key = String((mesh as any).uniqueId ?? mesh.id ?? mesh.name ?? targets.length);
      if (seen.has(key)) return;
      seen.add(key);
      targets.push(mesh);
    };
    add(preferredTarget ?? null);
    // Named GLB print-surface helpers represent the exact intended projection
    // surface. Do not also project the same decal onto nearby product meshes,
    // otherwise history restore can rebuild extra fragments and the visual state
    // differs from the original add/move operation.
    if (isNamedPrintSurfaceMesh(preferredTarget ?? null)) return targets;
    for (const mesh of getRenderableModelMeshes(modelMeshes)) {
      if (mesh === preferredTarget || isNamedPrintSurfaceMesh(mesh)) continue;
      add(mesh);
    }
    return targets;
  };

  const getExpandedMultiDecalSize = (input: Vector3, normalInput?: Vector3 | null): Vector3 => {
    const size = getSafeDecalProjectionSize(input);
    const bounds = getModelBounds();
    const modelSize = bounds.max.subtract(bounds.min);
    let normal = normalInput?.clone?.() ?? new Vector3(0, 0, 1);
    if (normal.lengthSquared() < 0.0001) normal = new Vector3(0, 0, 1);
    normal.normalize();
    const half = modelSize.scale(0.5);
    const projectedExtent = Math.abs(normal.x) * Math.max(half.x, 0.01)
      + Math.abs(normal.y) * Math.max(half.y, 0.01)
      + Math.abs(normal.z) * Math.max(half.z, 0.01);
    const maxFace = Math.max(size.x, size.y);
    // Multi-mesh decals need enough projection depth to intersect adjacent
    // faces when the sticker crosses a hard product edge. Keep it bounded so we
    // do not paint through the whole scene on normal models.
    const depth = Math.max(
      size.z,
      Math.min(
        Math.max(maxFace * 0.42, 0.09),
        Math.max(projectedExtent * 0.55, maxFace * 0.28, 0.08),
      ),
    );
    return new Vector3(size.x, size.y, depth);
  };

  const filterDecalMeshToAdjacentProjection = (
    mesh: Mesh,
    projection: { position: Vector3; normal: Vector3; size: Vector3 },
  ): boolean => {
    try {
      const positions = mesh.getVerticesData('position' as any) as number[] | null;
      const normals = mesh.getVerticesData('normal' as any) as number[] | null;
      const uvs = mesh.getVerticesData('uv' as any) as number[] | null;
      const indices = mesh.getIndices() as number[] | null;
      if (!positions || !indices || indices.length < 3) return true;
      let projectionNormal = projection.normal.clone();
      if (projectionNormal.lengthSquared() < 0.0001) projectionNormal = new Vector3(0, 0, 1);
      projectionNormal.normalize();
      const world = mesh.getWorldMatrix();
      const keptPositions: number[] = [];
      const keptNormals: number[] = [];
      const keptUvs: number[] = [];
      const keptIndices: number[] = [];
      const pushVertex = (sourceIndex: number, fallbackNormal: Vector3) => {
        const px = Number(positions[sourceIndex * 3] ?? 0) || 0;
        const py = Number(positions[sourceIndex * 3 + 1] ?? 0) || 0;
        const pz = Number(positions[sourceIndex * 3 + 2] ?? 0) || 0;
        keptPositions.push(px, py, pz);
        if (normals && normals.length >= sourceIndex * 3 + 3) {
          keptNormals.push(
            Number(normals[sourceIndex * 3] ?? fallbackNormal.x) || fallbackNormal.x,
            Number(normals[sourceIndex * 3 + 1] ?? fallbackNormal.y) || fallbackNormal.y,
            Number(normals[sourceIndex * 3 + 2] ?? fallbackNormal.z) || fallbackNormal.z,
          );
        } else {
          keptNormals.push(fallbackNormal.x, fallbackNormal.y, fallbackNormal.z);
        }
        if (uvs && uvs.length >= sourceIndex * 2 + 2) {
          keptUvs.push(Number(uvs[sourceIndex * 2] ?? 0) || 0, Number(uvs[sourceIndex * 2 + 1] ?? 0) || 0);
        }
        keptIndices.push(keptPositions.length / 3 - 1);
      };
      for (let i = 0; i + 2 < indices.length; i += 3) {
        const ia = Number(indices[i]) || 0;
        const ib = Number(indices[i + 1]) || 0;
        const ic = Number(indices[i + 2]) || 0;
        const a = Vector3.TransformCoordinates(new Vector3(positions[ia * 3] || 0, positions[ia * 3 + 1] || 0, positions[ia * 3 + 2] || 0), world);
        const b = Vector3.TransformCoordinates(new Vector3(positions[ib * 3] || 0, positions[ib * 3 + 1] || 0, positions[ib * 3 + 2] || 0), world);
        const c = Vector3.TransformCoordinates(new Vector3(positions[ic * 3] || 0, positions[ic * 3 + 1] || 0, positions[ic * 3 + 2] || 0), world);
        const centroid = a.add(b).add(c).scale(1 / 3);
        let faceNormal = Vector3.Cross(b.subtract(a), c.subtract(a));
        if (faceNormal.lengthSquared() < 0.0000001) continue;
        faceNormal.normalize();
        const averageNormal = normals && normals.length >= Math.max(ia, ib, ic) * 3 + 3
          ? new Vector3(
            (Number(normals[ia * 3] ?? 0) + Number(normals[ib * 3] ?? 0) + Number(normals[ic * 3] ?? 0)) / 3,
            (Number(normals[ia * 3 + 1] ?? 0) + Number(normals[ib * 3 + 1] ?? 0) + Number(normals[ic * 3 + 1] ?? 0)) / 3,
            (Number(normals[ia * 3 + 2] ?? 0) + Number(normals[ib * 3 + 2] ?? 0) + Number(normals[ic * 3 + 2] ?? 0)) / 3,
          )
          : faceNormal.clone();
        if (averageNormal.lengthSquared() > 0.0000001) {
          averageNormal.normalize();
          try { averageNormal.copyFrom(Vector3.TransformNormal(averageNormal, world).normalize()); } catch {}
        }
        const normalScore = Math.max(Vector3.Dot(faceNormal, projectionNormal), Vector3.Dot(averageNormal, projectionNormal));
        // Keep the source face and perpendicular adjacent faces. Reject the
        // triangles facing away from the projection: they are the back/opposite
        // side duplicates, not the poster wrapping around an edge.
        if (normalScore < -0.18) continue;
        // Extra guard: if the generated triangle is far behind the projection
        // center along the decal normal, it is almost certainly a through-object
        // duplicate on the opposite face.
        const behind = Vector3.Dot(projection.position.subtract(centroid), projectionNormal);
        if (behind > Math.max(Math.abs(projection.size.z) * 0.72, Math.max(projection.size.x, projection.size.y) * 0.42, 0.08)) continue;
        pushVertex(ia, faceNormal);
        pushVertex(ib, faceNormal);
        pushVertex(ic, faceNormal);
      }
      if (keptIndices.length < 3) {
        try { mesh.dispose(false, false); } catch {}
        return false;
      }
      if (keptIndices.length !== indices.length) {
        const vertexData = new VertexData();
        vertexData.positions = keptPositions;
        vertexData.indices = keptIndices;
        vertexData.normals = keptNormals;
        if (keptUvs.length === (keptPositions.length / 3) * 2) vertexData.uvs = keptUvs;
        vertexData.applyToMesh(mesh, true);
        try { mesh.refreshBoundingInfo(); } catch {}
      }
      return true;
    } catch {
      return true;
    }
  };

  const filterDecalMeshToSourceFace = (
    mesh: Mesh,
    projection: { position: Vector3; normal: Vector3; size: Vector3 },
  ): boolean => {
    try {
      const positions = mesh.getVerticesData('position' as any) as number[] | null;
      const normals = mesh.getVerticesData('normal' as any) as number[] | null;
      const uvs = mesh.getVerticesData('uv' as any) as number[] | null;
      const indices = mesh.getIndices() as number[] | null;
      if (!positions || !indices || indices.length < 3) return true;
      let projectionNormal = projection.normal.clone();
      if (projectionNormal.lengthSquared() < 0.0001) projectionNormal = new Vector3(0, 0, 1);
      projectionNormal.normalize();
      const world = mesh.getWorldMatrix();
      const keptPositions: number[] = [];
      const keptNormals: number[] = [];
      const keptUvs: number[] = [];
      const keptIndices: number[] = [];
      const pushVertex = (sourceIndex: number, fallbackNormal: Vector3) => {
        const px = Number(positions[sourceIndex * 3] ?? 0) || 0;
        const py = Number(positions[sourceIndex * 3 + 1] ?? 0) || 0;
        const pz = Number(positions[sourceIndex * 3 + 2] ?? 0) || 0;
        keptPositions.push(px, py, pz);
        if (normals && normals.length >= sourceIndex * 3 + 3) {
          keptNormals.push(
            Number(normals[sourceIndex * 3] ?? fallbackNormal.x) || fallbackNormal.x,
            Number(normals[sourceIndex * 3 + 1] ?? fallbackNormal.y) || fallbackNormal.y,
            Number(normals[sourceIndex * 3 + 2] ?? fallbackNormal.z) || fallbackNormal.z,
          );
        } else {
          keptNormals.push(fallbackNormal.x, fallbackNormal.y, fallbackNormal.z);
        }
        if (uvs && uvs.length >= sourceIndex * 2 + 2) {
          keptUvs.push(Number(uvs[sourceIndex * 2] ?? 0) || 0, Number(uvs[sourceIndex * 2 + 1] ?? 0) || 0);
        }
        keptIndices.push(keptPositions.length / 3 - 1);
      };
      for (let i = 0; i + 2 < indices.length; i += 3) {
        const ia = Number(indices[i]) || 0;
        const ib = Number(indices[i + 1]) || 0;
        const ic = Number(indices[i + 2]) || 0;
        const a = Vector3.TransformCoordinates(new Vector3(positions[ia * 3] || 0, positions[ia * 3 + 1] || 0, positions[ia * 3 + 2] || 0), world);
        const b = Vector3.TransformCoordinates(new Vector3(positions[ib * 3] || 0, positions[ib * 3 + 1] || 0, positions[ib * 3 + 2] || 0), world);
        const c = Vector3.TransformCoordinates(new Vector3(positions[ic * 3] || 0, positions[ic * 3 + 1] || 0, positions[ic * 3 + 2] || 0), world);
        const centroid = a.add(b).add(c).scale(1 / 3);
        let faceNormal = Vector3.Cross(b.subtract(a), c.subtract(a));
        if (faceNormal.lengthSquared() < 0.0000001) continue;
        faceNormal.normalize();
        const normalScore = Math.abs(Vector3.Dot(faceNormal, projectionNormal));
        // Keep only the source-facing triangles; perpendicular faces are handled
        // by the explicit corner continuation fragment, not by the main decal.
        if (normalScore < 0.78) continue;
        const behind = Math.abs(Vector3.Dot(projection.position.subtract(centroid), projectionNormal));
        if (behind > Math.max(Math.abs(projection.size.z) * 0.45, 0.05)) continue;
        pushVertex(ia, faceNormal);
        pushVertex(ib, faceNormal);
        pushVertex(ic, faceNormal);
      }
      if (keptIndices.length < 3) {
        try { mesh.dispose(false, false); } catch {}
        return false;
      }
      const vertexData = new VertexData();
      vertexData.positions = keptPositions;
      vertexData.indices = keptIndices;
      vertexData.normals = keptNormals;
      if (keptUvs.length === (keptPositions.length / 3) * 2) vertexData.uvs = keptUvs;
      vertexData.applyToMesh(mesh, true);
      try { mesh.refreshBoundingInfo(); } catch {}
      return true;
    } catch {
      return true;
    }
  };

  const makeDecalTextureFragmentMaterial = (
    ownerId: string,
    baseMaterial: any,
    suffix: string,
    crop: { uOffset: number; vOffset: number; uScale: number; vScale: number },
  ): StandardMaterial | null => {
    try {
      const sc = requireScene();
      const mat = new StandardMaterial(`${ownerId}-corner-fragment-material-${suffix}`, sc);
      const sourceTexture = baseMaterial?.diffuseTexture ?? baseMaterial?.albedoTexture ?? baseMaterial?.opacityTexture ?? null;
      let texture: any = sourceTexture;
      try { texture = sourceTexture?.clone?.(`${ownerId}-corner-fragment-texture-${suffix}`) ?? sourceTexture; } catch { texture = sourceTexture; }
      if (texture) {
        try { texture.wrapU = Texture.CLAMP_ADDRESSMODE; } catch {}
        try { texture.wrapV = Texture.CLAMP_ADDRESSMODE; } catch {}
        try { texture.uOffset = crop.uOffset; } catch {}
        try { texture.vOffset = crop.vOffset; } catch {}
        try { texture.uScale = crop.uScale; } catch {}
        try { texture.vScale = crop.vScale; } catch {}
        mat.diffuseTexture = texture;
        try { mat.opacityTexture = texture; } catch {}
      }
      mat.useAlphaFromDiffuseTexture = true;
      mat.emissiveColor = Color3.White();
      mat.backFaceCulling = false;
      mat.forceDepthWrite = false;
      try { (mat as any).transparencyMode = 2; } catch {}
      applyDecorationMaterialRenderBias(mat);
      return mat;
    } catch {
      return null;
    }
  };

  const makeDecalTextureCloneMaterial = (
    ownerId: string,
    baseMaterial: any,
    suffix: string,
  ): StandardMaterial | null => {
    try {
      const sc = requireScene();
      const mat = new StandardMaterial(`${ownerId}-corner-clone-material-${suffix}`, sc);
      const sourceTexture = baseMaterial?.diffuseTexture ?? baseMaterial?.albedoTexture ?? baseMaterial?.opacityTexture ?? null;
      let texture: any = sourceTexture;
      try { texture = sourceTexture?.clone?.(`${ownerId}-corner-clone-texture-${suffix}`) ?? sourceTexture; } catch { texture = sourceTexture; }
      if (texture) {
        try { texture.wrapU = Texture.CLAMP_ADDRESSMODE; } catch {}
        try { texture.wrapV = Texture.CLAMP_ADDRESSMODE; } catch {}
        try { texture.uOffset = 0; } catch {}
        try { texture.vOffset = 0; } catch {}
        try { texture.uScale = 1; } catch {}
        try { texture.vScale = 1; } catch {}
        mat.diffuseTexture = texture;
        try { mat.opacityTexture = texture; } catch {}
      }
      mat.useAlphaFromDiffuseTexture = true;
      mat.emissiveColor = Color3.White();
      mat.backFaceCulling = false;
      mat.forceDepthWrite = false;
      try { (mat as any).transparencyMode = 2; } catch {}
      applyDecorationMaterialRenderBias(mat);
      return mat;
    } catch {
      return null;
    }
  };

  const createCornerContinuationPlane = (
    ownerId: string,
    label: string,
    suffix: string,
    center: Vector3,
    rightAxisInput: Vector3,
    upAxisInput: Vector3,
    width: number,
    height: number,
    material: StandardMaterial,
    options: { flipU?: boolean; flipV?: boolean; uvRect?: { uMin: number; uMax: number; vMin: number; vMax: number }; renderingGroupId?: number } = {},
  ): Mesh | null => {
    try {
      const sc = requireScene();
      let right = rightAxisInput.clone();
      let up = upAxisInput.clone();
      if (right.lengthSquared() < 0.0001 || up.lengthSquared() < 0.0001) return null;
      right.normalize();
      up.normalize();
      const safeWidth = Math.max(0.01, Math.abs(width));
      const safeHeight = Math.max(0.01, Math.abs(height));
      const hw = safeWidth / 2;
      const hh = safeHeight / 2;
      const p0 = center.subtract(right.scale(hw)).subtract(up.scale(hh));
      const p1 = center.add(right.scale(hw)).subtract(up.scale(hh));
      const p2 = center.add(right.scale(hw)).add(up.scale(hh));
      const p3 = center.subtract(right.scale(hw)).add(up.scale(hh));
      const mesh = new Mesh(`${ownerId}:corner:${suffix}`, sc);
      const vertexData = new VertexData();
      vertexData.positions = [
        p0.x, p0.y, p0.z,
        p1.x, p1.y, p1.z,
        p2.x, p2.y, p2.z,
        p3.x, p3.y, p3.z,
      ];
      vertexData.indices = [0, 1, 2, 0, 2, 3];
      const flipU = options.flipU === true;
      const flipV = options.flipV === true;
      const uvRect = options.uvRect;
      const rawUMin = uvRect ? uvRect.uMin : 0;
      const rawUMax = uvRect ? uvRect.uMax : 1;
      const rawVMin = uvRect ? uvRect.vMin : 0;
      const rawVMax = uvRect ? uvRect.vMax : 1;
      const uMin = flipU ? rawUMax : rawUMin;
      const uMax = flipU ? rawUMin : rawUMax;
      const vBottom = flipV ? rawVMin : rawVMax;
      const vTop = flipV ? rawVMax : rawVMin;
      vertexData.uvs = [uMin, vBottom, uMax, vBottom, uMax, vTop, uMin, vTop];
      const normal = Vector3.Cross(right, up);
      if (normal.lengthSquared() > 0.0001) normal.normalize();
      vertexData.normals = [
        normal.x, normal.y, normal.z,
        normal.x, normal.y, normal.z,
        normal.x, normal.y, normal.z,
        normal.x, normal.y, normal.z,
      ];
      vertexData.applyToMesh(mesh, true);
      mesh.id = `${ownerId}:corner:${suffix}`;
      mesh.name = label || ownerId;
      mesh.material = material;
      mesh.isPickable = true;
      try { mesh.renderingGroupId = Number.isFinite(Number(options.renderingGroupId)) ? Number(options.renderingGroupId) : 0; } catch {}
      try { mesh.refreshBoundingInfo(); } catch {}
      setDecorationFragmentMetadata(ownerId, mesh);
      return mesh;
    } catch {
      return null;
    }
  };

  const createCornerStickerFragments = (
    ownerId: string,
    name: string,
    baseMaterial: any,
    projection: { targetMesh: Mesh; position: Vector3; normal: Vector3; size: Vector3 },
    size: Vector3,
    angle = 0,
  ): Mesh[] => {
    const created: Mesh[] = [];
    try {
      const targetBounds = getMeshWorldBounds(projection.targetMesh);
      let normal = projection.normal.clone();
      if (normal.lengthSquared() < 0.0001) normal = new Vector3(0, 0, 1);
      normal.normalize();
      const basis = getProjectedStickerStableBasis(normal, angle);
      const intervalRight = getProjectedBoundsInterval(targetBounds, basis.right);
      const intervalUp = getProjectedBoundsInterval(targetBounds, basis.up);
      const intervalNormal = getProjectedBoundsInterval(targetBounds, normal);
      const centerRight = Vector3.Dot(projection.position, basis.right);
      const centerUp = Vector3.Dot(projection.position, basis.up);
      const halfX = Math.max(0.01, Math.abs(size.x) * 0.5);
      const halfY = Math.max(0.01, Math.abs(size.y) * 0.5);
      const rightOverflow = Math.max(0, centerRight + halfX - intervalRight.max);
      const leftOverflow = Math.max(0, intervalRight.min - (centerRight - halfX));
      const topOverflow = Math.max(0, centerUp + halfY - intervalUp.max);
      const bottomOverflow = Math.max(0, intervalUp.min - (centerUp - halfY));
      const trigger = Math.max(0.01, Math.min(Math.max(size.x, size.y) * 0.08, 0.035));
      const candidates = [
        { side: 'right', amount: rightOverflow, axis: 'x' },
        { side: 'left', amount: leftOverflow, axis: 'x' },
        { side: 'top', amount: topOverflow, axis: 'y' },
        { side: 'bottom', amount: bottomOverflow, axis: 'y' },
      ].filter((item) => item.amount > trigger).sort((a, b) => b.amount - a.amount);
      if (!candidates.length) return created;
      const chosen = candidates[0];
      const sideFaceDepth = Math.max(0.02, intervalNormal.max - intervalNormal.min);
      const maxContinuation = Math.max(
        0.018,
        Math.min(
          chosen.axis === 'x' ? size.x * 0.5 : size.y * 0.5,
          Math.max(0.02, sideFaceDepth - 0.006),
        ),
      );
      const overflow = Math.max(0.018, Math.min(chosen.amount, maxContinuation));
      const leftCut = chosen.side === 'left' ? overflow : 0;
      const rightCut = chosen.side === 'right' ? overflow : 0;
      const topCut = chosen.side === 'top' ? overflow : 0;
      const bottomCut = chosen.side === 'bottom' ? overflow : 0;
      const sourceWidth = Math.max(0.02, size.x - leftCut - rightCut);
      const sourceHeight = Math.max(0.02, size.y - topCut - bottomCut);
      const sourceAnchorCenter = projection.position
        .add(basis.right.scale((leftCut - rightCut) * 0.5))
        .add(basis.up.scale((bottomCut - topCut) * 0.5));
      const sourceSurfaceOffset = 0.002;
      const sourceCenter = sourceAnchorCenter.add(normal.scale(sourceSurfaceOffset));
      const sourceCrop = {
        uOffset: Math.max(0, leftCut / Math.max(size.x, 0.001)),
        vOffset: Math.max(0, topCut / Math.max(size.y, 0.001)),
        uScale: Math.max(0.02, sourceWidth / Math.max(size.x, 0.001)),
        vScale: Math.max(0.02, sourceHeight / Math.max(size.y, 0.001)),
      };
      const sourceMaterial = makeDecalTextureFragmentMaterial(ownerId, baseMaterial, 'source', sourceCrop);
      if (!sourceMaterial) return created;
      const sourcePlane = createCornerContinuationPlane(ownerId, name, 'source', sourceCenter, basis.right, basis.up, sourceWidth, sourceHeight, sourceMaterial, { renderingGroupId: Number((projection.targetMesh as any)?.renderingGroupId ?? 0) });
      if (!sourcePlane) {
        try { sourceMaterial.dispose?.(false, true); } catch {}
        return created;
      }
      sourcePlane.id = ownerId;
      (sourcePlane as any).__asukaDecorationProxyFor = '';
      setDecalRuntimeMetadata(sourcePlane, projection.targetMesh, size, projection.normal, 0, projection.position);
      created.push(sourcePlane);

      const sourceRightEdgeCenter = sourceAnchorCenter.add(basis.right.scale(sourceWidth * 0.5));
      const sourceLeftEdgeCenter = sourceAnchorCenter.subtract(basis.right.scale(sourceWidth * 0.5));
      const sourceTopEdgeCenter = sourceAnchorCenter.add(basis.up.scale(sourceHeight * 0.5));
      const sourceBottomEdgeCenter = sourceAnchorCenter.subtract(basis.up.scale(sourceHeight * 0.5));
      const surfaceOffset = 0.0015;
      const projectionNormalCoord = Vector3.Dot(projection.position, normal);
      const frontNormalCoord = Math.abs(projectionNormalCoord - intervalNormal.max) <= Math.abs(projectionNormalCoord - intervalNormal.min)
        ? intervalNormal.max
        : intervalNormal.min;
      const depthAxis = frontNormalCoord === intervalNormal.max ? normal.scale(-1) : normal.clone();
      const snapToFrontPlane = (point: Vector3) => point.add(normal.scale(frontNormalCoord - Vector3.Dot(point, normal)));

      const addAdjacent = (
        suffix: string,
        planeCenter: Vector3,
        rightAxis: Vector3,
        upAxis: Vector3,
        width: number,
        height: number,
        crop: { uOffset: number; vOffset: number; uScale: number; vScale: number },
        options: { flipU?: boolean; flipV?: boolean } = {},
      ) => {
        const mat = makeDecalTextureCloneMaterial(ownerId, baseMaterial, suffix);
        if (!mat) return;
        const uvRect = {
          uMin: Math.max(0, Math.min(1, crop.uOffset)),
          uMax: Math.max(0, Math.min(1, crop.uOffset + crop.uScale)),
          vMin: Math.max(0, Math.min(1, crop.vOffset)),
          vMax: Math.max(0, Math.min(1, crop.vOffset + crop.vScale)),
        };
        const plane = createCornerContinuationPlane(ownerId, name, suffix, planeCenter, rightAxis, upAxis, width, height, mat, { ...options, uvRect, renderingGroupId: Number((projection.targetMesh as any)?.renderingGroupId ?? 0) });
        if (plane) {
          setDecalRuntimeMetadata(plane, projection.targetMesh, size, projection.normal, 0, projection.position);
          created.push(plane);
        } else {
          try { mat.dispose?.(false, true); } catch {}
        }
      };

      if (chosen.side === 'right') {
        const ratio = Math.max(0.02, overflow / Math.max(size.x, 0.001));
        const sideNormal = basis.right.clone().normalize();
        const seam = snapToFrontPlane(sourceRightEdgeCenter);
        const center = seam
          .add(depthAxis.scale(overflow * 0.5))
          .add(sideNormal.scale(surfaceOffset));
        addAdjacent('right', center, depthAxis, basis.up, overflow, sourceHeight, {
          uOffset: Math.max(0, 1 - ratio),
          vOffset: sourceCrop.vOffset,
          uScale: ratio,
          vScale: sourceCrop.vScale,
        });
      } else if (chosen.side === 'left') {
        const ratio = Math.max(0.02, overflow / Math.max(size.x, 0.001));
        const sideNormal = basis.right.clone().scale(-1).normalize();
        const seam = snapToFrontPlane(sourceLeftEdgeCenter);
        const center = seam
          .add(depthAxis.scale(overflow * 0.5))
          .add(sideNormal.scale(surfaceOffset));
        addAdjacent('left', center, depthAxis.scale(-1), basis.up, overflow, sourceHeight, {
          uOffset: 0,
          vOffset: sourceCrop.vOffset,
          uScale: ratio,
          vScale: sourceCrop.vScale,
        }, { flipV: true });
      } else if (chosen.side === 'top') {
        const ratio = Math.max(0.02, overflow / Math.max(size.y, 0.001));
        const sideNormal = basis.up.clone().normalize();
        const seam = snapToFrontPlane(sourceTopEdgeCenter);
        const center = seam
          .add(depthAxis.scale(overflow * 0.5))
          .add(sideNormal.scale(surfaceOffset));
        addAdjacent('top', center, basis.right, depthAxis, sourceWidth, overflow, {
          uOffset: sourceCrop.uOffset,
          vOffset: 0,
          uScale: sourceCrop.uScale,
          vScale: ratio,
        });
      } else if (chosen.side === 'bottom') {
        const ratio = Math.max(0.02, overflow / Math.max(size.y, 0.001));
        const sideNormal = basis.up.clone().scale(-1).normalize();
        const seam = snapToFrontPlane(sourceBottomEdgeCenter);
        const center = seam
          .add(depthAxis.scale(overflow * 0.5))
          .add(sideNormal.scale(surfaceOffset));
        addAdjacent('bottom', center, basis.right, depthAxis.scale(-1), sourceWidth, overflow, {
          uOffset: sourceCrop.uOffset,
          vOffset: Math.max(0, 1 - ratio),
          uScale: sourceCrop.uScale,
          vScale: ratio,
        }, { flipV: true });
      }
    } catch {}
    return created;
  };

  const ensureProjectedStickerShaderRegistered = () => {
    if (!Effect.ShadersStore['asukaProjectedStickerVertexShader']
      || !(Effect.ShadersStore['asukaProjectedStickerVertexShader'] as string).includes('TARGET_BOUND_SURFACE_SHELL_V1')) {
      Effect.ShadersStore['asukaProjectedStickerVertexShader'] = `
        precision highp float;
        attribute vec3 position;
        attribute vec3 normal;
        uniform mat4 world;
        uniform mat4 worldViewProjection;
        uniform mat4 viewProjection;
        uniform vec3 uStickerNormal;
        uniform float uSurfaceOffset;
        varying vec3 vWorldPosition;
        varying vec3 vWorldNormal;
        void main(void) {
          vec4 wp = world * vec4(position, 1.0);
          vec3 worldNormal = normalize((world * vec4(normal, 0.0)).xyz);
          // TARGET_BOUND_SURFACE_SHELL_V1
          // The projected texture is rendered on a clone of the product mesh.
          // Keeping both surfaces exactly coplanar makes the depth buffer choose
          // different fragments as the camera moves. Offset only the rendered
          // shell along the normal that faces the selected print surface; the
          // logical projection coordinates keep using the untouched position.
          float shellDirection = dot(worldNormal, normalize(uStickerNormal)) >= 0.0 ? 1.0 : -1.0;
          vec3 renderedWorldPosition = wp.xyz + worldNormal * shellDirection * uSurfaceOffset;
          vWorldPosition = wp.xyz;
          vWorldNormal = worldNormal;
          gl_Position = viewProjection * vec4(renderedWorldPosition, 1.0);
        }
      `;
    }
    if (!Effect.ShadersStore['asukaProjectedStickerFragmentShader']
      || !(Effect.ShadersStore['asukaProjectedStickerFragmentShader'] as string).includes('uFlipU')
      || !(Effect.ShadersStore['asukaProjectedStickerFragmentShader'] as string).includes('uTargetBound')
      || !(Effect.ShadersStore['asukaProjectedStickerFragmentShader'] as string).includes('TARGET_BOUND_CURVED_SURFACE_V3')) {
      Effect.ShadersStore['asukaProjectedStickerFragmentShader'] = `
        precision highp float;
        varying vec3 vWorldPosition;
        varying vec3 vWorldNormal;
        uniform vec3 uStickerCenter;
        uniform vec3 uStickerRight;
        uniform vec3 uStickerUp;
        uniform vec3 uStickerNormal;
        uniform vec3 uStickerSize;
        uniform vec3 uCameraPosition;
        uniform float uWrapSide;
        uniform float uEdgeLocal;
        uniform float uStickerBackDepth;
        uniform vec4 uStickerEdges;
        uniform float uFlipU;
        uniform float uTargetBound;
        uniform sampler2D uSampler;

        float insideRange(float value, float minValue, float maxValue) {
          return step(minValue, value) * step(value, maxValue);
        }

        void main(void) {
          vec3 rightAxis = normalize(uStickerRight);
          vec3 upAxis = normalize(uStickerUp);
          vec3 normalAxis = normalize(uStickerNormal);
          vec3 delta = vWorldPosition - uStickerCenter;
          float lx = dot(delta, rightAxis);
          float ly = dot(delta, upAxis);
          float ld = dot(delta, normalAxis);
          float width = max(uStickerSize.x, 0.0001);
          float height = max(uStickerSize.y, 0.0001);
          float depth = max(uStickerSize.z, 0.012);
          float halfW = width * 0.5;
          float halfH = height * 0.5;
          // Keep the side band wide enough for real box/cuboid edges where
          // vertices are not perfectly coplanar after GLB transforms. A band that
          // is too thin lets the permissive front projection take over on the
          // adjacent face, which creates the visible seam break/trail at the edge.
          float sideBand = max(0.018, min(max(width, height) * 0.045, 0.09));
          float normalScore = dot(normalize(vWorldNormal), normalAxis);
          float rightScore = dot(normalize(vWorldNormal), rightAxis);
          float upScore = dot(normalize(vWorldNormal), upAxis);
          float depthBehind = max(-ld, 0.0);
          // TARGET_BOUND_CURVED_SURFACE_V3
          // A mesh-scoped Drawing Area is a local surface projection, even when
          // its target is one closed mesh. Restrict it to the nearby normal lobe;
          // otherwise the projector volume also paints the far side of a shoe.
          float targetBound = step(0.5, uTargetBound);
          // The sticker is projected onto the visible product surface, not onto
          // a mathematically thin front plane. Shoes, seams and laces can move
          // many triangles several centimeters behind the initial picked point;
          // if the shader clips by a small absolute depth, the design looks cut
          // even when the material is rendered above the mesh. Keep the X/Y
          // sticker rectangle strict, but let visible curved surface triangles
          // inside that rectangle receive the texture.
          float broadFrontBehindDepth = max(uStickerBackDepth * 0.92, max(depth * 2.2, max(0.10, max(width, height) * 0.30)));
          float targetSurfaceDepth = max(depth * 1.6, max(0.035, max(width, height) * ${TARGET_BOUND_STICKER_RENDER_POLICY.surfaceDepthRatio.toFixed(2)}));
          float frontBehindDepth = mix(broadFrontBehindDepth, min(broadFrontBehindDepth, targetSurfaceDepth), targetBound);
          float frontForwardDepth = max(depth * 0.55, max(0.035, max(width, height) * 0.045));
          // Curved cloth can locally turn away from the original picked normal.
          // Keep rejecting the opposite side, but retain folds that still belong
          // to the explicitly targeted mesh surface.
          float frontNormalThreshold = mix(-0.42, ${TARGET_BOUND_STICKER_RENDER_POLICY.surfaceNormalThreshold.toFixed(2)}, targetBound);
          // Side continuation must stay enabled for every edge where the sticker
          // actually overflows. Do not gate it with a single dominant wrap side:
          // a design can touch two adjacent faces at once. Instead, reject only
          // true perpendicular faces from the front projection and let the side
          // mapping own those pixels. This keeps continuity while cutting the
          // stray front-projection trails on box-like products.
          float sideNormalThreshold = 0.10;
          float allowFaceWrap = mix(
            1.0,
            ${TARGET_BOUND_STICKER_RENDER_POLICY.wrapAcrossOppositeFaces ? '1.0' : '0.0'},
            targetBound
          );
          float perpendicularScore = max(abs(rightScore), abs(upScore));
          float trueAdjacentFace = step(0.72, perpendicularScore) * (1.0 - step(0.24, normalScore));
          float rejectAdjacentFace = trueAdjacentFace * mix(
            1.0,
            ${TARGET_BOUND_STICKER_RENDER_POLICY.rejectAdjacentFaces ? '1.0' : '0.0'},
            targetBound
          );
          vec3 viewAxis = uCameraPosition - vWorldPosition;
          float viewDistance = length(viewAxis);
          float cameraFacing = viewDistance > 0.0001 ? dot(normalize(vWorldNormal), normalize(viewAxis)) : 1.0;
          if (uTargetBound < 0.5 && cameraFacing <= -0.08) discard;
          vec2 uv = vec2(0.0);
          float inside = 0.0;

          float rightEdge = uStickerEdges.x;
          float leftEdge = uStickerEdges.y;
          float topEdge = uStickerEdges.z;
          float bottomEdge = uStickerEdges.w;

          float backDepth = max(uStickerBackDepth, 0.001);
          float backBand = max(depth * 0.30, 0.012);
          float visibleRight = clamp(rightEdge + halfW, 0.0, width);
          float remainRight = width - visibleRight;
          float rightMask = insideRange(abs(lx - rightEdge), 0.0, sideBand)
            * insideRange(ly, -halfH, halfH)
            * insideRange(depthBehind, 0.0, max(remainRight, 0.0))
            * step(sideNormalThreshold, rightScore)
            * allowFaceWrap;
          if (rightMask > 0.5 && remainRight > 0.001) {
            uv = vec2((visibleRight + depthBehind) / width, 0.5 + (ly / height));
            inside = 1.0;
          }

          float visibleLeft = clamp(leftEdge + halfW, 0.0, width);
          float leftMask = insideRange(abs(lx - leftEdge), 0.0, sideBand)
            * insideRange(ly, -halfH, halfH)
            * insideRange(depthBehind, 0.0, max(visibleLeft, 0.0))
            * step(sideNormalThreshold, -rightScore)
            * allowFaceWrap;
          if (leftMask > 0.5 && visibleLeft > 0.001) {
            uv = vec2((visibleLeft - depthBehind) / width, 0.5 + (ly / height));
            inside = 1.0;
          }

          float visibleTop = clamp(topEdge + halfH, 0.0, height);
          float remainTop = height - visibleTop;
          float topMask = insideRange(abs(ly - topEdge), 0.0, sideBand)
            * insideRange(lx, -halfW, halfW)
            * insideRange(depthBehind, 0.0, max(remainTop, 0.0))
            * step(sideNormalThreshold, upScore)
            * allowFaceWrap;
          if (topMask > 0.5 && remainTop > 0.001) {
            uv = vec2((lx + halfW) / width, (visibleTop + depthBehind) / height);
            inside = 1.0;
          }

          float visibleBottom = clamp(bottomEdge + halfH, 0.0, height);
          float bottomMask = insideRange(abs(ly - bottomEdge), 0.0, sideBand)
            * insideRange(lx, -halfW, halfW)
            * insideRange(depthBehind, 0.0, max(visibleBottom, 0.0))
            * step(sideNormalThreshold, -upScore)
            * allowFaceWrap;
          if (bottomMask > 0.5 && visibleBottom > 0.001) {
            uv = vec2((lx + halfW) / width, (visibleBottom - depthBehind) / height);
            inside = 1.0;
          }

          float sideMask = max(max(rightMask, leftMask), max(topMask, bottomMask));
          float frontMask = insideRange(lx, -halfW, halfW)
            * insideRange(ly, -halfH, halfH)
            * insideRange(ld, -frontBehindDepth, frontForwardDepth)
            * step(frontNormalThreshold, normalScore)
            * (1.0 - rejectAdjacentFace);
          if (frontMask > 0.5 && sideMask < 0.5) {
            uv = vec2((lx + halfW) / width, 0.5 + (ly / height));
            inside = 1.0;
          }

          // Third-face continuation: once the sticker has consumed the source
          // face plus the adjacent side depth, keep projecting the remaining
          // slice on the opposite/back face. This preserves the single logical
          // projected-sticker shader while avoiding destructive CPU triangle
          // filtering or manual plane fragments. The camera-facing discard above
          // still prevents this back slice from being visible through the model.
          float backPlaneMask = insideRange(abs(ld + backDepth), 0.0, backBand)
            * step(0.35, -normalScore)
            * allowFaceWrap;

          float rightBackStart = visibleRight + backDepth;
          float rightBackRemain = width - rightBackStart;
          float rightBackMask = backPlaneMask
            * insideRange(ly, -halfH, halfH)
            * insideRange(lx, rightEdge - max(rightBackRemain, 0.0), rightEdge);
          if (rightBackMask > 0.5 && rightBackRemain > 0.001) {
            uv = vec2((rightBackStart + (rightEdge - lx)) / width, 0.5 + (ly / height));
            inside = 1.0;
          }

          float leftBackStart = visibleLeft - backDepth;
          float leftBackRemain = leftBackStart;
          float leftBackMask = backPlaneMask
            * insideRange(ly, -halfH, halfH)
            * insideRange(lx, leftEdge, leftEdge + max(leftBackRemain, 0.0));
          if (leftBackMask > 0.5 && leftBackRemain > 0.001) {
            uv = vec2((leftBackStart - (lx - leftEdge)) / width, 0.5 + (ly / height));
            inside = 1.0;
          }

          float topBackStart = visibleTop + backDepth;
          float topBackRemain = height - topBackStart;
          float topBackMask = backPlaneMask
            * insideRange(lx, -halfW, halfW)
            * insideRange(ly, topEdge - max(topBackRemain, 0.0), topEdge);
          if (topBackMask > 0.5 && topBackRemain > 0.001) {
            uv = vec2((lx + halfW) / width, (topBackStart + (topEdge - ly)) / height);
            inside = 1.0;
          }

          float bottomBackStart = visibleBottom - backDepth;
          float bottomBackRemain = bottomBackStart;
          float bottomBackMask = backPlaneMask
            * insideRange(lx, -halfW, halfW)
            * insideRange(ly, bottomEdge, bottomEdge + max(bottomBackRemain, 0.0));
          if (bottomBackMask > 0.5 && bottomBackRemain > 0.001) {
            uv = vec2((lx + halfW) / width, (bottomBackStart - (ly - bottomEdge)) / height);
            inside = 1.0;
          }

          if (inside < 0.5 || uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) discard;
          // uFlipU == 0 (default): normal points away from camera → local +U axis
          // points world-left → flip sampling X to restore correct orientation.
          // uFlipU == 1: normal points toward camera (3D slot) → rightAxis already
          // points world-right → sample without flip.
          vec2 sampleUv = uFlipU > 0.5 ? vec2(uv.x, uv.y) : vec2(1.0 - uv.x, uv.y);
          vec4 tex = texture2D(uSampler, sampleUv);
          if (tex.a < 0.01) discard;
          gl_FragColor = tex;
        }
      `;
    }
  };

  const getProjectedStickerStableBasis = (normalInput: Vector3, angle = 0): { right: Vector3; up: Vector3 } => {
    let normal = normalInput.clone();
    if (normal.lengthSquared() < 0.0001) normal = new Vector3(0, 0, 1);
    normal.normalize();
    let up = new Vector3(0, 1, 0).subtract(normal.scale(Vector3.Dot(new Vector3(0, 1, 0), normal)));
    if (up.lengthSquared() < 0.0001) {
      up = new Vector3(0, 0, 1).subtract(normal.scale(Vector3.Dot(new Vector3(0, 0, 1), normal)));
    }
    if (up.lengthSquared() < 0.0001) up = getSurfaceTangentBasis(normal).up;
    up.normalize();
    let right = Vector3.Cross(up, normal);
    if (right.lengthSquared() < 0.0001) right = getSurfaceTangentBasis(normal).right;
    right.normalize();
    up = Vector3.Cross(normal, right);
    if (up.lengthSquared() < 0.0001) up = getSurfaceTangentBasis(normal).up;
    up.normalize();
    const safeAngle = Number(angle) || 0;
    if (safeAngle) {
      right = rotateVectorAroundAxis(right, normal, safeAngle);
      up = rotateVectorAroundAxis(up, normal, safeAngle);
      if (right.lengthSquared() > 0.0001) right.normalize();
      if (up.lengthSquared() > 0.0001) up.normalize();
    }
    return { right, up };
  };

  const getProjectedStickerAngleForSurfaceFrame = (normalInput: Vector3, surfaceRightInput?: Vector3 | null, surfaceUpInput?: Vector3 | null): number | null => {
    if (!surfaceRightInput || surfaceRightInput.lengthSquared() < 0.0001) return null;
    let normal = normalInput.clone();
    if (normal.lengthSquared() < 0.0001) normal = new Vector3(0, 0, 1);
    normal.normalize();
    let surfaceRight = surfaceRightInput.subtract(normal.scale(Vector3.Dot(surfaceRightInput, normal)));
    if (surfaceRight.lengthSquared() < 0.0001) return null;
    surfaceRight.normalize();
    let surfaceUp = surfaceUpInput && surfaceUpInput.lengthSquared() > 0.0001
      ? surfaceUpInput.subtract(normal.scale(Vector3.Dot(surfaceUpInput, normal)))
      : null;
    if (surfaceUp && surfaceUp.lengthSquared() > 0.0001) surfaceUp.normalize();
    else surfaceUp = null;
    const baseRight = getProjectedStickerStableBasis(normal, 0).right;
    const sin = Vector3.Dot(Vector3.Cross(baseRight, surfaceRight), normal);
    const cos = Vector3.Dot(baseRight, surfaceRight);
    let angle = Math.atan2(sin, cos);
    if (surfaceUp) {
      const basis = getProjectedStickerStableBasis(normal, angle);
      if (Vector3.Dot(basis.up, surfaceUp) < 0) angle += Math.PI;
      return angle;
    }
    if (camera) {
      const basis = getProjectedStickerStableBasis(normal, angle);
      const viewBasis = getViewTransformBasis();
      const viewUp = viewBasis.up.subtract(normal.scale(Vector3.Dot(viewBasis.up, normal)));
      if (viewUp.lengthSquared() > 0.0001) {
        viewUp.normalize();
        if (Vector3.Dot(basis.up, viewUp) < 0) angle += Math.PI;
      }
    }
    return angle;
  };

  const getProjectionSurfaceAngle = (projection: { normal: Vector3; surfaceRight?: Vector3 | null; surfaceUp?: Vector3 | null; angle?: number | null }): number => {
    const surfaceAngle = getProjectedStickerAngleForSurfaceFrame(projection.normal, (projection as any).surfaceRight ?? null, (projection as any).surfaceUp ?? null);
    if (typeof surfaceAngle === 'number' && Number.isFinite(surfaceAngle)) return surfaceAngle;
    return Number((projection as any).angle ?? 0) || 0;
  };

  const getProjectedStickerWrapInfo = (projection: { targetMesh: Mesh; position: Vector3; normal: Vector3; size: Vector3 }, size: Vector3, angle = 0) => {
    const targetBounds = getMeshWorldBounds(projection.targetMesh);
    let normal = projection.normal.clone();
    if (normal.lengthSquared() < 0.0001) normal = new Vector3(0, 0, 1);
    normal.normalize();
    const basis = getProjectedStickerStableBasis(normal, angle);
    const intervalRight = getProjectedBoundsInterval(targetBounds, basis.right);
    const intervalUp = getProjectedBoundsInterval(targetBounds, basis.up);
    const intervalNormal = getProjectedBoundsInterval(targetBounds, normal);
    const centerRight = Vector3.Dot(projection.position, basis.right);
    const centerUp = Vector3.Dot(projection.position, basis.up);
    const centerNormal = Vector3.Dot(projection.position, normal);
    const halfX = Math.max(0.01, Math.abs(size.x) * 0.5);
    const halfY = Math.max(0.01, Math.abs(size.y) * 0.5);
    const rightEdgeLocal = intervalRight.max - centerRight;
    const leftEdgeLocal = intervalRight.min - centerRight;
    const topEdgeLocal = intervalUp.max - centerUp;
    const bottomEdgeLocal = intervalUp.min - centerUp;
    const candidates = [
      { side: 1, amount: Math.max(0, halfX - rightEdgeLocal), edge: rightEdgeLocal },
      { side: -1, amount: Math.max(0, leftEdgeLocal + halfX), edge: leftEdgeLocal },
      { side: 2, amount: Math.max(0, halfY - topEdgeLocal), edge: topEdgeLocal },
      { side: -2, amount: Math.max(0, bottomEdgeLocal + halfY), edge: bottomEdgeLocal },
    ];
    const trigger = Math.max(0.008, Math.min(Math.max(size.x, size.y) * 0.06, 0.028));
    const best = candidates.filter((candidate) => candidate.amount > trigger).sort((a, b) => b.amount - a.amount)[0];
    return {
      basis,
      normal,
      wrapSide: best ? best.side : 0,
      edgeLocal: best ? best.edge : 0,
      backDepth: Math.max(0.02, centerNormal - intervalNormal.min),
      edges: { right: rightEdgeLocal, left: leftEdgeLocal, top: topEdgeLocal, bottom: bottomEdgeLocal },
    };
  };

  const createProjectedStickerControlMesh = (
    ownerId: string,
    name: string,
    projection: { targetMesh: Mesh; position: Vector3; normal: Vector3; size: Vector3 },
    size: Vector3,
    basis: { right: Vector3; up: Vector3 },
    angle = 0,
  ): Mesh | null => {
    try {
      const sc = requireScene();
      const center = projection.position.add(projection.normal.scale(0.003));
      const width = Math.max(0.02, Math.abs(size.x));
      const height = Math.max(0.02, Math.abs(size.y));
      const right = basis.right.clone().normalize();
      const up = basis.up.clone().normalize();
      const hw = width / 2;
      const hh = height / 2;
      const p0 = center.subtract(right.scale(hw)).subtract(up.scale(hh));
      const p1 = center.add(right.scale(hw)).subtract(up.scale(hh));
      const p2 = center.add(right.scale(hw)).add(up.scale(hh));
      const p3 = center.subtract(right.scale(hw)).add(up.scale(hh));
      const mesh = new Mesh(ownerId, sc);
      mesh.id = ownerId;
      mesh.name = name || ownerId;
      const vertexData = new VertexData();
      vertexData.positions = [p0.x, p0.y, p0.z, p1.x, p1.y, p1.z, p2.x, p2.y, p2.z, p3.x, p3.y, p3.z];
      vertexData.indices = [0, 1, 2, 0, 2, 3];
      vertexData.uvs = [0, 1, 1, 1, 1, 0, 0, 0];
      const planeNormal = Vector3.Cross(right, up).normalize();
      vertexData.normals = [planeNormal.x, planeNormal.y, planeNormal.z, planeNormal.x, planeNormal.y, planeNormal.z, planeNormal.x, planeNormal.y, planeNormal.z, planeNormal.x, planeNormal.y, planeNormal.z];
      vertexData.applyToMesh(mesh, true);
      const mat = new StandardMaterial(`${ownerId}-projected-sticker-control-material`, sc);
      (mat as any).__asukaRuntimeProjectedStickerMaterial = true;
      mat.diffuseColor = Color3.White();
      // This mesh is a picking/control proxy only. It used to render with a very
      // low alpha, but on some engines that proxy became the exact black rectangle
      // visible when adding a design. Disable color writes entirely: the actual
      // sticker is rendered by the projected shader fragments below.
      mat.alpha = 0;
      mat.backFaceCulling = false;
      mat.disableLighting = true;
      mat.forceDepthWrite = false;
      try { (mat as any).transparencyMode = 2; } catch {}
      try { (mat as any).disableColorWrite = true; } catch {}
      try { (mat as any).needDepthPrePass = false; } catch {}
      mesh.material = mat;
      mesh.isPickable = true;
      try { mesh.visibility = 0; } catch {}
      try { (mesh as any).__asukaInvisibleControlMesh = true; } catch {}
      try { mesh.renderingGroupId = Number((projection.targetMesh as any)?.renderingGroupId ?? 0); } catch {}
      setDecalRuntimeMetadata(mesh, projection.targetMesh, size, projection.normal, angle, projection.position, basis.right, basis.up);
      (mesh as any).__asukaProjectionMode = 'decal';
      return mesh;
    } catch {
      return null;
    }
  };

  type SurfacePatchVertex = {
    position: Vector3;
    normal: Vector3;
    u: number;
    v: number;
    depth: number;
  };

  type SurfacePatchCandidate = {
    face: number;
    vertexKeys: number[];
    pointKeys: string[];
    vertices: SurfacePatchVertex[];
    normal: Vector3;
    score: number;
  };

  const interpolateSurfacePatchVertex = (a: SurfacePatchVertex, b: SurfacePatchVertex, tInput: number): SurfacePatchVertex => {
    const t = Math.max(0, Math.min(1, Number(tInput) || 0));
    const normal = a.normal.scale(1 - t).add(b.normal.scale(t));
    if (normal.lengthSquared() < 0.000001) normal.copyFrom(a.normal);
    if (normal.lengthSquared() < 0.000001) normal.copyFrom(new Vector3(0, 0, 1));
    normal.normalize();
    return {
      position: a.position.scale(1 - t).add(b.position.scale(t)),
      normal,
      u: a.u + (b.u - a.u) * t,
      v: a.v + (b.v - a.v) * t,
      depth: a.depth + (b.depth - a.depth) * t,
    };
  };

  const clipSurfacePatchPolygon = (
    input: SurfacePatchVertex[],
    axis: 'u' | 'v',
    limit: number,
    keepGreater: boolean,
  ): SurfacePatchVertex[] => {
    if (!input.length) return [];
    const output: SurfacePatchVertex[] = [];
    const valueOf = (vertex: SurfacePatchVertex) => axis === 'u' ? vertex.u : vertex.v;
    const isInside = (vertex: SurfacePatchVertex) => keepGreater ? valueOf(vertex) >= limit - 1e-7 : valueOf(vertex) <= limit + 1e-7;
    for (let i = 0; i < input.length; i += 1) {
      const current = input[i];
      const previous = input[(i + input.length - 1) % input.length];
      const currentInside = isInside(current);
      const previousInside = isInside(previous);
      if (currentInside !== previousInside) {
        const from = valueOf(previous);
        const to = valueOf(current);
        const denominator = to - from;
        const t = Math.abs(denominator) > 1e-9 ? (limit - from) / denominator : 0;
        output.push(interpolateSurfacePatchVertex(previous, current, t));
      }
      if (currentInside) output.push(current);
    }
    return output;
  };

  const sortSurfacePatchPolygonByUv = (vertices: SurfacePatchVertex[]): SurfacePatchVertex[] => {
    if (vertices.length <= 2) return vertices.slice();
    const centerU = vertices.reduce((total, vertex) => total + vertex.u, 0) / vertices.length;
    const centerV = vertices.reduce((total, vertex) => total + vertex.v, 0) / vertices.length;
    return vertices.slice().sort((a, b) => Math.atan2(a.v - centerV, a.u - centerU) - Math.atan2(b.v - centerV, b.u - centerU));
  };

  const clipSurfacePatchTriangleToStickerRect = (
    vertices: SurfacePatchVertex[],
    halfWidth: number,
    halfHeight: number,
  ): SurfacePatchVertex[] => {
    // Some GLB/primitives expose duplicated or reversed triangle windings per
    // face. Sutherland-Hodgman clipping expects a stable polygon order; sorting
    // in decal UV space keeps coplanar quad halves from being rejected only
    // because their source triangle order is reversed.
    let polygon = sortSurfacePatchPolygonByUv(vertices);
    polygon = clipSurfacePatchPolygon(polygon, 'u', -halfWidth, true);
    polygon = clipSurfacePatchPolygon(polygon, 'u', halfWidth, false);
    polygon = clipSurfacePatchPolygon(polygon, 'v', -halfHeight, true);
    polygon = clipSurfacePatchPolygon(polygon, 'v', halfHeight, false);
    return polygon.length >= 3 ? polygon : [];
  };

  const applySurfacePatchMaterialBias = (material: any) => {
    try {
      if (material instanceof StandardMaterial) {
        material.backFaceCulling = false;
        material.forceDepthWrite = false;
        material.disableLighting = true;
        material.useAlphaFromDiffuseTexture = true;
        try { (material as any).transparencyMode = 2; } catch {}
        // Keep the patch in the same rendering group as the product and use only
        // a small depth bias. The previous visible-plane fallback used a separate
        // group and a very aggressive bias, which made the image appear through
        // unrelated faces.
        try { (material as any).zOffset = -2; } catch {}
        try { (material as any).zOffsetUnits = -2; } catch {}
        try { (material.diffuseTexture as any).wrapU = Texture.CLAMP_ADDRESSMODE; } catch {}
        try { (material.diffuseTexture as any).wrapV = Texture.CLAMP_ADDRESSMODE; } catch {}
        try { (material.opacityTexture as any).wrapU = Texture.CLAMP_ADDRESSMODE; } catch {}
        try { (material.opacityTexture as any).wrapV = Texture.CLAMP_ADDRESSMODE; } catch {}
      }
    } catch {}
  };

  const getExplicitProjectionPrintAreaTargetMesh = (projection?: any): Mesh | null => {
    const area = projection?.__asukaPrintSurfaceArea ?? null;
    if (!area || isAutomaticProductSurfaceArea(area)) return null;
    const targetMeshId = String(
      area?.targetMeshId
      ?? split3DList(area?.targetMeshIds)[0]
      ?? area?.metadata?.printSurface3D?.targetMeshId
      ?? '',
    ).trim();
    if (!targetMeshId || targetMeshId.toLowerCase() === 'product') return null;
    return findModelMesh(targetMeshId) ?? null;
  };

  const applyExplicitPrintAreaDepthDiscipline = (mesh: Mesh | null | undefined, material: any, targetMesh: Mesh | null | undefined) => {
    if (!mesh || !targetMesh) return;
    try {
      const targetRenderingGroupId = Number((targetMesh as any)?.renderingGroupId ?? 0);
      mesh.renderingGroupId = Number.isFinite(targetRenderingGroupId) ? targetRenderingGroupId : 0;
    } catch {}
    try {
      if (material) {
        const targetBoundProjector = (material as any).__asukaTargetBoundProjectedStickerMaterial === true;
        if (targetBoundProjector) {
          // The overlay reuses the exact target-mesh vertices. LEQUAL lets it
          // pass on the target's own depth while every nearer product mesh keeps
          // occluding it. Any polygon offset would let the sticker leak through
          // adjacent meshes again as the camera angle changes.
          (material as any).zOffset = TARGET_BOUND_STICKER_RENDER_POLICY.zOffset;
          (material as any).zOffsetUnits = TARGET_BOUND_STICKER_RENDER_POLICY.zOffsetUnits;
          (material as any).depthFunction = Engine.LEQUAL;
          (material as any).forceDepthWrite = false;
          (material as any).needDepthPrePass = false;
          return;
        }
        // Use the smallest practical polygon offset: enough to prevent the
        // sticker from being swallowed by its own support mesh, but not enough
        // to make it win depth against neighbouring meshes such as the swoosh.
        (material as any).zOffset = -1;
        (material as any).zOffsetUnits = -1;
      }
    } catch {}
  };


  type DeclaredBoxFaceId = 'front' | 'back' | 'left' | 'right' | 'top' | 'bottom';

  const normalizeDeclaredBoxFaceId = (value: unknown, fallback: DeclaredBoxFaceId = 'front'): DeclaredBoxFaceId => {
    const key = String(value ?? '').trim().toLowerCase().replace(/[\s_]+/g, '-');
    if (key === 'front' || key === '+z' || key === 'z+' || key === 'face-front') return 'front';
    if (key === 'back' || key === '-z' || key === 'z-' || key === 'face-back') return 'back';
    if (key === 'left' || key === '-x' || key === 'x-' || key === 'face-left') return 'left';
    if (key === 'right' || key === '+x' || key === 'x+' || key === 'face-right') return 'right';
    if (key === 'top' || key === '+y' || key === 'y+' || key === 'face-top') return 'top';
    if (key === 'bottom' || key === '-y' || key === 'y-' || key === 'face-bottom') return 'bottom';
    return fallback;
  };

  const normalizeDeclaredBoxOrientation = (input: any = {}): { rotation: 0 | 90 | 180 | 270; flipX: boolean; flipY: boolean } => {
    const raw = input?.orientation && typeof input.orientation === 'object' ? input.orientation : input;
    const rawRotation = Number(raw?.rotation ?? raw?.rotationDeg ?? raw?.degrees ?? 0) || 0;
    const normalized = ((((Math.round(rawRotation / 90) * 90) % 360) + 360) % 360) as 0 | 90 | 180 | 270;
    return {
      rotation: normalized,
      flipX: raw?.flipX === true || raw?.mirrorX === true,
      flipY: raw?.flipY === true || raw?.mirrorY === true,
    };
  };

  const applyDeclaredBoxTextureOrientation = (u: number, v: number, orientation: { rotation: 0 | 90 | 180 | 270; flipX: boolean; flipY: boolean }): { u: number; v: number } => {
    let x = Math.max(0, Math.min(1, Number(u) || 0));
    let y = Math.max(0, Math.min(1, Number(v) || 0));
    if (orientation.flipX) x = 1 - x;
    if (orientation.flipY) y = 1 - y;
    if (orientation.rotation === 90) return { u: 1 - y, v: x };
    if (orientation.rotation === 180) return { u: 1 - x, v: 1 - y };
    if (orientation.rotation === 270) return { u: y, v: 1 - x };
    return { u: x, v: y };
  };

  const applyStickerSurfaceUvCorrection = (projection: any, u: number, v: number): { u: number; v: number } => {
    const x = Math.max(0, Math.min(1, Number(u) || 0));
    const y = Math.max(0, Math.min(1, Number(v) || 0));
    if (projection?.__asukaDisableSurfacePatchUvFlip === true) return { u: x, v: y };
    return { u: 1 - x, v: 1 - y };
  };

  const getDeclaredBoxFaceIdFromNormal = (normalInput?: Vector3 | null): DeclaredBoxFaceId => {
    const normal = normalInput?.clone?.() ?? new Vector3(0, 0, 1);
    if (normal.lengthSquared() < 0.0001) return 'front';
    normal.normalize();
    const ax = Math.abs(normal.x);
    const ay = Math.abs(normal.y);
    const az = Math.abs(normal.z);
    if (ax >= ay && ax >= az) return normal.x >= 0 ? 'right' : 'left';
    if (ay >= ax && ay >= az) return normal.y >= 0 ? 'top' : 'bottom';
    return normal.z >= 0 ? 'front' : 'back';
  };

  const getDeclaredBoxPrintSurfaceMetadata = (projection: any): { type: string; primaryFace: DeclaredBoxFaceId; faces: DeclaredBoxFaceId[]; orientation: { rotation: 0 | 90 | 180 | 270; flipX: boolean; flipY: boolean } } | null => {
    const area = projection?.__asukaPrintSurfaceArea ?? null;
    const raw = area?.metadata?.printSurface3D ?? area?.printSurface3D ?? area?.metadata?.printSurface ?? null;
    if (!raw || typeof raw !== 'object') return null;
    const type = String((raw as any).type ?? (raw as any).unwrapMode ?? '').trim().toLowerCase();
    if (!['face-net', 'box-unwrap', 'box', 'box-wrap'].includes(type)) return null;
    const primaryFace = normalizeDeclaredBoxFaceId((raw as any).primaryFace ?? (raw as any).seedFace ?? (raw as any).face, 'front');
    const sourceFaces = Array.isArray((raw as any).faces) ? (raw as any).faces : [primaryFace];
    const faces: DeclaredBoxFaceId[] = [];
    for (const item of sourceFaces) {
      const face = normalizeDeclaredBoxFaceId(item, primaryFace);
      if (!faces.includes(face)) faces.push(face);
    }
    if (!faces.includes(primaryFace)) faces.unshift(primaryFace);
    const orientation = normalizeDeclaredBoxOrientation((raw as any).orientation ?? raw);
    return { type: 'face-net', primaryFace, faces, orientation } as any;
  };



  type FaceNetTriangleRef = { meshId?: string; triangleIndex: number };
  type FaceNetPoint2 = { u: number; v: number };
  type FaceNetTriangleRuntime = {
    triangleIndex: number;
    vertexIndices: [number, number, number];
    vertexKeys: [string, string, string];
    local: [Vector3, Vector3, Vector3];
    world: [Vector3, Vector3, Vector3];
    normalLocal: Vector3;
    normalWorld: Vector3;
    centroidLocal: Vector3;
    centroidWorld: Vector3;
    net?: [FaceNetPoint2, FaceNetPoint2, FaceNetPoint2];
  };

  const getFaceNetMeshId = (mesh?: Mesh | null): string => String(mesh?.id || mesh?.name || '').trim() || 'product';

  const normalizeFaceNetTriangleRefs = (input: unknown, defaultMeshId = ''): FaceNetTriangleRef[] => {
    const source = Array.isArray(input) ? input : [];
    const out: FaceNetTriangleRef[] = [];
    const seen = new Set<string>();
    for (const item of source) {
      const rawIndex = typeof item === 'number' ? item : (item as any)?.triangleIndex ?? (item as any)?.faceId ?? (item as any)?.index;
      const triangleIndex = Number(rawIndex);
      if (!Number.isFinite(triangleIndex) || triangleIndex < 0) continue;
      const meshId = String(typeof item === 'object' && item ? ((item as any).meshId ?? (item as any).targetMeshId ?? (item as any).mesh) : defaultMeshId).trim() || defaultMeshId;
      const key = `${meshId || '*'}:${Math.floor(triangleIndex)}`;
      if (seen.has(key)) continue;
      seen.add(key);
      out.push({ ...(meshId ? { meshId } : null), triangleIndex: Math.floor(triangleIndex) });
    }
    return out;
  };

  const getDeclaredExactFaceNetPrintSurfaceMetadata = (projection: any): ({
    type: 'face-net';
    targetMeshId?: string;
    selectedTriangles: FaceNetTriangleRef[];
    primaryTriangleIndex?: number;
    orientation: { rotation: 0 | 90 | 180 | 270; flipX: boolean; flipY: boolean };
    authoredIn?: string;
  } | null) => {
    const area = projection?.__asukaPrintSurfaceArea ?? null;
    const raw = area?.metadata?.printSurface3D ?? area?.printSurface3D ?? area?.metadata?.printSurface ?? null;
    if (!raw || typeof raw !== 'object') return null;
    const type = String((raw as any).type ?? (raw as any).unwrapMode ?? '').trim().toLowerCase();
    if (type !== 'face-net') return null;
    const targetMeshId = String((raw as any).targetMeshId ?? area?.targetMeshId ?? area?.metadata?.targetMeshId ?? '').trim();
    const selectedTriangles = normalizeFaceNetTriangleRefs((raw as any).selectedTriangles ?? (raw as any).triangles ?? (raw as any).faceNetTriangles, targetMeshId);
    const primaryRaw = Number((raw as any).primaryTriangleIndex ?? (raw as any).seedTriangleIndex ?? selectedTriangles[0]?.triangleIndex);
    const primaryTriangleIndex = Number.isFinite(primaryRaw) && primaryRaw >= 0 ? Math.floor(primaryRaw) : undefined;
    return {
      type: 'face-net',
      ...(targetMeshId ? { targetMeshId } : null),
      selectedTriangles,
      ...(primaryTriangleIndex != null ? { primaryTriangleIndex } : null),
      orientation: normalizeDeclaredBoxOrientation((raw as any).orientation ?? raw),
      authoredIn: String((raw as any).authoredIn ?? '').trim() || undefined,
    };
  };


  const getExactFaceNetPrintSurfaceFromArea = (area?: Asuka3DPrintArea | null): any | null => {
    const raw = (area as any)?.metadata?.printSurface3D ?? (area as any)?.printSurface3D ?? (area as any)?.metadata?.printSurface ?? null;
    if (!raw || typeof raw !== 'object') return null;
    const type = String((raw as any).type ?? (raw as any).unwrapMode ?? '').trim().toLowerCase();
    return type === 'face-net' ? raw : null;
  };

  const isExactFaceNetPrintArea = (area?: Asuka3DPrintArea | null): boolean => !!getExactFaceNetPrintSurfaceFromArea(area);

  const getExactFaceNetAreaTargetMeshId = (area?: Asuka3DPrintArea | null): string => {
    const raw = getExactFaceNetPrintSurfaceFromArea(area);
    const selectedTriangles = normalizeFaceNetTriangleRefs(
      (raw as any)?.selectedTriangles ?? (raw as any)?.triangles ?? (raw as any)?.faceNetTriangles,
      String((raw as any)?.targetMeshId ?? (area as any)?.targetMeshId ?? (area as any)?.metadata?.targetMeshId ?? '').trim(),
    );
    const triangleMeshId = String(selectedTriangles[0]?.meshId ?? '').trim();
    if (triangleMeshId) return triangleMeshId;
    return String((raw as any)?.targetMeshId ?? (area as any)?.targetMeshId ?? (area as any)?.metadata?.targetMeshId ?? '').trim();
  };

  const getMeshFaceNetGeometry = (mesh: Mesh | null | undefined): { positions: ArrayLike<number>; indices: number[]; vertexCount: number; world: Matrix; inverseWorld: Matrix } | null => {
    if (!mesh || mesh.isDisposed?.()) return null;
    const positions = mesh.getVerticesData?.('position' as any) as ArrayLike<number> | null;
    if (!positions || positions.length < 9) return null;
    const vertexCount = Math.floor(positions.length / 3);
    const rawIndices = mesh.getIndices?.() as ArrayLike<number> | null;
    const indices = rawIndices && rawIndices.length >= 3
      ? Array.from(rawIndices as ArrayLike<number>, (value) => Number(value) || 0)
      : Array.from({ length: vertexCount }, (_, index) => index);
    if (indices.length < 3) return null;
    mesh.computeWorldMatrix?.(true);
    const world = mesh.getWorldMatrix?.() ?? Matrix.Identity();
    const inverseWorld = world.clone();
    try { inverseWorld.invert(); } catch {}
    return { positions, indices, vertexCount, world, inverseWorld };
  };

  const readFaceNetLocalVertex = (geometry: { positions: ArrayLike<number>; vertexCount: number }, indexInput: number): Vector3 | null => {
    const index = Math.floor(Number(indexInput) || 0);
    if (index < 0 || index >= geometry.vertexCount) return null;
    const offset = index * 3;
    if (offset + 2 >= geometry.positions.length) return null;
    return new Vector3(
      Number(geometry.positions[offset] ?? 0) || 0,
      Number(geometry.positions[offset + 1] ?? 0) || 0,
      Number(geometry.positions[offset + 2] ?? 0) || 0,
    );
  };

  const faceNetVertexKey = (point: Vector3): string => `${Math.round(point.x * 100000)}:${Math.round(point.y * 100000)}:${Math.round(point.z * 100000)}`;

  const getFaceNetTriangleRuntime = (mesh: Mesh, geometry: ReturnType<typeof getMeshFaceNetGeometry>, triangleIndexInput: number): FaceNetTriangleRuntime | null => {
    if (!geometry) return null;
    const triangleIndex = Math.floor(Number(triangleIndexInput) || 0);
    const base = triangleIndex * 3;
    if (base < 0 || base + 2 >= geometry.indices.length) return null;
    const vertexIndices: [number, number, number] = [geometry.indices[base], geometry.indices[base + 1], geometry.indices[base + 2]] as any;
    const a = readFaceNetLocalVertex(geometry, vertexIndices[0]);
    const b = readFaceNetLocalVertex(geometry, vertexIndices[1]);
    const c = readFaceNetLocalVertex(geometry, vertexIndices[2]);
    if (!a || !b || !c) return null;
    let normalLocal = Vector3.Cross(b.subtract(a), c.subtract(a));
    if (normalLocal.lengthSquared() < 0.0000001) return null;
    normalLocal.normalize();
    let normalWorld = Vector3.TransformNormal(normalLocal, geometry.world);
    if (normalWorld.lengthSquared() < 0.0000001) return null;
    normalWorld.normalize();
    const world: [Vector3, Vector3, Vector3] = [
      Vector3.TransformCoordinates(a, geometry.world),
      Vector3.TransformCoordinates(b, geometry.world),
      Vector3.TransformCoordinates(c, geometry.world),
    ];
    return {
      triangleIndex,
      vertexIndices,
      vertexKeys: [faceNetVertexKey(a), faceNetVertexKey(b), faceNetVertexKey(c)],
      local: [a, b, c],
      world,
      normalLocal,
      normalWorld,
      centroidLocal: a.add(b).add(c).scale(1 / 3),
      centroidWorld: world[0].add(world[1]).add(world[2]).scale(1 / 3),
    };
  };

  const getFaceNetTriangleComponentForPick = (mesh: Mesh | null | undefined, seedTriangleIndexInput: number, pickedNormalWorld?: Vector3 | null): FaceNetTriangleRef[] => {
    if (!mesh) return [];
    const geometry = getMeshFaceNetGeometry(mesh);
    if (!geometry) return [];
    const seedTriangleIndex = Math.floor(Number(seedTriangleIndexInput) || 0);
    const seed = getFaceNetTriangleRuntime(mesh, geometry, seedTriangleIndex);
    if (!seed) return [];
    const all = new Map<number, FaceNetTriangleRuntime>();
    const edgeOwners = new Map<string, number[]>();
    const edgeKey = (a: string, b: string): string => a < b ? `${a}|${b}` : `${b}|${a}`;
    const addEdge = (triangle: FaceNetTriangleRuntime, aIndex: number, bIndex: number) => {
      const key = edgeKey(triangle.vertexKeys[aIndex], triangle.vertexKeys[bIndex]);
      const list = edgeOwners.get(key) ?? [];
      list.push(triangle.triangleIndex);
      edgeOwners.set(key, list);
    };
    const totalTriangles = Math.floor(geometry.indices.length / 3);
    for (let triangleIndex = 0; triangleIndex < totalTriangles; triangleIndex += 1) {
      const triangle = getFaceNetTriangleRuntime(mesh, geometry, triangleIndex);
      if (!triangle) continue;
      all.set(triangleIndex, triangle);
      addEdge(triangle, 0, 1);
      addEdge(triangle, 1, 2);
      addEdge(triangle, 2, 0);
    }
    let seedNormal = seed.normalWorld.clone();
    if (pickedNormalWorld && pickedNormalWorld.lengthSquared?.() > 0.0001 && Vector3.Dot(seedNormal, pickedNormalWorld) < 0) seedNormal = seedNormal.scale(-1);
    const modelBounds = getMeshWorldBounds(mesh);
    const tolerance = Math.max(0.0008, modelBounds.size.length() * 0.0035);
    const queue = [seedTriangleIndex];
    const visited = new Set<number>([seedTriangleIndex]);
    const result: FaceNetTriangleRef[] = [];
    const meshId = getFaceNetMeshId(mesh);
    while (queue.length) {
      const currentIndex = queue.shift()!;
      const current = all.get(currentIndex);
      if (!current) continue;
      result.push({ meshId, triangleIndex: currentIndex });
      for (let edge = 0; edge < 3; edge += 1) {
        const aKey = current.vertexKeys[edge];
        const bKey = current.vertexKeys[(edge + 1) % 3];
        for (const nextIndex of edgeOwners.get(edgeKey(aKey, bKey)) ?? []) {
          if (visited.has(nextIndex)) continue;
          const next = all.get(nextIndex);
          if (!next) continue;
          const normalDot = Math.abs(Vector3.Dot(seed.normalWorld, next.normalWorld));
          if (normalDot < 0.985) continue;
          const planeDistance = Math.abs(Vector3.Dot(next.centroidWorld.subtract(seed.centroidWorld), seedNormal));
          if (planeDistance > tolerance) continue;
          visited.add(nextIndex);
          queue.push(nextIndex);
        }
      }
    }
    debug3DLayer('face-net:pick-component', {
      meshId,
      seedTriangleIndex,
      triangles: result.map((item) => item.triangleIndex),
      tolerance,
      normal: fromVec3(seedNormal),
    });
    return result;
  };

  const barycentricFromLocalTriangle = (point: Vector3, tri: [Vector3, Vector3, Vector3]): [number, number, number] | null => {
    const [a, b, c] = tri;
    const v0 = b.subtract(a);
    const v1 = c.subtract(a);
    const v2 = point.subtract(a);
    const d00 = Vector3.Dot(v0, v0);
    const d01 = Vector3.Dot(v0, v1);
    const d11 = Vector3.Dot(v1, v1);
    const d20 = Vector3.Dot(v2, v0);
    const d21 = Vector3.Dot(v2, v1);
    const denom = d00 * d11 - d01 * d01;
    if (Math.abs(denom) < 0.0000001) return null;
    const v = (d11 * d20 - d01 * d21) / denom;
    const w = (d00 * d21 - d01 * d20) / denom;
    const u = 1 - v - w;
    return [u, v, w];
  };

  const faceNetPointFromBarycentric = (bary: [number, number, number], tri: [FaceNetPoint2, FaceNetPoint2, FaceNetPoint2]): FaceNetPoint2 => ({
    u: tri[0].u * bary[0] + tri[1].u * bary[1] + tri[2].u * bary[2],
    v: tri[0].v * bary[0] + tri[1].v * bary[1] + tri[2].v * bary[2],
  });

  /**
   * Given a world-space hit point, find which of the declared selectedTriangles
   * best matches it in local mesh space.
   *
   * Babylon's scene.pickWithRay() faceId uses its own internal numbering which
   * does NOT match our FaceNetTriangleRef.triangleIndex. Passing the raw Babylon
   * faceId to createExactFaceNetPrintSurfaceStickerMesh means triangleByIndex.get()
   * always returns undefined → activeFaceTriangles collapses to primaryFace →
   * the image never moves. This helper bypasses that by doing our own raycast
   * against selectedTriangles in local space.
   */
  const resolveActiveFaceNetTriangle = (
    rawPositionWorld: Vector3,
    targetMesh: Mesh,
    refs: FaceNetTriangleRef[],
  ): number | null => {
    if (!refs.length) return null;
    const geometry = getMeshFaceNetGeometry(targetMesh);
    if (!geometry) return null;
    const targetMeshId = getFaceNetMeshId(targetMesh);
    const localPoint = Vector3.TransformCoordinates(rawPositionWorld, geometry.inverseWorld);
    let bestIndex: number | null = null;
    let bestScore = Infinity;
    for (const ref of refs) {
      const meshId = String(ref.meshId ?? targetMeshId).trim() || targetMeshId;
      if (meshId && meshId !== targetMeshId && meshId !== targetMesh.name && meshId !== targetMesh.id) continue;
      const triangle = getFaceNetTriangleRuntime(targetMesh, geometry, ref.triangleIndex);
      if (!triangle) continue;
      const bary = barycentricFromLocalTriangle(localPoint, triangle.local);
      if (!bary) continue;
      const outside = Math.max(0, -bary[0], -bary[1], -bary[2], bary[0] - 1, bary[1] - 1, bary[2] - 1);
      const planeDistance = Math.abs(Vector3.Dot(localPoint.subtract(triangle.local[0]), triangle.normalLocal));
      const score = outside * 1000 + planeDistance;
      if (score < bestScore) {
        bestScore = score;
        bestIndex = triangle.triangleIndex;
      }
    }
    return bestIndex;
  };

  const buildExactFaceNetRuntime = (targetMesh: Mesh, refs: FaceNetTriangleRef[], primaryTriangleIndex?: number): { triangles: FaceNetTriangleRuntime[]; bounds: { minU: number; maxU: number; minV: number; maxV: number }; disconnected: number } | null => {
    const geometry = getMeshFaceNetGeometry(targetMesh);
    if (!geometry) return null;
    const targetMeshId = getFaceNetMeshId(targetMesh);
    const selected = new Map<number, FaceNetTriangleRuntime>();
    for (const ref of refs) {
      const meshId = String(ref.meshId ?? targetMeshId).trim() || targetMeshId;
      if (meshId && meshId !== targetMeshId && meshId !== targetMesh.name && meshId !== targetMesh.id) continue;
      const triangle = getFaceNetTriangleRuntime(targetMesh, geometry, ref.triangleIndex);
      if (triangle) selected.set(triangle.triangleIndex, triangle);
    }
    if (!selected.size) return null;
    const edgeOwners = new Map<string, number[]>();
    const edgeKey = (a: string, b: string): string => a < b ? `${a}|${b}` : `${b}|${a}`;
    const addEdgeOwner = (triangle: FaceNetTriangleRuntime, aIndex: number, bIndex: number) => {
      const key = edgeKey(triangle.vertexKeys[aIndex], triangle.vertexKeys[bIndex]);
      const list = edgeOwners.get(key) ?? [];
      list.push(triangle.triangleIndex);
      edgeOwners.set(key, list);
    };
    for (const triangle of selected.values()) {
      addEdgeOwner(triangle, 0, 1);
      addEdgeOwner(triangle, 1, 2);
      addEdgeOwner(triangle, 2, 0);
    }
    const seed = selected.get(Number(primaryTriangleIndex)) ?? selected.values().next().value;
    if (!seed) return null;
    const netByVertex = new Map<string, FaceNetPoint2>();
    const seedA = seed.local[0];
    // Stable UV axes from the face normal + world Y, NOT from the seed triangle
    // edge. Using the edge would rotate the net 45° for diagonally-triangulated
    // quad faces (the most common case in Babylon.js box meshes).
    //   right = up × normal   (right-hand rule, U increases rightward)
    //   up    = Y − (Y·n)n    (world Y projected onto the face plane)
    const localNormal = seed.normalLocal.clone().normalize();
    // Transform world Y into LOCAL mesh space to handle rotated meshes
    // (e.g. Blender GLB exports where local Y ≠ world Y).
    const worldY = Vector3.TransformNormal(new Vector3(0, 1, 0), geometry.inverseWorld);
    if (worldY.lengthSquared() > 0.0001) worldY.normalize();
    let up = worldY.subtract(localNormal.scale(Vector3.Dot(worldY, localNormal)));
    if (up.lengthSquared() < 0.01) {
      // Face is top/bottom → fall back to world –Z in local space
      const worldNegZ = Vector3.TransformNormal(new Vector3(0, 0, -1), geometry.inverseWorld);
      if (worldNegZ.lengthSquared() > 0.0001) worldNegZ.normalize();
      const fallbackRef = worldNegZ;
      up = fallbackRef.subtract(localNormal.scale(Vector3.Dot(fallbackRef, localNormal)));
    }
    if (up.lengthSquared() < 0.0001) up = seed.local[1].subtract(seedA); // last resort
    up.normalize();
    // right = n × up (NOT up × n — that would reverse the direction for inward-facing normals)
    // n × up gives the correct 'rightward' direction when viewing the face from outside:
    //   n=(0,0,−1) front: (0,0,−1)×Y=(+X) → U increases rightward ✓
    //   n=(1,0, 0) right: (1,0, 0)×Y=(+Z) → U increases toward back ✓
    let right = Vector3.Cross(localNormal, up);
    if (right.lengthSquared() < 0.0001) {
      // Degenerate: fall back to seed edge
      right = seed.local[1].subtract(seedA);
      if (right.lengthSquared() < 0.0001) right = seed.local[2].subtract(seedA);
      if (right.lengthSquared() < 0.0001) return null;
    }
    right.normalize();
    // Recompute up for strict orthogonality
    up = Vector3.Cross(right, localNormal);
    if (up.lengthSquared() < 0.0001) up = new Vector3(0, 1, 0);
    up.normalize();
    const seedNet = seed.local.map((point) => ({ u: Vector3.Dot(point.subtract(seedA), right), v: Vector3.Dot(point.subtract(seedA), up) })) as [FaceNetPoint2, FaceNetPoint2, FaceNetPoint2];
    seed.net = seedNet;
    seed.vertexKeys.forEach((key, index) => netByVertex.set(key, seedNet[index]));

    const lineSide = (a: FaceNetPoint2, b: FaceNetPoint2, p: FaceNetPoint2): number => ((b.u - a.u) * (p.v - a.v)) - ((b.v - a.v) * (p.u - a.u));
    const placeThirdPoint = (p0: FaceNetPoint2, p1: FaceNetPoint2, d0: number, d1: number, sideSign: number): FaceNetPoint2 => {
      const dx = p1.u - p0.u;
      const dy = p1.v - p0.v;
      const length = Math.max(0.0000001, Math.sqrt(dx * dx + dy * dy));
      const x = Math.max(0, Math.min(length, ((d0 * d0) - (d1 * d1) + (length * length)) / (2 * length)));
      const h = Math.sqrt(Math.max(0, d0 * d0 - x * x));
      const ux = dx / length;
      const uy = dy / length;
      const sign = sideSign >= 0 ? 1 : -1;
      return { u: p0.u + ux * x + (-uy) * h * sign, v: p0.v + uy * x + ux * h * sign };
    };

    const visited = new Set<number>([seed.triangleIndex]);
    const queue = [seed.triangleIndex];
    let disconnected = 0;
    while (queue.length) {
      const currentIndex = queue.shift()!;
      const current = selected.get(currentIndex);
      if (!current?.net) continue;
      for (let edge = 0; edge < 3; edge += 1) {
        const aIndex = edge;
        const bIndex = (edge + 1) % 3;
        const aKey = current.vertexKeys[aIndex];
        const bKey = current.vertexKeys[bIndex];
        const owners = edgeOwners.get(edgeKey(aKey, bKey)) ?? [];
        for (const nextIndex of owners) {
          if (nextIndex === currentIndex || visited.has(nextIndex)) continue;
          const next = selected.get(nextIndex);
          if (!next) continue;
          const shared: Array<{ key: string; currentIndex: number; nextIndex: number }> = [];
          for (let ci = 0; ci < 3; ci += 1) {
            for (let ni = 0; ni < 3; ni += 1) {
              if (current.vertexKeys[ci] === next.vertexKeys[ni]) shared.push({ key: current.vertexKeys[ci], currentIndex: ci, nextIndex: ni });
            }
          }
          if (shared.length < 2) continue;
          const s0 = shared[0];
          const s1 = shared[1];
          const p0 = netByVertex.get(s0.key);
          const p1 = netByVertex.get(s1.key);
          if (!p0 || !p1) continue;
          const nextThirdIndex = [0, 1, 2].find((idx) => idx !== s0.nextIndex && idx !== s1.nextIndex) ?? 0;
          const currentThirdIndex = [0, 1, 2].find((idx) => idx !== s0.currentIndex && idx !== s1.currentIndex) ?? 0;
          const currentThird = current.net[currentThirdIndex];
          const currentSide = lineSide(p0, p1, currentThird);
          const nextThirdLocal = next.local[nextThirdIndex];
          const d0 = nextThirdLocal.subtract(next.local[s0.nextIndex]).length();
          const d1 = nextThirdLocal.subtract(next.local[s1.nextIndex]).length();
          const thirdNet = placeThirdPoint(p0, p1, d0, d1, currentSide <= 0 ? 1 : -1);
          const nextNet: [FaceNetPoint2, FaceNetPoint2, FaceNetPoint2] = [{ u: 0, v: 0 }, { u: 0, v: 0 }, { u: 0, v: 0 }];
          nextNet[s0.nextIndex] = { ...p0 };
          nextNet[s1.nextIndex] = { ...p1 };
          nextNet[nextThirdIndex] = thirdNet;
          next.net = nextNet;
          next.vertexKeys.forEach((key, idx) => { if (!netByVertex.has(key)) netByVertex.set(key, nextNet[idx]); });
          visited.add(nextIndex);
          queue.push(nextIndex);
        }
      }
    }

    // Keep disconnected selections visible for diagnostics, but place them aside
    // instead of inventing a global rectangle. A disconnected island is still
    // rendered triangle-by-triangle, never as a proxy plane.
    let islandOffset = 0;
    for (const triangle of selected.values()) {
      if (triangle.net) continue;
      disconnected += 1;
      islandOffset += 1;
      const a = triangle.local[0];
      let r = triangle.local[1].subtract(a);
      if (r.lengthSquared() < 0.0000001) r = triangle.local[2].subtract(a);
      if (r.lengthSquared() < 0.0000001) continue;
      r.normalize();
      let uVec = Vector3.Cross(triangle.normalLocal, r);
      if (uVec.lengthSquared() < 0.0000001) uVec = up.clone();
      uVec.normalize();
      const offset = islandOffset * (getMeshWorldBounds(targetMesh).size.length() + 0.1);
      triangle.net = triangle.local.map((point) => ({ u: offset + Vector3.Dot(point.subtract(a), r), v: Vector3.Dot(point.subtract(a), uVec) })) as [FaceNetPoint2, FaceNetPoint2, FaceNetPoint2];
    }

    const triangles = Array.from(selected.values()).filter((triangle) => !!triangle.net);
    if (!triangles.length) return null;
    const allPoints = triangles.flatMap((triangle) => triangle.net ?? []);
    const minU = Math.min(...allPoints.map((point) => point.u));
    const maxU = Math.max(...allPoints.map((point) => point.u));
    const minV = Math.min(...allPoints.map((point) => point.v));
    const maxV = Math.max(...allPoints.map((point) => point.v));
    return { triangles, bounds: { minU, maxU, minV, maxV }, disconnected };
  };

  const createExactFaceNetPrintSurfaceStickerMesh = (
    ownerIdInput: string,
    kind: string,
    name: string,
    baseMaterial: any,
    projection: { targetMesh: Mesh; position: Vector3; normal: Vector3; size: Vector3; faceId?: number | null; rawPosition?: Vector3 | null },
    angle = 0,
  ): Mesh | null => {
    const ownerId = String(ownerIdInput ?? '').trim();
    const declared = getDeclaredExactFaceNetPrintSurfaceMetadata(projection as any);
    if (!ownerId || !declared || !projection?.targetMesh || !baseMaterial) return null;
    if (!declared.selectedTriangles.length) {
      debug3DLayer('face-net:render-result', { ownerId, kind, result: 'blocked', reason: 'missing-selected-triangles', message: 'Exact face-net surfaces require real picked mesh triangles. Box/proxy fallback is intentionally disabled.' }, 'warn');
      return null;
    }
    try {
      const faceNetArea = ((projection as any).__asukaPrintSurfaceArea ?? null) as Asuka3DPrintArea | null;
      const declaredTargetMeshId = getExactFaceNetAreaTargetMeshId(faceNetArea) || String(declared.targetMeshId ?? declared.selectedTriangles[0]?.meshId ?? '').trim();
      const candidateMeshes = [
        declaredTargetMeshId ? findModelMesh(declaredTargetMeshId) : null,
        projection.targetMesh,
      ].filter((mesh, index, list): mesh is Mesh => !!mesh && list.findIndex((item) => item === mesh) === index);
      let targetMesh: Mesh | null = null;
      let geometry: { positions: ArrayLike<number>; indices: number[]; vertexCount: number; world: Matrix; inverseWorld: Matrix } | null = null;
      let runtime: ReturnType<typeof buildExactFaceNetRuntime> | null = null;
      const targetCandidatesDebug: any[] = [];
      for (const candidateMesh of candidateMeshes) {
        const candidateGeometry = getMeshFaceNetGeometry(candidateMesh);
        const candidateRuntime = candidateGeometry ? buildExactFaceNetRuntime(candidateMesh, declared.selectedTriangles, declared.primaryTriangleIndex) : null;
        targetCandidatesDebug.push({
          mesh: summarizeLayerMeshForDebug(candidateMesh),
          hasGeometry: !!candidateGeometry,
          runtimeTriangles: candidateRuntime?.triangles.length ?? 0,
        });
        if (candidateGeometry && candidateRuntime?.triangles.length) {
          targetMesh = candidateMesh;
          geometry = candidateGeometry;
          runtime = candidateRuntime;
          break;
        }
      }
      if (!targetMesh || !geometry || !runtime?.triangles.length) {
        debug3DLayer('face-net:render-result', {
          ownerId,
          kind,
          result: 'blocked',
          reason: 'no-runtime-triangles',
          declaredTargetMeshId,
          selectedTriangles: declared.selectedTriangles,
          candidates: targetCandidatesDebug,
        }, 'warn');
        return null;
      }
      if (targetMesh !== projection.targetMesh) {
        debug3DLayer('face-net:target-mesh-resolved', {
          ownerId,
          kind,
          declaredTargetMeshId,
          projectionTargetMesh: summarizeLayerMeshForDebug(projection.targetMesh),
          resolvedTargetMesh: summarizeLayerMeshForDebug(targetMesh),
        }, 'warn');
      }

      // ── Active-face re-seeding ────────────────────────────────────────────
      // Extract explicitFaceId early (injected by resolveActiveFaceNetTriangle
      // via recreateDecorationDecalOnSurface). When present, rebuild the BFS net
      // seeded from the ACTIVE face so that:
      //   1. Adjacent faces unfold naturally around the cursor position.
      //   2. Faces disconnected from the declared primary but adjacent to the
      //      cursor face become reachable (fixes the "missing face" bug).
      //   3. No seam artifacts caused by the fixed-primary-seed layout.
      const explicitFaceId = Number((projection as any).faceId ?? (projection as any).__asukaFaceNetTriangleIndex);
      if (Number.isFinite(explicitFaceId)) {
        const activeRuntime = buildExactFaceNetRuntime(targetMesh, declared.selectedTriangles, Math.floor(explicitFaceId));
        if (activeRuntime?.triangles.length) {
          debug3DLayer('face-net:reseed-from-active-face', {
            ownerId, kind,
            activeFaceId: Math.floor(explicitFaceId),
            declaredPrimaryTriangleIndex: declared.primaryTriangleIndex ?? null,
            triangles: activeRuntime.triangles.length,
          });
          runtime = activeRuntime;
        }
      }
      // ─────────────────────────────────────────────────────────────────────

      // If the majority of triangles are disconnected islands (GLB organic mesh
      // with per-face duplicate vertices), the face-net UV layout becomes a vast
      // grid of isolated triangles. The sticker image ends up spread across an
      // enormous UV space — each triangle receives a fraction of a pixel and the
      // result is effectively invisible. Fall back to surface-patch instead.
      const disconnectedRatio = runtime.triangles.length > 0
        ? runtime.disconnected / runtime.triangles.length
        : 0;
      console.warn('[ASUKA-DEBUG] face-net disconnectedRatio:', disconnectedRatio.toFixed(3), '(' + runtime.disconnected + '/' + runtime.triangles.length + ')');
      if (disconnectedRatio > 0.5) {
        console.warn('[ASUKA-DEBUG] face-net: too many disconnected triangles (' + runtime.disconnected + '/' + runtime.triangles.length + ') → falling back to surface-patch');
        debug3DLayer('face-net:render-result', { ownerId, kind, result: 'blocked', reason: 'too-many-disconnected', disconnected: runtime.disconnected, total: runtime.triangles.length, ratio: disconnectedRatio }, 'warn');
        return null;
      }

      const size = getSafeDecalProjectionSize(projection.size);
      const worldAreaBounds = getPrintAreaBounds(faceNetArea);
      const runtimeNetWidth = Math.max(0.0001, Math.abs(runtime.bounds.maxU - runtime.bounds.minU));
      const runtimeNetHeight = Math.max(0.0001, Math.abs(runtime.bounds.maxV - runtime.bounds.minV));
      const worldAreaWidth = Math.max(0.0001, Math.abs(Number(worldAreaBounds?.size?.x ?? 0)) || Math.abs(size.x) || 1);
      const worldAreaHeight = Math.max(0.0001, Math.abs(Number(worldAreaBounds?.size?.y ?? 0)) || Math.abs(size.y) || 1);
      const worldStickerWidth = Math.max(0.0001, Math.abs(size.x));
      const worldStickerHeight = Math.max(0.0001, Math.abs(size.y));
      const widthRatio = Math.max(0.02, Math.min(1, worldStickerWidth / Math.max(worldAreaWidth, worldStickerWidth, 0.0001)));
      const heightRatio = Math.max(0.02, Math.min(1, worldStickerHeight / Math.max(worldAreaHeight, worldStickerHeight, 0.0001)));
      let stickerWidth = Math.max(Math.min(runtimeNetWidth, runtimeNetWidth * 0.08), Math.min(runtimeNetWidth, runtimeNetWidth * widthRatio));
      let stickerHeight = Math.max(Math.min(runtimeNetHeight, runtimeNetHeight * 0.08), Math.min(runtimeNetHeight, runtimeNetHeight * heightRatio));
      const surfaceOffset = Math.max(0.0008, Math.min(Math.max(worldStickerWidth, worldStickerHeight) * 0.002, 0.003));
      debug3DLayer('face-net:units-check', {
        ownerId,
        kind,
        runtimeBounds: runtime.bounds,
        runtimeNetSize: { width: runtimeNetWidth, height: runtimeNetHeight },
        worldArea: {
          center: worldAreaBounds?.center ? fromVec3(worldAreaBounds.center) : null,
          size: worldAreaBounds?.size ? fromVec3(worldAreaBounds.size) : null,
        },
        projectionSizeWorld: fromVec3(size),
        ratios: { widthRatio, heightRatio },
        stickerSizeNet: { width: stickerWidth, height: stickerHeight },
      });
      const inverseWorld = geometry.inverseWorld;
      // explicitFaceId already declared in the active-face re-seeding block above.
      const hasRealSurfaceHit = !!(projection as any).rawPosition || Number.isFinite(explicitFaceId);
      const pickedLocalPoint = Vector3.TransformCoordinates(((projection as any).rawPosition ?? projection.position), inverseWorld);
      const triangleByIndex = new Map(runtime.triangles.map((triangle) => [triangle.triangleIndex, triangle]));
      let centerNet: FaceNetPoint2 | null = null;
      let centerSource = 'fallback-centroid';
      let centerTriangleIndex: number | null = null;
      const getTriangleNetCentroid = (triangle?: FaceNetTriangleRuntime | null): FaceNetPoint2 | null => {
        const net = triangle?.net;
        if (!net) return null;
        return {
          u: (net[0].u + net[1].u + net[2].u) / 3,
          v: (net[0].v + net[1].v + net[2].v) / 3,
        };
      };
      const getRuntimeBoundsCenter = (): FaceNetPoint2 | null => {
        const bounds = runtime.bounds;
        if (!Number.isFinite(bounds.minU) || !Number.isFinite(bounds.maxU) || !Number.isFinite(bounds.minV) || !Number.isFinite(bounds.maxV)) return null;
        return { u: (bounds.minU + bounds.maxU) / 2, v: (bounds.minV + bounds.maxV) / 2 };
      };
      const getRuntimeSeedTriangle = (): FaceNetTriangleRuntime | null => {
        if (Number.isFinite(Number(declared.primaryTriangleIndex))) {
          const byDeclared = triangleByIndex.get(Math.floor(Number(declared.primaryTriangleIndex)));
          if (byDeclared) return byDeclared;
        }
        return runtime.triangles[0] ?? null;
      };
      const getFaceNetTrianglesBounds = (triangles: FaceNetTriangleRuntime[]) => {
        const points = triangles.flatMap((triangle) => triangle.net ?? []);
        if (!points.length) return null;
        const minU = Math.min(...points.map((point) => point.u));
        const maxU = Math.max(...points.map((point) => point.u));
        const minV = Math.min(...points.map((point) => point.v));
        const maxV = Math.max(...points.map((point) => point.v));
        return { minU, maxU, minV, maxV, width: Math.max(0.0001, maxU - minU), height: Math.max(0.0001, maxV - minV), center: { u: (minU + maxU) / 2, v: (minV + maxV) / 2 } as FaceNetPoint2 };
      };
      const getCoplanarFaceTriangles = (seedInput?: FaceNetTriangleRuntime | null): FaceNetTriangleRuntime[] => {
        const seed = seedInput ?? getRuntimeSeedTriangle() ?? runtime.triangles[0] ?? null;
        if (!seed) return runtime.triangles.filter((triangle) => !!triangle.net);
        const seedNormal = seed.normalWorld.clone();
        if (seedNormal.lengthSquared() > 0.0001) seedNormal.normalize();
        const seedCentroid = seed.centroidWorld;
        const modelSize = getMeshWorldBounds(targetMesh).size.length();
        const planeTolerance = Math.max(0.0015, modelSize * 0.01);
        const coplanar = runtime.triangles.filter((triangle) => {
          if (!triangle.net) return false;
          const normal = triangle.normalWorld.clone();
          if (normal.lengthSquared() > 0.0001) normal.normalize();
          const normalDot = Vector3.Dot(seedNormal, normal);
          if (normalDot < 0.985) return false;
          const planeDistance = Math.abs(Vector3.Dot(triangle.centroidWorld.subtract(seedCentroid), seedNormal));
          return planeDistance <= planeTolerance;
        });
        return coplanar.length ? coplanar : [seed].filter((triangle) => !!triangle?.net) as FaceNetTriangleRuntime[];
      };
      // Primary seed = active face when available so primaryFaceBounds reflects
      // the face under the cursor (used for face-relative size calculation below).
      const explicitFaceTriangle = Number.isFinite(explicitFaceId) ? triangleByIndex.get(Math.floor(explicitFaceId)) ?? null : null;
      const primarySeedTriangle = explicitFaceTriangle ?? getRuntimeSeedTriangle() ?? runtime.triangles[0] ?? null;
      const primaryFaceTriangles = getCoplanarFaceTriangles(primarySeedTriangle);
      const primaryFaceBounds = getFaceNetTrianglesBounds(primaryFaceTriangles);
      // ── Size override: face-relative, not net-global ────────────────────────
      // Root cause of "image too big": resolveDecalProjection sets
      // projection.size = areaBounds.size (bbox of ALL selected faces). For a
      // face-net area spanning front+sides, worldStickerWidth ≈ worldAreaWidth
      // → widthRatio = 1 → stickerWidth = full net width → image fills everything.
      //
      // Fix: express size as a fraction of ONE face (primaryFaceBounds).
      //   auto-sized (first placement) → default to 35 % of face width
      //   user-defined size            → scale proportionally to the face
      if (primaryFaceBounds) {
        const faceW = Math.max(0.0001, primaryFaceBounds.width);
        const faceH = Math.max(0.0001, primaryFaceBounds.height);
        const isAutoSizedX = worldStickerWidth >= worldAreaWidth * 0.88;
        const isAutoSizedY = worldStickerHeight >= worldAreaHeight * 0.88;
        // ── Image aspect-ratio-aware sticker sizing ──────────────────────────
        // `normalizeDecalProjectionAspect` is called upstream (in createImageDecal)
        // with the actual image pixel dimensions from `resolveImageAspectRatio`.
        // It stores the corrected image proportions in `projection.size.x / .y`.
        // So `worldStickerWidth / worldStickerHeight` already IS the correct
        // image aspect ratio — no need to read the texture again here.
        //
        // Previous bug: mixed auto/non-auto detection:
        //   worldStickerWidth ≈ areaWidth  → isAutoSizedX = true  → faceW * 0.35
        //   worldStickerHeight < areaHeight → isAutoSizedY = false → worldH * conv
        // Result: sticker 0.14 × 0.272 → aspect 0.51 (portrait) instead of 1.47
        //
        // Fix: detect auto-size with OR, then use worldAspect for BOTH dimensions.
        const isAutoSized = worldStickerWidth >= worldAreaWidth * 0.88
                          || worldStickerHeight >= worldAreaHeight * 0.88;
        // The worldSticker aspect is always the image's true aspect ratio.
        const worldAspect = Math.max(0.05, Math.min(20,
          worldStickerWidth / Math.max(worldStickerHeight, 0.0001)));

        if (isAutoSized) {
          // Auto-placement: fit inside 40 % of face while honouring image aspect.
          // The image may extend beyond the face in one dimension — that is fine
          // (the clip naturally limits what's visible to the selected triangles).
          const maxW = faceW * 0.40;
          const maxH = faceH * 0.40;
          if (worldAspect >= 1) {
            // Landscape image: anchor to face width
            stickerWidth = maxW;
            stickerHeight = maxW / worldAspect;
            if (stickerHeight > maxH) { stickerHeight = maxH; stickerWidth = maxH * worldAspect; }
          } else {
            // Portrait image: anchor to face height
            stickerHeight = maxH;
            stickerWidth = maxH * worldAspect;
            if (stickerWidth > maxW) { stickerWidth = maxW; stickerHeight = maxW / worldAspect; }
          }
        } else {
          // User-defined size: convert world-space dimensions to net (local) space,
          // using the face-to-area ratio as the scale factor. Both dimensions use
          // the same formula so the worldAspect (= image aspect) is preserved.
          const convX = faceW / Math.max(worldAreaWidth, 0.0001);
          const convY = faceH / Math.max(worldAreaHeight, 0.0001);
          stickerWidth  = Math.max(faceW * 0.04, worldStickerWidth  * convX);
          stickerHeight = Math.max(faceH * 0.04, worldStickerHeight * convY);
        }
        // ─────────────────────────────────────────────────────────────────────
        debug3DLayer('face-net:size-face-relative', {
          ownerId, kind,
          faceSize: { w: faceW, h: faceH },
          world: { stickerW: worldStickerWidth, stickerH: worldStickerHeight, areaW: worldAreaWidth, areaH: worldAreaHeight },
          isAutoSized: { x: isAutoSizedX, y: isAutoSizedY },
          result: { stickerWidth, stickerHeight },
        });
      }
      // ─────────────────────────────────────────────────────────────────────

      let activeFaceTriangles = explicitFaceTriangle ? getCoplanarFaceTriangles(explicitFaceTriangle) : primaryFaceTriangles;
      if (!activeFaceTriangles.length) activeFaceTriangles = primaryFaceTriangles.length ? primaryFaceTriangles : runtime.triangles.filter((triangle) => !!triangle.net);
      const activeFaceBounds = getFaceNetTrianglesBounds(activeFaceTriangles) ?? primaryFaceBounds ?? getFaceNetTrianglesBounds(runtime.triangles.filter((triangle) => !!triangle.net));
      const getNetFromTriangle = (triangle: FaceNetTriangleRuntime): FaceNetPoint2 | null => {
        if (!triangle.net) return null;
        const bary = barycentricFromLocalTriangle(pickedLocalPoint, triangle.local);
        if (!bary) return null;
        return faceNetPointFromBarycentric(bary, triangle.net);
      };
      // NOTE: the !hasRealSurfaceHit branch (first placement) is handled later
      // by the initial-fit block which centres on the full net bounds.
      if (!hasRealSurfaceHit) {
        // Defer: initial-fit block below will set centerNet from the full net.
        const seed = getRuntimeSeedTriangle();
        centerNet = getRuntimeBoundsCenter() ?? getTriangleNetCentroid(seed);
        centerSource = 'full-net-bounds';
        centerTriangleIndex = seed?.triangleIndex ?? null;
      }
      // During drag: if we have an explicit face-id resolved from selectedTriangles
      // (set by recreateDecorationDecalOnSurface via resolveActiveFaceNetTriangle),
      // use the picked point projected onto that triangle's net coords.
      if (!centerNet && Number.isFinite(explicitFaceId) && triangleByIndex.has(Math.floor(explicitFaceId))) {
        const triangle = triangleByIndex.get(Math.floor(explicitFaceId))!;
        centerNet = getNetFromTriangle(triangle);
        centerSource = 'picked-triangle';
        centerTriangleIndex = triangle.triangleIndex;
      }
      if (!centerNet && hasRealSurfaceHit) {
        let best: { triangle: FaceNetTriangleRuntime; score: number; net: FaceNetPoint2; outside: number; planeDistance: number } | null = null;
        for (const triangle of runtime.triangles) {
          if (!triangle.net) continue;
          const bary = barycentricFromLocalTriangle(pickedLocalPoint, triangle.local);
          if (!bary) continue;
          const outside = Math.max(0, -bary[0], -bary[1], -bary[2], bary[0] - 1, bary[1] - 1, bary[2] - 1);
          const planeDistance = Math.abs(Vector3.Dot(pickedLocalPoint.subtract(triangle.local[0]), triangle.normalLocal));
          const score = outside * 1000 + planeDistance;
          const net = faceNetPointFromBarycentric(bary, triangle.net);
          if (!best || score < best.score) best = { triangle, score, net, outside, planeDistance };
        }
        if (best) {
          centerNet = best.net;
          centerSource = best.outside <= 0.02 ? 'nearest-triangle' : 'nearest-triangle-outside-clamped';
          centerTriangleIndex = best.triangle.triangleIndex;
        }
      }
      if (!centerNet) {
        centerNet = getRuntimeBoundsCenter();
        centerSource = 'surface-net-bounds-recovery';
        centerTriangleIndex = getRuntimeSeedTriangle()?.triangleIndex ?? null;
      }
      if (!centerNet) {
        const seed = runtime.triangles[0];
        centerNet = getTriangleNetCentroid(seed);
        centerTriangleIndex = seed.triangleIndex;
      }
      const eps = 0.00001;
      const netPointInsideTriangle = (point: FaceNetPoint2, tri: [FaceNetPoint2, FaceNetPoint2, FaceNetPoint2]): boolean => {
        const denom = ((tri[1].v - tri[2].v) * (tri[0].u - tri[2].u)) + ((tri[2].u - tri[1].u) * (tri[0].v - tri[2].v));
        if (Math.abs(denom) < 0.0000001) return false;
        const a = (((tri[1].v - tri[2].v) * (point.u - tri[2].u)) + ((tri[2].u - tri[1].u) * (point.v - tri[2].v))) / denom;
        const b = (((tri[2].v - tri[0].v) * (point.u - tri[2].u)) + ((tri[0].u - tri[2].u) * (point.v - tri[2].v))) / denom;
        const c = 1 - a - b;
        return a >= -0.0001 && b >= -0.0001 && c >= -0.0001;
      };
      // Check against the full net (all selected faces), not just the active face.
      // This prevents the center from being snapped back to the primary face when
      // the sticker is dragged onto a neighbouring authorised face.
      const centerIsOnSelectedTriangle = (point: FaceNetPoint2 | null): boolean => !!point && runtime.triangles.some((triangle) => triangle.net && netPointInsideTriangle(point, triangle.net));
      if (!centerIsOnSelectedTriangle(centerNet)) {
        const seed = getRuntimeSeedTriangle();
        // Prefer primaryFaceBounds.center over the full-net center so that
        // snapping lands on the primary face, not somewhere between all faces.
        const snapped = getTriangleNetCentroid(explicitFaceTriangle) ?? primaryFaceBounds?.center ?? getTriangleNetCentroid(seed) ?? getRuntimeBoundsCenter() ?? getTriangleNetCentroid(runtime.triangles[0]);
        if (snapped) {
          debug3DLayer('face-net:projection-center-snapped', {
            ownerId,
            kind,
            reason: 'center-outside-net-triangles',
            from: centerNet,
            to: snapped,
            previousSource: centerSource,
            triangleIndex: explicitFaceTriangle?.triangleIndex ?? seed?.triangleIndex ?? runtime.triangles[0]?.triangleIndex ?? null,
          }, 'warn');
          centerNet = snapped;
          centerSource = `${centerSource}:snapped-to-primary-face`;
          centerTriangleIndex = explicitFaceTriangle?.triangleIndex ?? activeFaceTriangles[0]?.triangleIndex ?? seed?.triangleIndex ?? runtime.triangles[0]?.triangleIndex ?? centerTriangleIndex;
        }
      }
      if (!centerNet) return null;

      // ── Auto-placement center override ────────────────────────────────────
      // When explicitFaceId is absent (NaN), this is NOT a user drag — it is
      // an automatic placement (first add, API call, etc.). The camera surface
      // pick that produced rawPosition may have landed on the top face, a
      // corner triangle, or anywhere on the visible mesh, so the centerNet
      // computed above can be at the edge of the primary face instead of its
      // centre. Override to primaryFaceBounds.center so the first placement
      // is always visually centred on the declared primary face.
      if (!Number.isFinite(explicitFaceId) && primaryFaceBounds) {
        centerNet = primaryFaceBounds.center;
        centerSource = 'auto-placement:primary-face-center';
        debug3DLayer('face-net:auto-placement-center', {
          ownerId, kind, centerNet,
          stickerSizeNet: { width: stickerWidth, height: stickerHeight },
          selectedTriangles: declared.selectedTriangles,
        });
      } else if (!hasRealSurfaceHit) {
        // No real surface hit at all: fall back to primary face center.
        centerNet = primaryFaceBounds?.center ?? getRuntimeBoundsCenter();
        centerSource = 'no-hit:primary-face-center';
      }
      // ─────────────────────────────────────────────────────────────────────
      centerNet = centerNet ?? getRuntimeBoundsCenter() ?? { u: 0, v: 0 };
      const originalCenterNet: FaceNetPoint2 = { ...centerNet };
      const clampCenterToRuntimeBounds = (point: FaceNetPoint2): FaceNetPoint2 => {
        // Always clamp against the full net bounds (all selected faces), not just
        // the active face. This lets the sticker center move freely across the
        // entire unfolded surface and reach any authorised face during drag.
        const bounds = runtime.bounds;
        const netWidth = Math.max(0, bounds.maxU - bounds.minU);
        const netHeight = Math.max(0, bounds.maxV - bounds.minV);
        if (!Number.isFinite(netWidth) || !Number.isFinite(netHeight) || netWidth <= eps || netHeight <= eps) return point;
        const minCenterU = bounds.minU + Math.min(stickerWidth * 0.5, netWidth * 0.5);
        const maxCenterU = bounds.maxU - Math.min(stickerWidth * 0.5, netWidth * 0.5);
        const minCenterV = bounds.minV + Math.min(stickerHeight * 0.5, netHeight * 0.5);
        const maxCenterV = bounds.maxV - Math.min(stickerHeight * 0.5, netHeight * 0.5);
        return {
          u: minCenterU <= maxCenterU ? Math.max(minCenterU, Math.min(maxCenterU, point.u)) : (bounds.minU + bounds.maxU) / 2,
          v: minCenterV <= maxCenterV ? Math.max(minCenterV, Math.min(maxCenterV, point.v)) : (bounds.minV + bounds.maxV) / 2,
        };
      };
      centerNet = clampCenterToRuntimeBounds(centerNet);
      if (Math.abs(centerNet.u - originalCenterNet.u) > eps || Math.abs(centerNet.v - originalCenterNet.v) > eps) {
        debug3DLayer('face-net:projection-center-clamped', {
          ownerId,
          kind,
          centerSource,
          from: originalCenterNet,
          to: centerNet,
          bounds: runtime.bounds,
          sticker: { width: stickerWidth, height: stickerHeight },
        });
      }
      const sU0 = centerNet.u - stickerWidth * 0.5;
      const sU1 = centerNet.u + stickerWidth * 0.5;
      const sV0 = centerNet.v - stickerHeight * 0.5;
      const sV1 = centerNet.v + stickerHeight * 0.5;
      debug3DLayer('face-net:projection-center', {
        ownerId,
        kind,
        centerSource,
        centerTriangleIndex,
        explicitFaceId: Number.isFinite(explicitFaceId) ? Math.floor(explicitFaceId) : null,
        centerNet,
        pickedWorld: fromVec3(projection.rawPosition ?? projection.position),
        pickedLocal: fromVec3(pickedLocalPoint),
        sticker: { sU0, sU1, sV0, sV1, width: stickerWidth, height: stickerHeight },
        activeFace: { triangles: activeFaceTriangles.map((triangle) => triangle.triangleIndex), bounds: activeFaceBounds },
      });

      const interpolateNetPoint = (a: FaceNetPoint2, b: FaceNetPoint2, t: number): FaceNetPoint2 => ({ u: a.u + (b.u - a.u) * t, v: a.v + (b.v - a.v) * t });
      const boundaryT = (start: number, end: number, boundary: number): number => {
        const denom = end - start;
        if (Math.abs(denom) <= eps) return 0;
        return Math.max(0, Math.min(1, (boundary - start) / denom));
      };
      const clipBoundary = (input: FaceNetPoint2[], inside: (point: FaceNetPoint2) => boolean, intersect: (a: FaceNetPoint2, b: FaceNetPoint2) => FaceNetPoint2): FaceNetPoint2[] => {
        if (!input.length) return [];
        const output: FaceNetPoint2[] = [];
        let previous = input[input.length - 1];
        let previousInside = inside(previous);
        for (const current of input) {
          const currentInside = inside(current);
          if (currentInside) {
            if (!previousInside) output.push(intersect(previous, current));
            output.push(current);
          } else if (previousInside) output.push(intersect(previous, current));
          previous = current;
          previousInside = currentInside;
        }
        return output;
      };
      const cleanupPolygon = (input: FaceNetPoint2[]): FaceNetPoint2[] => {
        const output: FaceNetPoint2[] = [];
        for (const point of input) {
          const previous = output[output.length - 1];
          if (previous && Math.abs(previous.u - point.u) <= eps && Math.abs(previous.v - point.v) <= eps) continue;
          output.push(point);
        }
        if (output.length > 1) {
          const first = output[0];
          const last = output[output.length - 1];
          if (Math.abs(first.u - last.u) <= eps && Math.abs(first.v - last.v) <= eps) output.pop();
        }
        return output;
      };
      const clipTriangleToStickerRect = (triangle: FaceNetTriangleRuntime): FaceNetPoint2[] => {
        let poly = (triangle.net ?? []).map((point) => ({ ...point }));
        poly = clipBoundary(poly, (point) => point.u >= sU0 - eps, (a, b) => interpolateNetPoint(a, b, boundaryT(a.u, b.u, sU0)));
        poly = clipBoundary(poly, (point) => point.u <= sU1 + eps, (a, b) => interpolateNetPoint(a, b, boundaryT(a.u, b.u, sU1)));
        poly = clipBoundary(poly, (point) => point.v >= sV0 - eps, (a, b) => interpolateNetPoint(a, b, boundaryT(a.v, b.v, sV0)));
        poly = clipBoundary(poly, (point) => point.v <= sV1 + eps, (a, b) => interpolateNetPoint(a, b, boundaryT(a.v, b.v, sV1)));
        return cleanupPolygon(poly);
      };
      const barycentricFromNet = (point: FaceNetPoint2, tri: [FaceNetPoint2, FaceNetPoint2, FaceNetPoint2]): [number, number, number] | null => {
        const [a, b, c] = tri;
        const denom = ((b.v - c.v) * (a.u - c.u)) + ((c.u - b.u) * (a.v - c.v));
        if (Math.abs(denom) < 0.0000001) return null;
        const wa = (((b.v - c.v) * (point.u - c.u)) + ((c.u - b.u) * (point.v - c.v))) / denom;
        const wb = (((c.v - a.v) * (point.u - c.u)) + ((a.u - c.u) * (point.v - c.v))) / denom;
        const wc = 1 - wa - wb;
        return [wa, wb, wc];
      };

      const outPositions: number[] = [];
      const outNormals: number[] = [];
      const outUvs: number[] = [];
      const outIndices: number[] = [];
      const renderedFragments: Array<{ triangleIndex: number; points: number; worldBounds: { min: [number, number, number]; max: [number, number, number] } }> = [];
      const pushVertex = (triangle: FaceNetTriangleRuntime, point: FaceNetPoint2): number | null => {
        if (!triangle.net) return null;
        const bary = barycentricFromNet(point, triangle.net);
        if (!bary) return null;
        const local = triangle.local[0].scale(bary[0]).add(triangle.local[1].scale(bary[1])).add(triangle.local[2].scale(bary[2]));
        const worldPoint = Vector3.TransformCoordinates(local, geometry.world).add(triangle.normalWorld.scale(surfaceOffset));
        const index = outPositions.length / 3;
        outPositions.push(worldPoint.x, worldPoint.y, worldPoint.z);
        outNormals.push(triangle.normalWorld.x, triangle.normalWorld.y, triangle.normalWorld.z);
        const baseTexU = Math.max(0, Math.min(1, (point.u - sU0) / Math.max(0.00001, sU1 - sU0)));
        // V_face increases upward (up=+Y). With Babylon.js invertY=true:
        // V=1→visual top, V=0→visual bottom. Direct mapping (no 1-minus).
        const baseTexV = Math.max(0, Math.min(1, (point.v - sV0) / Math.max(0.00001, sV1 - sV0)));
        const orientedUv = applyDeclaredBoxTextureOrientation(baseTexU, baseTexV, declared.orientation);
        const correctedUv = applyStickerSurfaceUvCorrection(projection, orientedUv.u, orientedUv.v);
        outUvs.push(correctedUv.u, correctedUv.v);
        return index;
      };

      // Render against ALL triangles in the net, not just the active face.
      // The Sutherland-Hodgman clip naturally limits output to where the sticker
      // rectangle overlaps each triangle, so the image can span several faces
      // simultaneously and glide smoothly across edges during drag.
      for (const triangle of runtime.triangles) {
        if (!triangle.net) continue;
        const clipped = clipTriangleToStickerRect(triangle);
        debug3DLayer('face-net:clip', { ownerId, triangleIndex: triangle.triangleIndex, inputPoints: triangle.net?.length ?? 0, outputPoints: clipped.length });
        if (clipped.length < 3) continue;
        const before = outPositions.length;
        const baseIndex = pushVertex(triangle, clipped[0]);
        if (baseIndex == null) continue;
        for (let i = 1; i + 1 < clipped.length; i += 1) {
          const bIndex = pushVertex(triangle, clipped[i]);
          const cIndex = pushVertex(triangle, clipped[i + 1]);
          if (bIndex == null || cIndex == null) continue;
          outIndices.push(baseIndex, bIndex, cIndex);
        }
        const newPositions = outPositions.slice(before);
        const xs: number[] = [];
        const ys: number[] = [];
        const zs: number[] = [];
        for (let i = 0; i + 2 < newPositions.length; i += 3) { xs.push(newPositions[i]); ys.push(newPositions[i + 1]); zs.push(newPositions[i + 2]); }
        const worldBounds = { min: [Math.min(...xs), Math.min(...ys), Math.min(...zs)] as [number, number, number], max: [Math.max(...xs), Math.max(...ys), Math.max(...zs)] as [number, number, number] };
        renderedFragments.push({ triangleIndex: triangle.triangleIndex, points: clipped.length, worldBounds });
        debug3DLayer('face-net:fragment-created', { ownerId, triangleIndex: triangle.triangleIndex, points: clipped.length, worldBounds });
      }

      if (outIndices.length < 3) {
        debug3DLayer('face-net:no-fragment-diagnostics', {
          ownerId,
          kind,
          centerSource,
          centerNet,
          sticker: { sU0, sU1, sV0, sV1, width: stickerWidth, height: stickerHeight },
          runtimeBounds: runtime.bounds,
          selectedTriangles: declared.selectedTriangles,
          triangles: runtime.triangles.map((triangle) => ({
            triangleIndex: triangle.triangleIndex,
            net: triangle.net,
          })),
        }, 'warn');
        // Last-chance exact recovery: never fall back to a floating plane/proxy.
        // Render only the primary authored face, not the whole selected net. The
        // selected faces define where the layer may move later; they must not make
        // the initial bitmap stretch diagonally across the entire product.
        const recoveryTriangles = activeFaceTriangles.length ? activeFaceTriangles : primaryFaceTriangles.length ? primaryFaceTriangles : runtime.triangles.filter((triangle) => !!triangle.net).slice(0, 1);
        const recoveryBounds = getFaceNetTrianglesBounds(recoveryTriangles) ?? activeFaceBounds ?? primaryFaceBounds;
        outPositions.length = 0;
        outNormals.length = 0;
        outUvs.length = 0;
        outIndices.length = 0;
        renderedFragments.length = 0;
        for (const triangle of recoveryTriangles) {
          if (!triangle.net || !recoveryBounds) continue;
          const baseIndex = outPositions.length / 3;
          for (let i = 0; i < 3; i += 1) {
            const local = triangle.local[i];
            const point = triangle.net[i];
            const worldPoint = Vector3.TransformCoordinates(local, geometry.world).add(triangle.normalWorld.scale(surfaceOffset));
            outPositions.push(worldPoint.x, worldPoint.y, worldPoint.z);
            outNormals.push(triangle.normalWorld.x, triangle.normalWorld.y, triangle.normalWorld.z);
            const baseTexU = Math.max(0, Math.min(1, (point.u - recoveryBounds.minU) / recoveryBounds.width));
            const baseTexV = Math.max(0, Math.min(1, (point.v - recoveryBounds.minV) / recoveryBounds.height));
            const orientedUv = applyDeclaredBoxTextureOrientation(baseTexU, baseTexV, declared.orientation);
            const correctedUv = applyStickerSurfaceUvCorrection(projection, orientedUv.u, orientedUv.v);
            outUvs.push(correctedUv.u, correctedUv.v);
          }
          outIndices.push(baseIndex, baseIndex + 1, baseIndex + 2);
          const newPositions = outPositions.slice(baseIndex * 3, (baseIndex + 3) * 3);
          const xs = [newPositions[0], newPositions[3], newPositions[6]];
          const ys = [newPositions[1], newPositions[4], newPositions[7]];
          const zs = [newPositions[2], newPositions[5], newPositions[8]];
          const worldBounds = { min: [Math.min(...xs), Math.min(...ys), Math.min(...zs)] as [number, number, number], max: [Math.max(...xs), Math.max(...ys), Math.max(...zs)] as [number, number, number] };
          renderedFragments.push({ triangleIndex: triangle.triangleIndex, points: 3, worldBounds });
        }
        debug3DLayer('face-net:clip-recovery', {
          ownerId,
          kind,
          result: outIndices.length >= 3 ? 'created-from-primary-face-triangles' : 'failed',
          scope: 'primary-face-only',
          fragments: renderedFragments.length,
          indices: outIndices.length,
          primaryFaceBounds: recoveryBounds,
          fullRuntimeBounds: runtime.bounds,
        }, outIndices.length >= 3 ? 'warn' : 'error');
      }
      if (outIndices.length < 3) {
        debug3DLayer('face-net:render-result', { ownerId, kind, result: 'blocked', reason: 'no-clipped-fragments-after-recovery', centerNet, sticker: { sU0, sU1, sV0, sV1 }, selectedTriangles: declared.selectedTriangles }, 'warn');
        return null;
      }
      const patch = new Mesh(ownerId, requireScene());
      patch.id = ownerId;
      patch.name = name || ownerId;
      const vertexData = new VertexData();
      vertexData.positions = outPositions;
      vertexData.indices = outIndices;
      vertexData.normals = outNormals;
      vertexData.uvs = outUvs;
      vertexData.applyToMesh(patch, true);
      patch.material = baseMaterial;
      applySurfacePatchMaterialBias(baseMaterial);
      try { (baseMaterial as any).backFaceCulling = true; } catch {}
      try { (baseMaterial as any).zOffset = -1; } catch {}
      try { (baseMaterial as any).zOffsetUnits = -1; } catch {}
      patch.isPickable = true;
      try { patch.renderingGroupId = Number((targetMesh as any)?.renderingGroupId ?? 0) || 0; } catch {}
      setDecalRuntimeMetadata(patch, targetMesh, size, projection.normal, angle, projection.rawPosition ?? projection.position);
      applyExplicitPrintAreaDepthDiscipline(patch, baseMaterial, getExplicitProjectionPrintAreaTargetMesh(projection as any));
      (patch as any).__asukaProjectionMode = 'decal';
      (patch as any).__asukaDeclaredPrintSurface3D = true;
      (patch as any).__asukaFaceNetPrintSurface3D = true;
      (patch as any).__asukaExactFaceNetPrintSurface3D = true;
      (patch as any).__asukaTargetPrintSurface3D = clonePlain(declared);
      // Tag as surface-patch so drag skips per-frame rebuilds (face-net rebuild
      // is expensive and the position doesn't change — only mouse-up rebuilds).
      try { (patch as any).__asukaUsingSurfacePatch = true; } catch {}
      objectMeshes.set(ownerId, patch);
      objectKinds.set(ownerId, kind);
      decalFragmentsByObjectId.set(ownerId, []);
      debug3DLayer('face-net:render-result', {
        ownerId,
        kind,
        result: 'created-exact',
        renderer: 'exact-triangle-fragments',
        sourceTriangles: runtime.triangles.length,
        activeFaceTriangles: activeFaceTriangles.map((triangle) => triangle.triangleIndex),
        disconnected: runtime.disconnected,
        fragments: renderedFragments.length,
        indices: outIndices.length,
        netBounds: runtime.bounds,
        selectedTriangles: declared.selectedTriangles,
        targetMesh: summarizeLayerMeshForDebug(targetMesh),
        patch: summarizeLayerMeshForDebug(patch),
      });
      return patch;
    } catch (error) {
      debug3DLayer('face-net:render-result', { ownerId, kind, result: 'error', error }, 'error');
      return null;
    }
  };

  const createDeclaredBoxPrintSurfaceStickerMesh = (
    ownerIdInput: string,
    kind: string,
    name: string,
    baseMaterial: any,
    projection: { targetMesh: Mesh; position: Vector3; normal: Vector3; size: Vector3 },
    angle = 0,
  ): Mesh | null => {
    const ownerId = String(ownerIdInput ?? '').trim();
    const declared = getDeclaredBoxPrintSurfaceMetadata(projection as any);
    if (!ownerId || !declared || !projection?.targetMesh || !baseMaterial) return null;
    try {
      const targetMesh = projection.targetMesh;
      targetMesh.computeWorldMatrix(true);
      const rawPositions = targetMesh.getVerticesData?.('position' as any) as ArrayLike<number> | null;
      if (!rawPositions || rawPositions.length < 9) return null;
      const vertexCount = Math.floor(rawPositions.length / 3);
      const rawIndices = targetMesh.getIndices?.() as ArrayLike<number> | null;
      const indices = rawIndices && rawIndices.length >= 3
        ? Array.from(rawIndices as ArrayLike<number>, (value) => Number(value) || 0)
        : Array.from({ length: vertexCount }, (_, index) => index);
      if (indices.length < 3) return null;

      const world = targetMesh.getWorldMatrix?.() ?? Matrix.Identity();
      const inverseWorld = world.clone();
      try { inverseWorld.invert(); } catch {}

      const readLocalVertex = (index: number): Vector3 | null => {
        const safeIndex = Number(index) || 0;
        const offset = safeIndex * 3;
        if (offset < 0 || offset + 2 >= rawPositions.length) return null;
        return new Vector3(
          Number(rawPositions[offset] ?? 0) || 0,
          Number(rawPositions[offset + 1] ?? 0) || 0,
          Number(rawPositions[offset + 2] ?? 0) || 0,
        );
      };

      const localPoints: Vector3[] = [];
      for (let i = 0; i < vertexCount; i += 1) {
        const point = readLocalVertex(i);
        if (point) localPoints.push(point);
      }
      if (localPoints.length < 3) return null;
      const min = localPoints.reduce((acc, point) => Vector3.Minimize(acc, point), localPoints[0].clone());
      const max = localPoints.reduce((acc, point) => Vector3.Maximize(acc, point), localPoints[0].clone());
      const localCenter = min.add(max).scale(0.5);
      const widthX = Math.max(0.001, max.x - min.x);
      const heightY = Math.max(0.001, max.y - min.y);
      const depthZ = Math.max(0.001, max.z - min.z);
      const maxLocalSpan = Math.max(widthX, heightY, depthZ, 0.001);
      const outerPlaneEps = Math.max(0.001, maxLocalSpan * 0.035);

      const projectionLocalPosition = Vector3.TransformCoordinates(projection.position, inverseWorld);
      const projectionLocalNormal = Vector3.TransformNormal(projection.normal, inverseWorld);
      if (projectionLocalNormal.lengthSquared() < 0.0001) projectionLocalNormal.copyFromFloats(0, 0, 1);
      try { projectionLocalNormal.normalize(); } catch {}

      const size = getSafeDecalProjectionSize(projection.size);
      const stickerWidth = Math.max(0.01, Math.abs(size.x));
      const stickerHeight = Math.max(0.01, Math.abs(size.y));
      const surfaceOffset = Math.max(0.0008, Math.min(Math.max(stickerWidth, stickerHeight) * 0.002, 0.003));
      const primaryFace = declared.primaryFace;
      const orientation = declared.orientation;
      const faceSet = new Set<DeclaredBoxFaceId>(declared.faces);

      const faceNormal = (face: DeclaredBoxFaceId): Vector3 => {
        if (face === 'front') return new Vector3(0, 0, 1);
        if (face === 'back') return new Vector3(0, 0, -1);
        if (face === 'right') return new Vector3(1, 0, 0);
        if (face === 'left') return new Vector3(-1, 0, 0);
        if (face === 'top') return new Vector3(0, 1, 0);
        return new Vector3(0, -1, 0);
      };

      type DeclaredBoxNetRect = { face: DeclaredBoxFaceId; u0: number; u1: number; v0: number; v1: number };
      type NetPoint = { u: number; v: number };
      type SourceTriangle = {
        triangleIndex: number;
        face: DeclaredBoxFaceId;
        local: [Vector3, Vector3, Vector3];
        normalLocal: Vector3;
        normalWorld: Vector3;
        net: [NetPoint, NetPoint, NetPoint];
      };
      const netRects: DeclaredBoxNetRect[] = [];
      const addNetRect = (face: DeclaredBoxFaceId, u0: number, u1: number, v0: number, v1: number) => {
        if (!faceSet.has(face)) return;
        if (u1 <= u0 || v1 <= v0) return;
        netRects.push({ face, u0, u1, v0, v1 });
      };

      if (primaryFace === 'front') {
        addNetRect('front', 0, widthX, 0, heightY);
        addNetRect('left', -depthZ, 0, 0, heightY);
        addNetRect('right', widthX, widthX + depthZ, 0, heightY);
        addNetRect('top', 0, widthX, heightY, heightY + depthZ);
        addNetRect('bottom', 0, widthX, -depthZ, 0);
        addNetRect('back', widthX + depthZ, widthX + depthZ + widthX, 0, heightY);
      } else if (primaryFace === 'back') {
        addNetRect('back', 0, widthX, 0, heightY);
        addNetRect('right', -depthZ, 0, 0, heightY);
        addNetRect('left', widthX, widthX + depthZ, 0, heightY);
        addNetRect('top', 0, widthX, heightY, heightY + depthZ);
        addNetRect('bottom', 0, widthX, -depthZ, 0);
        addNetRect('front', widthX + depthZ, widthX + depthZ + widthX, 0, heightY);
      } else if (primaryFace === 'right') {
        addNetRect('right', 0, depthZ, 0, heightY);
        addNetRect('front', -widthX, 0, 0, heightY);
        addNetRect('back', depthZ, depthZ + widthX, 0, heightY);
        addNetRect('top', 0, depthZ, heightY, heightY + widthX);
        addNetRect('bottom', 0, depthZ, -widthX, 0);
        addNetRect('left', depthZ + widthX, depthZ + widthX + depthZ, 0, heightY);
      } else if (primaryFace === 'left') {
        addNetRect('left', 0, depthZ, 0, heightY);
        addNetRect('back', -widthX, 0, 0, heightY);
        addNetRect('front', depthZ, depthZ + widthX, 0, heightY);
        addNetRect('top', 0, depthZ, heightY, heightY + widthX);
        addNetRect('bottom', 0, depthZ, -widthX, 0);
        addNetRect('right', depthZ + widthX, depthZ + widthX + depthZ, 0, heightY);
      } else if (primaryFace === 'top') {
        addNetRect('top', 0, widthX, 0, depthZ);
        addNetRect('left', -depthZ, 0, 0, depthZ);
        addNetRect('right', widthX, widthX + depthZ, 0, depthZ);
        addNetRect('front', 0, widthX, -heightY, 0);
        addNetRect('back', 0, widthX, depthZ, depthZ + heightY);
        addNetRect('bottom', widthX + depthZ, widthX + depthZ + widthX, 0, depthZ);
      } else {
        addNetRect('bottom', 0, widthX, 0, depthZ);
        addNetRect('left', -depthZ, 0, 0, depthZ);
        addNetRect('right', widthX, widthX + depthZ, 0, depthZ);
        addNetRect('back', 0, widthX, -heightY, 0);
        addNetRect('front', 0, widthX, depthZ, depthZ + heightY);
        addNetRect('top', widthX + depthZ, widthX + depthZ + widthX, 0, depthZ);
      }

      const localPointToNet = (face: DeclaredBoxFaceId, point: Vector3): NetPoint => {
        if (primaryFace === 'front') {
          if (face === 'left') return { u: point.z - max.z, v: point.y - min.y };
          if (face === 'right') return { u: widthX + (max.z - point.z), v: point.y - min.y };
          if (face === 'top') return { u: point.x - min.x, v: heightY + (max.z - point.z) };
          if (face === 'bottom') return { u: point.x - min.x, v: point.z - max.z };
          if (face === 'back') return { u: widthX + depthZ + (max.x - point.x), v: point.y - min.y };
          return { u: point.x - min.x, v: point.y - min.y };
        }
        if (primaryFace === 'back') {
          if (face === 'right') return { u: min.z - point.z, v: point.y - min.y };
          if (face === 'left') return { u: widthX + (point.z - min.z), v: point.y - min.y };
          if (face === 'top') return { u: max.x - point.x, v: heightY + (point.z - min.z) };
          if (face === 'bottom') return { u: max.x - point.x, v: min.z - point.z };
          if (face === 'front') return { u: widthX + depthZ + (point.x - min.x), v: point.y - min.y };
          return { u: max.x - point.x, v: point.y - min.y };
        }
        if (primaryFace === 'right') {
          if (face === 'front') return { u: point.x - max.x, v: point.y - min.y };
          if (face === 'back') return { u: depthZ + (max.x - point.x), v: point.y - min.y };
          if (face === 'top') return { u: max.z - point.z, v: heightY + (max.x - point.x) };
          if (face === 'bottom') return { u: max.z - point.z, v: point.x - max.x };
          if (face === 'left') return { u: depthZ + widthX + (point.z - min.z), v: point.y - min.y };
          return { u: max.z - point.z, v: point.y - min.y };
        }
        if (primaryFace === 'left') {
          if (face === 'back') return { u: min.x - point.x, v: point.y - min.y };
          if (face === 'front') return { u: depthZ + (point.x - min.x), v: point.y - min.y };
          if (face === 'top') return { u: point.z - min.z, v: heightY + (point.x - min.x) };
          if (face === 'bottom') return { u: point.z - min.z, v: min.x - point.x };
          if (face === 'right') return { u: depthZ + widthX + (max.z - point.z), v: point.y - min.y };
          return { u: point.z - min.z, v: point.y - min.y };
        }
        if (primaryFace === 'top') {
          if (face === 'left') return { u: max.y - point.y - depthZ, v: max.z - point.z };
          if (face === 'right') return { u: widthX + (max.y - point.y), v: max.z - point.z };
          if (face === 'front') return { u: point.x - min.x, v: point.y - max.y };
          if (face === 'back') return { u: point.x - min.x, v: depthZ + (max.y - point.y) };
          if (face === 'bottom') return { u: widthX + depthZ + (point.x - min.x), v: max.z - point.z };
          return { u: point.x - min.x, v: max.z - point.z };
        }
        if (face === 'left') return { u: point.y - min.y - depthZ, v: point.z - min.z };
        if (face === 'right') return { u: widthX + (point.y - min.y), v: point.z - min.z };
        if (face === 'back') return { u: point.x - min.x, v: min.y - point.y };
        if (face === 'front') return { u: point.x - min.x, v: depthZ + (point.y - min.y) };
        if (face === 'top') return { u: widthX + depthZ + (point.x - min.x), v: point.z - min.z };
        return { u: point.x - min.x, v: point.z - min.z };
      };

      const pickedFace = getDeclaredBoxFaceIdFromNormal(projectionLocalNormal);
      const activeFace = faceSet.has(pickedFace) ? pickedFace : primaryFace;
      const center = localPointToNet(activeFace, projectionLocalPosition);
      const sU0 = center.u - stickerWidth * 0.5;
      const sU1 = center.u + stickerWidth * 0.5;
      const sV0 = center.v - stickerHeight * 0.5;
      const sV1 = center.v + stickerHeight * 0.5;
      const eps = 0.00001;

      const facePlaneDistance = (face: DeclaredBoxFaceId, point: Vector3): number => {
        if (face === 'front') return Math.abs(max.z - point.z);
        if (face === 'back') return Math.abs(point.z - min.z);
        if (face === 'right') return Math.abs(max.x - point.x);
        if (face === 'left') return Math.abs(point.x - min.x);
        if (face === 'top') return Math.abs(max.y - point.y);
        return Math.abs(point.y - min.y);
      };

      const signedNetArea2 = (a: NetPoint, b: NetPoint, c: NetPoint): number => (
        (b.u - a.u) * (c.v - a.v) - (b.v - a.v) * (c.u - a.u)
      );

      const sourceTriangles: SourceTriangle[] = [];
      const edgeOwners = new Map<string, number[]>();
      const vertexKey = (point: Vector3): string => `${Math.round(point.x * 100000)}:${Math.round(point.y * 100000)}:${Math.round(point.z * 100000)}`;
      const addEdgeOwner = (a: Vector3, b: Vector3, triangleIndex: number) => {
        const ka = vertexKey(a);
        const kb = vertexKey(b);
        const key = ka < kb ? `${ka}|${kb}` : `${kb}|${ka}`;
        const list = edgeOwners.get(key) ?? [];
        list.push(triangleIndex);
        edgeOwners.set(key, list);
      };

      for (let triangleIndex = 0; triangleIndex + 2 < indices.length / 3; triangleIndex += 1) {
        const base = triangleIndex * 3;
        const a = readLocalVertex(indices[base]);
        const b = readLocalVertex(indices[base + 1]);
        const c = readLocalVertex(indices[base + 2]);
        if (!a || !b || !c) continue;
        let normalLocal = Vector3.Cross(b.subtract(a), c.subtract(a));
        if (normalLocal.lengthSquared() < 0.0000001) continue;
        normalLocal.normalize();
        const centroid = a.add(b).add(c).scale(1 / 3);
        const outwardProbe = centroid.subtract(localCenter);
        if (outwardProbe.lengthSquared() > 0.0000001 && Vector3.Dot(normalLocal, outwardProbe) < 0) normalLocal = normalLocal.scale(-1);
        const face = getDeclaredBoxFaceIdFromNormal(normalLocal);
        if (!faceSet.has(face)) continue;
        const expectedNormal = faceNormal(face);
        if (Vector3.Dot(normalLocal, expectedNormal) < 0.72) continue;
        if (facePlaneDistance(face, centroid) > outerPlaneEps) continue;
        const net: [NetPoint, NetPoint, NetPoint] = [localPointToNet(face, a), localPointToNet(face, b), localPointToNet(face, c)];
        if (Math.abs(signedNetArea2(net[0], net[1], net[2])) < 0.0000001) continue;
        const normalWorld = Vector3.TransformNormal(normalLocal, world);
        if (normalWorld.lengthSquared() < 0.0000001) continue;
        normalWorld.normalize();
        sourceTriangles.push({ triangleIndex, face, local: [a, b, c], normalLocal, normalWorld, net });
        addEdgeOwner(a, b, triangleIndex);
        addEdgeOwner(b, c, triangleIndex);
        addEdgeOwner(c, a, triangleIndex);
      }

      const adjacencyCount = Array.from(edgeOwners.values()).filter((owners) => owners.length > 1).length;
      debug3DLayer('face-net:adjacency', {
        ownerId,
        targetMesh: summarizeLayerMeshForDebug(targetMesh),
        selectedFaces: declared.faces,
        triangles: sourceTriangles.length,
        sharedEdges: adjacencyCount,
      });
      debug3DLayer('face-net:unwrap', {
        ownerId,
        primaryFace,
        activeFace,
        pickedFace,
        netRects,
        sticker: { sU0, sU1, sV0, sV1 },
      });

      if (!sourceTriangles.length) {
        debug3DLayer('face-net:render-result', { ownerId, kind, reason: 'no-source-triangles', selectedFaces: declared.faces }, 'warn');
        return null;
      }

      const interpolateNetPoint = (a: NetPoint, b: NetPoint, t: number): NetPoint => ({
        u: a.u + (b.u - a.u) * t,
        v: a.v + (b.v - a.v) * t,
      });
      const boundaryT = (start: number, end: number, boundary: number): number => {
        const denom = end - start;
        if (Math.abs(denom) <= eps) return 0;
        return Math.max(0, Math.min(1, (boundary - start) / denom));
      };
      const clipBoundary = (
        input: NetPoint[],
        inside: (point: NetPoint) => boolean,
        intersect: (a: NetPoint, b: NetPoint) => NetPoint,
      ): NetPoint[] => {
        if (!input.length) return [];
        const output: NetPoint[] = [];
        let previous = input[input.length - 1];
        let previousInside = inside(previous);
        for (const current of input) {
          const currentInside = inside(current);
          if (currentInside) {
            if (!previousInside) output.push(intersect(previous, current));
            output.push(current);
          } else if (previousInside) {
            output.push(intersect(previous, current));
          }
          previous = current;
          previousInside = currentInside;
        }
        return output;
      };
      const cleanupPolygon = (input: NetPoint[]): NetPoint[] => {
        const output: NetPoint[] = [];
        for (const point of input) {
          const previous = output[output.length - 1];
          if (previous && Math.abs(previous.u - point.u) <= eps && Math.abs(previous.v - point.v) <= eps) continue;
          output.push(point);
        }
        if (output.length > 1) {
          const first = output[0];
          const last = output[output.length - 1];
          if (Math.abs(first.u - last.u) <= eps && Math.abs(first.v - last.v) <= eps) output.pop();
        }
        return output;
      };
      const clipTriangleToStickerRect = (triangle: SourceTriangle): NetPoint[] => {
        let poly: NetPoint[] = triangle.net.map((point) => ({ ...point }));
        poly = clipBoundary(poly, (point) => point.u >= sU0 - eps, (a, b) => {
          const t = boundaryT(a.u, b.u, sU0);
          return interpolateNetPoint(a, b, t);
        });
        poly = clipBoundary(poly, (point) => point.u <= sU1 + eps, (a, b) => {
          const t = boundaryT(a.u, b.u, sU1);
          return interpolateNetPoint(a, b, t);
        });
        poly = clipBoundary(poly, (point) => point.v >= sV0 - eps, (a, b) => {
          const t = boundaryT(a.v, b.v, sV0);
          return interpolateNetPoint(a, b, t);
        });
        poly = clipBoundary(poly, (point) => point.v <= sV1 + eps, (a, b) => {
          const t = boundaryT(a.v, b.v, sV1);
          return interpolateNetPoint(a, b, t);
        });
        return cleanupPolygon(poly);
      };

      const barycentricFromNet = (point: NetPoint, tri: [NetPoint, NetPoint, NetPoint]): [number, number, number] | null => {
        const [a, b, c] = tri;
        const denom = ((b.v - c.v) * (a.u - c.u)) + ((c.u - b.u) * (a.v - c.v));
        if (Math.abs(denom) < 0.0000001) return null;
        const wa = (((b.v - c.v) * (point.u - c.u)) + ((c.u - b.u) * (point.v - c.v))) / denom;
        const wb = (((c.v - a.v) * (point.u - c.u)) + ((a.u - c.u) * (point.v - c.v))) / denom;
        const wc = 1 - wa - wb;
        return [wa, wb, wc];
      };

      const outPositions: number[] = [];
      const outNormals: number[] = [];
      const outUvs: number[] = [];
      const outIndices: number[] = [];
      const renderedFragments: Array<{ triangleIndex: number; face: DeclaredBoxFaceId; points: number }> = [];
      const pushVertex = (triangle: SourceTriangle, point: NetPoint): number | null => {
        const bary = barycentricFromNet(point, triangle.net);
        if (!bary) return null;
        const [wa, wb, wc] = bary;
        const local = triangle.local[0].scale(wa).add(triangle.local[1].scale(wb)).add(triangle.local[2].scale(wc));
        const worldPoint = Vector3.TransformCoordinates(local, world).add(triangle.normalWorld.scale(surfaceOffset));
        const index = outPositions.length / 3;
        outPositions.push(worldPoint.x, worldPoint.y, worldPoint.z);
        outNormals.push(triangle.normalWorld.x, triangle.normalWorld.y, triangle.normalWorld.z);
        const baseTexU = Math.max(0, Math.min(1, (point.u - sU0) / Math.max(0.00001, sU1 - sU0)));
        const baseTexV = Math.max(0, Math.min(1, 1 - ((point.v - sV0) / Math.max(0.00001, sV1 - sV0))));
        const orientedUv = applyDeclaredBoxTextureOrientation(baseTexU, baseTexV, orientation);
        const correctedUv = applyStickerSurfaceUvCorrection(projection, orientedUv.u, orientedUv.v);
        outUvs.push(correctedUv.u, correctedUv.v);
        return index;
      };

      let clippedTriangleCount = 0;
      for (const triangle of sourceTriangles) {
        const clipped = clipTriangleToStickerRect(triangle);
        debug3DLayer('face-net:clip', {
          ownerId,
          triangleIndex: triangle.triangleIndex,
          face: triangle.face,
          inputPoints: triangle.net.length,
          outputPoints: clipped.length,
        });
        if (clipped.length < 3) continue;
        clippedTriangleCount += 1;
        const baseIndex = pushVertex(triangle, clipped[0]);
        if (baseIndex == null) continue;
        for (let i = 1; i + 1 < clipped.length; i += 1) {
          const bIndex = pushVertex(triangle, clipped[i]);
          const cIndex = pushVertex(triangle, clipped[i + 1]);
          if (bIndex == null || cIndex == null) continue;
          outIndices.push(baseIndex, bIndex, cIndex);
        }
        renderedFragments.push({ triangleIndex: triangle.triangleIndex, face: triangle.face, points: clipped.length });
        debug3DLayer('face-net:fragment-created', {
          ownerId,
          triangleIndex: triangle.triangleIndex,
          face: triangle.face,
          points: clipped.length,
        });
      }

      if (outIndices.length < 3) {
        debug3DLayer('face-net:render-result', {
          ownerId,
          kind,
          reason: 'no-clipped-fragments',
          selectedFaces: declared.faces,
          clippedTriangleCount,
          sticker: { sU0, sU1, sV0, sV1 },
        }, 'warn');
        return null;
      }

      const patch = new Mesh(ownerId, requireScene());
      patch.id = ownerId;
      patch.name = name || ownerId;
      const vertexData = new VertexData();
      vertexData.positions = outPositions;
      vertexData.indices = outIndices;
      vertexData.normals = outNormals;
      vertexData.uvs = outUvs;
      vertexData.applyToMesh(patch, true);
      patch.material = baseMaterial;
      applySurfacePatchMaterialBias(baseMaterial);
      try { (baseMaterial as any).backFaceCulling = false; } catch {}
      try { (baseMaterial as any).zOffset = -1; } catch {}
      try { (baseMaterial as any).zOffsetUnits = -1; } catch {}
      patch.isPickable = true;
      if ((projection as any).__asukaFromFixedSlot3D === true) {
        try { (patch as any).__asukaFromFixedSlot3D = true; } catch {}
      }
      try {
        const targetRenderingGroupId = Number((targetMesh as any)?.renderingGroupId ?? 0);
        patch.renderingGroupId = Number.isFinite(targetRenderingGroupId) ? targetRenderingGroupId : 0;
      } catch {}
      const activeNormalWorld = Vector3.TransformNormal(faceNormal(activeFace), world);
      if (activeNormalWorld.lengthSquared() < 0.0001) activeNormalWorld.copyFrom(projection.normal);
      try { activeNormalWorld.normalize(); } catch {}
      setDecalRuntimeMetadata(patch, targetMesh, size, activeNormalWorld, angle, projection.position);
      applyExplicitPrintAreaDepthDiscipline(patch, baseMaterial, getExplicitProjectionPrintAreaTargetMesh(projection as any));
      (patch as any).__asukaProjectionMode = 'decal';
      (patch as any).__asukaDeclaredPrintSurface3D = true;
      (patch as any).__asukaFaceNetPrintSurface3D = true;
      (patch as any).__asukaTargetPrintSurface3D = clonePlain({ ...declared, type: 'face-net' });
      objectMeshes.set(ownerId, patch);
      objectKinds.set(ownerId, kind);
      decalFragmentsByObjectId.set(ownerId, []);
      debug3DLayer('face-net:render-result', {
        ownerId,
        kind,
        result: 'created',
        primaryFace,
        activeFace,
        pickedFace,
        faces: declared.faces,
        orientation,
        sourceTriangles: sourceTriangles.length,
        fragments: renderedFragments.length,
        indices: outIndices.length,
        targetMesh: summarizeLayerMeshForDebug(targetMesh),
        patch: summarizeLayerMeshForDebug(patch),
      });
      return patch;
    } catch (error) {
      debug3DLayer('face-net:render-result', { ownerId, kind, result: 'error', error }, 'error');
      return null;
    }
  };

  type BoxPrintSurfaceFragmentSpec = {
    id: 'source' | 'right' | 'left' | 'top' | 'bottom';
    netU0: number;
    netU1: number;
    netV0: number;
    netV1: number;
  };

  const createBoxPrintSurfaceStickerMesh = (
    ownerIdInput: string,
    kind: string,
    name: string,
    baseMaterial: any,
    projection: { targetMesh: Mesh; position: Vector3; normal: Vector3; size: Vector3 },
    angle = 0,
  ): Mesh | null => {
    const ownerId = String(ownerIdInput ?? '').trim();
    if (!ownerId || !projection?.targetMesh || !baseMaterial) return null;
    try {
      const targetMesh = projection.targetMesh;
      const vertexCount = Number(targetMesh.getTotalVertices?.() ?? 0) || 0;
      const indexCount = Number(targetMesh.getTotalIndices?.() ?? 0) || 0;
      // This path is deliberately limited to small box-like meshes. Dense GLBs
      // keep using the geometric surface-patch fallback.
      if (vertexCount < 4 || vertexCount > 32 || indexCount > 96) return null;
      const rawPositions = targetMesh.getVerticesData?.('position' as any) as number[] | null;
      const rawIndices = targetMesh.getIndices?.() as number[] | null;
      if (!rawPositions || rawPositions.length < 12) return null;
      const indices = rawIndices && rawIndices.length >= 3
        ? rawIndices.map((value) => Number(value) || 0)
        : Array.from({ length: Math.floor(rawPositions.length / 3) }, (_, index) => index);
      if (indices.length < 3) return null;

      targetMesh.computeWorldMatrix(true);
      const world = targetMesh.getWorldMatrix?.() ?? Matrix.Identity();
      const readWorldVertex = (index: number): Vector3 | null => {
        const offset = index * 3;
        if (offset + 2 >= rawPositions.length) return null;
        return Vector3.TransformCoordinates(new Vector3(
          Number(rawPositions[offset] ?? 0) || 0,
          Number(rawPositions[offset + 1] ?? 0) || 0,
          Number(rawPositions[offset + 2] ?? 0) || 0,
        ), world);
      };
      const allWorldVertices: Vector3[] = [];
      for (let i = 0; i < Math.floor(rawPositions.length / 3); i += 1) {
        const vertex = readWorldVertex(i);
        if (vertex) allWorldVertices.push(vertex);
      }
      if (allWorldVertices.length < 4) return null;

      const size = getSafeDecalProjectionSize(projection.size);
      const width = Math.max(0.025, Math.abs(size.x));
      const height = Math.max(0.025, Math.abs(size.y));
      const halfWidth = width * 0.5;
      const halfHeight = height * 0.5;
      let normalAxis = projection.normal.clone();
      if (normalAxis.lengthSquared() < 0.0001) normalAxis = new Vector3(0, 0, 1);
      normalAxis.normalize();
      const stableBasis = getProjectedStickerStableBasis(normalAxis, angle);
      const stableRight = stableBasis.right.clone().normalize();
      const stableUp = stableBasis.up.clone().normalize();
      const automaticSurface = isAutomaticProductSurfaceArea((projection as any).__asukaPrintSurfaceArea);

      type SourceTriangle = { face: number; vertices: Vector3[]; normal: Vector3; centroid: Vector3; planeD: number; score: number };
      const triangles: SourceTriangle[] = [];
      let seed: SourceTriangle | null = null;
      for (let face = 0; face + 2 < indices.length / 3; face += 1) {
        const base = face * 3;
        const a = readWorldVertex(Number(indices[base]) || 0);
        const b = readWorldVertex(Number(indices[base + 1]) || 0);
        const c = readWorldVertex(Number(indices[base + 2]) || 0);
        if (!a || !b || !c) continue;
        let faceNormal = Vector3.Cross(b.subtract(a), c.subtract(a));
        if (faceNormal.lengthSquared() < 0.0000001) continue;
        faceNormal.normalize();
        let normalDot = Vector3.Dot(faceNormal, normalAxis);
        if (normalDot < 0) {
          faceNormal = faceNormal.scale(-1);
          normalDot = -normalDot;
        }
        const centroid = a.add(b).add(c).scale(1 / 3);
        const planeD = Vector3.Dot(centroid.subtract(projection.position), normalAxis);
        const score = Math.abs(planeD) + (1 - Math.min(1, normalDot)) * 0.35 + centroid.subtract(projection.position).length() * 0.015;
        const triangle = { face, vertices: [a, b, c], normal: faceNormal, centroid, planeD, score };
        triangles.push(triangle);
        if (normalDot > 0.55 && (!seed || score < seed.score)) seed = triangle;
      }
      if (!seed) return null;
      normalAxis = seed.normal.clone().normalize();

      // Rebuild the printable source face from the actual mesh triangles, not
      // from an arbitrary projected bounding box. This is the important part:
      // adjacent fragments must be folded from the real face edge, otherwise they
      // can appear as detached panels next to the object.
      const planeEpsilon = Math.max(0.004, Math.min(Math.max(width, height) * 0.05, 0.035));
      const sourcePlaneD = seed.planeD;
      const sourcePoints: Vector3[] = [];
      const sourcePointKeys = new Set<string>();
      const addSourcePoint = (point: Vector3) => {
        const key = `${Math.round(point.x * 100000)}:${Math.round(point.y * 100000)}:${Math.round(point.z * 100000)}`;
        if (sourcePointKeys.has(key)) return;
        sourcePointKeys.add(key);
        sourcePoints.push(point);
      };
      for (const triangle of triangles) {
        const samePlane = Math.abs(triangle.planeD - sourcePlaneD) <= planeEpsilon;
        const sameNormal = Vector3.Dot(triangle.normal, normalAxis) >= 0.82;
        if (!samePlane || !sameNormal) continue;
        for (const point of triangle.vertices) addSourcePoint(point);
      }
      if (sourcePoints.length < 3) return null;
      const sourceOrigin = projection.position.add(normalAxis.scale(sourcePlaneD));

      let bestRight: Vector3 | null = null;
      let bestRightScore = -1;
      for (let i = 0; i < sourcePoints.length; i += 1) {
        for (let j = i + 1; j < sourcePoints.length; j += 1) {
          const edge = sourcePoints[j].subtract(sourcePoints[i]);
          const projected = edge.subtract(normalAxis.scale(Vector3.Dot(edge, normalAxis)));
          const length = projected.length();
          if (length < 0.01) continue;
          let dir = projected.scale(1 / length);
          const alignment = Vector3.Dot(dir, stableRight);
          if (alignment < 0) dir = dir.scale(-1);
          const score = Math.abs(alignment) * length;
          if (score > bestRightScore) {
            bestRightScore = score;
            bestRight = dir;
          }
        }
      }
      // An automatic surface already owns a stable world-up frame. Looking for
      // the longest aligned pair of face vertices can select a rectangle's
      // diagonal and turn that triangulation detail into a visible layer angle.
      let rightAxis = !automaticSurface && bestRight && bestRight.lengthSquared() > 0.0001
        ? bestRight.clone().normalize()
        : stableRight.clone().normalize();
      rightAxis = rightAxis.subtract(normalAxis.scale(Vector3.Dot(rightAxis, normalAxis)));
      if (rightAxis.lengthSquared() < 0.0001) rightAxis = stableRight.clone();
      rightAxis.normalize();
      let upAxis = Vector3.Cross(normalAxis, rightAxis);
      if (upAxis.lengthSquared() < 0.0001) upAxis = stableUp.clone();
      upAxis.normalize();
      // Keep vertical orientation close to the camera/stable basis without
      // breaking handedness. If the up axis flips, flip both in-plane axes.
      if (Vector3.Dot(upAxis, stableUp) < 0) {
        upAxis = upAxis.scale(-1);
        rightAxis = rightAxis.scale(-1);
      }

      let minU = Number.POSITIVE_INFINITY;
      let maxU = Number.NEGATIVE_INFINITY;
      let minV = Number.POSITIVE_INFINITY;
      let maxV = Number.NEGATIVE_INFINITY;
      for (const point of sourcePoints) {
        const delta = point.subtract(sourceOrigin);
        const u = Vector3.Dot(delta, rightAxis);
        const v = Vector3.Dot(delta, upAxis);
        minU = Math.min(minU, u); maxU = Math.max(maxU, u);
        minV = Math.min(minV, v); maxV = Math.max(maxV, v);
      }
      if (![minU, maxU, minV, maxV].every((value) => Number.isFinite(value))) return null;
      if (maxU - minU < 0.02 || maxV - minV < 0.02) return null;

      let depthSpan = 0;
      for (const point of allWorldVertices) {
        const insideDepth = -Vector3.Dot(point.subtract(sourceOrigin), normalAxis);
        if (insideDepth > depthSpan) depthSpan = insideDepth;
      }
      depthSpan = Math.max(0.02, depthSpan);
      const maxFace = Math.max(width, height, depthSpan);
      const surfaceOffset = Math.max(0.001, Math.min(maxFace * 0.003, 0.004));
      const overflowEpsilon = Math.max(0.0015, Math.min(Math.max(width, height) * 0.006, 0.004));
      const sU0 = -halfWidth;
      const sU1 = halfWidth;
      const sV0 = -halfHeight;
      const sV1 = halfHeight;
      const specs: BoxPrintSurfaceFragmentSpec[] = [];
      const addSpec = (spec: BoxPrintSurfaceFragmentSpec) => {
        if (spec.netU1 - spec.netU0 <= 0.0008 || spec.netV1 - spec.netV0 <= 0.0008) return;
        specs.push(spec);
      };
      const sourceU0 = Math.max(sU0, minU);
      const sourceU1 = Math.min(sU1, maxU);
      const sourceV0 = Math.max(sV0, minV);
      const sourceV1 = Math.min(sV1, maxV);
      const hasSourceOverlap = sourceU1 - sourceU0 > 0.001 && sourceV1 - sourceV0 > 0.001;
      if (!hasSourceOverlap) {
        debug3DLayer('print-surface3d:box-no-source-overlap', {
          ownerId,
          kind,
          targetMesh: summarizeLayerMeshForDebug(targetMesh),
          seedFace: seed.face,
          stickerRect: { sU0, sU1, sV0, sV1 },
          sourceBounds: { minU, maxU, minV, maxV },
        }, 'warn');
        return null;
      }
      addSpec({ id: 'source', netU0: sourceU0, netU1: sourceU1, netV0: sourceV0, netV1: sourceV1 });

      const sideV0 = sourceV0;
      const sideV1 = sourceV1;
      const sideU0 = sourceU0;
      const sideU1 = sourceU1;
      if (sideV1 > sideV0) {
        if (sU1 > maxU + overflowEpsilon) addSpec({ id: 'right', netU0: maxU, netU1: Math.min(sU1, maxU + depthSpan), netV0: sideV0, netV1: sideV1 });
        if (sU0 < minU - overflowEpsilon) addSpec({ id: 'left', netU0: Math.max(sU0, minU - depthSpan), netU1: minU, netV0: sideV0, netV1: sideV1 });
      }
      if (sideU1 > sideU0) {
        if (sV1 > maxV + overflowEpsilon) addSpec({ id: 'top', netU0: sideU0, netU1: sideU1, netV0: maxV, netV1: Math.min(sV1, maxV + depthSpan) });
        if (sV0 < minV - overflowEpsilon) addSpec({ id: 'bottom', netU0: sideU0, netU1: sideU1, netV0: Math.max(sV0, minV - depthSpan), netV1: minV });
      }
      if (!specs.length) return null;

      const sc = requireScene();
      const positionFor = (spec: BoxPrintSurfaceFragmentSpec, netU: number, netV: number): { position: Vector3; normal: Vector3 } => {
        let normal = normalAxis.clone();
        let position = sourceOrigin.add(rightAxis.scale(netU)).add(upAxis.scale(netV)).add(normalAxis.scale(surfaceOffset));
        if (spec.id === 'right') {
          const overflow = Math.max(0, netU - maxU);
          normal = rightAxis.clone();
          position = sourceOrigin.add(rightAxis.scale(maxU)).add(upAxis.scale(netV)).add(normalAxis.scale(-overflow)).add(normal.scale(surfaceOffset));
        } else if (spec.id === 'left') {
          const overflow = Math.max(0, minU - netU);
          normal = rightAxis.scale(-1);
          position = sourceOrigin.add(rightAxis.scale(minU)).add(upAxis.scale(netV)).add(normalAxis.scale(-overflow)).add(normal.scale(surfaceOffset));
        } else if (spec.id === 'top') {
          const overflow = Math.max(0, netV - maxV);
          normal = upAxis.clone();
          position = sourceOrigin.add(rightAxis.scale(netU)).add(upAxis.scale(maxV)).add(normalAxis.scale(-overflow)).add(normal.scale(surfaceOffset));
        } else if (spec.id === 'bottom') {
          const overflow = Math.max(0, minV - netV);
          normal = upAxis.scale(-1);
          position = sourceOrigin.add(rightAxis.scale(netU)).add(upAxis.scale(minV)).add(normalAxis.scale(-overflow)).add(normal.scale(surfaceOffset));
        }
        if (normal.lengthSquared() < 0.0001) normal = normalAxis.clone();
        normal.normalize();
        return { position, normal };
      };
      const makeFragment = (spec: BoxPrintSurfaceFragmentSpec, index: number): Mesh | null => {
        const meshId = index === 0 ? ownerId : `${ownerId}:print-surface:${spec.id}:${index}`;
        const mesh = new Mesh(meshId, sc);
        mesh.id = meshId;
        mesh.name = name || ownerId;
        const corners = [
          [spec.netU0, spec.netV0],
          [spec.netU1, spec.netV0],
          [spec.netU1, spec.netV1],
          [spec.netU0, spec.netV1],
        ] as const;
        const positions: number[] = [];
        const normals: number[] = [];
        const uvs: number[] = [];
        for (const [netU, netV] of corners) {
          const point = positionFor(spec, netU, netV);
          positions.push(point.position.x, point.position.y, point.position.z);
          normals.push(point.normal.x, point.normal.y, point.normal.z);
          const baseU = Math.max(0, Math.min(1, 0.5 + (netU / width)));
          const baseV = Math.max(0, Math.min(1, 0.5 - (netV / height)));
          const correctedUv = applyStickerSurfaceUvCorrection(projection, baseU, baseV);
          uvs.push(correctedUv.u, correctedUv.v);
        }
        const vertexData = new VertexData();
        vertexData.positions = positions;
        vertexData.normals = normals;
        vertexData.uvs = uvs;
        vertexData.indices = [0, 1, 2, 0, 2, 3];
        vertexData.applyToMesh(mesh, true);
        mesh.material = baseMaterial;
        mesh.isPickable = true;
        try {
          const groupId = Number((targetMesh as any)?.renderingGroupId ?? 0);
          mesh.renderingGroupId = Number.isFinite(groupId) ? groupId : 0;
        } catch {}
        setDecalRuntimeMetadata(mesh, targetMesh, size, normalAxis, angle, sourceOrigin);
        applyExplicitPrintAreaDepthDiscipline(mesh, baseMaterial, getExplicitProjectionPrintAreaTargetMesh(projection as any));
        setDecorationFragmentMetadata(ownerId, mesh);
        (mesh as any).__asukaProjectionMode = 'decal';
        (mesh as any).__asukaPrintSurface3DSticker = true;
        (mesh as any).__asukaPrintSurface3DFragment = spec.id;
        return mesh;
      };

      const fragments = specs.map((spec, index) => makeFragment(spec, index)).filter((mesh): mesh is Mesh => !!mesh && !mesh.isDisposed?.());
      if (!fragments.length) return null;
      applySurfacePatchMaterialBias(baseMaterial);
      const primary = fragments[0];
      objectMeshes.set(ownerId, primary);
      objectKinds.set(ownerId, kind);
      decalFragmentsByObjectId.set(ownerId, fragments.slice(1));
      if (selectedObjectId === ownerId) {
        try {
          primary.renderOutline = true;
          primary.outlineColor = color3FromInput('#2563eb');
          primary.outlineWidth = 0.035;
          (primary as any).showBoundingBox = true;
        } catch {}
        setDecalFragmentsSelectionState(ownerId, true);
      }
      debug3DLayer('print-surface3d:box-created', {
        ownerId,
        kind,
        targetMesh: summarizeLayerMeshForDebug(targetMesh),
        fragmentCount: fragments.length,
        fragments: fragments.map((fragment) => summarizeLayerMeshForDebug(fragment)),
        specs,
        seedFace: seed.face,
        sourceFacePoints: sourcePoints.map((point) => fromVec3(point)),
        frame: {
          origin: fromVec3(sourceOrigin),
          normal: fromVec3(normalAxis),
          right: fromVec3(rightAxis),
          up: fromVec3(upAxis),
          depthSpan,
        },
        stickerSize: { width, height, depth: size.z },
        bounds: { minU, maxU, minV, maxV, overflowEpsilon, planeEpsilon },
      });
      return primary;
    } catch (error) {
      debug3DLayer('print-surface3d:box-error', { ownerId, kind, targetMesh: summarizeLayerMeshForDebug(projection.targetMesh), error }, 'error');
      return null;
    }
  };


  const createSurfacePatchStickerMesh = (
    ownerIdInput: string,
    kind: string,
    name: string,
    baseMaterial: any,
    projection: { targetMesh: Mesh; position: Vector3; normal: Vector3; size: Vector3 },
    angle = 0,
    options: { register?: boolean; meshId?: string; wrapTargetMesh?: Mesh | null; allowedTriangleIndices?: Set<number> | null; normalThresholdOverride?: number | null; backDepthOverride?: number | null; searchWidthMultiplier?: number | null } = {},
  ): Mesh | null => {
    const ownerId = String(ownerIdInput ?? '').trim();
    const meshId = String(options.meshId ?? ownerId).trim() || ownerId;
    const registerPatch = options.register !== false;
    if (!ownerId || !projection?.targetMesh || !baseMaterial) return null;
    try {
      const sc = requireScene();
      if (registerPatch) {
        const targets = getProjectedStickerOverlayTargets(projection.targetMesh, projection);
        const uniqueTargets: Mesh[] = [];
        const seenTargets = new Set<string>();
        const addTarget = (target?: Mesh | null) => {
          if (!target || target.isDisposed?.()) return;
          const key = String((target as any).uniqueId ?? target.id ?? target.name ?? uniqueTargets.length);
          if (seenTargets.has(key)) return;
          seenTargets.add(key);
          uniqueTargets.push(target);
        };
        addTarget(projection.targetMesh);
        for (const target of targets) addTarget(target);
        if (uniqueTargets.length > 1) {
          const patches: Mesh[] = [];
          for (let index = 0; index < uniqueTargets.length; index += 1) {
            const target = uniqueTargets[index];
            const patch = createSurfacePatchStickerMesh(ownerId, kind, name, baseMaterial, { ...projection, targetMesh: target }, angle, {
              register: false,
              meshId: index === 0 ? ownerId : `${ownerId}:surface-patch:${index}`,
              wrapTargetMesh: projection.targetMesh,
            });
            if (patch && !patch.isDisposed?.()) patches.push(patch);
          }
          if (patches.length) {
            const primary = patches[0];
            const fragments = patches.slice(1);
            objectMeshes.set(ownerId, primary);
            objectKinds.set(ownerId, kind);
            decalFragmentsByObjectId.set(ownerId, fragments);
            if (selectedObjectId === ownerId) {
              try {
                primary.renderOutline = true;
                primary.outlineColor = color3FromInput('#2563eb');
                primary.outlineWidth = 0.035;
                (primary as any).showBoundingBox = true;
              } catch {}
              setDecalFragmentsSelectionState(ownerId, true);
            }
            debug3DLayer('decal-renderer:surface-patch-multi-target-created', {
              ownerId,
              kind,
              targetCount: uniqueTargets.length,
              patchCount: patches.length,
              targets: uniqueTargets.map((target) => summarizeLayerMeshForDebug(target)),
            });
            return primary;
          }
        }
      }
      const targetMesh = projection.targetMesh;
      const explicitProjectionTarget = getExplicitProjectionPrintAreaTargetMesh(projection as any);
      const rawPositions = targetMesh.getVerticesData?.('position' as any) as number[] | null;
      const rawIndices = targetMesh.getIndices?.() as number[] | null;
      if (!rawPositions || rawPositions.length < 9) {
        debug3DLayer('decal-renderer:surface-patch-no-positions', { ownerId, kind, targetMesh: summarizeLayerMeshForDebug(targetMesh) }, 'warn');
        return null;
      }
      const indices = rawIndices && rawIndices.length >= 3
        ? rawIndices.map((value) => Number(value) || 0)
        : Array.from({ length: Math.floor(rawPositions.length / 3) }, (_, index) => index);
      if (indices.length < 3) return null;

      const size = getSafeDecalProjectionSize(projection.size);
      const width = Math.max(0.025, Math.abs(size.x));
      const height = Math.max(0.025, Math.abs(size.y));
      // searchWidthMultiplier expands the candidate-search rect without
      // changing UV mapping — lets curved surfaces fill the sticker completely
      // instead of being clipped at the shoe's edge/toe/heel.
      const swm = Math.max(1, options.searchWidthMultiplier ?? 1);
      const halfWidth = width * 0.5 * swm;
      const halfHeight = height * 0.5 * swm;
      const maxFace = Math.max(width, height);
      const sourceTextureForDebug = baseMaterial?.diffuseTexture ?? baseMaterial?.albedoTexture ?? baseMaterial?.opacityTexture ?? null;
      let sourceImageWidth: number | null = null;
      let sourceImageHeight: number | null = null;
      let sourceImageAspect: number | null = null;
      try {
        const textureSize = sourceTextureForDebug && typeof sourceTextureForDebug.getSize === 'function'
          ? sourceTextureForDebug.getSize()
          : null;
        const w = Number((textureSize as any)?.width ?? 0) || 0;
        const h = Number((textureSize as any)?.height ?? 0) || 0;
        if (w > 0 && h > 0) {
          sourceImageWidth = w;
          sourceImageHeight = h;
          sourceImageAspect = w / h;
        }
      } catch {}
      const vertexCount = Math.floor(rawPositions.length / 3);
      const indexCount = indices.length;
      const simpleSurface = vertexCount <= 96 && indexCount <= 512;
      const isSecondarySurfaceTarget = !!options.wrapTargetMesh && options.wrapTargetMesh !== projection.targetMesh;
      // A sticker that crosses a hard cube edge often has a projector normal
      // halfway between the two real face normals (for example x/z). A strict
      // 0.74 threshold keeps only one face and makes the image stop at the edge.
      // Keep curved/complex meshes conservative, but let simple box-like meshes
      // include the second adjacent face when the projector is intentionally on
      // an edge/corner.
      const normalThreshold = options.normalThresholdOverride != null
        ? options.normalThresholdOverride
        : isSecondarySurfaceTarget ? (simpleSurface ? 0.50 : 0.24) : (simpleSurface ? 0.50 : 0.30);
      const frontDepth = isSecondarySurfaceTarget
        ? Math.max(0.018, Math.min(0.16, Math.max(size.z * 0.65, maxFace * 0.08)))
        : (simpleSurface
          ? Math.max(0.018, Math.min(0.12, Math.max(size.z * 0.35, maxFace * 0.11)))
          : Math.max(0.010, Math.min(0.035, maxFace * 0.035)));
      const backDepth = options.backDepthOverride != null
        ? options.backDepthOverride
        : (isSecondarySurfaceTarget
          ? Math.max(0.045, Math.min(0.22, Math.max(size.z * 1.15, maxFace * 0.18)))
          : (simpleSurface
            ? Math.max(0.045, Math.min(0.16, Math.max(size.z * 0.90, maxFace * 0.16)))
            : Math.max(0.035, Math.min(Math.max(size.z * 0.75, maxFace * 0.10), 0.13))));
      const rectMargin = Math.max(0.002, Math.min(maxFace * 0.012, 0.015));
      const surfaceOffset = Math.max(0.0008, Math.min(maxFace * 0.0025, 0.0035));

      let normalAxis = projection.normal.clone();
      if (normalAxis.lengthSquared() < 0.0001) normalAxis = new Vector3(0, 0, 1);
      normalAxis.normalize();
      const wrapTargetMesh = options.wrapTargetMesh && !options.wrapTargetMesh.isDisposed?.() ? options.wrapTargetMesh : projection.targetMesh;
      const wrap = getProjectedStickerWrapInfo({ ...projection, targetMesh: wrapTargetMesh }, size, angle);
      const basis = wrap.basis;
      const rightAxis = basis.right.clone().normalize();
      const upAxis = basis.up.clone().normalize();
      const rightEdge = wrap.edges.right;
      const leftEdge = wrap.edges.left;
      const topEdge = wrap.edges.top;
      const bottomEdge = wrap.edges.bottom;
      const visibleRight = Math.max(0, Math.min(width, rightEdge + halfWidth));
      const visibleLeft = Math.max(0, Math.min(width, leftEdge + halfWidth));
      const visibleTop = Math.max(0, Math.min(height, topEdge + halfHeight));
      const visibleBottom = Math.max(0, Math.min(height, bottomEdge + halfHeight));
      const rightRemain = Math.max(0, width - visibleRight);
      const leftRemain = Math.max(0, visibleLeft);
      const topRemain = Math.max(0, height - visibleTop);
      const bottomRemain = Math.max(0, visibleBottom);
      const sideBand = Math.max(0.012, Math.min(maxFace * 0.075, 0.055));
      const edgeSnapBand = simpleSurface ? Math.max(sideBand, Math.min(maxFace * 0.42, 0.18)) : sideBand;
      const sideWrapDepth = simpleSurface ? Math.max(size.z * 1.35, maxFace * 0.92, 0.16) : Math.max(size.z * 0.75, maxFace * 0.25, 0.06);
      const sideNormalThreshold = simpleSurface ? 0.08 : 0.16;
      const world = targetMesh.getWorldMatrix?.() ?? Matrix.Identity();
      const readLocal = (index: number) => {
        const offset = index * 3;
        if (offset + 2 >= rawPositions.length) return null;
        return new Vector3(
          Number(rawPositions[offset] ?? 0) || 0,
          Number(rawPositions[offset + 1] ?? 0) || 0,
          Number(rawPositions[offset + 2] ?? 0) || 0,
        );
      };
      const worldPointKey = (point: Vector3) => {
        // Babylon primitives and imported GLBs may duplicate vertices per face.
        // Index-only adjacency then sees the two triangles of a single rectangle
        // as disconnected, which produces the diagonal half-sticker visible in
        // the UI. Quantized world-space keys reconnect triangles that share the
        // same geometric corner even when their vertex indices differ.
        const scale = 100000;
        return `${Math.round(point.x * scale)}:${Math.round(point.y * scale)}:${Math.round(point.z * scale)}`;
      };
      type SurfacePatchWrapMode = 'front' | 'right' | 'left' | 'top' | 'bottom';
      const projectWorldPoint = (position: Vector3, orientedNormal: Vector3, mode: SurfacePatchWrapMode): SurfacePatchVertex => {
        const delta = position.subtract(projection.position);
        const rawU = Vector3.Dot(delta, rightAxis);
        const rawV = Vector3.Dot(delta, upAxis);
        const depth = Vector3.Dot(delta, normalAxis);
        const depthBehind = Math.max(-depth, 0);
        let u = rawU;
        let v = rawV;
        // For hard-edge wrapping, keep UV continuity from the real edge
        // coordinate instead of using the clamped visible span. When a sticker
        // reaches a cube edge, the clamped span can be equal to the sticker
        // width and pushes the adjacent face outside the [0..1] UV range,
        // creating a detached/floating-looking fragment. The unwrapped distance
        // from the edge is the stable coordinate we want here.
        if (mode === 'right') u = rightEdge + depthBehind;
        else if (mode === 'left') u = leftEdge - depthBehind;
        else if (mode === 'top') v = topEdge + depthBehind;
        else if (mode === 'bottom') v = bottomEdge - depthBehind;
        return { position, normal: orientedNormal, u, v, depth };
      };
      const buildSurfacePatchRectFallback = (candidate: SurfacePatchCandidate): SurfacePatchVertex[] => {
        const minU = Math.max(-halfWidth, Math.min(...candidate.vertices.map((vertex) => vertex.u)));
        const maxU = Math.min(halfWidth, Math.max(...candidate.vertices.map((vertex) => vertex.u)));
        const minV = Math.max(-halfHeight, Math.min(...candidate.vertices.map((vertex) => vertex.v)));
        const maxV = Math.min(halfHeight, Math.max(...candidate.vertices.map((vertex) => vertex.v)));
        if (!(maxU > minU && maxV > minV)) return [];
        const planePoint = candidate.vertices[0]?.position ?? projection.position;
        const planeNormal = candidate.normal.lengthSquared() > 0.000001 ? candidate.normal.clone().normalize() : normalAxis.clone();
        const rayNormalDot = Vector3.Dot(normalAxis, planeNormal);
        const projectToFacePlane = (u: number, v: number): SurfacePatchVertex => {
          const projected = projection.position.add(rightAxis.scale(u)).add(upAxis.scale(v));
          const t = Math.abs(rayNormalDot) > 0.000001 ? Vector3.Dot(planePoint.subtract(projected), planeNormal) / rayNormalDot : 0;
          const position = projected.add(normalAxis.scale(t));
          const depth = Vector3.Dot(position.subtract(projection.position), normalAxis);
          return { position, normal: planeNormal, u, v, depth };
        };
        return [
          projectToFacePlane(minU, minV),
          projectToFacePlane(maxU, minV),
          projectToFacePlane(maxU, maxV),
          projectToFacePlane(minU, maxV),
        ];
      };

      const candidates = new Map<number, SurfacePatchCandidate>();
      let seedFace: number | null = null;
      let bestSeedScore = Number.POSITIVE_INFINITY;
      const allowedTriangleIndices = options.allowedTriangleIndices ?? null;
      const triangleCount = Math.floor(indices.length / 3);
      // Fast path: when a triangle filter is active (complex-mesh Drawing Area
      // fallback), iterate the selected-triangle Set directly instead of scanning
      // ALL mesh triangles.  This is O(N_selected) vs O(N_total) — critical for
      // GLBs with tens-of-thousands of triangles where only a small subset is
      // painted by the user.
      const triangleIterator: Iterable<number> = allowedTriangleIndices
        ? allowedTriangleIndices
        : (function* () { for (let f = 0; f < triangleCount; f++) yield f; })();
      for (const face of triangleIterator) {
        if (face < 0 || face >= triangleCount) continue;
        const base = face * 3;
        const ia = Number(indices[base]);
        const ib = Number(indices[base + 1]);
        const ic = Number(indices[base + 2]);
        if (!Number.isFinite(ia) || !Number.isFinite(ib) || !Number.isFinite(ic)) continue;
        const aLocal = readLocal(ia);
        const bLocal = readLocal(ib);
        const cLocal = readLocal(ic);
        if (!aLocal || !bLocal || !cLocal) continue;
        const aWorld = Vector3.TransformCoordinates(aLocal, world);
        const bWorld = Vector3.TransformCoordinates(bLocal, world);
        const cWorld = Vector3.TransformCoordinates(cLocal, world);
        let faceNormal = Vector3.Cross(bWorld.subtract(aWorld), cWorld.subtract(aWorld));
        if (faceNormal.lengthSquared() < 0.0000001) continue;
        faceNormal.normalize();
        let normalDot = Vector3.Dot(faceNormal, normalAxis);
        const absNormalDot = Math.abs(normalDot);
        // Negative root scales can flip winding. Use an oriented copy for the decal
        // normals, but keep the spatial/depth tests strict so the opposite side is
        // not painted through the object.
        if (normalDot < 0) faceNormal = faceNormal.scale(-1);
        const centroid = aWorld.add(bWorld).add(cWorld).scale(1 / 3);
        const centroidDelta = centroid.subtract(projection.position);
        const cu = Vector3.Dot(centroidDelta, rightAxis);
        const cv = Vector3.Dot(centroidDelta, upAxis);
        const cd = Vector3.Dot(centroidDelta, normalAxis);
        const rawVertices = [
          projectWorldPoint(aWorld, faceNormal, 'front'),
          projectWorldPoint(bWorld, faceNormal, 'front'),
          projectWorldPoint(cWorld, faceNormal, 'front'),
        ];
        const rawMinU = Math.min(rawVertices[0].u, rawVertices[1].u, rawVertices[2].u);
        const rawMaxU = Math.max(rawVertices[0].u, rawVertices[1].u, rawVertices[2].u);
        const rawMinV = Math.min(rawVertices[0].v, rawVertices[1].v, rawVertices[2].v);
        const rawMaxV = Math.max(rawVertices[0].v, rawVertices[1].v, rawVertices[2].v);
        const rawMinDepth = Math.min(rawVertices[0].depth, rawVertices[1].depth, rawVertices[2].depth);
        const rawMaxDepth = Math.max(rawVertices[0].depth, rawVertices[1].depth, rawVertices[2].depth);
        const rawMinBehind = Math.min(
          Math.max(-rawVertices[0].depth, 0),
          Math.max(-rawVertices[1].depth, 0),
          Math.max(-rawVertices[2].depth, 0),
        );
        const rawMaxBehind = Math.max(
          Math.max(-rawVertices[0].depth, 0),
          Math.max(-rawVertices[1].depth, 0),
          Math.max(-rawVertices[2].depth, 0),
        );
        const rectIntersects = rawMaxU >= -halfWidth - rectMargin
          && rawMinU <= halfWidth + rectMargin
          && rawMaxV >= -halfHeight - rectMargin
          && rawMinV <= halfHeight + rectMargin;
        let mode: SurfacePatchWrapMode | null = null;
        if (allowedTriangleIndices) {
          // Pre-selected triangles on organic/complex meshes: relax the strict
          // normal threshold (0.30) to allow curved surfaces, but still require
          // normalDot > 0 so backface (internal) triangles are excluded — without
          // this the projector paints the inside of the shoe instead of the outside.
          const rectAndDepthOk = rectIntersects
            && normalDot > 0
            && rawMinDepth <= frontDepth + rectMargin
            && rawMaxDepth >= -backDepth - rectMargin;
          if (rectAndDepthOk) mode = 'front';
        } else {
          // Candidate detection must be intersection-based, not centroid-based.
          // A sticker crossing the diagonal split of a quad often intersects the
          // second triangle while that triangle centroid is outside the sticker
          // rectangle. Centroid gating was the source of triangular missing areas.
          const frontCandidate = absNormalDot >= normalThreshold
            && rectIntersects
            && rawMinDepth <= frontDepth + rectMargin
            && rawMaxDepth >= -backDepth - rectMargin;
          if (frontCandidate) mode = 'front';
          else if ((rightRemain > 0.001 || (simpleSurface && rightEdge <= halfWidth + edgeSnapBand && rightEdge >= -halfWidth - edgeSnapBand))
            && rawMaxV >= -halfHeight - rectMargin && rawMinV <= halfHeight + rectMargin
            && rawMaxU >= rightEdge - edgeSnapBand && rawMinU <= rightEdge + edgeSnapBand
            && rawMaxBehind >= -rectMargin && rawMinBehind <= Math.max(rightRemain, sideWrapDepth) + sideBand
            && Math.abs(Vector3.Dot(faceNormal, rightAxis)) >= sideNormalThreshold) mode = 'right';
          else if ((leftRemain > 0.001 || (simpleSurface && leftEdge <= halfWidth + edgeSnapBand && leftEdge >= -halfWidth - edgeSnapBand))
            && rawMaxV >= -halfHeight - rectMargin && rawMinV <= halfHeight + rectMargin
            && rawMaxU >= leftEdge - edgeSnapBand && rawMinU <= leftEdge + edgeSnapBand
            && rawMaxBehind >= -rectMargin && rawMinBehind <= Math.max(leftRemain, sideWrapDepth) + sideBand
            && Math.abs(Vector3.Dot(faceNormal, rightAxis)) >= sideNormalThreshold) mode = 'left';
          else if ((topRemain > 0.001 || (simpleSurface && topEdge <= halfHeight + edgeSnapBand && topEdge >= -halfHeight - edgeSnapBand))
            && rawMaxU >= -halfWidth - rectMargin && rawMinU <= halfWidth + rectMargin
            && rawMaxV >= topEdge - edgeSnapBand && rawMinV <= topEdge + edgeSnapBand
            && rawMaxBehind >= -rectMargin && rawMinBehind <= Math.max(topRemain, sideWrapDepth) + sideBand
            && Math.abs(Vector3.Dot(faceNormal, upAxis)) >= sideNormalThreshold) mode = 'top';
          else if ((bottomRemain > 0.001 || (simpleSurface && bottomEdge <= halfHeight + edgeSnapBand && bottomEdge >= -halfHeight - edgeSnapBand))
            && rawMaxU >= -halfWidth - rectMargin && rawMinU <= halfWidth + rectMargin
            && rawMaxV >= bottomEdge - edgeSnapBand && rawMinV <= bottomEdge + edgeSnapBand
            && rawMaxBehind >= -rectMargin && rawMinBehind <= Math.max(bottomRemain, sideWrapDepth) + sideBand
            && Math.abs(Vector3.Dot(faceNormal, upAxis)) >= sideNormalThreshold) mode = 'bottom';
        }
        if (!mode) continue;
        const vertices = mode === 'front' ? rawVertices : [
          projectWorldPoint(aWorld, faceNormal, mode),
          projectWorldPoint(bWorld, faceNormal, mode),
          projectWorldPoint(cWorld, faceNormal, mode),
        ];
        const minU = Math.min(vertices[0].u, vertices[1].u, vertices[2].u);
        const maxU = Math.max(vertices[0].u, vertices[1].u, vertices[2].u);
        const minV = Math.min(vertices[0].v, vertices[1].v, vertices[2].v);
        const maxV = Math.max(vertices[0].v, vertices[1].v, vertices[2].v);
        const minDepth = Math.min(vertices[0].depth, vertices[1].depth, vertices[2].depth);
        const maxDepth = Math.max(vertices[0].depth, vertices[1].depth, vertices[2].depth);
        if (maxU < -halfWidth - rectMargin || minU > halfWidth + rectMargin) continue;
        if (maxV < -halfHeight - rectMargin || minV > halfHeight + rectMargin) continue;
        if (mode === 'front' && (minDepth > frontDepth || maxDepth < -backDepth)) continue;
        const score = Math.abs(cd) * 8 + Math.max(0, Math.abs(cu) - halfWidth) + Math.max(0, Math.abs(cv) - halfHeight) + (1 - Math.min(absNormalDot, 1)) * 0.5;
        candidates.set(face, {
          face,
          vertexKeys: [ia, ib, ic],
          pointKeys: [worldPointKey(aWorld), worldPointKey(bWorld), worldPointKey(cWorld)],
          vertices,
          normal: faceNormal,
          score,
        });
        if (score < bestSeedScore) {
          bestSeedScore = score;
          seedFace = face;
        }
      }
      if (seedFace == null || !candidates.size) {
        debug3DLayer('decal-renderer:surface-patch-no-candidates', {
          ownerId,
          kind,
          targetMesh: summarizeLayerMeshForDebug(targetMesh),
          projection: { position: fromVec3(projection.position), normal: fromVec3(normalAxis), size: fromVec3(size) },
          thresholds: { normalThreshold, frontDepth, backDepth, simpleSurface, isSecondarySurfaceTarget, rightRemain, leftRemain, topRemain, bottomRemain, rightEdge, leftEdge, topEdge, bottomEdge, edgeSnapBand, sideWrapDepth },
        }, 'warn');
        return null;
      }

      const seedCandidate = seedFace != null ? (candidates.get(seedFace) ?? null) : null;
      if (seedCandidate) {
        const centroid = seedCandidate.vertices.reduce((acc, vertex) => acc.add(vertex.position), Vector3.Zero()).scale(1 / Math.max(seedCandidate.vertices.length, 1));
        debug3DLayer('seed-face:selected', {
          ownerId,
          kind,
          meshId: String(targetMesh.id || targetMesh.name || ''),
          faceId: seedFace,
          pickedPoint: fromVec3(projection.position),
          faceCentroid: fromVec3(centroid),
          normal: fromVec3(seedCandidate.normal),
          tangent: fromVec3(rightAxis),
          bitangent: fromVec3(upAxis),
          imageSize: { width: sourceImageWidth, height: sourceImageHeight, aspect: sourceImageAspect },
          stickerSize: { width, height, depth: size.z },
          thresholds: { normalThreshold, frontDepth, backDepth, simpleSurface, isSecondarySurfaceTarget, rightRemain, leftRemain, topRemain, bottomRemain, rightEdge, leftEdge, topEdge, bottomEdge, edgeSnapBand, sideWrapDepth },
        });
      }

      const facesByVertex = new Map<number, number[]>();
      const facesByPoint = new Map<string, number[]>();
      for (const candidate of candidates.values()) {
        for (const key of candidate.vertexKeys) {
          const list = facesByVertex.get(key) ?? [];
          list.push(candidate.face);
          facesByVertex.set(key, list);
        }
        for (const key of candidate.pointKeys) {
          const list = facesByPoint.get(key) ?? [];
          list.push(candidate.face);
          facesByPoint.set(key, list);
        }
      }
      const keptFaces = new Set<number>();
      if (allowedTriangleIndices) {
        // Pre-selected triangles: include all candidates unconditionally.
        // GLB organic meshes have per-face duplicate vertices so the BFS would
        // find zero adjacency and keep only the seed — include everything.
        for (const face of candidates.keys()) keptFaces.add(face);
      }
      const queue = allowedTriangleIndices ? [] : [seedFace];
      const loggedAdjacencyEdges = new Set<string>();
      while (queue.length) {
        const face = queue.shift()!;
        if (keptFaces.has(face)) continue;
        const candidate = candidates.get(face);
        if (!candidate) continue;
        keptFaces.add(face);
        const enqueueNeighbor = (neighbor: number, via: 'vertex' | 'point', key: number | string) => {
          if (neighbor === face) return;
          const neighborCandidate = candidates.get(neighbor);
          if (!neighborCandidate) return;
          const edgeKey = `${Math.min(face, neighbor)}:${Math.max(face, neighbor)}:${via}`;
          if (!loggedAdjacencyEdges.has(edgeKey)) {
            loggedAdjacencyEdges.add(edgeKey);
            const sharedVertexKeys = candidate.vertexKeys.filter((value) => neighborCandidate.vertexKeys.includes(value));
            const sharedPointKeys = candidate.pointKeys.filter((value) => neighborCandidate.pointKeys.includes(value));
            const dot = Math.max(-1, Math.min(1, Vector3.Dot(candidate.normal, neighborCandidate.normal)));
            const angleDeg = (Math.acos(dot) * 180) / Math.PI;
            const handedness = Vector3.Dot(Vector3.Cross(rightAxis, upAxis), normalAxis);
            debug3DLayer('patch:adjacency-edge', {
              ownerId,
              kind,
              meshId: String(targetMesh.id || targetMesh.name || ''),
              fromFaceId: face,
              toFaceId: neighbor,
              via,
              sharedKey: key,
              sharedVertexKeys,
              sharedPointKeys,
              fromNormal: fromVec3(candidate.normal),
              toNormal: fromVec3(neighborCandidate.normal),
              angleDeg,
            });
            debug3DLayer('patch:frame-transport', {
              ownerId,
              kind,
              meshId: String(targetMesh.id || targetMesh.name || ''),
              fromFaceId: face,
              toFaceId: neighbor,
              via,
              tangentBefore: fromVec3(rightAxis),
              bitangentBefore: fromVec3(upAxis),
              normalBefore: fromVec3(candidate.normal),
              tangentAfter: fromVec3(rightAxis),
              bitangentAfter: fromVec3(upAxis),
              normalAfter: fromVec3(neighborCandidate.normal),
              handedness,
              flipped: dot < 0,
            });
          }
          if (!keptFaces.has(neighbor)) queue.push(neighbor);
        };
        for (const key of candidate.vertexKeys) {
          for (const neighbor of facesByVertex.get(key) ?? []) enqueueNeighbor(neighbor, 'vertex', key);
        }
        for (const key of candidate.pointKeys) {
          for (const neighbor of facesByPoint.get(key) ?? []) enqueueNeighbor(neighbor, 'point', key);
        }
      }

      const outPositions: number[] = [];
      const outNormals: number[] = [];
      const outUvs: number[] = [];
      const outIndices: number[] = [];
      let triangleEvalLogCount = 0;
      const maxTriangleEvalLogs = 240;
      const pushPatchVertex = (vertex: SurfacePatchVertex) => {
        const normal = vertex.normal.lengthSquared() > 0.000001 ? vertex.normal.clone().normalize() : normalAxis.clone();
        const position = vertex.position.add(normal.scale(surfaceOffset));
        const index = outPositions.length / 3;
        outPositions.push(position.x, position.y, position.z);
        outNormals.push(normal.x, normal.y, normal.z);
        const baseU = 0.5 + (vertex.u / width);
        const baseV = 0.5 - (vertex.v / height);
        const correctedUv = applyStickerSurfaceUvCorrection(projection, baseU, baseV);
        outUvs.push(correctedUv.u, correctedUv.v);
        return index;
      };
      for (const face of keptFaces) {
        const candidate = candidates.get(face);
        if (!candidate) continue;
        const rawMinU = Math.min(...candidate.vertices.map((vertex) => vertex.u));
        const rawMaxU = Math.max(...candidate.vertices.map((vertex) => vertex.u));
        const rawMinV = Math.min(...candidate.vertices.map((vertex) => vertex.v));
        const rawMaxV = Math.max(...candidate.vertices.map((vertex) => vertex.v));
        const rawMinDepth = Math.min(...candidate.vertices.map((vertex) => vertex.depth));
        const rawMaxDepth = Math.max(...candidate.vertices.map((vertex) => vertex.depth));
        let polygon = clipSurfacePatchTriangleToStickerRect(candidate.vertices, halfWidth, halfHeight);
        // Keep the fallback disabled on simple product meshes. It creates a
        // mathematically valid rectangle on the target face plane, but it is no
        // longer clipped by the real triangle boundaries; that is exactly what
        // produced the detached strip visible outside the cube. For boxes we
        // prefer the real clipped triangle patch, even when that means a face is
        // skipped until the edge-wrapping test accepts it properly.
        const usedRectFallback = !simpleSurface && polygon.length < 3 && rawMaxU >= -halfWidth - rectMargin
          && rawMinU <= halfWidth + rectMargin
          && rawMaxV >= -halfHeight - rectMargin
          && rawMinV <= halfHeight + rectMargin;
        if (usedRectFallback) polygon = buildSurfacePatchRectFallback(candidate);
        if (triangleEvalLogCount < maxTriangleEvalLogs) {
          triangleEvalLogCount += 1;
          debug3DLayer('patch:triangle-eval', {
            ownerId,
            kind,
            meshId: String(targetMesh.id || targetMesh.name || ''),
            faceId: face,
            uvBounds: { uMin: rawMinU, uMax: rawMaxU, vMin: rawMinV, vMax: rawMaxV },
            depthRange: { min: rawMinDepth, max: rawMaxDepth },
            normalDot: Vector3.Dot(candidate.normal, normalAxis),
            accepted: polygon.length >= 3,
            clippedVertexCount: polygon.length,
            usedRectFallback,
            rejectReason: polygon.length >= 3 ? null : 'clipped-outside-sticker-rect',
          });
        }
        if (polygon.length < 3) continue;
        const firstIndex = pushPatchVertex(polygon[0]);
        for (let i = 1; i + 1 < polygon.length; i += 1) {
          const bIndex = pushPatchVertex(polygon[i]);
          const cIndex = pushPatchVertex(polygon[i + 1]);
          // Reverse winding (c,b instead of b,c): the shoe GLB uses CCW winding
          // (right-handed Blender convention). Babylon.js renders it with
          // backFaceCulling=false so the shoe looks correct, but the surface
          // patch inherits CCW triangles and the camera sees their back face,
          // which causes UV to appear mirrored. CW = front-face in Babylon.js.
          outIndices.push(firstIndex, cIndex, bIndex);
        }
      }
      if (outIndices.length < 3 || outPositions.length < 9) {
        debug3DLayer('decal-renderer:surface-patch-empty-after-clip', {
          ownerId,
          kind,
          candidateCount: candidates.size,
          connectedFaces: keptFaces.size,
          targetMesh: summarizeLayerMeshForDebug(targetMesh),
        }, 'warn');
        return null;
      }

      const patch = new Mesh(meshId, sc);
      patch.id = meshId;
      patch.name = name || ownerId;
      const vertexData = new VertexData();
      vertexData.positions = outPositions;
      vertexData.indices = outIndices;
      vertexData.normals = outNormals;
      vertexData.uvs = outUvs;
      vertexData.applyToMesh(patch, true);
      patch.material = baseMaterial;
      applySurfacePatchMaterialBias(baseMaterial);
      if (explicitProjectionTarget) applyExplicitPrintAreaDepthDiscipline(patch, baseMaterial, targetMesh);
      patch.isPickable = true;
      try {
        const targetRenderingGroupId = Number((targetMesh as any)?.renderingGroupId ?? 0);
        patch.renderingGroupId = Number.isFinite(targetRenderingGroupId) ? targetRenderingGroupId : 0;
      } catch {}
      setDecalRuntimeMetadata(patch, targetMesh, size, normalAxis, angle, projection.position);
      if (explicitProjectionTarget) applyExplicitPrintAreaDepthDiscipline(patch, baseMaterial, targetMesh);
      (patch as any).__asukaProjectionMode = 'decal';
      (patch as any).__asukaSurfacePatchSticker = true;
      (patch as any).__asukaUsingSurfacePatch = true;
      (patch as any).__asukaSurfacePatchSourceMeshId = String(targetMesh.id || targetMesh.name || '');
      if ((projection as any).__asukaFromFixedSlot3D === true) {
        try { (patch as any).__asukaFromFixedSlot3D = true; } catch {}
      }
      if (registerPatch) {
        objectMeshes.set(ownerId, patch);
        objectKinds.set(ownerId, kind);
        decalFragmentsByObjectId.set(ownerId, []);
      } else {
        setDecorationFragmentMetadata(ownerId, patch);
      }
      if (selectedObjectId === ownerId) {
        try {
          patch.renderOutline = true;
          patch.outlineColor = color3FromInput('#2563eb');
          patch.outlineWidth = 0.035;
          (patch as any).showBoundingBox = (patch as any).__asukaExactFaceNetPrintSurface3D === true ? false : true;
        } catch {}
      }
      debug3DLayer('decal-renderer:surface-patch-created', {
        ownerId,
        kind,
        targetMesh: summarizeLayerMeshForDebug(targetMesh),
        patch: summarizeLayerMeshForDebug(patch),
        candidateCount: candidates.size,
        connectedFaces: keptFaces.size,
        outputTriangles: outIndices.length / 3,
        seedFace,
        simpleSurface,
        thresholds: { normalThreshold, frontDepth, backDepth, surfaceOffset, isSecondarySurfaceTarget, rightRemain, leftRemain, topRemain, bottomRemain, rightEdge, leftEdge, topEdge, bottomEdge, edgeSnapBand, sideWrapDepth },
        projection: { position: fromVec3(projection.position), normal: fromVec3(normalAxis), size: fromVec3(size) },
      });
      debug3DLayer('patch:build-result', {
        ownerId,
        kind,
        meshId: String(targetMesh.id || targetMesh.name || ''),
        seedFace,
        candidateCount: candidates.size,
        connectedFaces: keptFaces.size,
        outputTriangles: outIndices.length / 3,
        outputVertices: outPositions.length / 3,
        patch: summarizeLayerMeshForDebug(patch),
        targetMesh: summarizeLayerMeshForDebug(targetMesh),
        hasSelectionProxy: true,
      });
      debug3DLayer('layer:selection-proxy', {
        ownerId,
        kind,
        proxyCreated: true,
        proxyMeshId: patch.id,
        fragmentIds: (decalFragmentsByObjectId.get(ownerId) ?? []).map((fragment) => String(fragment?.id ?? fragment?.name ?? '')),
        patch: summarizeLayerMeshForDebug(patch),
      });
      return patch;
    } catch (error) {
      debug3DLayer('decal-renderer:surface-patch-error', { ownerId, kind, error }, 'error');
      return null;
    }
  };

  const getMeshStableName = (mesh?: Mesh | null): string => String(mesh?.id ?? mesh?.name ?? '').trim().toLowerCase();

  const isLogical3DPartMesh = (mesh?: Mesh | null): boolean => {
    const id = getMeshStableName(mesh);
    if (!id) return false;
    if (id.startsWith('asuka_part_')) return true;
    const partId = String((mesh as any)?.metadata?.asuka?.partId ?? (mesh as any)?.metadata?.partId ?? '').trim();
    if (partId) return true;
    const extras = (mesh as any)?.metadata?.gltf?.extras ?? (mesh as any)?._gltf?.extras ?? null;
    if (extras?.asuka?.partId || extras?.partId) return true;
    return false;
  };

  const getTargetBoundProjectedStickerTargetMesh = (projection?: any): Mesh | null => {
    const explicitTarget = getExplicitProjectionPrintAreaTargetMesh(projection);
    if (explicitTarget) return explicitTarget;
    const markerTarget = projection?.__asukaFromFixedSlot3D === true ? projection?.targetMesh : null;
    return markerTarget instanceof Mesh && isLogical3DPartMesh(markerTarget) && !markerTarget.isDisposed?.()
      ? markerTarget
      : null;
  };

  const getProjectedStickerOverlayTargets = (
    preferredTarget?: Mesh | null,
    projection?: { position: Vector3; normal: Vector3; size: Vector3 } | null,
  ): Mesh[] => {
    const preferred = preferredTarget && !preferredTarget.isDisposed?.() ? preferredTarget : null;
    const renderable = getRenderableModelMeshes(modelMeshes);
    const logicalPartMeshes = renderable.filter((mesh) => isLogical3DPartMesh(mesh));
    const preferredIsLogicalPart = isLogical3DPartMesh(preferred);
    const projectionArea = (projection as any)?.__asukaPrintSurfaceArea ?? null;
    const autoProductSurface = isAutomaticProductSurfaceArea(projectionArea)
      || projectionArea?.metadata?.autoProductSurface === true
      || projectionArea?.id === AUTO_PRODUCT_SURFACE_AREA_ID;
    const targetBoundProjectionTarget = getTargetBoundProjectedStickerTargetMesh(projection as any);
    if (!autoProductSurface && targetBoundProjectionTarget) {
      return !targetBoundProjectionTarget.isDisposed?.() ? [targetBoundProjectionTarget] : [];
    }

    // Automatic product-surface stickers on dense multi-part GLBs (shoe upper,
    // laces, support meshes, sole...) must survive crossing part boundaries.
    // A one-mesh overlay disappears as soon as the projected rectangle moves
    // from the original part onto a neighboring part, so render the shader over
    // the product's visible renderable meshes and let the shader/depth tests
    // discard pixels outside the sticker volume.
    if (autoProductSurface && logicalPartMeshes.length > 1) return renderable;

    // Critical for multi-part GLB products such as shoes: the projected shader
    // renders by cloning target meshes. Cloning every renderable mesh makes a
    // sticker placed on the exterior also appear on tongue/interior/collar
    // meshes that happen to be inside the same projector rectangle. When the
    // source surface is a real product part, keep the shader overlay on that
    // exact part only. Wrap across two/three faces still works on box-like
    // products because those faces are usually the same mesh.
    if (preferred && (preferredIsLogicalPart || logicalPartMeshes.length > 1)) return [preferred];

    const volumeTargets = getProjectedDecalTargetMeshes(preferred, projection ?? null);
    if (volumeTargets.length) return volumeTargets;
    return preferred ? [preferred] : renderable;
  };

  const createProjectedStickerShaderFragments = (
    ownerIdInput: string,
    kind: string,
    name: string,
    baseMaterial: any,
    projection: { targetMesh: Mesh; position: Vector3; normal: Vector3; size: Vector3 },
    angle = 0,
  ): Mesh | null => {
    const ownerId = String(ownerIdInput ?? '').trim();
    if (!ownerId || !projection?.targetMesh || !baseMaterial) return null;
    const sourceTexture = baseMaterial?.diffuseTexture ?? baseMaterial?.albedoTexture ?? baseMaterial?.opacityTexture ?? null;
    debug3DLayer('projected-sticker:create:start', {
      ownerId,
      kind,
      name,
      targetMesh: summarizeLayerMeshForDebug(projection.targetMesh),
      projection: { position: fromVec3(projection.position), normal: fromVec3(projection.normal), size: fromVec3(projection.size) },
      sourceTexture: sourceTexture ? { name: String(sourceTexture?.name ?? sourceTexture?.url ?? ''), url: String(sourceTexture?.url ?? '').slice(0, 180), hasAlpha: sourceTexture?.hasAlpha, ready: (() => { try { return sourceTexture.isReady?.(); } catch { return undefined; } })() } : null,
    });
    if (!sourceTexture) {
      debug3DLayer('projected-sticker:create:no-source-texture', { ownerId, kind, material: summarizeLayerMaterialForDebug(baseMaterial) }, 'warn');
      return null;
    }
    const size = getSafeDecalProjectionSize(projection.size);
    const safeProjection = {
      ...projection,
      size,
      position: getSafeDecalProjectionPosition(projection.position, projection.normal, size),
    };
    const wrap = getProjectedStickerWrapInfo(safeProjection, size, angle);
    const control = createProjectedStickerControlMesh(ownerId, name, safeProjection, size, wrap.basis, angle);
    if (!control) {
      debug3DLayer('projected-sticker:create:no-control', { ownerId, kind }, 'warn');
      return null;
    }
    (control as any).__asukaSourceMaterial = baseMaterial;
    (control as any).__asukaSourceTexture = sourceTexture;
    (control as any).__asukaUsingSurfacePatch = true;
    (control as any).__asukaProjectedStickerShader = true;
    try {
      // Surface drags build the replacement alongside the currently visible
      // overlay. The caller swaps and disposes the old fragments only after the
      // replacement is valid, so a transient shader/build failure cannot erase
      // the layer on pointerup.
      if ((projection as any).__asukaPreserveExistingFragments !== true) {
        disposeDecalFragments(ownerId, true);
      }
      ensureProjectedStickerShaderRegistered();
      const sc = requireScene();
      const targetBoundProjectionTarget = getTargetBoundProjectedStickerTargetMesh(safeProjection as any);
      const shader = new ShaderMaterial(`${ownerId}-projected-sticker-shader`, sc, {
        vertex: 'asukaProjectedSticker',
        fragment: 'asukaProjectedSticker',
      }, {
        attributes: ['position', 'normal', 'uv'],
        uniforms: ['world', 'worldViewProjection', 'viewProjection', 'uStickerCenter', 'uStickerRight', 'uStickerUp', 'uStickerNormal', 'uStickerSize', 'uCameraPosition', 'uWrapSide', 'uEdgeLocal', 'uStickerBackDepth', 'uStickerEdges', 'uFlipU', 'uTargetBound', 'uSurfaceOffset'],
        samplers: ['uSampler'],
        needAlphaBlending: true,
      } as any);
      (shader as any).__asukaRuntimeProjectedStickerMaterial = true;
      (shader as any).__asukaTargetBoundProjectedStickerMaterial = !!targetBoundProjectionTarget;
      // Keep the stable sticker basis untouched. Occlusion must not depend on
      // Babylon face winding, because some GLB meshes expose valid visible
      // surfaces as back-facing triangles. The shader now discards fragments
      // whose surface normal faces away from the camera, and the overlay is
      // rendered in the same rendering group as the product so the product
      // depth buffer hides opposite faces.
      shader.backFaceCulling = false;
      try { (shader as any).transparencyMode = 2; } catch {}
      try { (shader as any).forceDepthWrite = false; } catch {}
      try { (shader as any).needDepthPrePass = false; } catch {}
      try { (shader as any).zOffset = targetBoundProjectionTarget ? TARGET_BOUND_STICKER_RENDER_POLICY.zOffset : -8; } catch {}
      try { (shader as any).zOffsetUnits = targetBoundProjectionTarget ? TARGET_BOUND_STICKER_RENDER_POLICY.zOffsetUnits : -8; } catch {}
      if (targetBoundProjectionTarget) {
        try { (shader as any).depthFunction = Engine.LEQUAL; } catch {}
      }
      let _shCamRight = new Vector3(1, 0, 0);
      try {
        let _shCamFwd = new Vector3(0, 0, -1);
        const _shCamTgt = camera ? (camera as any).target : null;
        if (_shCamTgt instanceof Vector3 && camera?.position) {
          _shCamFwd = _shCamTgt.subtract(camera.position);
          if (_shCamFwd.lengthSquared() > 0.0001) _shCamFwd.normalize(); else _shCamFwd.set(0, 0, -1);
        }
        const _shCamUp = camera ? ((camera as any).upVector ?? new Vector3(0, 1, 0)) : new Vector3(0, 1, 0);
        const _shR = Vector3.Cross(_shCamUp, _shCamFwd);
        if (_shR.lengthSquared() > 0.0001) _shCamRight = _shR.normalize();
      } catch {}
      const _shRightAxis = wrap.basis.right.clone().normalize();
      const _shRightAligned = Vector3.Dot(_shRightAxis, _shCamRight) >= 0;
      shader.setTexture('uSampler', sourceTexture);
      // A mesh-scoped Drawing Area owns a stable tangent frame. This GLB path
      // needs one stable U inversion, never a camera-dependent inversion.
      const stableSurfaceFlipU = targetBoundProjectionTarget && !TARGET_BOUND_STICKER_RENDER_POLICY.cameraRelativeFlipU
        ? (TARGET_BOUND_STICKER_RENDER_POLICY.textureSampleFlipU ? 0.0 : 1.0)
        : (_shRightAligned ? 1.0 : 0.0);
      try { shader.setFloat('uFlipU', stableSurfaceFlipU); } catch {}
      try { shader.setFloat('uTargetBound', targetBoundProjectionTarget ? 1.0 : 0.0); } catch {}
      shader.setVector3('uStickerCenter', safeProjection.position);
      shader.setVector3('uStickerRight', wrap.basis.right);
      shader.setVector3('uStickerUp', wrap.basis.up);
      shader.setVector3('uStickerNormal', wrap.normal);
      shader.setVector3('uStickerSize', size);
      const projectedSurfaceOffset = targetBoundProjectionTarget
        ? clampNumber(
          Math.max(Math.abs(size.x), Math.abs(size.y)) * TARGET_BOUND_STICKER_RENDER_POLICY.surfaceShellOffsetRatio,
          TARGET_BOUND_STICKER_RENDER_POLICY.surfaceShellOffsetMin,
          TARGET_BOUND_STICKER_RENDER_POLICY.surfaceShellOffsetMax,
        )
        : 0;
      shader.setFloat('uSurfaceOffset', projectedSurfaceOffset);
      (shader as any).__asukaSurfaceShellOffset = projectedSurfaceOffset;
      shader.setVector3('uCameraPosition', camera?.position ?? safeProjection.position.add(wrap.normal));
      try {
        shader.onBindObservable.add(() => {
          try { shader.setVector3('uCameraPosition', camera?.position ?? safeProjection.position.add(wrap.normal)); } catch {}
          try {
            if ((!targetBoundProjectionTarget || TARGET_BOUND_STICKER_RENDER_POLICY.cameraRelativeFlipU) && camera) {
              let _bindCamFwd = new Vector3(0, 0, -1);
              const _bindCamTgt = (camera as any).target;
              if (_bindCamTgt instanceof Vector3 && camera.position) {
                const _bf = _bindCamTgt.subtract(camera.position);
                if (_bf.lengthSquared() > 0.0001) _bindCamFwd = _bf.normalize();
              }
              const _bindCamUp = (camera as any).upVector ?? new Vector3(0, 1, 0);
              const _bindR = Vector3.Cross(_bindCamUp, _bindCamFwd);
              const _bindCamRight = _bindR.lengthSquared() > 0.0001 ? _bindR.normalize() : new Vector3(1, 0, 0);
              const _bindAligned = Vector3.Dot(_shRightAxis, _bindCamRight) >= 0;
              shader.setFloat('uFlipU', _bindAligned ? 1.0 : 0.0);
            }
          } catch {}
        });
      } catch {}
      shader.setFloat('uWrapSide', wrap.wrapSide);
      shader.setFloat('uEdgeLocal', wrap.edgeLocal);
      shader.setFloat('uStickerBackDepth', wrap.backDepth);
      try { shader.setVector4('uStickerEdges', new Vector4(wrap.edges.right, wrap.edges.left, wrap.edges.top, wrap.edges.bottom)); } catch {}
      const overlays: Mesh[] = [];
      const targets = getProjectedStickerOverlayTargets(projection.targetMesh, safeProjection);
      debug3DLayer('projected-sticker:targets', {
        ownerId,
        targetCount: targets.length,
        targets: targets.map((target) => summarizeLayerMeshForDebug(target)),
      }, targets.length ? 'info' : 'warn');
      for (let index = 0; index < targets.length; index += 1) {
        const target = targets[index];
        if (!target || target.isDisposed?.()) continue;
        try {
          const overlay = target.clone(`${ownerId}:projected-sticker-overlay:${index}`, null, false) as Mesh | null;
          if (!overlay) continue;
          overlay.id = `${ownerId}:projected-sticker-overlay:${index}`;
          overlay.name = name || ownerId;
          // Do not destructively trim the overlay mesh here. The CPU triangle
          // filter was too strict on real GLB meshes and could remove every
          // renderable triangle, leaving only the control box visible. Keep the
          // full clone and let the shader discard non-sticker pixels.
          overlay.material = shader;
          overlay.isPickable = false;
          try {
            const targetRenderingGroupId = Number((target as any).renderingGroupId ?? 0);
            // The overlay clone is coplanar with the product mesh. Drawing it in
            // the next rendering group avoids the product material covering the
            // uploaded image/text/design while the depth buffer still prevents
            // opposite faces from bleeding through the object.
            overlay.renderingGroupId = targetBoundProjectionTarget
              ? (Number.isFinite(targetRenderingGroupId) ? targetRenderingGroupId : 0)
              : (Number.isFinite(targetRenderingGroupId) ? Math.min(3, targetRenderingGroupId + 1) : 1);
          } catch {}
          setDecorationFragmentMetadata(ownerId, overlay);
          (overlay as any).__asukaProjectionMode = 'decal';
          (overlay as any).__asukaTargetMeshId = String(target.id || target.name || 'product');
          (overlay as any).__asukaProjectedStickerOverlay = true;
          (overlay as any).__asukaTargetBoundProjectedSticker = !!targetBoundProjectionTarget;
          (overlay as any).__asukaSurfaceShellOffset = projectedSurfaceOffset;
          overlays.push(overlay);
          debug3DLayer('projected-sticker:overlay-created', { ownerId, index, target: summarizeLayerMeshForDebug(target), overlay: summarizeLayerMeshForDebug(overlay) });
        } catch (error) {
          debug3DLayer('projected-sticker:overlay-error', { ownerId, index, target: summarizeLayerMeshForDebug(target), error }, 'error');
        }
      }
      const setProjectedStickerOverlaysEnabled = (enabled: boolean) => {
        for (const overlay of overlays) {
          try { overlay.setEnabled(enabled); } catch {}
        }
      };
      try {
        // Do not disable sticker overlays while an uploaded image is still
        // loading. Some blob/data URL textures can miss the onLoad callback in
        // browser caches, leaving the layer permanently present in the Layers
        // panel but invisible on the product. Keep the overlay enabled; Babylon
        // will show the texture as soon as it becomes ready.
        setProjectedStickerOverlaysEnabled(true);
        const textureWasReady = (() => {
          try { return sourceTexture.isReady?.() === true; } catch { return false; }
        })();
        const alreadyHasProjectedFragments = (decalFragmentsByObjectId.get(ownerId) ?? [])
          .some((fragment) => (fragment as any)?.__asukaProjectedStickerOverlay === true);
        if (textureWasReady && !alreadyHasProjectedFragments) {
          // Cached images skip Babylon's load callback. Stabilize the first
          // projection once after it has been registered, just like an async
          // upload, so a fresh re-add does not depend on the first drag.
          try {
            setTimeout(() => scheduleProjectedDecorationRefresh?.(ownerId, 'source-texture-cached'), 0);
          } catch {}
        } else if (!textureWasReady) {
          let stabilizationRequested = false;
          const stabilizeLoadedProjection = () => {
            setProjectedStickerOverlaysEnabled(true);
            if (stabilizationRequested) return;
            stabilizationRequested = true;
            try {
              setTimeout(() => scheduleProjectedDecorationRefresh?.(ownerId, 'source-texture-ready'), 0);
            } catch {}
          };
          try { (sourceTexture as any)?.onLoadObservable?.addOnce?.(stabilizeLoadedProjection); } catch {}
          try {
            let attempts = 0;
            const refreshWhenReady = () => {
              attempts += 1;
              let ready = false;
              try { ready = sourceTexture.isReady?.() === true; } catch {}
              if (ready) {
                stabilizeLoadedProjection();
                return;
              }
              if (attempts < 8) setTimeout(refreshWhenReady, Math.min(240, 30 * attempts));
            };
            setTimeout(refreshWhenReady, 30);
          } catch {}
        }
        try { setTimeout(() => setProjectedStickerOverlaysEnabled(true), 0); } catch {}
      } catch {}
      if (!overlays.length) {
        debug3DLayer('projected-sticker:create:no-overlays', { ownerId, kind, targetMesh: summarizeLayerMeshForDebug(projection.targetMesh) }, 'warn');
        try { control.dispose(false, true); } catch {}
        try { shader.dispose(false, false); } catch {}
        return null;
      }
      objectMeshes.set(ownerId, control);
      objectKinds.set(ownerId, kind);
      decalFragmentsByObjectId.set(ownerId, overlays);
      debug3DLayer('projected-sticker:create:ready', {
        ownerId,
        control: summarizeLayerMeshForDebug(control),
        overlays: overlays.map((overlay) => summarizeLayerMeshForDebug(overlay)),
        textureReady: (() => { try { return sourceTexture.isReady?.(); } catch { return undefined; } })(),
        surfaceOffset: projectedSurfaceOffset,
      });
      if (selectedObjectId === ownerId) {
        try {
          control.renderOutline = false;
          control.outlineWidth = 0;
          (control as any).showBoundingBox = false;
        } catch {}
        setDecalFragmentsSelectionState(ownerId, true);
      }
      return control;
    } catch (error) {
      debug3DLayer('projected-sticker:create:error', { ownerId, kind, error }, 'error');
      try { control.dispose(false, true); } catch {}
      return null;
    }
  };

  const shouldPreferNativeDecalProjection = (projection: { targetMesh: Mesh; position: Vector3; normal: Vector3; size: Vector3 }): boolean => {
    const mesh = projection?.targetMesh;
    if (!mesh) return false;
    // Only explicit helper print surfaces should use Babylon's native decal path.
    // Regular product meshes, even low-poly parametric boxes, must use the
    // surface-patch renderer so the decal size/ratio is controlled by Asuka and
    // not expanded into a full-surface/corner continuation skin.
    return isNamedPrintSurfaceMesh(mesh);
  };

  const isDenseOrganicStickerTarget = (mesh?: Mesh | null): boolean => {
    if (!mesh || mesh.isDisposed?.()) return false;
    try {
      if (isNamedPrintSurfaceMesh(mesh)) return false;
      const vertexCount = Number(mesh.getTotalVertices?.() ?? 0) || 0;
      const indexCount = Number(mesh.getTotalIndices?.() ?? mesh.getIndices?.()?.length ?? 0) || 0;
      return vertexCount > 900 || indexCount > 2400;
    } catch {
      return false;
    }
  };

  const translateDecorationRuntimeMeshes = (ownerId: string, delta: Vector3) => {
    if (!ownerId || delta.lengthSquared() < 0.0000000001) return;
    const translateOne = (mesh?: Mesh | null) => {
      if (!mesh || mesh.isDisposed?.()) return;
      try { mesh.position.addInPlace(delta); } catch {}
      const stored = (mesh as any).__asukaDecalPosition;
      if (Array.isArray(stored) && stored.length >= 3) {
        (mesh as any).__asukaDecalPosition = [
          (Number(stored[0]) || 0) + delta.x,
          (Number(stored[1]) || 0) + delta.y,
          (Number(stored[2]) || 0) + delta.z,
        ];
      }
    };
    translateOne(objectMeshes.get(ownerId) ?? null);
    for (const fragment of decalFragmentsByObjectId.get(ownerId) ?? []) translateOne(fragment);
  };

  const previewMoveComplexSurfaceSticker = (
    ownerId: string,
    surface: { targetMesh: Mesh; position: Vector3; normal: Vector3; surfaceRight?: Vector3 | null; surfaceUp?: Vector3 | null },
    size: Vector3,
    angle: number,
  ): Vector3 | null => {
    const mesh = objectMeshes.get(ownerId) ?? null;
    if (!mesh || mesh.isDisposed?.()) return null;
    const safePosition = getSafeDecalProjectionPosition(surface.position, surface.normal, size);
    const stored = (mesh as any).__asukaDecalPosition;
    const previousPosition = Array.isArray(stored) && stored.length >= 3
      ? new Vector3(Number(stored[0]) || 0, Number(stored[1]) || 0, Number(stored[2]) || 0)
      : safePosition.clone();
    const delta = safePosition.subtract(previousPosition);
    const projectionAngle = getProjectionSurfaceAngle({ normal: surface.normal, surfaceRight: surface.surfaceRight ?? null, surfaceUp: surface.surfaceUp ?? null, angle });
    const explicitAreaTarget = getExplicitPrintAreaTargetMesh(getDecorationPrintArea(findDecorationRecord(ownerId)));
    if ((mesh as any).__asukaProjectedStickerShader === true) {
      const projection = { targetMesh: surface.targetMesh, position: safePosition, normal: surface.normal, size };
      const wrap = getProjectedStickerWrapInfo(projection, size, projectionAngle);
      for (const fragment of decalFragmentsByObjectId.get(ownerId) ?? []) {
        const material = fragment?.material as any;
        if (!material || typeof material.setVector3 !== 'function') continue;
        try { material.setVector3('uStickerCenter', safePosition); } catch {}
        try { material.setVector3('uStickerRight', wrap.basis.right); } catch {}
        try { material.setVector3('uStickerUp', wrap.basis.up); } catch {}
        try { material.setVector3('uStickerNormal', wrap.normal); } catch {}
        try { material.setVector3('uStickerSize', size); } catch {}
        try { material.setFloat('uWrapSide', wrap.wrapSide); } catch {}
        try { material.setFloat('uEdgeLocal', wrap.edgeLocal); } catch {}
        try { material.setFloat('uStickerBackDepth', wrap.backDepth); } catch {}
        try { material.setVector4('uStickerEdges', new Vector4(wrap.edges.right, wrap.edges.left, wrap.edges.top, wrap.edges.bottom)); } catch {}
        applyExplicitPrintAreaDepthDiscipline(fragment, material, explicitAreaTarget);
      }
      if (delta.lengthSquared() > 0.0000000001) {
        try { mesh.position.addInPlace(delta); } catch {}
      }
      setDecalRuntimeMetadata(mesh, surface.targetMesh, size, surface.normal, projectionAngle, safePosition, surface.surfaceRight ?? null, surface.surfaceUp ?? null);
      applyExplicitPrintAreaDepthDiscipline(mesh, mesh.material, explicitAreaTarget);
      return safePosition;
    }
    translateDecorationRuntimeMeshes(ownerId, delta);
    setDecalRuntimeMetadata(mesh, surface.targetMesh, size, surface.normal, projectionAngle, safePosition, surface.surfaceRight ?? null, surface.surfaceUp ?? null);
    applyExplicitPrintAreaDepthDiscipline(mesh, mesh.material, explicitAreaTarget);
    return safePosition;
  };

  const createProjectedDecalFragments = (
    ownerIdInput: string,
    kind: string,
    name: string,
    material: any,
    projection: { targetMesh: Mesh; position: Vector3; normal: Vector3; size: Vector3; faceId?: number | null; rawPosition?: Vector3 | null; surfaceRight?: Vector3 | null; surfaceUp?: Vector3 | null; angle?: number | null },
    angle = 0,
  ): Mesh | null => {
    const ownerId = String(ownerIdInput ?? '').trim();
    if (!ownerId || !projection?.targetMesh || !material) return null;
    const projectionAngle = getProjectionSurfaceAngle({ ...projection, angle });
    // Native GLB print-surface helper meshes already define the exact printable
    // plane. Simple parametric/cuboid product parts also render more reliably
    // with Babylon's native decal path. The projected shader stays reserved for
    // dense/curved surfaces where it adds value.
    const declaredMetadata = getDeclaredExactFaceNetPrintSurfaceMetadata(projection as any);
    // For non-face-net areas that still carry selectedTriangles (e.g. Drawing
    // Areas "texture slot" type on a complex mesh), extract the triangle refs so
    // we can route to surface-patch instead of the full-UV texture-slot path.
    // Without this, images paint over the entire mesh UV space.
    const altPrintSurface3D = !declaredMetadata ? (() => {
      const a = (projection as any).__asukaPrintSurfaceArea;
      const ps3 = a?.metadata?.printSurface3D ?? a?.printSurface3D ?? null;
      if (!ps3 || typeof ps3 !== 'object') return null;
      const refs = normalizeFaceNetTriangleRefs(
        (ps3 as any).selectedTriangles ?? (ps3 as any).triangles ?? [],
        String((ps3 as any).targetMeshId ?? (a as any)?.targetMeshId ?? ''),
      );
      return refs.length > 0 ? { selectedTriangles: refs } : null;
    })() : null;
    if (declaredMetadata || altPrintSurface3D) {
      debug3DLayer('decal-renderer:selected', {
        ownerId,
        kind,
        renderer: 'face-net',
        targetMesh: summarizeLayerMeshForDebug(projection.targetMesh),
        declared: declaredMetadata
          ? { targetMeshId: declaredMetadata.targetMeshId, selectedTriangles: declaredMetadata.selectedTriangles?.length ?? 0 }
          : null,
      });
      const selectedTriangleRefs = declaredMetadata?.selectedTriangles ?? altPrintSurface3D?.selectedTriangles ?? [];
      const declaredTargetMeshId = String(
        declaredMetadata?.targetMeshId
        ?? selectedTriangleRefs[0]?.meshId
        ?? (projection as any).__asukaPrintSurfaceArea?.targetMeshId
        ?? '',
      ).trim();
      const constrainedTargetMesh = declaredTargetMeshId ? (findModelMesh(declaredTargetMeshId) ?? projection.targetMesh) : projection.targetMesh;
      const broadMeshPrintArea = !!constrainedTargetMesh && resolve3DStickerProjectionStrategy({
        targetMeshId: declaredTargetMeshId,
        selectedTriangleCount: selectedTriangleRefs.length,
        targetIsNamedPrintSurface: isNamedPrintSurfaceMesh(constrainedTargetMesh),
      }) === 'target-bound-projector';
      if (broadMeshPrintArea) {
        const meshProjection = { ...projection, targetMesh: constrainedTargetMesh };
        const targetBoundProjected = createProjectedStickerShaderFragments(ownerId, kind, name, material, meshProjection, projectionAngle);
        if (targetBoundProjected) {
          (targetBoundProjected as any).__asukaBroadMeshTargetProjector = true;
          debug3DLayer('face-net:broad-mesh-target-projector-success', {
            ownerId,
            kind,
            selectedTriangles: selectedTriangleRefs.length,
            targetMesh: summarizeLayerMeshForDebug(constrainedTargetMesh),
            control: summarizeLayerMeshForDebug(targetBoundProjected),
            overlays: (decalFragmentsByObjectId.get(ownerId) ?? []).map((fragment) => summarizeLayerMeshForDebug(fragment)),
          }, 'warn');
          return targetBoundProjected;
        }
        debug3DLayer('face-net:broad-mesh-target-projector-empty', {
          ownerId,
          kind,
          selectedTriangles: selectedTriangleRefs.length,
          targetMesh: summarizeLayerMeshForDebug(constrainedTargetMesh),
        }, 'warn');
        try {
          const nativeSize = getSafeDecalProjectionSize(meshProjection.size);
          const nativePosition = getSafeDecalProjectionPosition(meshProjection.position, meshProjection.normal, nativeSize);
          const nativeDecal = (MeshBuilder as any).CreateDecal(ownerId, constrainedTargetMesh, {
            position: nativePosition,
            normal: meshProjection.normal,
            size: nativeSize,
            projectionAngle,
            cullBackFaces: false,
          }) as Mesh;
          if (nativeDecal && (nativeDecal.getTotalVertices?.() ?? 0) > 0) {
            nativeDecal.id = ownerId;
            nativeDecal.name = name || ownerId;
            nativeDecal.material = material;
            nativeDecal.isPickable = true;
            setDecalRuntimeMetadata(nativeDecal, constrainedTargetMesh, nativeSize, meshProjection.normal, projectionAngle, nativePosition, meshProjection.surfaceRight ?? null, meshProjection.surfaceUp ?? null);
            applyExplicitPrintAreaDepthDiscipline(nativeDecal, material, constrainedTargetMesh);
            setDecorationFragmentMetadata(ownerId, nativeDecal);
            (nativeDecal as any).__asukaProjectionMode = 'decal';
            (nativeDecal as any).__asukaUsingSurfacePatch = true;
            (nativeDecal as any).__asukaNativeSurfaceDecal = true;
            objectMeshes.set(ownerId, nativeDecal);
            objectKinds.set(ownerId, kind);
            decalFragmentsByObjectId.set(ownerId, []);
            debug3DLayer('face-net:broad-mesh-native-decal-success', {
              ownerId,
              kind,
              selectedTriangles: selectedTriangleRefs.length,
              targetMesh: summarizeLayerMeshForDebug(constrainedTargetMesh),
              vertices: nativeDecal.getTotalVertices?.(),
            }, 'warn');
            return nativeDecal;
          }
          try { nativeDecal?.dispose(false, false); } catch {}
        } catch (error) {
          debug3DLayer('face-net:broad-mesh-native-decal-error', { ownerId, kind, error }, 'warn');
        }
        debug3DLayer('face-net:broad-mesh-projection-empty', {
          ownerId,
          kind,
          selectedTriangles: selectedTriangleRefs.length,
          targetMesh: summarizeLayerMeshForDebug(constrainedTargetMesh),
        }, 'warn');
        return null;
      }

      const exactFaceNetPrintSurface3D = createExactFaceNetPrintSurfaceStickerMesh(ownerId, kind, name, material, projection as any, projectionAngle);
      if (exactFaceNetPrintSurface3D) return exactFaceNetPrintSurface3D;

      const allowedTriangleIndices = new Set<number>(
        selectedTriangleRefs
          .map((ref: any) => Math.floor(Number(ref?.triangleIndex)))
          .filter((value: number) => Number.isFinite(value) && value >= 0),
      );
      if (allowedTriangleIndices.size > 0) {
        const surfacePatch = createSurfacePatchStickerMesh(
          ownerId,
          kind,
          name,
          material,
          { ...projection, targetMesh: constrainedTargetMesh },
          projectionAngle,
          {
            allowedTriangleIndices,
            normalThresholdOverride: -0.001,
            backDepthOverride: Math.max(0.08, Math.abs(projection.size.z) * 1.25, Math.max(Math.abs(projection.size.x), Math.abs(projection.size.y)) * 0.22),
          },
        );
        if (surfacePatch) {
          debug3DLayer('face-net:fallback-selected-triangle-surface-patch-success', {
            ownerId,
            kind,
            selectedTriangles: allowedTriangleIndices.size,
            targetMesh: summarizeLayerMeshForDebug(constrainedTargetMesh),
            patch: summarizeLayerMeshForDebug(surfacePatch),
          }, 'warn');
          return surfacePatch;
        }
        debug3DLayer('face-net:fallback-selected-triangle-surface-patch-empty', {
          ownerId,
          kind,
          selectedTriangles: allowedTriangleIndices.size,
          targetMesh: summarizeLayerMeshForDebug(constrainedTargetMesh),
        }, 'warn');
        const meshScopedSurfacePatch = createSurfacePatchStickerMesh(
          ownerId,
          kind,
          name,
          material,
          { ...projection, targetMesh: constrainedTargetMesh },
          projectionAngle,
          {
            normalThresholdOverride: 0.08,
            backDepthOverride: Math.max(0.08, Math.abs(projection.size.z) * 1.35, Math.max(Math.abs(projection.size.x), Math.abs(projection.size.y)) * 0.28),
            searchWidthMultiplier: 1.12,
          },
        );
        if (meshScopedSurfacePatch) {
          debug3DLayer('face-net:fallback-mesh-scoped-surface-patch-success', {
            ownerId,
            kind,
            selectedTriangles: allowedTriangleIndices.size,
            targetMesh: summarizeLayerMeshForDebug(constrainedTargetMesh),
            patch: summarizeLayerMeshForDebug(meshScopedSurfacePatch),
          }, 'warn');
          return meshScopedSurfacePatch;
        }
        debug3DLayer('face-net:fallback-mesh-scoped-surface-patch-empty', {
          ownerId,
          kind,
          selectedTriangles: allowedTriangleIndices.size,
          targetMesh: summarizeLayerMeshForDebug(constrainedTargetMesh),
        }, 'warn');
        return null;
      }

      // Face-net BFS failed (curved/complex mesh, disconnected vertices).
      // Use Babylon's native MeshBuilder.CreateDecal: it does EXACT geometric
      // triangle clipping against the decal's oriented box (Sutherland-Hodgman,
      // not approximate dot-product/depth thresholds), and cullBackFaces uses
      // each triangle's real vertex normal to exclude interior-facing geometry.
      // This is deterministic — no hand-tuned magic numbers that drift between
      // mesh scales or camera angles.
      //
      // IMPORTANT: cullBackFaces trusts the mesh's STORED normal buffer, not
      // geometry. This GLB's normals are unreliable (consistent with the 97%
      // "disconnected vertices" finding — a non-standard export pipeline), so
      // vertices=0 was returned. Recompute normals geometrically from the
      // triangle winding (once, cached) before calling CreateDecal so
      // cullBackFaces has correct data to work with.
      try {
        const targetMeshAny = projection.targetMesh as any;
        if (!targetMeshAny.__asukaNormalsRecomputed) {
          try {
            const positions = projection.targetMesh.getVerticesData?.('position' as any);
            const indices = projection.targetMesh.getIndices?.();
            if (positions && indices && indices.length >= 3) {
              const normals: number[] = [];
              (VertexData as any).ComputeNormals(positions, indices, normals);
              projection.targetMesh.setVerticesData?.('normal' as any, normals, true);
            }
          } catch (_e) {
          }
          targetMeshAny.__asukaNormalsRecomputed = true;
        }
      } catch {}
      try {
        const meshBounds = getMeshWorldBounds(projection.targetMesh);
        const rawPos = (projection as any).rawPosition as Vector3 | null | undefined;
        const meshDiag = meshBounds.size.length();
        const origS = projection.size;
        const origAspect = origS.x / Math.max(origS.y, 0.001);
        const targetW = rawPos ? origS.x : meshDiag * 0.18;
        const targetH = targetW / origAspect;
        const depth = Math.max(targetW, targetH) * 2.0;
        let decalPos: Vector3;
        let decalNormal: Vector3;
        if (rawPos) {
          decalPos = rawPos;
          decalNormal = camera?.position ? camera.position.subtract(rawPos).normalize() : projection.normal.clone();
        } else {
          // pickVisibleModelSurface samples multiple screen-space rays from the
          // viewport center — proven to hit the visible surface reliably.
          const surfaceHit = pickVisibleModelSurface(projection.targetMesh);
          if (surfaceHit?.rawPosition) {
            decalPos = surfaceHit.rawPosition;
            decalNormal = surfaceHit.normal;
          } else {
            // Last resort: bounding box front face from camera direction
            const camDir = camera?.position ? camera.position.subtract(meshBounds.center).normalize() : projection.normal.clone();
            const half = meshBounds.size.scale(0.5);
            const extent = Math.abs(camDir.x) * Math.max(half.x, 0.01)
              + Math.abs(camDir.y) * Math.max(half.y, 0.01)
              + Math.abs(camDir.z) * Math.max(half.z, 0.01);
            decalPos = meshBounds.center.add(camDir.scale(extent));
            decalNormal = camDir;
          }
        }
        const decalSize = new Vector3(targetW, targetH, depth);
        const decal = (MeshBuilder as any).CreateDecal(ownerId, projection.targetMesh, {
          position: decalPos,
          normal: decalNormal,
          size: decalSize,
          projectionAngle,
          cullBackFaces: false,
        }) as Mesh;
        if (decal && (decal.getTotalVertices?.() ?? 0) > 0) {
          decal.id = ownerId;
          decal.name = name || ownerId;
          decal.material = material;
          decal.isPickable = true;
          try { decal.renderingGroupId = Number((projection.targetMesh as any)?.renderingGroupId ?? 0) || 0; } catch {}
          try { applySurfacePatchMaterialBias(material); } catch {}
          setDecalRuntimeMetadata(decal, projection.targetMesh, decalSize, decalNormal, projectionAngle, decalPos, projection.surfaceRight ?? null, projection.surfaceUp ?? null);
          applyExplicitPrintAreaDepthDiscipline(decal, material, getExplicitProjectionPrintAreaTargetMesh(projection as any));
          (decal as any).__asukaProjectionMode = 'decal';
          // Tag as surface-patch so drag uses the reduced-rate rebuild path —
          // native decal creation is also relatively expensive on dense meshes.
          (decal as any).__asukaUsingSurfacePatch = true;
          (decal as any).__asukaNativeSurfaceDecal = true;
          objectMeshes.set(ownerId, decal);
          objectKinds.set(ownerId, kind);
          decalFragmentsByObjectId.set(ownerId, []);
          debug3DLayer('face-net:fallback-native-decal-success', { ownerId, kind, vertices: decal.getTotalVertices?.() });
          return decal;
        }
        try { decal?.dispose(false, false); } catch {}
      } catch (error) {
        debug3DLayer('face-net:fallback-native-decal-error', { ownerId, kind, error }, 'error');
      }
      console.warn('[ASUKA-DEBUG] ALL fallbacks failed — no mesh created');
      debug3DLayer('face-net:fallback-blocked', { ownerId, kind, targetMesh: summarizeLayerMeshForDebug(projection.targetMesh), declared: declaredMetadata }, 'warn');
      return null;
    }
    const preferNativeDecal = shouldPreferNativeDecalProjection(projection);
    debug3DLayer('decal-renderer:selected', {
      ownerId,
      kind,
      renderer: preferNativeDecal ? 'native-decal' : 'surface-patch',
      targetMesh: summarizeLayerMeshForDebug(projection.targetMesh),
    });
    if (!preferNativeDecal) {
      if ((projection as any).__asukaFromFixedSlot3D === true) {
        if (isDenseOrganicStickerTarget(projection.targetMesh)) {
          const markerProjected = createProjectedStickerShaderFragments(ownerId, kind, name, material, projection, projectionAngle);
          if (markerProjected) {
            (markerProjected as any).__asukaMarkerTargetProjector = true;
            return markerProjected;
          }
        }
        const markerSurfacePatch = createSurfacePatchStickerMesh(ownerId, kind, name, material, projection, projectionAngle);
        if (markerSurfacePatch) return markerSurfacePatch;
      }
      const declaredPrintSurface3D = createDeclaredBoxPrintSurfaceStickerMesh(ownerId, kind, name, material, projection, projectionAngle);
      if (declaredPrintSurface3D) return declaredPrintSurface3D;
      const printSurface3D = createBoxPrintSurfaceStickerMesh(ownerId, kind, name, material, projection, projectionAngle);
      if (printSurface3D) return printSurface3D;
      if (isDenseOrganicStickerTarget(projection.targetMesh)) {
        const shaderProjected = createProjectedStickerShaderFragments(ownerId, kind, name, material, projection, projectionAngle);
        if (shaderProjected) return shaderProjected;
      }
      const surfacePatch = createSurfacePatchStickerMesh(ownerId, kind, name, material, projection, projectionAngle);
      if (surfacePatch) return surfacePatch;
      const shaderProjected = createProjectedStickerShaderFragments(ownerId, kind, name, material, projection, projectionAngle);
      if (shaderProjected) return shaderProjected;
    }
    if ((projection as any).__asukaPreserveExistingFragments !== true) {
      disposeDecalFragments(ownerId, true);
    }
    const size = getExpandedMultiDecalSize(projection.size, projection.normal);
    const position = getSafeDecalProjectionPosition(projection.position, projection.normal, size);
    const explicitProjectionTarget = getExplicitProjectionPrintAreaTargetMesh(projection as any);
    const meshes = explicitProjectionTarget
      ? [explicitProjectionTarget]
      : getProjectedDecalTargetMeshes(projection.targetMesh, projection);
    const fragments: Mesh[] = [];
    for (const targetMesh of meshes) {
      try {
        const fragment = (MeshBuilder as any).CreateDecal(`${ownerId}:fragment:${fragments.length}`, targetMesh, {
          position,
          normal: projection.normal,
          size,
          projectionAngle,
        }) as Mesh;
        if (!filterDecalMeshToAdjacentProjection(fragment, projection)) continue;
        try {
          if (fragment.getTotalVertices?.() <= 0) {
            fragment.dispose(false, false);
            continue;
          }
        } catch {}
        fragment.id = fragments.length === 0 ? ownerId : `${ownerId}:fragment:${fragments.length}`;
        fragment.name = name || ownerId;
        fragment.material = material;
        setDecalRuntimeMetadata(fragment, targetMesh, size, projection.normal, projectionAngle, position, projection.surfaceRight ?? null, projection.surfaceUp ?? null);
        applyExplicitPrintAreaDepthDiscipline(fragment, material, explicitProjectionTarget);
        setDecorationFragmentMetadata(ownerId, fragment);
        if (fragments.length === 0) {
          (fragment as any).__asukaDecorationProxyFor = '';
        }
        fragments.push(fragment);
      } catch {}
    }
    const cornerFragments = createCornerStickerFragments(ownerId, name, material, { ...projection, position, size }, size, projectionAngle);
    if (cornerFragments.length > 1 && fragments.length) {
      // Keep Babylon's native decal as the source, but clip it down to the
      // source-facing triangles only. The adjacent continuation is handled by
      // explicit corner fragments so we must remove wrap triangles from the
      // main decal to avoid overlap/ghosting on the seam.
      const sourcePlane = cornerFragments[0];
      try { sourcePlane.dispose(false, false); } catch {}
      const adjacentCornerFragments = cornerFragments.slice(1).filter((fragment) => !!fragment && !fragment.isDisposed?.());
      const primary = fragments[0];
      const sourceOk = filterDecalMeshToSourceFace(primary, { ...projection, position, size });
      if (!sourceOk) {
        for (const fragment of adjacentCornerFragments) {
          try { fragment.dispose(false, false); } catch {}
        }
      }
      for (const fragment of fragments.slice(1)) {
        try { fragment.dispose(false, false); } catch {}
      }
      objectMeshes.set(ownerId, primary);
      objectKinds.set(ownerId, kind);
      decalFragmentsByObjectId.set(ownerId, sourceOk ? adjacentCornerFragments : []);
      if (selectedObjectId === ownerId) {
        try {
          primary.renderOutline = true;
          primary.outlineColor = color3FromInput('#2563eb');
          primary.outlineWidth = 0.035;
          (primary as any).showBoundingBox = true;
        } catch {}
        setDecalFragmentsSelectionState(ownerId, true);
      }
      return primary;
    }
    for (const fragment of cornerFragments) {
      try { fragment.dispose(false, false); } catch {}
    }
    if (!fragments.length) {
      if (preferNativeDecal) {
        debug3DLayer('decal-renderer:native-empty-fallback-surface-patch', { ownerId, kind, targetMesh: summarizeLayerMeshForDebug(projection.targetMesh) }, 'warn');
        const surfacePatchFallback = createSurfacePatchStickerMesh(ownerId, kind, name, material, { ...projection, position, size }, projectionAngle);
        if (surfacePatchFallback) return surfacePatchFallback;
        if (!isNamedPrintSurfaceMesh(projection.targetMesh)) {
          debug3DLayer('decal-renderer:surface-patch-empty-fallback-shader', { ownerId, kind, targetMesh: summarizeLayerMeshForDebug(projection.targetMesh) }, 'warn');
          const shaderFallback = createProjectedStickerShaderFragments(ownerId, kind, name, material, projection, projectionAngle);
          if (shaderFallback) return shaderFallback;
        }
      }
      return null;
    }
    const primary = fragments[0];
    objectMeshes.set(ownerId, primary);
    objectKinds.set(ownerId, kind);
    decalFragmentsByObjectId.set(ownerId, fragments.slice(1));
    if (selectedObjectId === ownerId) {
      try {
        primary.renderOutline = true;
        primary.outlineColor = color3FromInput('#2563eb');
        primary.outlineWidth = 0.035;
        (primary as any).showBoundingBox = true;
      } catch {}
      setDecalFragmentsSelectionState(ownerId, true);
    }
    return primary;
  };

  const createImageDecal = (id: string, url: string, kind: string, label?: string, area?: Asuka3DPrintArea | null, projectionOverride?: { targetMesh: Mesh; position: Vector3; normal: Vector3; size: Vector3; angle?: number; faceId?: number | null; rawPosition?: Vector3 | null; surfaceRight?: Vector3 | null; surfaceUp?: Vector3 | null } | null, aspectOverride?: number | null): Mesh | null => {
    debug3DLayer('image-decal:create:start', { id, kind, label, hasArea: Boolean(area), areaId: String(area?.id ?? ''), url, aspectOverride });
    if (!area && !projectionOverride) {
      debug3DLayer('image-decal:create:skip-no-area', { id, kind }, 'warn');
      return null;
    }
    let projection = projectionOverride ?? resolveDecalProjection(area);
    if (!projection) {
      debug3DLayer('image-decal:create:no-projection', { id, kind, areaId: String(area?.id ?? ''), targetMeshId: String(area?.targetMeshId ?? '') }, 'warn');
      return null;
    }
    const parsedAspect = getImageAspectRatioFromSource(url);
    const imageAspect = (Number(aspectOverride) && Number.isFinite(Number(aspectOverride)) && Number(aspectOverride) > 0.001)
      ? Number(aspectOverride)
      : parsedAspect;
    // A persisted projection already stores the fitted physical sticker size.
    // Applying the drawing-area fit again on undo/redo compounds the ratio and
    // makes the layer shrink after every restore.
    projection = (projectionOverride as any)?.__asukaPreserveProjectionSize === true
      ? { ...projection, size: getSafeDecalProjectionSize(projection.size) }
      : normalizeDecalProjectionAspect(projection, imageAspect, {
          autoSurface: String(area?.id ?? '') === AUTO_PRODUCT_SURFACE_AREA_ID,
          fitRatio: isExactFaceNetPrintArea(area) ? 0.32 : 1,
        });
    try { (projection as any).__asukaPrintSurfaceArea = area ? clonePlain(area) : null; } catch { (projection as any).__asukaPrintSurfaceArea = area ?? null; }
    const angle = Number((projection as any)?.angle ?? 0) || 0;
    try {
      const texture = new Texture(url, requireScene(), false, true, Texture.TRILINEAR_SAMPLINGMODE);
      texture.hasAlpha = true;
      try { (texture as any).onLoadObservable?.addOnce?.(() => debug3DLayer('image-texture:loaded', { id, kind, ready: texture.isReady?.(), url, aspect: imageAspect })); } catch {}
      try { (texture as any).onErrorObservable?.add?.((message: any, exception: any) => debug3DLayer('image-texture:error', { id, kind, message, exception, url }, 'error')); } catch {}
      const material = createDecorationMaterial(id, texture);
      const mesh = createProjectedDecalFragments(id, kind, label || id, material, projection, angle);
      debug3DLayer('image-decal:create:result', { id, kind, mesh: summarizeLayerMeshForDebug(mesh), fragments: (decalFragmentsByObjectId.get(id) ?? []).map((fragment) => summarizeLayerMeshForDebug(fragment)) }, mesh ? 'info' : 'warn');
      return mesh;
    } catch (error) {
      debug3DLayer('image-decal:create:error', { id, kind, error }, 'error');
      return null;
    }
  };

  const createTextDecal = (id: string, input: AsukaTextInput, area?: Asuka3DPrintArea | null, projectionOverride?: { targetMesh: Mesh; position: Vector3; normal: Vector3; size: Vector3; angle?: number; faceId?: number | null; rawPosition?: Vector3 | null; surfaceRight?: Vector3 | null; surfaceUp?: Vector3 | null } | null): Mesh | null => {
    debug3DLayer('text-decal:create:start', { id, text: input?.text, hasArea: Boolean(area), areaId: String(area?.id ?? '') });
    if (!area && !projectionOverride) {
      debug3DLayer('text-decal:create:skip-no-area', { id }, 'warn');
      return null;
    }
    let projection = projectionOverride ?? resolveDecalProjection(area);
    if (!projection) {
      debug3DLayer('text-decal:create:no-projection', { id, areaId: String(area?.id ?? ''), targetMeshId: String(area?.targetMeshId ?? '') }, 'warn');
      return null;
    }
    const angle = Number((projection as any)?.angle ?? 0) || 0;
    try {
      const { texture, text, width, height } = createTextTexture(id, input);
      projection = (projectionOverride as any)?.__asukaPreserveProjectionSize === true
        ? { ...projection, size: getSafeDecalProjectionSize(projection.size) }
        : normalizeDecalProjectionAspect(projection, width / Math.max(height, 1), {
            autoSurface: String(area?.id ?? '') === AUTO_PRODUCT_SURFACE_AREA_ID,
            fitRatio: isExactFaceNetPrintArea(area) ? 0.32 : 1,
          });
      try { (projection as any).__asukaPrintSurfaceArea = area ? clonePlain(area) : null; } catch { (projection as any).__asukaPrintSurfaceArea = area ?? null; }
      const material = createDecorationMaterial(id, texture);
      const mesh = createProjectedDecalFragments(id, 'text', text, material, projection, angle);
      debug3DLayer('text-decal:create:result', { id, mesh: summarizeLayerMeshForDebug(mesh), fragments: (decalFragmentsByObjectId.get(id) ?? []).map((fragment) => summarizeLayerMeshForDebug(fragment)) }, mesh ? 'info' : 'warn');
      return mesh;
    } catch (error) {
      debug3DLayer('text-decal:create:error', { id, error }, 'error');
      return null;
    }
  };

  const createImagePlane = (id: string, url: string, kind: string, label?: string, area?: Asuka3DPrintArea | null, aspectOverride?: number | null) => {
    const sc = requireScene();
    const planeSize = getCameraFacingImagePlaneSize(aspectOverride ?? getImageAspectRatioFromSource(url));
    // Babylon planes face local -Z by default, while lookAt() points local +Z at
    // the camera. Use the reversed face so empty-scene fallbacks are not viewed
    // from behind with mirrored UVs.
    const mesh = MeshBuilder.CreatePlane(id, { ...planeSize, sideOrientation: Mesh.BACKSIDE }, sc);
    mesh.id = id;
    mesh.name = label || id;
    (mesh as any).__asukaProjectionMode = 'plane';
    placeDecorationMesh(mesh, area, planeSize);
    const mat = new StandardMaterial(`${id}-material`, sc);
    const texture = orientTextureForCameraFacingPlane(new Texture(url, sc, false, true, Texture.TRILINEAR_SAMPLINGMODE));
    texture.hasAlpha = true;
    mat.diffuseTexture = texture;
    mat.useAlphaFromDiffuseTexture = true;
    mat.emissiveColor = Color3.White();
    mat.backFaceCulling = false;
    mesh.material = mat;
    objectMeshes.set(id, mesh);
    objectKinds.set(id, kind);
    return mesh;
  };

  const createTextPlane = (id: string, input: AsukaTextInput, area?: Asuka3DPrintArea | null) => {
    const sc = requireScene();
    const { texture, text, width, height } = createTextTexture(id, input);
    orientTextureForCameraFacingPlane(texture);
    const ratio = width / height;
    const planeWidth = Math.min(2.2, ratio * 0.55);
    const planeHeight = 0.55;
    const mesh = MeshBuilder.CreatePlane(id, { width: planeWidth, height: planeHeight, sideOrientation: Mesh.BACKSIDE }, sc);
    mesh.id = id;
    mesh.name = text;
    (mesh as any).__asukaProjectionMode = 'plane';
    placeDecorationMesh(mesh, area, { width: planeWidth, height: planeHeight });
    mesh.material = createDecorationMaterial(id, texture);
    objectMeshes.set(id, mesh);
    objectKinds.set(id, 'text');
    return mesh;
  };

  const clampScaleComponent = (value: number): number => Math.max(0.025, Math.min(25, Number(value) || 1));

  const getViewTransformBasis = () => {
    const cam = camera;
    let forward = cam ? cam.target.subtract(cam.position) : new Vector3(0, 0, -1);
    if (forward.lengthSquared() < 0.0001) forward = new Vector3(0, 0, -1);
    forward.normalize();
    let right = Vector3.Cross(forward, new Vector3(0, 1, 0));
    if (right.lengthSquared() < 0.0001) right = new Vector3(1, 0, 0);
    right.normalize();
    let up = Vector3.Cross(right, forward);
    if (up.lengthSquared() < 0.0001) up = new Vector3(0, 1, 0);
    up.normalize();
    return { right, up, forward };
  };

  const applyPlacementToMesh = (mesh: Mesh, placement: Asuka3DDecoration['placement'] | null | undefined) => {
    if (!placement) return;
    mesh.position = toVec3(placement.position, fromVec3(mesh.position));
    mesh.rotation = toVec3(placement.rotation, fromVec3(mesh.rotation));
    mesh.scaling = toVec3(placement.scale, fromVec3(mesh.scaling));
  };

  const applyPatchToMesh = (mesh: Mesh, patch: AsukaObjectPatch, deco?: DecorationRecord | null) => {
    const isDecalPlacement = projectionModeFromMesh(mesh, deco?.placement?.mode ?? 'plane') === 'decal';
    if ((patch as any).resetTransform) {
      const initialPlacement = (deco as any)?.metadata?.initialPlacement as Asuka3DDecoration['placement'] | undefined;
      if (initialPlacement) {
        applyPlacementToMesh(mesh, initialPlacement);
        if (isDecalPlacement) (mesh as any).__asukaDecalAngle = Number(initialPlacement?.rotation?.[2] ?? 0) || 0;
      } else {
        const areaId = String((deco as any)?.targetPrintAreaId ?? (deco as any)?.metadata?.printAreaId ?? '').trim();
        const area = areaId ? getPrintAreas().find((candidate) => String(candidate?.id ?? '') === areaId) ?? null : null;
        if (area && !isDecalPlacement) placeDecorationMesh(mesh, area);
        else {
          mesh.rotation = Vector3.Zero();
          mesh.scaling = new Vector3(1, 1, 1);
          if (isDecalPlacement) (mesh as any).__asukaDecalAngle = 0;
        }
      }
    }

    const basis = getViewTransformBasis();
    const deltaX = Number((patch as any).deltaX ?? (patch as any).moveX ?? (patch as any).nudgeX ?? 0) || 0;
    const deltaY = Number((patch as any).deltaY ?? (patch as any).moveY ?? (patch as any).nudgeY ?? 0) || 0;
    const deltaZ = Number((patch as any).deltaZ ?? (patch as any).moveZ ?? (patch as any).nudgeZ ?? 0) || 0;
    if (deltaX || deltaY || deltaZ) {
      mesh.position = mesh.position
        .add(basis.right.scale(deltaX))
        .add(basis.up.scale(deltaY))
        .add(basis.forward.scale(deltaZ));
    }

    if (typeof patch.x === 'number') mesh.position.x = patch.x;
    if (typeof patch.y === 'number') mesh.position.y = patch.y;
    if (typeof (patch as any).z === 'number') mesh.position.z = Number((patch as any).z) || 0;
    const currentDecalAngle = Number((mesh as any).__asukaDecalAngle ?? (deco as any)?.metadata?.decalAngle ?? 0) || 0;
    if (typeof (patch as any).rotationBy === 'number') {
      const deltaAngle = Number((patch as any).rotationBy) || 0;
      if (isDecalPlacement) (mesh as any).__asukaDecalAngle = currentDecalAngle + deltaAngle;
      mesh.rotation.z += deltaAngle;
    }
    if (typeof patch.rotation === 'number') {
      const angle = Number(patch.rotation) || 0;
      if (isDecalPlacement) (mesh as any).__asukaDecalAngle = angle;
      mesh.rotation.z = angle;
    }
    if (typeof (patch as any).scaleBy === 'number') {
      const factor = Math.max(0.05, Math.min(20, Number((patch as any).scaleBy) || 1));
      mesh.scaling = new Vector3(
        clampScaleComponent(mesh.scaling.x * factor),
        clampScaleComponent(mesh.scaling.y * factor),
        clampScaleComponent(mesh.scaling.z * factor),
      );
    }
    if (typeof patch.scale === 'number') mesh.scaling = new Vector3(patch.scale, patch.scale, patch.scale);
    if (typeof patch.scaleX === 'number') mesh.scaling.x = patch.scaleX;
    if (typeof patch.scaleY === 'number') mesh.scaling.y = patch.scaleY;
    if (typeof (patch as any).scaleZ === 'number') mesh.scaling.z = Number((patch as any).scaleZ) || 1;
    if (typeof patch.visible === 'boolean') mesh.setEnabled(patch.visible);
    if (typeof patch.opacity === 'number') {
      const opacity = Math.max(0, Math.min(1, Number(patch.opacity) || 0));
      const materials = new Set<any>([
        mesh.material,
        (mesh as any).__asukaSourceMaterial,
        ...(decalFragmentsByObjectId.get(String(mesh.id ?? '')) ?? []).map((fragment) => fragment?.material),
      ].filter(Boolean));
      for (const material of materials) {
        try { material.alpha = opacity; } catch {}
      }
    }
    if ((patch as any).color) applyDecorationColor(mesh, String((patch as any).color), deco);
  };

  const objectRef = (id: string, kind?: string): AsukaObjectRef => ({ id, kind });

  const requestSelectionMenuForAdded3DLayer = (idInput: string, source: string): void => {
    const id = String(idInput ?? '').trim();
    if (!id) return;
    const emitRequest = () => {
      const object = get3DObjectSummaries({
        objectId: id,
        includeReadonly: true,
        includeTechnical: false,
        includeScreenBox: true,
        source: 'added-layer-selection-menu',
      })[0] ?? null;
      if (!object) return;
      try {
        emitAsukaEvent('selection.request.menu', {
          engine: '3d',
          id,
          source,
          forceOpen: true,
          object,
          kind: object.kind,
          label: object.label,
          locked: object.locked,
          visible: object.visible,
          metadata: object.metadata,
          screenBox: (object as any)?.metadata?.screenBox,
        });
      } catch {}
    };
    try {
      if (typeof requestAnimationFrame === 'function') requestAnimationFrame(() => emitRequest());
      else if (typeof queueMicrotask === 'function') queueMicrotask(emitRequest);
      else setTimeout(emitRequest, 0);
    } catch {}
  };

  const projectionModeFromMesh = (mesh: Mesh | null | undefined, fallback: RuntimeProjectionMode = 'plane'): RuntimeProjectionMode => {
    const mode = String((mesh as any)?.__asukaProjectionMode ?? fallback).trim();
    return mode === 'decal' || mode === 'surface' || mode === 'texture' ? mode : 'plane';
  };

  const findDecorationRecord = (id: string): DecorationRecord | null => decorationRecords.find((deco) => String(deco.id ?? '') === String(id)) ?? null;

  const getFixedSlot3DDefinitionForDecoration = (deco?: DecorationRecord | null): any | null => {
    const slotId = String((deco as any)?.metadata?.slotId3D ?? '').trim();
    if (!slotId) return null;
    const slots = (useAsukaStore as any).getState?.()?.fixedSlots;
    return (Array.isArray(slots) ? slots : []).find((slot: any) => String(slot?.id ?? '').trim() === slotId) ?? null;
  };

  const getFixedSlot3DConstraintsForDecoration = (deco?: DecorationRecord | null): AsukaFixedSlotConstraintsLike => {
    const slot = getFixedSlot3DDefinitionForDecoration(deco);
    return normalizeFixedSlotConstraints(
      slot?.constraints
        ?? (deco as any)?.metadata?.fixedSlotConstraints
        ?? null,
    );
  };

  const getFixedSlot3DDecorationMetadata = (slot?: any | null): Record<string, unknown> => {
    const slotId = String(slot?.id ?? '').trim();
    if (!slotId) return {};
    return {
      fixedSlot3D: true,
      slotId3D: slotId,
      fixedSlotConstraints: clonePlain(normalizeFixedSlotConstraints(slot?.constraints ?? {})),
      fixedSlotSurface3D: clonePlain(slot?.surface3d ?? null),
    };
  };

  const clampFixedSlot3DMeshSize = (mesh?: Mesh | null, deco?: DecorationRecord | null): void => {
    if (!mesh || !deco) return;
    const constraints = getFixedSlot3DConstraintsForDecoration(deco);
    if (![constraints.minWidth, constraints.maxWidth, constraints.minHeight, constraints.maxHeight].some((value) => typeof value === 'number')) return;
    const runtimeSize = (mesh as any).__asukaDecalSize;
    const metadataSize = (deco as any)?.metadata?.decalSize;
    const rawSize = Array.isArray(runtimeSize) ? runtimeSize : Array.isArray(metadataSize) ? metadataSize : null;
    if (!rawSize) return;
    const baseWidth = Math.max(0.0001, Math.abs(Number(rawSize[0]) || 0));
    const baseHeight = Math.max(0.0001, Math.abs(Number(rawSize[1]) || 0));
    const signX = Number(mesh.scaling.x) < 0 ? -1 : 1;
    const signY = Number(mesh.scaling.y) < 0 ? -1 : 1;
    let width = baseWidth * Math.max(0.025, Math.abs(Number(mesh.scaling.x) || 1));
    let height = baseHeight * Math.max(0.025, Math.abs(Number(mesh.scaling.y) || 1));
    const toWorldSize = (value: number | undefined) => typeof value === 'number' ? value / 100 : undefined;
    const minWidth = toWorldSize(constraints.minWidth);
    const maxWidth = toWorldSize(constraints.maxWidth);
    const minHeight = toWorldSize(constraints.minHeight);
    const maxHeight = toWorldSize(constraints.maxHeight);
    if (typeof minWidth === 'number') width = Math.max(width, minWidth);
    if (typeof maxWidth === 'number') width = Math.min(width, maxWidth);
    if (typeof minHeight === 'number') height = Math.max(height, minHeight);
    if (typeof maxHeight === 'number') height = Math.min(height, maxHeight);
    mesh.scaling.x = signX * clampScaleComponent(width / baseWidth);
    mesh.scaling.y = signY * clampScaleComponent(height / baseHeight);
  };

  const syncDecorationPlacementFromMesh = (id: string): void => {
    const key = String(id ?? '').trim();
    const mesh = key ? objectMeshes.get(key) : null;
    if (!key || !mesh) return;
    decorationRecords = decorationRecords.map((deco) => {
      if (String(deco.id ?? '') !== key) return deco;
      const runtimeMetadata = getDecalRuntimeMetadata(mesh, deco);
      return {
        ...deco,
        targetMeshId: String(runtimeMetadata.targetMeshId ?? deco.targetMeshId ?? ''),
        metadata: { ...((deco as any).metadata ?? {}), ...runtimeMetadata },
        placement: decorationPlacementFromMesh(mesh, projectionModeFromMesh(mesh, deco.placement?.mode ?? 'plane')),
      } as DecorationRecord;
    });
    currentState = {
      ...(clonePlain(currentState) ?? {}),
      decorations: clonePlain(decorationRecords as Asuka3DDecoration[]),
    } as Asuka3DSceneState;
  };

  const setDecorationRuntimeEnabled = (idInput: string, enabled: boolean): void => {
    const key = String(idInput ?? '').trim();
    if (!key) return;
    const mesh = objectMeshes.get(key);
    try { mesh?.setEnabled(enabled); } catch {}
    for (const fragment of decalFragmentsByObjectId.get(key) ?? []) {
      try { fragment?.setEnabled?.(enabled); } catch {}
    }
  };

  type Preview3DMeshState = {
    enabled: boolean;
    visibility: number;
    renderOutline: boolean;
    outlineColor: Color3 | null;
    outlineWidth: number;
  };
  type Preview3DTextureObjectState = {
    area: Asuka3DPrintArea;
    object: Record<string, any>;
    hadOpacity: boolean;
    opacity: unknown;
  };
  const preview3DMeshStates = new Map<Mesh, Preview3DMeshState>();
  const preview3DTextureObjectStates: Preview3DTextureObjectState[] = [];
  let previewed3DObjectId: string | null = null;

  const get3DPreviewMeshes = (idInput: string): Mesh[] => {
    const id = String(idInput ?? '').trim();
    if (!id) return [];
    return Array.from(new Set([
      objectMeshes.get(id) ?? null,
      ...(decalFragmentsByObjectId.get(id) ?? []),
    ].filter((mesh): mesh is Mesh => !!mesh && !mesh.isDisposed())));
  };

  const clear3DObjectPreview = (): void => {
    const textureAreas = new Set<Asuka3DPrintArea>();
    for (const [mesh, state] of preview3DMeshStates) {
      if (!mesh || mesh.isDisposed()) continue;
      try { mesh.setEnabled(state.enabled); } catch {}
      try { mesh.visibility = state.visibility; } catch {}
      try { mesh.renderOutline = state.renderOutline; } catch {}
      try { if (state.outlineColor) mesh.outlineColor = state.outlineColor.clone(); } catch {}
      try { mesh.outlineWidth = state.outlineWidth; } catch {}
    }
    for (const state of preview3DTextureObjectStates) {
      if (state.hadOpacity) state.object.opacity = state.opacity;
      else delete state.object.opacity;
      textureAreas.add(state.area);
    }
    for (const area of textureAreas) {
      try { renderPrintAreaTexture(area); } catch {}
    }
    preview3DMeshStates.clear();
    preview3DTextureObjectStates.length = 0;
    previewed3DObjectId = null;
    try { scene?.render(); } catch {}
  };

  const preview3DObject = (idInput: string, options: AsukaObjectPreviewOptions = {}): void => {
    const id = String(idInput ?? '').trim();
    if (!id) {
      clear3DObjectPreview();
      return;
    }
    if (previewed3DObjectId === id) return;
    clear3DObjectPreview();

    const targetMeshes = new Set(get3DPreviewMeshes(id));
    const targetTextureObject = findTexturePrintAreaObject(id);
    if (!targetMeshes.size && !targetTextureObject) return;

    const dimOpacity = Math.max(0.08, Math.min(1, Number(options.dimOpacity ?? 0.18) || 0.18));
    const fadeOthers = options.mode !== 'outline';
    const layerIds = Array.from(new Set([
      ...decorationRecords.map((record) => String(record?.id ?? '').trim()).filter(Boolean),
      ...Array.from(decalFragmentsByObjectId.keys()),
    ]));
    for (const layerId of layerIds) {
      for (const mesh of get3DPreviewMeshes(layerId)) {
        if (!preview3DMeshStates.has(mesh)) {
          preview3DMeshStates.set(mesh, {
            enabled: mesh.isEnabled(),
            visibility: Number(mesh.visibility ?? 1),
            renderOutline: mesh.renderOutline === true,
            outlineColor: mesh.outlineColor?.clone?.() ?? null,
            outlineWidth: Number(mesh.outlineWidth ?? 0.02) || 0.02,
          });
        }
        if (targetMeshes.has(mesh)) {
          try { mesh.setEnabled(true); } catch {}
          try { mesh.visibility = 1; } catch {}
          try { mesh.renderOutline = true; } catch {}
          try { mesh.outlineColor = new Color3(37 / 255, 99 / 255, 235 / 255); } catch {}
          try { mesh.outlineWidth = 0.025; } catch {}
        } else if (fadeOthers) {
          try { mesh.visibility = Math.max(0.01, Number(mesh.visibility ?? 1) * dimOpacity); } catch {}
        }
      }
    }

    const textureAreas = new Set<Asuka3DPrintArea>();
    for (const area of getPrintAreas()) {
      if (!isTextureSlotPrintArea(area)) continue;
      for (const object of area.objects ?? []) {
        preview3DTextureObjectStates.push({
          area,
          object,
          hadOpacity: Object.prototype.hasOwnProperty.call(object, 'opacity'),
          opacity: object.opacity,
        });
        if (String(object?.id ?? '') === id) object.opacity = 1;
        else if (fadeOthers) object.opacity = Math.max(0.01, Number(object.opacity ?? 1) * dimOpacity);
        textureAreas.add(area);
      }
    }
    for (const area of textureAreas) {
      try { renderPrintAreaTexture(area); } catch {}
    }
    previewed3DObjectId = id;
    try { scene?.render(); } catch {}
  };

  const isHistoryMutationAction = (action: string): boolean => {
    const key = String(action ?? '').trim();
    if (!key || isRestoringHistory || historyMutationSuppressDepth > 0) return false;
    if (key === 'loadView' || key === 'selectObject' || key === 'selectMaterial' || key === 'focusPart') return false;
    if (key.startsWith('history.')) return false;
    return /^(add|remove|update|duplicate|bring|send|create|set|apply|reorder|move|transform|constrain)/.test(key);
  };

  const emitChanged = (action: string, id?: string | null, options: { commitHistory?: boolean } = {}) => {
    try {
      emitRenderEngineEvent({ type: 'changed', action, id: id ?? null, viewId: currentViewId, engine: '3d' });
      emitAsukaEvent('engine3d.changed', { action, id: id ?? null, viewId: currentViewId, engine: 'babylon' });
      emitAsukaEvent('canvas.object.update', { action: `3d:${action}`, id: id ?? null, viewId: currentViewId, engine: 'babylon' });
    } catch {}
    if (options.commitHistory !== false && isHistoryMutationAction(action)) {
      try { commit3DHistorySnapshot({ source: action, reason: action }); } catch {}
    }
  };

  const getProductSelectionId = (): string | null => {
    const productId = getSelectedProductId();
    return productId ? `product:${productId}` : null;
  };

  const setSelectedObject = (id: string | null, shouldEmit = true, options: { commitProductDeselect?: boolean; syncState?: boolean } = {}) => {
    const requested = id ? String(id) : null;
    const previousSelectedObjectId = selectedObjectId;
    const productSelectionId = getProductSelectionId();
    const knownProductIds = getKnownProductIds().map((id) => `product:${id}`);
    const canSelectProduct = !!requested && requested.startsWith('product:') && (requested === productSelectionId || knownProductIds.includes(requested) || !knownProductIds.length);
    const canSelectDecoration = !!requested && objectMeshes.has(requested) && objectKinds.get(requested) !== 'mesh';
    const canSelectTextureObject = !!requested && !!findTexturePrintAreaObject(requested);
    const nextId = canSelectProduct || canSelectDecoration || canSelectTextureObject ? requested : null;
    debug3DLayer('layer:selection-hit', {
      requestedLayerId: requested,
      resolvedLayerId: nextId,
      previousSelectedObjectId,
      via: canSelectDecoration ? 'decoration-primary-or-proxy' : (canSelectTextureObject ? 'texture-print-area' : (canSelectProduct ? 'product' : 'none')),
      decorationKnown: !!requested && objectMeshes.has(requested),
      fragmentCount: requested ? ((decalFragmentsByObjectId.get(requested) ?? []).length) : 0,
    }, nextId ? 'info' : 'warn');
    const previousWasProductSelection = !!previousSelectedObjectId && previousSelectedObjectId.startsWith('product:');
    const nextIsProductSelection = !!nextId && nextId.startsWith('product:');
    const leavingProductSelection = previousWasProductSelection && !nextIsProductSelection;
    if (leavingProductSelection && options.commitProductDeselect !== false) {
      const changed = runPendingProductGroupCenterOnDeselect('productGroup.center.onDeselect');
      // Deselect is the single commit point for Product 3D placement centering.
      // Switching from one Product 3D instance to another from the Elements
      // module is still a selection action, not a deselect/commit action: it
      // must never recenter the group or move the camera.
      // Be robust even if the movement path did not mark the pending flag
      // correctly: when leaving a Product selection, perform one final cheap
      // placement-center correction and persist it. This avoids repeated work
      // while arrow keys are held, but guarantees the final 0/0/0 contract.
      if (!changed && centerProductGroupOnTransform) {
        const forced = recenterProductGroup('productGroup.center.onDeselect.force');
        if (forced) emitChanged('updateProductTransform', previousSelectedObjectId);
      }
    }
    if (options.syncState !== false) {
      if (nextIsProductSelection) {
        const productId = String(nextId ?? '').slice('product:'.length).trim();
        currentState = select3DProduct(currentState, productId, { selectObject: true });
      } else {
        currentState = {
          ...(currentState ?? {}),
          selectedObjectId: nextId,
          selectedMaterialId: nextId ? null : (currentState.selectedMaterialId ?? null),
        } as Asuka3DSceneState;
      }
    }
    selectedObjectId = nextId;
    selectedPartId = String(currentState.selectedPartId ?? '').trim() || null;
    if (selectedObjectId) selectedMaterialId = null;
    const productSelected = !!selectedObjectId && selectedObjectId.startsWith('product:');
    const selectedProductInstanceId = productSelected ? selectedObjectId!.slice('product:'.length).trim() : '';
    if (!productSelected) lastProductPick = null;

    for (const [key, mesh] of objectMeshes.entries()) {
      const kind = objectKinds.get(key);
      const selected = selectedObjectId === key && kind !== 'mesh';
      const productMeshSelected = productSelected && kind === 'mesh' && meshBelongsToProductInstance(mesh, selectedProductInstanceId);
      try {
        const isInvisibleDecalControl = kind !== 'mesh' && ((mesh as any).__asukaInvisibleControlMesh === true || (projectionModeFromMesh(mesh, 'plane') === 'decal' && (mesh as any).__asukaSurfacePatchSticker !== true));
        const outlineOnPrimaryMesh = (selected || productMeshSelected) && !isInvisibleDecalControl;
        mesh.renderOutline = outlineOnPrimaryMesh;
        mesh.outlineColor = color3FromInput(productMeshSelected ? '#16a34a' : '#2563eb');
        mesh.outlineWidth = outlineOnPrimaryMesh ? 0.035 : 0;
        (mesh as any).showBoundingBox = outlineOnPrimaryMesh && (mesh as any).__asukaExactFaceNetPrintSurface3D !== true;
      } catch {}
      if (kind !== 'mesh') setDecalFragmentsSelectionState(key, selected);
    }
    for (const key of decalFragmentsByObjectId.keys()) {
      if (!objectMeshes.has(key)) setDecalFragmentsSelectionState(key, selectedObjectId === key);
    }
    renderPrintAreaVisuals();
    renderAssemblyEnvironment();
    if (shouldEmit) emitChanged('selectObject', selectedObjectId);
  };

  const selectProductInternal = (id: string | null, options: { emit?: boolean; source?: string; selectObject?: boolean } = {}): string | null => {
    const raw = String(id ?? '').trim();
    const shouldEmit = options.emit !== false;
    if (!raw) {
      setSelectedObject(null, false);
      if (shouldEmit) emitChanged('selectProduct', null);
      return null;
    }
    const requested = raw.replace(/^product:/, '').trim();
    const knownProductIds = getKnownProductIds();
    let selectedProductId = requested;
    if (selectedProductId && !knownProductIds.includes(selectedProductId)) {
      const found = getProductsFromState(currentState).find((product: any) => [product?.id, product?.name, product?.catalogId]
        .map((value) => String(value ?? '').trim())
        .filter(Boolean)
        .includes(selectedProductId));
      selectedProductId = getProductId(found) || '';
    }
    if (!selectedProductId) {
      selectedProductId = getSelectedProductId() || knownProductIds[knownProductIds.length - 1] || '';
    }
    const productId = selectedProductId ? `product:${selectedProductId}` : getProductSelectionId();
    if (!productId) {
      setSelectedObject(null, false);
      currentState = select3DProduct(currentState, null, { selectObject: false });
      selectedPartId = null;
      if (shouldEmit) emitChanged('selectProduct', null);
      return null;
    }
    if (options.selectObject === false) {
      setSelectedObject(null, false);
      currentState = select3DProduct(currentState, selectedProductId, { selectObject: false });
      selectedPartId = currentState.selectedPartId ?? null;
    } else {
      setSelectedObject(productId, false);
    }
    if (shouldEmit) emitChanged('selectProduct', selectedProductId);
    return selectedProductId;
  };

  const getProductIdFromPickTarget = (target?: AsukaSurfacePickTarget | null, pickedMesh?: Mesh | null): string => {
    const metadata: any = target?.metadata && typeof target.metadata === 'object' ? target.metadata : {};
    const candidates = [
      metadata?.asukaProductId,
      metadata?.productId,
      getProductIdFromMesh(pickedMesh),
      (pickedMesh as any)?.metadata?.asukaProductId,
      (pickedMesh as any)?.metadata?.productId,
    ];
    for (const candidate of candidates) {
      const value = String(candidate ?? '').trim();
      if (value) return value;
    }
    const known = getKnownProductIds();
    return known.length === 1 ? known[0] : '';
  };

  const isCtrlLeftClickProductSelectEvent = (event?: PointerEvent | MouseEvent | null): boolean => {
    if (!event || (event as any).ctrlKey !== true) return false;
    const button = Number((event as any).button ?? 0);
    return !Number.isFinite(button) || button === 0;
  };

  const selectProductFromCanvasTarget = (target?: AsukaSurfacePickTarget | null, event?: PointerEvent | MouseEvent | null, pickedMesh?: Mesh | null): boolean => {
    if (!isCtrlLeftClickProductSelectEvent(event)) return false;
    const kind = String(target?.kind ?? '').trim();
    if (kind !== 'product' && kind !== 'mesh') return false;
    const instanceId = getProductIdFromPickTarget(target, pickedMesh ?? null);
    if (!instanceId) return false;
    selectProductInternal(instanceId, { emit: true, source: 'canvas.ctrl-click-select-product' });
    return true;
  };

  const getDecorationIdFromPickedMesh = (pickedMesh: any): string | null => {
    let cursor = pickedMesh;
    while (cursor) {
      const id = String(cursor?.id ?? cursor?.name ?? '').trim();
      if (id && objectMeshes.has(id)) {
        const kind = objectKinds.get(id);
        if (kind !== 'mesh') return id;
        const metadata: any = (cursor as any)?.metadata ?? {};
        const instanceId = String(metadata?.asukaProductId ?? metadata?.productId ?? '').trim();
        if (instanceId) return `product:${instanceId}`;
        const productSelectionId = getProductSelectionId();
        if (productSelectionId) return productSelectionId;
      }
      const proxyId = String((cursor as any)?.__asukaDecorationProxyFor ?? '').trim();
      if (proxyId && objectMeshes.has(proxyId)) return proxyId;
      cursor = cursor?.parent ?? null;
    }
    return null;
  };

  const findTexturePrintAreaAtPointer = (event?: PointerEvent | MouseEvent | null, pickedMesh?: any): { area: Asuka3DPrintArea; point: { x: number; y: number; uv: { x: number; y: number } }; contentRect: { x: number; y: number; width: number; height: number; constrained: boolean } } | null => {
    for (const area of getPrintAreas().filter((candidate) => isTextureSlotPrintArea(candidate))) {
      const point = getTexturePrintAreaPointerPoint(area, event ?? null, pickedMesh);
      if (!point) continue;
      const contentRect = getTexturePrintAreaContentRect(area);
      const pad = Math.max(6, Math.min(24, Math.min(contentRect.width, contentRect.height) * 0.035));
      const insideContent = point.x >= contentRect.x - pad
        && point.x <= contentRect.x + contentRect.width + pad
        && point.y >= contentRect.y - pad
        && point.y <= contentRect.y + contentRect.height + pad;
      if (insideContent) return { area, point, contentRect };
    }
    return null;
  };

  const isDecorationScreenFallbackPickVisible = (id: string, event?: PointerEvent | MouseEvent | null): boolean => {
    const key = String(id ?? '').trim();
    const mesh = objectMeshes.get(key);
    const deco = findDecorationRecord(key);
    if (!mesh || !deco) return false;
    if (projectionModeFromMesh(mesh, deco?.placement?.mode ?? 'plane') !== 'decal') return true;
    const ray = getPointerPickingRay(event ?? null);
    if (!scene || !ray) return false;
    const targetMesh = getDecorationSurfaceTargetMesh(deco);
    if (!targetMesh) return false;
    try {
      const targetId = String(targetMesh.id || targetMesh.name || '').trim();
      const pick = scene.pickWithRay(ray, (candidate: any) => !!candidate && (candidate === targetMesh || candidate.id === targetId || candidate.name === targetId), false);
      if (!pick?.hit || !pick.pickedPoint) return false;
      const point = pick.pickedPoint as Vector3;
      let normal = (typeof (pick as any).getNormal === 'function' ? (pick as any).getNormal(true, true) : null) as Vector3 | null;
      if (!normal || normal.lengthSquared() < 0.0001) normal = camera ? camera.position.subtract(point) : new Vector3(0, 0, 1);
      if (normal.lengthSquared() < 0.0001) return false;
      normal.normalize();
      const viewBasis = getViewTransformBasis();
      let right = viewBasis.right.subtract(normal.scale(Vector3.Dot(viewBasis.right, normal)));
      if (right.lengthSquared() < 0.0001) right = Vector3.Cross(new Vector3(0, 1, 0), normal);
      if (right.lengthSquared() < 0.0001) right = new Vector3(1, 0, 0);
      right.normalize();
      let up = viewBasis.up.subtract(normal.scale(Vector3.Dot(viewBasis.up, normal)));
      if (up.lengthSquared() < 0.0001) up = Vector3.Cross(normal, right);
      if (up.lengthSquared() < 0.0001) up = new Vector3(0, 1, 0);
      up.normalize();
      let center = mesh.position.clone();
      try {
        mesh.computeWorldMatrix(true);
        center = mesh.getBoundingInfo().boundingSphere.centerWorld.clone();
      } catch {}
      const offset = point.subtract(center);
      const size = getDecorationDecalSize(mesh, deco, getDecorationPrintArea(deco));
      const pad = Math.max(0.025, Math.max(size.x, size.y) * 0.22);
      const localX = Math.abs(Vector3.Dot(offset, right));
      const localY = Math.abs(Vector3.Dot(offset, up));
      return localX <= size.x / 2 + pad && localY <= size.y / 2 + pad;
    } catch {
      return false;
    }
  };

  const pickDecorationIdFromPointer = (event?: PointerEvent | MouseEvent | null, pickedMesh?: any): string | null => {
    const textureHit = findTexturePrintAreaObjectAtPointer(event ?? null, pickedMesh);
    if (textureHit) return String(textureHit.object?.id ?? '').trim() || null;
    const direct = getDecorationIdFromPickedMesh(pickedMesh);
    const directDecoration = direct
      && !direct.startsWith('product:')
      && objectMeshes.has(direct)
      && objectKinds.get(direct) !== 'mesh'
      ? direct
      : null;
    // Projected decorations use broad helper/proxy meshes so they remain easy
    // to drag on curved products. A Babylon mesh hit is therefore only valid
    // when the pointer is also inside the decoration's visible footprint.
    if (directDecoration && isDecorationScreenFallbackPickVisible(directDecoration, event ?? null)) {
      return directDecoration;
    }
    const ray = getPointerPickingRay(event ?? null);
    if (!scene || !camera || !ray) return null;
    try {
      const hits = typeof (scene as any).multiPickWithRay === 'function'
        ? ((scene as any).multiPickWithRay(ray, (mesh: any) => {
          const id = getDecorationIdFromPickedMesh(mesh);
          return !!id && !id.startsWith('product:') && objectMeshes.has(id) && objectKinds.get(id) !== 'mesh';
        }, false) ?? [])
        : [];
      for (const hit of hits) {
        const id = getDecorationIdFromPickedMesh(hit?.pickedMesh);
        if (
          id
          && !id.startsWith('product:')
          && objectMeshes.has(id)
          && objectKinds.get(id) !== 'mesh'
          && isDecorationScreenFallbackPickVisible(id, event ?? null)
        ) {
          return id;
        }
      }
    } catch {}

    // Decal geometry can be hard to hit because it is clipped and almost flush
    // with the product. As a last resort, use the screen position of the layer
    // so a click near the visible decoration still selects it like Fabric does.
    const pointer = getCanvasPointerCoordinates(event ?? null);
    if (!pointer || !engine || !scene) return null;
    let best: { id: string; distance: number } | null = null;
    try {
      const transform = scene.getTransformMatrix();
      const renderWidth = Math.max(1, engine.getRenderWidth());
      const renderHeight = Math.max(1, engine.getRenderHeight());
      const viewport = camera.viewport.toGlobal(renderWidth, renderHeight);
      const canvasRect = canvas?.getBoundingClientRect?.();
      const screenScaleX = canvasRect ? canvasRect.width / renderWidth : 1;
      const screenScaleY = canvasRect ? canvasRect.height / renderHeight : 1;
      for (const [id, mesh] of objectMeshes.entries()) {
        if (!mesh || mesh.isDisposed?.() || objectKinds.get(id) === 'mesh') continue;
        let center = mesh.position;
        try {
          mesh.computeWorldMatrix(true);
          center = mesh.getBoundingInfo().boundingSphere.centerWorld.clone();
        } catch {}
        const projected = Vector3.Project(center, Matrix.Identity(), transform, viewport);
        // Vector3.Project returns render-target pixels while pointer coordinates
        // are CSS pixels relative to the canvas. Comparing them directly makes
        // sticker hit-testing miss as soon as Babylon's render size differs from
        // the displayed canvas size (high-DPI and responsive layouts).
        const dx = Number(projected.x) * screenScaleX - pointer.x;
        const dy = Number(projected.y) * screenScaleY - pointer.y;
        const centerDistance = Math.sqrt(dx * dx + dy * dy);
        let distance = centerDistance;
        let box: ReturnType<typeof getMeshScreenBox> = null;
        let insideExpandedBox = false;
        try {
          box = getMeshScreenBox(mesh);
          if (box) {
            const px = Number((event as any)?.clientX ?? pointer.x);
            const py = Number((event as any)?.clientY ?? pointer.y);
            const pad = 18;
            insideExpandedBox = px >= box.left - pad && px <= box.left + box.width + pad && py >= box.top - pad && py <= box.top + box.height + pad;
            if (insideExpandedBox) distance = 0;
          }
        } catch {}
        const threshold = Math.max(72, Math.min(140, 36 + Math.max(0, Number(box?.width ?? 0), Number(box?.height ?? 0)) * 0.45));
        const hasVisibleFragment = (decalFragmentsByObjectId.get(id) ?? []).some(
          (fragment) => !!fragment && !fragment.isDisposed?.() && fragment.isVisible !== false && fragment.isEnabled(),
        );
        const selectedHit = id === selectedObjectId
          && ((mesh.isVisible !== false && mesh.isEnabled()) || hasVisibleFragment)
          && (insideExpandedBox || centerDistance <= 48);
        const surfaceVisible = distance <= threshold && (selectedHit || isDecorationScreenFallbackPickVisible(id, event ?? null));
        const rankedDistance = selectedHit ? -1 : distance;
        if (surfaceVisible && (!best || rankedDistance < best.distance)) best = { id, distance: rankedDistance };
      }
    } catch {}
    return best?.id ?? null;
  };

  const getSurfacePickEventFromOptions = (options?: AsukaSurfacePickOptions | null): (PointerEvent | MouseEvent | any) | null => {
    if (!options || typeof options !== 'object') return null;
    const rect = canvas?.getBoundingClientRect?.();
    const rawClientX = Number((options as any).clientX);
    const rawClientY = Number((options as any).clientY);
    if ((options as any).space === 'screen' || Number.isFinite(rawClientX) || Number.isFinite(rawClientY)) {
      assertCompatibleCoordinateFrame(options, { space: 'screen', unit: 'css-px' }, {
        engine: '3d',
        operation: 'surface.pick',
      });
      if (Number.isFinite(rawClientX) && Number.isFinite(rawClientY)) return { clientX: rawClientX, clientY: rawClientY };
      throw new AsukaRendererError('INVALID_INPUT', 'surface.pick requires finite clientX and clientY screen coordinates.', {
        engine: '3d',
        operation: 'surface.pick',
      });
    }
    const rawX = Number((options as any).x);
    const rawY = Number((options as any).y);
    if ((options as any).space === 'canvas' || Number.isFinite(rawX) || Number.isFinite(rawY)) {
      assertCompatibleCoordinateFrame(options, { space: 'canvas', unit: 'canvas-px' }, {
        engine: '3d',
        operation: 'surface.pick',
      });
      if (rect && Number.isFinite(rawX) && Number.isFinite(rawY)) return { clientX: rect.left + rawX, clientY: rect.top + rawY };
      throw new AsukaRendererError('INVALID_INPUT', 'surface.pick requires a mounted canvas and finite x/y canvas coordinates.', {
        engine: '3d',
        operation: 'surface.pick',
      });
    }
    throw new AsukaRendererError('INVALID_COORDINATE_FRAME', 'surface.pick requires screen/css-px or canvas/canvas-px coordinates.', {
      engine: '3d',
      operation: 'surface.pick',
    });
  };

  const makeSurfacePickTarget = (target: Partial<AsukaSurfacePickTarget> & { kind: AsukaSurfacePickTarget['kind'] }): AsukaSurfacePickTarget => ({
    kind: target.kind,
    objectId: target.objectId ?? null,
    printAreaId: target.printAreaId ?? null,
    meshId: target.meshId ?? null,
    label: target.label ?? null,
    point: target.point ?? null,
    ...(target.point ? { pointSpace: 'world' as const, pointUnit: 'world-unit' as const } : null),
    normal: target.normal ?? null,
    uv: target.uv ?? null,
    ...(target.uv ? { uvSpace: 'uv' as const, uvUnit: 'normalized' as const } : null),
    distance: typeof target.distance === 'number' && Number.isFinite(target.distance) ? target.distance : null,
    priority: typeof target.priority === 'number' ? target.priority : undefined,
    metadata: target.metadata,
  });

  const recordSurfaceInteraction = (target: AsukaSurfacePickTarget | null, metadata: Record<string, unknown> = {}) => {
    lastSurfaceInteraction = {
      engine: '3d',
      selectedObjectId,
      activePrintAreaId: getActivePrintAreaId() || null,
      pickedObjectId: target?.objectId ?? null,
      pickedPrintAreaId: target?.printAreaId ?? null,
      pickedMeshId: target?.meshId ?? null,
      lastPick: target ? clonePlain(target) : null,
      occlusion: {
        depthTest: true,
        depthWrite: false,
        renderingGroupId: typeof (target?.metadata as any)?.renderingGroupId === 'number' ? Number((target?.metadata as any).renderingGroupId) : null,
        textureSlotsOpaque: true,
        overlaysMatchProductRenderingGroup: true,
      },
      metadata: {
        ...(metadata ?? {}),
        productPick: lastProductPick ? clonePlain(lastProductPick) : null,
      },
    };
  };

  const getSurfaceInteractionDebugInternal = (): AsukaSurfaceInteractionDebug => clonePlain({
    ...lastSurfaceInteraction,
    engine: '3d',
    selectedObjectId,
    activePrintAreaId: getActivePrintAreaId() || null,
  });

  const pickSurfaceAtInternal = (options?: AsukaSurfacePickOptions | null, pointerInfo?: any): AsukaSurfacePickTarget | null => {
    const event = (pointerInfo?.event as PointerEvent | MouseEvent | undefined) ?? getSurfacePickEventFromOptions(options);
    const pickedMesh = pointerInfo?.pickInfo?.pickedMesh ?? null;
    const textureHit = findTexturePrintAreaObjectAtPointer(event ?? null, pickedMesh);
    if (textureHit) {
      const objectId = String(textureHit.object?.id ?? '').trim() || null;
      return makeSurfacePickTarget({
        kind: 'object',
        objectId,
        printAreaId: String(textureHit.area?.id ?? '').trim() || null,
        meshId: textureHit.area?.targetMeshId ? String(textureHit.area.targetMeshId) : null,
        label: String(textureHit.object?.label ?? textureHit.object?.name ?? textureHit.object?.type ?? objectId ?? 'Layer'),
        uv: textureHit.point.uv,
        priority: 100,
        metadata: { mode: 'texture', texturePoint: { x: textureHit.point.x, y: textureHit.point.y } },
      });
    }

    const textureArea = findTexturePrintAreaAtPointer(event ?? null, pickedMesh);
    if (textureArea) {
      const areaId = String(textureArea.area?.id ?? '').trim() || null;
      return makeSurfacePickTarget({
        kind: 'print-area',
        printAreaId: areaId,
        meshId: textureArea.area?.targetMeshId ? String(textureArea.area.targetMeshId) : null,
        label: String(textureArea.area?.label ?? areaId ?? 'Print area'),
        uv: textureArea.point.uv,
        priority: 80,
        metadata: { mode: 'texture', texturePoint: { x: textureArea.point.x, y: textureArea.point.y } },
      });
    }

    const printAreaId = getPrintAreaIdFromPickedMesh(pickedMesh);
    if (printAreaId) {
      const area = getPrintAreaById(printAreaId);
      return makeSurfacePickTarget({
        kind: 'print-area',
        printAreaId,
        meshId: area?.targetMeshId ? String(area.targetMeshId) : null,
        label: String(area?.label ?? printAreaId),
        priority: 70,
        metadata: { mode: area?.mode ?? 'print-area-visual' },
      });
    }

    const objectId = pickDecorationIdFromPointer(event ?? null, pickedMesh);
    if (objectId && objectId !== getProductSelectionId()) {
      const object = get3DObjectSummaries().find((candidate) => String(candidate?.id ?? '') === objectId) as any;
      return makeSurfacePickTarget({
        kind: 'object',
        objectId,
        printAreaId: String(object?.targetPrintAreaId ?? object?.metadata?.targetPrintAreaId ?? object?.metadata?.printAreaId ?? '').trim() || null,
        meshId: String(object?.targetMeshId ?? object?.metadata?.targetMeshId ?? '').trim() || null,
        label: String(object?.label ?? objectId),
        priority: 60,
        metadata: { mode: object?.metadata?.placement ?? 'surface' },
      });
    }

    let pick = pointerInfo?.pickInfo ?? null;
    if ((!pick?.hit || !pick?.pickedMesh) && scene) {
      const ray = getPointerPickingRay(event ?? null);
      if (ray) {
        try { pick = scene.pickWithRay(ray, (mesh: any) => modelMeshes.some((candidate) => candidate === mesh || candidate.id === mesh?.id || candidate.name === mesh?.name), false); } catch {}
      }
    }
    const mesh = (pick?.pickedMesh ?? pickedMesh ?? null) as Mesh | null;
    if (mesh && modelMeshes.some((candidate) => candidate === mesh || candidate.id === mesh.id || candidate.name === mesh.name)) {
      const productPick = getProductPickFromPickInfo(pick);
      const point = pick?.pickedPoint ? fromVec3(pick.pickedPoint as Vector3) : null;
      let normal: [number, number, number] | null = null;
      try {
        const raw = pick?.getNormal?.(true) ?? pick?.getNormal?.() ?? null;
        if (raw && raw.lengthSquared?.() > 0.0001) normal = fromVec3((raw as Vector3).normalize());
      } catch {}
      const meshId = String(mesh.id || mesh.name || '').trim() || null;
      return makeSurfacePickTarget({
        kind: productPick?.productPartId ? 'product' : 'mesh',
        meshId,
        label: productPick?.productPartLabel ?? mesh?.name ?? meshId ?? 'Product surface',
        point,
        normal,
        uv: getPickTextureUv(pick),
        priority: 40,
        metadata: { ...clonePlain(productPick ?? {}), renderingGroupId: Number((mesh as any).renderingGroupId ?? 0) },
      });
    }

    return makeSurfacePickTarget({ kind: 'none', priority: 0 });
  };

  const isDraggableDecoration = (id: string | null): id is string => {
    const key = String(id ?? '').trim();
    if (!key) return false;
    const textureHit = findTexturePrintAreaObject(key);
    if (textureHit) return textureHit.object?.locked !== true && (textureHit.area as any)?.locked !== true;
    if (!objectMeshes.has(key)) return false;
    if (objectKinds.get(key) === 'mesh') return false;
    const deco = findDecorationRecord(key) as any;
    return deco?.locked !== true;
  };

  const clearCameraInteractionMomentum = () => {
    const cam: any = camera as any;
    if (!cam) return;
    for (const key of ['inertialAlphaOffset', 'inertialBetaOffset', 'inertialRadiusOffset', 'inertialPanningX', 'inertialPanningY']) {
      try { cam[key] = 0; } catch {}
    }
  };

  const detachCameraForObjectDrag = () => {
    clearCameraInteractionMomentum();
    try { camera?.detachControl(); } catch {}
  };

  const restoreCameraAfterObjectDrag = (cameraState?: Asuka3DSceneState['camera']) => {
    clearCameraInteractionMomentum();
    const cam = camera;
    if (cam && cameraState) {
      try {
        cameraPersistenceSuppressedUntil = Date.now() + 120;
        const target = toVec3(cameraState.target, fromVec3(cam.target));
        try { cam.setTarget(target); } catch { cam.target = target; }
        const alpha = Number((cameraState as any).alpha);
        const beta = Number((cameraState as any).beta);
        const radius = Number((cameraState as any).radius);
        if (Number.isFinite(alpha) && Number.isFinite(beta) && Number.isFinite(radius) && radius > 0) {
          cam.alpha = alpha;
          cam.beta = beta;
          cam.radius = radius;
        } else {
          const position = toVec3(cameraState.position, fromVec3(cam.position));
          try { cam.setPosition(position); } catch { cam.position = position; }
        }
        if (typeof cameraState.fov === 'number' && Number.isFinite(cameraState.fov)) cam.fov = cameraState.fov;
        lastPersistedCameraSignature = getCameraStateSignature(readCameraState());
      } catch {}
    }
    clearCameraInteractionMomentum();
    try { if (canvas) camera?.attachControl(canvas, true); } catch {}
    clearCameraInteractionMomentum();
  };

  const getCanvasPointerCoordinates = (event?: PointerEvent | MouseEvent | null): { x: number; y: number } | null => {
    if (canvas && event && typeof (event as any).clientX === 'number' && typeof (event as any).clientY === 'number') {
      const rect = canvas.getBoundingClientRect();
      return {
        x: (Number((event as any).clientX) || rect.left) - rect.left,
        y: (Number((event as any).clientY) || rect.top) - rect.top,
      };
    }
    const x = Number((scene as any)?.pointerX);
    const y = Number((scene as any)?.pointerY);
    return Number.isFinite(x) && Number.isFinite(y) ? { x, y } : null;
  };

  const getPointerPickingRay = (event?: PointerEvent | MouseEvent | null) => {
    if (!scene || !camera) return null;
    const point = getCanvasPointerCoordinates(event);
    if (!point) return null;
    try { return scene.createPickingRay(point.x, point.y, Matrix.Identity(), camera); } catch { return null; }
  };

  const getDragPlaneForMesh = (mesh: Mesh): Plane | null => {
    let normal = camera ? camera.position.subtract(mesh.position) : getViewTransformBasis().forward.scale(-1);
    if (normal.lengthSquared() < 0.0001) normal = new Vector3(0, 0, 1);
    normal.normalize();
    try { return Plane.FromPositionAndNormal(mesh.position, normal); } catch { return null; }
  };

  const getPointerWorldOnPlane = (plane: Plane | null | undefined, event?: PointerEvent | MouseEvent | null): Vector3 | null => {
    if (!plane) return null;
    const ray = getPointerPickingRay(event);
    if (!ray) return null;
    const distance = ray.intersectsPlane(plane);
    if (typeof distance !== 'number' || !Number.isFinite(distance)) return null;
    try { return ray.origin.add(ray.direction.scale(distance)); } catch { return null; }
  };

  const getDecorationPrintArea = (deco?: DecorationRecord | null): Asuka3DPrintArea | null => {
    const areaId = String((deco as any)?.targetPrintAreaId ?? (deco as any)?.metadata?.printAreaId ?? '').trim();
    if (!areaId) return null;
    if (areaId === AUTO_PRODUCT_SURFACE_AREA_ID) {
      return getAutomaticProductSurfacePrintArea({ targetMeshId: deco?.targetMeshId, metadata: deco?.metadata });
    }
    return getPrintAreas().find((area) => String(area?.id ?? '') === areaId) ?? null;
  };

  const getSafeDecalProjectionSize = (input?: Vector3 | null): Vector3 => {
    const source = input && Number.isFinite(input.x) && Number.isFinite(input.y) && Number.isFinite(input.z)
      ? input
      : new Vector3(0.8, 0.5, 0.055);
    const width = Math.max(0.025, Math.abs(source.x));
    const height = Math.max(0.025, Math.abs(source.y));
    // The previous safety pass made this box too thin. Babylon decals need the
    // projection volume to actually intersect the product surface; otherwise the
    // layer exists in state/Layers but renders empty. Keep it controlled, but
    // not so shallow that curved or slightly offset surfaces disappear.
    const maxFace = Math.max(width, height);
    const depthHint = Math.abs(source.z) || Math.max(0.045, Math.min(maxFace * 0.28, 0.09));
    const depth = Math.max(0.035, Math.min(depthHint, maxFace * 0.45, 0.12));
    return new Vector3(width, height, depth);
  };

  const getSafeDecalProjectionPosition = (position: Vector3, normal: Vector3, size?: Vector3 | null): Vector3 => {
    // The picked surface position is already nudged a few millimeters outward.
    // Adding another offset can move the decal box outside the mesh, producing
    // invisible decals. Keep the projection center at the picked surface point
    // and rely on material bias for z-fighting.
    void normal;
    void size;
    return position;
  };

  const getDecorationDecalSize = (mesh?: Mesh | null, deco?: DecorationRecord | null, area?: Asuka3DPrintArea | null): Vector3 => {
    const runtimeSize = (mesh as any)?.__asukaDecalSize;
    const metadataSize = (deco as any)?.metadata?.decalSize;
    const rawSize = Array.isArray(runtimeSize) ? runtimeSize : Array.isArray(metadataSize) ? metadataSize : null;
    let size = rawSize ? toVec3(rawSize, [0.8, 0.5, 0.08]) : null;
    if (!size) {
      const bounds = getPrintAreaBounds(area ?? getDecorationPrintArea(deco));
      size = bounds.size.clone();
    }
    // Restored projected meshes are rebuilt from their physical projection box,
    // so fold the serialized transform scale into that box when no runtime mesh
    // exists yet. This preserves user resizing without scaling world-space decal
    // vertices around the scene origin after recreation.
    const placementScale = (deco as any)?.placement?.scale;
    const scaling = mesh?.scaling
      ?? (Array.isArray(placementScale) ? toVec3(placementScale, [1, 1, 1]) : null);
    const scaleX = Math.max(0.025, Math.abs(Number(scaling?.x ?? 1) || 1));
    const scaleY = Math.max(0.025, Math.abs(Number(scaling?.y ?? 1) || 1));
    return new Vector3(
      Math.max(0.025, Math.abs(size.x) * scaleX),
      Math.max(0.025, Math.abs(size.y) * scaleY),
      Math.max(0.01, Math.abs(size.z)),
    );
  };

  const getDecorationSurfaceTargetMesh = (deco?: DecorationRecord | null): Mesh | null => {
    const targetId = String((deco as any)?.targetMeshId ?? (deco as any)?.metadata?.targetMeshId ?? '').trim();
    return findModelMesh(targetId || 'product') ?? findModelMesh(null);
  };

  const getExplicitPrintAreaTargetMesh = (area?: Asuka3DPrintArea | null): Mesh | null => {
    const targetMeshId = String((area as any)?.targetMeshId ?? split3DList((area as any)?.targetMeshIds)[0] ?? '').trim();
    if (!area || isAutomaticProductSurfaceArea(area) || !targetMeshId || targetMeshId.toLowerCase() === 'product') return null;
    return findModelMesh(targetMeshId) ?? null;
  };

  const getDecorationDragPreferredSurfaceMesh = (deco?: DecorationRecord | null): Mesh | null => {
    const areaId = String((deco as any)?.targetPrintAreaId ?? (deco as any)?.metadata?.printAreaId ?? '').trim();
    // Automatic product-surface decorations should be able to slide across the
    // whole visible product, even when the GLB is split into several meshes.
    if (!areaId || areaId === AUTO_PRODUCT_SURFACE_AREA_ID || (deco as any)?.metadata?.autoProductSurface === true) return null;
    const area = getDecorationPrintArea(deco);
    const faceNetTargetMeshId = getExactFaceNetAreaTargetMeshId(area);
    if (faceNetTargetMeshId) return findModelMesh(faceNetTargetMeshId) ?? getDecorationSurfaceTargetMesh(deco);
    const areaTargetMesh = getExplicitPrintAreaTargetMesh(area);
    if (areaTargetMesh) return areaTargetMesh;
    return getDecorationSurfaceTargetMesh(deco);
  };

  const pickPointerModelSurface = (event?: PointerEvent | MouseEvent | null, preferredTarget?: Mesh | null) => {
    if (!scene || !camera) return null;
    const ray = getPointerPickingRay(event);
    if (!ray) return null;
    const candidates = modelMeshes.filter((mesh) => !!mesh && !mesh.isDisposed());
    if (!candidates.length) return null;
    const preferredId = String(preferredTarget?.id ?? preferredTarget?.name ?? '').trim();
    const predicate = (mesh: any) => {
      if (!mesh) return false;
      if (preferredId) return mesh === preferredTarget || mesh.id === preferredId || mesh.name === preferredId;
      return candidates.some((candidate) => candidate === mesh || candidate.id === mesh.id || candidate.name === mesh.name);
    };
    try {
      const pick = scene.pickWithRay(ray, predicate, false);
      if (!pick?.hit || !pick.pickedMesh || !pick.pickedPoint) return null;
      if (isPreferredTargetOccludedOnRay(ray, preferredTarget, pick, candidates)) return null;
      const targetMesh = pick.pickedMesh as Mesh;
      const point = pick.pickedPoint as Vector3;
      const geometricFrame = getPickGeometricWorldFrame(pick, targetMesh, point);
      const bestPickNormal = geometricFrame ? { normal: geometricFrame.normal, source: 'face-frame' } : getBestPickWorldNormal(pick, targetMesh, point);
      let normal = bestPickNormal.normal;
      if (!normal || normal.lengthSquared() < 0.0001) normal = camera.position.subtract(point);
      if (normal.lengthSquared() < 0.0001) normal = new Vector3(0, 0, 1);
      normal.normalize();
      const toCamera = camera.position.subtract(point);
      if (toCamera.lengthSquared() > 0.0001 && Vector3.Dot(normal, toCamera) < 0) normal = normal.scale(-1);
      return {
        targetMesh,
        position: point.add(normal.scale(0.006)),
        rawPosition: point.clone(),
        normal,
        faceId: Number.isFinite(Number((pick as any)?.faceId)) ? Number((pick as any).faceId) : null,
        ...(geometricFrame?.right ? { surfaceRight: geometricFrame.right } : null),
        ...(geometricFrame?.up ? { surfaceUp: geometricFrame.up } : null),
      };
    } catch {
      return null;
    }
  };

  const projectPointToModelSurface = (
    desiredPosition: Vector3,
    normalInput: Vector3,
    preferredTarget?: Mesh | null,
  ): SurfacePickResult | null => {
    if (!scene) return null;
    const candidates = modelMeshes.filter((mesh) => !!mesh && !mesh.isDisposed());
    if (!candidates.length) return null;
    let normal = normalInput.clone();
    if (normal.lengthSquared() < 0.0001) normal = new Vector3(0, 0, 1);
    normal.normalize();
    const targetId = String(preferredTarget?.id ?? preferredTarget?.name ?? '').trim();
    const predicate = (mesh: any) => {
      if (!mesh) return false;
      if (targetId) return mesh === preferredTarget || mesh.id === targetId || mesh.name === targetId;
      return candidates.some((candidate) => candidate === mesh || candidate.id === mesh.id || candidate.name === mesh.name);
    };
    const targetBounds = preferredTarget
      ? getMeshWorldBounds(preferredTarget)
      : candidates.reduce((acc, mesh) => {
          const bounds = getMeshWorldBounds(mesh);
          return acc ? { size: new Vector3(
            Math.max(acc.size.x, bounds.size.x),
            Math.max(acc.size.y, bounds.size.y),
            Math.max(acc.size.z, bounds.size.z),
          ) } : { size: bounds.size.clone() };
        }, null as { size: Vector3 } | null);
    const searchDistance = Math.max(0.12, Math.min(3, Number(targetBounds?.size?.length?.() ?? 0) * 0.35 || 0.35));
    const origins = [
      desiredPosition.add(normal.scale(searchDistance)),
      desiredPosition.subtract(normal.scale(searchDistance)),
      desiredPosition.add(normal.scale(searchDistance * 0.35)),
    ];
    const directions = [
      normal.scale(-1),
      normal,
      normal.scale(-1),
    ];
    let best: { pick: any; targetMesh: Mesh; point: Vector3; normal: Vector3; score: number; surfaceRight?: Vector3 | null; surfaceUp?: Vector3 | null } | null = null;
    for (let index = 0; index < origins.length; index += 1) {
      try {
        const direction = directions[index].clone();
        if (direction.lengthSquared() < 0.0001) continue;
        direction.normalize();
        const ray = new Ray(origins[index], direction, searchDistance * 2.25);
        const pick = scene.pickWithRay(ray, predicate, false);
        if (!pick?.hit || !pick.pickedMesh || !pick.pickedPoint) continue;
        const targetMesh = pick.pickedMesh as Mesh;
        const point = pick.pickedPoint as Vector3;
        const geometricFrame = getPickGeometricWorldFrame(pick, targetMesh, point);
        const bestPickNormal = geometricFrame ? { normal: geometricFrame.normal } : getBestPickWorldNormal(pick, targetMesh, point);
        let pickedNormal = bestPickNormal.normal?.clone?.() ?? normal.clone();
        if (pickedNormal.lengthSquared() < 0.0001) pickedNormal = normal.clone();
        pickedNormal.normalize();
        if (Vector3.Dot(pickedNormal, normal) < 0) pickedNormal = pickedNormal.scale(-1);
        const score = point.subtract(desiredPosition).lengthSquared();
        if (!best || score < best.score) {
          best = {
            pick,
            targetMesh,
            point,
            normal: pickedNormal,
            score,
            surfaceRight: geometricFrame?.right ?? null,
            surfaceUp: geometricFrame?.up ?? null,
          };
        }
      } catch {}
    }
    if (!best) return null;
    return {
      targetMesh: best.targetMesh,
      position: best.point.add(best.normal.scale(0.006)),
      rawPosition: best.point.clone(),
      normal: best.normal,
      faceId: Number.isFinite(Number((best.pick as any)?.faceId)) ? Number((best.pick as any).faceId) : null,
      ...(best.surfaceRight ? { surfaceRight: best.surfaceRight } : null),
      ...(best.surfaceUp ? { surfaceUp: best.surfaceUp } : null),
    };
  };

  const getDecorationCurrentSurface = (mesh: Mesh, deco?: DecorationRecord | null, area?: Asuka3DPrintArea | null): SurfacePickResult => {
    const basis = getPrintAreaSurfaceBasis(area);
    const targetMesh = getDecorationDragPreferredSurfaceMesh(deco)
      ?? getDecorationSurfaceTargetMesh(deco)
      ?? findModelMesh(String(area?.targetMeshId ?? '').trim() || 'product')
      ?? mesh;
    const rawPosition = (mesh as any).__asukaDecalPosition ?? (deco as any)?.metadata?.decalPosition ?? (deco as any)?.metadata?.surfacePosition ?? (deco as any)?.placement?.position;
    const rawNormal = (mesh as any).__asukaSurfaceNormal ?? (deco as any)?.metadata?.surfaceNormal;
    const rawRight = (mesh as any).__asukaSurfaceRight ?? (deco as any)?.metadata?.surfaceRight ?? (area as any)?.metadata?.surfaceRight;
    const rawUp = (mesh as any).__asukaSurfaceUp ?? (deco as any)?.metadata?.surfaceUp ?? (area as any)?.metadata?.surfaceUp;
    let position = Array.isArray(rawPosition) ? toVec3(rawPosition, fromVec3(mesh.position)) : mesh.position.clone();
    let normal = Array.isArray(rawNormal) ? toVec3(rawNormal, fromVec3(basis.normal)) : basis.normal.clone();
    if (normal.lengthSquared() < 0.0001) normal = basis.normal.clone();
    if (normal.lengthSquared() < 0.0001) normal = new Vector3(0, 0, 1);
    normal.normalize();
    const projected = projectPointToModelSurface(position, normal, targetMesh);
    if (projected) {
      position = projected.position;
      normal = projected.normal;
    }
    return {
      targetMesh: projected?.targetMesh ?? targetMesh,
      position,
      normal,
      rawPosition: projected?.rawPosition ?? position,
      faceId: projected?.faceId ?? null,
      surfaceRight: projected?.surfaceRight ?? (Array.isArray(rawRight) ? toVec3(rawRight, fromVec3(basis.right)) : basis.right),
      surfaceUp: projected?.surfaceUp ?? (Array.isArray(rawUp) ? toVec3(rawUp, fromVec3(basis.up)) : basis.up),
    };
  };

  const getSavedDecalProjection = (deco: DecorationRecord, area?: Asuka3DPrintArea | null) => {
    if (String(deco?.placement?.mode ?? '') !== 'surface') return null;
    const metadataPosition = (deco as any)?.metadata?.decalPosition ?? (deco as any)?.metadata?.surfacePosition;
    const placementPosition = (deco as any)?.placement?.position;
    const rawPosition = Array.isArray(metadataPosition) ? metadataPosition : placementPosition;
    if (!Array.isArray(rawPosition)) return null;
    const metadataNormal = (deco as any)?.metadata?.surfaceNormal;
    if (!Array.isArray(metadataNormal)) return null;
    const faceNetTargetMeshId = getExactFaceNetAreaTargetMeshId(area);
    const targetMesh = (faceNetTargetMeshId ? findModelMesh(faceNetTargetMeshId) : null) ?? getDecorationSurfaceTargetMesh(deco);
    if (!targetMesh) return null;
    let normal = toVec3(metadataNormal, [0, 0, 1]);
    if (normal.lengthSquared() < 0.0001) return null;
    normal.normalize();
    const fromFixedSlot3D = (deco as any)?.metadata?.fixedSlot3D === true || !!(deco as any)?.metadata?.slotId3D;
    const metadataRight = (deco as any)?.metadata?.surfaceRight;
    const metadataUp = (deco as any)?.metadata?.surfaceUp;
    const faceId = Number((deco as any)?.metadata?.faceId);
    return {
      targetMesh,
      position: toVec3(rawPosition, fromVec3(targetMesh.position)),
      normal,
      size: getDecorationDecalSize(null, deco, area ?? getDecorationPrintArea(deco)),
      angle: Number((deco as any)?.metadata?.decalAngle ?? (deco as any)?.placement?.rotation?.[2] ?? 0) || 0,
      ...(Array.isArray(metadataRight) ? { surfaceRight: toVec3(metadataRight, [1, 0, 0]) } : null),
      ...(Array.isArray(metadataUp) ? { surfaceUp: toVec3(metadataUp, [0, 1, 0]) } : null),
      ...(Number.isFinite(faceId) ? { faceId } : null),
      ...(fromFixedSlot3D ? { __asukaFromFixedSlot3D: true } : null),
      __asukaPreserveProjectionSize: true,
    };
  };

  const getDecalRuntimeMetadata = (mesh?: Mesh | null, deco?: DecorationRecord | null): Record<string, unknown> => {
    const metadata: Record<string, unknown> = {};
    const decalSize = (mesh as any)?.__asukaDecalSize ?? (deco as any)?.metadata?.decalSize;
    const decalPosition = (mesh as any)?.__asukaDecalPosition ?? (deco as any)?.metadata?.decalPosition ?? (deco as any)?.metadata?.surfacePosition;
    const surfaceNormal = (mesh as any)?.__asukaSurfaceNormal ?? (deco as any)?.metadata?.surfaceNormal;
    const surfaceRight = (mesh as any)?.__asukaSurfaceRight ?? (deco as any)?.metadata?.surfaceRight;
    const surfaceUp = (mesh as any)?.__asukaSurfaceUp ?? (deco as any)?.metadata?.surfaceUp;
    const targetMeshId = (mesh as any)?.__asukaTargetMeshId ?? (deco as any)?.targetMeshId ?? (deco as any)?.metadata?.targetMeshId;
    const decalAngle = (mesh as any)?.__asukaDecalAngle ?? (deco as any)?.metadata?.decalAngle;
    const fixedSlot3D = (mesh as any)?.__asukaFromFixedSlot3D === true
      || (deco as any)?.metadata?.fixedSlot3D === true
      || !!(deco as any)?.metadata?.slotId3D;
    if (Array.isArray(decalSize)) metadata.decalSize = decalSize;
    if (Array.isArray(decalPosition)) metadata.decalPosition = decalPosition;
    if (Array.isArray(surfaceNormal)) metadata.surfaceNormal = surfaceNormal;
    if (Array.isArray(surfaceRight)) metadata.surfaceRight = surfaceRight;
    if (Array.isArray(surfaceUp)) metadata.surfaceUp = surfaceUp;
    if (targetMeshId) metadata.targetMeshId = String(targetMeshId);
    if (typeof decalAngle === 'number' && Number.isFinite(decalAngle)) metadata.decalAngle = decalAngle;
    if (fixedSlot3D) metadata.fixedSlot3D = true;
    return metadata;
  };

  const resolveDecorationDragSurface = (
    id: string,
    surface: SurfacePickResult,
    size: Vector3,
    decoInput?: DecorationRecord | null,
    runtimeMeshInput?: Mesh | null,
  ): { surface: SurfacePickResult; area: Asuka3DPrintArea | null; areaTargetMesh: Mesh | null } | null => {
    const deco = decoInput ?? findDecorationRecord(id);
    const runtimeMesh = runtimeMeshInput ?? objectMeshes.get(id) ?? null;
    const area = getDecorationPrintArea(deco);
    const areaTargetMesh = getExplicitPrintAreaTargetMesh(area);
    const targetBoundRuntime = !!runtimeMesh && (
      (runtimeMesh as any).__asukaBroadMeshTargetProjector === true
      || (decalFragmentsByObjectId.get(id) ?? []).some((fragment) => (fragment as any)?.__asukaTargetBoundProjectedSticker === true)
    );

    // A mesh-scoped projector is already constrained by two exact operations:
    // pointer picking only accepts the selected mesh, and its shader is rendered
    // only on a clone of that mesh. Projecting the hit through the Drawing Area's
    // planar bounds changes its position on curved GLBs and causes the live layer
    // to jump again when the pointer is released.
    let constrainedSurface: SurfacePickResult = surface;
    const hasStrictDrawnBounds = String((area as any)?.metadata?.createdFrom ?? '').trim() === 'surface-draw';
    if (!(areaTargetMesh && targetBoundRuntime && !hasStrictDrawnBounds && TARGET_BOUND_STICKER_RENDER_POLICY.dragConstraint === 'target-mesh')) {
      constrainedSurface = area ? clampSurfacePointToPrintArea(surface, area, size) : surface;
      if (areaTargetMesh && !getDeclaredBoxPrintSurfaceMetadata({ __asukaPrintSurfaceArea: area } as any)) {
        const reprojection = projectPointToModelSurface(constrainedSurface.position, constrainedSurface.normal, areaTargetMesh);
        if (reprojection) {
          constrainedSurface = {
            ...constrainedSurface,
            ...reprojection,
            targetMesh: areaTargetMesh,
            surfaceRight: reprojection.surfaceRight ?? constrainedSurface.surfaceRight,
            surfaceUp: reprojection.surfaceUp ?? constrainedSurface.surfaceUp,
          };
        }
      }
    }

    if (areaTargetMesh && !isSameRuntimeMesh(constrainedSurface.targetMesh, areaTargetMesh)) {
      debug3DLayer('decal-drag:blocked-outside-print-area-mesh', {
        id,
        areaId: String((area as any)?.id ?? ''),
        expectedMesh: summarizeLayerMeshForDebug(areaTargetMesh),
        pickedMesh: summarizeLayerMeshForDebug(constrainedSurface.targetMesh),
      }, 'warn');
      return null;
    }
    if (isAutomaticProductSurfaceArea(area) || (areaTargetMesh && targetBoundRuntime)) {
      // Imported organic meshes may use a different edge direction for every
      // triangle. Reusing that raw per-triangle tangent makes the sticker twist
      // or slide visually while crossing triangle boundaries. Preserve the
      // layer's current rotation in the stable surface basis instead; its normal
      // still follows the exact picked face.
      const preservedAngle = Number(
        (runtimeMesh as any)?.__asukaDecalAngle
        ?? (deco as any)?.metadata?.decalAngle
        ?? 0,
      ) || 0;
      const stableFrame = getProjectedStickerStableBasis(constrainedSurface.normal, preservedAngle);
      constrainedSurface = {
        ...constrainedSurface,
        ...(areaTargetMesh ? { targetMesh: areaTargetMesh } : null),
        surfaceRight: stableFrame.right,
        surfaceUp: stableFrame.up,
      };
    }
    return { surface: constrainedSurface, area, areaTargetMesh };
  };

  const recreateDecorationDecalOnSurface = (id: string, surface: { targetMesh: Mesh; position: Vector3; normal: Vector3; faceId?: number | null; rawPosition?: Vector3 | null; surfaceRight?: Vector3 | null; surfaceUp?: Vector3 | null }): Mesh | null => {
    const key = String(id ?? '').trim();
    const previous = key ? objectMeshes.get(key) : null;
    if (!key || !previous) return null;
    const deco = findDecorationRecord(key);
    const kind = objectKinds.get(key) ?? String(deco?.type ?? 'object');
    const material = (previous as any).__asukaSourceMaterial ?? previous.material;
    const size = getSafeDecalProjectionSize(getDecorationDecalSize(previous, deco));
    const resolvedDragSurface = resolveDecorationDragSurface(key, surface as SurfacePickResult, size, deco, previous);
    if (!resolvedDragSurface) return null;
    const { surface: constrainedSurface, area, areaTargetMesh } = resolvedDragSurface;
    const angle = Number((previous as any).__asukaDecalAngle ?? (deco as any)?.metadata?.decalAngle ?? 0) || 0;

    // Resolve a face-net triangle index from the actual hit point in local mesh
    // space. Babylon's raw faceId uses its own internal numbering that does NOT
    // match FaceNetTriangleRef.triangleIndex, so triangleByIndex.get(babylonFaceId)
    // always misses → the rendering loop used activeFaceTriangles (primary face
    // only) → image frozen on one face. We replace it with a proper local-space
    // raycast against selectedTriangles so the resolved index is always valid
    // inside runtime.triangles, enabling smooth cross-face drag.
    let resolvedFaceId: number | null = Number.isFinite(Number((constrainedSurface as any).faceId))
      ? Number((constrainedSurface as any).faceId)
      : null;
    if (isExactFaceNetPrintArea(area)) {
      const rawHitWorld = (constrainedSurface as any).rawPosition ?? constrainedSurface.position;
      const declared = getDeclaredExactFaceNetPrintSurfaceMetadata({ __asukaPrintSurfaceArea: area } as any);
      if (declared && declared.selectedTriangles.length && rawHitWorld) {
        const faceNetResolved = resolveActiveFaceNetTriangle(rawHitWorld, constrainedSurface.targetMesh, declared.selectedTriangles);
        if (faceNetResolved !== null) {
          debug3DLayer('face-net:drag-face-resolved', {
            id: key,
            kind,
            babylonFaceId: resolvedFaceId,
            resolvedFaceId: faceNetResolved,
            rawPosition: fromVec3(rawHitWorld),
          });
          resolvedFaceId = faceNetResolved;
        }
      }
    }

    try {
      const previousMaterial = (previous as any)?.material ?? null;
      // ── Build new mesh FIRST, dispose old only after success ─────────────
      // Disposing before creating means a failed creation leaves no sticker
      // visible at all. Keep the old mesh alive until the new one is ready.
      const dragProjection: any = {
        targetMesh: areaTargetMesh ?? constrainedSurface.targetMesh,
        position: getSafeDecalProjectionPosition(constrainedSurface.position, constrainedSurface.normal, size),
        normal: constrainedSurface.normal,
        size,
        faceId: resolvedFaceId,
        rawPosition: (constrainedSurface as any).rawPosition ?? constrainedSurface.position,
        surfaceRight: (constrainedSurface as any).surfaceRight ?? null,
        surfaceUp: (constrainedSurface as any).surfaceUp ?? null,
        __asukaPreserveExistingFragments: true,
      };
      if ((previous as any).__asukaFromFixedSlot3D === true || (deco as any)?.metadata?.fixedSlot3D === true || !!(deco as any)?.metadata?.slotId3D) {
        dragProjection.__asukaFromFixedSlot3D = true;
      }
      try { dragProjection.__asukaPrintSurfaceArea = area ? clonePlain(area) : null; } catch { dragProjection.__asukaPrintSurfaceArea = area ?? null; }
      debug3DLayer('face-net:drag-projection', {
        id: key,
        kind,
        isFaceNetArea: isExactFaceNetPrintArea(area),
        targetMesh: summarizeLayerMeshForDebug(dragProjection.targetMesh),
        position: fromVec3(dragProjection.position),
        rawPosition: fromVec3(dragProjection.rawPosition),
        normal: fromVec3(dragProjection.normal),
        faceId: dragProjection.faceId,
        area: isExactFaceNetPrintArea(area) ? clonePlain(area) : undefined,
      });
      const previousFragments = [...(decalFragmentsByObjectId.get(key) ?? [])];
      const previousVertexCount = getDecalRenderableVertexCount(previous, previousFragments);
      const next = createProjectedDecalFragments(key, kind, previous.name || key, material, dragProjection, angle);
      if (!next) return null;  // old mesh stays visible — do NOT dispose
      const nextFragments = [...(decalFragmentsByObjectId.get(key) ?? [])];
      const nextVertexCount = getDecalRenderableVertexCount(next, nextFragments);
      if (areaTargetMesh && previousVertexCount >= 12 && (nextVertexCount < 12 || nextVertexCount < previousVertexCount * 0.18)) {
        debug3DLayer('decal-drag:rebuild-rejected-empty', {
          id: key,
          areaId: String((area as any)?.id ?? ''),
          targetMesh: summarizeLayerMeshForDebug(areaTargetMesh),
          previousVertexCount,
          nextVertexCount,
        }, 'warn');
        const previousFragmentSet = new Set(previousFragments);
        const newFragments = nextFragments.filter((fragment) => !!fragment && !previousFragmentSet.has(fragment as Mesh) && fragment !== next);
        decalFragmentsByObjectId.set(key, previousFragments);
        objectMeshes.set(key, previous);
        objectKinds.set(key, kind);
        disposeSpecificDecalFragments(newFragments, true);
        if (next !== previous) {
          try { (next as any).material = null; } catch {}
          try { if (!next.isDisposed?.()) next.dispose(false, false); } catch {}
        }
        return null;
      }
      // New mesh ready: now safe to tear down the previous one
      try { previous.material = null; } catch {}
      if (isRuntimeProjectedStickerMaterial(previousMaterial)) disposeRuntimeProjectedStickerMaterial(previousMaterial);
      disposeSpecificDecalFragments(previousFragments, true);
      try { if (!(previous as any).isDisposed?.()) previous.dispose(false, false); } catch {}
      return next;
    } catch {
      return null;
    }
  };

  const pendingProjectedDecorationRefreshes = new Map<string, Set<string>>();
  let projectedDecorationRefreshScheduled = false;
  const flushProjectedDecorationRefreshes = () => {
    projectedDecorationRefreshScheduled = false;
    const pending = Array.from(pendingProjectedDecorationRefreshes.entries());
    pendingProjectedDecorationRefreshes.clear();
    for (const [ownerId, reasons] of pending) {
      if (dragState?.id === ownerId) {
        try {
          setTimeout(() => scheduleProjectedDecorationRefresh?.(ownerId, 'deferred-until-drag-end'), 48);
        } catch {}
        continue;
      }
      const mesh = objectMeshes.get(ownerId);
      const deco = findDecorationRecord(ownerId);
      const fragments = decalFragmentsByObjectId.get(ownerId) ?? [];
      if (!mesh || !deco || !fragments.some((fragment) => (fragment as any)?.__asukaProjectedStickerOverlay === true)) continue;
      try {
        const area = getDecorationPrintArea(deco);
        const surface = getDecorationCurrentSurface(mesh, deco, area);
        try { surface.targetMesh?.computeWorldMatrix?.(true); } catch {}
        const refreshed = recreateDecorationDecalOnSurface(ownerId, surface);
        debug3DLayer('projected-sticker:stabilized', {
          ownerId,
          reasons: Array.from(reasons),
          refreshed: !!refreshed,
          targetMesh: summarizeLayerMeshForDebug(surface.targetMesh),
        }, refreshed ? 'info' : 'warn');
      } catch (error) {
        debug3DLayer('projected-sticker:stabilize-error', {
          ownerId,
          reasons: Array.from(reasons),
          error,
        }, 'error');
      }
    }
  };
  const queueProjectedDecorationRefreshFlush = () => {
    if (projectedDecorationRefreshScheduled) return;
    projectedDecorationRefreshScheduled = true;
    const runAfterBrowserFrame = () => {
      try {
        if (typeof requestAnimationFrame === 'function') {
          requestAnimationFrame(() => flushProjectedDecorationRefreshes());
          return;
        }
      } catch {}
      setTimeout(flushProjectedDecorationRefreshes, 16);
    };
    try { setTimeout(runAfterBrowserFrame, 0); } catch { runAfterBrowserFrame(); }
  };
  scheduleProjectedDecorationRefresh = (ownerIdInput: string, reasonInput: string) => {
    const ownerId = String(ownerIdInput ?? '').trim();
    if (!ownerId || !objectMeshes.has(ownerId)) return;
    const reasons = pendingProjectedDecorationRefreshes.get(ownerId) ?? new Set<string>();
    reasons.add(String(reasonInput ?? '').trim() || 'runtime-refresh');
    pendingProjectedDecorationRefreshes.set(ownerId, reasons);
    queueProjectedDecorationRefreshFlush();
  };
  scheduleProjectedDecorationRefreshForTargets = (targetsInput: Mesh[], reason: string) => {
    const targets = (Array.isArray(targetsInput) ? targetsInput : []).filter((mesh): mesh is Mesh => !!mesh && !mesh.isDisposed?.());
    if (!targets.length) return;
    const targetAliases = new Set<string>();
    for (const target of targets) {
      targetAliases.add(String(target.id ?? '').trim());
      targetAliases.add(String(target.name ?? '').trim());
    }
    targetAliases.delete('');
    for (const [ownerId, fragments] of decalFragmentsByObjectId.entries()) {
      const matchesChangedTarget = fragments.some((fragment) => (
        (fragment as any)?.__asukaProjectedStickerOverlay === true
        && targetAliases.has(String((fragment as any)?.__asukaTargetMeshId ?? '').trim())
      ));
      if (matchesChangedTarget) scheduleProjectedDecorationRefresh?.(ownerId, reason);
    }
  };

  const replaceDecorationVisualSource = (idInput: string, sourceInput: Record<string, unknown>): boolean => {
    const id = String(idInput ?? '').trim();
    const mesh = id ? objectMeshes.get(id) : null;
    const deco = id ? findDecorationRecord(id) : null;
    if (!id || !mesh || !deco) return false;
    const source = { ...((deco as any)?.source ?? {}), ...(sourceInput ?? {}) } as Record<string, any>;
    const kind = String(objectKinds.get(id) ?? deco.type ?? '').toLowerCase();
    let nextMaterial: StandardMaterial | null = null;
    try {
      if (kind === 'text') {
        const created = createTextTexture(id, {
          text: String(source.text ?? 'Text'),
          fontFamily: String(source.fontFamily ?? 'Arial'),
          fontSize: Number(source.fontSize ?? 72) || 72,
          color: String(source.color ?? '#111827'),
          fontStyle: source.fontStyle,
          fontWeight: source.fontWeight,
          underline: source.underline === true,
        } as any);
        nextMaterial = createDecorationMaterial(id, created.texture);
      } else {
        const url = String(source.url ?? source.src ?? '').trim();
        if (!url) return false;
        const texture = new Texture(url, requireScene(), false, true, Texture.TRILINEAR_SAMPLINGMODE);
        texture.hasAlpha = true;
        nextMaterial = createDecorationMaterial(id, texture);
      }
    } catch {
      return false;
    }

    const previousMaterial = mesh.material as any;
    const previousSourceMaterial = (mesh as any).__asukaSourceMaterial;
    const fragments = decalFragmentsByObjectId.get(id) ?? [];
    const disposeSourceMaterial = (material: any) => {
      if (!material || material === nextMaterial || isRuntimeProjectedStickerMaterial(material)) return;
      try { material.dispose?.(true, true); } catch {}
    };
    const isDecalPlacement = projectionModeFromMesh(mesh, deco.placement?.mode ?? 'plane') === 'decal';
    if (!isDecalPlacement) {
      mesh.material = nextMaterial;
      (mesh as any).__asukaSourceMaterial = nextMaterial;
      disposeSourceMaterial(previousMaterial);
      if (previousSourceMaterial !== previousMaterial) disposeSourceMaterial(previousSourceMaterial);
      return true;
    }

    const projectedShader = (mesh as any).__asukaProjectedStickerShader === true
      || fragments.some((fragment) => (fragment as any)?.__asukaProjectedStickerOverlay === true);
    if (projectedShader) {
      const sourceTexture = (nextMaterial as any).diffuseTexture ?? null;
      if (!sourceTexture) {
        try { nextMaterial.dispose?.(true, true); } catch {}
        return false;
      }
      let updated = false;
      for (const fragment of fragments) {
        const material = fragment?.material as any;
        if ((fragment as any)?.__asukaProjectedStickerOverlay !== true || typeof material?.setTexture !== 'function') continue;
        try {
          material.setTexture('uSampler', sourceTexture);
          updated = true;
        } catch {}
      }
      if (!updated) {
        try { nextMaterial.dispose?.(true, true); } catch {}
        return false;
      }
      (mesh as any).__asukaSourceMaterial = nextMaterial;
      disposeSourceMaterial(previousSourceMaterial);
      return true;
    }

    const replacedMaterials = new Set<any>([previousMaterial, previousSourceMaterial]);
    mesh.material = nextMaterial;
    (mesh as any).__asukaSourceMaterial = nextMaterial;
    for (const fragment of fragments) {
      replacedMaterials.add(fragment?.material);
      if (fragment && !fragment.isDisposed?.()) fragment.material = nextMaterial;
    }
    for (const material of replacedMaterials) disposeSourceMaterial(material);
    return true;
  };

  const getPrintAreaIdFromPickedMesh = (mesh: unknown): string | null => {
    const direct = String((mesh as any)?.__asukaPrintAreaId ?? '').trim();
    if (direct) return direct;
    const rawId = String((mesh as any)?.id ?? '').trim();
    if (rawId.startsWith('print-area:')) return rawId.slice('print-area:'.length);
    return null;
  };

  const getSurfaceTangentBasis = (normalInput: Vector3): { right: Vector3; up: Vector3 } => {
    let normal = normalInput.clone();
    if (normal.lengthSquared() < 0.0001) normal = new Vector3(0, 0, 1);
    normal.normalize();
    const viewBasis = getViewTransformBasis();
    let right = viewBasis.right.subtract(normal.scale(Vector3.Dot(viewBasis.right, normal)));
    if (right.lengthSquared() < 0.0001) right = Vector3.Cross(new Vector3(0, 1, 0), normal);
    if (right.lengthSquared() < 0.0001) right = new Vector3(1, 0, 0);
    right.normalize();
    let up = viewBasis.up.subtract(normal.scale(Vector3.Dot(viewBasis.up, normal)));
    if (up.lengthSquared() < 0.0001) up = Vector3.Cross(normal, right);
    if (up.lengthSquared() < 0.0001) up = new Vector3(0, 1, 0);
    up.normalize();
    return { right, up };
  };

  const getPrintAreaSurfaceBasis = (area?: Asuka3DPrintArea | null): { right: Vector3; up: Vector3; normal: Vector3 } => {
    const declaredBoxBounds = getDeclaredBoxPrintSurfaceNetBounds(area);
    if (declaredBoxBounds) return declaredBoxBounds.basis;
    let normal = getPrintAreaNormal(area) ?? getPrintAreaBounds(area).direction;
    if (normal.lengthSquared() < 0.0001) normal = new Vector3(0, 0, 1);
    normal.normalize();
    const rawRight = (area as any)?.metadata?.surfaceRight;
    const rawUp = (area as any)?.metadata?.surfaceUp;
    let right = Array.isArray(rawRight) ? toVec3(rawRight, [1, 0, 0]) : getSurfaceTangentBasis(normal).right;
    right = right.subtract(normal.scale(Vector3.Dot(right, normal)));
    if (right.lengthSquared() < 0.0001) right = getSurfaceTangentBasis(normal).right;
    right.normalize();
    let up = Array.isArray(rawUp) ? toVec3(rawUp, [0, 1, 0]) : getSurfaceTangentBasis(normal).up;
    up = up.subtract(normal.scale(Vector3.Dot(up, normal))).subtract(right.scale(Vector3.Dot(up, right)));
    if (up.lengthSquared() < 0.0001) up = Vector3.Cross(normal, right);
    if (up.lengthSquared() < 0.0001) up = getSurfaceTangentBasis(normal).up;
    up.normalize();
    return { right, up, normal };
  };

  const clampSurfacePointToPrintArea = (surface: SurfacePickResult, area?: Asuka3DPrintArea | null, decalSize?: Vector3 | null): SurfacePickResult => {
    if (!area || isAutomaticProductSurfaceArea(area)) return surface;
    const declaredBox = getDeclaredBoxPrintSurfaceMetadata({ __asukaPrintSurfaceArea: area } as any);
    if (declaredBox) {
      // Declared box unwraps are constrained by their own 2D net in the renderer.
      // Do not project the picked surface point back onto the primary face plane,
      // otherwise selected side/back faces become unreachable during drag.
      return surface;
    }
    const bounds = getPrintAreaBounds(area);
    const basis = getPrintAreaSurfaceBasis(area);
    const delta = surface.position.subtract(bounds.center);
    const halfW = Math.max(0.001, bounds.size.x / 2);
    const halfH = Math.max(0.001, bounds.size.y / 2);
    const safeSize = decalSize ? getSafeDecalProjectionSize(decalSize) : null;
    const maxX = Math.max(0, halfW - (safeSize ? safeSize.x / 2 : 0));
    const maxY = Math.max(0, halfH - (safeSize ? safeSize.y / 2 : 0));
    const x = Math.max(-maxX, Math.min(maxX, Vector3.Dot(delta, basis.right)));
    const y = Math.max(-maxY, Math.min(maxY, Vector3.Dot(delta, basis.up)));
    const position = bounds.center.add(basis.right.scale(x)).add(basis.up.scale(y));
    return {
      targetMesh: surface.targetMesh,
      position,
      normal: basis.normal.lengthSquared() > 0.0001 ? basis.normal : surface.normal,
      faceId: surface.faceId ?? null,
      rawPosition: surface.rawPosition ?? surface.position,
      surfaceRight: surface.surfaceRight ?? basis.right,
      surfaceUp: surface.surfaceUp ?? basis.up,
    };
  };

  const make3DBoundsBox = (input: {
    x?: number;
    y?: number;
    left?: number;
    top?: number;
    width?: number;
    height?: number;
    z?: number;
    depth?: number;
    space: AsukaCoordinateFrame['space'];
    unit: AsukaCoordinateFrame['unit'];
    objectId?: string | null;
    printAreaId?: string | null;
    metadata?: Record<string, unknown>;
  }): AsukaBoundsBox => {
    const left = Number(input.left ?? input.x ?? 0) || 0;
    const top = Number(input.top ?? input.y ?? 0) || 0;
    const width = Math.max(0, Number(input.width ?? 0) || 0);
    const height = Math.max(0, Number(input.height ?? 0) || 0);
    return {
      x: left,
      y: top,
      left,
      top,
      width,
      height,
      right: left + width,
      bottom: top + height,
      centerX: left + width / 2,
      centerY: top + height / 2,
      space: input.space,
      unit: input.unit,
      ...(typeof input.z === 'number' ? { z: input.z } : null),
      ...(typeof input.depth === 'number' ? { depth: Math.max(0, input.depth) } : null),
      engine: 'babylon',
      ...(input.objectId != null ? { objectId: input.objectId } : null),
      ...(input.printAreaId != null ? { printAreaId: input.printAreaId } : null),
      ...(input.metadata ? { metadata: input.metadata } : null),
    };
  };

  const make3DConstraintStatus = (input: {
    objectId: string;
    printAreaId?: string | null;
    objectBounds?: AsukaBoundsBox | null;
    printAreaBounds?: AsukaBoundsBox | null;
    metadata?: Record<string, unknown>;
  }): AsukaPrintAreaConstraintStatus => {
    const objectBounds = input.objectBounds ?? null;
    const printAreaBounds = input.printAreaBounds ?? null;
    const inside = !!objectBounds && !!printAreaBounds
      && objectBounds.left >= printAreaBounds.left - 0.001
      && objectBounds.top >= printAreaBounds.top - 0.001
      && objectBounds.right <= printAreaBounds.right + 0.001
      && objectBounds.bottom <= printAreaBounds.bottom + 0.001;
    const intersects = !!objectBounds && !!printAreaBounds
      && objectBounds.right >= printAreaBounds.left
      && objectBounds.left <= printAreaBounds.right
      && objectBounds.bottom >= printAreaBounds.top
      && objectBounds.top <= printAreaBounds.bottom;
    return {
      objectId: String(input.objectId ?? ''),
      printAreaId: String(input.printAreaId ?? '') || null,
      inside,
      intersects,
      objectBounds,
      printAreaBounds,
      engine: 'babylon',
      ...(input.metadata ? { metadata: input.metadata } : null),
    };
  };

  const get3DPrintAreaBoundsBox = (printAreaIdInput: string | null | undefined): AsukaBoundsBox | null => {
    const area = getPrintAreaById(printAreaIdInput);
    if (!area) return null;
    const areaId = String(area.id ?? printAreaIdInput ?? '').trim();
    if (isTextureSlotPrintArea(area)) {
      const rect = getTexturePrintAreaContentRect(area);
      return make3DBoundsBox({
        left: rect.x,
        top: rect.y,
        width: rect.width,
        height: rect.height,
        space: 'texture',
        unit: 'texture-px',
        printAreaId: areaId,
        metadata: { mode: 'texture', constrained: rect.constrained, canvasSize: getPrintAreaCanvasSize(area) },
      });
    }
    const bounds = getPrintAreaBounds(area);
    return make3DBoundsBox({
      left: -bounds.size.x / 2,
      top: -bounds.size.y / 2,
      width: bounds.size.x,
      height: bounds.size.y,
      z: 0,
      depth: bounds.size.z,
      space: 'surface-tangent',
      unit: 'world-unit',
      printAreaId: areaId,
      metadata: { ...(getDeclaredBoxPrintSurfaceNetBounds(area)?.metadata ?? {}), mode: area.mode ?? 'surface', center: fromVec3(bounds.center), normal: fromVec3(getPrintAreaSurfaceBasis(area).normal) },
    });
  };

  const get3DObjectBoundsBox = (objectIdInput: string | null | undefined): AsukaBoundsBox | null => {
    const key = String(objectIdInput ?? '').trim();
    if (!key) return null;
    if (key.startsWith('product:')) {
      const instanceId = key.slice('product:'.length).trim();
      const bounds = getProductInstanceBounds(instanceId);
      const offset = getProductInstanceTransformOffset(instanceId);
      return make3DBoundsBox({
        left: Number(bounds?.min?.x ?? offset?.x ?? 0) || 0,
        top: Number(bounds?.min?.y ?? offset?.y ?? 0) || 0,
        z: Number(bounds?.center?.z ?? offset?.z ?? 0) || 0,
        width: Math.max(0, Number(bounds?.size?.x ?? 0) || 0),
        height: Math.max(0, Number(bounds?.size?.y ?? 0) || 0),
        depth: Math.max(0, Number(bounds?.size?.z ?? 0) || 0),
        space: 'world',
        unit: 'world-unit',
        objectId: key,
        metadata: {
          productId: instanceId,
          screenBox: bounds ? getMeshScreenBox(getProductInstanceMeshes(instanceId)[0] ?? null) : undefined,
        },
      });
    }
    const textureHit = findTexturePrintAreaObject(key);
    if (textureHit) {
      const object = normalizeTexturePrintAreaObjectPlacement(textureHit.area, textureHit.object);
      return make3DBoundsBox({
        left: Number(object.x ?? 0) || 0,
        top: Number(object.y ?? 0) || 0,
        width: Math.max(0, Number(object.width ?? 0) || 0),
        height: Math.max(0, Number(object.height ?? 0) || 0),
        space: 'texture',
        unit: 'texture-px',
        objectId: key,
        printAreaId: String(textureHit.area.id ?? '').trim(),
        metadata: { mode: 'texture', rotation: object.rotation, type: object.type },
      });
    }
    const mesh = objectMeshes.get(key);
    if (!mesh) return null;
    const deco = findDecorationRecord(key);
    const area = getDecorationPrintArea(deco);
    const areaId = String((deco as any)?.targetPrintAreaId ?? (deco as any)?.metadata?.printAreaId ?? area?.id ?? '').trim();
    if (area) {
      const bounds = getPrintAreaBounds(area);
      const basis = getPrintAreaSurfaceBasis(area);
      const size = getDecorationDecalSize(mesh, deco, area);
      // Projected sticker control meshes can keep position at the origin while
      // their vertices and shader center live in world space. Constraint status
      // must use the projection center or a perfectly contained sticker is
      // reported outside its drawing area after add/drag.
      const center = getDecalRuntimePosition(mesh) ?? mesh.position;
      const delta = center.subtract(bounds.center);
      const centerX = Vector3.Dot(delta, basis.right);
      const centerY = Vector3.Dot(delta, basis.up);
      const centerZ = Vector3.Dot(delta, basis.normal);
      return make3DBoundsBox({
        left: centerX - size.x / 2,
        top: centerY - size.y / 2,
        width: size.x,
        height: size.y,
        z: centerZ,
        depth: size.z,
        space: 'surface-tangent',
        unit: 'world-unit',
        objectId: key,
        printAreaId: areaId || null,
        metadata: { mode: projectionModeFromMesh(mesh, deco?.placement?.mode ?? 'plane'), center: fromVec3(center), targetMeshId: (mesh as any).__asukaTargetMeshId ?? deco?.targetMeshId },
      });
    }
    const bounds = getMeshWorldBounds(mesh);
    return make3DBoundsBox({
      left: bounds.min.x,
      top: bounds.min.y,
      width: bounds.size.x,
      height: bounds.size.y,
      z: bounds.center.z,
      depth: bounds.size.z,
      space: 'world',
      unit: 'world-unit',
      objectId: key,
      printAreaId: areaId || null,
      metadata: { mode: projectionModeFromMesh(mesh, deco?.placement?.mode ?? 'plane') },
    });
  };

  const resolve3DConstraintPrintArea = (objectId: string, printAreaId?: string | null, options?: AsukaPrintAreaConstraintOptions): Asuka3DPrintArea | null => {
    const textureHit = findTexturePrintAreaObject(objectId);
    const deco = findDecorationRecord(objectId);
    const areaId = String(
      options?.printAreaId
      ?? options?.targetPrintAreaId
      ?? printAreaId
      ?? textureHit?.area?.id
      ?? (deco as any)?.targetPrintAreaId
      ?? (deco as any)?.metadata?.printAreaId
      ?? getActivePrintAreaId()
      ?? '',
    ).trim();
    if (!areaId) return null;
    if (areaId === AUTO_PRODUCT_SURFACE_AREA_ID) return getAutomaticProductSurfacePrintArea({ targetMeshId: deco?.targetMeshId, metadata: deco?.metadata });
    return getPrintAreaById(areaId);
  };

  const get3DConstraintStatus = (objectId: string, printAreaId?: string | null, options?: AsukaPrintAreaConstraintOptions): AsukaPrintAreaConstraintStatus => {
    const key = String(objectId ?? '').trim();
    const area = resolve3DConstraintPrintArea(key, printAreaId, options);
    const areaId = String(area?.id ?? printAreaId ?? '').trim();
    return make3DConstraintStatus({
      objectId: key,
      printAreaId: areaId || null,
      objectBounds: get3DObjectBoundsBox(key),
      printAreaBounds: areaId ? get3DPrintAreaBoundsBox(areaId) : null,
    });
  };

  const constrain3DObjectToPrintArea = (objectId: string, printAreaId?: string | null, options?: AsukaPrintAreaConstraintOptions): AsukaPrintAreaConstraintStatus => {
    const key = String(objectId ?? '').trim();
    const area = resolve3DConstraintPrintArea(key, printAreaId, options);
    const areaId = String(area?.id ?? printAreaId ?? '').trim();
    if (!key || !area) return get3DConstraintStatus(key, areaId, options);
    const shouldCommit = options?.commit === true;
    const status = runWithoutIntermediateHistory(() => {
      const textureHit = findTexturePrintAreaObject(key);
      if (textureHit && isTextureSlotPrintArea(area)) {
        updateTexturePrintAreaObjects(area.id, (objects) => objects.map((object) => String(object?.id ?? '') === key ? normalizeTexturePrintAreaObjectPlacement(area, object) : object));
        return get3DConstraintStatus(key, area.id, options);
      }

      const mesh = objectMeshes.get(key);
      const deco = findDecorationRecord(key);
      if (!mesh || !deco) return get3DConstraintStatus(key, areaId, options);
      const basis = getPrintAreaSurfaceBasis(area);
      const size = getDecorationDecalSize(mesh, deco, area);
      const isDecalPlacement = projectionModeFromMesh(mesh, deco?.placement?.mode ?? 'plane') === 'decal';
      const currentSurface: SurfacePickResult = isDecalPlacement
        ? getDecorationCurrentSurface(mesh, deco, area)
        : {
            targetMesh: getDecorationSurfaceTargetMesh(deco) ?? findModelMesh(String(area.targetMeshId ?? '').trim() || 'product') ?? mesh,
            position: mesh.position.clone(),
            normal: basis.normal,
          };
      const constrained = clampSurfacePointToPrintArea(currentSurface, area, size);
      if (isDecalPlacement) {
        recreateDecorationDecalOnSurface(key, constrained);
      } else {
        mesh.position = constrained.position.add(basis.normal.scale(0.012));
        orientMeshForPrintArea(mesh, area);
      }
      syncDecorationPlacementFromMesh(key);
      return get3DConstraintStatus(key, area.id, options);
    });
    if (shouldCommit && !isRestoringHistory) commitFinal3DHistorySnapshot('constrainObjectToPrintArea', key);
    return status;
  };

  const move3DObjectWithinPrintArea = (objectId: string, delta: Partial<AsukaObjectTransform> & AsukaObjectPatch = {}, options?: AsukaPrintAreaConstraintOptions) => {
    const key = String(objectId ?? '').trim();
    if (!key) return;
    const currentFrame = get3DObjectTransform(key) ?? { space: 'world' as const, unit: 'world-unit' as const };
    assertCompatibleCoordinateFrame(delta, currentFrame, {
      engine: '3d',
      operation: 'areas.moveObject',
      details: { objectId: key },
    });
    delta = { ...(delta ?? {}) };
    delete delta.space;
    delete delta.unit;
    delta = constrainFixedSlotTransformPatch(
      { ...(delta ?? {}) },
      getFixedSlot3DConstraintsForDecoration(findDecorationRecord(key)),
    ) as Partial<AsukaObjectTransform> & AsukaObjectPatch;
    if (!Object.keys(delta).length) return;
    runWithoutIntermediateHistory(() => {
      const area = resolve3DConstraintPrintArea(key, null, options);
      const textureHit = findTexturePrintAreaObject(key);
      if (textureHit && area && isTextureSlotPrintArea(area)) {
        updateTexturePrintAreaObjects(area.id, (objects) => objects.map((object) => {
          if (String(object?.id ?? '') !== key) return object;
          const next: any = { ...object };
          if (typeof (delta as any).deltaX === 'number') next.x = Number(next.x ?? 0) + Number((delta as any).deltaX);
          if (typeof (delta as any).deltaY === 'number') next.y = Number(next.y ?? 0) + Number((delta as any).deltaY);
          if (typeof delta.x === 'number') next.x = delta.x;
          if (typeof delta.y === 'number') next.y = delta.y;
          return normalizeTexturePrintAreaObjectPlacement(area, next);
        }));
        return;
      }
      const mesh = objectMeshes.get(key);
      const deco = findDecorationRecord(key);
      if (mesh && deco && area) {
        const basis = getPrintAreaSurfaceBasis(area);
        const isDecalPlacement = projectionModeFromMesh(mesh, deco?.placement?.mode ?? 'plane') === 'decal';
        let nextSurface: SurfacePickResult = isDecalPlacement
          ? getDecorationCurrentSurface(mesh, deco, area)
          : {
              targetMesh: getDecorationSurfaceTargetMesh(deco) ?? findModelMesh(String(area.targetMeshId ?? '').trim() || 'product') ?? mesh,
              position: mesh.position.clone(),
              normal: basis.normal,
            };
        let moveRight = nextSurface.surfaceRight?.clone?.() ?? basis.right.clone();
        moveRight = moveRight.subtract(nextSurface.normal.scale(Vector3.Dot(moveRight, nextSurface.normal)));
        if (moveRight.lengthSquared() < 0.0001) moveRight = basis.right.clone();
        if (moveRight.lengthSquared() < 0.0001) moveRight = getSurfaceTangentBasis(nextSurface.normal).right;
        moveRight.normalize();
        let moveUp = nextSurface.surfaceUp?.clone?.() ?? basis.up.clone();
        moveUp = moveUp
          .subtract(nextSurface.normal.scale(Vector3.Dot(moveUp, nextSurface.normal)))
          .subtract(moveRight.scale(Vector3.Dot(moveUp, moveRight)));
        if (moveUp.lengthSquared() < 0.0001) moveUp = Vector3.Cross(nextSurface.normal, moveRight);
        if (moveUp.lengthSquared() < 0.0001) moveUp = basis.up.clone();
        moveUp.normalize();
        const dx = Number((delta as any).deltaX ?? 0) || 0;
        const dy = Number((delta as any).deltaY ?? 0) || 0;
        if (dx || dy) nextSurface.position = nextSurface.position.add(moveRight.scale(dx)).add(moveUp.scale(dy));
        if (typeof delta.x === 'number' || typeof delta.y === 'number') {
          const bounds = getPrintAreaBounds(area);
          const localX = typeof delta.x === 'number' ? Number(delta.x) || 0 : Vector3.Dot(mesh.position.subtract(bounds.center), basis.right);
          const localY = typeof delta.y === 'number' ? Number(delta.y) || 0 : Vector3.Dot(mesh.position.subtract(bounds.center), basis.up);
          nextSurface.position = bounds.center.add(basis.right.scale(localX)).add(basis.up.scale(localY));
        }
        const projectedSurface = projectPointToModelSurface(nextSurface.position, nextSurface.normal, nextSurface.targetMesh);
        if (projectedSurface) nextSurface = { ...nextSurface, ...projectedSurface, surfaceRight: projectedSurface.surfaceRight ?? moveRight, surfaceUp: projectedSurface.surfaceUp ?? moveUp };
        const constrained = clampSurfacePointToPrintArea(nextSurface, area, getDecorationDecalSize(mesh, deco, area));
        if (isDecalPlacement) recreateDecorationDecalOnSurface(key, constrained);
        else mesh.position = constrained.position.add(basis.normal.scale(0.012));
        syncDecorationPlacementFromMesh(key);
        return;
      }
      update3DObjectTransform(key, delta);
      constrain3DObjectToPrintArea(key, null, { ...(options ?? {}), commit: false });
    });
    emitChanged('moveObjectWithinPrintArea', key, { commitHistory: options?.commit !== false });
  };

  const transform3DObjectWithinPrintArea = (objectId: string, patch: Partial<AsukaObjectTransform> & AsukaObjectPatch = {}, options?: AsukaPrintAreaConstraintOptions) => {
    const key = String(objectId ?? '').trim();
    if (!key) return;
    runWithoutIntermediateHistory(() => {
      update3DObjectTransform(key, patch);
      constrain3DObjectToPrintArea(key, null, { ...(options ?? {}), commit: false });
    });
    emitChanged('transformObjectWithinPrintArea', key, { commitHistory: options?.commit !== false });
  };

  const getPickablePrintAreasForAdd = (): Asuka3DPrintArea[] => getPrintAreas().filter((area) => {
    const id = String((area as any)?.id ?? '').trim();
    return Boolean(id && id !== AUTO_PRODUCT_SURFACE_AREA_ID && !isTextureSlotPrintArea(area));
  });

  const findPrintAreaForAddAtSurface = (surface: SurfacePickResult | null): Asuka3DPrintArea | null => {
    if (!surface) {
      lastPrintAreaAddPickDiagnostic = { phase: 'pointerdown-miss', reason: 'no-surface' };
      return null;
    }
    const point = surface.rawPosition?.clone?.() ?? surface.position;
    let best: { area: Asuka3DPrintArea; score: number } | null = null;
    const candidates: Array<Record<string, unknown>> = [];
    for (const area of getPickablePrintAreasForAdd()) {
      const target = findModelMesh(String((area as any)?.targetMeshId ?? '').trim() || 'product');
      const pickedTarget = surface.targetMesh;
      const targetMatches = !(target && pickedTarget && target !== pickedTarget && target.id !== pickedTarget.id && target.name !== pickedTarget.name);
      if (!targetMatches) {
        candidates.push({ id: area.id, targetMatches: false, targetMeshId: target?.id ?? null, pickedMeshId: pickedTarget?.id ?? null });
        continue;
      }
      const bounds = getPrintAreaBounds(area);
      const basis = getPrintAreaSurfaceBasis(area);
      const delta = point.subtract(bounds.center);
      const x = Vector3.Dot(delta, basis.right);
      const y = Vector3.Dot(delta, basis.up);
      const inside = Math.abs(x) <= bounds.size.x / 2 + 0.006 && Math.abs(y) <= bounds.size.y / 2 + 0.006;
      candidates.push({ id: area.id, targetMatches: true, x, y, halfWidth: bounds.size.x / 2, halfHeight: bounds.size.y / 2, inside });
      if (!inside) continue;
      const score = Math.max(0.000001, bounds.size.x * bounds.size.y);
      if (!best || score < best.score) best = { area, score };
    }
    lastPrintAreaAddPickDiagnostic = {
      phase: best ? 'pointerdown-hit' : 'pointerdown-miss',
      pickedMeshId: surface.targetMesh?.id ?? null,
      point: fromVec3(point),
      selectedAreaId: best?.area?.id ?? null,
      candidates,
    };
    return best?.area ?? null;
  };

  const finishPrintAreaAddPickInternal = (id: string): void => {
    const state = printAreaAddPickState;
    if (!state) return;
    printAreaAddPickState = null;
    try { state.cleanup(); } catch {}
    restoreCameraAfterObjectDrag();
    selectPrintAreaInternal(id, false);
    renderPrintAreaVisuals();
    try { state.resolve(id); } catch {}
  };

  const cancelPrintAreaAddPickInternal = (): void => {
    const state = printAreaAddPickState;
    if (!state) return;
    printAreaAddPickState = null;
    try { state.cleanup(); } catch {}
    restoreCameraAfterObjectDrag();
    renderPrintAreaVisuals();
    try { state.resolve(null); } catch {}
  };

  const handlePrintAreaAddPickPointer = (type: number, event?: PointerEvent | MouseEvent | null): boolean => {
    if (!printAreaAddPickState) return false;
    try { event?.preventDefault?.(); } catch {}
    try { event?.stopPropagation?.(); } catch {}
    if (type !== PointerEventTypes.POINTERDOWN) return true;
    const area = findPrintAreaForAddAtSurface(pickPointerModelSurface(event ?? null, null));
    const rect = canvas?.getBoundingClientRect?.();
    lastPrintAreaAddPickDiagnostic = {
      ...(lastPrintAreaAddPickDiagnostic ?? {}),
      pointer: {
        clientX: Number((event as any)?.clientX),
        clientY: Number((event as any)?.clientY),
        local: getCanvasPointerCoordinates(event ?? null),
        canvasRect: rect ? { left: rect.left, top: rect.top, width: rect.width, height: rect.height } : null,
        renderWidth: Number(engine?.getRenderWidth?.() ?? 0),
        renderHeight: Number(engine?.getRenderHeight?.() ?? 0),
      },
    };
    if (area?.id) finishPrintAreaAddPickInternal(String(area.id));
    return true;
  };

  const pickPrintAreaForAddInternal = async (): Promise<string | null> => {
    const areas = getPickablePrintAreasForAdd();
    if (!areas.length) return null;
    if (areas.length === 1) {
      const id = String(areas[0]?.id ?? '').trim();
      selectPrintAreaInternal(id, false);
      return id || null;
    }
    if (printAreaAddPickState) cancelPrintAreaAddPickInternal();
    if (printAreaDrawState) cancelPrintAreaDrawingInternal();
    if (printSurfaceFacePickState) cancelPrintSurfaceFacePickingInternal();
    return await new Promise<string | null>((resolve) => {
      const previousCursor = canvas?.style.cursor ?? '';
      const onPointerDown = (event: PointerEvent) => {
        handlePrintAreaAddPickPointer(PointerEventTypes.POINTERDOWN, event);
      };
      const onKeyDown = (event: KeyboardEvent) => {
        if (event.key !== 'Escape') return;
        try { event.preventDefault(); } catch {}
        cancelPrintAreaAddPickInternal();
      };
      const cleanup = () => {
        try { window.removeEventListener('keydown', onKeyDown, { capture: true } as any); } catch {}
        try { canvas?.removeEventListener('pointerdown', onPointerDown, { capture: true } as any); } catch {}
        try { if (canvas) canvas.style.cursor = previousCursor; } catch {}
      };
      printAreaAddPickState = { resolve, previousCursor, cleanup };
      try { window.addEventListener('keydown', onKeyDown, { capture: true }); } catch {}
      // Listen at the DOM boundary as well as through Babylon's pointer observer.
      // Firefox can defer the scene observable while camera inputs are detached,
      // which otherwise leaves the area picker pending after a valid click.
      try { canvas?.addEventListener('pointerdown', onPointerDown, { capture: true }); } catch {}
      try { if (canvas) canvas.style.cursor = 'crosshair'; } catch {}
      detachCameraForObjectDrag();
      setSelectedObject(null, false);
      renderPrintAreaVisuals();
    });
  };

  const resolve3DLayerPrintAreaInput = async <T extends Record<string, any>>(input: T): Promise<T> => {
    const explicitId = String(
      input?.printAreaId
      ?? input?.targetPrintAreaId
      ?? input?.metadata?.printAreaId
      ?? input?.metadata?.targetPrintAreaId
      ?? '',
    ).trim();
    if (explicitId) return input;
    const activeSlotId = String((useAsukaStore as any).getState?.()?.fixedSlotActiveId ?? '').trim();
    if (activeSlotId || !getPickablePrintAreasForAdd().length) return input;
    const pickedId = await pickPrintAreaForAddInternal();
    if (!pickedId) throw new Error('Print area selection cancelled.');
    return {
      ...input,
      printAreaId: pickedId,
      targetPrintAreaId: pickedId,
      metadata: { ...(input?.metadata ?? {}), printAreaId: pickedId, targetPrintAreaId: pickedId },
    };
  };

  const createPrintAreaFromSurfaceDrag = (
    start: SurfacePickResult,
    end: SurfacePickResult,
    input: { id?: string; label?: string; targetMeshId?: string; physicalSize?: AsukaPhysicalSize; metadata?: Record<string, unknown> } = {},
  ): Asuka3DPrintArea | null => {
    const resolvedTarget = end.targetMesh ?? start.targetMesh;
    if (!resolvedTarget) return null;
    let normal = start.normal.add(end.normal).scale(0.5);
    if (normal.lengthSquared() < 0.0001) normal = end.normal.clone();
    if (normal.lengthSquared() < 0.0001) normal = new Vector3(0, 0, 1);
    normal.normalize();
    const center = start.position.add(end.position).scale(0.5);
    const delta = end.position.subtract(start.position);
    const basis = getSurfaceTangentBasis(normal);
    const targetBounds = getMeshWorldBounds(resolvedTarget);
    const modelScale = Math.max(0.1, targetBounds.size.length());
    const dragRight = Math.abs(Vector3.Dot(delta, basis.right));
    const dragUp = Math.abs(Vector3.Dot(delta, basis.up));
    const dragLength = Math.max(delta.length(), modelScale * 0.06);
    const width = Math.max(0.08, dragRight || dragLength * 0.7, modelScale * 0.06);
    const height = Math.max(0.08, dragUp || dragLength * 0.45, modelScale * 0.06);
    const depth = Math.max(0.035, Math.min(Math.max(width, height) * 0.4, Math.min(width, height) * 0.8));
    const id = String(input.id ?? makeId('print-area')).trim();
    const area = {
      id,
      label: String(input.label ?? `Print area ${getPrintAreas().length + 1}`).trim() || `Print area ${getPrintAreas().length + 1}`,
      targetMeshId: String(resolvedTarget.id || resolvedTarget.name || 'product'),
      mode: 'surface',
      space: 'surface-tangent',
      unit: 'world-unit',
      worldBounds: { center: fromVec3(center), size: [width, height, depth] },
      placement: { position: fromVec3(center), scale: [width, height, depth] },
      ...(input.physicalSize ? { physicalSize: clonePlain(input.physicalSize) } : null),
      metadata: {
        ...(input.metadata ?? {}),
        surfaceNormal: fromVec3(normal),
        surfaceRight: fromVec3(basis.right),
        surfaceUp: fromVec3(basis.up),
        drawStart: fromVec3(start.position),
        drawEnd: fromVec3(end.position),
        createdFrom: 'surface-draw',
      },
    } as Asuka3DPrintArea;
    return area;
  };

  const previewSurfacePrintArea = (start: SurfacePickResult, end: SurfacePickResult) => {
    if (!printAreaDrawState) return;
    clearPrintAreaDrawPreview();
    const area = createPrintAreaFromSurfaceDrag(start, end, {
      id: printAreaDrawState.id,
      label: printAreaDrawState.label,
      targetMeshId: printAreaDrawState.targetMeshId,
      physicalSize: printAreaDrawState.physicalSize,
      metadata: printAreaDrawState.metadata,
    });
    if (!area) return;
    printAreaDrawState.previewMesh = createPrintAreaVisualMesh(area, `asuka-print-area-draw-preview-${area.id}`, { preview: true });
  };

  const previewPrintAreaDrawHover = (surface: SurfacePickResult | null) => {
    const state = printAreaDrawState;
    if (!state || state.isDragging) return;
    const current = state.hoverMesh;
    if (current) {
      try { current.dispose(false, true); } catch {}
      state.hoverMesh = null;
    }
    if (!surface) return;
    const basis = getSurfaceTangentBasis(surface.normal);
    const bounds = getMeshWorldBounds(surface.targetMesh);
    const size = Math.max(0.06, Math.min(bounds.size.length() * 0.035, 0.18));
    const area = {
      id: state.id || '__draw-hover',
      label: 'Draw start',
      targetMeshId: String(surface.targetMesh.id || surface.targetMesh.name || 'product'),
      mode: 'surface',
      space: 'surface-tangent',
      unit: 'world-unit',
      worldBounds: { center: fromVec3(surface.position), size: [size, size, Math.max(0.035, size * 0.35)] },
      placement: { position: fromVec3(surface.position), scale: [size, size, Math.max(0.035, size * 0.35)] },
      metadata: {
        ...(state.metadata ?? {}),
        surfaceNormal: fromVec3(surface.normal),
        surfaceRight: fromVec3(basis.right),
        surfaceUp: fromVec3(basis.up),
        hover: true,
      },
    } as Asuka3DPrintArea;
    state.hoverMesh = createPrintAreaVisualMesh(area, `asuka-print-area-draw-hover-${area.id}`, { preview: true });
  };

  const finishSurfacePrintAreaDrawing = (end: SurfacePickResult | null, event?: PointerEvent | MouseEvent | null) => {
    const state = printAreaDrawState;
    if (!state) return;
    const start = state.startSurface;
    const px = Number((event as any)?.clientX ?? state.startClientX) || 0;
    const py = Number((event as any)?.clientY ?? state.startClientY) || 0;
    const dragPixels = Math.sqrt((px - state.startClientX) ** 2 + (py - state.startClientY) ** 2);
    const worldDistance = start && end ? start.position.subtract(end.position).length() : 0;
    const shouldCommit = !!start && !!end && dragPixels >= 8 && worldDistance > 0.008;
    const priorDiagnostic = clonePlain(lastPrintAreaDrawDiagnostic);
    lastPrintAreaDrawDiagnostic = {
      phase: 'finish',
      shouldCommit,
      dragPixels,
      worldDistance,
      hasStart: start != null,
      hasEnd: end != null,
      targetMeshId: state.targetMeshId ?? null,
      startTargetMeshId: start ? String(start.targetMesh?.id ?? start.targetMesh?.name ?? '') : null,
      endTargetMeshId: end ? String(end.targetMesh?.id ?? end.targetMesh?.name ?? '') : null,
      prior: priorDiagnostic,
    };
    clearPrintAreaDrawPreview();
    printAreaDrawState = null;
    detachPrintAreaDrawingDomListeners();
    restoreCameraAfterObjectDrag();
    try { if (state.pointerId !== null) (event?.target as any)?.releasePointerCapture?.(state.pointerId); } catch {}
    if (!shouldCommit || !start || !end) { try { state.onCancel?.(); } catch {} return; }
    const area = createPrintAreaFromSurfaceDrag(start, end, {
      id: state.id,
      label: state.label,
      targetMeshId: state.targetMeshId,
      physicalSize: state.physicalSize,
      metadata: state.metadata,
    });
    if (!area) return;
    setPrintAreasState([...getPrintAreas(), area], area.id, 'addPrintArea');
    emitChanged('printAreaDrawing:complete', area.id);
    try { state.onCreate?.(clonePlain(area)); } catch {}
  };

  const stopPrintAreaDrawingDomEvent = (event?: Event | null) => {
    try { event?.preventDefault?.(); } catch {}
    try { event?.stopPropagation?.(); } catch {}
    try { (event as any)?.stopImmediatePropagation?.(); } catch {}
  };

  const detachPrintAreaDrawingDomListeners = () => {
    try { printAreaDrawDomCleanup?.(); } catch {}
    printAreaDrawDomCleanup = null;
    try {
      if (canvas) {
        canvas.style.cursor = printAreaDrawPreviousCursor || '';
        canvas.style.touchAction = printAreaDrawPreviousTouchAction || '';
      }
    } catch {}
  };

  const attachPrintAreaDrawingDomListeners = () => {
    if (!canvas || printAreaDrawDomCleanup) return;
    const handleDown = (event: PointerEvent) => {
      if (!printAreaDrawState) return;
      stopPrintAreaDrawingDomEvent(event);
      handlePrintAreaDrawingPointer(PointerEventTypes.POINTERDOWN, event);
    };
    const handleMove = (event: PointerEvent) => {
      if (!printAreaDrawState) return;
      stopPrintAreaDrawingDomEvent(event);
      handlePrintAreaDrawingPointer(PointerEventTypes.POINTERMOVE, event);
    };
    const handleUp = (event: PointerEvent) => {
      if (!printAreaDrawState) return;
      stopPrintAreaDrawingDomEvent(event);
      handlePrintAreaDrawingPointer(PointerEventTypes.POINTERUP, event);
    };
    const handleKey = (event: KeyboardEvent) => {
      if (event.key === 'Escape') {
        stopPrintAreaDrawingDomEvent(event);
        cancelPrintAreaDrawingInternal();
      }
    };
    try {
      printAreaDrawPreviousCursor = canvas.style.cursor || '';
      printAreaDrawPreviousTouchAction = canvas.style.touchAction || '';
      canvas.style.cursor = 'crosshair';
      canvas.style.touchAction = 'none';
    } catch {}
    canvas.addEventListener('pointerdown', handleDown, { passive: false, capture: true });
    canvas.addEventListener('pointermove', handleMove, { passive: false, capture: true });
    canvas.addEventListener('pointerup', handleUp, { passive: false, capture: true });
    window.addEventListener('pointermove', handleMove, { passive: false, capture: true });
    window.addEventListener('pointerup', handleUp, { passive: false, capture: true });
    window.addEventListener('keydown', handleKey, { capture: true });
    printAreaDrawDomCleanup = () => {
      try { canvas?.removeEventListener('pointerdown', handleDown, { capture: true } as any); } catch {}
      try { canvas?.removeEventListener('pointermove', handleMove, { capture: true } as any); } catch {}
      try { canvas?.removeEventListener('pointerup', handleUp, { capture: true } as any); } catch {}
      try { window.removeEventListener('pointermove', handleMove, { capture: true } as any); } catch {}
      try { window.removeEventListener('pointerup', handleUp, { capture: true } as any); } catch {}
      try { window.removeEventListener('keydown', handleKey, { capture: true } as any); } catch {}
    };
  };

  const startPrintAreaDrawingInternal = (input: { id?: string; label?: string; targetMeshId?: string; physicalSize?: AsukaPhysicalSize; metadata?: Record<string, unknown>; onCreate?: (area: Asuka3DPrintArea) => void; onCancel?: () => void } = {}) => {
    if (printAreaAddPickState) cancelPrintAreaAddPickInternal();
    if (printAreaDrawState) cancelPrintAreaDrawingInternal();
    if (printSurfaceFacePickState) cancelPrintSurfaceFacePickingInternal();
    clearPrintAreaDrawPreview();
    printAreaDrawState = {
      id: input.id,
      label: input.label,
      targetMeshId: input.targetMeshId,
      physicalSize: input.physicalSize,
      metadata: input.metadata,
      onCreate: input.onCreate,
      onCancel: input.onCancel,
      pointerId: null,
      startClientX: 0,
      startClientY: 0,
      startSurface: null,
      lastSurface: null,
      lastSurfaceClientX: 0,
      lastSurfaceClientY: 0,
      previewMesh: null,
      hoverMesh: null,
      isDragging: false,
    };
    lastPrintAreaDrawDiagnostic = { phase: 'start', targetMeshId: input.targetMeshId ?? null };
    attachPrintAreaDrawingDomListeners();
    detachCameraForObjectDrag();
    // Do not create a visual area immediately when the toolbar button is clicked.
    // The user must explicitly click-drag on the product surface before a print
    // area is previewed or committed.
    setSelectedObject(null, false);
    emitChanged('printAreaDrawing:start', null);
  };

  const cancelPrintAreaDrawingInternal = () => {
    const state = printAreaDrawState;
    if (!state) {
      detachPrintAreaDrawingDomListeners();
      restoreCameraAfterObjectDrag();
      return;
    }
    clearPrintAreaDrawPreview();
    printAreaDrawState = null;
    detachPrintAreaDrawingDomListeners();
    restoreCameraAfterObjectDrag();
    lastPrintAreaDrawDiagnostic = { phase: 'cancel', targetMeshId: state.targetMeshId ?? null };
    try { if (state?.pointerId !== null) canvas?.releasePointerCapture?.(state!.pointerId as number); } catch {}
    emitChanged('printAreaDrawing:cancel', null);
    try { state.onCancel?.(); } catch {}
  };

  const handlePrintAreaDrawingPointer = (type: number, event?: PointerEvent | MouseEvent | null): boolean => {
    const state = printAreaDrawState;
    if (!state) return false;
    stopPrintAreaDrawingDomEvent(event as any);
    if (type === PointerEventTypes.POINTERDOWN) {
      if (state.isDragging && state.startSurface) return true;
      const preferredTarget = findModelMesh(String(state.targetMeshId ?? '').trim() || 'product') ?? null;
      const surface = pickPointerModelSurface(event ?? null, preferredTarget);
      if (!surface) {
        const unrestrictedSurface = pickPointerModelSurface(event ?? null, null);
        const local = getCanvasPointerCoordinates(event ?? null);
        const rect = canvas?.getBoundingClientRect?.();
        lastPrintAreaDrawDiagnostic = {
          phase: 'pointerdown-miss',
          targetMeshId: state.targetMeshId ?? null,
          preferredMeshId: String(preferredTarget?.id ?? preferredTarget?.name ?? '') || null,
          unrestrictedMeshId: String(unrestrictedSurface?.targetMesh?.id ?? unrestrictedSurface?.targetMesh?.name ?? '') || null,
          clientX: Number((event as any)?.clientX ?? 0) || 0,
          clientY: Number((event as any)?.clientY ?? 0) || 0,
          local,
          canvasRect: rect ? { left: rect.left, top: rect.top, width: rect.width, height: rect.height } : null,
        };
        try { event?.preventDefault?.(); } catch {}
        try { event?.stopPropagation?.(); } catch {}
        return true;
      }
      clearPrintAreaDrawPreview();
      state.pointerId = typeof (event as any)?.pointerId === 'number' ? Number((event as any).pointerId) : null;
      state.startClientX = Number((event as any)?.clientX ?? 0) || 0;
      state.startClientY = Number((event as any)?.clientY ?? 0) || 0;
      state.startSurface = surface;
      state.lastSurface = surface;
      state.lastSurfaceClientX = state.startClientX;
      state.lastSurfaceClientY = state.startClientY;
      state.isDragging = true;
      lastPrintAreaDrawDiagnostic = {
        phase: 'pointerdown-hit',
        targetMeshId: state.targetMeshId ?? null,
        pickedMeshId: String(surface.targetMesh?.id ?? surface.targetMesh?.name ?? ''),
        clientX: state.startClientX,
        clientY: state.startClientY,
      };
      detachCameraForObjectDrag();
      try { if (state.pointerId !== null) (event?.target as any)?.setPointerCapture?.(state.pointerId); } catch {}
      try { event?.preventDefault?.(); } catch {}
      try { event?.stopPropagation?.(); } catch {}
      return true;
    }
    if (type === PointerEventTypes.POINTERMOVE) {
      const surface = pickPointerModelSurface(event ?? null, findModelMesh(String(state.targetMeshId ?? '').trim() || 'product') ?? null);
      if (surface) {
        state.lastSurface = surface;
        state.lastSurfaceClientX = Number((event as any)?.clientX ?? state.lastSurfaceClientX) || 0;
        state.lastSurfaceClientY = Number((event as any)?.clientY ?? state.lastSurfaceClientY) || 0;
      }
      if (!state.isDragging || !state.startSurface) {
        previewPrintAreaDrawHover(surface);
      } else if (surface) {
        const dx = (Number((event as any)?.clientX ?? state.startClientX) || 0) - state.startClientX;
        const dy = (Number((event as any)?.clientY ?? state.startClientY) || 0) - state.startClientY;
        if (Math.sqrt(dx * dx + dy * dy) >= 6) previewSurfacePrintArea(state.startSurface, surface);
      }
      try { event?.preventDefault?.(); } catch {}
      try { event?.stopPropagation?.(); } catch {}
      return true;
    }
    if (type === PointerEventTypes.POINTERUP) {
      const pickedSurface = state.isDragging
        ? pickPointerModelSurface(event ?? null, findModelMesh(String(state.targetMeshId ?? '').trim() || 'product') ?? null)
        : null;
      const releaseX = Number((event as any)?.clientX ?? state.lastSurfaceClientX) || 0;
      const releaseY = Number((event as any)?.clientY ?? state.lastSurfaceClientY) || 0;
      const fallbackDistance = Math.hypot(
        releaseX - state.lastSurfaceClientX,
        releaseY - state.lastSurfaceClientY,
      );
      const surface = pickedSurface ?? (
        state.isDragging && state.lastSurface && fallbackDistance <= 12
          ? state.lastSurface
          : null
      );
      finishSurfacePrintAreaDrawing(surface, event ?? null);
      try { event?.preventDefault?.(); } catch {}
      try { event?.stopPropagation?.(); } catch {}
      return true;
    }
    return true;
  };

  const getAssemblyBuildPoint = (
    event?: PointerEvent | MouseEvent | null,
    start?: Vector3 | null,
    tool?: AsukaAssemblyBuildTool,
  ): Vector3 | null => {
    const assembly = getAssemblyState();
    const floorTool = tool === 'room'
      || tool === 'floor'
      || tool === 'polygon-room'
      || tool === 'polygon-floor';
    const floorY = floorTool
      ? getAssemblyFloorElevationForScene(null, Number(assembly.plan?.floorY ?? 0) || 0)
      : Number(assembly.plan?.floorY ?? 0) || 0;
    const point = getPointerWorldOnPlane(
      Plane.FromPositionAndNormal(new Vector3(0, floorY, 0), Vector3.Up()),
      event ?? null,
    );
    if (!point) return null;
    const gridSize = Math.max(0.01, Number(assembly.settings?.grid?.size ?? 0.1) || 0.1);
    if (assembly.settings?.snap?.grid !== false) {
      point.x = Math.round(point.x / gridSize) * gridSize;
      point.z = Math.round(point.z / gridSize) * gridSize;
    }
    point.y = floorY;
    // Walls follow the pointer freely. Shift provides an optional 15-degree
    // angle guide without imposing right angles on the room.
    if (
      tool === 'wall'
      && start
      && (event as any)?.shiftKey === true
      && assembly.settings?.snap?.rotation !== false
    ) {
      const dx = point.x - start.x;
      const dz = point.z - start.z;
      const length = Math.sqrt(dx * dx + dz * dz);
      if (length > 0.0001) {
        const angleStep = Math.PI / 12;
        const angle = Math.round(Math.atan2(dz, dx) / angleStep) * angleStep;
        point.x = start.x + Math.cos(angle) * length;
        point.z = start.z + Math.sin(angle) * length;
      }
    }
    if (tool === 'wall' && assembly.settings?.snap?.boundaries !== false) {
      const snapDistance = Math.max(
        gridSize * 0.75,
        Number(assembly.settings?.snap?.distance ?? 0.28) || 0.28,
      );
      let nearest: { x: number; z: number; distance: number } | null = null;
      for (const floor of assembly.plan?.floors ?? []) {
        const points = floor.points ?? [];
        for (let index = 0; index < points.length; index += 1) {
          const segmentStart = points[index];
          const segmentEnd = points[(index + 1) % points.length];
          const segmentX = Number(segmentEnd?.x ?? 0) - Number(segmentStart?.x ?? 0);
          const segmentZ = Number(segmentEnd?.z ?? 0) - Number(segmentStart?.z ?? 0);
          const segmentLengthSquared = segmentX * segmentX + segmentZ * segmentZ;
          if (segmentLengthSquared <= 0.000001) continue;
          const projected = Math.max(0, Math.min(1, (
            (point.x - Number(segmentStart?.x ?? 0)) * segmentX
            + (point.z - Number(segmentStart?.z ?? 0)) * segmentZ
          ) / segmentLengthSquared));
          const candidateX = Number(segmentStart?.x ?? 0) + segmentX * projected;
          const candidateZ = Number(segmentStart?.z ?? 0) + segmentZ * projected;
          const candidateDistance = Math.hypot(point.x - candidateX, point.z - candidateZ);
          if (candidateDistance > snapDistance || candidateDistance >= (nearest?.distance ?? Infinity)) continue;
          nearest = { x: candidateX, z: candidateZ, distance: candidateDistance };
        }
      }
      if (nearest) {
        point.x = nearest.x;
        point.z = nearest.z;
      }
    }
    return point;
  };

  const makeAssemblyPreviewMaterial = (): StandardMaterial | null => {
    if (!scene) return null;
    const material = new StandardMaterial('asuka-assembly-build-preview-material', scene);
    material.diffuseColor = color3FromInput('#2563eb');
    material.emissiveColor = color3FromInput('#2563eb').scale(0.28);
    material.alpha = 0.34;
    material.backFaceCulling = false;
    material.disableDepthWrite = true;
    assemblyBuildPreviewMaterials.push(material);
    return material;
  };

  const addAssemblyPreviewWall = (
    start: Vector3,
    end: Vector3,
    height: number,
    thickness: number,
    material: StandardMaterial,
  ): void => {
    if (!scene) return;
    const dx = end.x - start.x;
    const dz = end.z - start.z;
    const length = Math.sqrt(dx * dx + dz * dz);
    if (length <= 0.0001) return;
    const wall = MeshBuilder.CreateBox('asuka-assembly-build-preview-wall', {
      width: length,
      height,
      depth: thickness,
    }, scene);
    wall.position = new Vector3(
      (start.x + end.x) * 0.5,
      start.y + height * 0.5,
      (start.z + end.z) * 0.5,
    );
    wall.rotation.y = -Math.atan2(dz, dx);
    wall.material = material;
    wall.isPickable = false;
    wall.renderingGroupId = 3;
    assemblyBuildPreviewMeshes.push(wall);
  };

  const renderAssemblyBuildPreview = (): void => {
    clearAssemblyBuildPreview();
    if (!scene || !assemblyBuildState) return;
    const { start, current, tool } = assemblyBuildState;
    const assembly = getAssemblyState();
    const gridSize = Math.max(0.01, Number(assembly.settings?.grid?.size ?? 0.1) || 0.1);
    if (tool === 'polygon-floor' || tool === 'polygon-room') {
      const committedPoints = assemblyBuildState.points;
      if (!committedPoints.length) return;
      const previewPoints = committedPoints.map((point) => point.clone());
      if (
        current
        && Vector3.Distance(current, previewPoints[previewPoints.length - 1]) > 0.0001
      ) previewPoints.push(current.clone());
      const lines: Vector3[][] = [];
      for (let index = 0; index + 1 < previewPoints.length; index += 1) {
        lines.push([previewPoints[index], previewPoints[index + 1]]);
      }
      if (previewPoints.length >= 3) {
        lines.push([previewPoints[previewPoints.length - 1], previewPoints[0]]);
      }
      if (lines.length) {
        const outline = MeshBuilder.CreateLineSystem(
          'asuka-assembly-build-preview-outline',
          { lines },
          scene,
        );
        outline.color = color3FromInput('#2563eb');
        outline.alpha = 0.92;
        outline.isPickable = false;
        outline.renderingGroupId = 3;
        assemblyBuildPreviewMeshes.push(outline);
      }
      const markerMaterial = makeAssemblyPreviewMaterial();
      if (markerMaterial) {
        for (const point of committedPoints) {
          const marker = MeshBuilder.CreateSphere(
            'asuka-assembly-build-preview-point',
            { diameter: Math.max(0.06, Math.min(0.16, gridSize * 0.8)), segments: 10 },
            scene,
          );
          marker.position = point.add(new Vector3(0, 0.03, 0));
          marker.material = markerMaterial;
          marker.isPickable = false;
          marker.renderingGroupId = 3;
          assemblyBuildPreviewMeshes.push(marker);
        }
        const polygon = previewPoints.map((point) => ({ x: point.x, z: point.z }));
        if (isAssemblyPolygonSimple(polygon)) {
          const fill = createAssemblyPolygonMesh(
            'asuka-assembly-build-preview-floor',
            polygon,
            0.006,
          );
          if (fill) {
            fill.position.y = committedPoints[0]?.y
              ?? getAssemblyFloorElevationForScene(null, Number(assembly.plan?.floorY ?? 0) || 0);
            fill.material = markerMaterial;
            fill.isPickable = false;
            fill.renderingGroupId = 2;
            assemblyBuildPreviewMeshes.push(fill);
          }
        }
      }
      try { scene.render(); } catch {}
      return;
    }
    if (!start || !current) return;
    const width = Math.abs(current.x - start.x);
    const depth = Math.abs(current.z - start.z);
    const minSize = gridSize * 0.5;
    if (tool === 'wall') {
      if (Math.hypot(current.x - start.x, current.z - start.z) < minSize) return;
    } else if (width < minSize || depth < minSize) {
      return;
    }
    const material = makeAssemblyPreviewMaterial();
    if (!material) return;
    const wallHeight = Math.max(0.05, Number(assembly.plan?.height ?? 2.6) || 2.6);
    const wallThickness = Math.max(0.005, Number(assembly.plan?.wallThickness ?? 0.08) || 0.08);
    if (tool === 'wall') {
      addAssemblyPreviewWall(start, current, wallHeight, wallThickness, material);
      try { scene.render(); } catch {}
      return;
    }
    const center = new Vector3(
      (start.x + current.x) * 0.5,
      start.y - 0.006,
      (start.z + current.z) * 0.5,
    );
    const floor = MeshBuilder.CreateBox('asuka-assembly-build-preview-floor', {
      width: Math.max(0.01, width),
      height: 0.012,
      depth: Math.max(0.01, depth),
    }, scene);
    floor.position = center;
    floor.material = material;
    floor.isPickable = false;
    floor.renderingGroupId = 3;
    assemblyBuildPreviewMeshes.push(floor);
    if (tool === 'room') {
      const corners = [
        new Vector3(Math.min(start.x, current.x), start.y, Math.min(start.z, current.z)),
        new Vector3(Math.max(start.x, current.x), start.y, Math.min(start.z, current.z)),
        new Vector3(Math.max(start.x, current.x), start.y, Math.max(start.z, current.z)),
        new Vector3(Math.min(start.x, current.x), start.y, Math.max(start.z, current.z)),
      ];
      for (let index = 0; index < corners.length; index += 1) {
        addAssemblyPreviewWall(
          corners[index],
          corners[(index + 1) % corners.length],
          wallHeight,
          wallThickness,
          material,
        );
      }
    }
    try { scene.render(); } catch {}
  };

  const deriveLegacyAssemblySurfaces = (): {
    floors: AsukaAssemblyFloorSurface[];
    walls: AsukaAssemblyWallSurface[];
  } => {
    const plan = getAssemblyState().plan ?? ASUKA_ASSEMBLY_DEFAULT_PLAN;
    const floors = [{
      id: 'floor-existing',
      kind: 'floor' as const,
      label: 'Floor',
      points: clonePlain(plan.points),
      y: Number(plan.floorY ?? 0) || 0,
      thickness: 0.012,
      material: { color: String(plan.floorColor ?? '#f6f8fb') },
    }];
    const walls: AsukaAssemblyWallSurface[] = [];
    const segmentCount = plan.closed === false ? Math.max(0, plan.points.length - 1) : plan.points.length;
    for (let index = 0; index < segmentCount; index += 1) {
      walls.push({
        id: `wall-existing-${index + 1}`,
        kind: 'wall',
        label: `Wall ${index + 1}`,
        start: clonePlain(plan.points[index]),
        end: clonePlain(plan.points[(index + 1) % plan.points.length]),
        baseY: Number(plan.floorY ?? 0) || 0,
        height: Math.max(0.05, Number(plan.height ?? 2.6) || 2.6),
        thickness: Math.max(0.005, Number(plan.wallThickness ?? 0.08) || 0.08),
        material: { color: String(plan.wallColor ?? '#e7ebf1') },
      });
    }
    return { floors, walls };
  };

  const commitAssemblyBuildGesture = (start: Vector3, end: Vector3): AsukaAssemblySurface[] => {
    const build = assemblyBuildState;
    if (!build) return [];
    const assembly = getAssemblyState();
    const plan = assembly.plan ?? ASUKA_ASSEMBLY_DEFAULT_PLAN;
    const gridSize = Math.max(0.01, Number(assembly.settings?.grid?.size ?? 0.1) || 0.1);
    const width = Math.abs(end.x - start.x);
    const depth = Math.abs(end.z - start.z);
    const distance = Math.hypot(end.x - start.x, end.z - start.z);
    if (build.tool === 'wall' ? distance < gridSize * 0.5 : width < gridSize * 0.5 || depth < gridSize * 0.5) {
      return [];
    }

    const hasExplicitSurfaces = Array.isArray(plan.floors) || Array.isArray(plan.walls);
    const inherited = hasExplicitSurfaces
      ? { floors: clonePlain(plan.floors ?? []), walls: clonePlain(plan.walls ?? []) }
      : assemblyConstructionModeActive
        ? { floors: [], walls: [] }
        : deriveLegacyAssemblySurfaces();
    const replace = build.replaceOnFirstCommit;
    const floors = replace ? [] : inherited.floors;
    let walls = replace ? [] : inherited.walls;
    const created: AsukaAssemblySurface[] = [];
    const defaultFloorY = Number(plan.floorY ?? 0) || 0;
    let floorY = defaultFloorY;
    const wallHeight = Math.max(0.05, Number(plan.height ?? 2.6) || 2.6);
    const wallThickness = Math.max(0.005, Number(plan.wallThickness ?? 0.08) || 0.08);
    const floorMaterial: AsukaAssemblySurfaceMaterial = {
      color: String(build.input.floorMaterial?.color ?? plan.floorColor ?? '#f6f8fb'),
      ...(build.input.floorMaterial ?? {}),
    };
    const wallMaterial: AsukaAssemblySurfaceMaterial = {
      color: String(build.input.wallMaterial?.color ?? plan.wallColor ?? '#e7ebf1'),
      ...(build.input.wallMaterial ?? {}),
    };

    if (build.tool === 'wall') {
      const wall = {
        id: makeId('assembly-wall'),
        kind: 'wall' as const,
        label: `Wall ${walls.length + 1}`,
        start: { x: start.x, z: start.z },
        end: { x: end.x, z: end.z },
        baseY: floorY,
        height: wallHeight,
        thickness: wallThickness,
        material: wallMaterial,
        interiorMaterial: clonePlain(wallMaterial),
        exteriorMaterial: clonePlain(wallMaterial),
      };
      walls.push(wall);
      created.push(wall);
    } else {
      const points = [
        { x: Math.min(start.x, end.x), z: Math.min(start.z, end.z) },
        { x: Math.max(start.x, end.x), z: Math.min(start.z, end.z) },
        { x: Math.max(start.x, end.x), z: Math.max(start.z, end.z) },
        { x: Math.min(start.x, end.x), z: Math.max(start.z, end.z) },
      ];
      floorY = getAssemblyFloorElevationForScene(points, defaultFloorY);
      const floor = {
        id: makeId('assembly-floor'),
        kind: 'floor' as const,
        label: `Floor ${floors.length + 1}`,
        points,
        y: floorY,
        thickness: 0.012,
        material: floorMaterial,
        ...(build.tool === 'room'
          ? { metadata: { roomSurface: true } }
          : {}),
      };
      floors.push(floor);
      created.push(floor);
      if (build.tool === 'room') {
        const previousWallIds = new Set(walls.map((wall) => wall.id));
        walls = reconcileAssemblyRoomWalls(floor, walls, {
          height: wallHeight,
          thickness: wallThickness,
          material: wallMaterial,
          interiorMaterial: wallMaterial,
          exteriorMaterial: wallMaterial,
          createId: () => makeId('assembly-wall'),
        });
        created.push(...walls.filter((wall) => !previousWallIds.has(wall.id)));
      }
    }

    build.replaceOnFirstCommit = false;
    configureAssemblyInternal({
      enabled: true,
      plan: {
        ...plan,
        floors,
        walls,
        points: getAssemblySurfaceEnvelope(floors, walls, plan.points),
        ...(build.tool !== 'wall' && floors.length === 1 ? { floorY } : {}),
      },
    }, {
      source: `assembly.build.${build.tool}`,
      commitHistory: true,
      emitChange: true,
    });
    const first = created[0] ?? null;
    assemblyBuildSelectedSurfaceId = first?.id ?? null;
    renderAssemblyEnvironment();
    try { build.input.onCreate?.(clonePlain(created)); } catch {}
    try { build.input.onSelect?.(clonePlain(first)); } catch {}
    return clonePlain(created);
  };

  const commitAssemblyPolygonGesture = (
    pointsInput: Vector3[],
  ): AsukaAssemblySurface[] => {
    const build = assemblyBuildState;
    if (
      !build
      || (build.tool !== 'polygon-floor' && build.tool !== 'polygon-room')
    ) return [];
    const points = pointsInput.map((point) => ({ x: point.x, z: point.z }));
    if (!isAssemblyPolygonSimple(points)) {
      setAssemblyBuildDomDiagnostic('polygon-invalid', pointsInput[pointsInput.length - 1] ?? null);
      return [];
    }
    const assembly = getAssemblyState();
    const plan = assembly.plan ?? ASUKA_ASSEMBLY_DEFAULT_PLAN;
    const hasExplicitSurfaces = Array.isArray(plan.floors) || Array.isArray(plan.walls);
    const inherited = hasExplicitSurfaces
      ? { floors: clonePlain(plan.floors ?? []), walls: clonePlain(plan.walls ?? []) }
      : assemblyConstructionModeActive
        ? { floors: [], walls: [] }
        : deriveLegacyAssemblySurfaces();
    const floors = build.replaceOnFirstCommit ? [] : inherited.floors;
    let walls = build.replaceOnFirstCommit ? [] : inherited.walls;
    const floorY = getAssemblyFloorElevationForScene(
      points,
      Number(plan.floorY ?? 0) || 0,
    );
    const wallHeight = Math.max(0.05, Number(plan.height ?? 2.6) || 2.6);
    const wallThickness = Math.max(0.005, Number(plan.wallThickness ?? 0.08) || 0.08);
    const floorMaterial: AsukaAssemblySurfaceMaterial = {
      color: String(build.input.floorMaterial?.color ?? plan.floorColor ?? '#f6f8fb'),
      ...(build.input.floorMaterial ?? {}),
    };
    const wallMaterial: AsukaAssemblySurfaceMaterial = {
      color: String(build.input.wallMaterial?.color ?? plan.wallColor ?? '#e7ebf1'),
      ...(build.input.wallMaterial ?? {}),
    };
    const room = build.tool === 'polygon-room';
    const floor: AsukaAssemblyFloorSurface = {
      id: makeId('assembly-floor'),
      kind: 'floor',
      label: `Floor ${floors.length + 1}`,
      points,
      y: floorY,
      thickness: 0.012,
      material: floorMaterial,
      ...(room ? { metadata: { roomSurface: true } } : {}),
    };
    floors.push(floor);
    const created: AsukaAssemblySurface[] = [floor];
    if (room) {
      const previousWallIds = new Set(walls.map((wall) => wall.id));
      walls = reconcileAssemblyRoomWalls(floor, walls, {
        height: wallHeight,
        thickness: wallThickness,
        material: wallMaterial,
        interiorMaterial: wallMaterial,
        exteriorMaterial: wallMaterial,
        createId: () => makeId('assembly-wall'),
      });
      created.push(...walls.filter((wall) => !previousWallIds.has(wall.id)));
    }
    build.points = [];
    build.current = null;
    build.replaceOnFirstCommit = false;
    configureAssemblyInternal({
      enabled: true,
      plan: {
        ...plan,
        floors,
        walls,
        points: getAssemblySurfaceEnvelope(floors, walls, plan.points),
        ...(floors.length === 1 ? { floorY } : {}),
      },
    }, {
      source: `assembly.build.${build.tool}`,
      commitHistory: true,
      emitChange: true,
    });
    assemblyBuildSelectedSurfaceId = floor.id;
    renderAssemblyEnvironment();
    try { build.input.onCreate?.(clonePlain(created)); } catch {}
    try { build.input.onSelect?.(clonePlain(floor)); } catch {}
    return clonePlain(created);
  };

  const pickAssemblySurfaceAtPointer = (
    event?: PointerEvent | MouseEvent | null,
  ): AsukaAssemblySurface | null => {
    if (!scene) return null;
    const point = getCanvasPointerCoordinates(event ?? null);
    if (!point) return null;
    let pick: any = null;
    try {
      pick = scene.pick(
        point.x,
        point.y,
        (mesh: any) => mesh?.metadata?.asukaAssemblySurface === true,
        false,
        camera ?? undefined,
      );
    } catch {}
    const id = String(pick?.pickedMesh?.metadata?.surfaceId ?? '').trim();
    return id ? listAssemblySurfacesInternal().find((surface) => surface.id === id) ?? null : null;
  };

  const pickAssemblyFloorPointHandleAtPointer = (
    event?: PointerEvent | MouseEvent | null,
  ): { floorId: string; pointIndex: number } | null => {
    if (!scene) return null;
    const point = getCanvasPointerCoordinates(event ?? null);
    if (!point) return null;
    let pick: any = null;
    try {
      pick = scene.pick(
        point.x,
        point.y,
        (mesh: any) => mesh?.metadata?.asukaAssemblySurfaceHandle === true,
        false,
        camera ?? undefined,
      );
    } catch {}
    const floorId = String(pick?.pickedMesh?.metadata?.surfaceId ?? '').trim();
    const pointIndex = Math.floor(Number(pick?.pickedMesh?.metadata?.pointIndex ?? -1));
    return floorId && pointIndex >= 0 ? { floorId, pointIndex } : null;
  };

  const pickAssemblySurfaceGizmoAtPointer = (
    event?: PointerEvent | MouseEvent | null,
  ): {
    surfaceId: string;
    handle: 'edge' | 'move' | 'rotate' | 'wall-start' | 'wall-end';
    edgeIndex: number | null;
  } | null => {
    if (!scene) return null;
    const point = getCanvasPointerCoordinates(event ?? null);
    if (!point) return null;
    let pick: any = null;
    try {
      pick = scene.pick(
        point.x,
        point.y,
        (mesh: any) => mesh?.metadata?.asukaAssemblySurfaceGizmo === true,
        false,
        camera ?? undefined,
      );
    } catch {}
    const metadata = pick?.pickedMesh?.metadata ?? {};
    const surfaceId = String(metadata.surfaceId ?? '').trim();
    const handle = String(metadata.handle ?? '') as
      | 'edge'
      | 'move'
      | 'rotate'
      | 'wall-start'
      | 'wall-end';
    if (!surfaceId || !['edge', 'move', 'rotate', 'wall-start', 'wall-end'].includes(handle)) {
      return null;
    }
    const edgeIndex = Number.isFinite(Number(metadata.edgeIndex))
      ? Math.floor(Number(metadata.edgeIndex))
      : null;
    return { surfaceId, handle, edgeIndex };
  };

  const assemblySurfaceToPatch = (
    surface: AsukaAssemblySurface,
  ): AsukaAssemblySurfacePatch => surface.kind === 'floor'
    ? {
        points: clonePlain(surface.points),
        pointIds: clonePlain(surface.pointIds),
      }
    : {
        start: clonePlain(surface.start),
        end: clonePlain(surface.end),
      };

  const startAssemblySurfaceGizmoDrag = (
    event?: PointerEvent | MouseEvent | null,
  ): boolean => {
    const picked = pickAssemblySurfaceGizmoAtPointer(event ?? null);
    if (!picked) return false;
    const surface = listAssemblySurfacesInternal().find(
      (entry) => entry.id === picked.surfaceId,
    ) ?? null;
    if (!surface) return false;
    const transform = getAssemblySurfaceTransform(surface);
    if (!transform) return false;
    if (
      surface.kind === 'wall'
      && picked.handle !== 'move'
      && (
        surface.lockLength === true
        || surface.lockAngle === true
      )
    ) return false;
    if (surface.kind === 'floor' && picked.handle === 'rotate') {
      const hasLockedAngle = Object.values(surface.edgeConstraints ?? {})
        .some((constraint) => constraint.lockAngle === true);
      if (hasLockedAngle) return false;
    }
    if (surface.kind === 'floor' && picked.handle === 'edge' && picked.edgeIndex != null) {
      const metrics = getAssemblyFloorMetrics(surface);
      const previousIndex = (picked.edgeIndex - 1 + metrics.edges.length) % metrics.edges.length;
      const nextIndex = (picked.edgeIndex + 1) % metrics.edges.length;
      const adjacentLocked = [metrics.edges[previousIndex], metrics.edges[nextIndex]]
        .some((edge) => edge?.lockAngle || edge?.lockLength);
      if (adjacentLocked) return false;
    }
    const point = getAssemblyBuildPoint(event ?? null, null, 'select');
    if (!point) return false;
    const pivot = transform.center;
    assemblySurfaceGizmoDragState = {
      surfaceId: surface.id,
      handle: picked.handle,
      edgeIndex: picked.edgeIndex,
      pointerId: typeof (event as any)?.pointerId === 'number'
        ? Number((event as any).pointerId)
        : null,
      originalSurface: clonePlain(surface),
      currentSurface: clonePlain(surface),
      startPointerWorld: point.clone(),
      pivot: clonePlain(pivot),
      startAngle: Math.atan2(point.z - pivot.z, point.x - pivot.x),
      changed: false,
    };
    try {
      if (assemblySurfaceGizmoDragState.pointerId !== null) {
        (event?.target as any)?.setPointerCapture?.(assemblySurfaceGizmoDragState.pointerId);
      }
    } catch {}
    try { if (canvas) canvas.style.cursor = picked.handle === 'rotate' ? 'crosshair' : 'grabbing'; } catch {}
    return true;
  };

  const updateAssemblySurfaceGizmoDrag = (
    event?: PointerEvent | MouseEvent | null,
  ): boolean => {
    const drag = assemblySurfaceGizmoDragState;
    if (!drag) return false;
    const point = getAssemblyBuildPoint(event ?? null, null, 'select');
    if (!point) return true;
    const original = drag.originalSurface;
    let next: AsukaAssemblySurface = clonePlain(original);
    if (drag.handle === 'move') {
      const currentTransform = getAssemblySurfaceTransform(original);
      if (!currentTransform) return true;
      next = transformAssemblySurface(original, {
        center: {
          x: currentTransform.center.x + point.x - drag.startPointerWorld.x,
          z: currentTransform.center.z + point.z - drag.startPointerWorld.z,
        },
      });
    } else if (drag.handle === 'rotate') {
      const currentTransform = getAssemblySurfaceTransform(original);
      if (!currentTransform) return true;
      const pointerAngle = Math.atan2(point.z - drag.pivot.z, point.x - drag.pivot.x);
      let angleDegrees = currentTransform.rotationDegrees
        + (pointerAngle - drag.startAngle) * 180 / Math.PI;
      const settings = normalizeAssemblySettings(getAssemblyState().settings);
      if ((event as any)?.shiftKey !== true && settings.snap.rotation) {
        const step = Math.max(1, Number(settings.snap.rotationStepDegrees) || 10);
        angleDegrees = Math.round(angleDegrees / step) * step;
      }
      next = transformAssemblySurface(original, {
        center: currentTransform.center,
        rotationDegrees: angleDegrees,
      });
    } else if (original.kind === 'floor' && drag.handle === 'edge' && drag.edgeIndex != null) {
      const edgeIndex = drag.edgeIndex;
      const nextIndex = (edgeIndex + 1) % original.points.length;
      const dx = point.x - drag.startPointerWorld.x;
      const dz = point.z - drag.startPointerWorld.z;
      const points = original.points.map((entry, index) => (
        index === edgeIndex || index === nextIndex
          ? { x: entry.x + dx, z: entry.z + dz }
          : clonePlain(entry)
      ));
      next = { ...original, points };
    } else if (original.kind === 'wall') {
      if (drag.handle === 'wall-start') {
        next = { ...original, start: { x: point.x, z: point.z } };
      } else if (drag.handle === 'wall-end') {
        next = { ...original, end: { x: point.x, z: point.z } };
      }
    }
    const updated = updateAssemblySurfaceInternal(
      drag.surfaceId,
      assemblySurfaceToPatch(next),
      {
        source: 'assembly.surface.gizmo.preview',
        commitHistory: false,
        emitChange: true,
      },
    );
    if (updated) {
      drag.currentSurface = clonePlain(updated);
      drag.changed = true;
      try { if (canvas) canvas.style.cursor = 'grabbing'; } catch {}
    } else {
      try { if (canvas) canvas.style.cursor = 'not-allowed'; } catch {}
    }
    return true;
  };

  const finishAssemblySurfaceGizmoDrag = (
    event?: PointerEvent | MouseEvent | null,
    cancelled = false,
  ): boolean => {
    const drag = assemblySurfaceGizmoDragState;
    if (!drag) return false;
    assemblySurfaceGizmoDragState = null;
    try {
      if (drag.pointerId !== null) {
        (event?.target as any)?.releasePointerCapture?.(drag.pointerId);
      }
    } catch {}
    updateAssemblySurfaceInternal(
      drag.surfaceId,
      assemblySurfaceToPatch(drag.originalSurface),
      {
        source: 'assembly.surface.gizmo.restore',
        commitHistory: false,
        emitChange: false,
      },
    );
    if (!cancelled && drag.changed) {
      updateAssemblySurfaceInternal(
        drag.surfaceId,
        assemblySurfaceToPatch(drag.currentSurface),
        {
          source: 'assembly.surface.gizmo.commit',
          commitHistory: true,
          emitChange: true,
        },
      );
    } else {
      renderAssemblyEnvironment();
    }
    try { if (canvas) canvas.style.cursor = 'crosshair'; } catch {}
    return true;
  };

  const startAssemblyFloorPointDrag = (
    event?: PointerEvent | MouseEvent | null,
  ): boolean => {
    const handle = pickAssemblyFloorPointHandleAtPointer(event ?? null);
    if (!handle) return false;
    const floor = (getAssemblyState().plan?.floors ?? []).find(
      (surface) => surface.id === handle.floorId,
    );
    if (!floor || !floor.points[handle.pointIndex]) return false;
    const metrics = getAssemblyFloorMetrics(floor);
    const previousEdge = metrics.edges[
      (handle.pointIndex - 1 + metrics.edges.length) % metrics.edges.length
    ];
    const nextEdge = metrics.edges[handle.pointIndex];
    if (
      previousEdge?.lockLength
      || previousEdge?.lockAngle
      || nextEdge?.lockLength
      || nextEdge?.lockAngle
    ) return false;
    assemblySurfacePointDragState = {
      floorId: floor.id,
      pointIndex: handle.pointIndex,
      pointerId: typeof (event as any)?.pointerId === 'number'
        ? Number((event as any).pointerId)
        : null,
      originalPoints: clonePlain(floor.points),
      currentPoints: clonePlain(floor.points),
      changed: false,
    };
    try {
      if (assemblySurfacePointDragState.pointerId !== null) {
        (event?.target as any)?.setPointerCapture?.(assemblySurfacePointDragState.pointerId);
      }
    } catch {}
    setAssemblyBuildDomDiagnostic('floor-point-drag-start', new Vector3(
      floor.points[handle.pointIndex].x,
      Number(floor.y ?? getAssemblyState().plan?.floorY ?? 0) || 0,
      floor.points[handle.pointIndex].z,
    ));
    return true;
  };

  const updateAssemblyFloorPointDrag = (
    event?: PointerEvent | MouseEvent | null,
  ): boolean => {
    const drag = assemblySurfacePointDragState;
    if (!drag) return false;
    const point = getAssemblyBuildPoint(event ?? null, null, 'select');
    if (!point) return true;
    const nextPoints = drag.currentPoints.map((entry, index) => index === drag.pointIndex
      ? { x: point.x, z: point.z }
      : { ...entry });
    const updated = updateAssemblySurfaceInternal(drag.floorId, { points: nextPoints }, {
      source: 'assembly.surface.point.preview',
      commitHistory: false,
      emitChange: true,
    });
    if (updated?.kind === 'floor') {
      drag.currentPoints = clonePlain(updated.points);
      drag.changed = true;
    }
    setAssemblyBuildDomDiagnostic('floor-point-drag', point);
    return true;
  };

  const finishAssemblyFloorPointDrag = (
    event?: PointerEvent | MouseEvent | null,
    cancelled = false,
  ): boolean => {
    const drag = assemblySurfacePointDragState;
    if (!drag) return false;
    assemblySurfacePointDragState = null;
    try {
      if (drag.pointerId !== null) {
        (event?.target as any)?.releasePointerCapture?.(drag.pointerId);
      }
    } catch {}
    if (cancelled) {
      updateAssemblySurfaceInternal(drag.floorId, { points: drag.originalPoints }, {
        source: 'assembly.surface.point.cancel',
        commitHistory: false,
        emitChange: true,
      });
    } else if (drag.changed) {
      updateAssemblySurfaceInternal(drag.floorId, { points: drag.currentPoints }, {
        source: 'assembly.surface.point.commit',
        commitHistory: true,
        emitChange: true,
      });
    }
    setAssemblyBuildDomDiagnostic(
      cancelled ? 'floor-point-drag-cancel' : 'floor-point-drag-commit',
      null,
    );
    return true;
  };

  const stopAssemblyBuildDomEvent = (event?: Event | null): void => {
    try { event?.preventDefault?.(); } catch {}
    try { event?.stopPropagation?.(); } catch {}
    try { (event as any)?.stopImmediatePropagation?.(); } catch {}
  };

  const setAssemblyBuildDomDiagnostic = (
    phase: string,
    point?: Vector3 | null,
  ): void => {
    try {
      if (!canvas) return;
      canvas.dataset.asukaAssemblyBuild = JSON.stringify({
        phase,
        tool: assemblyBuildState?.tool ?? null,
        dragging: assemblyBuildState?.dragging === true,
        point: point ? fromVec3(point) : null,
        surfaces: (currentState.assembly?.plan?.floors?.length ?? 0)
          + (currentState.assembly?.plan?.walls?.length ?? 0),
      });
    } catch {}
  };

  const isAssemblyComponentDragStartEvent = (
    event?: PointerEvent | MouseEvent | null,
  ): boolean => {
    if (
      !event
      || ((event as any).ctrlKey !== true && !assemblyMoveToolActive)
      || !canEditAssemblyRuntime()
    ) return false;
    const button = Number((event as any).button ?? 0);
    return !Number.isFinite(button) || button === 0;
  };

  const getAssemblyComponentDragPoint = (
    event?: PointerEvent | MouseEvent | null,
  ): Vector3 | null => {
    const floorY = Number(getAssemblyState().plan?.floorY ?? 0) || 0;
    const point = getPointerWorldOnPlane(
      Plane.FromPositionAndNormal(new Vector3(0, floorY, 0), Vector3.Up()),
      event ?? null,
    );
    if (point) point.y = floorY;
    return point;
  };

  const getAssemblyWorldPerPixel = (): number => {
    const cameraDistance = Math.max(
      0.1,
      Number((camera as any)?.radius ?? readCameraState()?.radius ?? 8) || 8,
    );
    const viewportHeight = Math.max(1, Number(canvas?.clientHeight ?? 600) || 600);
    const fieldOfView = Math.max(0.1, Number((camera as any)?.fov ?? 0.8) || 0.8);
    return (
      (2 * cameraDistance * Math.tan(fieldOfView * 0.5))
      / viewportHeight
    ) * 0.35;
  };

  const getAssemblyScreenDragDelta = (dx: number, dy: number): Vector3 => {
    const basis = getViewTransformBasis();
    let right = new Vector3(basis.right.x, 0, basis.right.z);
    if (right.lengthSquared() < 0.0001) right = new Vector3(1, 0, 0);
    right.normalize();
    let forward = new Vector3(basis.forward.x, 0, basis.forward.z);
    if (forward.lengthSquared() < 0.0001) {
      forward = Vector3.Cross(Vector3.Up(), right);
    }
    forward.normalize();
    const worldPerPixel = getAssemblyWorldPerPixel();
    return right.scale(dx * worldPerPixel).add(forward.scale(-dy * worldPerPixel));
  };

  const pickAssemblyComponentIdAtPointer = (
    event?: PointerEvent | MouseEvent | null,
    fallbackMesh?: any,
  ): string => {
    const componentIds = new Set(listAssemblyComponents().map((component) => component.id));
    const fallbackId = getProductIdFromMesh(fallbackMesh);
    if (fallbackId && componentIds.has(fallbackId)) return fallbackId;
    const point = getCanvasPointerCoordinates(event ?? null);
    if (!scene || !point || !componentIds.size) return '';
    let pickedId = '';
    try {
      const pick = scene.pick(
        point.x,
        point.y,
        (mesh: any) => componentIds.has(getProductIdFromMesh(mesh)),
        false,
        camera ?? undefined,
      );
      pickedId = getProductIdFromMesh((pick?.pickedMesh ?? null) as Mesh | null);
    } catch {}
    if (componentIds.has(pickedId)) return pickedId;

    // Keep canvas interactions aligned with the public surface API. Babylon's
    // direct predicate pick can miss imported child meshes on some renderers,
    // while the canonical surface resolver also maps those children back to
    // their runtime product instance.
    try {
      const target = pickSurfaceAtInternal({
        space: 'screen',
        unit: 'css-px',
        clientX: Number((event as any)?.clientX),
        clientY: Number((event as any)?.clientY),
      });
      const metadata = target?.metadata ?? {};
      pickedId = String(metadata.asukaProductId ?? metadata.productId ?? '')
        .replace(/^product:/i, '')
        .trim();
    } catch {}
    return componentIds.has(pickedId) ? pickedId : '';
  };

  const setAssemblyComponentDragDiagnostic = (
    phase: string,
    result?: AsukaAssemblyPlacementResult | null,
    context: { componentId?: string; moved?: boolean; source?: string } = {},
  ): void => {
    try {
      if (!canvas) return;
      canvas.dataset.asukaAssemblyPlacement = JSON.stringify({
        phase,
        componentId: context.componentId
          ?? assemblyComponentDragState?.componentId
          ?? result?.componentId
          ?? null,
        moved: context.moved ?? assemblyComponentDragState?.moved === true,
        source: context.source ?? assemblyComponentDragState?.source ?? null,
        accepted: result?.accepted ?? null,
        snapped: result?.snapped ?? false,
        snapKind: result?.snap?.kind ?? null,
        issues: result?.validation?.issues?.map((issue) => issue.code) ?? [],
      });
    } catch {}
  };

  const stopAssemblyComponentDragDomEvent = (
    event?: Event | null,
  ): void => {
    try { event?.preventDefault?.(); } catch {}
    try { event?.stopPropagation?.(); } catch {}
    try { (event as any)?.stopImmediatePropagation?.(); } catch {}
  };

  const startAssemblyComponentDrag = (
    event?: PointerEvent | MouseEvent | null,
    fallbackMesh?: any,
  ): boolean => {
    const eligible = isAssemblyComponentDragStartEvent(event) && !assemblyComponentDragState;
    if (!eligible) return false;
    if (!isAssemblyModeEnabled()) {
      configureAssemblyInternal({ enabled: true }, {
        source: 'assembly.interaction.drag-enable',
        commitHistory: false,
        emitChange: false,
      });
    }
    const canvasPoint = getCanvasPointerCoordinates(event ?? null);
    let rotationGizmoComponentId = String(
      fallbackMesh?.metadata?.asukaAssemblyComponentRotationGizmo === true
        ? fallbackMesh?.metadata?.componentId ?? ''
        : '',
    ).trim();
    if (!rotationGizmoComponentId && scene && canvasPoint) {
      try {
        const gizmoPick = scene.pick(
          canvasPoint.x,
          canvasPoint.y,
          (mesh: any) => mesh?.metadata?.asukaAssemblyComponentRotationGizmo === true,
          false,
          camera ?? undefined,
        );
        rotationGizmoComponentId = String(
          gizmoPick?.pickedMesh?.metadata?.componentId ?? '',
        ).trim();
      } catch {}
    }
    const mode: 'move' | 'rotate' = rotationGizmoComponentId ? 'rotate' : 'move';
    const componentId = rotationGizmoComponentId
      || pickAssemblyComponentIdAtPointer(event ?? null, fallbackMesh ?? null);
    const component = listAssemblyComponents().find((entry) => entry.id === componentId) ?? null;
    const pointerWorldOnPlane = getAssemblyComponentDragPoint(event ?? null);
    const floorY = Number(getAssemblyState().plan?.floorY ?? 0) || 0;
    const pointerWorld = pointerWorldOnPlane ?? (component
      ? new Vector3(component.bounds.center.x, floorY, component.bounds.center.z)
      : null);
    const screenSpaceFallback = pointerWorldOnPlane == null;
    try {
      if (canvas) {
        canvas.dataset.asukaAssemblyMoveAttempt = JSON.stringify({
          componentId: componentId || null,
          moveToolActive: assemblyMoveToolActive,
          modifier: (event as any)?.ctrlKey === true,
          hasComponent: component != null,
          hasPointerWorld: pointerWorld != null,
          screenSpaceFallback,
        });
      }
    } catch {}
    if (!component || !pointerWorld) return false;
    const pointerType = String((event as any)?.pointerType ?? '').toLowerCase();
    // Mouse pointer ids are not stable in every browser automation/runtime.
    // There can only be one mouse drag, while touch and pen still need strict
    // pointer matching to avoid another contact taking over the placement.
    const pointerId = pointerType && pointerType !== 'mouse' && typeof (event as any)?.pointerId === 'number'
      ? Number((event as any).pointerId)
      : null;
    const verticalMode = mode === 'move' && (event as any)?.ctrlKey === true;
    const source = mode === 'rotate'
      ? 'assembly.canvas.rotation-gizmo'
      : verticalMode
      ? 'assembly.canvas.vertical-drag'
      : 'assembly.canvas.move-tool';
    const originalTransform = normalizeAssemblyTransform(component.transform);
    const assemblyState = getAssemblyState();
    const supportMode = getAssemblyComponentSupport(assemblyState, component);
    const supportOffset = Number(
      assemblyState.componentPlacements?.[component.id]?.supportOffset
      ?? component.definition?.placement?.supportOffset
      ?? 0,
    ) || 0;
    const supportTolerance = Math.max(
      0.03,
      (Number(getAssemblyState().settings?.grid?.size ?? 0.1) || 0.1) * 0.55,
    );
    const supportOnHorizontal = supportMode === 'floor'
      ? Math.abs(component.bounds.min.y - (floorY + supportOffset)) <= supportTolerance
      : supportMode !== 'free';
    const initialPlacementInput: AsukaAssemblyPlacementInput = {
      ...originalTransform,
      snap: false,
      snapSupport: false,
      allowInvalid: false,
      metadata: { source },
    };
    const initialPlacementResult = resolveAssemblyPlacement(
      getAssemblyState(),
      listAssemblyComponents(),
      componentId,
      initialPlacementInput,
    );
    assemblyComponentDragState = {
      componentId,
      mode,
      pointerId,
      startClientX: Number((event as any)?.clientX ?? 0) || 0,
      startClientY: Number((event as any)?.clientY ?? 0) || 0,
      startPointerWorld: pointerWorld,
      rotationPivot: new Vector3(
        component.bounds.center.x,
        floorY,
        component.bounds.center.z,
      ),
      startRotationAngle: Math.atan2(
        pointerWorld.z - component.bounds.center.z,
        pointerWorld.x - component.bounds.center.x,
      ),
      screenSpaceFallback,
      verticalMode,
      supportOnHorizontal,
      originalTransform,
      originalBounds: clonePlain(component.bounds),
      lastPlacementInput: null,
      lastResult: null,
      lastAcceptedPlacementInput: initialPlacementResult.accepted ? initialPlacementInput : null,
      lastAcceptedResult: initialPlacementResult.accepted ? initialPlacementResult : null,
      moved: false,
      previousCursor: canvas?.style.cursor ?? '',
      source,
      startCameraState: readCameraState(),
    };
    productCanvasPointerDown = null;
    clearProductHover(true);
    selectProductInternal(componentId, {
      emit: true,
      source,
      selectObject: true,
    });
    detachCameraForObjectDrag();
    try {
      if (canvas) canvas.style.cursor = mode === 'rotate' ? 'crosshair' : 'grabbing';
      if (pointerId !== null) (event?.target as any)?.setPointerCapture?.(pointerId);
    } catch {}
    stopAssemblyComponentDragDomEvent(event as any);
    setAssemblyComponentDragDiagnostic('start', null);
    return true;
  };

  const updateAssemblyComponentDrag = (
    event?: PointerEvent | MouseEvent | null,
  ): boolean => {
    const drag = assemblyComponentDragState;
    if (!drag) return false;
    const clientX = Number((event as any)?.clientX ?? drag.startClientX) || drag.startClientX;
    const clientY = Number((event as any)?.clientY ?? drag.startClientY) || drag.startClientY;
    const dx = clientX - drag.startClientX;
    const dy = clientY - drag.startClientY;
    if (!drag.moved && dx * dx + dy * dy <= 16) {
      stopAssemblyComponentDragDomEvent(event as any);
      return true;
    }
    drag.moved = true;
    const snappingEnabled = (event as any)?.shiftKey !== true;
    const originalPosition = drag.originalTransform.position ?? { x: 0, y: 0, z: 0 };
    if (drag.mode === 'rotate') {
      const pointerWorld = getAssemblyComponentDragPoint(event ?? null)
        ?? drag.startPointerWorld.add(getAssemblyScreenDragDelta(dx, dy));
      const pointerAngle = Math.atan2(
        pointerWorld.z - drag.rotationPivot.z,
        pointerWorld.x - drag.rotationPivot.x,
      );
      const originalRotation = drag.originalTransform.rotation ?? { x: 0, y: 0, z: 0 };
      let yaw = Number(originalRotation.y ?? 0)
        + pointerAngle
        - drag.startRotationAngle;
      const settings = normalizeAssemblySettings(getAssemblyState().settings);
      if (snappingEnabled && settings.snap.rotation) {
        const stepRadians = (
          Math.max(1, Number(settings.snap.rotationStepDegrees) || 10)
          * Math.PI
          / 180
        );
        yaw = Math.round(yaw / stepRadians) * stepRadians;
      }
      const placementInput: AsukaAssemblyPlacementInput = {
        position: clonePlain(originalPosition),
        rotation: {
          x: Number(originalRotation.x ?? 0),
          y: yaw,
          z: Number(originalRotation.z ?? 0),
        },
        axis: 'free',
        snap: false,
        snapSupport: drag.supportOnHorizontal,
        allowInvalid: false,
        metadata: {
          source: drag.source,
          mode: 'rotate',
          precise: !snappingEnabled,
        },
      };
      const result = resolveAssemblyPlacement(
        getAssemblyState(),
        listAssemblyComponents(),
        drag.componentId,
        placementInput,
      );
      drag.lastPlacementInput = placementInput;
      drag.lastResult = result;
      if (result.accepted) {
        drag.lastAcceptedPlacementInput = placementInput;
        drag.lastAcceptedResult = result;
        applyAssemblyProductTransform(drag.componentId, result.applied, { render: true });
      } else {
        renderAssemblyEnvironment();
      }
      try { if (canvas) canvas.style.cursor = result.accepted ? 'crosshair' : 'not-allowed'; } catch {}
      stopAssemblyComponentDragDomEvent(event as any);
      setAssemblyComponentDragDiagnostic(result.accepted ? 'rotation-preview' : 'blocked', result);
      return true;
    }
    const pointerWorldOnPlane = drag.verticalMode || drag.screenSpaceFallback
      ? null
      : getAssemblyComponentDragPoint(event ?? null);
    const pointerWorld = pointerWorldOnPlane
      ?? drag.startPointerWorld.add(getAssemblyScreenDragDelta(dx, dy));
    const verticalY = originalPosition.y - dy * getAssemblyWorldPerPixel();
    const placementInput: AsukaAssemblyPlacementInput = {
      position: drag.verticalMode
        ? {
            x: originalPosition.x,
            y: verticalY,
            z: originalPosition.z,
          }
        : {
            x: originalPosition.x + pointerWorld.x - drag.startPointerWorld.x,
            y: originalPosition.y,
            z: originalPosition.z + pointerWorld.z - drag.startPointerWorld.z,
          },
      axis: drag.verticalMode ? 'vertical' : 'horizontal',
      snap: snappingEnabled,
      snapAnchors: !drag.verticalMode && snappingEnabled,
      snapBoundaries: !drag.verticalMode && snappingEnabled,
      snapGrid: snappingEnabled,
      snapSupport: !drag.verticalMode && drag.supportOnHorizontal,
      allowInvalid: false,
      metadata: {
        source: drag.source,
        axis: drag.verticalMode ? 'vertical' : 'horizontal',
        precise: !snappingEnabled,
      },
    };
    const result = resolveAssemblyPlacement(
      getAssemblyState(),
      listAssemblyComponents(),
      drag.componentId,
      placementInput,
    );
    drag.lastPlacementInput = placementInput;
    drag.lastResult = result;
    if (result.accepted) {
      drag.lastAcceptedPlacementInput = placementInput;
      drag.lastAcceptedResult = result;
      applyAssemblyProductTransform(drag.componentId, result.applied, { render: true });
    } else {
      renderAssemblyEnvironment();
    }
    try { if (canvas) canvas.style.cursor = result.accepted ? 'grabbing' : 'not-allowed'; } catch {}
    stopAssemblyComponentDragDomEvent(event as any);
    setAssemblyComponentDragDiagnostic(result.accepted ? 'preview' : 'blocked', result);
    return true;
  };

  const finishAssemblyComponentDrag = (
    event?: PointerEvent | MouseEvent | null,
    cancelled = false,
  ): boolean => {
    const drag = assemblyComponentDragState;
    if (!drag) return false;
    const previewResult = drag.lastAcceptedResult ?? drag.lastResult;
    assemblyComponentDragState = null;
    try {
      if (drag.pointerId !== null) (event?.target as any)?.releasePointerCapture?.(drag.pointerId);
    } catch {}
    // Restore the previewed group, then commit the final requested placement once.
    applyAssemblyProductTransform(drag.componentId, drag.originalTransform, { render: false });
    let committed: AsukaAssemblyPlacementResult | null = null;
    if (
      !cancelled
      && drag.moved
      && drag.lastAcceptedPlacementInput
      && drag.lastAcceptedResult?.accepted
    ) {
      committed = placeAssemblyComponentInternal(
        drag.componentId,
        drag.lastAcceptedPlacementInput,
        {
          source: drag.source,
          commitHistory: true,
          emitChange: true,
        },
      );
    } else {
      renderAssemblyEnvironment();
    }
    restoreCameraAfterObjectDrag(drag.startCameraState);
    if (assemblyBuildState) applyAssemblyBuildCameraControls();
    try { if (canvas) canvas.style.cursor = drag.previousCursor; } catch {}
    stopAssemblyComponentDragDomEvent(event as any);
    consumedProductPointerUpClick = {
      at: Date.now(),
      x: Number((event as any)?.clientX ?? drag.startClientX) || drag.startClientX,
      y: Number((event as any)?.clientY ?? drag.startClientY) || drag.startClientY,
    };
    setAssemblyComponentDragDiagnostic(
      cancelled ? 'cancel' : 'commit',
      committed ?? previewResult,
      { componentId: drag.componentId, moved: drag.moved, source: drag.source },
    );
    return true;
  };

  const handleAssemblyBuildPointer = (
    type: number,
    event?: PointerEvent | MouseEvent | null,
  ): boolean => {
    const build = assemblyBuildState;
    if (!build) return false;
    if (assemblySurfaceGizmoDragState) {
      stopAssemblyBuildDomEvent(event as any);
      if (type === PointerEventTypes.POINTERMOVE) {
        return updateAssemblySurfaceGizmoDrag(event ?? null);
      }
      if (type === PointerEventTypes.POINTERUP) {
        return finishAssemblySurfaceGizmoDrag(event ?? null);
      }
      return true;
    }
    if (assemblySurfacePointDragState) {
      stopAssemblyBuildDomEvent(event as any);
      if (type === PointerEventTypes.POINTERMOVE) {
        return updateAssemblyFloorPointDrag(event ?? null);
      }
      if (type === PointerEventTypes.POINTERUP) {
        return finishAssemblyFloorPointDrag(event ?? null);
      }
      return true;
    }
    if (assemblyComponentDragState) {
      if (type === PointerEventTypes.POINTERMOVE) return updateAssemblyComponentDrag(event ?? null);
      if (type === PointerEventTypes.POINTERUP) return finishAssemblyComponentDrag(event ?? null);
      return true;
    }
    if (type === PointerEventTypes.POINTERDOWN) {
      const button = Number((event as any)?.button ?? 0);
      if (Number.isFinite(button) && button !== 0) return false;
      if ((event as any)?.ctrlKey === true) {
        return startAssemblyComponentDrag(event ?? null) || false;
      }
    }
    const polygonTool = build.tool === 'polygon-floor' || build.tool === 'polygon-room';
    if (
      polygonTool
      && type === PointerEventTypes.POINTERMOVE
      && build.points.length
    ) {
      stopAssemblyBuildDomEvent(event as any);
      const point = getAssemblyBuildPoint(event ?? null, build.points[build.points.length - 1], build.tool);
      if (point) {
        build.current = point;
        renderAssemblyBuildPreview();
        setAssemblyBuildDomDiagnostic('polygon-preview', point);
      }
      return true;
    }
    if (polygonTool && type === PointerEventTypes.POINTERUP) {
      stopAssemblyBuildDomEvent(event as any);
      try {
        if (build.pointerId !== null) {
          (event?.target as any)?.releasePointerCapture?.(build.pointerId);
        }
      } catch {}
      build.pointerId = null;
      return true;
    }
    if (type !== PointerEventTypes.POINTERDOWN && !build.dragging) {
      return false;
    }
    stopAssemblyBuildDomEvent(event as any);
    if (build.tool === 'select' || build.tool === 'erase') {
      if (type !== PointerEventTypes.POINTERDOWN) return true;
      if (build.tool === 'select' && startAssemblySurfaceGizmoDrag(event ?? null)) {
        return true;
      }
      if (build.tool === 'select' && startAssemblyFloorPointDrag(event ?? null)) {
        return true;
      }
      const surface = pickAssemblySurfaceAtPointer(event ?? null);
      setAssemblyBuildDomDiagnostic(build.tool, null);
      if (build.tool === 'erase' && surface) {
        removeAssemblySurfaceInternal(surface.id, {
          source: 'assembly.build.erase',
          commitHistory: true,
          emitChange: true,
        });
        try { build.input.onSelect?.(null); } catch {}
      } else {
        selectAssemblySurfaceInternal(surface?.id ?? null);
        try { build.input.onSelect?.(clonePlain(surface)); } catch {}
      }
      return true;
    }
    if (polygonTool) {
      if (type !== PointerEventTypes.POINTERDOWN) return true;
      build.pointerId = typeof (event as any)?.pointerId === 'number'
        ? Number((event as any).pointerId)
        : null;
      try {
        if (build.pointerId !== null) {
          (event?.target as any)?.setPointerCapture?.(build.pointerId);
        }
      } catch {}
      const point = getAssemblyBuildPoint(
        event ?? null,
        build.points[build.points.length - 1] ?? null,
        build.tool,
      );
      if (!point) return true;
      const closeDistance = Math.max(
        0.08,
        Number(getAssemblyState().settings?.snap?.distance ?? 0.28) || 0.28,
      );
      const closesAtStart = build.points.length >= 3
        && Vector3.Distance(point, build.points[0]) <= closeDistance;
      const doubleClick = Number((event as any)?.detail ?? 0) >= 2;
      if ((closesAtStart || doubleClick) && build.points.length >= 3) {
        commitAssemblyPolygonGesture(build.points);
        clearAssemblyBuildPreview();
        setAssemblyBuildDomDiagnostic('polygon-commit', point);
        return true;
      }
      const previousPoint = build.points[build.points.length - 1] ?? null;
      if (previousPoint && Vector3.Distance(previousPoint, point) <= 0.0001) return true;
      const nextPoints = [...build.points, point.clone()];
      if (
        nextPoints.length >= 3
        && !isAssemblyPolygonSimple(nextPoints.map((entry) => ({ x: entry.x, z: entry.z })))
      ) {
        setAssemblyBuildDomDiagnostic('polygon-point-rejected', point);
        return true;
      }
      build.points = nextPoints;
      build.current = point.clone();
      renderAssemblyBuildPreview();
      setAssemblyBuildDomDiagnostic('polygon-point', point);
      return true;
    }
    if (type === PointerEventTypes.POINTERDOWN) {
      const point = getAssemblyBuildPoint(event ?? null, null, build.tool);
      if (!point) return true;
      build.pointerId = typeof (event as any)?.pointerId === 'number' ? Number((event as any).pointerId) : null;
      build.start = point;
      build.current = point.clone();
      build.dragging = true;
      setAssemblyBuildDomDiagnostic('pointerdown', point);
      try { if (build.pointerId !== null) (event?.target as any)?.setPointerCapture?.(build.pointerId); } catch {}
      return true;
    }
    if (type === PointerEventTypes.POINTERMOVE && build.dragging && build.start) {
      const point = getAssemblyBuildPoint(event ?? null, build.start, build.tool);
      if (!point) return true;
      build.current = point;
      setAssemblyBuildDomDiagnostic('pointermove', point);
      renderAssemblyBuildPreview();
      return true;
    }
    if (type === PointerEventTypes.POINTERUP && build.dragging && build.start) {
      const point = getAssemblyBuildPoint(event ?? null, build.start, build.tool) ?? build.current;
      const start = build.start.clone();
      build.dragging = false;
      build.start = null;
      build.current = null;
      clearAssemblyBuildPreview();
      try { if (build.pointerId !== null) (event?.target as any)?.releasePointerCapture?.(build.pointerId); } catch {}
      build.pointerId = null;
      if (point) commitAssemblyBuildGesture(start, point);
      setAssemblyBuildDomDiagnostic('pointerup', point);
      return true;
    }
    return true;
  };

  const cancelAssemblyBuildPointer = (
    event?: PointerEvent | null,
  ): boolean => {
    if (assemblySurfaceGizmoDragState) {
      stopAssemblyBuildDomEvent(event);
      finishAssemblySurfaceGizmoDrag(event, true);
      setAssemblyBuildDomDiagnostic('surface-gizmo-pointercancel', null);
      return true;
    }
    if (assemblySurfacePointDragState) {
      stopAssemblyBuildDomEvent(event);
      finishAssemblyFloorPointDrag(event, true);
      setAssemblyBuildDomDiagnostic('floor-point-pointercancel', null);
      return true;
    }
    if (assemblyComponentDragState) {
      stopAssemblyBuildDomEvent(event);
      finishAssemblyComponentDrag(event, true);
      setAssemblyBuildDomDiagnostic('component-pointercancel', null);
      return true;
    }
    const build = assemblyBuildState;
    if (!build || (!build.dragging && build.pointerId == null)) return false;
    stopAssemblyBuildDomEvent(event);
    try {
      if (build.pointerId !== null) {
        (event?.target as any)?.releasePointerCapture?.(build.pointerId);
      }
    } catch {}
    build.pointerId = null;
    build.dragging = false;
    build.start = null;
    build.current = build.points[build.points.length - 1]?.clone?.() ?? null;
    clearAssemblyBuildPreview();
    if (build.points.length) renderAssemblyBuildPreview();
    setAssemblyBuildDomDiagnostic('pointercancel', build.current);
    return true;
  };

  const detachAssemblyBuildDomListeners = (): void => {
    try { assemblyBuildDomCleanup?.(); } catch {}
    assemblyBuildDomCleanup = null;
    try {
      if (canvas) {
        canvas.style.cursor = assemblyBuildPreviousCursor || '';
        canvas.style.touchAction = assemblyBuildPreviousTouchAction || '';
      }
    } catch {}
  };

  const cancelAssemblyDrawingInternal = (): void => {
    const previous = assemblyBuildState;
    if (assemblySurfaceGizmoDragState) finishAssemblySurfaceGizmoDrag(null, true);
    if (assemblySurfacePointDragState) finishAssemblyFloorPointDrag(null, true);
    if (assemblyComponentDragState) finishAssemblyComponentDrag(null, true);
    clearAssemblyBuildPreview();
    assemblyBuildState = null;
    detachAssemblyBuildDomListeners();
    restoreAssemblyBuildCameraControls();
    restoreCameraAfterObjectDrag();
    renderAssemblyEnvironment();
    setAssemblyBuildDomDiagnostic('cancel', null);
    emitChanged('assemblyBuild:cancel', null, { commitHistory: false });
    try { previous?.input.onCancel?.(); } catch {}
  };

  const getAssemblyInteractionStateInternal = (): AsukaAssemblyInteractionState => ({
    constructionModeActive: assemblyConstructionModeActive && canEditAssemblyRuntime(),
    moveToolActive: assemblyMoveToolActive && canEditAssemblyRuntime(),
  });

  const detachAssemblyMoveToolDomListeners = (): void => {
    try { assemblyMoveToolDomCleanup?.(); } catch {}
    assemblyMoveToolDomCleanup = null;
  };

  const attachAssemblyMoveToolDomListeners = (): void => {
    if (!canvas || assemblyMoveToolDomCleanup) return;

    const matchesActivePointer = (event: PointerEvent): boolean => {
      const pointerId = assemblyComponentDragState?.pointerId;
      return pointerId == null || pointerId === event.pointerId;
    };
    const handlePointerDown = (event: PointerEvent) => {
      if (!assemblyMoveToolActive || assemblyComponentDragState) return;
      startAssemblyComponentDrag(event);
    };
    const handlePointerMove = (event: PointerEvent) => {
      if (!assemblyComponentDragState || !matchesActivePointer(event)) return;
      updateAssemblyComponentDrag(event);
    };
    const handlePointerUp = (event: PointerEvent) => {
      if (!assemblyComponentDragState || !matchesActivePointer(event)) return;
      finishAssemblyComponentDrag(event);
    };
    const handlePointerCancel = (event: PointerEvent) => {
      if (!assemblyComponentDragState || !matchesActivePointer(event)) return;
      finishAssemblyComponentDrag(event, true);
    };
    const handleMouseDown = (event: MouseEvent) => {
      if (!assemblyMoveToolActive || assemblyComponentDragState) return;
      startAssemblyComponentDrag(event);
    };
    const handleMouseMove = (event: MouseEvent) => {
      if (!assemblyComponentDragState || assemblyComponentDragState.pointerId != null) return;
      updateAssemblyComponentDrag(event);
    };
    const handleMouseUp = (event: MouseEvent) => {
      if (!assemblyComponentDragState || assemblyComponentDragState.pointerId != null) return;
      finishAssemblyComponentDrag(event);
    };

    canvas.addEventListener('pointerdown', handlePointerDown, { passive: false, capture: true });
    canvas.addEventListener('mousedown', handleMouseDown, { passive: false, capture: true });
    window.addEventListener('pointermove', handlePointerMove, { passive: false, capture: true });
    window.addEventListener('pointerup', handlePointerUp, { passive: false, capture: true });
    window.addEventListener('pointercancel', handlePointerCancel, { passive: false, capture: true });
    window.addEventListener('mousemove', handleMouseMove, { passive: false, capture: true });
    window.addEventListener('mouseup', handleMouseUp, { passive: false, capture: true });

    assemblyMoveToolDomCleanup = () => {
      try { canvas?.removeEventListener('pointerdown', handlePointerDown, { capture: true } as any); } catch {}
      try { canvas?.removeEventListener('mousedown', handleMouseDown, { capture: true } as any); } catch {}
      try { window.removeEventListener('pointermove', handlePointerMove, { capture: true } as any); } catch {}
      try { window.removeEventListener('pointerup', handlePointerUp, { capture: true } as any); } catch {}
      try { window.removeEventListener('pointercancel', handlePointerCancel, { capture: true } as any); } catch {}
      try { window.removeEventListener('mousemove', handleMouseMove, { capture: true } as any); } catch {}
      try { window.removeEventListener('mouseup', handleMouseUp, { capture: true } as any); } catch {}
    };
  };

  const setAssemblyMoveToolInternal = (active: boolean): AsukaAssemblyInteractionState => {
    const nextActive = active === true;
    if (nextActive) assertAssemblyEditPermission('setMoveTool');
    if (nextActive === assemblyMoveToolActive) {
      if (nextActive) attachAssemblyMoveToolDomListeners();
      else detachAssemblyMoveToolDomListeners();
      return getAssemblyInteractionStateInternal();
    }

    if (nextActive && assemblyBuildState) cancelAssemblyDrawingInternal();
    if (!nextActive && assemblyComponentDragState) finishAssemblyComponentDrag(null, true);

    if (nextActive) {
      assemblyConstructionModeActive = true;
      if (!isAssemblyModeEnabled()) {
        configureAssemblyInternal({ enabled: true }, {
          source: 'assembly.interaction.enable',
          commitHistory: false,
          emitChange: false,
        });
      }
      assemblyMoveToolPreviousCursor = canvas?.style.cursor ?? '';
      assemblyMoveToolActive = true;
      attachAssemblyMoveToolDomListeners();
      try {
        if (canvas) {
          canvas.style.cursor = 'grab';
          canvas.dataset.asukaAssemblyMoveTool = 'active';
        }
      } catch {}
    } else {
      assemblyMoveToolActive = false;
      detachAssemblyMoveToolDomListeners();
      try {
        if (canvas) {
          canvas.style.cursor = assemblyMoveToolPreviousCursor;
          delete canvas.dataset.asukaAssemblyMoveTool;
        }
      } catch {}
      assemblyMoveToolPreviousCursor = '';
    }

    const interaction = getAssemblyInteractionStateInternal();
    emitChanged('assemblyMoveTool:change', null, { commitHistory: false });
    return interaction;
  };

  const setAssemblyConstructionModeInternal = (active: boolean): AsukaAssemblyInteractionState => {
    const nextActive = active === true;
    if (nextActive) assertAssemblyEditPermission('setConstructionMode');
    if (nextActive === assemblyConstructionModeActive) return getAssemblyInteractionStateInternal();

    if (!nextActive) {
      assemblyConstructionModeActive = false;
      if (assemblyBuildState) cancelAssemblyDrawingInternal();
      if (assemblyMoveToolActive) setAssemblyMoveToolInternal(false);
      const interaction = getAssemblyInteractionStateInternal();
      emitChanged('assemblyConstructionMode:change', null, { commitHistory: false });
      return interaction;
    }

    assemblyConstructionModeActive = true;
    if (!isAssemblyModeEnabled()) {
      configureAssemblyInternal({ enabled: true }, {
        source: 'assembly.construction-mode.enable',
        commitHistory: false,
        emitChange: false,
      });
    }
    for (const component of listAssemblyComponents()) {
      if (component.definition?.placement?.support !== 'floor') continue;
      placeAssemblyComponentInternal(component.id, {
        position: clonePlain(component.transform.position),
        rotation: clonePlain(component.transform.rotation),
        snap: false,
        snapSupport: true,
        allowInvalid: false,
      }, {
        source: 'assembly.construction-mode.floor-support',
        commitHistory: false,
        emitChange: false,
      });
    }
    renderAssemblyEnvironment();
    setAssemblyMoveToolInternal(true);
    const interaction = getAssemblyInteractionStateInternal();
    emitChanged('assemblyConstructionMode:change', null, { commitHistory: false });
    return interaction;
  };

  const attachAssemblyBuildDomListeners = (): void => {
    if (!canvas || assemblyBuildDomCleanup) return;
    const handleDown = (event: PointerEvent) => handleAssemblyBuildPointer(PointerEventTypes.POINTERDOWN, event);
    const handleMove = (event: PointerEvent) => handleAssemblyBuildPointer(PointerEventTypes.POINTERMOVE, event);
    const handleUp = (event: PointerEvent) => handleAssemblyBuildPointer(PointerEventTypes.POINTERUP, event);
    const handleCancel = (event: PointerEvent) => cancelAssemblyBuildPointer(event);
    const handleMouseDown = (event: MouseEvent) => {
      if (assemblyBuildState?.dragging || assemblyBuildState?.pointerId != null) return;
      handleAssemblyBuildPointer(PointerEventTypes.POINTERDOWN, event);
    };
    const handleMouseMove = (event: MouseEvent) => {
      if (!assemblyBuildState?.dragging || assemblyBuildState?.pointerId != null) return;
      handleAssemblyBuildPointer(PointerEventTypes.POINTERMOVE, event);
    };
    const handleMouseUp = (event: MouseEvent) => {
      if (!assemblyBuildState?.dragging || assemblyBuildState?.pointerId != null) return;
      handleAssemblyBuildPointer(PointerEventTypes.POINTERUP, event);
    };
    const handleKey = (event: KeyboardEvent) => {
      const polygonTool = assemblyBuildState?.tool === 'polygon-floor'
        || assemblyBuildState?.tool === 'polygon-room';
      if (
        polygonTool
        && event.key === 'Enter'
        && (assemblyBuildState?.points.length ?? 0) >= 3
      ) {
        stopAssemblyBuildDomEvent(event);
        commitAssemblyPolygonGesture(assemblyBuildState!.points);
        clearAssemblyBuildPreview();
        return;
      }
      if (polygonTool && event.key === 'Backspace') {
        stopAssemblyBuildDomEvent(event);
        assemblyBuildState!.points = assemblyBuildState!.points.slice(0, -1);
        assemblyBuildState!.current = assemblyBuildState!.points[
          assemblyBuildState!.points.length - 1
        ]?.clone?.() ?? null;
        renderAssemblyBuildPreview();
        setAssemblyBuildDomDiagnostic('polygon-point-remove', assemblyBuildState!.current);
        return;
      }
      if (event.key !== 'Escape') return;
      stopAssemblyBuildDomEvent(event);
      if (assemblySurfaceGizmoDragState) {
        finishAssemblySurfaceGizmoDrag(null, true);
        return;
      }
      if (assemblySurfacePointDragState) {
        finishAssemblyFloorPointDrag(null, true);
        return;
      }
      if (assemblyComponentDragState) {
        finishAssemblyComponentDrag(null, true);
        return;
      }
      if (polygonTool && (assemblyBuildState?.points.length ?? 0) > 0) {
        assemblyBuildState!.points = [];
        assemblyBuildState!.current = null;
        clearAssemblyBuildPreview();
        setAssemblyBuildDomDiagnostic('polygon-draft-clear', null);
        return;
      }
      cancelAssemblyDrawingInternal();
      if (assemblyConstructionModeActive) setAssemblyMoveToolInternal(true);
    };
    try {
      assemblyBuildPreviousCursor = canvas.style.cursor || '';
      assemblyBuildPreviousTouchAction = canvas.style.touchAction || '';
      canvas.style.cursor = assemblyBuildState?.tool === 'erase' ? 'not-allowed' : 'crosshair';
      canvas.style.touchAction = 'none';
    } catch {}
    canvas.addEventListener('pointerdown', handleDown, { passive: false, capture: true });
    canvas.addEventListener('pointermove', handleMove, { passive: false, capture: true });
    canvas.addEventListener('pointerup', handleUp, { passive: false, capture: true });
    canvas.addEventListener('pointercancel', handleCancel, { passive: false, capture: true });
    canvas.addEventListener('mousedown', handleMouseDown, { passive: false, capture: true });
    canvas.addEventListener('mousemove', handleMouseMove, { passive: false, capture: true });
    canvas.addEventListener('mouseup', handleMouseUp, { passive: false, capture: true });
    window.addEventListener('pointermove', handleMove, { passive: false, capture: true });
    window.addEventListener('pointerup', handleUp, { passive: false, capture: true });
    window.addEventListener('pointercancel', handleCancel, { passive: false, capture: true });
    window.addEventListener('mousemove', handleMouseMove, { passive: false, capture: true });
    window.addEventListener('mouseup', handleMouseUp, { passive: false, capture: true });
    window.addEventListener('keydown', handleKey, { capture: true });
    assemblyBuildDomCleanup = () => {
      try { canvas?.removeEventListener('pointerdown', handleDown, { capture: true } as any); } catch {}
      try { canvas?.removeEventListener('pointermove', handleMove, { capture: true } as any); } catch {}
      try { canvas?.removeEventListener('pointerup', handleUp, { capture: true } as any); } catch {}
      try { canvas?.removeEventListener('pointercancel', handleCancel, { capture: true } as any); } catch {}
      try { canvas?.removeEventListener('mousedown', handleMouseDown, { capture: true } as any); } catch {}
      try { canvas?.removeEventListener('mousemove', handleMouseMove, { capture: true } as any); } catch {}
      try { canvas?.removeEventListener('mouseup', handleMouseUp, { capture: true } as any); } catch {}
      try { window.removeEventListener('pointermove', handleMove, { capture: true } as any); } catch {}
      try { window.removeEventListener('pointerup', handleUp, { capture: true } as any); } catch {}
      try { window.removeEventListener('pointercancel', handleCancel, { capture: true } as any); } catch {}
      try { window.removeEventListener('mousemove', handleMouseMove, { capture: true } as any); } catch {}
      try { window.removeEventListener('mouseup', handleMouseUp, { capture: true } as any); } catch {}
      try { window.removeEventListener('keydown', handleKey, { capture: true } as any); } catch {}
    };
  };

  const startAssemblyDrawingInternal = (input: AsukaAssemblyStartDrawingInput): void => {
    assertAssemblyEditPermission('startDrawing');
    const tool = input?.tool;
    if (![
      'select',
      'room',
      'floor',
      'polygon-room',
      'polygon-floor',
      'wall',
      'erase',
    ].includes(String(tool ?? ''))) return;
    assemblyConstructionModeActive = true;
    if (assemblyMoveToolActive) setAssemblyMoveToolInternal(false);
    if (printAreaDrawState) cancelPrintAreaDrawingInternal();
    if (printAreaAddPickState) cancelPrintAreaAddPickInternal();
    if (printSurfaceFacePickState) cancelPrintSurfaceFacePickingInternal();
    if (assemblyBuildState) cancelAssemblyDrawingInternal();
    assemblyBuildState = {
      input,
      tool,
      pointerId: null,
      start: null,
      current: null,
      points: [],
      dragging: false,
      replaceOnFirstCommit: input.replace === true,
    };
    configureAssemblyInternal({ enabled: true }, {
      source: 'assembly.build.enable',
      commitHistory: false,
      emitChange: false,
    });
    renderAssemblyEnvironment();
    attachAssemblyBuildDomListeners();
    applyAssemblyBuildCameraControls();
    setAssemblyBuildDomDiagnostic('start', null);
    emitChanged('assemblyBuild:start', tool, { commitHistory: false });
  };

  const createPrintAreaFromView = (input: {
    id?: string;
    label?: string;
    targetMeshId?: string;
    mode?: string;
    width?: number;
    height?: number;
    textureSlot?: string;
    canvasSize?: { width?: number; height?: number };
    worldBounds?: { center?: number[]; size?: number[] };
    placement?: { position?: number[]; scale?: number[] };
    metadata?: Record<string, unknown>;
  } = {}): Asuka3DPrintArea | null => {
    const declaredPrintSurfaceInput = (input as any)?.metadata?.printSurface3D ?? (input as any)?.printSurface3D ?? null;
    const declaredPrintSurfaceType = String(declaredPrintSurfaceInput?.type ?? declaredPrintSurfaceInput?.unwrapMode ?? '').trim().toLowerCase();
    const declaredBoxPrimaryFace = normalizeDeclaredBoxFaceId(declaredPrintSurfaceInput?.primaryFace ?? declaredPrintSurfaceInput?.seedFace ?? declaredPrintSurfaceInput?.face, 'front');
    const isDeclaredBoxPrintSurface = ['face-net', 'box-unwrap', 'box', 'box-wrap'].includes(declaredPrintSurfaceType);
    const mode = isDeclaredBoxPrintSurface || String(input.mode ?? '').trim().toLowerCase() === 'surface' ? 'surface' : 'texture';
    const coordinateFrame = get3DPrintAreaCoordinateFrame(mode);
    assertCompatibleCoordinateFrame(input as any, coordinateFrame, {
      engine: '3d',
      operation: 'areas.create',
    });
    let declaredTriangleRefs = normalizeFaceNetTriangleRefs((declaredPrintSurfaceInput as any)?.selectedTriangles ?? (declaredPrintSurfaceInput as any)?.triangles ?? (declaredPrintSurfaceInput as any)?.faceNetTriangles, String(input.targetMeshId ?? '').trim());
    const declaredTargetMeshId = String(declaredTriangleRefs[0]?.meshId ?? input.targetMeshId ?? 'product').trim() || 'product';
    const targetMesh = findModelMesh(declaredTargetMeshId) ?? findModelMesh(null);
    const surface = isDeclaredBoxPrintSurface && declaredTriangleRefs.length ? null : pickVisibleModelSurface(targetMesh);
    if (!surface && !targetMesh) return null;
    const resolvedTarget = surface?.targetMesh ?? targetMesh;
    if (!resolvedTarget) return null;
    let autoPrimaryTriangleIndex: number | undefined;
    if (isDeclaredBoxPrintSurface && !declaredTriangleRefs.length) {
      const declaredFaces = Array.from(new Set(
        (Array.isArray((declaredPrintSurfaceInput as any)?.faces) && (declaredPrintSurfaceInput as any).faces.length
          ? (declaredPrintSurfaceInput as any).faces
          : [declaredBoxPrimaryFace]
        ).map((face: any) => normalizeDeclaredBoxFaceId(face, declaredBoxPrimaryFace)),
      ));
      if (!declaredFaces.includes(declaredBoxPrimaryFace)) declaredFaces.unshift(declaredBoxPrimaryFace);
      const geometry = getMeshFaceNetGeometry(resolvedTarget);
      const meshId = getFaceNetMeshId(resolvedTarget);
      const refs: FaceNetTriangleRef[] = [];
      if (geometry) {
        const localPoints: Vector3[] = [];
        for (let vertexIndex = 0; vertexIndex < geometry.vertexCount; vertexIndex += 1) {
          const point = readFaceNetLocalVertex(geometry, vertexIndex);
          if (point) localPoints.push(point);
        }
        const localCenter = localPoints.length
          ? localPoints.reduce((acc, point) => acc.add(point), Vector3.Zero()).scale(1 / localPoints.length)
          : Vector3.Zero();
        const faceSet = new Set(declaredFaces);
        const faceCounts: Record<string, number> = {};
        for (let triangleIndex = 0; triangleIndex + 2 < geometry.indices.length / 3; triangleIndex += 1) {
          const base = triangleIndex * 3;
          const a = readFaceNetLocalVertex(geometry, geometry.indices[base]);
          const b = readFaceNetLocalVertex(geometry, geometry.indices[base + 1]);
          const c = readFaceNetLocalVertex(geometry, geometry.indices[base + 2]);
          if (!a || !b || !c) continue;
          let normalLocal = Vector3.Cross(b.subtract(a), c.subtract(a));
          if (normalLocal.lengthSquared() < 0.0000001) continue;
          normalLocal.normalize();
          const centroid = a.add(b).add(c).scale(1 / 3);
          const outward = centroid.subtract(localCenter);
          if (outward.lengthSquared() > 0.0000001 && Vector3.Dot(normalLocal, outward) < 0) normalLocal = normalLocal.scale(-1);
          const face = getDeclaredBoxFaceIdFromNormal(normalLocal);
          faceCounts[face] = (faceCounts[face] ?? 0) + 1;
          if (!faceSet.has(face)) continue;
          refs.push({ meshId, triangleIndex });
          if (autoPrimaryTriangleIndex == null && face === declaredBoxPrimaryFace) autoPrimaryTriangleIndex = triangleIndex;
        }
        if (autoPrimaryTriangleIndex == null && refs.length) autoPrimaryTriangleIndex = refs[0].triangleIndex;
        declaredTriangleRefs = refs;
        debug3DLayer('face-net:auto-selected-triangles', {
          targetMeshId: meshId,
          requestedFaces: declaredFaces,
          primaryFace: declaredBoxPrimaryFace,
          selectedTriangles: refs,
          primaryTriangleIndex: autoPrimaryTriangleIndex ?? null,
          faceCounts,
          vertices: geometry.vertexCount,
          indices: geometry.indices.length,
        }, refs.length ? 'info' : 'warn');
      } else {
        debug3DLayer('face-net:auto-selected-triangles', {
          targetMeshId: meshId,
          requestedFaces: declaredFaces,
          primaryFace: declaredBoxPrimaryFace,
          selectedTriangles: [],
          reason: 'no-geometry',
        }, 'warn');
      }
    }
    const exactPrimaryTriangleIndex = Number((declaredPrintSurfaceInput as any)?.primaryTriangleIndex ?? autoPrimaryTriangleIndex ?? declaredTriangleRefs[0]?.triangleIndex);
    const exactRuntime = isDeclaredBoxPrintSurface && declaredTriangleRefs.length
      ? buildExactFaceNetRuntime(resolvedTarget, declaredTriangleRefs, exactPrimaryTriangleIndex)
      : null;
    const exactWorldPoints = exactRuntime?.triangles?.flatMap((triangle) => triangle.world) ?? [];
    const exactPrimaryTriangle = exactRuntime?.triangles?.find((triangle) => triangle.triangleIndex === Math.floor(exactPrimaryTriangleIndex)) ?? exactRuntime?.triangles?.[0] ?? null;
    const exactNormal = exactPrimaryTriangle?.normalWorld?.clone?.() ?? null;
    if (exactNormal && exactNormal.lengthSquared() > 0.0001) exactNormal.normalize();
    const exactBounds = exactWorldPoints.length ? (() => {
      const xs = exactWorldPoints.map((point) => point.x);
      const ys = exactWorldPoints.map((point) => point.y);
      const zs = exactWorldPoints.map((point) => point.z);
      const min = new Vector3(Math.min(...xs), Math.min(...ys), Math.min(...zs));
      const max = new Vector3(Math.max(...xs), Math.max(...ys), Math.max(...zs));
      return { min, max, center: min.add(max).scale(0.5), size: max.subtract(min) };
    })() : null;
    const bounds = exactBounds ?? getMeshWorldBounds(resolvedTarget);
    const basis = getViewTransformBasis();
    const modelSize = bounds.size;
    const projectedWidth = Math.abs(basis.right.x) * Math.max(modelSize.x, 0.01)
      + Math.abs(basis.right.y) * Math.max(modelSize.y, 0.01)
      + Math.abs(basis.right.z) * Math.max(modelSize.z, 0.01);
    const projectedHeight = Math.abs(basis.up.x) * Math.max(modelSize.x, 0.01)
      + Math.abs(basis.up.y) * Math.max(modelSize.y, 0.01)
      + Math.abs(basis.up.z) * Math.max(modelSize.z, 0.01);
    const exactNetWidth = exactRuntime ? Math.max(0.08, Math.abs(exactRuntime.bounds.maxU - exactRuntime.bounds.minU)) : 0;
    const exactNetHeight = exactRuntime ? Math.max(0.08, Math.abs(exactRuntime.bounds.maxV - exactRuntime.bounds.minV)) : 0;
    const requestedCreatedFrom = String(input.metadata?.createdFrom ?? '').trim();
    const isAuthoredSurfaceDraw = !isDeclaredBoxPrintSurface && requestedCreatedFrom === 'surface-draw';
    const authoredSizeRaw = isAuthoredSurfaceDraw
      ? (input.worldBounds?.size ?? input.placement?.scale)
      : null;
    const authoredSize = Array.isArray(authoredSizeRaw) ? toVec3(authoredSizeRaw, [0, 0, 0]) : null;
    const width = Math.max(0.08, Number(input.width ?? 0) || Number(authoredSize?.x ?? 0) || exactNetWidth || projectedWidth * 0.46 || 0.9);
    const height = Math.max(0.08, Number(input.height ?? 0) || Number(authoredSize?.y ?? 0) || exactNetHeight || projectedHeight * 0.46 || 0.55);
    const depth = Math.max(0.035, Number(authoredSize?.z ?? 0) || Math.min(Math.max(width, height) * 0.32, Math.min(width, height) * 0.65));
    const declaredBoxNormal = isDeclaredBoxPrintSurface
      ? (declaredBoxPrimaryFace === 'front' ? new Vector3(0, 0, 1)
        : declaredBoxPrimaryFace === 'back' ? new Vector3(0, 0, -1)
        : declaredBoxPrimaryFace === 'right' ? new Vector3(1, 0, 0)
        : declaredBoxPrimaryFace === 'left' ? new Vector3(-1, 0, 0)
        : declaredBoxPrimaryFace === 'top' ? new Vector3(0, 1, 0)
        : new Vector3(0, -1, 0))
      : null;
    const authoredNormalRaw = isAuthoredSurfaceDraw ? input.metadata?.surfaceNormal : null;
    const authoredNormal = Array.isArray(authoredNormalRaw) ? toVec3(authoredNormalRaw, [0, 0, 1]) : null;
    const normal = exactNormal ?? declaredBoxNormal ?? authoredNormal ?? surface?.normal ?? getViewTransformBasis().forward.scale(-1);
    if (normal.lengthSquared() < 0.0001) normal.copyFromFloats(0, 0, 1);
    normal.normalize();
    const declaredBoxCenter = isDeclaredBoxPrintSurface
      ? new Vector3(
        declaredBoxPrimaryFace === 'right' ? bounds.max.x : (declaredBoxPrimaryFace === 'left' ? bounds.min.x : bounds.center.x),
        declaredBoxPrimaryFace === 'top' ? bounds.max.y : (declaredBoxPrimaryFace === 'bottom' ? bounds.min.y : bounds.center.y),
        declaredBoxPrimaryFace === 'front' ? bounds.max.z : (declaredBoxPrimaryFace === 'back' ? bounds.min.z : bounds.center.z),
      )
      : null;
    const authoredCenterRaw = isAuthoredSurfaceDraw
      ? (input.worldBounds?.center ?? input.placement?.position)
      : null;
    const authoredCenter = Array.isArray(authoredCenterRaw) ? toVec3(authoredCenterRaw, fromVec3(bounds.center)) : null;
    const center = (exactPrimaryTriangle ? exactPrimaryTriangle.centroidWorld.add(normal.scale(0.006)) : null) ?? declaredBoxCenter ?? authoredCenter ?? surface?.position ?? bounds.center.add(normal.scale(Math.max(0.01, depth * 0.18)));
    const id = String(input.id ?? makeId('print-area')).trim();
    const uv = surface?.uv ?? null;
    const uvBounds = uv ? makeUvBoundsFromCenter(uv, 0.46, 0.46) : null;
    const canvasSize = normalize3DPrintAreaCanvasSize((input as any).canvasSize);
    const area = {
      id,
      label: String(input.label ?? `Print area ${getPrintAreas().length + 1}`).trim() || `Print area ${getPrintAreas().length + 1}`,
      targetMeshId: String(declaredTriangleRefs[0]?.meshId || resolvedTarget.id || resolvedTarget.name || 'product'),
      mode,
      ...coordinateFrame,
      textureSlot: isDeclaredBoxPrintSurface ? undefined : (String((input as any).textureSlot ?? 'diffuseTexture').trim() || 'diffuseTexture'),
      canvasSize,
      ...((input as any).physicalSize && typeof (input as any).physicalSize === 'object' ? { physicalSize: clonePlain((input as any).physicalSize) } : null),
      ...(uvBounds ? { uvBounds } : {}),
      objects: [],
      visible: true,
      locked: false,
      worldBounds: { center: fromVec3(center), size: [width, height, depth] },
      placement: { position: fromVec3(center), scale: [width, height, depth] },
      metadata: {
        ...(input.metadata ?? {}),
        surfaceNormal: fromVec3(normal),
        ...(uv ? { visibleUvCenter: uv, visibleUvBounds: uvBounds, uvPlacementSource: 'center-screen-pick' } : {}),
        ...(isDeclaredBoxPrintSurface ? { printSurface3D: { ...(declaredPrintSurfaceInput ?? {}), type: 'face-net', primaryFace: declaredBoxPrimaryFace, targetMeshId: String(declaredTriangleRefs[0]?.meshId || resolvedTarget.id || resolvedTarget.name || 'product'), selectedTriangles: declaredTriangleRefs, primaryTriangleIndex: Number.isFinite(exactPrimaryTriangleIndex) ? Math.floor(exactPrimaryTriangleIndex) : undefined } } : null),
        ...(exactRuntime ? { faceNetRuntime: { triangles: exactRuntime.triangles.length, disconnected: exactRuntime.disconnected, bounds: exactRuntime.bounds } } : null),
        createdFrom: isDeclaredBoxPrintSurface ? 'asuka-print-surface-studio' : (requestedCreatedFrom || 'visible-texture-area'),
      },
    } as Asuka3DPrintArea;
    if (isDeclaredBoxPrintSurface) {
      debug3DLayer('face-net:area-created', {
        id,
        targetMeshId: area.targetMeshId,
        selectedTriangles: declaredTriangleRefs,
        exactRuntime: exactRuntime ? { triangles: exactRuntime.triangles.length, disconnected: exactRuntime.disconnected, bounds: exactRuntime.bounds } : null,
        center: fromVec3(center),
        size: [width, height, depth],
        normal: fromVec3(normal),
      }, exactRuntime ? 'info' : 'warn');
    }
    setPrintAreasState([...getExplicitPrintAreas(), area], id, 'addPrintArea');
    return clonePlain(area);
  };

  const createPrintAreaInternal = (input: any = {}): Asuka3DPrintArea | null => createPrintAreaFromView(input ?? {});

  const updatePrintAreaInternal = (id: string, patch: any = {}): Asuka3DPrintArea | null => {
    const key = String(id ?? '').trim();
    if (!key) return null;
    const current = getPrintAreaById(key);
    if (!current) return null;
    const mode = String(patch?.mode ?? current.mode ?? 'texture').trim().toLowerCase() === 'surface' ? 'surface' : 'texture';
    const coordinateFrame = get3DPrintAreaCoordinateFrame(mode);
    assertCompatibleCoordinateFrame(patch, coordinateFrame, {
      engine: '3d',
      operation: 'areas.update',
      details: { printAreaId: key },
    });
    const merged = {
      ...(current as any),
      ...(patch && typeof patch === 'object' ? patch : {}),
      id: key,
      mode,
      ...coordinateFrame,
      metadata: {
        ...(((current as any)?.metadata && typeof (current as any).metadata === 'object') ? (current as any).metadata : {}),
        ...(((patch as any)?.metadata && typeof (patch as any).metadata === 'object') ? (patch as any).metadata : {}),
      },
    } as Asuka3DPrintArea;
    const explicit = getExplicitPrintAreas().filter((area) => String(area?.id ?? '').trim() !== key);
    setPrintAreasState([...explicit, merged], key, 'updatePrintArea');
    return clonePlain(merged);
  };

  const focusPrintAreaInternal = (id: string) => {
    const area = getPrintAreaById(id);
    if (!area) return;
    const targetMesh = findModelMesh(String((area as any)?.targetMeshId ?? '').trim() || 'product') ?? findModelMesh(null);
    const bounds = targetMesh ? getMeshWorldBounds(targetMesh) : null;
    const rawCenter = (area as any)?.worldBounds?.center ?? (area as any)?.placement?.position;
    const center = Array.isArray(rawCenter) ? toVec3(rawCenter, bounds ? fromVec3(bounds.center) : [0, 0, 0]) : (bounds?.center ?? Vector3.Zero());
    let normal = getPrintAreaNormal(area);
    if (!normal) {
      try { normal = camera ? camera.position.subtract(center).normalize() : new Vector3(0, 0, 1); } catch { normal = new Vector3(0, 0, 1); }
    }
    if (!normal || normal.lengthSquared() < 0.0001) normal = new Vector3(0, 0, 1);
    const size = Array.isArray((area as any)?.worldBounds?.size) ? toVec3((area as any).worldBounds.size, [1, 1, 0.1]) : (bounds?.size ?? new Vector3(1, 1, 1));
    const distance = Math.max(0.8, Math.min(5, size.length() * 1.65));
    animateCameraTo(center, center.add(normal.scale(distance)), true);
    selectPrintAreaInternal(String(area.id ?? id));
    emitChanged('focusPrintArea', area.id ?? id);
  };

  const removePrintAreaInternal = (id: string) => {
    const key = String(id ?? '').trim();
    if (!key) return;
    const nextAreas = getExplicitPrintAreas().filter((area) => String(area?.id ?? '') !== key);
    const nextActive = getActivePrintAreaId() === key ? null : getActivePrintAreaId();
    decorationRecords = decorationRecords.map((deco) => String((deco as any)?.targetPrintAreaId ?? '') === key
      ? { ...deco, targetPrintAreaId: undefined, metadata: { ...((deco as any).metadata ?? {}), printAreaId: undefined } }
      : deco);
    setPrintAreasState(nextAreas, nextActive, 'removePrintArea');
  };


  const restorePrintSurfaceFacePickDom = () => {
    if (!canvas) return;
    try { canvas.style.cursor = printSurfaceFacePickPreviousCursor; } catch {}
    try { canvas.style.touchAction = printSurfaceFacePickPreviousTouchAction; } catch {}
  };

  /**
   * Classify a mesh as 'box' (simple geometry with axis-aligned flat faces)
   * or 'complex' (organic/curved — shoe, shirt, etc.).
   * Used by Drawing Areas UI to auto-select surface mode and hide
   * irrelevant controls (Front/Left/Right dropdowns) for complex shapes.
   */
  const classifyMeshForPrintArea = (meshId: string): {
    type: 'box' | 'complex';
    triangleCount: number;
    distinctFaceCount: number;
    detectedFaces: DeclaredBoxFaceId[];
    recommendedSurfaceMode: 'face-net' | 'surface-patch';
    isAxisAligned: boolean;
    isComplex: boolean;
  } => {
    const FAIL = { type: 'complex' as const, triangleCount: 0, distinctFaceCount: 0,
      detectedFaces: [] as DeclaredBoxFaceId[], recommendedSurfaceMode: 'surface-patch' as const,
      isAxisAligned: false, isComplex: true };
    // findModelMesh tries id, name, scene lookup, instanced-part name — more
    // reliable than a bare scene.getMeshById for generic IDs like 'product'.
    const mesh = findModelMesh(meshId);
    const geometry = mesh ? getMeshFaceNetGeometry(mesh as Mesh) : null;
    if (!geometry) return FAIL;
    const { indices, positions } = geometry;
    const triangleCount = Math.floor(indices.length / 3);
    if (triangleCount === 0) return FAIL;
    // Large meshes are always complex — skip expensive axis-alignment scan.
    if (triangleCount > 500) return { ...FAIL, triangleCount };
    // Small mesh: verify all face normals align to a canonical box axis (±X/Y/Z).
    const THRESHOLD = 0.92; // cos ≈ 23°
    const faceSet = new Set<DeclaredBoxFaceId>();
    let isAxisAligned = true;
    const wm = geometry.world.m;
    for (let f = 0; f < triangleCount; f++) {
      const ia = indices[f * 3], ib = indices[f * 3 + 1], ic = indices[f * 3 + 2];
      const ax = positions[ia * 3], ay = positions[ia * 3 + 1], az = positions[ia * 3 + 2];
      const bx = positions[ib * 3], by = positions[ib * 3 + 1], bz = positions[ib * 3 + 2];
      const cx = positions[ic * 3], cy = positions[ic * 3 + 1], cz = positions[ic * 3 + 2];
      const ex = bx - ax, ey = by - ay, ez = bz - az;
      const fx2 = cx - ax, fy = cy - ay, fz = cz - az;
      const nx = ey * fz - ez * fy, ny = ez * fx2 - ex * fz, nz = ex * fy - ey * fx2;
      const ln = Math.sqrt(nx * nx + ny * ny + nz * nz);
      if (ln < 1e-6) continue;
      const nnx = nx / ln, nny = ny / ln, nnz = nz / ln;
      // Rotate local normal to world space (upper-left 3×3 of world matrix).
      let wnx = nnx * wm[0] + nny * wm[1] + nnz * wm[2];
      let wny = nnx * wm[4] + nny * wm[5] + nnz * wm[6];
      let wnz = nnx * wm[8] + nny * wm[9] + nnz * wm[10];
      const wl = Math.sqrt(wnx * wnx + wny * wny + wnz * wnz);
      if (wl > 1e-6) { wnx /= wl; wny /= wl; wnz /= wl; }
      const maxComp = Math.max(Math.abs(wnx), Math.abs(wny), Math.abs(wnz));
      if (maxComp < THRESHOLD) { isAxisAligned = false; }
      faceSet.add(getDeclaredBoxFaceIdFromNormal(new Vector3(wnx, wny, wnz)));
    }
    const isBox = isAxisAligned;
    return {
      type: isBox ? 'box' : 'complex',
      triangleCount,
      distinctFaceCount: faceSet.size,
      detectedFaces: Array.from(faceSet) as DeclaredBoxFaceId[],
      recommendedSurfaceMode: isBox ? 'face-net' : 'surface-patch',
      isAxisAligned,
      isComplex: !isBox,
    };
  };

  const cancelPrintSurfaceFacePickingInternal = () => {
    const state = printSurfaceFacePickState;
    printSurfaceFacePickState = null;
    restorePrintSurfaceFacePickDom();
    try { state?.onCancel?.(); } catch {}
  };

  const startPrintSurfaceFacePickingInternal = (input: {
    targetMeshId?: string;
    primaryFace?: string;
    faces?: string[];
    selectedTriangles?: FaceNetTriangleRef[];
    primaryTriangleIndex?: number;
    onChange?: (selection: { pickedFace: string; primaryFace: string; faces: string[]; meshId?: string; point?: [number, number, number]; normal?: [number, number, number]; faceId?: number | null; pickedTriangleIndex?: number | null; primaryTriangleIndex?: number | null; selectedTriangles?: FaceNetTriangleRef[] }) => void;
    onCancel?: () => void;
  } = {}) => {
    if (!scene || !canvas) return;
    if (printAreaAddPickState) cancelPrintAreaAddPickInternal();
    if (printAreaDrawState) cancelPrintAreaDrawingInternal();
    if (printSurfaceFacePickState) cancelPrintSurfaceFacePickingInternal();
    const primaryFace = normalizeDeclaredBoxFaceId(input.primaryFace, 'front');
    const faces = Array.from(new Set((Array.isArray(input.faces) ? input.faces : [primaryFace]).map((item) => normalizeDeclaredBoxFaceId(item, primaryFace))));
    if (!faces.includes(primaryFace)) faces.unshift(primaryFace);
    const selectedTriangles = normalizeFaceNetTriangleRefs((input as any).selectedTriangles, String(input.targetMeshId ?? '').trim() || '');
    const primaryTriangleIndex = Number((input as any).primaryTriangleIndex ?? selectedTriangles[0]?.triangleIndex);
    printSurfaceFacePickState = {
      targetMeshId: String(input.targetMeshId ?? '').trim() || undefined,
      primaryFace,
      faces,
      selectedTriangles,
      primaryTriangleIndex: Number.isFinite(primaryTriangleIndex) ? Math.floor(primaryTriangleIndex) : undefined,
      onChange: input.onChange,
      onCancel: input.onCancel,
    };
    try { printSurfaceFacePickPreviousCursor = canvas.style.cursor || ''; canvas.style.cursor = 'crosshair'; } catch {}
    try { printSurfaceFacePickPreviousTouchAction = canvas.style.touchAction || ''; canvas.style.touchAction = 'none'; } catch {}
    debug3DLayer('print-surface3d:face-pick-start', { targetMeshId: printSurfaceFacePickState.targetMeshId, primaryFace, faces, selectedTriangles });
  };

  const handlePrintSurfaceFacePickingPointer = (type: number, event?: PointerEvent | MouseEvent | null): boolean => {
    const state = printSurfaceFacePickState;
    if (!state) return false;
    if (type === PointerEventTypes.POINTERMOVE) {
      try { event?.preventDefault?.(); } catch {}
      try { event?.stopPropagation?.(); } catch {}
      return true;
    }
    if (type === PointerEventTypes.POINTERDOWN) {
      try { event?.preventDefault?.(); } catch {}
      try { event?.stopPropagation?.(); } catch {}
      return true;
    }
    if (type !== PointerEventTypes.POINTERUP) return true;
    const rawButton = Number((event as any)?.button);
    const isLeftPointerUp = !Number.isFinite(rawButton) || rawButton === 0;
    if (!isLeftPointerUp) {
      cancelPrintSurfaceFacePickingInternal();
      return true;
    }
    const preferredTarget = findModelMesh(String(state.targetMeshId ?? '').trim() || 'product') ?? null;
    const surface = pickPointerModelSurface(event ?? null, preferredTarget);
    if (!surface) {
      try { event?.preventDefault?.(); } catch {}
      try { event?.stopPropagation?.(); } catch {}
      return true;
    }
    const pickedLocalNormal = (() => {
      try {
        const world = surface.targetMesh?.getWorldMatrix?.() ?? Matrix.Identity();
        const inverseWorld = world.clone();
        inverseWorld.invert();
        const localNormal = Vector3.TransformNormal(surface.normal, inverseWorld);
        if (localNormal.lengthSquared() > 0.0001) return localNormal.normalize();
      } catch {}
      return surface.normal;
    })();
    const pickedTriangleIndex = Number((surface as any).faceId);
    const meshId = String(surface.targetMesh?.id || surface.targetMesh?.name || '').trim() || undefined;
    // Detect complex meshes (organic shapes, GLBs) where box-face labels are
    // meaningless. A simple heuristic: if the mesh has more than ~500 triangles,
    // treat it as complex. For complex meshes:
    //   • use a single generic 'area' face name instead of 'front'/'left'/etc.
    //   • select individual triangles, not the full coplanar component
    const meshGeomForComplexCheck = getMeshFaceNetGeometry(surface.targetMesh);
    const meshTriangleCount = meshGeomForComplexCheck ? Math.floor((meshGeomForComplexCheck.indices?.length ?? 0) / 3) : 0;
    const isComplexMesh = meshTriangleCount > 500;
    const pickedFace: DeclaredBoxFaceId = isComplexMesh ? 'front' : getDeclaredBoxFaceIdFromNormal(pickedLocalNormal);
    const pickedComponent: FaceNetTriangleRef[] = isComplexMesh
      ? (Number.isFinite(pickedTriangleIndex) ? [{ meshId: String(meshId ?? ''), triangleIndex: Math.floor(pickedTriangleIndex) }] : [])
      : (Number.isFinite(pickedTriangleIndex)
        ? getFaceNetTriangleComponentForPick(surface.targetMesh, Math.floor(pickedTriangleIndex), surface.normal)
        : []);
    const currentTriangles = new Map<string, FaceNetTriangleRef>();
    for (const ref of normalizeFaceNetTriangleRefs(state.selectedTriangles ?? [], meshId ?? '')) {
      currentTriangles.set(`${String(ref.meshId ?? meshId ?? '').trim()}:${ref.triangleIndex}`, ref);
    }
    const componentKeys = pickedComponent.map((ref) => `${String(ref.meshId ?? meshId ?? '').trim()}:${ref.triangleIndex}`);
    const componentIsSelected = componentKeys.length > 0 && componentKeys.every((key) => currentTriangles.has(key));
    if (componentKeys.length) {
      if (componentIsSelected && currentTriangles.size > componentKeys.length) {
        for (const key of componentKeys) currentTriangles.delete(key);
      } else {
        for (const ref of pickedComponent) currentTriangles.set(`${String(ref.meshId ?? meshId ?? '').trim()}:${ref.triangleIndex}`, ref);
      }
    }
    const selectedTriangles = Array.from(currentTriangles.values());
    const primaryTriangleIndex = Number.isFinite(Number(state.primaryTriangleIndex)) && selectedTriangles.some((ref) => ref.triangleIndex === Number(state.primaryTriangleIndex))
      ? Math.floor(Number(state.primaryTriangleIndex))
      : (selectedTriangles[0]?.triangleIndex ?? (Number.isFinite(pickedTriangleIndex) ? Math.floor(pickedTriangleIndex) : undefined));
    const current = new Set((state.faces ?? []).map((face) => normalizeDeclaredBoxFaceId(face, pickedFace)));
    const currentPrimary = normalizeDeclaredBoxFaceId(state.primaryFace, pickedFace);
    if (!current.has(pickedFace)) current.add(pickedFace);
    current.add(currentPrimary);
    const faces = Array.from(current);
    state.faces = faces;
    state.primaryFace = currentPrimary;
    state.selectedTriangles = selectedTriangles;
    state.primaryTriangleIndex = primaryTriangleIndex;
    // Classify the mesh so the UI can adapt its controls automatically
    // (hide face dropdowns for complex shapes, change surface-type label, etc.)
    const classification = classifyMeshForPrintArea(String(meshId ?? state.targetMeshId ?? ''));
    // For complex meshes, replace box face labels with a neutral 'area' value
    // so that any UI consuming `faces` / `primaryFace` doesn't show confusing
    // directional names like "Front", "Left", "Right", "Top", "Bottom", "Back".
    // For complex meshes use 'zone-1' (numbered) instead of directional labels.
    const reportedFace: DeclaredBoxFaceId = classification.isComplex ? ('zone-1' as any) : pickedFace;
    const reportedPrimary: DeclaredBoxFaceId = classification.isComplex ? ('zone-1' as any) : currentPrimary;
    const reportedFaces: string[] = classification.isComplex ? ['zone-1'] : faces;
    const result = {
      pickedFace: reportedFace,
      primaryFace: reportedPrimary,
      faces: reportedFaces,
      meshId,
      point: fromVec3((surface as any).rawPosition ?? surface.position),
      normal: fromVec3(surface.normal),
      faceId: Number.isFinite(pickedTriangleIndex) ? Math.floor(pickedTriangleIndex) : null,
      pickedTriangleIndex: Number.isFinite(pickedTriangleIndex) ? Math.floor(pickedTriangleIndex) : null,
      primaryTriangleIndex: Number.isFinite(Number(primaryTriangleIndex)) ? Math.floor(Number(primaryTriangleIndex)) : null,
      selectedTriangles,
      // ── New fields consumed by Drawing Areas UI ──
      meshType: classification.type,            // 'box' | 'complex'
      isComplexMesh: classification.type === 'complex', // shorthand flag
      recommendedSurfaceMode: classification.recommendedSurfaceMode, // 'face-net' | 'surface-patch'
      detectedFaces: classification.detectedFaces,  // auto-detected box faces (empty for complex)
      triangleCount: classification.triangleCount,
    };
    debug3DLayer('face-net:pick', result);
    try { state.onChange?.(clonePlain(result)); } catch {}
    try { event?.preventDefault?.(); } catch {}
    try { event?.stopPropagation?.(); } catch {}
    return true;
  };

  const begin3DPlacement = (objectId: string, printAreaId?: string | null, mode = 'surface') => (
    emit3DPlacement('placement.begin', placementController.begin({
      objectId,
      printAreaId: String(printAreaId ?? '').trim() || null,
      transform: get3DObjectTransform(objectId),
      source: 'pointer',
      metadata: { mode },
    }))
  );

  const update3DPlacement = (
    objectId: string,
    input: { valid: boolean; constrained?: boolean; reason?: string | null; printAreaId?: string | null; metadata?: Record<string, unknown> },
  ) => {
    const current = placementController.getStatus();
    if (!current || current.objectId !== objectId || ['committed', 'cancelled'].includes(current.phase)) return null;
    return emit3DPlacement('placement.update', placementController.update({
      transform: get3DObjectTransform(objectId),
      printAreaId: input.printAreaId,
      valid: input.valid,
      constrained: input.constrained,
      remember: input.valid,
      reason: input.reason,
      metadata: input.metadata,
    }));
  };

  const commit3DPlacement = (objectId: string) => {
    const current = placementController.getStatus();
    if (!current || current.objectId !== objectId || ['committed', 'cancelled'].includes(current.phase)) return current;
    return emit3DPlacement('placement.commit', placementController.commit());
  };

  const startTexturePrintAreaObjectDrag = (id: string, event?: PointerEvent | MouseEvent | null, pickedMesh?: any, initialHit?: ReturnType<typeof findTexturePrintAreaObjectAtPointer> | null): boolean => {
    const key = String(id ?? '').trim();
    const hit = initialHit ?? findTexturePrintAreaObjectAtPointer(event ?? null, pickedMesh);
    const fallback = hit && String(hit.object?.id ?? '') === key ? hit : null;
    const existing = fallback ? { area: fallback.area, object: fallback.object } : findTexturePrintAreaObject(key);
    if (!existing || existing.object?.locked === true || (existing.area as any)?.locked === true) return false;
    try { ensure3DHistoryBaseline('texture-drag:start', currentViewId); } catch {}
    const point = fallback?.point ?? getTexturePrintAreaPointerPoint(existing.area, event ?? null, pickedMesh);
    if (!point) return false;
    dragState = {
      id: key,
      mode: 'texture',
      pointerId: typeof (event as any)?.pointerId === 'number' ? Number((event as any).pointerId) : null,
      startClientX: Number((event as any)?.clientX ?? 0) || 0,
      startClientY: Number((event as any)?.clientY ?? 0) || 0,
      startPosition: Vector3.Zero(),
      startPointerWorld: null,
      dragPlane: null,
      moved: false,
      surfaceConforming: false,
      areaId: String(existing.area?.id ?? '').trim(),
      startTexturePoint: { x: point.x, y: point.y },
      startTextureObject: clonePlain(existing.object),
      startCameraState: clonePlain(readCameraState()),
      previousCursor: canvas?.style.cursor ?? '',
    };
    begin3DPlacement(key, existing.area.id, 'texture');
    try { if (canvas) canvas.style.cursor = 'grabbing'; } catch {}
    detachCameraForObjectDrag();
    try { if (dragState.pointerId !== null) (event?.target as any)?.setPointerCapture?.(dragState.pointerId); } catch {}
    startDragPerf(key, 'texture');
    try { event?.preventDefault?.(); } catch {}
    try { event?.stopPropagation?.(); } catch {}
    return true;
  };

  const updateTexturePrintAreaObjectDrag = (event?: PointerEvent | MouseEvent | null) => {
    const perfStart = false ? perfNow() : 0;
    if (!dragState || dragState.mode !== 'texture') return;
    const key = dragState.id;
    const existing = findTexturePrintAreaObject(key);
    if (!existing || existing.object?.locked === true || (existing.area as any)?.locked === true) return;
    const point = getTexturePrintAreaPointerPoint(existing.area, event ?? null);
    if (!point || !dragState.startTexturePoint) {
      try { if (canvas) canvas.style.cursor = 'not-allowed'; } catch {}
      update3DPlacement(key, {
        valid: false,
        constrained: true,
        reason: 'outside-area',
        printAreaId: existing.area.id,
      });
      try { event?.preventDefault?.(); } catch {}
      try { event?.stopPropagation?.(); } catch {}
      return;
    }
    const dx = point.x - dragState.startTexturePoint.x;
    const dy = point.y - dragState.startTexturePoint.y;
    const screenDx = (Number((event as any)?.clientX ?? dragState.startClientX) || 0) - dragState.startClientX;
    const screenDy = (Number((event as any)?.clientY ?? dragState.startClientY) || 0) - dragState.startClientY;
    if (Math.abs(screenDx) > 1 || Math.abs(screenDy) > 1 || Math.abs(dx) > 0.5 || Math.abs(dy) > 0.5) dragState.moved = true;
    const startObject = dragState.startTextureObject ?? existing.object;
    const updateStart = false ? perfNow() : 0;
    updateTexturePrintAreaObjects(existing.area.id, (objects) => objects.map((object) => {
      if (String(object?.id ?? '') !== key) return object;
      return {
        ...object,
        x: Number(startObject?.x ?? object?.x ?? 0) + dx,
        y: Number(startObject?.y ?? object?.y ?? 0) + dy,
      };
    }), { render: 'scheduled', cloneObjects: false });
    update3DPlacement(key, {
      valid: true,
      printAreaId: existing.area.id,
      metadata: { mode: 'texture' },
    });
    try { if (canvas) canvas.style.cursor = 'grabbing'; } catch {}
    const textureMoveMs = updateStart ? perfNow() - updateStart : 0;
    if (perfStart) noteDragMovePerf(perfNow() - perfStart, { textureMoves: 1, textureMoveMs });
    try { event?.preventDefault?.(); } catch {}
    try { event?.stopPropagation?.(); } catch {}
  };

  const getDragWorldScale = (): number => {
    const bounds = getModelBounds();
    const modelSize = bounds.max.subtract(bounds.min).length();
    const radius = Math.max(0.8, Number((camera?.radius ?? modelSize) || 3));
    const viewport = Math.max(1, Math.min(Number(canvas?.clientWidth ?? 900) || 900, Number(canvas?.clientHeight ?? 700) || 700));
    return Math.max(0.0008, (radius / viewport) * 1.85);
  };

  const constrainFixedSlot3DDragSurface = (
    id: string,
    candidate: SurfacePickResult,
  ): SurfacePickResult | null => {
    const deco = findDecorationRecord(id);
    const constraints = getFixedSlot3DConstraintsForDecoration(deco);
    if (!constraints.lockX && !constraints.lockY) return candidate;

    const slot = getFixedSlot3DDefinitionForDecoration(deco);
    const anchor = slot?.surface3d ?? (deco as any)?.metadata?.fixedSlotSurface3D;
    const anchorPoint = anchor?.rawPosition ?? anchor?.position;
    if (!anchorPoint) return candidate;

    const anchorRaw = new Vector3(Number(anchorPoint.x) || 0, Number(anchorPoint.y) || 0, Number(anchorPoint.z) || 0);
    let anchorNormal = anchor?.normal
      ? new Vector3(Number(anchor.normal.x) || 0, Number(anchor.normal.y) || 0, Number(anchor.normal.z) || 0)
      : candidate.normal.clone();
    if (anchorNormal.lengthSquared() < 0.0001) anchorNormal = candidate.normal.clone();
    if (anchorNormal.lengthSquared() < 0.0001) anchorNormal = new Vector3(0, 0, 1);
    anchorNormal.normalize();

    const fallbackBasis = getSurfaceTangentBasis(anchorNormal);
    let anchorRight = anchor?.surfaceRight
      ? new Vector3(Number(anchor.surfaceRight.x) || 0, Number(anchor.surfaceRight.y) || 0, Number(anchor.surfaceRight.z) || 0)
      : fallbackBasis.right.clone();
    anchorRight = anchorRight.subtract(anchorNormal.scale(Vector3.Dot(anchorRight, anchorNormal)));
    if (anchorRight.lengthSquared() < 0.0001) anchorRight = fallbackBasis.right.clone();
    anchorRight.normalize();

    let anchorUp = anchor?.surfaceUp
      ? new Vector3(Number(anchor.surfaceUp.x) || 0, Number(anchor.surfaceUp.y) || 0, Number(anchor.surfaceUp.z) || 0)
      : fallbackBasis.up.clone();
    anchorUp = anchorUp
      .subtract(anchorNormal.scale(Vector3.Dot(anchorUp, anchorNormal)))
      .subtract(anchorRight.scale(Vector3.Dot(anchorUp, anchorRight)));
    if (anchorUp.lengthSquared() < 0.0001) anchorUp = Vector3.Cross(anchorNormal, anchorRight);
    if (anchorUp.lengthSquared() < 0.0001) anchorUp = fallbackBasis.up.clone();
    anchorUp.normalize();

    const candidateRaw = candidate.rawPosition?.clone?.() ?? candidate.position.subtract(candidate.normal.scale(0.006));
    const delta = candidateRaw.subtract(anchorRaw);
    let desired = candidateRaw.clone();
    if (constraints.lockX) desired = desired.subtract(anchorRight.scale(Vector3.Dot(delta, anchorRight)));
    if (constraints.lockY) desired = desired.subtract(anchorUp.scale(Vector3.Dot(delta, anchorUp)));

    return projectPointToModelSurface(
      desired,
      candidate.normal.lengthSquared() >= 0.0001 ? candidate.normal : anchorNormal,
      getDecorationDragPreferredSurfaceMesh(deco) ?? candidate.targetMesh,
    );
  };

  const startDecorationDrag = (id: string, event?: PointerEvent | MouseEvent | null, pickedMesh?: any, textureHit?: ReturnType<typeof findTexturePrintAreaObjectAtPointer> | null) => {
    if (!isDraggableDecoration(id)) return;
    const deco = findDecorationRecord(id);
    const fixedSlotConstraints = getFixedSlot3DConstraintsForDecoration(deco);
    if (fixedSlotConstraints.lockX && fixedSlotConstraints.lockY) return;
    if (findTexturePrintAreaObject(id)) {
      startTexturePrintAreaObjectDrag(id, event ?? null, pickedMesh, textureHit ?? null);
      return;
    }
    const mesh = objectMeshes.get(id);
    if (!mesh) return;
    try { ensure3DHistoryBaseline('object-drag:start', currentViewId); } catch {}
    const dragPlane = getDragPlaneForMesh(mesh);
    dragState = {
      id,
      mode: 'mesh',
      pointerId: typeof (event as any)?.pointerId === 'number' ? Number((event as any).pointerId) : null,
      startClientX: Number((event as any)?.clientX ?? 0) || 0,
      startClientY: Number((event as any)?.clientY ?? 0) || 0,
      startPosition: mesh.position.clone(),
      startPointerWorld: getPointerWorldOnPlane(dragPlane, event ?? null),
      dragPlane,
      moved: false,
      surfaceConforming: projectionModeFromMesh(mesh, deco?.placement?.mode ?? 'plane') === 'decal',
      startCameraState: clonePlain(readCameraState()),
      previousCursor: canvas?.style.cursor ?? '',
    };
    begin3DPlacement(id, String(getDecorationPrintArea(deco)?.id ?? '').trim() || null, dragState.surfaceConforming ? 'surface' : 'plane');
    try { if (canvas) canvas.style.cursor = 'grabbing'; } catch {}
    detachCameraForObjectDrag();
    try { if (dragState.pointerId !== null) (event?.target as any)?.setPointerCapture?.(dragState.pointerId); } catch {}
    startDragPerf(id, 'mesh');
    try { event?.preventDefault?.(); } catch {}
    try { event?.stopPropagation?.(); } catch {}
  };

  const updateDecorationDrag = (event?: PointerEvent | MouseEvent | null) => {
    const perfStart = false ? perfNow() : 0;
    if (!dragState) return;
    if (dragState.mode === 'texture') {
      updateTexturePrintAreaObjectDrag(event ?? null);
      return;
    }
    const mesh = objectMeshes.get(dragState.id);
    if (!mesh) return;
    const dx = (Number((event as any)?.clientX ?? dragState.startClientX) || 0) - dragState.startClientX;
    const dy = (Number((event as any)?.clientY ?? dragState.startClientY) || 0) - dragState.startClientY;
    if (Math.abs(dx) > 1 || Math.abs(dy) > 1) dragState.moved = true;

    if (dragState.surfaceConforming) {
      const deco = findDecorationRecord(dragState.id);
      const pickStart = false ? perfNow() : 0;
      const rawPickedSurface = pickPointerModelSurface(event ?? null, getDecorationDragPreferredSurfaceMesh(deco));
      const pickedSurface = rawPickedSurface
        ? constrainFixedSlot3DDragSurface(dragState.id, rawPickedSurface)
        : null;
      const surfacePickMs = pickStart ? perfNow() - pickStart : 0;
      if (pickedSurface) {
        const currentStickerMesh = objectMeshes.get(dragState.id);
        const previewSize = getSafeDecalProjectionSize(getDecorationDecalSize(currentStickerMesh ?? null, deco));
        const resolvedDragSurface = resolveDecorationDragSurface(dragState.id, pickedSurface, previewSize, deco, currentStickerMesh ?? null);
        if (!resolvedDragSurface) {
          try { if (canvas) canvas.style.cursor = 'not-allowed'; } catch {}
          update3DPlacement(dragState.id, {
            valid: false,
            constrained: true,
            reason: 'outside-area',
            printAreaId: String(getDecorationPrintArea(deco)?.id ?? '').trim() || null,
          });
          if (perfStart) noteDragMovePerf(perfNow() - perfStart, { surfacePicks: 1, surfacePickMs });
          try { event?.preventDefault?.(); } catch {}
          try { event?.stopPropagation?.(); } catch {}
          return;
        }
        const surface = resolvedDragSurface.surface;
        // Keep the raw hit for pointerup. The final rebuild resolves it through
        // this same function, so preview and commit remain deterministic without
        // applying the constraint twice.
        dragState.pendingSurface = pickedSurface;
        const now = (typeof performance !== 'undefined' && typeof performance.now === 'function') ? performance.now() : Date.now();
        const lastAt = Number(dragState.lastSurfaceDragRenderAt ?? 0) || 0;
        const lastPoint = dragState.lastSurfaceDragPoint ?? null;
        const samePoint = !!lastPoint && surface.position.subtract(lastPoint).lengthSquared() < 0.000001;
        let didRecreateDecal = false;
        let decalRecreateMs = 0;
        const isComplexSurfacePatch = !!(currentStickerMesh as any)?.__asukaUsingSurfacePatch;
        const isNativeSurfaceDecal = !!(currentStickerMesh as any)?.__asukaNativeSurfaceDecal;
        const explicitAreaTarget = getExplicitPrintAreaTargetMesh(getDecorationPrintArea(deco));
        if (isNativeSurfaceDecal && explicitAreaTarget && !samePoint && (!lastAt || now - lastAt >= 16)) {
          // Native Babylon decals are already clipped to their target mesh. For
          // explicit drawing areas, rebuild them live so the preview is exactly
          // what will be committed and never floats onto neighbouring meshes.
          const decalStart = false ? perfNow() : 0;
          didRecreateDecal = !!recreateDecorationDecalOnSurface(dragState.id, pickedSurface);
          decalRecreateMs = decalStart ? perfNow() - decalStart : 0;
        } else if (isComplexSurfacePatch && explicitAreaTarget) {
          // Rebuilding a dense mesh-clipped surface patch on every pointer move
          // is fragile on imported GLBs: a transient pick can produce a tiny
          // clipped result and visually erase the layer mid-drag. Keep a cheap
          // preview while dragging; finishDecorationDrag performs the exact
          // mesh-clipped rebuild once with the last valid surface.
          if (!samePoint) {
            const previewAngle = Number((currentStickerMesh as any)?.__asukaDecalAngle ?? (deco as any)?.metadata?.decalAngle ?? 0) || 0;
            const previewPosition = previewMoveComplexSurfaceSticker(dragState.id, surface, previewSize, previewAngle);
            if (previewPosition) {
              dragState.lastSurfacePreviewPoint = previewPosition.clone();
              dragState.lastSurfaceDragRenderAt = now;
              dragState.lastSurfaceDragPoint = surface.position.clone();
            }
          }
        } else if (isComplexSurfacePatch) {
          // Complex/curved meshes are too expensive to rebuild on every pointer
          // move. Keep a cheap visual preview during drag and do the exact
          // surface-conforming rebuild once on pointerup.
          if (!samePoint) {
            const previewAngle = Number((currentStickerMesh as any)?.__asukaDecalAngle ?? (deco as any)?.metadata?.decalAngle ?? 0) || 0;
            const previewPosition = previewMoveComplexSurfaceSticker(dragState.id, surface, previewSize, previewAngle);
            if (previewPosition) {
              dragState.lastSurfacePreviewPoint = previewPosition.clone();
              dragState.lastSurfaceDragRenderAt = now;
              dragState.lastSurfaceDragPoint = surface.position.clone();
            }
          }
        } else if (!samePoint && (!lastAt || now - lastAt >= 16)) {
          // Face-net stickers (flat geometry): fast enough to rebuild at 60 fps.
          const decalStart = false ? perfNow() : 0;
          didRecreateDecal = !!recreateDecorationDecalOnSurface(dragState.id, pickedSurface);
          decalRecreateMs = decalStart ? perfNow() - decalStart : 0;
        }
        update3DPlacement(dragState.id, {
          valid: true,
          constrained: surface.position.subtract(pickedSurface.position).lengthSquared() > 0.000001,
          printAreaId: String(getDecorationPrintArea(deco)?.id ?? '').trim() || null,
          metadata: { mode: 'surface' },
        });
        try { if (canvas) canvas.style.cursor = 'grabbing'; } catch {}
        if (didRecreateDecal) {
          dragState.lastSurfaceDragRenderAt = now;
          dragState.lastSurfaceDragPoint = surface.position.clone();
          dragState.pendingSurface = null;
          if (perfStart) noteDragMovePerf(perfNow() - perfStart, { surfacePicks: 1, surfacePickMs, decalRecreates: 1, decalRecreateMs });
          try { event?.preventDefault?.(); } catch {}
          try { event?.stopPropagation?.(); } catch {}
          return;
        }
      }
      // Surface-conforming layers must never fall back to free-plane movement.
      // If the pointer leaves the product mesh, keep the last valid decal position
      // instead of letting the layer drift away from the product surface.
      if (!pickedSurface) {
        try { if (canvas) canvas.style.cursor = 'not-allowed'; } catch {}
        update3DPlacement(dragState.id, {
          valid: false,
          constrained: true,
          reason: 'outside-surface',
          printAreaId: String(getDecorationPrintArea(findDecorationRecord(dragState.id))?.id ?? '').trim() || null,
        });
      }
      if (perfStart) noteDragMovePerf(perfNow() - perfStart, { surfacePicks: pickedSurface ? 1 : 0, surfacePickMs });
      try { event?.preventDefault?.(); } catch {}
      try { event?.stopPropagation?.(); } catch {}
      return;
    }

    const fixedSlotConstraints = getFixedSlot3DConstraintsForDecoration(findDecorationRecord(dragState.id));
    const basis = getViewTransformBasis();
    const currentPointerWorld = getPointerWorldOnPlane(dragState.dragPlane, event ?? null);
    if (currentPointerWorld && dragState.startPointerWorld) {
      let movement = currentPointerWorld.subtract(dragState.startPointerWorld);
      if (fixedSlotConstraints.lockX) movement = movement.subtract(basis.right.scale(Vector3.Dot(movement, basis.right)));
      if (fixedSlotConstraints.lockY) movement = movement.subtract(basis.up.scale(Vector3.Dot(movement, basis.up)));
      mesh.position = dragState.startPosition.add(movement);
    } else {
      const scale = getDragWorldScale();
      mesh.position = dragState.startPosition
        .add(basis.right.scale((fixedSlotConstraints.lockX ? 0 : dx) * scale))
        .add(basis.up.scale((fixedSlotConstraints.lockY ? 0 : -dy) * scale));
    }
    update3DPlacement(dragState.id, {
      valid: true,
      printAreaId: String(getDecorationPrintArea(findDecorationRecord(dragState.id))?.id ?? '').trim() || null,
      metadata: { mode: 'plane' },
    });
    try { if (canvas) canvas.style.cursor = 'grabbing'; } catch {}

    if (perfStart) noteDragMovePerf(perfNow() - perfStart);
    try { event?.preventDefault?.(); } catch {}
    try { event?.stopPropagation?.(); } catch {}
  };

  const finishDecorationDrag = (event?: PointerEvent | MouseEvent | null) => {
    const finishPerfStart = false ? perfNow() : 0;
    if (!dragState) return;
    const finished = dragState;
    dragState = null;
    try { (event?.target as any)?.releasePointerCapture?.(finished.pointerId); } catch {}
    try { if (canvas) canvas.style.cursor = finished.previousCursor ?? ''; } catch {}
    restoreCameraAfterObjectDrag(finished.startCameraState);
    if (finished.moved) {
      asukaHistoryDebug('engine3d.drag.finish.moved', {
        id: finished.id,
        mode: finished.mode,
        viewId: currentViewId,
        historyBefore: summarizeHistoryInfo(get3DHistoryInfo(currentViewId)),
        trackBefore: summarize3DHistoryTrackForDebug(currentViewId),
      });
      runWithoutIntermediateHistory(() => {
        if (finished.mode === 'texture') {
          if (dragPerfStats) dragPerfStats.emitChangedCalls += 1;
          emitChanged('updateObject', finished.id);
        } else {
          if (finished.surfaceConforming && finished.pendingSurface) {
            const decalStart = false ? perfNow() : 0;
            try { recreateDecorationDecalOnSurface(finished.id, finished.pendingSurface); } catch {}
            if (dragPerfStats && decalStart) { dragPerfStats.decalRecreates += 1; dragPerfStats.decalRecreateMs += perfNow() - decalStart; }
          }
          if (dragPerfStats) dragPerfStats.syncPlacementCalls += 1;
          syncDecorationPlacementFromMesh(finished.id);
          if (dragPerfStats) dragPerfStats.emitChangedCalls += 1;
          emitChanged('updateObject', finished.id);
        }
      });
      update3DPlacement(finished.id, {
        valid: true,
        printAreaId: String(resolve3DConstraintPrintArea(finished.id)?.id ?? finished.areaId ?? '').trim() || null,
        metadata: { committedAfterSurfaceRebuild: finished.surfaceConforming },
      });
      const commitStart = false ? perfNow() : 0;
      try { commit3DHistorySnapshot({ source: 'drag:end', reason: 'drag:end' }); } catch (error) {
        asukaHistoryDebug('engine3d.drag.finish.commit.error', { id: finished.id, error: String((error as any)?.message ?? error) });
      }
      if (dragPerfStats && commitStart) dragPerfStats.commitMs += perfNow() - commitStart;
      asukaHistoryDebug('engine3d.drag.finish.afterCommit', {
        id: finished.id,
        viewId: currentViewId,
        historyAfter: summarizeHistoryInfo(get3DHistoryInfo(currentViewId)),
        trackAfter: summarize3DHistoryTrackForDebug(currentViewId),
      });
    }
    commit3DPlacement(finished.id);
    if (dragPerfStats && finishPerfStart) dragPerfStats.finishMs += perfNow() - finishPerfStart;
    finishDragPerf(finished.moved ? 'moved' : 'not-moved', { moved: finished.moved });
    try { event?.preventDefault?.(); } catch {}
    try { event?.stopPropagation?.(); } catch {}
  };

  async function loadView(viewId: string, state: unknown): Promise<void> {
    asukaHistoryDebug('engine3d.loadView.start', {
      requestedViewId: viewId,
      previousViewId: currentViewId,
      isRestoringHistory,
      incomingScene: summarize3DScene(state),
      currentTrackBefore: summarize3DHistoryTrackForDebug(currentViewId),
    });
    ensureBaseScene();
    cameraPersistenceSuppressedUntil = Date.now() + 1000;
    const preservedCameraState = isRestoringHistory ? readCameraState() : null;
    const previousRuntimeState = clonePlain(currentState ?? {}) as Asuka3DSceneState;
    // Clear the previous view selection without committing product placement or
    // mutating the incoming view. A normal view switch must never recenter the
    // old product group inside the new view/history track.
    setSelectedObject(null, false, { commitProductDeselect: false, syncState: false });
    assertCanonical3DSceneState(state && typeof state === 'object' ? state : {});
    const rawState = clonePlain((state && typeof state === 'object' ? state : {}) as Partial<Asuka3DSceneState>) as any;
    let incomingState = create3DSceneState({
      ...rawState,
      products: compactProducts(rawState?.products),
      materials: compact3DMaterialStateForPersistence(rawState?.materials ?? {}),
      decorations: Array.isArray(rawState?.decorations) ? clonePlain(rawState.decorations) : [],
      printAreas: Array.isArray(rawState?.printAreas) ? normalize3DPrintAreas(rawState.printAreas) : [],
    });
    if (isRestoringHistory) {
      incomingState = hydrateHistoryProductsFromRuntime(incomingState, previousRuntimeState) as Asuka3DSceneState;
    } else {
      incomingState = clear3DTransientSelection(incomingState);
    }
    if (!incomingState.selectedProductId && incomingState.products.length) {
      incomingState = select3DProduct(
        incomingState,
        getProductId(incomingState.products[incomingState.products.length - 1] ?? null),
        { selectObject: false },
      );
    }
    currentViewId = String(viewId ?? '').trim() || null;
    currentState = incomingState;
    cameraLimits = normalizeCameraLimits(currentState.cameraLimits, configuredCameraLimits);
    currentState = { ...currentState, cameraLimits: { ...cameraLimits } } as Asuka3DSceneState;
    applyCameraAngleLimitsToCamera(camera);
    if (isRestoringHistory && preservedCameraState) {
      currentState = { ...currentState, camera: preservedCameraState };
    }
    selectedPartId = String(currentState.selectedPartId ?? '').trim() || null;
    const restoredSelectedObjectId = isRestoringHistory
      ? (String(currentState.selectedObjectId ?? '').trim() || null)
      : null;
    const restoredSelectedMaterialId = isRestoringHistory
      ? (String(currentState.selectedMaterialId ?? '').trim() || null)
      : null;
    const nextDecorationRecords = clonePlain(currentState.decorations ?? []) as DecorationRecord[];
    for (const key of Object.keys(materialState)) delete materialState[key];
    Object.assign(materialState, currentState.materials ?? {});
    clearDecorations();
    clearPrintAreaVisuals();
    decorationRecords = nextDecorationRecords;
    selectedObjectId = null;
    selectedMaterialId = null;
    runtimeProductCatalog = findRuntimeProductCatalog(
      getProductById(getSelectedProductId()) ?? getProductsFromState(currentState)[0] ?? null,
    );
    await loadProducts(currentState.products);
    await restorePersistedPartVariations();
    renderAssemblyEnvironment();
    applyMaterialState();
    if (!isRestoringHistory) applyCameraState(currentState.camera);
    else if (preservedCameraState) applyCameraState(preservedCameraState);
    else lastPersistedCameraSignature = getCameraStateSignature(readCameraState());
    cameraPersistenceSuppressedUntil = Date.now() + 120;
    renderPrintAreaTextures();
    renderPrintAreaVisuals();

    for (const deco of decorationRecords) {
      const id = String(deco.id ?? makeId(`asuka-${deco.type ?? 'decoration'}`));
      const type = String(deco.type ?? 'design');
      let url = String(deco.source?.url ?? '').trim();
      const svg = String(deco.source?.svg ?? '').trim();
      const text = String(deco.source?.text ?? '').trim();
      if (svg || looksLikeSvgUrl(url) || looksLikeInlineSvg(url)) {
        const prepared = await prepareTransparent3DImageSource({ url, svg });
        if (prepared.url) url = prepared.url;
        if (url && /^blob:/i.test(url)) objectUrls.add(url);
      }
      let mesh: Mesh | null = null;
      const area = getActivePrintArea({ targetPrintAreaId: (deco as any).targetPrintAreaId, metadata: deco?.metadata });
      const savedDecalProjection = getSavedDecalProjection(deco, area);
      if (url) {
        const shouldUseDecal = (deco.placement?.mode === 'surface' || !!area) && (shouldUseDecalForPrintArea(area) || !!savedDecalProjection);
        const imageAspect = getImageAspectRatioFromMetadata(deco.metadata) ?? await resolveImageAspectRatio(url);
        mesh = shouldUseDecal ? createImageDecal(id, url, type, id, area, savedDecalProjection, imageAspect) : null;
        if (!mesh && isExactFaceNetPrintArea(area)) {
          debug3DLayer('face-net:plane-fallback-blocked', { id, type, source: 'loadView:image', area: clonePlain(area) }, 'warn');
        }
        // On restore, always fall through to plane as last resort so the
        // decoration is never silently lost (better a flat sticker than nothing).
        if (!mesh) mesh = createImagePlane(id, url, type, id, deco.placement ? null : area, imageAspect);
      } else if (text) {
        const shouldUseDecal = (deco.placement?.mode === 'surface' || !!area) && (shouldUseDecalForPrintArea(area) || !!savedDecalProjection);
        mesh = shouldUseDecal ? createTextDecal(id, { text, metadata: deco as any }, area, savedDecalProjection) : null;
        if (!mesh && isExactFaceNetPrintArea(area)) {
          debug3DLayer('face-net:plane-fallback-blocked', { id, type: 'text', source: 'loadView:text', area: clonePlain(area) }, 'warn');
        }
        if (!mesh) mesh = createTextPlane(id, { text, metadata: deco as any }, deco.placement ? null : area);
      }
      if (mesh && deco.placement && !savedDecalProjection) {
        applyPlacementToMesh(mesh, deco.placement);
      }
      if (mesh) {
        setDecorationRuntimeEnabled(id, (deco as any).visible !== false);
      }
    }
    if (restoredSelectedObjectId) {
      setSelectedObject(restoredSelectedObjectId, false, { commitProductDeselect: false });
    } else {
      setSelectedObject(null, false, { commitProductDeselect: false });
      selectedMaterialId = restoredSelectedMaterialId;
      currentState = { ...currentState, selectedMaterialId } as Asuka3DSceneState;
    }
    lastProductPick = null;
    scheduleEngineResize();
    emitChanged('loadView', currentViewId);
    if (!isRestoringHistory) {
      const track = get3DHistoryTrack(currentViewId, true);
      track.assets = collect3DHistoryAssets(getProductsFromState(currentState) as Asuka3DProduct[], track.assets);
      historyTracksByView.set(get3DHistoryViewId(currentViewId), track);
      ensure3DHistoryBaseline('loadView', currentViewId);
    }
    asukaHistoryDebug('engine3d.loadView.end', {
      currentViewId,
      isRestoringHistory,
      scene: summarize3DScene(currentState),
      historyInfo: summarizeHistoryInfo(get3DHistoryInfo(currentViewId)),
      track: summarize3DHistoryTrackForDebug(currentViewId),
    });
  }


  async function applyProduct(input: AsukaElementInput): Promise<AsukaObjectRef> {
    const element = (input?.element ?? {}) as any;
    const endSpan = beginAsuka3DPerfSpan('add', 'apply-product', {
      elementId: element?.id ?? null,
      elementName: element?.name ?? null,
      variationId: input?.variationId ?? null,
      source: (input as any)?.source ?? null,
      existingProducts: getProductsFromState(currentState).length,
    });
    if (!isProduct3DElementInput(element)) {
      endSpan({ ok: false, reason: 'invalid-element-type' }, { level: 'error' });
      throw new Error('The 3D engine only accepts product-3d catalog items.');
    }
    const variationPath = normalizeVariationPathInput(element, input);
    const variation = (input?.variation && typeof input.variation === 'object')
      ? input.variation
      : (variationPath[variationPath.length - 1] ?? findElementVariationById(Array.isArray(element?.variations) ? element.variations : [], String(input?.variationId ?? '')));
    const config = normalizeProduct3DConfig(element, variation, variationPath);
    if (!String(config?.model?.url ?? '').trim()) {
      endSpan({ ok: false, reason: 'missing-model-url' }, { level: 'error' });
      throw new Error('A 3D product requires product3d.model.url.');
    }

    const catalogId = String(element?.id ?? element?.name ?? makeId('product')).trim();
    const name = String(element?.name ?? catalogId).trim() || catalogId;
    const variationId = String(input?.variationId ?? variation?.id ?? '').trim();
    const variationName = String(variation?.name ?? variationId).trim() || undefined;
    const variationPathRecords = compactVariationPath(variationPath);
    const variationPathLabel = getVariationPathLabel(variationPath);
    const previousState = currentState;
    const products = getProductsFromState(previousState);
    const productId = makeProductId(catalogId || name || 'product', previousState);
    const productIndex = products.length;
    const cameraState = readCameraState();
    rememberRuntimeProductCatalog(element);
    if (element?.assembly?.schemaVersion === 1) {
      for (const key of [element?.id, element?.name, element?.familyId]) {
        const normalized = String(key ?? '').trim();
        if (normalized) runtimeAssemblyCatalog.set(normalized, element);
      }
      runtimeAssemblyElementByProductId.set(productId, element);
    }

    const clearStart = perfNow();
    if (!products.length) clearModel();
    const clearMs = perfNow() - clearStart;
    const importStart = perfNow();
    const imported = await importProductMeshes({
      productId,
      catalogId,
      model: config.model,
      parts: config.parts ?? [],
      productIndex,
      refit: false,
    });
    const importMs = perfNow() - importStart;
    const meshes = imported.meshes;
    const scopeStart = perfNow();
    const materials = scopeProductMaterials(config.materials, meshes, productId) as Record<string, any>;
    const parts = scopeProductParts(config.parts ?? [], meshes, productId) as any[];
    const printAreas = scopeProductPrintAreas(config.printAreas ?? [], meshes, productId);
    const scopeMs = perfNow() - scopeStart;
    for (const [key, value] of Object.entries(materials)) (materialState as any)[key] = value;

    const selections = collectActive3DPartVariationSelectionsFromVariationTree(Array.isArray(element?.variations) ? element.variations : [], config.parts ?? []);
    const partVariationIds: Record<string, string> = {};
    const partVariationPrices: Record<string, number> = {};
    for (const [partId, selection] of Object.entries(selections)) {
      const key = String(partId ?? '').trim();
      const value = String((selection as any)?.variationId ?? '').trim();
      if (key && value) partVariationIds[key] = value;
      if (key) partVariationPrices[key] = normalizePriceInput((selection as any)?.variationPrice) ?? 0;
    }
    const elementPrice = getElementPriceBreakdown(element, variation, variationPath);
    const variationPrice = Object.keys(partVariationPrices).length
      ? sumPriceInputs(Object.values(partVariationPrices))
      : elementPrice.variation;

    const product = compactProduct({
      id: productId,
      catalogId,
      name,
      model: config.model,
      parts,
      meshIds: meshes.map((mesh) => String(mesh.id ?? '').trim()).filter(Boolean),
      variationId,
      variationName,
      variationPath: variationPathRecords,
      variationPathLabel,
      partVariationIds,
      partVariationPrices,
      basePrice: elementPrice.base,
      variationPrice,
      price: elementPrice.base + variationPrice,
      ...(element?.parametric?.schemaVersion === 1 ? { parametric: { values: createParametricDefaultValues(element.parametric) } } : {}),
      metadata: {
        ...(compact3DMetadataForState(input?.metadata) ?? {}),
        elementId: catalogId || undefined,
        elementFamilyId: String(element?.familyId ?? '').trim() || undefined,
        elementName: name || undefined,
        source: 'engine.addProduct',
        materialTarget: `${productId}::${String(config.materialTarget ?? 'product').trim() || 'product'}`,
        previewUrl: String(config.previewUrl ?? '').trim() || undefined,
        renderableMeshCount: getRenderableModelMeshes(meshes).length,
        baseOffset: fromVec3(imported.placementOffset),
      },
    }, productIndex);
    if (!product) throw new Error('Unable to create the canonical 3D product state.');

    currentState = {
      ...previousState,
      products: compactProducts([...products, product]),
      selectedProductId: productId,
      selectedPartId: null,
      camera: cameraState ?? previousState.camera,
      materials: compact3DMaterialStateForPersistence(materialState),
      printAreas: [...(Array.isArray(previousState.printAreas) ? previousState.printAreas : []), ...printAreas],
      activePrintAreaId: printAreas.length
        ? (String(printAreas[0]?.id ?? '').trim() || null)
        : (previousState.activePrintAreaId ?? null),
      decorations: clonePlain(previousState.decorations ?? decorationRecords ?? []),
    };

    currentState = select3DProduct(currentState, productId, { selectObject: false });
    selectedPartId = String(currentState.selectedPartId ?? '').trim() || null;
    const renderStateStart = perfNow();
    applyMaterialState();
    const materialApplyMs = perfNow() - renderStateStart;
    if (element?.parametric?.schemaVersion === 1) initializeParametricProduct(productId, element, true);
    if (element?.assembly?.schemaVersion === 1 && isAssemblyModeEnabled()) {
      autoPlaceAssemblyComponentInternal(productId, {
        source: 'assembly.add-product.auto-place',
        commitHistory: false,
        emitChange: false,
      });
    }
    const printRenderStart = perfNow();
    renderPrintAreaTextures();
    renderPrintAreaVisuals();
    renderAssemblyEnvironment();
    const printRenderMs = perfNow() - printRenderStart;
    const recenteredProductGroup = centerProductGroupOnAdd && !isAssemblyModeEnabled();
    if (recenteredProductGroup) recenterProductGroup('addProduct');
    centerCameraTargetOnProductGroup('addProduct.cameraTarget', {
      force: centerProductGroupInView,
      render: false,
      compute: !recenteredProductGroup,
    });
    if (cameraFitOnAddElement) {
      fitView({ radiusRatio: cameraAddElementFitRadiusRatio });
      centerCameraTargetOnProductGroup('addProduct.fit.cameraTarget', { force: centerProductGroupInView, render: false, compute: false });
    }
    const resizeStart = perfNow();
    scheduleEngineResize();
    const emitStart = perfNow();
    emitChanged('addProduct', productId);
    const emitMs = perfNow() - emitStart;
    endSpan({
      ok: true,
      productId,
      clearMs,
      importMs,
      scopeMs,
      materialApplyMs,
      printRenderMs,
      resizeScheduleMs: emitStart - resizeStart,
      emitChangedMs: emitMs,
      meshCount: meshes.length,
      partCount: parts.length,
      materialCount: Object.keys(materials).length,
      printAreaCount: printAreas.length,
      sceneMeshes: scene?.meshes?.length ?? 0,
      sceneMaterials: scene?.materials?.length ?? 0,
      history: get3DHistoryInfo(currentViewId),
    });
    return { id: productId, kind: 'product-3d' };
  }


  const catalogIdentityMatchesProduct = (elementInput: any, productInput: any): boolean => {
    const catalogId = String(elementInput?.id ?? '').trim();
    const name = String(elementInput?.name ?? '').trim();
    return (!!catalogId && String(productInput?.catalogId ?? '').trim() === catalogId)
      || (!!name && String(productInput?.name ?? '').trim() === name);
  };

  async function updateProduct(elementInput: any, options: Record<string, unknown> = {}): Promise<AsukaObjectRef | null> {
    const element = elementInput && typeof elementInput === 'object' ? elementInput : null;
    if (!element || !isProduct3DElementInput(element)) return null;
    rememberRuntimeProductCatalog(element);
    const config = normalizeProduct3DConfig(element);
    if (element?.assembly?.schemaVersion === 1) {
      for (const key of [element?.id, element?.name, element?.familyId]) {
        const normalized = String(key ?? '').trim();
        if (normalized) runtimeAssemblyCatalog.set(normalized, element);
      }
    }
    const catalogId = String(element?.id ?? '').trim();
    if (element?.parametric?.schemaVersion === 1) {
      for (const key of [element?.id, element?.name, element?.familyId]) {
        const normalized = String(key ?? '').trim();
        if (normalized) runtimeParametricCatalog.set(normalized, element);
      }
    }
    const name = String(element?.name ?? catalogId ?? '').trim();
    const products = getProductsFromState(currentState);
    const nextProducts = products.map((product: any, index: number) => catalogIdentityMatchesProduct(element, product)
      ? (compactProduct({
          ...product,
          catalogId: catalogId || product.catalogId,
          name: name || product.name,
          parts: Array.isArray(config.parts) && config.parts.length ? config.parts : product.parts,
          metadata: {
            ...((product?.metadata && typeof product.metadata === 'object') ? product.metadata : {}),
            elementId: catalogId || undefined,
            elementFamilyId: String(element?.familyId ?? '').trim() || undefined,
            elementName: name || undefined,
          },
        }, index) ?? product)
      : product);
    currentState = { ...currentState, products: compactProducts(nextProducts) };
    for (const product of nextProducts.filter((item: any) => catalogIdentityMatchesProduct(element, item))) {
      const productId = getProductId(product);
      if (productId && element?.assembly?.schemaVersion === 1) {
        runtimeAssemblyElementByProductId.set(productId, element);
      }
    }

    if (options?.applyActiveVariations !== false && options?.transient !== true) {
      const targets = collectActive3DPartVariationApplyTargetsFromVariationTree(Array.isArray(element?.variations) ? element.variations : [], config.parts ?? []);
      for (const product of nextProducts.filter((item: any) => catalogIdentityMatchesProduct(element, item))) {
        const productId = getProductId(product);
        if (!productId) continue;
        for (const [partId, target] of Object.entries(targets)) {
          const variationId = String((target as any)?.variationId ?? '').trim();
          if (!partId || !variationId) continue;
          try {
            await applyPartVariationInternal(partId, variationId, {
              element,
              variation: (target as any).variation,
              variationPath: (target as any).variationPath,
              productId,
              focus: false,
              animate: false,
              source: String(options?.source ?? 'updateProduct'),
              commitHistory: false,
              emitChange: false,
            } as any);
          } catch {}
        }
      }
    }

    if (element?.parametric?.schemaVersion === 1) {
      for (const product of nextProducts.filter((item: any) => catalogIdentityMatchesProduct(element, item))) {
        const productId = getProductId(product);
        if (productId) initializeParametricProduct(productId, element, !parametricBaselinesByProductId.has(productId));
      }
    }
    if (options?.commit !== false && options?.transient !== true && options?.persist !== false) emitChanged('updateProduct', catalogId || name || null);
    return { id: catalogId || name || 'product', kind: 'product-3d' };
  }


  async function removeProduct(id?: string, options: Record<string, unknown> = {}): Promise<void> {
    const requested = String(id ?? '').replace(/^product:/, '').trim();
    const keepDecorations = (options as any)?.keepDecorations === true;
    const products = getProductsFromState(currentState);
    const matches = (product: any): boolean => !requested || [product?.id, product?.catalogId, product?.name]
      .map((value) => String(value ?? '').trim())
      .filter(Boolean)
      .includes(requested);
    const removed = products.filter(matches);
    if (!removed.length) return;
    const removedIds = new Set(removed.map((product: any) => getProductId(product)).filter(Boolean));
    const remaining = products.filter((product: any) => !matches(product));
    for (const productId of removedIds) {
      runtimeParametricElementByProductId.delete(productId);
      parametricBaselinesByProductId.delete(productId);
      parametricMeasurementsByProductId.delete(productId);
    }

    for (const mesh of Array.from(modelMeshes)) {
      const productId = String((mesh as any)?.metadata?.asukaProductId ?? '').trim();
      if (!productId || !removedIds.has(productId)) continue;
      try { mesh.dispose(false, true); } catch {}
    }
    modelMeshes = modelMeshes.filter((mesh) => {
      const productId = String((mesh as any)?.metadata?.asukaProductId ?? '').trim();
      return !productId || !removedIds.has(productId);
    });
    for (const [meshId, mesh] of Array.from(objectMeshes.entries())) {
      const productId = String((mesh as any)?.metadata?.asukaProductId ?? '').trim();
      if (productId && removedIds.has(productId)) {
        objectMeshes.delete(meshId);
        objectKinds.delete(meshId);
      }
    }
    objectMeshes.delete('product');
    objectKinds.delete('product');
    const firstRenderable = getRenderableModelMeshes(modelMeshes)[0];
    if (firstRenderable) {
      objectMeshes.set('product', firstRenderable);
      objectKinds.set('product', 'mesh');
    }
    for (const key of Object.keys(materialState)) {
      if (Array.from(removedIds).some((productId) => key.startsWith(`${productId}::`))) delete (materialState as any)[key];
    }

    const printAreas = (currentState.printAreas ?? []).filter((area: any) => {
      const productId = String(area?.metadata?.asukaProductId ?? '').trim();
      return !productId || !removedIds.has(productId);
    });
    const decorations = keepDecorations
      ? clonePlain(currentState.decorations ?? decorationRecords ?? []) as Asuka3DDecoration[]
      : (currentState.decorations ?? decorationRecords ?? []).filter((deco: any) => {
          const productId = String(deco?.metadata?.asukaProductId ?? '').trim();
          return !productId || !removedIds.has(productId);
        });
    if (!keepDecorations) {
      for (const deco of decorationRecords) {
        const productId = String((deco as any)?.metadata?.asukaProductId ?? '').trim();
        if (!productId || !removedIds.has(productId)) continue;
        const decorationId = String(deco?.id ?? '').trim();
        if (!decorationId) continue;
        disposeDecalFragments(decorationId);
        try { objectMeshes.get(decorationId)?.dispose(false, true); } catch {}
        objectMeshes.delete(decorationId);
        objectKinds.delete(decorationId);
      }
      decorationRecords = clonePlain(decorations) as DecorationRecord[];
    }

    const selectedProductId = remaining.some((product: any) => getProductId(product) === currentState.selectedProductId)
      ? currentState.selectedProductId
      : (getProductId(remaining[remaining.length - 1] ?? null) || null);
    for (const productId of removedIds) {
      runtimeAssemblyElementByProductId.delete(productId);
    }
    const assembly = getAssemblyState();
    currentState = {
      ...currentState,
      products: compactProducts(remaining),
      selectedProductId,
      selectedPartId: selectedProductId ? currentState.selectedPartId ?? null : null,
      materials: compact3DMaterialStateForPersistence(materialState),
      printAreas: clonePlain(printAreas),
      activePrintAreaId: printAreas.some((area: any) => String(area?.id ?? '') === String(currentState.activePrintAreaId ?? ''))
        ? currentState.activePrintAreaId ?? null
        : (String(printAreas[0]?.id ?? '').trim() || null),
      decorations: clonePlain(decorations),
      assembly: normalizeAssemblyState({
        ...assembly,
        connections: (assembly.connections ?? []).filter((connection) =>
          !removedIds.has(connection.source.componentId) && !removedIds.has(connection.target.componentId)),
        stackLinks: (assembly.stackLinks ?? []).filter((link) => (
          !removedIds.has(link.lowerComponentId) && !removedIds.has(link.upperComponentId)
        )),
      }),
    };
    currentState = select3DProduct(currentState, selectedProductId, { selectObject: false });
    selectedPartId = String(currentState.selectedPartId ?? '').trim() || null;
    if (!remaining.length) {
      runtimeProductCatalog = null;
      setCachedProductGroupViewTarget(Vector3.Zero());
    }
    setSelectedObject(null, false);
    renderPrintAreaVisuals();
    renderAssemblyEnvironment();
    if (centerProductGroup) recenterProductGroup('removeProduct');
    emitChanged('removeProduct', requested || Array.from(removedIds).join(','));
  }


  const update3DObjectInternal = (id: string, patch: AsukaObjectPatch, options: { emit?: boolean; deferProductGroupCenter?: boolean } = {}) => {
      const key = String(id ?? '').trim();
      const shouldEmit = options.emit !== false;
      if (key.startsWith('product:')) {
        const instanceId = key.slice('product:'.length).trim();
        const meshes = getProductInstanceMeshes(instanceId);
        if (typeof patch.visible === 'boolean') {
          for (const mesh of meshes) {
            try { mesh.setEnabled(patch.visible); } catch {}
          }
          if (shouldEmit) emitChanged('updateProduct', key);
        }
        const dx = Number((patch as any).deltaX ?? 0) || 0;
        const dy = Number((patch as any).deltaY ?? 0) || 0;
        const dz = Number((patch as any).deltaZ ?? 0) || 0;
        if (dx || dy || dz) {
          if (isAssemblyModeEnabled() && getAssemblyDefinitionForProduct(getProductById(instanceId))) {
            placeAssemblyComponentInternal(instanceId, {
              delta: { x: dx, y: dy, z: dz },
            }, {
              source: 'objects.update',
              emitChange: shouldEmit,
              commitHistory: shouldEmit,
            });
            return;
          }
          const delta = new Vector3(dx, dy, dz);
          for (const mesh of getProductInstanceRootMeshes(meshes)) {
            translateMeshRootByWorldDelta(mesh, delta);
          }
          for (const mesh of meshes) {
            try { mesh.computeWorldMatrix(true); } catch {}
          }
          const previous = getProductInstanceTransformOffset(instanceId);
          patchProductInState(instanceId, (instance: any) => ({
            ...(instance ?? {}),
            transform: {
              ...(((instance as any).transform && typeof (instance as any).transform === 'object') ? (instance as any).transform : {}),
              position: {
                x: (Number(previous?.x ?? 0) || 0) + dx,
                y: (Number(previous?.y ?? 0) || 0) + dy,
                z: (Number(previous?.z ?? 0) || 0) + dz,
              },
            },
          }));
          if (centerProductGroupOnTransform) {
            markProductGroupCenterPendingOnDeselect('updateProductTransform.onDeselect', key);
          } else {
            invalidateProductGroupViewTarget();
            centerCameraTargetOnProductGroup('updateProductTransform.cameraTarget', { force: centerProductGroupInView, render: false, compute: true });
          }
          if (shouldEmit) emitChanged('updateProductTransform', key);
        }
        return;
      }
      if (getPrintAreas().some((area) => String(area?.id ?? '') === key)) {
        const nextAreas = getPrintAreas().map((area) => {
          if (String(area?.id ?? '') !== key) return area;
          const next: any = { ...area };
          if (typeof patch.visible === 'boolean') next.visible = patch.visible;
          if (typeof patch.locked === 'boolean') next.locked = patch.locked;
          if (patch.metadata && typeof patch.metadata === 'object') next.metadata = { ...(next.metadata ?? {}), ...(patch.metadata as any) };
          return next;
        });
        setPrintAreasState(nextAreas, getActivePrintAreaId(), 'updatePrintArea');
        return;
      }
      const textureHit = findTexturePrintAreaObject(key);
      const rawSourcePatch = (patch as any)?.source && typeof (patch as any).source === 'object'
        ? { ...((patch as any).source as Record<string, unknown>) }
        : {};
      const directSourceUrl = String((patch as any)?.url ?? (patch as any)?.src ?? '').trim();
      if (directSourceUrl) (rawSourcePatch as any).url = directSourceUrl;
      if (textureHit) {
        if (textureHit.object?.locked === true && !Object.keys(patch ?? {}).every((patchKey) => patchKey === 'visible' || patchKey === 'locked')) return;
        updateTexturePrintAreaObjects(textureHit.area.id, (objects) => objects.map((object) => {
          if (String(object?.id ?? '') !== key) return object;
          const next: any = { ...object };
          const initialPlacement = (object?.metadata && typeof object.metadata === 'object') ? object.metadata.initialPlacement : null;
          if ((patch as any).resetTransform && initialPlacement && typeof initialPlacement === 'object') {
            for (const prop of ['x', 'y', 'width', 'height', 'rotation']) {
              if (typeof initialPlacement[prop] === 'number' && Number.isFinite(Number(initialPlacement[prop]))) next[prop] = Number(initialPlacement[prop]);
            }
          }
          for (const prop of ['x', 'y', 'width', 'height', 'opacity']) {
            if (typeof (patch as any)[prop] === 'number' && Number.isFinite(Number((patch as any)[prop]))) next[prop] = Number((patch as any)[prop]);
          }
          next.rotation = resolveTextureLayerRotationDegrees(
            next.rotation,
            patch as Record<string, unknown>,
            initialPlacement?.rotation ?? 0,
          );
          if (typeof (patch as any).scaleBy === 'number') {
            const factor = Math.max(0.05, Math.min(20, Number((patch as any).scaleBy) || 1));
            const centerX = Number(next.x ?? 0) + Math.max(1, Number(next.width ?? 1) || 1) / 2;
            const centerY = Number(next.y ?? 0) + Math.max(1, Number(next.height ?? 1) || 1) / 2;
            next.width = Math.max(1, Math.max(1, Number(next.width ?? 1) || 1) * factor);
            next.height = Math.max(1, Math.max(1, Number(next.height ?? 1) || 1) * factor);
            next.x = centerX - next.width / 2;
            next.y = centerY - next.height / 2;
          }
          if (typeof patch.visible === 'boolean') next.visible = patch.visible;
          if (typeof patch.locked === 'boolean') next.locked = patch.locked;
          if (Object.prototype.hasOwnProperty.call(patch, 'price')) next.price = (patch as any).price;
          if (Object.prototype.hasOwnProperty.call(patch, 'printingMethodId')) next.printingMethodId = (patch as any).printingMethodId ?? null;
          if ((patch as any).color) next.source = { ...(next.source ?? {}), color: String((patch as any).color) };
          if (Object.keys(rawSourcePatch).length) next.source = { ...(next.source ?? {}), ...rawSourcePatch };
          if (patch.metadata && typeof patch.metadata === 'object') next.metadata = { ...(next.metadata ?? {}), ...(patch.metadata as any) };
          return normalizeTexturePrintAreaObjectPlacement(textureHit.area, next);
        }));
        if (shouldEmit) emitChanged('updateObject', key);
        return;
      }
      let mesh = objectMeshes.get(key);
      const existing = findDecorationRecord(key);
      patch = constrainFixedSlotTransformPatch(
        { ...(patch ?? {}) },
        getFixedSlot3DConstraintsForDecoration(existing),
      ) as AsukaObjectPatch;
      if (!Object.keys(patch).length) return;
      const fixedSlotConstraints = getFixedSlot3DConstraintsForDecoration(existing);
      const allowedColors = fixedSlotConstraints.allowedColors ?? [];
      if ((patch as any).color && allowedColors.length) {
        const requestedColor = String((patch as any).color).trim();
        if (!allowedColors.some((color) => color.toLowerCase() === requestedColor.toLowerCase())) {
          (patch as any).color = allowedColors[0];
        }
      }
      const patchKeys = Object.keys(patch ?? {});
      const onlyVisibilityOrLock = patchKeys.every((patchKey) => patchKey === 'visible' || patchKey === 'locked');
      if ((existing as any)?.locked === true && !onlyVisibilityOrLock) return;
      const visualSourcePatch = {
        ...rawSourcePatch,
        ...(String((existing as any)?.type ?? '').toLowerCase() === 'text' && (patch as any).color
          ? { color: String((patch as any).color) }
          : null),
      } as Record<string, unknown>;
      if (Object.keys(visualSourcePatch).length) {
        if (!replaceDecorationVisualSource(key, visualSourcePatch)) return;
        mesh = objectMeshes.get(key) ?? mesh;
      }
      if (mesh) {
        const rotationChanged = (patch as any).resetTransform === true
          || typeof (patch as any).rotationBy === 'number'
          || typeof patch.rotation === 'number'
          || typeof (patch as any).rotationZ === 'number';
        const isDecalPlacement = projectionModeFromMesh(mesh, existing?.placement?.mode ?? 'plane') === 'decal';
        const meshPatch = String((existing as any)?.type ?? '').toLowerCase() === 'text'
          && Object.prototype.hasOwnProperty.call(visualSourcePatch, 'color')
          ? { ...patch, color: undefined }
          : patch;
        applyPatchToMesh(mesh, meshPatch, existing);
        clampFixedSlot3DMeshSize(mesh, existing);
        if (isDecalPlacement && rotationChanged) {
          const targetMesh = getDecorationSurfaceTargetMesh(existing);
          const position = (mesh as any).__asukaDecalPosition ?? (existing as any)?.metadata?.decalPosition ?? (existing as any)?.placement?.position;
          const normal = (mesh as any).__asukaSurfaceNormal ?? (existing as any)?.metadata?.surfaceNormal;
          if (targetMesh && Array.isArray(position) && Array.isArray(normal)) {
            const nextMesh = recreateDecorationDecalOnSurface(key, {
              targetMesh,
              position: toVec3(position, fromVec3(mesh.position)),
              normal: toVec3(normal, [0, 0, 1]),
            });
            if (nextMesh) mesh = nextMesh;
          }
        }
        if (typeof patch.visible === 'boolean') setDecorationRuntimeEnabled(key, patch.visible);
      }
      decorationRecords = decorationRecords.map((deco) => {
        if (String(deco.id ?? '') !== key) return deco;
        const next: DecorationRecord = { ...deco };
        if (typeof patch.visible === 'boolean') (next as any).visible = patch.visible;
        if (typeof patch.locked === 'boolean') (next as any).locked = patch.locked;
        if (typeof patch.opacity === 'number') (next as any).opacity = Math.max(0, Math.min(1, Number(patch.opacity) || 0));
        if (Object.prototype.hasOwnProperty.call(patch, 'price')) (next as any).price = (patch as any).price;
        if (Object.prototype.hasOwnProperty.call(patch, 'printingMethodId')) (next as any).printingMethodId = (patch as any).printingMethodId ?? null;
        if (Object.keys(rawSourcePatch).length) (next as any).source = { ...((next as any).source ?? {}), ...rawSourcePatch };
        if ((patch as any).color) {
          const color = String((patch as any).color);
          (next as any).source = { ...((next as any).source ?? {}), color };
          (next as any).metadata = { ...((next as any).metadata ?? {}), color };
        }
        if (patch.metadata && typeof patch.metadata === 'object') {
          (next as any).metadata = { ...((next as any).metadata ?? {}), ...(patch.metadata as any) };
        }
        if (mesh) next.placement = decorationPlacementFromMesh(mesh, projectionModeFromMesh(mesh, next.placement?.mode ?? 'plane'));
        return next;
      });
      if (shouldEmit) emitChanged('updateObject', key);

  };


  const get3DObjectTransform = (id: string): AsukaObjectTransform | null => {
    const key = String(id ?? '').trim();
    if (!key) return null;
    const textureHit = findTexturePrintAreaObject(key);
    if (textureHit) {
      const object = textureHit.object ?? {};
      return {
        id: key,
        space: 'texture',
        unit: 'texture-px',
        x: Number(object.x ?? 0) || 0,
        y: Number(object.y ?? 0) || 0,
        z: 0,
        width: Number(object.width ?? 0) || 0,
        height: Number(object.height ?? 0) || 0,
        rotation: textureLayerDegreesToRadians(object.rotation),
        rotationZ: textureLayerDegreesToRadians(object.rotation),
        scaleX: 1,
        scaleY: 1,
        scaleZ: 1,
        opacity: typeof object.opacity === 'number' ? object.opacity : undefined,
        visible: object.visible !== false,
        locked: object.locked === true || (textureHit.area as any)?.locked === true,
        metadata: { engine: 'babylon', mode: 'texture', areaId: textureHit.area.id, source: clonePlain(object.source ?? {}) },
      };
    }
    const mesh = objectMeshes.get(key);
    if (!mesh) return null;
    const deco = findDecorationRecord(key);
    const bounds = getMeshWorldBounds(mesh);
    const area = getDecorationPrintArea(deco);
    const surfacePosition = getDecalRuntimePosition(mesh) ?? mesh.position;
    const surfaceBounds = area ? getPrintAreaBounds(area) : null;
    const surfaceBasis = area ? getPrintAreaSurfaceBasis(area) : null;
    const surfaceDelta = surfaceBounds ? surfacePosition.subtract(surfaceBounds.center) : null;
    const surfaceSize = area ? getDecorationDecalSize(mesh, deco, area) : null;
    const frame: AsukaCoordinateFrame = area
      ? { space: 'surface-tangent', unit: 'world-unit' }
      : { space: 'world', unit: 'world-unit' };
    return {
      id: key,
      ...frame,
      x: surfaceDelta && surfaceBasis ? Vector3.Dot(surfaceDelta, surfaceBasis.right) : Number(mesh.position.x ?? 0) || 0,
      y: surfaceDelta && surfaceBasis ? Vector3.Dot(surfaceDelta, surfaceBasis.up) : Number(mesh.position.y ?? 0) || 0,
      z: surfaceDelta && surfaceBasis ? Vector3.Dot(surfaceDelta, surfaceBasis.normal) : Number(mesh.position.z ?? 0) || 0,
      width: Number(surfaceSize?.x ?? bounds.size.x ?? 0) || undefined,
      height: Number(surfaceSize?.y ?? bounds.size.y ?? 0) || undefined,
      rotation: Number(mesh.rotation.z ?? 0) || 0,
      rotationX: Number(mesh.rotation.x ?? 0) || 0,
      rotationY: Number(mesh.rotation.y ?? 0) || 0,
      rotationZ: Number(mesh.rotation.z ?? 0) || 0,
      scaleX: Number(mesh.scaling.x ?? 1) || 1,
      scaleY: Number(mesh.scaling.y ?? 1) || 1,
      scaleZ: Number(mesh.scaling.z ?? 1) || 1,
      opacity: typeof (deco as any)?.opacity === 'number'
        ? Math.max(0, Math.min(1, Number((deco as any).opacity)))
        : (typeof (mesh.material as any)?.alpha === 'number' ? Number((mesh.material as any).alpha) : undefined),
      visible: mesh.isEnabled(),
      locked: objectKinds.get(key) === 'mesh' || (deco as any)?.locked === true,
      metadata: {
        engine: 'babylon',
        printAreaId: area?.id ?? null,
        worldPosition: fromVec3(surfacePosition),
        placement: deco?.placement,
        source: clonePlain(deco?.source ?? {}),
        ...((deco as any)?.metadata?.slotId3D ? {
          fixedSlot3D: true,
          fixedSlotId: String((deco as any).metadata.slotId3D),
          fixedSlotConstraints: clonePlain(getFixedSlot3DConstraintsForDecoration(deco)),
        } : null),
      },
    };
  };

  const get3DSelectionAnchorBox = (): AsukaSelectionAnchorBox | null => {
    const key = String(selectedObjectId ?? '').trim();
    if (!key || findTexturePrintAreaObject(key)) return null;
    const mesh = key.startsWith('product:')
      ? getProductInstanceMeshes(key.slice('product:'.length).trim())[0] ?? null
      : objectMeshes.get(key) ?? null;
    const box = getMeshScreenBox(mesh);
    if (!box) return null;
    return {
      ...box,
      space: 'screen',
      unit: 'css-px',
      objectId: key,
      engine: '3d',
      metadata: { meshId: mesh?.id ?? null },
    };
  };

  const update3DObjectTransform = (id: string, patch: Partial<AsukaObjectTransform> & AsukaObjectPatch, options: Record<string, unknown> = {}) => {
    const key = String(id ?? '').trim();
    if (!key) return;
    const currentFrame = get3DObjectTransform(key) ?? { space: 'world' as const, unit: 'world-unit' as const };
    assertCompatibleCoordinateFrame(patch, currentFrame, {
      engine: '3d',
      operation: 'objects.updateTransform',
      details: { objectId: key },
    });
    const transient = options?.transient === true || options?.commit === false || options?.persist === false;
    const isProductTransform = key.startsWith('product:');
    const normalizedPatch: any = isProductTransform
      ? { ...(patch ?? {}) }
      : constrainFixedSlotTransformPatch(
          { ...(patch ?? {}) },
          getFixedSlot3DConstraintsForDecoration(findDecorationRecord(key)),
        );
    delete normalizedPatch.space;
    delete normalizedPatch.unit;
    if (!isProductTransform && !Object.keys(normalizedPatch).length) return;
    const patchKeys = Object.keys(normalizedPatch);
    const hasSurfaceMovePatch = patchKeys.some((patchKey) => ['deltaX', 'deltaY', 'x', 'y'].includes(patchKey));
    const onlySurfaceMovePatch = hasSurfaceMovePatch && patchKeys.every((patchKey) => ['deltaX', 'deltaY', 'x', 'y'].includes(patchKey));
    if (!isProductTransform && onlySurfaceMovePatch) {
      const mesh = objectMeshes.get(key);
      const deco = findDecorationRecord(key);
      const area = resolve3DConstraintPrintArea(key);
      if (mesh && deco && area && projectionModeFromMesh(mesh, deco?.placement?.mode ?? 'plane') === 'decal') {
        move3DObjectWithinPrintArea(key, normalizedPatch as any, { source: String(options?.source ?? 'updateObjectTransform'), commit: false });
        if (!transient) emitChanged('updateObjectTransform', key);
        return;
      }
    }
    const isCommitOnlyProductTransform = isProductTransform && !transient && Object.keys(normalizedPatch).length === 0;
    runWithoutIntermediateHistory(() => {
      if (typeof normalizedPatch.rotationZ === 'number' && typeof normalizedPatch.rotation !== 'number') normalizedPatch.rotation = normalizedPatch.rotationZ;
      update3DObjectInternal(key, normalizedPatch as AsukaObjectPatch, { emit: false, deferProductGroupCenter: transient && isProductTransform });
      const area = resolve3DConstraintPrintArea(key);
      if (area) constrain3DObjectToPrintArea(key, area.id, { source: 'updateObjectTransform', commit: false });
      if (isCommitOnlyProductTransform && centerProductGroupOnTransform) {
        markProductGroupCenterPendingOnDeselect('updateProductTransform.commit.onDeselect', key);
      }
    });
    try { scene?.render(); } catch {}
    if (!transient) emitChanged('updateObjectTransform', key);
  };

  const reorderPlainObjectsByNativeIds = <T,>(items: T[], nativeOrderedIds: string[], getId: (item: T) => string): { next: T[]; changed: boolean } => {
    const ids = nativeOrderedIds.map((id) => String(id ?? '').trim()).filter(Boolean);
    if (!ids.length || !Array.isArray(items) || items.length < 2) return { next: items, changed: false };
    const byId = new Map<string, T>();
    for (const item of items) {
      const id = String(getId(item) ?? '').trim();
      if (id) byId.set(id, item);
    }
    const orderedSet = new Set(ids.filter((id) => byId.has(id)));
    if (!orderedSet.size) return { next: items, changed: false };
    const orderedItems = ids.map((id) => byId.get(id)).filter(Boolean) as T[];
    const remaining = items.filter((item) => !orderedSet.has(String(getId(item) ?? '').trim()));
    const next = [...remaining, ...orderedItems];
    const changed = next.length === items.length && next.some((item, index) => item !== items[index]);
    return { next: changed ? next : items, changed };
  };

  const reorder3DObjects = (orderedIds: string[], options?: AsukaObjectOrderOptions): boolean => {
    const ids = Array.isArray(orderedIds) ? orderedIds.map((id) => String(id ?? '').trim()).filter(Boolean) : [];
    if (!ids.length) return false;
    const nativeOrderedIds = options?.order === 'canvas' ? ids : ids.slice().reverse();
    let changed = false;

    for (const area of getPrintAreas().filter((candidate) => isTextureSlotPrintArea(candidate))) {
      const areaId = String(area?.id ?? '').trim();
      const objects = Array.isArray((area as any)?.objects) ? (area as any).objects as any[] : [];
      if (!objects.some((object) => nativeOrderedIds.includes(String(object?.id ?? '').trim()))) continue;
      const updated = updateTexturePrintAreaObjects(areaId, (current) => {
        const result = reorderPlainObjectsByNativeIds(current, nativeOrderedIds, (object: any) => String(object?.id ?? ''));
        if (result.changed) changed = true;
        return result.next;
      });
      if (updated) changed = true;
    }

    const decorationResult = reorderPlainObjectsByNativeIds(decorationRecords, nativeOrderedIds, (deco: DecorationRecord) => String(deco?.id ?? ''));
    if (decorationResult.changed) {
      decorationRecords = decorationResult.next as DecorationRecord[];
      changed = true;
    }

    if (changed) emitChanged('reorderObjects', ids.join(','));
    return changed;
  };

  const duplicate3DObject = async (id: string, options?: AsukaObjectDuplicateOptions): Promise<AsukaObjectRef | null> => {
    const key = String(id ?? '').trim();
    if (!key) return null;
    const offsetX = Number(options?.offsetX ?? 0.08) || 0.08;
    const offsetY = Number(options?.offsetY ?? 0.08) || 0.08;
    const offsetZ = Number(options?.offsetZ ?? 0) || 0;
    const textureHit = findTexturePrintAreaObject(key);
    if (textureHit) {
      const newId = makeId(String(textureHit.object?.type ?? 'object') || 'object');
      const nextObject = {
        ...(clonePlain(textureHit.object) ?? {}),
        id: newId,
        x: Number(textureHit.object?.x ?? 0) + Math.max(8, offsetX * 100),
        y: Number(textureHit.object?.y ?? 0) + Math.max(8, offsetY * 100),
        metadata: { ...((textureHit.object as any)?.metadata ?? {}), duplicatedFrom: key },
      };
      updateTexturePrintAreaObjects(textureHit.area.id, (objects) => [...objects, nextObject]);
      selectedObjectId = newId;
      emitChanged('duplicateObject', newId);
      return objectRef(newId, String(nextObject?.type ?? 'object'));
    }
    const sourceMesh = objectMeshes.get(key);
    const sourceKind = objectKinds.get(key) ?? 'object';
    const deco = findDecorationRecord(key);
    if (!sourceMesh || sourceKind === 'mesh') return null;
    const newId = makeId(sourceKind);
    let clonedMesh: Mesh | null = null;
    try { clonedMesh = sourceMesh.clone(newId, null, true) as Mesh; } catch { clonedMesh = null; }
    if (!clonedMesh) return null;
    clonedMesh.id = newId;
    clonedMesh.name = `${String(sourceMesh.name || sourceKind)} copy`;
    try {
      const basis = getViewTransformBasis();
      clonedMesh.position = sourceMesh.position
        .add(basis.right.scale(offsetX))
        .add(basis.up.scale(offsetY))
        .add(basis.forward.scale(offsetZ));
    } catch {}
    objectMeshes.set(newId, clonedMesh);
    objectKinds.set(newId, sourceKind);
    if (deco) {
      const nextPlacement = decorationPlacementFromMesh(clonedMesh, projectionModeFromMesh(clonedMesh, deco.placement?.mode ?? 'plane'));
      decorationRecords.push({
        ...(clonePlain(deco) ?? deco),
        id: newId,
        placement: nextPlacement,
        metadata: { ...((deco as any).metadata ?? {}), duplicatedFrom: key, initialPlacement: clonePlain(nextPlacement) },
      } as DecorationRecord);
    }
    selectedObjectId = newId;
    emitChanged('duplicateObject', newId);
    return objectRef(newId, sourceKind);
  };

  const move3DObjectRelative = (id: string, delta: number): boolean => {
    const key = String(id ?? '').trim();
    if (!key) return false;
    const textureHit = findTexturePrintAreaObject(key);
    if (textureHit) {
      let moved = false;
      updateTexturePrintAreaObjects(textureHit.area.id, (objects) => {
        const index = objects.findIndex((object) => String(object?.id ?? '') === key);
        if (index < 0) return objects;
        // Public bring/send controls use display-direction deltas. Texture arrays are
        // rendered bottom-to-top, so the native direction is intentionally inverted.
        const nativeDelta = delta < 0 ? 1 : -1;
        const nextIndex = Math.max(0, Math.min(objects.length - 1, index + nativeDelta));
        if (nextIndex === index) return objects;
        const next = objects.slice();
        const [record] = next.splice(index, 1);
        next.splice(nextIndex, 0, record);
        moved = true;
        return next;
      });
      if (moved) emitChanged(delta < 0 ? 'bringObjectForward' : 'sendObjectBackward', key);
      return moved;
    }
    const index = decorationRecords.findIndex((deco) => String(deco?.id ?? '') === key);
    if (index < 0) return false;
    const nextIndex = Math.max(0, Math.min(decorationRecords.length - 1, index + delta));
    if (nextIndex === index) return false;
    const [record] = decorationRecords.splice(index, 1);
    decorationRecords.splice(nextIndex, 0, record);
    emitChanged(delta < 0 ? 'bringObjectForward' : 'sendObjectBackward', key);
    return true;
  };

  const set3DObjectVisible = (id: string, visible: boolean) => update3DObjectInternal(id, { visible });
  const set3DObjectLocked = (id: string, locked: boolean) => update3DObjectInternal(id, { locked });



  function buildExportable3DState(_viewId?: string | null): Asuka3DSceneState {
    const cam = camera;
    const decorations = decorationRecords.map((deco) => {
      const id = String(deco.id ?? '');
      const mesh = id ? objectMeshes.get(id) : null;
      if (!mesh) return deco;
      const runtimeMetadata = getDecalRuntimeMetadata(mesh, deco);
      return {
        ...deco,
        targetMeshId: String(runtimeMetadata.targetMeshId ?? deco.targetMeshId ?? ''),
        metadata: { ...((deco as any).metadata ?? {}), ...runtimeMetadata },
        visible: mesh.isEnabled(),
        placement: {
          ...(deco.placement ?? {}),
          ...decorationPlacementFromMesh(mesh, projectionModeFromMesh(mesh, deco.placement?.mode ?? 'plane')),
        },
      };
    });
    // Keep exportView() light: it runs after most 3D engine events. Avoid
    // deep-cloning currentState because Product 3D runtime state can still carry
    // large model/part payloads in third-party integrations. Only compact the
    // fields that need persistence, then explicitly clone mutable arrays below.
    const products = compactProducts(currentState.products);
    const selectedProductId = String(currentState.selectedProductId ?? '').trim();
    return clear3DTransientSelection({
      products,
      selectedProductId: selectedProductId && products.some((product: any) => getProductId(product) === selectedProductId)
        ? selectedProductId
        : (getProductId(products[products.length - 1] ?? null) || null),
      selectedPartId: selectedPartId ?? currentState.selectedPartId ?? null,
      camera: cam ? {
        position: fromVec3(cam.position),
        target: fromVec3(cam.target),
        fov: cam.fov,
        alpha: Number((cam as any).alpha),
        beta: Number((cam as any).beta),
        radius: Number((cam as any).radius),
      } : currentState.camera,
      cameraLimits: { ...cameraLimits },
      materials: compact3DMaterialStateForPersistence(materialState),
      decorations,
      printAreas: clonePlain((currentState.printAreas ?? []) as Asuka3DPrintArea[]),
      activePrintAreaId: currentState.activePrintAreaId ?? null,
      selectedObjectId: null,
      selectedMaterialId: null,
      ...(currentState.assembly && typeof currentState.assembly === 'object'
        ? { assembly: normalizeAssemblyState(currentState.assembly) }
        : {}),
    } as Asuka3DSceneState);
  }


  const compactHistoryMetadata = (input: any): Record<string, unknown> | undefined => {
    if (!input || typeof input !== 'object') return undefined;
    const metadata = clonePlain(input) as Record<string, unknown>;
    for (const key of Object.keys(metadata)) {
      if (/^(screenBox|boundingBox|preview|hover|imageElement|canvas|texture|runtime|mesh|material)$/i.test(key)) delete (metadata as any)[key];
      if (key.startsWith('__')) delete (metadata as any)[key];
    }
    return metadata;
  };

  const compactHistoryDecoration = (input: any): any => {
    if (!input || typeof input !== 'object') return input;
    const deco = clonePlain(input) as any;
    if (deco.metadata) deco.metadata = compactHistoryMetadata(deco.metadata);
    return deco;
  };

  const compactHistoryPrintArea = (input: any): any => {
    if (!input || typeof input !== 'object') return input;
    const area = clonePlain(input) as any;
    if (area.metadata) area.metadata = compactHistoryMetadata(area.metadata);
    if (Array.isArray(area.objects)) {
      area.objects = area.objects.map((object: any) => {
        const next = clonePlain(object) as any;
        if (next.metadata) next.metadata = compactHistoryMetadata(next.metadata);
        return next;
      });
    }
    return area;
  };

  function buildHistory3DState(_viewId?: string | null): Asuka3DSceneState {
    const decorations = decorationRecords.map((deco) => {
      const id = String(deco.id ?? '');
      const mesh = id ? objectMeshes.get(id) : null;
      if (!mesh) return compactHistoryDecoration(deco);
      const runtimeMetadata = getDecalRuntimeMetadata(mesh, deco);
      return compactHistoryDecoration({
        ...deco,
        targetMeshId: String(runtimeMetadata.targetMeshId ?? deco.targetMeshId ?? ''),
        metadata: { ...((deco as any).metadata ?? {}), ...runtimeMetadata },
        visible: mesh.isEnabled(),
        placement: {
          ...(deco.placement ?? {}),
          ...decorationPlacementFromMesh(mesh, projectionModeFromMesh(mesh, deco.placement?.mode ?? 'plane')),
        },
      });
    });
    return {
      products: compactProducts(currentState.products, { history: true }),
      selectedProductId: currentState.selectedProductId ?? null,
      selectedPartId: selectedPartId ?? currentState.selectedPartId ?? null,
      materials: compact3DMaterialStateForPersistence(materialState, { history: true }),
      decorations,
      printAreas: Array.isArray(currentState.printAreas) ? currentState.printAreas.map(compactHistoryPrintArea) : [],
      activePrintAreaId: currentState.activePrintAreaId ?? null,
      selectedObjectId: selectedObjectId ?? null,
      selectedMaterialId: selectedMaterialId ?? null,
      ...(currentState.assembly && typeof currentState.assembly === 'object'
        ? { assembly: normalizeAssemblyState(currentState.assembly) }
        : {}),
    } as Asuka3DSceneState;
  }

  function sanitize3DHistorySnapshot(input: any, _viewId?: string | null): Asuka3DSceneState {
    assertCanonical3DSceneState(input);
    const raw = clonePlain(input && typeof input === 'object' ? input : {}) as any;
    const state: Asuka3DSceneState = {
      products: compactProducts(Array.isArray(raw.products) ? raw.products : [], { history: true }),
      selectedProductId: String(raw.selectedProductId ?? '').trim() || null,
      selectedPartId: String(raw.selectedPartId ?? '').trim() || null,
      materials: raw.materials ? compact3DMaterialStateForPersistence(raw.materials, { history: true }) : {},
      decorations: Array.isArray(raw.decorations) ? raw.decorations.map(compactHistoryDecoration) : [],
      printAreas: Array.isArray(raw.printAreas) ? raw.printAreas.map(compactHistoryPrintArea) : [],
      activePrintAreaId: String(raw.activePrintAreaId ?? '').trim() || null,
      selectedObjectId: String(raw.selectedObjectId ?? '').trim() || null,
      selectedMaterialId: String(raw.selectedMaterialId ?? '').trim() || null,
      ...(raw.assembly && typeof raw.assembly === 'object'
        ? { assembly: normalizeAssemblyState(raw.assembly) }
        : {}),
    };
    if (state.selectedProductId && !state.products.some((product: any) => getProductId(product) === state.selectedProductId)) {
      state.selectedProductId = getProductId(state.products[state.products.length - 1] ?? null) || null;
    }
    return state;
  }

  async function exportView(viewId: string): Promise<Asuka3DSceneState> {
    const exported = buildExportable3DState(viewId);
    return exported;
  }

  const normalize3DExportFormat = (value: unknown, fallback: 'png' | 'json' = 'png'): 'png' | 'json' => {
    const normalized = String(value ?? '').trim().toLowerCase().replace(/^\./, '');
    return normalized === 'json' || normalized === 'png' ? normalized : fallback;
  };

  const sanitize3DDownloadName = (value: unknown): string => {
    const normalized = String(value ?? 'asuka-3d-export')
      .trim()
      .replace(/\.(?:png|json|zip)$/i, '')
      .replace(/[\\/:*?"<>|]+/g, '-')
      .replace(/\s+/g, '-')
      .replace(/-+/g, '-')
      .replace(/^-|-$/g, '');
    return normalized || 'asuka-3d-export';
  };

  const save3DBlobAs = (blob: Blob, fileName: string): void => {
    const url = URL.createObjectURL(blob);
    const link = document.createElement('a');
    link.href = url;
    link.download = fileName;
    link.style.display = 'none';
    document.body.appendChild(link);
    link.click();
    link.remove();
    window.setTimeout(() => URL.revokeObjectURL(url), 0);
  };

  const zip3DCrcTable = (() => {
    const table = new Uint32Array(256);
    for (let index = 0; index < 256; index += 1) {
      let value = index;
      for (let bit = 0; bit < 8; bit += 1) value = (value & 1) ? (0xedb88320 ^ (value >>> 1)) : (value >>> 1);
      table[index] = value >>> 0;
    }
    return table;
  })();

  const crc32For3DZip = (bytes: Uint8Array): number => {
    let crc = 0xffffffff;
    for (let index = 0; index < bytes.length; index += 1) crc = zip3DCrcTable[(crc ^ bytes[index]) & 0xff] ^ (crc >>> 8);
    return (crc ^ 0xffffffff) >>> 0;
  };

  const make3DZipBlob = async (files: Array<{ name: string; blob: Blob }>): Promise<Blob> => {
    const encoder = new TextEncoder();
    const localParts: Uint8Array[] = [];
    const centralParts: Uint8Array[] = [];
    let offset = 0;
    for (const file of files) {
      const nameBytes = encoder.encode(String(file.name ?? 'file'));
      const data = new Uint8Array(await file.blob.arrayBuffer());
      const crc = crc32For3DZip(data);
      const local = new Uint8Array(30 + nameBytes.length + data.length);
      const localView = new DataView(local.buffer);
      localView.setUint32(0, 0x04034b50, true);
      localView.setUint16(4, 20, true);
      localView.setUint32(14, crc, true);
      localView.setUint32(18, data.length, true);
      localView.setUint32(22, data.length, true);
      localView.setUint16(26, nameBytes.length, true);
      local.set(nameBytes, 30);
      local.set(data, 30 + nameBytes.length);
      localParts.push(local);

      const central = new Uint8Array(46 + nameBytes.length);
      const centralView = new DataView(central.buffer);
      centralView.setUint32(0, 0x02014b50, true);
      centralView.setUint16(4, 20, true);
      centralView.setUint16(6, 20, true);
      centralView.setUint32(16, crc, true);
      centralView.setUint32(20, data.length, true);
      centralView.setUint32(24, data.length, true);
      centralView.setUint16(28, nameBytes.length, true);
      centralView.setUint32(42, offset, true);
      central.set(nameBytes, 46);
      centralParts.push(central);
      offset += local.length;
    }
    const centralSize = centralParts.reduce((total, part) => total + part.length, 0);
    const end = new Uint8Array(22);
    const endView = new DataView(end.buffer);
    endView.setUint32(0, 0x06054b50, true);
    endView.setUint16(8, files.length, true);
    endView.setUint16(10, files.length, true);
    endView.setUint32(12, centralSize, true);
    endView.setUint32(16, offset, true);
    return new Blob([...localParts, ...centralParts, end], { type: 'application/zip' });
  };

  function serializeHistoryState(state: Asuka3DSceneState): string {
    try { return JSON.stringify(state ?? {}); } catch { return ''; }
  }

  const get3DHistoryViewId = (viewId?: string | null): string => {
    const key = String(viewId ?? currentViewId ?? '').trim();
    return key || '__default__';
  };

  const get3DHistoryTrack = (viewId?: string | null, create = true): Asuka3DHistoryTrack => {
    const key = get3DHistoryViewId(viewId);
    let track = historyTracksByView.get(key);
    if (!track && create) {
      track = { snapshots: [], index: -1, lastSignature: '', assets: {} };
      historyTracksByView.set(key, track);
    }
    return track ?? { snapshots: [], index: -1, lastSignature: '', assets: {} };
  };

  function summarize3DHistoryTrackForDebug(viewId?: string | null): Record<string, unknown> | null {
    // Exporting a 3D history track deep-clones every snapshot. Keep that work out
    // of the normal path; otherwise each commit/undo gets slower as the stack grows.
    return isAsukaHistoryDebugEnabled() ? summarizeHistoryTrack(export3DHistoryTrack(viewId)) : null;
  }

  function normalize3DHistoryTrackState(viewIdInput: string | null | undefined, input: any): Asuka3DHistoryTrackState | null {
    const viewId = get3DHistoryViewId(viewIdInput ?? input?.viewId ?? currentViewId);
    const rawSnapshots = Array.isArray(input?.snapshots) ? input.snapshots : [];
    const normalizedSnapshots = rawSnapshots
      .filter((snapshot: any) => snapshot && typeof snapshot === 'object')
      .map((snapshot: any) => sanitize3DHistorySnapshot(snapshot, viewId));
    const snapshots = normalizedSnapshots.length > historyMaxSnapshots
      ? [normalizedSnapshots[0], ...normalizedSnapshots.slice(-(historyMaxSnapshots - 1))]
      : normalizedSnapshots;
    if (!snapshots.length) return null;
    const maxIndex = snapshots.length - 1;
    const index = Math.max(0, Math.min(maxIndex, Number.isFinite(Number(input?.index)) ? Number(input.index) : maxIndex));
    const selectedSnapshot = snapshots[index] ?? snapshots[maxIndex];
    return {
      engine: '3d',
      scope: 'view',
      viewId,
      snapshots,
      index,
      assets: collect3DHistoryAssets(
        snapshots.flatMap((snapshot: Asuka3DSceneState) => Array.isArray(snapshot?.products) ? snapshot.products : []),
        normalize3DHistoryAssets(input?.assets),
      ),
      maxSnapshots: historyMaxSnapshots,
      lastSignature: String(input?.lastSignature ?? serializeHistoryState(selectedSnapshot as Asuka3DSceneState) ?? ''),
    } as Asuka3DHistoryTrackState;
  }

  function export3DHistoryTrack(viewIdInput?: string | null, options: { clone?: boolean } = {}): Asuka3DHistoryTrackState | null {
    const viewId = get3DHistoryViewId(viewIdInput ?? currentViewId);
    const track = historyTracksByView.get(viewId);
    if (!track || !Array.isArray(track.snapshots) || track.snapshots.length === 0) {
      asukaHistoryDebug('engine3d.history.exportTrack.empty', { requestedViewId: viewIdInput ?? null, resolvedViewId: viewId, currentViewId });
      return null;
    }
    const safeIndex = Math.max(0, Math.min(track.snapshots.length - 1, Number(track.index ?? track.snapshots.length - 1) || 0));
    const result = {
      engine: '3d',
      scope: 'view',
      viewId,
      snapshots: options.clone === false ? track.snapshots.slice() as Asuka3DSceneState[] : clonePlain(track.snapshots) as Asuka3DSceneState[],
      assets: options.clone === false ? { ...(track.assets ?? {}) } : clonePlain(track.assets ?? {}),
      index: safeIndex,
      maxSnapshots: historyMaxSnapshots,
      lastSignature: String(track.lastSignature ?? serializeHistoryState(track.snapshots[safeIndex] as Asuka3DSceneState) ?? ''),
    } as Asuka3DHistoryTrackState;
    asukaHistoryDebug('engine3d.history.exportTrack', { requestedViewId: viewIdInput ?? null, resolvedViewId: viewId, track: summarizeHistoryTrack(result) });
    return result;
  }

  function get3DHistoryEventPayload(viewIdInput?: string | null, reason?: string | null, extra: Record<string, unknown> = {}): Record<string, unknown> {
    const viewId = get3DHistoryViewId(viewIdInput ?? currentViewId);
    return {
      viewId,
      engine: '3d',
      scope: 'view',
      info: get3DHistoryInfo(viewId),
      // Do not attach the full snapshots stack to every event. Consumers that
      // really need the track can pull it once from the engine; including it here
      // cloned and dispatched the whole history on every commit/undo/redo.
      reason: reason ?? null,
      ...extra,
    };
  }

  function import3DHistoryTrack(viewIdInput: string, trackInput?: Asuka3DHistoryTrackState | null): void {
    const normalized = normalize3DHistoryTrackState(viewIdInput, trackInput ?? null);
    const viewId = get3DHistoryViewId(viewIdInput ?? normalized?.viewId ?? currentViewId);
    asukaHistoryDebug('engine3d.history.importTrack.start', {
      requestedViewId: viewIdInput,
      resolvedViewId: viewId,
      currentViewId,
      incoming: summarizeHistoryTrack(trackInput),
      normalized: summarizeHistoryTrack(normalized),
      previous: summarize3DHistoryTrackForDebug(viewId),
    });
    if (!normalized) {
      historyTracksByView.delete(viewId);
      asukaHistoryDebug('engine3d.history.importTrack.emptyDelete', { viewId });
      try { emitAsukaEvent('after:engine.history.sync', get3DHistoryEventPayload(viewId, 'import-empty')); } catch {}
      emitHistoryEvent('history.sync', get3DHistoryEventPayload(viewId, 'import-empty'));
      return;
    }
    historyTracksByView.set(viewId, {
      snapshots: clonePlain(normalized.snapshots) as Asuka3DSceneState[],
      index: normalized.index,
      assets: normalize3DHistoryAssets(normalized.assets),
      lastSignature: String(normalized.lastSignature ?? serializeHistoryState(normalized.snapshots[normalized.index] as Asuka3DSceneState) ?? ''),
    });
    asukaHistoryDebug('engine3d.history.importTrack.end', { viewId, saved: summarize3DHistoryTrackForDebug(viewId) });
    try { emitAsukaEvent('after:engine.history.sync', get3DHistoryEventPayload(viewId, 'import')); } catch {}
    emitHistoryEvent('history.sync', get3DHistoryEventPayload(viewId, 'import'));
  }

  function get3DHistoryInfo(viewId?: string | null): AsukaHistoryInfo {
    const track = get3DHistoryTrack(viewId, false);
    const index = Number(track.index ?? -1);
    const length = Array.isArray(track.snapshots) ? track.snapshots.length : 0;
    return {
      index,
      length,
      canUndo: index > 0,
      canRedo: index >= 0 && index < length - 1,
      maxSnapshots: historyMaxSnapshots,
      engine: '3d',
      viewId: get3DHistoryViewId(viewId),
      scope: 'view',
    };
  }

  function commit3DHistorySnapshot(options: AsukaHistoryOptions = {}): void {
    const perfEnabled = isAsuka3DPerfDebugEnabled();
    const perfStart = perfEnabled ? perfNow() : 0;
    if (isRestoringHistory) {
      asukaHistoryDebug('engine3d.history.commit.skip.restoring', { options, currentViewId });
      return;
    }
    const viewId = get3DHistoryViewId((options as any)?.viewId ?? currentViewId);
    const track = get3DHistoryTrack(viewId, true);
    const beforeSummary = summarize3DHistoryTrackForDebug(viewId);
    track.assets = collect3DHistoryAssets(getProductsFromState(currentState) as Asuka3DProduct[], track.assets);
    const buildStart = perfEnabled ? perfNow() : 0;
    const rawSnapshot = (options as any)?.state ?? buildHistory3DState(viewId);
    const buildMs = buildStart ? perfNow() - buildStart : 0;
    const sanitizeStart = perfEnabled ? perfNow() : 0;
    const snapshot = sanitize3DHistorySnapshot(rawSnapshot, viewId);
    const sanitizeMs = sanitizeStart ? perfNow() - sanitizeStart : 0;
    const serializeStart = perfEnabled ? perfNow() : 0;
    const signature = serializeHistoryState(snapshot as Asuka3DSceneState);
    const serializeMs = serializeStart ? perfNow() - serializeStart : 0;
    const previous = track.index >= 0 ? track.snapshots[track.index] : null;
    const previousSignature = previous ? (String(track.lastSignature ?? '') || serializeHistoryState(previous)) : '';
    asukaHistoryDebug('engine3d.history.commit.start', {
      viewId,
      currentViewId,
      reason: String(options.reason ?? options.source ?? 'commit'),
      force: options.force === true,
      reset: options.reset === true,
      before: beforeSummary,
      snapshot: summarize3DScene(snapshot),
      signatureChanged: previousSignature !== signature,
    });
    if (options.reset === true) {
      track.snapshots = [snapshot as Asuka3DSceneState];
      track.index = 0;
      track.lastSignature = signature;
      historyTracksByView.set(viewId, track);
      asukaHistoryDebug('engine3d.history.commit.reset', { viewId, after: summarize3DHistoryTrackForDebug(viewId), info: summarizeHistoryInfo(get3DHistoryInfo(viewId)) });
      try { emitAsukaEvent('after:engine.history.commit', get3DHistoryEventPayload(viewId, String(options.reason ?? options.source ?? 'reset'))); } catch {}
      emitHistoryEvent('history.commit', get3DHistoryEventPayload(viewId, String(options.reason ?? options.source ?? 'reset')));
      return;
    }
    if (options.force !== true && previous && previousSignature === signature) {
      asukaHistoryDebug('engine3d.history.commit.skip.sameSignature', { viewId, before: beforeSummary });
      if (perfEnabled) logAsuka3DPerf('history', 'commit-skip-same-signature', { viewId, reason: String(options.reason ?? options.source ?? 'commit'), buildMs, sanitizeMs, serializeMs, products: snapshot.products?.length ?? 0, decorations: snapshot.decorations?.length ?? 0 }, { durationMs: perfNow() - perfStart });
      return;
    }
    const next = track.index >= 0 ? track.snapshots.slice(0, track.index + 1) : [];
    next.push(snapshot as Asuka3DSceneState);
    if (next.length > historyMaxSnapshots) {
      const tail = next.slice(-(historyMaxSnapshots - 1));
      next.splice(0, next.length, next[0], ...tail);
    }
    track.snapshots = next;
    track.index = track.snapshots.length - 1;
    track.lastSignature = signature;
    historyTracksByView.set(viewId, track);
    asukaHistoryDebug('engine3d.history.commit.end', { viewId, after: summarize3DHistoryTrackForDebug(viewId), info: summarizeHistoryInfo(get3DHistoryInfo(viewId)) });
    const eventStart = perfEnabled ? perfNow() : 0;
    try { emitAsukaEvent('after:engine.history.commit', get3DHistoryEventPayload(viewId, String(options.reason ?? options.source ?? 'commit'))); } catch {}
    emitHistoryEvent('history.commit', get3DHistoryEventPayload(viewId, String(options.reason ?? options.source ?? 'commit')));
    if (perfEnabled) logAsuka3DPerf('history', 'commit', {
      viewId,
      reason: String(options.reason ?? options.source ?? 'commit'),
      buildMs,
      sanitizeMs,
      serializeMs,
      eventMs: perfNow() - eventStart,
      index: track.index,
      length: track.snapshots.length,
      products: snapshot.products?.length ?? 0,
      decorations: snapshot.decorations?.length ?? 0,
      materials: Object.keys(snapshot.materials ?? {}).length,
    }, { durationMs: perfNow() - perfStart });
  }

  function reset3DHistoryBaseline(reason = 'baseline', viewIdInput?: string | null): void {
    const viewId = get3DHistoryViewId(viewIdInput ?? currentViewId);
    const snapshot = sanitize3DHistorySnapshot(buildHistory3DState(viewId), viewId);
    const track: Asuka3DHistoryTrack = {
      snapshots: [snapshot as Asuka3DSceneState],
      index: 0,
      lastSignature: serializeHistoryState(snapshot as Asuka3DSceneState),
      assets: collect3DHistoryAssets(getProductsFromState(currentState) as Asuka3DProduct[]),
    };
    historyTracksByView.set(viewId, track);
    asukaHistoryDebug('engine3d.history.resetBaseline', { reason, viewId, track: summarize3DHistoryTrackForDebug(viewId), snapshot: summarize3DScene(snapshot) });
    try { emitAsukaEvent('after:engine.history.reset', get3DHistoryEventPayload(viewId, reason)); } catch {}
    emitHistoryEvent('history.reset', get3DHistoryEventPayload(viewId, reason));
  }

  function ensure3DHistoryBaseline(reason = 'baseline', viewIdInput?: string | null): void {
    const viewId = get3DHistoryViewId(viewIdInput ?? currentViewId);
    const track = historyTracksByView.get(viewId);
    if (track && Array.isArray(track.snapshots) && track.snapshots.length > 0) return;
    reset3DHistoryBaseline(reason, viewId);
  }

  async function restore3DHistoryIndex(index: number, reason: string): Promise<void> {
    const viewId = get3DHistoryViewId(currentViewId);
    const track = get3DHistoryTrack(viewId, false);
    asukaHistoryDebug('engine3d.history.restore.start', {
      requestedIndex: index,
      reason,
      viewId,
      currentViewId,
      track: summarizeHistoryTrack(track),
      currentInfo: summarizeHistoryInfo(get3DHistoryInfo(viewId)),
    });
    if (!track.snapshots.length) {
      asukaHistoryDebug('engine3d.history.restore.abort.emptyTrack', { reason, viewId });
      return;
    }
    const safeIndex = Math.max(0, Math.min(track.snapshots.length - 1, Number(index) || 0));
    const compactSnapshot = sanitize3DHistorySnapshot(track.snapshots[safeIndex], viewId);
    const snapshot = hydrate3DHistoryScene(compactSnapshot, track.assets);
    assertCanonical3DSceneState(snapshot);
    if (!snapshot) {
      asukaHistoryDebug('engine3d.history.restore.abort.noSnapshot', { reason, viewId, safeIndex });
      return;
    }
    isRestoringHistory = true;
    try {
      await loadView(currentViewId ?? String(viewId ?? ''), snapshot);
      const nextTrack = get3DHistoryTrack(viewId, true);
      nextTrack.index = safeIndex;
      nextTrack.lastSignature = serializeHistoryState(compactSnapshot as Asuka3DSceneState);
      historyTracksByView.set(viewId, nextTrack);
      asukaHistoryDebug('engine3d.history.restore.end', { reason, viewId, safeIndex, track: summarize3DHistoryTrackForDebug(viewId), info: summarizeHistoryInfo(get3DHistoryInfo(viewId)), scene: summarize3DScene(currentState) });
      try { emitAsukaEvent(`after:engine.history.${reason}`, get3DHistoryEventPayload(viewId, reason, { index: safeIndex })); } catch {}
      emitHistoryEvent(`history.${reason}`, get3DHistoryEventPayload(viewId, reason, { index: safeIndex }));
    } catch (error) {
      asukaHistoryDebug('engine3d.history.restore.error', { reason, viewId, error: String((error as any)?.message ?? error) });
      throw error;
    } finally {
      isRestoringHistory = false;
    }
  }

  const get3DObjectSummaries = (options?: AsukaObjectListOptions): AsukaObjectSummary[] => {
      const objects: AsukaObjectSummary[] = [];
      const hasOptions = !!options;
      const requestedObjectId = String(options?.objectId ?? '').trim();
      // getObjects() keeps the historical complete registry. listObjects(options)
      // is intentionally lightweight unless a consumer explicitly asks for
      // readonly products, technical meshes or viewport screen boxes.
      const includeReadonly = options?.includeReadonly === true || !hasOptions;
      const includeTechnical = options?.includeTechnical === true || !hasOptions;
      const includeScreenBox = options?.includeScreenBox === true || !hasOptions;
      const products = getProductsFromState(currentState);
      for (const product of products as any[]) {
        const productId = getProductId(product);
        if (!productId) continue;
        const objectId = `product:${productId}`;
        if (!includeReadonly) continue;
        if (requestedObjectId && requestedObjectId !== objectId) continue;
        const variationName = String(product?.variationName ?? '').trim();
        const variationId = String(product?.variationId ?? '').trim();
        const suffix = String(product?.variationPathLabel ?? variationName).trim() || variationName || variationId;
        const meshes = getProductInstanceMeshes(productId);
        const bounds = includeScreenBox ? getProductInstanceBounds(productId) : null;
        const productColors = collectDeclaredPricingColors(Object.entries((currentState as any)?.materials ?? {})
          .filter(([key]) => String(key).startsWith(`${productId}::`))
          .map(([, material]: [string, any]) => String(material?.color ?? '').trim())
          .filter(Boolean));
        objects.push({
          id: objectId,
          kind: 'product-3d',
          label: `${String(product?.name ?? 'Product 3D')}${suffix ? ` / ${suffix}` : ''}`,
          visible: meshes.some((mesh) => !!mesh && !mesh.isDisposed() && mesh.isEnabled()),
          locked: false,
          selected: selectedObjectId === objectId,
          pricing: {
            key: objectId,
            category: 'product',
            price: product?.price,
            basePrice: product?.basePrice,
            variationPrice: product?.variationPrice,
            fill: productColors[0] ?? null,
            fills: productColors,
            viewId: currentViewId ?? undefined,
          },
          metadata: {
            engine: 'babylon',
            viewId: currentViewId,
            itemGroup: 'Product',
            productId,
            product: compactProductForUi(product),
            ...(selectedObjectId === objectId && lastProductPick ? clonePlain(lastProductPick) : {}),
            screenBox: includeScreenBox && bounds ? getMeshScreenBox(meshes[0] ?? null) : undefined,
          },
        } as AsukaObjectSummary);
      }
      if (requestedObjectId && objects.length > 0) return objects;
      for (const [id, mesh] of objectMeshes.entries()) {
        const kind = objectKinds.get(id) ?? 'object';
        if (kind === 'mesh' && !includeTechnical) continue;
        if (requestedObjectId && requestedObjectId !== id) continue;
        const deco = findDecorationRecord(id);
        const source = deco?.source ?? {};
        const decoPrintAreaId = String((deco as any)?.targetPrintAreaId ?? (deco as any)?.metadata?.printAreaId ?? (deco as any)?.metadata?.targetPrintAreaId ?? '').trim();
        const decoPrintArea = decoPrintAreaId ? getPrintAreaById(decoPrintAreaId) : null;
        const decoPrintAreaLabel = decoPrintArea ? String(decoPrintArea?.label ?? decoPrintAreaId).trim() : '';
        const transform = kind === 'mesh' ? null : get3DObjectTransform(id);
        const pricingArea = kind === 'mesh' ? null : getDecorationPrintArea(deco);
        const pricingColors = kind === 'mesh' ? [] : get3DDeclaredPricingColors(source, (deco as any)?.metadata);
        const pricingReference = (() => {
          if (!transform || !pricingArea) return null;
          if (transform.space === 'texture') return getPrintAreaCanvasSize(pricingArea);
          const size = getPrintAreaBounds(pricingArea).size;
          return { width: size.x, height: size.y };
        })();
        const pricingPhysicalSize = (pricingArea?.physicalSize
          ?? (pricingArea?.metadata as any)?.physicalSize
          ?? (pricingArea?.metadata as any)?.policy?.physicalSize) as AsukaPhysicalSize | undefined;
        const pricingPhysical = transform && pricingReference && pricingPhysicalSize
          ? createAsukaLayerMeasurement({
              objectId: id,
              native: {
                width: Math.max(0, Number(transform.width ?? 0) || 0),
                height: Math.max(0, Number(transform.height ?? 0) || 0),
                referenceWidth: Math.max(0, Number(pricingReference.width ?? 0) || 0),
                referenceHeight: Math.max(0, Number(pricingReference.height ?? 0) || 0),
                space: transform.space,
                unit: transform.unit,
              },
              physicalSize: pricingPhysicalSize,
              physicalSource: 'print-area',
            }).physical
          : undefined;
        objects.push({
          id,
          kind,
          label: (() => {
            if (kind !== 'mesh') return mesh.name || source.text || id;
            const owner = getProductById(getProductIdFromMesh(mesh));
            if (!owner) return mesh.name || id;
            const suffix = String(owner.variationPathLabel ?? owner.variationName ?? '').trim();
            return `${String(owner.name ?? 'Product 3D')}${suffix ? ` · ${suffix}` : ''}`;
          })(),
          visible: kind === 'mesh' ? mesh.isEnabled() : (deco as any)?.visible !== false && mesh.isEnabled(),
          locked: kind === 'mesh' || (deco as any)?.locked === true,
          selected: selectedObjectId === id,
          parentId: decoPrintAreaId || null,
          targetPrintAreaId: decoPrintAreaId || null,
          targetMeshId: deco?.targetMeshId ? String(deco.targetMeshId) : null,
          printAreaLabel: decoPrintAreaLabel || null,
          ...(kind !== 'mesh' ? {
            pricing: {
              key: id,
              category: 'layer',
              price: deco?.price,
              text: typeof source?.text === 'string' ? source.text : undefined,
              fill: pricingColors[0] ?? null,
              fills: pricingColors,
              printingMethodId: (deco as any)?.printingMethodId ?? null,
              width: Number(transform?.width ?? 0) || 0,
              height: Number(transform?.height ?? 0) || 0,
              scaleX: Number(transform?.scaleX ?? 1) || 1,
              scaleY: Number(transform?.scaleY ?? 1) || 1,
              viewId: currentViewId ?? undefined,
              ...(transform ? {
                size: {
                  width: Math.max(0, Number(transform.width ?? 0) || 0),
                  height: Math.max(0, Number(transform.height ?? 0) || 0),
                  referenceWidth: Math.max(0, Number(pricingReference?.width ?? 0) || 0),
                  referenceHeight: Math.max(0, Number(pricingReference?.height ?? 0) || 0),
                  space: transform.space,
                   unit: transform.unit,
                   ...(pricingPhysical ? { physical: pricingPhysical } : null),
                 },
              } : null),
            },
          } : null),
          metadata: {
            engine: 'babylon',
            viewId: currentViewId,
            price: deco?.price,
            source,
            sourcePixelWidth: Number((deco as any)?.metadata?.sourcePixelWidth) || undefined,
            sourcePixelHeight: Number((deco as any)?.metadata?.sourcePixelHeight) || undefined,
            sourceVector: (deco as any)?.metadata?.sourceVector === true || undefined,
            placement: deco?.placement,
            projection: deco?.placement?.mode === 'surface' ? 'surface' : 'plane',
            screenBox: includeScreenBox ? getMeshScreenBox(mesh) : undefined,
            targetMeshId: deco?.targetMeshId,
            targetPrintAreaId: decoPrintAreaId || undefined,
            printAreaId: decoPrintAreaId || undefined,
            printAreaLabel: decoPrintAreaLabel || undefined,
            ...((deco as any)?.metadata?.slotId3D ? {
              fixedSlot3D: true,
              fixedSlotId: String((deco as any).metadata.slotId3D),
              fixedSlotConstraints: clonePlain(getFixedSlot3DConstraintsForDecoration(deco)),
            } : null),
            product: kind === 'mesh' ? clonePlain(getProductById(getProductIdFromMesh(mesh)) ?? null) : undefined,
            itemGroup: kind === 'mesh' ? 'Product' : kind === 'text' ? 'Text' : kind === 'image' ? 'Images' : kind === 'design' ? 'Designs' : 'Other',
          },
        } as AsukaObjectSummary);
      }
      if (requestedObjectId && objects.length > 0) return objects;
      for (const area of getPrintAreas()) {
        if (!isTextureSlotPrintArea(area)) continue;
        const areaId = String(area?.id ?? '').trim();
        for (const object of Array.isArray((area as any)?.objects) ? (area as any).objects : []) {
          const id = String(object?.id ?? '').trim();
          if (!id) continue;
          if (requestedObjectId && requestedObjectId !== id) continue;
          const kind = String(object?.type ?? 'object');
          const pricingColors = get3DDeclaredPricingColors(object?.source, object?.metadata);
          const pricingReference = getPrintAreaCanvasSize(area);
          const pricingPhysicalSize = ((area as any)?.physicalSize
            ?? (area as any)?.metadata?.physicalSize
            ?? (area as any)?.metadata?.policy?.physicalSize) as AsukaPhysicalSize | undefined;
          const pricingPhysical = pricingPhysicalSize
            ? createAsukaLayerMeasurement({
                objectId: id,
                native: {
                  width: Math.max(0, Number(object?.width ?? 0) || 0),
                  height: Math.max(0, Number(object?.height ?? 0) || 0),
                  referenceWidth: pricingReference.width,
                  referenceHeight: pricingReference.height,
                  space: 'texture',
                  unit: 'texture-px',
                },
                physicalSize: pricingPhysicalSize,
                physicalSource: 'print-area',
              }).physical
            : undefined;
          objects.push({
            id,
            kind,
            label: String(object?.label ?? object?.source?.text ?? id),
            visible: object?.visible !== false,
            locked: object?.locked === true || (area as any)?.locked === true,
            selected: selectedObjectId === id,
            parentId: areaId || null,
            targetPrintAreaId: areaId || null,
            targetMeshId: area?.targetMeshId ? String(area.targetMeshId) : null,
            printAreaLabel: String(area?.label ?? areaId).trim() || null,
            pricing: {
              key: id,
              category: 'layer',
              price: object?.price,
              text: typeof object?.source?.text === 'string' ? object.source.text : undefined,
              fill: pricingColors[0] ?? null,
              fills: pricingColors,
              printingMethodId: object?.printingMethodId ?? null,
              width: Number(object?.width ?? 0) || 0,
              height: Number(object?.height ?? 0) || 0,
              scaleX: 1,
              scaleY: 1,
              viewId: currentViewId ?? undefined,
              size: {
                width: Math.max(0, Number(object?.width ?? 0) || 0),
                height: Math.max(0, Number(object?.height ?? 0) || 0),
                referenceWidth: pricingReference.width,
                referenceHeight: pricingReference.height,
                space: 'texture',
                unit: 'texture-px',
                ...(pricingPhysical ? { physical: pricingPhysical } : null),
              },
            },
            metadata: {
              engine: 'babylon',
              viewId: currentViewId,
              itemGroup: kind === 'text' ? 'Text' : kind === 'image' ? 'Images' : kind === 'design' ? 'Designs' : 'Other',
              placement: 'texture',
              textureObject: clonePlain(object),
              targetMeshId: area.targetMeshId,
              targetPrintAreaId: areaId,
              printAreaId: areaId,
              printAreaLabel: String(area?.label ?? areaId).trim() || undefined,
              source: clonePlain(object?.source ?? {}),
              price: object?.price,
              sourcePixelWidth: Number(object?.metadata?.sourcePixelWidth) || undefined,
              sourcePixelHeight: Number(object?.metadata?.sourcePixelHeight) || undefined,
              sourceVector: object?.metadata?.sourceVector === true || undefined,
            },
          } as AsukaObjectSummary);
        }
      }
      const requestedPrintAreaId = String(options?.printAreaId ?? '').trim();
      if (!requestedPrintAreaId) return objects;
      return objects.filter((object) => String((object as any)?.targetPrintAreaId ?? (object as any)?.parentId ?? (object as any)?.metadata?.targetPrintAreaId ?? (object as any)?.metadata?.printAreaId ?? '').trim() === requestedPrintAreaId);
  };

  const measure3DObject = (id: string, options: AsukaLayerMeasurementOptions = {}): AsukaLayerMeasurement | null => {
    const key = String(id ?? '').trim();
    if (!key) return null;
    const summary = get3DObjectSummaries({ objectId: key, includeReadonly: true, includeTechnical: false, includeScreenBox: false })[0] ?? null;
    const size = summary?.pricing?.size;
    if (!summary || !size || !(size.width > 0) || !(size.height > 0)) return null;
    const areaId = String(summary.targetPrintAreaId ?? summary.parentId ?? '').trim();
    const area = areaId ? getPrintAreaById(areaId) : null;
    const areaPhysicalSize = ((area as any)?.physicalSize
      ?? (area as any)?.metadata?.physicalSize
      ?? (area as any)?.metadata?.policy?.physicalSize) as AsukaPhysicalSize | undefined;
    return createAsukaLayerMeasurement({
      objectId: key,
      native: {
        width: size.width,
        height: size.height,
        referenceWidth: size.referenceWidth,
        referenceHeight: size.referenceHeight,
        space: size.space,
        unit: size.unit,
      },
      ...options,
      physicalSize: areaPhysicalSize ?? options.physicalSize,
      physicalSource: areaPhysicalSize ? 'print-area' : (options.physicalSource ?? 'view'),
      sourcePixelWidth: Number(summary.metadata?.sourcePixelWidth) || undefined,
      sourcePixelHeight: Number(summary.metadata?.sourcePixelHeight) || undefined,
      vector: summary.metadata?.sourceVector === true,
    });
  };

  const export3DPngInternal = async (exportOptions: AsukaExportOptions = {}): Promise<Blob> => {
    const cnv = canvas;
    if (!cnv) throw new Error('3D canvas is not mounted.');
    const quality = typeof exportOptions.quality === 'number' ? exportOptions.quality : 0.92;
    const rawScale = Number((exportOptions as any)?.scale);
    const scale = Number.isFinite(rawScale) ? Math.min(1, Math.max(0.05, rawScale)) : 1;
    const requestedWidth = Math.max(0, Math.floor(Number((exportOptions as any)?.width) || 0));
    const requestedHeight = Math.max(0, Math.floor(Number((exportOptions as any)?.height) || 0));
    const sourceWidth = Math.max(1, Number(cnv.width) || Number(cnv.clientWidth) || 1);
    const sourceHeight = Math.max(1, Number(cnv.height) || Number(cnv.clientHeight) || 1);
    let targetWidth = requestedWidth || Math.max(1, Math.round(sourceWidth * scale));
    let targetHeight = requestedHeight || Math.max(1, Math.round(sourceHeight * scale));
    if (requestedWidth && !requestedHeight) targetHeight = Math.max(1, Math.round((requestedWidth / sourceWidth) * sourceHeight));
    if (requestedHeight && !requestedWidth) targetWidth = Math.max(1, Math.round((requestedHeight / sourceHeight) * sourceWidth));
    targetWidth = Math.min(sourceWidth, targetWidth);
    targetHeight = Math.min(sourceHeight, targetHeight);

    const endSpan = beginAsuka3DPerfSpan('export', 'png', {
      sourceWidth,
      sourceHeight,
      targetWidth,
      targetHeight,
      scale,
    });
    const technicalVisuals = Array.from(new Set([
      ...printAreaMeshes.values(),
      ...assemblyEnvironmentMeshes,
      printAreaDrawState?.previewMesh ?? null,
      printAreaDrawState?.hoverMesh ?? null,
    ].filter((mesh): mesh is Mesh => !!mesh)));
    const technicalVisualStates = technicalVisuals.map((mesh) => ({ mesh, enabled: (() => { try { return mesh.isEnabled(); } catch { return true; } })() }));
    for (const { mesh } of technicalVisualStates) {
      try { mesh.setEnabled(false); } catch {}
    }
    try {
      try { scene?.render(); } catch {}
      let source: HTMLCanvasElement = cnv;
      if ((targetWidth !== sourceWidth || targetHeight !== sourceHeight) && typeof document !== 'undefined') {
        const scratch = document.createElement('canvas');
        scratch.width = targetWidth;
        scratch.height = targetHeight;
        const context = scratch.getContext('2d', { alpha: true });
        if (!context) throw new Error('3D PNG thumbnail canvas context is unavailable.');
        context.drawImage(cnv, 0, 0, targetWidth, targetHeight);
        source = scratch;
      }
      const blob = await new Promise<Blob>((resolve, reject) => {
        source.toBlob((value) => {
          if (value) resolve(value);
          else reject(new Error('3D PNG export failed.'));
        }, 'image/png', quality);
      });
      endSpan({ ok: true, bytes: blob.size });
      return blob;
    } catch (error) {
      endSpan({ ok: false, error }, { level: 'warn' });
      throw error;
    } finally {
      for (const { mesh, enabled } of technicalVisualStates) {
        try { mesh.setEnabled(enabled); } catch {}
      }
      try { scene?.render(); } catch {}
    }
  };

  type Resolved3DDownloadTarget = {
    kind: 'canvas' | 'view' | 'area' | 'object';
    id?: string;
  };

  const resolve3DDownloadTarget = (targetInput?: unknown): Resolved3DDownloadTarget => {
    if (!targetInput || typeof targetInput !== 'object') return { kind: 'canvas' };
    const target = targetInput as Record<string, unknown>;
    const kind = String(target.__asukaDownloadKind ?? '').trim().toLowerCase();
    const areaId = String(target.printAreaId ?? target.zoneId ?? ((kind === 'zone' || kind === 'drawing-area' || kind === 'area') ? target.id : '') ?? '').trim();
    if (areaId) return { kind: 'area', id: areaId };
    const objectId = String(target.objectId ?? (kind === 'object' ? target.id : '') ?? '').trim();
    if (objectId) return { kind: 'object', id: objectId };
    const viewId = String(target.viewId ?? (kind === 'view' ? target.id : '') ?? '').trim();
    if (kind === 'view' || viewId) return { kind: 'view', id: viewId || String(currentViewId ?? '').trim() || undefined };
    return { kind: 'canvas' };
  };

  const get3DExportObjectAreaId = (objectId: string, state: Asuka3DSceneState): string | null => {
    const decoration = (state.decorations ?? []).find((item) => String(item?.id ?? '') === objectId);
    const decorationAreaId = String((decoration as any)?.targetPrintAreaId ?? (decoration as any)?.metadata?.targetPrintAreaId ?? (decoration as any)?.metadata?.printAreaId ?? '').trim();
    if (decorationAreaId) return decorationAreaId;
    for (const area of state.printAreas ?? []) {
      if ((Array.isArray((area as any)?.objects) ? (area as any).objects : []).some((item: any) => String(item?.id ?? '') === objectId)) {
        return String(area?.id ?? '').trim() || null;
      }
    }
    return null;
  };

  const buildTargeted3DExportState = (target: Resolved3DDownloadTarget): Asuka3DSceneState => {
    const state = buildExportable3DState(target.kind === 'view' ? target.id ?? currentViewId : currentViewId);
    if (target.kind === 'canvas' || target.kind === 'view') return state;
    if (target.kind === 'area') {
      const areaId = String(target.id ?? '').trim();
      const area = (state.printAreas ?? []).find((candidate) => String(candidate?.id ?? '') === areaId);
      if (!area) throw new Error(`Unknown 3D print area: ${areaId || '(empty id)'}.`);
      const areaObjectIds = new Set((Array.isArray((area as any)?.objects) ? (area as any).objects : []).map((item: any) => String(item?.id ?? '')).filter(Boolean));
      const decorations = (state.decorations ?? []).filter((item) => {
        const itemAreaId = String((item as any)?.targetPrintAreaId ?? (item as any)?.metadata?.targetPrintAreaId ?? (item as any)?.metadata?.printAreaId ?? '').trim();
        return itemAreaId === areaId || areaObjectIds.has(String(item?.id ?? ''));
      });
      const selectedId = String(state.selectedObjectId ?? '').trim();
      const selectedBelongsToArea = decorations.some((item) => String(item?.id ?? '') === selectedId) || areaObjectIds.has(selectedId);
      return clonePlain({
        ...state,
        printAreas: [area],
        activePrintAreaId: areaId,
        decorations,
        selectedObjectId: selectedBelongsToArea ? selectedId : null,
      }) as Asuka3DSceneState;
    }

    const objectId = String(target.id ?? '').trim();
    const decoration = (state.decorations ?? []).find((item) => String(item?.id ?? '') === objectId) ?? null;
    const areaId = get3DExportObjectAreaId(objectId, state);
    let textureObjectFound = false;
    const printAreas = (state.printAreas ?? [])
      .filter((area) => !areaId || String(area?.id ?? '') === areaId)
      .map((area) => {
        const objects = (Array.isArray((area as any)?.objects) ? (area as any).objects : []).filter((item: any) => String(item?.id ?? '') === objectId);
        if (objects.length) textureObjectFound = true;
        return { ...(area as any), objects } as Asuka3DPrintArea;
      });
    if (!decoration && !textureObjectFound) throw new Error(`Unknown 3D object: ${objectId || '(empty id)'}.`);
    return clonePlain({
      ...state,
      printAreas: areaId ? printAreas : [],
      activePrintAreaId: areaId,
      decorations: decoration ? [decoration] : [],
      selectedObjectId: objectId,
    }) as Asuka3DSceneState;
  };

  const with3DExportTargetIsolation = async <T>(target: Resolved3DDownloadTarget, operation: () => Promise<T>): Promise<T> => {
    if (target.kind !== 'area' && target.kind !== 'object') return operation();
    const targetId = String(target.id ?? '').trim();
    const meshStates: Array<{ mesh: Mesh; enabled: boolean }> = [];
    const seenMeshes = new Set<Mesh>();
    const captureMesh = (mesh: Mesh | null | undefined, keep: boolean) => {
      if (!mesh || seenMeshes.has(mesh) || mesh.isDisposed?.()) return;
      seenMeshes.add(mesh);
      let enabled = true;
      try { enabled = mesh.isEnabled(false); } catch { try { enabled = mesh.isEnabled(); } catch {} }
      meshStates.push({ mesh, enabled });
      try { mesh.setEnabled(enabled && keep); } catch {}
    };
    for (const decoration of decorationRecords) {
      const objectId = String(decoration?.id ?? '').trim();
      const areaId = String((decoration as any)?.targetPrintAreaId ?? (decoration as any)?.metadata?.targetPrintAreaId ?? (decoration as any)?.metadata?.printAreaId ?? '').trim();
      const keep = target.kind === 'object' ? objectId === targetId : areaId === targetId;
      captureMesh(objectMeshes.get(objectId), keep);
      for (const fragment of decalFragmentsByObjectId.get(objectId) ?? []) captureMesh(fragment, keep);
    }

    const textureAreas = getPrintAreas().filter((area) => isTextureSlotPrintArea(area));
    for (const area of textureAreas) {
      const objects = Array.isArray((area as any)?.objects) ? (area as any).objects : [];
      const filteredObjects = target.kind === 'object'
        ? objects.filter((item: any) => String(item?.id ?? '') === targetId)
        : (String(area?.id ?? '') === targetId ? objects : []);
      renderPrintAreaTexture({ ...(area as any), objects: filteredObjects } as Asuka3DPrintArea);
    }
    try {
      try { scene?.render(); } catch {}
      return await operation();
    } finally {
      for (const { mesh, enabled } of meshStates) {
        try { mesh.setEnabled(enabled); } catch {}
      }
      for (const area of textureAreas) renderPrintAreaTexture(area);
      try { scene?.render(); } catch {}
    }
  };

  const render3DExportTarget = async (target: Resolved3DDownloadTarget, exportOptions: AsukaExportOptions = {}): Promise<{ blob: Blob; ext: 'png' | 'json' }> => {
    const format = normalize3DExportFormat(exportOptions?.format, 'png');
    if (format === 'json') {
      const state = buildTargeted3DExportState(target);
      return {
        blob: new Blob([JSON.stringify(state ?? {}, null, 2)], { type: 'application/json;charset=utf-8' }),
        ext: 'json',
      };
    }
    const blob = await with3DExportTargetIsolation(target, () => export3DPngInternal({ ...exportOptions, format: 'png' }));
    return { blob, ext: 'png' };
  };

  const get3DExportTargetName = (target: Resolved3DDownloadTarget): string => {
    if (target.kind === 'area') {
      const area = getPrintAreaById(String(target.id ?? ''));
      return sanitize3DDownloadName((area as any)?.label ?? area?.id ?? 'drawing-area');
    }
    if (target.kind === 'object') {
      const object = get3DObjectSummaries({ objectId: String(target.id ?? ''), includeReadonly: false, includeTechnical: false })[0];
      return sanitize3DDownloadName(object?.label ?? object?.id ?? 'object');
    }
    if (target.kind === 'view') return sanitize3DDownloadName(target.id ?? currentViewId ?? 'view');
    return 'asuka-3d-export';
  };

  const downloadMany3DEntries = async <T>(
    entries: Array<{ value: T; name: string }>,
    exportOptions: AsukaExportOptions = {},
    render: (value: T, options: AsukaExportOptions) => Promise<{ blob: Blob; ext: 'png' | 'json' }>,
  ): Promise<void> => {
    const files: Array<{ name: string; blob: Blob }> = [];
    let firstError: unknown = null;
    for (let index = 0; index < entries.length; index += 1) {
      const entry = entries[index];
      try {
        const rendered = await render(entry.value, exportOptions);
        const uniqueName = `${String(index + 1).padStart(2, '0')}-${sanitize3DDownloadName(entry.name)}`;
        files.push({ name: `${uniqueName}.${rendered.ext}`, blob: rendered.blob });
      } catch (error) {
        if (firstError == null) firstError = error;
      }
    }
    if (!files.length) throw firstError instanceof Error ? firstError : new Error('No exportable 3D files were generated.');
    if ((exportOptions as any)?.zip !== false && files.length > 1) {
      const zip = await make3DZipBlob(files);
      const zipName = sanitize3DDownloadName((exportOptions as any)?.name ?? exportOptions?.filename ?? 'asuka-3d-export');
      save3DBlobAs(zip, `${zipName}.zip`);
      return;
    }
    for (const file of files) save3DBlobAs(file.blob, file.name);
  };

  const downloadMany3DTargets = async (
    entries: Array<{ target: Resolved3DDownloadTarget; name: string }>,
    exportOptions: AsukaExportOptions = {},
  ): Promise<void> => downloadMany3DEntries(
    entries.map((entry) => ({ value: entry.target, name: entry.name })),
    exportOptions,
    render3DExportTarget,
  );

  type Stored3DViewExport = {
    viewId: string;
    name: string;
    state: Asuka3DSceneState;
    mounted: boolean;
  };

  const getStored3DViewExports = (): Stored3DViewExport[] => {
    const store = (useAsukaStore as any).getState?.() ?? {};
    const mountedViewId = String(currentViewId ?? store?.activeViewId ?? '').trim();
    const scenes = store?.document3dScenesByView && typeof store.document3dScenesByView === 'object'
      ? store.document3dScenesByView as Record<string, Asuka3DSceneState>
      : {};
    const views = Array.isArray(store?.views) ? store.views : (Array.isArray(store?.documentViews) ? store.documentViews : []);
    const entries: Stored3DViewExport[] = [];
    for (const view of views) {
      const viewId = String(view?.id ?? '').trim();
      if (!viewId) continue;
      const kind = String(view?.engine ?? view?.renderEngine ?? view?.engineKind ?? '').trim().toLowerCase();
      const storedState = scenes[viewId] ?? view?.scene3d ?? null;
      const is3DView = kind === '3d' || kind === 'babylon' || (!!storedState && kind !== 'fabric');
      if (!is3DView) continue;
      const mounted = viewId === mountedViewId;
      const state = mounted
        ? buildExportable3DState(viewId)
        : create3DSceneState(storedState && typeof storedState === 'object' ? storedState : {});
      entries.push({
        viewId,
        name: String(view?.name ?? view?.label ?? viewId).trim() || viewId,
        state: clonePlain(state) as Asuka3DSceneState,
        mounted,
      });
    }
    if (!entries.length) {
      const viewId = mountedViewId || '__default__';
      entries.push({ viewId, name: viewId, state: buildExportable3DState(viewId), mounted: true });
    }
    return entries;
  };

  const exportStored3DViewPng = async (entry: Stored3DViewExport, exportOptions: AsukaExportOptions): Promise<Blob> => {
    if (entry.mounted) return export3DPngInternal({ ...exportOptions, format: 'png' });
    const host = document.createElement('div');
    host.setAttribute('data-asuka-3d-export-host', entry.viewId);
    host.style.position = 'fixed';
    host.style.left = '0';
    host.style.top = '0';
    host.style.width = `${Math.max(320, Number(canvas?.clientWidth ?? canvas?.width ?? 960) || 960)}px`;
    host.style.height = `${Math.max(240, Number(canvas?.clientHeight ?? canvas?.height ?? 720) || 720)}px`;
    host.style.zIndex = '-1';
    host.style.opacity = '0.001';
    host.style.pointerEvents = 'none';
    host.style.contain = 'strict';
    document.body.appendChild(host);
    const temporaryEngine = createAsukaBabylonRenderEngine(options);
    try {
      await temporaryEngine.lifecycle.mount(host);
      await temporaryEngine.lifecycle.loadView(entry.viewId, clonePlain(entry.state) as Asuka3DSceneState);
      temporaryEngine.viewport.resize?.();
      await new Promise<void>((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
      return await temporaryEngine.exports.png({ ...exportOptions, format: 'png' });
    } finally {
      try { temporaryEngine.lifecycle.unmount(); } catch {}
      try { host.remove(); } catch {}
    }
  };

  const renderStored3DViewExport = async (entry: Stored3DViewExport, exportOptions: AsukaExportOptions = {}): Promise<{ blob: Blob; ext: 'png' | 'json' }> => {
    const format = normalize3DExportFormat(exportOptions?.format, 'png');
    if (format === 'json') {
      return {
        blob: new Blob([JSON.stringify(entry.state ?? {}, null, 2)], { type: 'application/json;charset=utf-8' }),
        ext: 'json',
      };
    }
    return { blob: await exportStored3DViewPng(entry, exportOptions), ext: 'png' };
  };

  const downloadAll3DViews = async (exportOptions: AsukaExportOptions = {}): Promise<void> => {
    const entries = getStored3DViewExports();
    await downloadMany3DEntries(
      entries.map((entry) => ({ value: entry, name: entry.name })),
      { ...exportOptions, name: (exportOptions as any)?.name ?? 'views' },
      renderStored3DViewExport,
    );
  };

  const download3DInternal = async (targetInput?: unknown, exportOptions: AsukaExportOptions = {}): Promise<void> => {
    const target = resolve3DDownloadTarget(targetInput);
    const rendered = await render3DExportTarget(target, exportOptions);
    const explicitName = (exportOptions as any)?.name ?? exportOptions?.filename;
    const baseName = sanitize3DDownloadName(explicitName ?? get3DExportTargetName(target));
    save3DBlobAs(rendered.blob, `${baseName}.${rendered.ext}`);
  };

  // Shared across mount and addText/addImage/addDesign — must live at engine scope.
  const fixedSlot3DIndicators = new Map<string, Mesh>();
  const fixedSlot3DConsumedIds = new Set<string>();
  let suppressFixedSlotIndicatorPointerUntil = 0;
  let _callEnsureIndicatorsForSlots3D: (() => void) | null = null;
  let unsubFixedSlots3D: (() => void) | null = null;
  let remove3DSlotIndicator: (slotId: string) => void = () => {};

  const get3DObjectSummaryById = (id: string): AsukaObjectSummary | null => get3DObjectSummaries({
    objectId: id,
    includeReadonly: true,
    includeTechnical: true,
    includeScreenBox: true,
    source: 'getObject',
  })[0] ?? null;

  return {
    kind: '3d',
    capabilities: getEngineCapabilities(),
    events: createRenderEngineEvents('3d'),
    placement: {
      getStatus: placementController.getStatus,
      begin: (input) => emit3DPlacement('placement.begin', placementController.begin(input))!,
      update: (input) => emit3DPlacement('placement.update', placementController.update(input))!,
      commit: (metadata) => emit3DPlacement('placement.commit', placementController.commit(metadata)),
      cancel: (metadata) => emit3DPlacement('placement.cancel', placementController.cancel(metadata)),
      clear: () => {
        placementController.clear();
        emit3DPlacement('placement.clear', null);
      },
    },
    lifecycle: {
    mount: async (target: HTMLElement) => {
      await ensureRuntimeStoreBridge();
      const elapsedSinceDispose = Date.now() - lastBabylonEngineDisposeAt;
      if (lastBabylonEngineDisposeAt > 0 && elapsedSinceDispose < WEBGL_CONTEXT_RELEASE_DELAY_MS) {
        await new Promise<void>((resolve) => {
          window.setTimeout(resolve, WEBGL_CONTEXT_RELEASE_DELAY_MS - elapsedSinceDispose);
        });
      }
      canvasPointerLeaveHandler = () => clearProductHover(true);
      const mountCanvas = (): HTMLCanvasElement => {
        const nextCanvas = document.createElement('canvas');
        nextCanvas.className = 'ask-3d-engine-canvas';
        nextCanvas.setAttribute('data-asuka-3d-engine', 'babylon');
        nextCanvas.style.width = '100%';
        nextCanvas.style.height = '100%';
        nextCanvas.style.display = 'block';
        try { nextCanvas.addEventListener('pointerleave', canvasPointerLeaveHandler!, { passive: true }); } catch {}
        target.appendChild(nextCanvas);
        return nextCanvas;
      };
      const releaseFailedCanvas = (): void => {
        try {
          if (canvas && canvasPointerLeaveHandler) canvas.removeEventListener('pointerleave', canvasPointerLeaveHandler);
        } catch {}
        try { canvas?.remove(); } catch {}
        canvas = null;
      };
      const createBabylonEngine = (safeMode: boolean): Engine => {
        if (!canvas) throw new Error('The 3D canvas is not mounted.');
        const antialias = safeMode ? false : options.antialias !== false;
        return new Engine(canvas, antialias, {
          preserveDrawingBuffer: safeMode ? false : true,
          stencil: true,
          antialias,
          powerPreference: safeMode ? 'default' : 'high-performance',
          failIfMajorPerformanceCaveat: false,
          loseContextOnDispose: true,
        });
      };

      canvas = mountCanvas();
      try {
        engine = createBabylonEngine(false);
      } catch (initialError) {
        // A browser can temporarily refuse a new WebGL context immediately
        // after another 3D view was disposed. Release the failed canvas, let
        // the compositor finish two frames, then retry with lighter options.
        // WebKit can suspend requestAnimationFrame after a context failure, so
        // a bounded timer guarantees that the retry always runs.
        releaseFailedCanvas();
        await new Promise<void>((resolve) => {
          let settled = false;
          const finish = () => {
            if (settled) return;
            settled = true;
            window.clearTimeout(timeoutId);
            resolve();
          };
          const timeoutId = window.setTimeout(finish, WEBGL_RETRY_FRAME_TIMEOUT_MS);
          try {
            requestAnimationFrame(() => requestAnimationFrame(finish));
          } catch {
            finish();
          }
        });
        canvas = mountCanvas();
        try {
          engine = createBabylonEngine(true);
          logAsuka3DPerf('engine', 'webgl-safe-mode', {
            initialError: initialError instanceof Error ? initialError.message : String(initialError),
          }, { level: 'warn' });
        } catch (retryError) {
          releaseFailedCanvas();
          const retryMessage = retryError instanceof Error ? retryError.message : String(retryError);
          if (/webgl not supported/i.test(retryMessage)) {
            throw new Error(
              'WebGL could not be initialized. Close other 3D tabs or enable browser hardware acceleration, then retry.',
              { cause: retryError },
            );
          }
          throw retryError;
        }
      }
      scene = new Scene(engine);
      scene.clearColor = color4FromInput(options.clearColor ?? '#f8fafcff');
      // Preserve depth buffer from group 0 (shoe mesh) when rendering group 1 (decals/indicators)
      // so decals and markers are correctly occluded and never visible through the mesh.
      scene.setRenderingAutoClearDepthStencil(1, false, false);
      // ─── 3D FIXED SLOTS (MARKERS) RUNTIME ───────────────────────────────
      const create3DSlotIndicator = (slot: any): Mesh | null => {
        if (!scene) return null;
        const surface3d = slot?.surface3d;
        const pos = surface3d?.position;
        if (!pos) return null;
        const slotId = String(slot.id);
        const existing = fixedSlot3DIndicators.get(slotId);
        if (existing) { try { existing.material?.dispose?.(); existing.dispose(); } catch {} fixedSlot3DIndicators.delete(slotId); }
        const type3d = String(slot.type ?? 'text');
        const color3d = String(slot.color || (type3d === 'image' ? '#2563eb' : type3d === 'design' ? '#7c3aed' : '#059669'));
        const indicatorId = `asuka-marker-3d-${slotId}`;
        try {
          const plane = (MeshBuilder as any).CreatePlane(indicatorId, { size: 0.06 }, scene) as Mesh;
          // `surface3d.position` already includes the small normal offset used to
          // avoid z-fighting, so the billboard origin is the exact picked point.
          plane.position = new Vector3(pos.x, pos.y, pos.z);
          (plane as any).billboardMode = 7; // BILLBOARDMODE_ALL
          plane.renderingGroupId = 2;
          plane.isPickable = true;
          const texSize = 128;
          const tex = new DynamicTexture(`${indicatorId}-tex`, { width: texSize, height: texSize }, scene, false);
          tex.hasAlpha = true;
          const ctx = (tex as any).getContext();
          ctx.clearRect(0, 0, texSize, texSize);
          ctx.fillStyle = color3d;
          ctx.beginPath();
          ctx.arc(texSize / 2, texSize / 2, texSize / 2 - 6, 0, Math.PI * 2);
          ctx.fill();
          ctx.strokeStyle = 'rgba(255,255,255,0.9)';
          ctx.lineWidth = 6;
          ctx.stroke();
          ctx.fillStyle = 'white';
          ctx.font = `bold ${Math.round(texSize * 0.44)}px Arial`;
          ctx.textAlign = 'center';
          ctx.textBaseline = 'middle';
          ctx.fillText(type3d === 'image' ? 'I' : type3d === 'design' ? 'D' : 'T', texSize / 2, texSize / 2 + 2);
          tex.update();
          const mat = new StandardMaterial(`${indicatorId}-mat`, scene);
          mat.diffuseTexture = tex;
          mat.emissiveColor = Color3.White();
          mat.disableLighting = true;
          mat.useAlphaFromDiffuseTexture = true;
          mat.backFaceCulling = false;
          plane.material = mat;
          (plane as any).__asukaFixedSlot3DIndicator = true;
          (plane as any).__asukaFixedSlotId = slotId;
          (plane as any).__asukaFixedSlotType = type3d;
          fixedSlot3DIndicators.set(slotId, plane);
          return plane;
        } catch { return null; }
      };

      remove3DSlotIndicator = (slotId: string) => {
        const m = fixedSlot3DIndicators.get(slotId);
        if (m) { try { m.material?.dispose?.(); m.dispose(); } catch {} fixedSlot3DIndicators.delete(slotId); }
      };

      const ensureIndicatorsForSlots3D = () => {
        try {
          // Source of truth: decorationRecords (cleaned up by removeObject) so deletion auto-restores indicators.
          fixedSlot3DConsumedIds.clear();
          for (const deco of decorationRecords) {
            const sid = String((deco as any).metadata?.slotId3D ?? '');
            if (sid) fixedSlot3DConsumedIds.add(sid);
          }
          const state = (useAsukaStore as any).getState?.() ?? {};
          const slots: any[] = Array.isArray(state.fixedSlots) ? state.fixedSlots : [];
          const slotIds = new Set(slots.map((s: any) => String(s?.id ?? '')).filter(Boolean));
          for (const [id] of fixedSlot3DIndicators) { if (!slotIds.has(id)) remove3DSlotIndicator(id); }
          for (const slot of slots) {
            const slotId = String(slot?.id ?? '');
            if (!slotId || !slot?.surface3d) continue;
            if (fixedSlot3DConsumedIds.has(slotId)) {
              const m = fixedSlot3DIndicators.get(slotId);
              if (m) { m.isVisible = false; m.isPickable = false; }
              continue;
            }
            if (!fixedSlot3DIndicators.has(slotId)) create3DSlotIndicator(slot);
            else { const m = fixedSlot3DIndicators.get(slotId); if (m) { m.isVisible = true; m.isPickable = true; } }
          }
        } catch {}
      };

      _callEnsureIndicatorsForSlots3D = ensureIndicatorsForSlots3D;
      try {
        unsubFixedSlots3D = (useAsukaStore as any).subscribe(() => { requestAnimationFrame(() => { try { ensureIndicatorsForSlots3D(); } catch {} }); });
        ensureIndicatorsForSlots3D();
      } catch {}

      const pickFixedSlot3DIndicator = (
        event?: PointerEvent | MouseEvent | null,
        fallback?: any,
        pickedSurfacePoint?: Vector3 | null,
      ): Mesh | null => {
        if (fallback && (fallback as any).__asukaFixedSlot3DIndicator) return fallback as Mesh;
        const point = getCanvasPointerCoordinates(event ?? null);
        if (!scene || !point) return null;
        try {
          const markerPick = scene.pick(
            point.x,
            point.y,
            (candidate: any) => !!candidate?.__asukaFixedSlot3DIndicator && candidate.isPickable !== false && candidate.isVisible !== false,
            false,
            camera ?? undefined,
          );
          if (markerPick?.hit && markerPick.pickedMesh) {
            return markerPick.pickedMesh as Mesh;
          }

          // If the product was picked instead of the nearly flush billboard,
          // compare that hit with marker anchors in world space. The surface
          // point directly behind a visible marker is the most stable signal.
          if (pickedSurfacePoint) {
            let nearestSurfaceMarker: { mesh: Mesh; distance: number } | null = null;
            for (const marker of fixedSlot3DIndicators.values()) {
              if (!marker || marker.isDisposed() || marker.isPickable === false || marker.isVisible === false || !marker.isEnabled()) continue;
              const distance = marker.position.subtract(pickedSurfacePoint).length();
              if (distance <= 0.08 && (!nearestSurfaceMarker || distance < nearestSurfaceMarker.distance)) {
                nearestSurfaceMarker = { mesh: marker, distance };
              }
            }
            if (nearestSurfaceMarker) {
              return nearestSurfaceMarker.mesh;
            }
          }

          // Billboard picking can miss when its tiny plane intersects the
          // product's depth surface. Match the rendered center in screen space
          // as a deterministic fallback, with a small touch-friendly radius.
          if (!camera || !canvas) return null;
          const renderEngine = scene.getEngine();
          const renderWidth = Math.max(1, renderEngine.getRenderWidth());
          const renderHeight = Math.max(1, renderEngine.getRenderHeight());
          const viewport = camera.viewport.toGlobal(renderWidth, renderHeight);
          const transform = scene.getTransformMatrix();
          const rect = canvas.getBoundingClientRect();
          const scaleX = rect.width / renderWidth;
          const scaleY = rect.height / renderHeight;
          let nearest: { mesh: Mesh; distance: number } | null = null;
          for (const marker of fixedSlot3DIndicators.values()) {
            if (!marker || marker.isDisposed() || marker.isPickable === false || marker.isVisible === false || !marker.isEnabled()) continue;
            const projected = Vector3.Project(marker.position, Matrix.Identity(), transform, viewport);
            const dx = Number(projected.x) * scaleX - point.x;
            const dy = Number(projected.y) * scaleY - point.y;
            const distance = Math.sqrt(dx * dx + dy * dy);
            if (distance <= 32 && (!nearest || distance < nearest.distance)) nearest = { mesh: marker, distance };
          }
          return nearest?.mesh ?? null;
        } catch {
          return null;
        }
      };

      const activateFixedSlot3DIndicator = (indicator: Mesh): void => {
        const slotId = String((indicator as any).__asukaFixedSlotId ?? '');
        const slotType = String((indicator as any).__asukaFixedSlotType ?? 'text');
        if (!slotId) return;
        const activate = () => {
          try { (useAsukaStore as any).getState?.()?.setFixedSlotActive?.(slotId); } catch {}
          const moduleId = slotType === 'image' ? 'image' : slotType === 'design' ? 'designs' : 'text';
          try { (useAsukaStore as any).getState?.()?.openModule?.(moduleId); } catch {}
        };
        try {
          if (typeof requestAnimationFrame === 'function') requestAnimationFrame(activate);
          else setTimeout(activate, 0);
        } catch {
          activate();
        }
      };
      // ─────────────────────────────────────────────────────────────────────

      pointerObserver = scene.onPointerObservable.add((pointerInfo: any) => {
        const type = pointerInfo?.type;
        const event = pointerInfo?.event as PointerEvent | MouseEvent | undefined;
        if (assemblyComponentDragState) {
          if (type === PointerEventTypes.POINTERMOVE) {
            updateAssemblyComponentDrag(event ?? null);
            return;
          }
          if (type === PointerEventTypes.POINTERUP) {
            finishAssemblyComponentDrag(event ?? null);
            return;
          }
        }
        if (printAreaAddPickState && handlePrintAreaAddPickPointer(type, event ?? null)) return;
        if (printSurfaceFacePickState && handlePrintSurfaceFacePickingPointer(type, event ?? null)) return;
        if (printAreaDrawState && handlePrintAreaDrawingPointer(type, event ?? null)) return;
        if (type === PointerEventTypes.POINTERMOVE) {
          if (productCanvasPointerDown && event) {
            const dx = Number((event as any).clientX) - productCanvasPointerDown.x;
            const dy = Number((event as any).clientY) - productCanvasPointerDown.y;
            if ((dx * dx + dy * dy) > 36) {
              productCanvasPointerDown.moved = true;
              suspendProductHoverForCameraMotion('pointer.drag', 180);
            }
          }
          if (dragState) {
            updateDecorationDrag(event ?? null);
          } else if ((event as any)?.buttons) {
            suspendProductHoverForCameraMotion('pointer.buttons', 180);
            clearProductHover(true);
          } else {
            hoverPointerMoveCount += 1;
            emitProductHoverMove(event ?? null);
            if (!hoveredProductKey && !productHoverActionsInstalled) scheduleProductHoverTargetUpdate(pointerInfo, event ?? null);
          }
          return;
        }
        if (type === PointerEventTypes.POINTERUP) {
          const finishedDecorationPointer = dragState ? {
            moved: dragState.moved,
            x: Number((event as any)?.clientX ?? dragState.startClientX) || dragState.startClientX,
            y: Number((event as any)?.clientY ?? dragState.startClientY) || dragState.startClientY,
          } : null;
          if (dragState) finishDecorationDrag(event ?? null);
          if (finishedDecorationPointer) {
            // Babylon emits POINTERPICK after POINTERUP. Without consuming that
            // follow-up event, the now-null dragState lets the underlying product
            // mesh handle it as a fresh click and open Elements/Variations.
            productCanvasPointerDown = null;
            consumedProductPointerUpClick = {
              at: Date.now(),
              x: finishedDecorationPointer.x,
              y: finishedDecorationPointer.y,
            };
            return;
          }
          if (productCanvasPointerDown?.moved === true || isArcRotateCameraInertiaActive()) {
            suspendProductHoverForCameraMotion('pointer.up.after-drag', 160);
          }
          const pendingCanvasPointer = productCanvasPointerDown;
          if (pendingCanvasPointer?.targetKind === 'product') {
            productCanvasPointerDown = null;
            const rawButton = Number((event as any)?.button);
            const isLeftPointerUp = !!event && (!Number.isFinite(rawButton) || rawButton === 0) && (event as any)?.isPrimary !== false;
            if (!pendingCanvasPointer.moved && isLeftPointerUp) {
              const targetAtUp = pickSurfaceAtInternal(null, pointerInfo);
              const resolvedTarget = targetAtUp?.kind === 'product' || targetAtUp?.kind === 'mesh'
                ? targetAtUp
                : (!targetAtUp || targetAtUp.kind === 'none' ? pendingCanvasPointer.target ?? null : null);
              if (resolvedTarget?.kind === 'product' || resolvedTarget?.kind === 'mesh') {
                recordSurfaceInteraction(resolvedTarget, { pointerType: 'up-click' });
                lastProductPick = {
                  ...(clonePlain((resolvedTarget.metadata ?? {}) as any) as any),
                  productPartMeshId: resolvedTarget.meshId ?? undefined,
                  productPartPoint: resolvedTarget.point ?? undefined,
                  productPartNormal: resolvedTarget.normal ?? undefined,
                };
                const pickedMesh = pointerInfo?.pickInfo?.pickedMesh ?? findModelMesh(String(resolvedTarget.meshId ?? '').trim());
                if (!selectProductFromCanvasTarget(resolvedTarget, event ?? null, pickedMesh)) {
                  emitCanvasProductPartClick(resolvedTarget, event ?? null);
                }
                consumedProductPointerUpClick = {
                  at: Date.now(),
                  x: Number((event as any)?.clientX ?? pendingCanvasPointer.x) || pendingCanvasPointer.x,
                  y: Number((event as any)?.clientY ?? pendingCanvasPointer.y) || pendingCanvasPointer.y,
                };
              }
            }
            return;
          }
          if (pendingCanvasPointer?.targetKind === 'empty') {
            productCanvasPointerDown = null;
            const rawButton = Number((event as any)?.button);
            const isLeftPointerUp = !!event && (!Number.isFinite(rawButton) || rawButton === 0) && (event as any)?.isPrimary !== false;
            const targetAtUp = isLeftPointerUp ? pickSurfaceAtInternal(null, pointerInfo) : null;
            const isStillEmpty = !targetAtUp || targetAtUp.kind === 'none';
            if (!pendingCanvasPointer.moved && isLeftPointerUp && isStillEmpty) {
              setSelectedObject(null);
            }
          }
          return;
        }
        if (type === PointerEventTypes.POINTERDOWN) {
          const pickedMesh = pointerInfo?.pickInfo?.pickedMesh ?? null;
          const pickedFixedSlotIndicator = pickFixedSlot3DIndicator(
            event ?? null,
            pickedMesh,
            pointerInfo?.pickInfo?.pickedPoint ?? null,
          );

          // ── 3D marker: click on billboard indicator → open linked module ──
          if (pickedFixedSlotIndicator) {
            if (Date.now() < suppressFixedSlotIndicatorPointerUntil) return;
            try { (event as any)?.preventDefault?.(); } catch {}
            try { (event as any)?.stopPropagation?.(); } catch {}
            productCanvasPointerDown = null;
            consumedProductPointerUpClick = {
              at: Date.now(),
              x: Number((event as any)?.clientX ?? 0) || 0,
              y: Number((event as any)?.clientY ?? 0) || 0,
            };
            activateFixedSlot3DIndicator(pickedFixedSlotIndicator);
            return;
          }

          // ── 3D marker: placement mode → next click places the marker ─────
          const fsPlacing3D = (useAsukaStore as any).getState?.()?.fixedSlotsPlacing ?? null;
          if (fsPlacing3D) {
            try { (event as any)?.preventDefault?.(); } catch {}
            try { (event as any)?.stopPropagation?.(); } catch {}
            const surface = pickPointerModelSurface(event ?? null, null);
            if (surface?.rawPosition && surface?.targetMesh) {
              const newSlotId = `fixed-3d-${Math.random().toString(36).slice(2)}-${Date.now()}`;
              const newSlot = {
                id: newSlotId,
                type: fsPlacing3D.type,
                x: 0, y: 0,
                surface3d: {
                  meshId: String(surface.targetMesh.id || surface.targetMesh.name || 'product'),
                  position: { x: surface.position.x, y: surface.position.y, z: surface.position.z },
                  rawPosition: { x: surface.rawPosition.x, y: surface.rawPosition.y, z: surface.rawPosition.z },
                  normal: { x: surface.normal.x, y: surface.normal.y, z: surface.normal.z },
                  surfaceRight: surface.surfaceRight ? { x: surface.surfaceRight.x, y: surface.surfaceRight.y, z: surface.surfaceRight.z } : null,
                  surfaceUp: surface.surfaceUp ? { x: surface.surfaceUp.x, y: surface.surfaceUp.y, z: surface.surfaceUp.z } : null,
                  faceId: Number.isFinite(Number((surface as any).faceId)) ? Number((surface as any).faceId) : null,
                },
                constraints: fsPlacing3D.constraints ?? {},
                label: String(fsPlacing3D.label ?? ''),
                color: String(fsPlacing3D.color ?? ''),
                targetWidth: fsPlacing3D.targetWidth,
                targetHeight: fsPlacing3D.targetHeight,
              };
              try { (useAsukaStore as any).getState?.()?.addFixedSlot?.(newSlot); } catch {}
              try { (useAsukaStore as any).getState?.()?.cancelPlaceFixedSlot?.(); } catch {}
              create3DSlotIndicator(newSlot);
              // Ignore the duplicate notification Babylon can emit for the same
              // pointer gesture after the indicator has just been created.
              suppressFixedSlotIndicatorPointerUntil = Date.now() + 220;
            }
            return;
          }

          // Assembly guides can sit in front of the product mesh. Resolve the
          // component with a dedicated product-only pick before generic surface
          // routing so the Move tool stays reliable with technical guides shown.
          if (startAssemblyComponentDrag(event ?? null, pickedMesh)) return;

          const target = pickSurfaceAtInternal(null, pointerInfo);
          recordSurfaceInteraction(target, { pointerType: 'down' });
          if (target?.kind === 'object' && target.objectId) {
            productCanvasPointerDown = null;
            if (target.printAreaId) selectPrintAreaInternal(target.printAreaId, false);
            setSelectedObject(target.objectId);
            if (isDraggableDecoration(target.objectId)) startDecorationDrag(target.objectId, event ?? null, pickedMesh);
            return;
          }
          if (target?.kind === 'print-area' && target.printAreaId) {
            selectPrintAreaInternal(target.printAreaId);
            return;
          }
          if (target?.kind === 'product' || target?.kind === 'mesh') {
            lastProductPick = {
              ...(clonePlain((target.metadata ?? {}) as any) as any),
              productPartMeshId: target.meshId ?? undefined,
              productPartPoint: target.point ?? undefined,
              productPartNormal: target.normal ?? undefined,
            };
            productCanvasPointerDown = event ? {
              x: Number((event as any).clientX) || 0,
              y: Number((event as any).clientY) || 0,
              moved: false,
              targetKind: 'product',
              target: clonePlain(target) as AsukaSurfacePickTarget,
            } : null;
            // Do not clear Product 3D selection on pointer down: orbiting the
            // camera starts with a pointer down on the product surface too.
            // Selection is now cleared only by a real click on empty space.
            return;
          }
          productCanvasPointerDown = event ? {
            x: Number((event as any).clientX) || 0,
            y: Number((event as any).clientY) || 0,
            moved: false,
            targetKind: 'empty',
          } : null;
          return;
        }
        if (type !== PointerEventTypes.POINTERPICK) return;
        const pickedFixedSlotIndicator = pickFixedSlot3DIndicator(
          event ?? null,
          pointerInfo?.pickInfo?.pickedMesh ?? null,
          pointerInfo?.pickInfo?.pickedPoint ?? null,
        );
        if (pickedFixedSlotIndicator) {
          if (Date.now() >= suppressFixedSlotIndicatorPointerUntil) {
            activateFixedSlot3DIndicator(pickedFixedSlotIndicator);
          }
          return;
        }
        if (dragState?.moved) return;
        const target = pickSurfaceAtInternal(null, pointerInfo);
        recordSurfaceInteraction(target, { pointerType: 'pick' });
        if (target?.kind === 'object' && target.objectId) {
          if (target.printAreaId) selectPrintAreaInternal(target.printAreaId, false);
          setSelectedObject(target.objectId);
          return;
        }
        if (target?.kind === 'print-area' && target.printAreaId) {
          selectPrintAreaInternal(target.printAreaId);
          return;
        }
        if (target?.kind === 'product' || target?.kind === 'mesh') {
          const consumed = consumedProductPointerUpClick;
          if (consumed && Date.now() - consumed.at <= 220) {
            const clientX = Number((event as any)?.clientX);
            const clientY = Number((event as any)?.clientY);
            const samePointer = !Number.isFinite(clientX) || !Number.isFinite(clientY)
              || ((clientX - consumed.x) * (clientX - consumed.x) + (clientY - consumed.y) * (clientY - consumed.y)) <= 64;
            consumedProductPointerUpClick = null;
            if (samePointer) return;
          } else if (consumed) {
            consumedProductPointerUpClick = null;
          }
          const wasCameraDrag = productCanvasPointerDown?.moved === true;
          productCanvasPointerDown = null;
          lastProductPick = {
            ...(clonePlain((target.metadata ?? {}) as any) as any),
            productPartMeshId: target.meshId ?? undefined,
            productPartPoint: target.point ?? undefined,
            productPartNormal: target.normal ?? undefined,
          };
          if (!wasCameraDrag) {
            const pickedMesh = pointerInfo?.pickInfo?.pickedMesh ?? null;
            if (!selectProductFromCanvasTarget(target, event ?? null, pickedMesh)) {
              emitCanvasProductPartClick(target, event ?? null);
            }
          }
          return;
        }
        // Do not clear a Product 3D selection from POINTERPICK with no target.
        // Babylon can emit target-less pick notifications while hover/pointer state
        // changes, so deselection must stay tied to the explicit empty-space
        // pointerdown -> pointerup path handled above.
        productCanvasPointerDown = null;
      });
      ensureBaseScene();
      engine.runRenderLoop(() => {
        const perfEnabled = isAsuka3DPerfDebugEnabled();
        const renderStart = perfEnabled ? perfNow() : 0;
        try { scene?.render(); } catch {}
        if (perfEnabled) {
          const renderMs = perfNow() - renderStart;
          noteCameraFirstRenderAfterInput('renderLoop', renderMs);
          noteRenderLoopPerf(renderMs);
        }
      });
      try {
        resizeObserver = new ResizeObserver(() => {
          scheduleEngineResize();
        });
        resizeObserver.observe(target);
      } catch {}
      scheduleEngineResize();
    },
    unmount: () => {
      flushCameraStatePersist('camera.unmount');
      clear3DObjectPreview();
      try { if (cameraPersistTimer) window.clearTimeout(cameraPersistTimer); } catch {}
      cameraPersistTimer = null;
      if (dragState) finishDecorationDrag(null);
      if (assemblyConstructionModeActive) setAssemblyConstructionModeInternal(false);
      else if (assemblyMoveToolActive) setAssemblyMoveToolInternal(false);
      else if (assemblyComponentDragState) finishAssemblyComponentDrag(null, true);
      detachAssemblyMoveToolDomListeners();
      if (printAreaAddPickState) cancelPrintAreaAddPickInternal();
      if (printSurfaceFacePickState) cancelPrintSurfaceFacePickingInternal();
      if (printAreaDrawState) cancelPrintAreaDrawingInternal();
      else detachPrintAreaDrawingDomListeners();
      if (assemblyBuildState) cancelAssemblyDrawingInternal();
      else detachAssemblyBuildDomListeners();
      cancelScheduledEngineResize();
      if (cameraFocusRaf) {
        try { cancelAnimationFrame(cameraFocusRaf); } catch {}
        cameraFocusRaf = 0;
      }
      try { resizeObserver?.disconnect(); } catch {}
      resizeObserver = null;
      for (const url of Array.from(objectUrls)) {
        try { URL.revokeObjectURL(url); } catch {}
      }
      objectUrls.clear();
      for (const raf of Array.from(printAreaTextureRenderRafs.values())) {
        try { cancelAnimationFrame(raf); } catch {}
      }
      printAreaTextureRenderRafs.clear();
      for (const timer of Array.from(printAreaTextureRenderTimers.values())) {
        try { clearTimeout(timer); } catch {}
      }
      printAreaTextureRenderTimers.clear();
      printAreaImageLoadAreaIds.clear();
      printAreaImageCache.clear();
      clearProductHover(true);
      try {
        if (canvas && canvasPointerLeaveHandler) canvas.removeEventListener('pointerleave', canvasPointerLeaveHandler);
      } catch {}
      canvasPointerLeaveHandler = null;
      try {
        if (scene && pointerObserver) scene.onPointerObservable.remove(pointerObserver);
      } catch {}
      pointerObserver = null;
      try { unsubFixedSlots3D?.(); } catch {}
      unsubFixedSlots3D = null;
      _callEnsureIndicatorsForSlots3D = null;
      try { for (const [id] of fixedSlot3DIndicators) remove3DSlotIndicator(id); fixedSlot3DIndicators.clear(); } catch {}
      fixedSlot3DConsumedIds.clear();
      try { if (camera && cameraViewObserver) (camera as any).onViewMatrixChangedObservable?.remove?.(cameraViewObserver); } catch {}
      cameraViewObserver = null;
      try { if (productHoverRestoreTimer != null) window.clearTimeout(productHoverRestoreTimer); } catch {}
      productHoverRestoreTimer = null;
      clearScheduledProductGroupCenter();
      try { cameraInputDiagnosticsCleanup?.(); } catch {}
      cameraInputDiagnosticsCleanup = null;
      clearPrintAreaTextures();
      disposePreloadedModelAssets();
      clearRuntimeProductMaterials();
      clearAssemblyEnvironment();
      clearAssemblyBuildPreview();
      runtimeProductCatalog = null;
      runtimeProductCatalogsByIdentity.clear();
      runtimeParametricCatalog.clear();
      runtimeParametricElementByProductId.clear();
      runtimeAssemblyCatalog.clear();
      runtimeAssemblyElementByProductId.clear();
      parametricBaselinesByProductId.clear();
      parametricMeasurementsByProductId.clear();
      try { scene?.dispose(); } catch {}
      try { engine?.dispose(); } catch {}
      lastBabylonEngineDisposeAt = Date.now();
      try { canvas?.remove(); } catch {}
      canvas = null;
      scene = null;
      engine = null;
      camera = null;
      modelMeshes = [];
      clearPrintAreaVisuals();
      for (const id of Array.from(decalFragmentsByObjectId.keys())) disposeDecalFragments(id, true);
      objectMeshes.clear();
      objectKinds.clear();
      decalFragmentsByObjectId.clear();
    },
    activateView: (viewId: string) => {
      try { (useAsukaStore as any).getState?.()?.setActiveView?.(viewId); } catch {}
    },
    deactivateView: () => {},
    loadView,
    exportView,
    },
    history: {
      getInfo: () => get3DHistoryInfo(),
      commit: (options?: AsukaHistoryOptions) => runAsukaRendererMutation('3d', 'history.commit', () => {
        commit3DHistorySnapshot(options ?? {});
        return get3DHistoryInfo();
      }),
      exportTrack: (viewId?: string | null) => export3DHistoryTrack(viewId),
      importTrack: (viewId: string, track?: unknown | null) => runAsukaRendererMutation('3d', 'history.importTrack', () => {
        import3DHistoryTrack(viewId, (track ?? null) as Asuka3DHistoryTrackState | null);
        return get3DHistoryInfo(viewId);
      }),
      undo: () => runAsukaRendererMutation('3d', 'history.undo', async () => {
        await restore3DHistoryIndex(get3DHistoryTrack(currentViewId, false).index - 1, 'undo');
        return get3DHistoryInfo();
      }),
      redo: () => runAsukaRendererMutation('3d', 'history.redo', async () => {
        await restore3DHistoryIndex(get3DHistoryTrack(currentViewId, false).index + 1, 'redo');
        return get3DHistoryInfo();
      }),
      reset: () => runAsukaRendererMutation('3d', 'history.reset', async () => {
        await restore3DHistoryIndex(0, 'reset');
        return get3DHistoryInfo();
      }),
      goTo: (index: number) => runAsukaRendererMutation('3d', 'history.goTo', async () => {
        await restore3DHistoryIndex(index, 'goto');
        return get3DHistoryInfo();
      }),
    },
    scene: {
      getGraph: () => getSceneGraphNodes(),
    },
    products: {
      list: () => getProductsFromState(currentState),
      get: (id: string) => getProductsFromState(currentState).find((product) => getProductId(product) === String(id ?? '').trim()) ?? null,
      add: (input: AsukaElementInput) => runAsukaRendererMutation('3d', 'products.add', () => applyProduct(input), { id: (object) => object.id }),
      remove: (id?: string, productOptions?: Record<string, unknown>) => {
        const productId = String(id ?? getSelectedProductId() ?? '').trim();
        const previous = productId ? { id: productId, kind: 'product-3d' } : null;
        return runAsukaRendererMutation('3d', 'products.remove', async () => {
          await removeProduct(id, productOptions ?? {});
          return previous;
        }, { changed: previous != null, id: productId || null });
      },
      select: (id: string | null, productOptions?: { emit?: boolean; source?: string; selectObject?: boolean }) => selectProductInternal(id, productOptions ?? {}),
      update: (element: unknown, productOptions?: Record<string, unknown>) => runAsukaRendererMutation('3d', 'products.update', () => updateProduct(element, productOptions ?? {}), { changed: (object) => object != null, id: (object) => object?.id ?? null }),
      syncCatalog: (elements: any[]) => {
        syncRuntimeProductCatalog(elements);
        synchronizeParametricCatalogAndScene(elements);
        syncRuntimeAssemblyCatalog(elements);
      },
      getParameters: (productId?: string | null) => buildParametricDescriptor(productId),
      validateParameters: (productId?: string | null, values?: Record<string, unknown>) => buildParametricDescriptor(productId, values ?? {}),
      setParameter: (productId: string | null | undefined, parameterId: string, value: AsukaParametricValue, mutationOptions?: AsukaParametricMutationOptions) => runAsukaRendererMutation('3d', 'products.setParameter', () => applyParametricProductState(productId, { [String(parameterId ?? '').trim()]: value }, mutationOptions ?? {}), { changed: (descriptor) => descriptor != null, id: productId ?? null }),
      setParameters: (productId: string | null | undefined, values: Record<string, unknown>, mutationOptions?: AsukaParametricMutationOptions) => runAsukaRendererMutation('3d', 'products.setParameters', () => applyParametricProductState(productId, values ?? {}, mutationOptions ?? {}), { changed: (descriptor) => descriptor != null, id: productId ?? null }),
    },
    assembly: {
      syncCatalog: (elements: any[]) => syncRuntimeAssemblyCatalog(elements),
      getState: () => clonePlain(getAssemblyState()),
      getBuildState: () => clonePlain(getAssemblyBuildStateInternal()),
      getInteractionState: () => clonePlain(getAssemblyInteractionStateInternal()),
      setConstructionMode: (active: boolean) => clonePlain(setAssemblyConstructionModeInternal(active)),
      setMoveTool: (active: boolean) => clonePlain(setAssemblyMoveToolInternal(active)),
      configure: (patch: Partial<AsukaAssemblyState>, mutationOptions?: AsukaAssemblyMutationOptions) => {
        assertAssemblyEditPermission('configure');
        return runAsukaRendererMutation(
          '3d',
          'assembly.configure',
          () => configureAssemblyInternal(patch ?? {}, mutationOptions ?? {}),
          { changed: true, id: currentViewId },
        );
      },
      listSurfaces: () => clonePlain(listAssemblySurfacesInternal()),
      getSurfaceMetrics: (id: string) => {
        const floor = listAssemblySurfacesInternal().find(
          (surface): surface is AsukaAssemblyFloorSurface => (
            surface.id === String(id ?? '').trim() && surface.kind === 'floor'
          ),
        ) ?? null;
        return floor ? clonePlain(getAssemblyFloorMetrics(floor)) : null;
      },
      getSurfaceTransform: (id: string) => {
        const surface = listAssemblySurfacesInternal().find(
          (entry) => entry.id === String(id ?? '').trim(),
        ) ?? null;
        return clonePlain(getAssemblySurfaceTransform(surface));
      },
      selectSurface: (id: string | null) => clonePlain(selectAssemblySurfaceInternal(id)),
      startDrawing: (input: AsukaAssemblyStartDrawingInput) => startAssemblyDrawingInternal(input),
      cancelDrawing: () => cancelAssemblyDrawingInternal(),
      updateSurface: (id: string, patch: AsukaAssemblySurfacePatch, mutationOptions?: AsukaAssemblyMutationOptions) => {
        assertAssemblyEditPermission('updateSurface');
        return runAsukaRendererMutation(
          '3d',
          'assembly.updateSurface',
          () => updateAssemblySurfaceInternal(id, patch ?? {}, mutationOptions ?? {}),
          { changed: (surface) => surface != null, id },
        );
      },
      transformSurface: (
        id: string,
        input: AsukaAssemblySurfaceTransformInput,
        mutationOptions?: AsukaAssemblyMutationOptions,
      ) => {
        assertAssemblyEditPermission('transformSurface');
        return runAsukaRendererMutation(
          '3d',
          'assembly.transformSurface',
          () => {
            const surface = listAssemblySurfacesInternal().find(
              (entry) => entry.id === String(id ?? '').trim(),
            ) ?? null;
            if (!surface) return null;
            const transformed = transformAssemblySurface(surface, input ?? {});
            const patch = transformed.kind === 'floor'
              ? {
                  points: transformed.points,
                  pointIds: transformed.pointIds,
                }
              : {
                  start: transformed.start,
                  end: transformed.end,
                };
            return updateAssemblySurfaceInternal(surface.id, patch, mutationOptions ?? {});
          },
          { changed: (surface) => surface != null, id },
        );
      },
      setFloorEdgeLength: (
        floorId: string,
        edgeIndex: number,
        length: number,
        mutationOptions?: AsukaAssemblyMutationOptions,
      ) => {
        assertAssemblyEditPermission('setFloorEdgeLength');
        return runAsukaRendererMutation(
          '3d',
          'assembly.setFloorEdgeLength',
          () => {
            const floor = (getAssemblyState().plan?.floors ?? []).find(
              (surface) => surface.id === String(floorId ?? '').trim(),
            ) ?? null;
            if (!floor) return null;
            const nextFloor = setAssemblyFloorEdgeLength(floor, edgeIndex, length);
            if (!isAssemblyPolygonSimple(nextFloor.points)) return null;
            const updated = updateAssemblySurfaceInternal(floor.id, {
              points: nextFloor.points,
              pointIds: nextFloor.pointIds,
            }, mutationOptions ?? {});
            return updated?.kind === 'floor' ? updated : null;
          },
          { changed: (surface) => surface != null, id: floorId },
        );
      },
      setFloorEdgeAngle: (
        floorId: string,
        edgeIndex: number,
        angleDegrees: number,
        mutationOptions?: AsukaAssemblyMutationOptions,
      ) => {
        assertAssemblyEditPermission('setFloorEdgeAngle');
        return runAsukaRendererMutation(
          '3d',
          'assembly.setFloorEdgeAngle',
          () => {
            const floor = (getAssemblyState().plan?.floors ?? []).find(
              (surface) => surface.id === String(floorId ?? '').trim(),
            ) ?? null;
            if (!floor) return null;
            const nextFloor = setAssemblyFloorEdgeAngle(floor, edgeIndex, angleDegrees);
            if (!isAssemblyPolygonSimple(nextFloor.points)) return null;
            const updated = updateAssemblySurfaceInternal(floor.id, {
              points: nextFloor.points,
              pointIds: nextFloor.pointIds,
            }, mutationOptions ?? {});
            return updated?.kind === 'floor' ? updated : null;
          },
          { changed: (surface) => surface != null, id: floorId },
        );
      },
      setFloorEdgeConstraint: (
        floorId: string,
        edgeIndex: number,
        constraint: AsukaAssemblyFloorEdgeConstraint,
        mutationOptions?: AsukaAssemblyMutationOptions,
      ) => {
        assertAssemblyEditPermission('setFloorEdgeConstraint');
        return runAsukaRendererMutation(
          '3d',
          'assembly.setFloorEdgeConstraint',
          () => {
            const floor = (getAssemblyState().plan?.floors ?? []).find(
              (surface) => surface.id === String(floorId ?? '').trim(),
            ) ?? null;
            if (!floor) return null;
            const nextFloor = setAssemblyFloorEdgeConstraint(floor, edgeIndex, constraint ?? {});
            const updated = updateAssemblySurfaceInternal(floor.id, {
              edgeConstraints: nextFloor.edgeConstraints,
            }, mutationOptions ?? {});
            return updated?.kind === 'floor' ? updated : null;
          },
          { changed: (surface) => surface != null, id: floorId },
        );
      },
      setRoomWallEnabled: (
        floorId: string,
        edgeIndex: number,
        enabled: boolean,
        mutationOptions?: AsukaAssemblyMutationOptions,
      ) => {
        assertAssemblyEditPermission('setRoomWallEnabled');
        return runAsukaRendererMutation(
          '3d',
          'assembly.setRoomWallEnabled',
          () => {
            const floor = (getAssemblyState().plan?.floors ?? []).find(
              (surface) => surface.id === String(floorId ?? '').trim(),
            ) ?? null;
            if (!floor || floor.metadata?.roomSurface !== true) return null;
            const nextFloor = setAssemblyRoomEdgeWallEnabled(floor, edgeIndex, enabled);
            const edgeId = getAssemblyFloorEdgeId(floor, edgeIndex);
            const linkedWall = (getAssemblyState().plan?.walls ?? []).find(
              (wall) => String(wall.metadata?.roomEdgeId ?? '').trim() === edgeId,
            ) ?? null;
            const roomWallTemplates = (
              floor.metadata?.roomWallTemplates
              && typeof floor.metadata.roomWallTemplates === 'object'
              && !Array.isArray(floor.metadata.roomWallTemplates)
            )
              ? clonePlain(floor.metadata.roomWallTemplates as Record<string, unknown>)
              : {};
            if (!enabled && edgeId && linkedWall) {
              roomWallTemplates[edgeId] = clonePlain(linkedWall);
            }
            const updated = updateAssemblySurfaceInternal(floor.id, {
              pointIds: nextFloor.pointIds,
              metadata: {
                ...(nextFloor.metadata ?? {}),
                roomWallTemplates,
              },
            }, mutationOptions ?? {});
            return updated?.kind === 'floor' ? updated : null;
          },
          { changed: (surface) => surface != null, id: floorId },
        );
      },
      listMaterialPresets: () => clonePlain(getAssemblyMaterialPresets(getAssemblyState())),
      saveMaterialPreset: (
        preset: Partial<AsukaAssemblyMaterialPreset>,
        mutationOptions?: AsukaAssemblyMutationOptions,
      ) => {
        assertAssemblyEditPermission('saveMaterialPreset');
        return runAsukaRendererMutation(
          '3d',
          'assembly.saveMaterialPreset',
          () => {
            const next = saveAssemblyMaterialPreset(getAssemblyState(), preset ?? {});
            configureAssemblyInternal({
              materialLibrary: next.materialLibrary,
            }, mutationOptions ?? {});
            const wantedId = String(preset?.id ?? '').trim();
            const presets = getAssemblyMaterialPresets(getAssemblyState());
            return clonePlain(
              (wantedId ? presets.find((entry) => entry.id === wantedId) : presets[presets.length - 1])
              ?? null,
            );
          },
          { changed: (presetResult) => presetResult != null, id: (presetResult) => presetResult?.id ?? null },
        );
      },
      removeMaterialPreset: (
        presetId: string,
        mutationOptions?: AsukaAssemblyMutationOptions,
      ) => {
        assertAssemblyEditPermission('removeMaterialPreset');
        return runAsukaRendererMutation(
          '3d',
          'assembly.removeMaterialPreset',
          () => {
            const before = getAssemblyState();
            const next = removeAssemblyMaterialPreset(before, presetId);
            const changed = JSON.stringify(before.materialLibrary ?? [])
              !== JSON.stringify(next.materialLibrary ?? []);
            if (changed) {
              configureAssemblyInternal({
                materialLibrary: next.materialLibrary,
              }, mutationOptions ?? {});
            }
            return changed;
          },
          { changed: (changed) => changed === true, id: presetId },
        );
      },
      applyMaterialPreset: (
        surfaceId: string,
        presetId: string,
        applyOptions?: AsukaAssemblyMaterialApplyOptions,
        mutationOptions?: AsukaAssemblyMutationOptions,
      ) => {
        assertAssemblyEditPermission('applyMaterialPreset');
        return runAsukaRendererMutation(
          '3d',
          'assembly.applyMaterialPreset',
          () => {
            const next = applyAssemblyMaterialPreset(
              getAssemblyState(),
              surfaceId,
              presetId,
              applyOptions ?? {},
            );
            configureAssemblyInternal({ plan: next.plan }, mutationOptions ?? {});
            return listAssemblySurfacesInternal().find(
              (surface) => surface.id === String(surfaceId ?? '').trim(),
            ) ?? null;
          },
          { changed: (surface) => surface != null, id: surfaceId },
        );
      },
      removeSurface: (id: string, mutationOptions?: AsukaAssemblyMutationOptions) => {
        assertAssemblyEditPermission('removeSurface');
        return runAsukaRendererMutation(
          '3d',
          'assembly.removeSurface',
          () => removeAssemblySurfaceInternal(id, mutationOptions ?? {}),
          { changed: (surface) => surface != null, id },
        );
      },
      clearSurfaces: (mutationOptions?: AsukaAssemblyMutationOptions) => {
        assertAssemblyEditPermission('clearSurfaces');
        return runAsukaRendererMutation(
          '3d',
          'assembly.clearSurfaces',
          () => clearAssemblySurfacesInternal(mutationOptions ?? {}),
          { changed: true, id: currentViewId },
        );
      },
      listComponents: () => clonePlain(listAssemblyComponents()),
      getComponent: (id: string) => clonePlain(listAssemblyComponents().find((component) => component.id === String(id ?? '').trim()) ?? null),
      previewPlacement: (id: string, input?: AsukaAssemblyPlacementInput) => clonePlain(resolveAssemblyPlacement(
        getAssemblyState(),
        listAssemblyComponents(),
        String(id ?? '').trim(),
        input ?? {},
      )),
      place: (id: string, input?: AsukaAssemblyPlacementInput, mutationOptions?: AsukaAssemblyMutationOptions) => {
        assertAssemblyEditPermission('place');
        return runAsukaRendererMutation(
          '3d',
          'assembly.place',
          () => placeAssemblyComponentInternal(String(id ?? '').trim(), input ?? {}, mutationOptions ?? {}),
          { changed: (result) => result.changed, id },
        );
      },
      autoPlace: (id: string, mutationOptions?: AsukaAssemblyMutationOptions) => {
        assertAssemblyEditPermission('autoPlace');
        return runAsukaRendererMutation(
          '3d',
          'assembly.autoPlace',
          () => autoPlaceAssemblyComponentInternal(String(id ?? '').trim(), mutationOptions ?? {}),
          { changed: (result) => result.changed, id },
        );
      },
      connect: (connection: Partial<AsukaAssemblyConnection>, mutationOptions?: AsukaAssemblyMutationOptions) => {
        assertAssemblyEditPermission('connect');
        return runAsukaRendererMutation(
          '3d',
          'assembly.connect',
          () => connectAssemblyInternal(connection ?? {}, mutationOptions ?? {}),
          { changed: (result) => result != null, id: (result) => result?.id ?? null },
        );
      },
      disconnect: (connectionId: string, mutationOptions?: AsukaAssemblyMutationOptions) => {
        assertAssemblyEditPermission('disconnect');
        return runAsukaRendererMutation(
          '3d',
          'assembly.disconnect',
          () => disconnectAssemblyInternal(connectionId, mutationOptions ?? {}),
          { changed: (result) => result != null, id: connectionId },
        );
      },
      listStackLinks: () => clonePlain(getAssemblyState().stackLinks ?? []),
      linkStack: (link: Partial<AsukaAssemblyStackLink>, mutationOptions?: AsukaAssemblyMutationOptions) => {
        assertAssemblyEditPermission('linkStack');
        return runAsukaRendererMutation(
          '3d',
          'assembly.linkStack',
          () => linkAssemblyStackInternal(link ?? {}, mutationOptions ?? {}),
          { changed: (result) => result != null, id: (result) => result?.id ?? null },
        );
      },
      unlinkStack: (linkId: string, mutationOptions?: AsukaAssemblyMutationOptions) => {
        assertAssemblyEditPermission('unlinkStack');
        return runAsukaRendererMutation(
          '3d',
          'assembly.unlinkStack',
          () => unlinkAssemblyStackInternal(linkId, mutationOptions ?? {}),
          { changed: (result) => result != null, id: linkId },
        );
      },
      validate: () => clonePlain(validateAssembly(getAssemblyState(), listAssemblyComponents())),
      getBom: (bomOptions?: { currency?: string }) => clonePlain(createAssemblyBom(listAssemblyComponents(), bomOptions ?? {})),
      getQuote: (quoteOptions?: AsukaAssemblyQuoteOptions) => clonePlain(createAssemblyQuote(listAssemblyComponents(), quoteOptions ?? {})),
      export: async (kind: AsukaAssemblyExportKind, exportOptions?: AsukaAssemblyExportOptions) => createAssemblyExportBlob(
        kind,
        getAssemblyState(),
        listAssemblyComponents(),
        exportOptions ?? {},
      ),
    },
    models: {
      listMeshes: () => getModelMeshSummaries(),
      inspectParts: (input: { model: Asuka3DModel }) => inspectModelParts(input),
      preload: (input: { model: Asuka3DModel }) => preloadModelAsset(input),
    },
    parts: {
      list: () => listPartsInternal(),
      get: (id: string) => getPartInternal(id),
      select: (id: string | null) => selectPartInternal(id),
      getSelected: () => getSelectedPartInternal(),
      listVariations: (partId: string) => listPartVariationsInternal(partId),
      getActiveVariation: (partId: string) => getActivePartVariationInternal(partId),
      applyVariation: (partId: string, variationId: string, partOptions?: Asuka3DPartVariationApplyOptions) => runAsukaRendererMutation('3d', 'parts.applyVariation', () => applyPartVariationInternal(partId, variationId, partOptions), { changed: (variation) => variation != null, id: partId }),
      focus: (input?: any) => focusPart(input ?? {}),
    },
    materials: {
      list: () => getMaterialSummaries(),
      select: (id: string | null) => selectMaterial(id),
      update: (id: string, patch: Asuka3DMaterialPatch) => runAsukaRendererMutation('3d', 'materials.update', () => {
        updateMaterial(id, patch);
        return getMaterialSummaries().find((material) => material.id === id) ?? null;
      }, { changed: (material) => material != null, id }),
    },
    areas: {
      list: () => clonePlain(getPrintAreas()),
      get: (id: string) => clonePlain(getPrintAreaById(id)),
      pickForAdd: () => pickPrintAreaForAddInternal(),
      cancelPickForAdd: () => cancelPrintAreaAddPickInternal(),
      create: (input?: any) => runAsukaRendererMutation('3d', 'areas.create', () => clonePlain(createPrintAreaInternal(input ?? {})), { changed: (area) => area != null, id: (area) => area?.id }),
      update: (id: string, patch: any) => runAsukaRendererMutation('3d', 'areas.update', () => clonePlain(updatePrintAreaInternal(id, patch)), { changed: (area) => area != null, id }),
      remove: (id: string) => {
        const previous = clonePlain(getPrintAreaById(id));
        return runAsukaRendererMutation('3d', 'areas.remove', () => {
          removePrintAreaInternal(id);
          return previous;
        }, { changed: previous != null, id });
      },
      select: (id: string | null) => selectPrintAreaInternal(id),
      getActive: () => clonePlain(getPrintAreaById(getActivePrintAreaId())),
      focus: (id: string) => focusPrintAreaInternal(id),
      getBounds: (id: string) => clonePlain(get3DPrintAreaBoundsBox(id)),
      contains: (id: string, printAreaId?: string | null, areaOptions?: AsukaPrintAreaConstraintOptions) => clonePlain(get3DConstraintStatus(id, printAreaId, areaOptions)),
      constrain: (id: string, printAreaId?: string | null, areaOptions?: AsukaPrintAreaConstraintOptions) => runAsukaRendererMutation('3d', 'areas.constrain', () => clonePlain(constrain3DObjectToPrintArea(id, printAreaId, areaOptions)), { id }),
      moveObject: (id: string, delta: Partial<AsukaObjectTransform> & AsukaObjectPatch, areaOptions?: AsukaPrintAreaConstraintOptions) => runAsukaRendererMutation('3d', 'areas.moveObject', () => {
        move3DObjectWithinPrintArea(id, delta, areaOptions);
        return clonePlain(get3DConstraintStatus(id, areaOptions?.printAreaId ?? areaOptions?.targetPrintAreaId ?? null, areaOptions));
      }, { id }),
      transformObject: (id: string, patch: Partial<AsukaObjectTransform> & AsukaObjectPatch, areaOptions?: AsukaPrintAreaConstraintOptions) => runAsukaRendererMutation('3d', 'areas.transformObject', () => {
        transform3DObjectWithinPrintArea(id, patch, areaOptions);
        return clonePlain(get3DConstraintStatus(id, areaOptions?.printAreaId ?? areaOptions?.targetPrintAreaId ?? null, areaOptions));
      }, { id }),
      startDrawing: (input?: any) => startPrintAreaDrawingInternal(input ?? {}),
      cancelDrawing: () => cancelPrintAreaDrawingInternal(),
      startFacePicking: (input?: any) => startPrintSurfaceFacePickingInternal(input ?? {}),
      cancelFacePicking: () => cancelPrintSurfaceFacePickingInternal(),
      classifyMesh: (meshId: string) => classifyMeshForPrintArea(String(meshId ?? '').trim()),
    },
    surface: {
      pick: (surfaceOptions?: AsukaSurfacePickOptions) => clonePlain(pickSurfaceAtInternal(surfaceOptions ?? null)),
      getInteractionDebug: () => getSurfaceInteractionDebugInternal(),
      getLayerRenderingDebug: (id?: string) => clonePlain(get3DLayerDebugSnapshot(id)),
      getLayerRenderingDebugReport: (id?: string) => clonePlain({
        logs: Array.isArray((globalThis as any).__ASUKA_3D_LAYER_LOGS__) ? (globalThis as any).__ASUKA_3D_LAYER_LOGS__ : [],
        snapshot: get3DLayerDebugSnapshot(id),
      }),
    },
    objects: {
      list: (objectOptions?: AsukaObjectListOptions) => get3DObjectSummaries(objectOptions ?? {}),
      get: (id: string) => get3DObjectSummaries({
        objectId: id,
        includeReadonly: true,
        includeTechnical: true,
        includeScreenBox: true,
        source: 'getObject',
      })[0] ?? null,
      getBounds: (id: string) => clonePlain(get3DObjectBoundsBox(id)),
      measure: (id: string, options?: AsukaLayerMeasurementOptions) => clonePlain(measure3DObject(id, options)),
      reorder: (orderedIds: string[], objectOptions?: AsukaObjectOrderOptions) => {
        const ids = Array.isArray(orderedIds) ? orderedIds.map((id) => String(id ?? '').trim()).filter(Boolean) : [];
        let changed = false;
        return runAsukaRendererMutation('3d', 'objects.reorder', () => {
          changed = reorder3DObjects(ids, objectOptions);
          return ids;
        }, { changed: () => changed });
      },
    addImage: (input: AsukaImageInput) => runAsukaRendererMutation('3d', 'objects.addImage', async () => {
      input = await resolve3DLayerPrintAreaInput(input as any) as AsukaImageInput;
      const id = makeId('image');
      debug3DLayer('addImage:start', { id, input });
      const normalizedSource = await prepareTransparent3DImageSource(input as any);
      let url = normalizedSource.url;
      const svg = normalizedSource.svg;
      if (url && /^blob:/i.test(url)) objectUrls.add(url);
      if (!url && typeof File !== "undefined" && input.file instanceof File) {
        // Persist user-uploaded images as data URLs so exported 3D scenes can be
        // restored after a page reload. Blob URLs are runtime-only and would make
        // the JSON state unusable later.
        url = await fileToDataUrl(input.file);
      }
      if (!url) throw new Error('3D addImage requires a url/src or File.');
      const imageMetrics = await getImageMetricsFromLoadedImage(url);
      input = {
        ...input,
        metadata: {
          ...(input.metadata ?? {}),
          ...(imageMetrics ? { sourcePixelWidth: imageMetrics.width, sourcePixelHeight: imageMetrics.height } : null),
          ...(svg ? { sourceVector: true } : null),
        },
      };
      const area = getActivePrintArea(input);

      // If a 3D fixed slot (marker) is active, inject its surface position so
      // the image lands exactly where the user placed the marker, then consume
      // the slot (hide its indicator until the content is removed).
      const activeSlotId3D = (useAsukaStore as any).getState?.()?.fixedSlotActiveId ?? null;
      const activeSlot3D = activeSlotId3D
        ? ((useAsukaStore as any).getState?.()?.fixedSlots ?? []).find((s: any) => s?.id === activeSlotId3D && s?.surface3d)
        : null;
      const surface3DFromSlot = activeSlot3D?.surface3d ?? null;
      if (activeSlotId3D) {
        try { (useAsukaStore as any).getState?.()?.setFixedSlotActive?.(null); } catch {}
        fixedSlot3DConsumedIds.add(String(activeSlotId3D));
        const _m3DImg = fixedSlot3DIndicators.get(String(activeSlotId3D));
        if (_m3DImg) { _m3DImg.isVisible = false; _m3DImg.isPickable = false; }
      }

      console.warn('[ASUKA-DEBUG] addImage:start', { id, hasArea: !!area, areaId: area?.id, areaMode: (area as any)?.mode, isFaceNet: isExactFaceNetPrintArea(area), isTextureSlot: shouldUseTextureSlotForNewObject(area) });
      debug3DLayer('addImage:source-and-area', { id, url, svg: svg || undefined, area: clonePlain(area), normalizedSource, activePrintAreaId: getActivePrintAreaId() });
      if (shouldUseTextureSlotForNewObject(area)) {
        const object = makeTexturePrintAreaObject(area as Asuka3DPrintArea, id, 'image', { url, svg: svg || undefined, color: input.color ?? input.fill ?? undefined }, input as any);
        return addTexturePrintAreaObject(area as Asuka3DPrintArea, object);
      }
      const useDecal = shouldUseDecalForPrintArea(area);
      const imageAspect = getImageAspectRatioFromSource(url) ?? imageMetrics?.aspect ?? getImageAspectRatioFromMetadata(input.metadata);
      debug3DLayer('addImage:placement-mode', { id, useDecal, textureSlotPath: false, area: clonePlain(area), imageAspect });

      // Build projectionOverride from active 3D slot surface data
      const slotProjectionOverride = surface3DFromSlot ? (() => {
        try {
          const targetMesh = findModelMesh(surface3DFromSlot.meshId) ?? findModelMesh(null);
          if (!targetMesh) return null;
          const pos = surface3DFromSlot.position;
          const nor = surface3DFromSlot.normal;
          const raw = surface3DFromSlot.rawPosition ?? pos;
          const rawPos = new Vector3(raw.x, raw.y, raw.z);
          const faceId = Number((surface3DFromSlot as any).faceId);
          const frame = Number.isFinite(faceId) ? getPickGeometricWorldFrame({ faceId }, targetMesh, rawPos) : null;
          let normal = frame?.normal ?? null;
          if (!normal || normal.lengthSquared() < 0.0001) normal = new Vector3(nor.x, nor.y, nor.z);
          if (normal.lengthSquared() < 0.0001) normal = new Vector3(0, 0, 1);
          normal.normalize();
          if (camera) {
            const toCamera = camera.position.subtract(rawPos);
            if (toCamera.lengthSquared() > 0.0001 && Vector3.Dot(normal, toCamera) < 0) normal = normal.scale(-1);
          }
          const storedRight = (surface3DFromSlot as any).surfaceRight;
          const storedUp = (surface3DFromSlot as any).surfaceUp;
          const surfaceRight = frame?.right ?? (storedRight ? new Vector3(storedRight.x, storedRight.y, storedRight.z) : null);
          const surfaceUp = frame?.up ?? (storedUp ? new Vector3(storedUp.x, storedUp.y, storedUp.z) : null);
          const position = surface3DFromSlot.rawPosition
            ? new Vector3(pos.x, pos.y, pos.z)
            : rawPos.add(normal.scale(0.006));
          const tw = Number(activeSlot3D.targetWidth) > 0 ? Number(activeSlot3D.targetWidth) / 100 : 0.25;
          const th = Number(activeSlot3D.targetHeight) > 0 ? Number(activeSlot3D.targetHeight) / 100 : tw;
          return { targetMesh, position, normal, size: new Vector3(tw, th, Math.max(tw, th) * 2), rawPosition: rawPos, surfaceRight, surfaceUp, ...(Number.isFinite(faceId) ? { faceId } : null), __asukaFromFixedSlot3D: true };
        } catch { return null; }
      })() : null;

      let mesh = useDecal ? createImageDecal(id, url, 'image', input.name, slotProjectionOverride ? null : area, slotProjectionOverride, imageAspect) : null;
      const faceNetFailed = !mesh && isExactFaceNetPrintArea(area);
      if (faceNetFailed) {
        debug3DLayer('face-net:plane-fallback-blocked', { id, type: 'image', source: 'addImage', area: clonePlain(area) }, 'warn');
        console.warn('[ASUKA-DEBUG] face-net:failed -> plane fallback blocked for surface print area');
      }
      if (!mesh && faceNetFailed) throw new Error('Unable to project image on the selected 3D print area.');
      if (!mesh) mesh = createImagePlane(id, url, 'image', input.name, area, imageAspect);
      console.warn('[ASUKA-DEBUG] addImage:final-mesh', { meshCreated: !!mesh, mode: projectionModeFromMesh(mesh, useDecal ? 'decal' : 'plane'), pos: mesh ? { x: (mesh as any).position?.x, y: (mesh as any).position?.y, z: (mesh as any).position?.z } : null, scaling: mesh ? { x: (mesh as any).scaling?.x, y: (mesh as any).scaling?.y } : null, isEnabled: (mesh as any)?.isEnabled?.(), visibility: (mesh as any)?.visibility });
      const placementMode = projectionModeFromMesh(mesh, useDecal ? 'decal' : 'plane');
      const placement = decorationPlacementFromMesh(mesh, placementMode);
      decorationRecords.push({ id, type: 'image', targetMeshId: String((mesh as any)?.__asukaTargetMeshId ?? area?.targetMeshId ?? ''), targetPrintAreaId: area?.id, placement, source: { url, svg: svg || undefined, color: input.color ?? input.fill ?? undefined }, price: input.price, printingMethodId: input.printingMethodId ?? null, metadata: { ...(input.metadata ?? {}), ...(normalizedSource.rasterized ? { rasterizedSvg: true } : null), ...(area?.id ? { printAreaId: area.id } : null), ...(activeSlot3D ? getFixedSlot3DDecorationMetadata(activeSlot3D) : null), ...getDecalRuntimeMetadata(mesh), initialPlacement: clonePlain(placement) } } as DecorationRecord);
      setSelectedObject(id, false);
      debug3DLayer('addImage:done', { id, placementMode, placement: clonePlain(placement), mesh: summarizeLayerMeshForDebug(mesh), fragments: (decalFragmentsByObjectId.get(id) ?? []).map((fragment) => summarizeLayerMeshForDebug(fragment)), record: summarizeBabylonDebugValue(decorationRecords[decorationRecords.length - 1]) }, mesh ? 'info' : 'warn');
      try { _callEnsureIndicatorsForSlots3D?.(); } catch {}
      schedule3DLayerRenderCheck(id, 'addImage:render-check');
      emitChanged('addImage', id);
      requestSelectionMenuForAdded3DLayer(id, 'addImage');
      return objectRef(id, 'image');
    }, { id: (object) => object.id }),
    addText: (input: AsukaTextInput) => runAsukaRendererMutation('3d', 'objects.addText', async () => {
      input = await resolve3DLayerPrintAreaInput(input as any) as AsukaTextInput;
      const id = makeId('text');
      debug3DLayer('addText:start', { id, input });
      const area = getActivePrintArea(input);
      debug3DLayer('addText:area', { id, area: clonePlain(area), activePrintAreaId: getActivePrintAreaId() });

      const activeSlotId3DText = (useAsukaStore as any).getState?.()?.fixedSlotActiveId ?? null;
      const activeSlot3DText = activeSlotId3DText
        ? ((useAsukaStore as any).getState?.()?.fixedSlots ?? []).find((s: any) => s?.id === activeSlotId3DText && s?.surface3d)
        : null;
      const surface3DText = activeSlot3DText?.surface3d ?? null;
      const effectiveTextInput = activeSlot3DText
        ? constrainFixedSlotTextInput(input, activeSlot3DText.constraints)
        : input;
      if (activeSlotId3DText) {
        try { (useAsukaStore as any).getState?.()?.setFixedSlotActive?.(null); } catch {}
        fixedSlot3DConsumedIds.add(String(activeSlotId3DText));
        const _m3DText = fixedSlot3DIndicators.get(String(activeSlotId3DText));
        if (_m3DText) { _m3DText.isVisible = false; _m3DText.isPickable = false; }
      }

      if (shouldUseTextureSlotForNewObject(area)) {
        const object = makeTexturePrintAreaObject(area as Asuka3DPrintArea, id, 'text', {
          text: effectiveTextInput.text,
          color: effectiveTextInput.color || '#111827',
          fontFamily: effectiveTextInput.fontFamily || 'Arial',
          fontSize: effectiveTextInput.fontSize,
        }, effectiveTextInput as any);
        return addTexturePrintAreaObject(area as Asuka3DPrintArea, object);
      }
      const useDecal = shouldUseDecalForPrintArea(area);
      debug3DLayer('addText:placement-mode', { id, useDecal, area: clonePlain(area) });
      const textSlotProjectionOverride = surface3DText ? (() => {
        try {
          const targetMesh = findModelMesh(surface3DText.meshId) ?? findModelMesh(null);
          if (!targetMesh) return null;
          const pos = surface3DText.position;
          const nor = surface3DText.normal;
          const raw = surface3DText.rawPosition ?? pos;
          const rawPos = new Vector3(raw.x, raw.y, raw.z);
          const faceId = Number((surface3DText as any).faceId);
          const frame = Number.isFinite(faceId) ? getPickGeometricWorldFrame({ faceId }, targetMesh, rawPos) : null;
          let normal = frame?.normal ?? null;
          if (!normal || normal.lengthSquared() < 0.0001) normal = new Vector3(nor.x, nor.y, nor.z);
          if (normal.lengthSquared() < 0.0001) normal = new Vector3(0, 0, 1);
          normal.normalize();
          if (camera) {
            const toCamera = camera.position.subtract(rawPos);
            if (toCamera.lengthSquared() > 0.0001 && Vector3.Dot(normal, toCamera) < 0) normal = normal.scale(-1);
          }
          const storedRight = (surface3DText as any).surfaceRight;
          const storedUp = (surface3DText as any).surfaceUp;
          const surfaceRight = frame?.right ?? (storedRight ? new Vector3(storedRight.x, storedRight.y, storedRight.z) : null);
          const surfaceUp = frame?.up ?? (storedUp ? new Vector3(storedUp.x, storedUp.y, storedUp.z) : null);
          const position = surface3DText.rawPosition
            ? new Vector3(pos.x, pos.y, pos.z)
            : rawPos.add(normal.scale(0.006));
          const tw = Number(activeSlot3DText.targetWidth) > 0 ? Number(activeSlot3DText.targetWidth) / 100 : 0.25;
          const th = Number(activeSlot3DText.targetHeight) > 0 ? Number(activeSlot3DText.targetHeight) / 100 : tw * 0.4;
          return { targetMesh, position, normal, size: new Vector3(tw, th, Math.max(tw, th) * 2), rawPosition: rawPos, surfaceRight, surfaceUp, ...(Number.isFinite(faceId) ? { faceId } : null), __asukaFromFixedSlot3D: true };
        } catch { return null; }
      })() : null;
      let mesh = useDecal ? createTextDecal(id, effectiveTextInput, textSlotProjectionOverride ? null : area, textSlotProjectionOverride) : null;
      const faceNetFailed = !mesh && isExactFaceNetPrintArea(area);
      if (faceNetFailed) {
        debug3DLayer('face-net:plane-fallback-blocked', { id, type: 'text', source: 'addText', area: clonePlain(area) }, 'warn');
      }
      if (!mesh && faceNetFailed) throw new Error('Unable to project text on the selected 3D print area.');
      if (!mesh) mesh = createTextPlane(id, effectiveTextInput, area);
      const placementMode = projectionModeFromMesh(mesh, useDecal ? 'decal' : 'plane');
      const placement = decorationPlacementFromMesh(mesh, placementMode);
      decorationRecords.push({ id, type: 'text', targetMeshId: String((mesh as any)?.__asukaTargetMeshId ?? area?.targetMeshId ?? ''), targetPrintAreaId: area?.id, placement, source: { text: effectiveTextInput.text, color: effectiveTextInput.color || undefined, fontFamily: effectiveTextInput.fontFamily || undefined }, price: effectiveTextInput.price, printingMethodId: effectiveTextInput.printingMethodId ?? null, metadata: { ...(effectiveTextInput.metadata ?? {}), ...(area?.id ? { printAreaId: area.id } : null), ...(activeSlot3DText ? getFixedSlot3DDecorationMetadata(activeSlot3DText) : null), ...getDecalRuntimeMetadata(mesh), initialPlacement: clonePlain(placement) } } as DecorationRecord);
      setSelectedObject(id, false);
      debug3DLayer('addText:done', { id, placementMode, placement: clonePlain(placement), mesh: summarizeLayerMeshForDebug(mesh), fragments: (decalFragmentsByObjectId.get(id) ?? []).map((fragment) => summarizeLayerMeshForDebug(fragment)), record: summarizeBabylonDebugValue(decorationRecords[decorationRecords.length - 1]) }, mesh ? 'info' : 'warn');
      try { _callEnsureIndicatorsForSlots3D?.(); } catch {}
      schedule3DLayerRenderCheck(id, 'addText:render-check');
      emitChanged('addText', id);
      requestSelectionMenuForAdded3DLayer(id, 'addText');
      return objectRef(id, 'text');
    }, { id: (object) => object.id }),
    addDesign: (input: AsukaDesignInput) => runAsukaRendererMutation('3d', 'objects.addDesign', async () => {
      input = await resolve3DLayerPrintAreaInput(input as any) as AsukaDesignInput;
      const id = makeId('design');
      debug3DLayer('addDesign:start', { id, input });
      const normalizedSource = await prepareTransparent3DImageSource(input as any);
      const url = normalizedSource.url;
      const svg = normalizedSource.svg;
      if (url && /^blob:/i.test(url)) objectUrls.add(url);
      if (!url) throw new Error('3D addDesign requires a url/src or svg/svgText.');
      const imageMetrics = await getImageMetricsFromLoadedImage(url);
      input = {
        ...input,
        metadata: {
          ...(input.metadata ?? {}),
          ...(imageMetrics ? { sourcePixelWidth: imageMetrics.width, sourcePixelHeight: imageMetrics.height } : null),
          ...(svg ? { sourceVector: true } : null),
        },
      };
      const area = getActivePrintArea(input);
      debug3DLayer('addDesign:source-and-area', { id, url, svg: svg || undefined, area: clonePlain(area), normalizedSource, activePrintAreaId: getActivePrintAreaId() });

      // Slot handling — same pattern as addImage / addText.
      const activeSlotId3DDesign = (useAsukaStore as any).getState?.()?.fixedSlotActiveId ?? null;
      const activeSlot3DDesign = activeSlotId3DDesign
        ? ((useAsukaStore as any).getState?.()?.fixedSlots ?? []).find((s: any) => s?.id === activeSlotId3DDesign && s?.surface3d)
        : null;
      const surface3DDesign = activeSlot3DDesign?.surface3d ?? null;
      if (activeSlotId3DDesign) {
        try { (useAsukaStore as any).getState?.()?.setFixedSlotActive?.(null); } catch {}
        fixedSlot3DConsumedIds.add(String(activeSlotId3DDesign));
        const _m3DDesign = fixedSlot3DIndicators.get(String(activeSlotId3DDesign));
        if (_m3DDesign) { _m3DDesign.isVisible = false; _m3DDesign.isPickable = false; }
      }
      const designSlotProjectionOverride = surface3DDesign ? (() => {
        try {
          const targetMesh = findModelMesh(surface3DDesign.meshId) ?? findModelMesh(null);
          if (!targetMesh) return null;
          const pos = surface3DDesign.position;
          const nor = surface3DDesign.normal;
          const raw = surface3DDesign.rawPosition ?? pos;
          const rawPos = new Vector3(raw.x, raw.y, raw.z);
          const faceId = Number((surface3DDesign as any).faceId);
          const frame = Number.isFinite(faceId) ? getPickGeometricWorldFrame({ faceId }, targetMesh, rawPos) : null;
          let normal = frame?.normal ?? null;
          if (!normal || normal.lengthSquared() < 0.0001) normal = new Vector3(nor.x, nor.y, nor.z);
          if (normal.lengthSquared() < 0.0001) normal = new Vector3(0, 0, 1);
          normal.normalize();
          if (camera) {
            const toCamera = camera.position.subtract(rawPos);
            if (toCamera.lengthSquared() > 0.0001 && Vector3.Dot(normal, toCamera) < 0) normal = normal.scale(-1);
          }
          const storedRight = (surface3DDesign as any).surfaceRight;
          const storedUp = (surface3DDesign as any).surfaceUp;
          const surfaceRight = frame?.right ?? (storedRight ? new Vector3(storedRight.x, storedRight.y, storedRight.z) : null);
          const surfaceUp = frame?.up ?? (storedUp ? new Vector3(storedUp.x, storedUp.y, storedUp.z) : null);
          const position = surface3DDesign.rawPosition
            ? new Vector3(pos.x, pos.y, pos.z)
            : rawPos.add(normal.scale(0.006));
          const tw = Number(activeSlot3DDesign.targetWidth) > 0 ? Number(activeSlot3DDesign.targetWidth) / 100 : 0.25;
          const th = Number(activeSlot3DDesign.targetHeight) > 0 ? Number(activeSlot3DDesign.targetHeight) / 100 : tw;
          return { targetMesh, position, normal, size: new Vector3(tw, th, Math.max(tw, th) * 2), rawPosition: rawPos, surfaceRight, surfaceUp, ...(Number.isFinite(faceId) ? { faceId } : null), __asukaFromFixedSlot3D: true };
        } catch { return null; }
      })() : null;

      if (shouldUseTextureSlotForNewObject(area)) {
        const object = makeTexturePrintAreaObject(area as Asuka3DPrintArea, id, 'design', { url, svg: svg || undefined, color: input.color ?? input.fill ?? undefined }, input as any);
        return addTexturePrintAreaObject(area as Asuka3DPrintArea, object);
      }
      const useDecal = shouldUseDecalForPrintArea(area);
      const imageAspect = getImageAspectRatioFromSource(url) ?? imageMetrics?.aspect ?? getImageAspectRatioFromMetadata(input.metadata);
      debug3DLayer('addDesign:placement-mode', { id, useDecal, textureSlotPath: false, area: clonePlain(area), imageAspect });
      let mesh = useDecal ? createImageDecal(id, url, 'design', input.name, designSlotProjectionOverride ? null : area, designSlotProjectionOverride, imageAspect) : null;
      const faceNetFailed = !mesh && isExactFaceNetPrintArea(area);
      if (faceNetFailed) {
        debug3DLayer('face-net:plane-fallback-blocked', { id, type: 'design', source: 'addDesign', area: clonePlain(area) }, 'warn');
      }
      if (!mesh && faceNetFailed) throw new Error('Unable to project design on the selected 3D print area.');
      if (!mesh) mesh = createImagePlane(id, url, 'design', input.name, area, imageAspect);
      const placementMode = projectionModeFromMesh(mesh, useDecal ? 'decal' : 'plane');
      const placement = decorationPlacementFromMesh(mesh, placementMode);
      decorationRecords.push({ id, type: 'design', targetMeshId: String((mesh as any)?.__asukaTargetMeshId ?? area?.targetMeshId ?? ''), targetPrintAreaId: area?.id, placement, source: { url, svg: svg || undefined, color: input.color ?? input.fill ?? undefined }, price: input.price, printingMethodId: input.printingMethodId ?? null, metadata: { ...(input.metadata ?? {}), ...(normalizedSource.rasterized ? { rasterizedSvg: true } : null), ...(area?.id ? { printAreaId: area.id } : null), ...(activeSlot3DDesign ? getFixedSlot3DDecorationMetadata(activeSlot3DDesign) : null), ...getDecalRuntimeMetadata(mesh), initialPlacement: clonePlain(placement) } } as DecorationRecord);
      setSelectedObject(id, false);
      debug3DLayer('addDesign:done', { id, placementMode, placement: clonePlain(placement), mesh: summarizeLayerMeshForDebug(mesh), fragments: (decalFragmentsByObjectId.get(id) ?? []).map((fragment) => summarizeLayerMeshForDebug(fragment)), record: summarizeBabylonDebugValue(decorationRecords[decorationRecords.length - 1]) }, mesh ? 'info' : 'warn');
      try { _callEnsureIndicatorsForSlots3D?.(); } catch {}
      schedule3DLayerRenderCheck(id, 'addDesign:render-check');
      emitChanged('addDesign', id);
      requestSelectionMenuForAdded3DLayer(id, 'addDesign');
      return objectRef(id, 'design');
    }, { id: (object) => object.id }),
    addElement: (input: AsukaElementInput) => runAsukaRendererMutation('3d', 'objects.addElement', () => applyProduct(input), { id: (object) => object.id }),
    removeElement: (id?: string, options?: Record<string, unknown>) => {
      const productId = String(id ?? getSelectedProductId() ?? '').trim();
      const previous = productId ? { id: productId, kind: 'product-3d' } : null;
      return runAsukaRendererMutation('3d', 'objects.removeElement', async () => {
        await removeProduct(id, options ?? {});
        return previous;
      }, { changed: previous != null, id: productId || null });
    },
    remove: (id: string) => {
      const key = String(id ?? '').trim();
      const previous = get3DObjectSummaryById(key)
        ?? (key.startsWith('print-area:') && getPrintAreaById(key.slice('print-area:'.length)) ? { id: key, kind: 'print-area' } : null)
        ?? (getPrintAreaById(key) ? { id: key, kind: 'print-area' } : null);
      let changed = false;
      return runAsukaRendererMutation('3d', 'objects.remove', () => {
        const mesh = objectMeshes.get(key);
        if (key.startsWith('print-area:')) {
          changed = getPrintAreaById(key.slice('print-area:'.length)) != null;
          removePrintAreaInternal(key.slice('print-area:'.length));
          return previous;
        }
        if (getPrintAreas().some((area) => String(area?.id ?? '') === key)) {
          changed = true;
          removePrintAreaInternal(key);
          return previous;
        }
        const textureHit = findTexturePrintAreaObject(key);
        if (textureHit) {
          changed = true;
          updateTexturePrintAreaObjects(textureHit.area.id, (objects) => objects.filter((object) => String(object?.id ?? '') !== key));
          if (selectedObjectId === key) setSelectedObject(null, true, { commitProductDeselect: false });
          emitChanged('removeObject', key);
          return previous;
        }
        if (mesh && objectKinds.get(key) !== 'mesh') {
          changed = true;
          disposeDecalFragments(key, true);
          try { mesh.dispose(false, true); } catch {}
          objectMeshes.delete(key);
          objectKinds.delete(key);
          decorationRecords = decorationRecords.filter((deco) => String(deco.id ?? '') !== key);
          if (selectedObjectId === key) setSelectedObject(null, true, { commitProductDeselect: false });
          try { _callEnsureIndicatorsForSlots3D?.(); } catch {}
          emitChanged('removeObject', key);
        }
        return previous;
      }, { changed: () => changed, id: key || null });
    },
    update: (id: string, patch: AsukaObjectPatch) => runAsukaRendererMutation('3d', 'objects.update', () => {
      update3DObjectInternal(id, patch);
      return get3DObjectSummaryById(id);
    }, { changed: (object) => object != null, id }),
    duplicate: (id: string, objectOptions?: AsukaObjectDuplicateOptions) => runAsukaRendererMutation('3d', 'objects.duplicate', () => duplicate3DObject(id, objectOptions), { changed: (object) => object != null, id: (object) => object?.id ?? null }),
    setVisible: (id: string, visible: boolean) => runAsukaRendererMutation('3d', 'objects.setVisible', () => {
      set3DObjectVisible(id, visible);
      return get3DObjectSummaryById(id);
    }, { changed: (object) => object != null, id }),
    setLocked: (id: string, locked: boolean) => runAsukaRendererMutation('3d', 'objects.setLocked', () => {
      set3DObjectLocked(id, locked);
      return get3DObjectSummaryById(id);
    }, { changed: (object) => object != null, id }),
    bringForward: (id: string) => {
      let changed = false;
      return runAsukaRendererMutation('3d', 'objects.bringForward', () => {
        changed = move3DObjectRelative(id, -1);
        return get3DObjectSummaryById(id);
      }, { changed: () => changed, id });
    },
    sendBackward: (id: string) => {
      let changed = false;
      return runAsukaRendererMutation('3d', 'objects.sendBackward', () => {
        changed = move3DObjectRelative(id, 1);
        return get3DObjectSummaryById(id);
      }, { changed: () => changed, id });
    },
    getTransform: (id: string) => get3DObjectTransform(id),
    updateTransform: (id: string, patch: Partial<AsukaObjectTransform> & AsukaObjectPatch, objectOptions?: Record<string, unknown>) => runAsukaRendererMutation('3d', 'objects.updateTransform', () => {
      update3DObjectTransform(id, patch, objectOptions ?? {});
      return get3DObjectTransform(id);
    }, { changed: (transform) => transform != null, id }),
    preview: (id: string, previewOptions?: AsukaObjectPreviewOptions) => preview3DObject(id, previewOptions),
    clearPreview: () => clear3DObjectPreview(),
    },
    selection: {
      list: () => selectedObjectId
        ? get3DObjectSummaries({
            objectId: selectedObjectId,
            includeReadonly: true,
            includeTechnical: true,
            includeScreenBox: true,
            source: 'selection.list',
          })
        : [],
      getAnchorBox: () => get3DSelectionAnchorBox(),
      select: (id: string | null) => {
      const key = String(id ?? '').trim();
      if (key.startsWith('print-area:')) {
        selectPrintAreaInternal(key.slice('print-area:'.length));
        return;
      }
      if (getPrintAreas().some((area) => String(area?.id ?? '') === key)) {
        selectPrintAreaInternal(key);
        return;
      }
      const textureHit = findTexturePrintAreaObject(key);
      if (textureHit) {
        selectPrintAreaInternal(textureHit.area.id, false);
        setSelectedObject(key || null);
        emitChanged('selectObject', key);
        return;
      }
      setSelectedObject(key || null);
      },
    },
    viewport: {
      getCamera: () => readCameraView(),
      setCamera: (view: Partial<AsukaCameraView>, moveOptions?: AsukaCameraMoveOptions) => setCameraViewInternal(view, moveOptions ?? {}),
      getCameraLimits: () => getCameraLimitsInternal(),
      setCameraLimits: (limits: Partial<AsukaCameraLimits>) => setCameraLimitsInternal(limits),
      zoomIn: () => {
      const cam = requireCamera();
      cam.radius = Math.max(0.2, cam.radius * 0.88);
      emitCameraEvent({ action: 'zoomIn', viewId: currentViewId, camera: readCameraState() });
      emitChanged('camera.changed', null);
    },
      zoomOut: () => {
      const cam = requireCamera();
      cam.radius = cam.radius * 1.12;
      emitCameraEvent({ action: 'zoomOut', viewId: currentViewId, camera: readCameraState() });
      emitChanged('camera.changed', null);
    },
      fit: () => {
      fitView();
      emitCameraEvent({ action: 'fitView', viewId: currentViewId, camera: readCameraState() });
      emitChanged('camera.changed', null);
      },
      resize: () => scheduleEngineResize(),
    },
    exports: {
      isFormatAvailable: (format: string) => {
      const normalized = normalize3DExportFormat(format);
      return normalized === 'png' || normalized === 'json';
    },
      canLoadSupport: () => false,
      ensureSupport: async (format: string) => {
      const normalized = normalize3DExportFormat(format);
      return normalized === 'png' || normalized === 'json';
    },
      download: download3DInternal,
      downloadAllViews: async (exportOptions: AsukaExportOptions = {}) => {
        await downloadAll3DViews(exportOptions);
      },
      downloadAllAreas: async (exportOptions: AsukaExportOptions = {}) => {
        const entries = getPrintAreas().map((area) => ({
          target: { kind: 'area' as const, id: String(area?.id ?? '').trim() },
          name: String(area?.label ?? area?.id ?? 'drawing-area'),
        })).filter((entry) => !!entry.target.id);
        await downloadMany3DTargets(entries, { ...exportOptions, name: (exportOptions as any)?.name ?? 'drawing-areas' });
      },
      downloadAllObjects: async (exportOptions: AsukaExportOptions = {}) => {
        const entries = get3DObjectSummaries({ includeReadonly: false, includeTechnical: false }).map((object) => ({
          target: { kind: 'object' as const, id: String(object?.id ?? '').trim() },
          name: String(object?.label ?? object?.id ?? 'object'),
        })).filter((entry) => !!entry.target.id);
        await downloadMany3DTargets(entries, { ...exportOptions, name: (exportOptions as any)?.name ?? 'objects' });
      },
      png: export3DPngInternal,
    },
  };
}

export const createAsuka3DEngine = createAsukaBabylonRenderEngine;

try {
  const g = globalThis as any;
  g.AsukaDesigner = {
    ...(g.AsukaDesigner ?? {}),
    createAsuka3DEngine,
    createAsukaBabylonRenderEngine,
  };
} catch {}

export default createAsukaBabylonRenderEngine;
