/**
 * STEP 3D 查看器（对标新迪 NDS 查看器）
 * - STEP 直读：occt-import-js（浏览器内 WASM 转换，无需任何云服务）
 * - 渲染：three.js r160
 * - 功能：零件树 / 测量 / 剖切 / 爆炸 / 隐藏 / 拖动 / 复位 / 视角
 */
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/OrbitControls.js';
import { DragControls } from 'three/addons/DragControls.js';
import { CSS2DRenderer, CSS2DObject } from 'three/addons/CSS2DRenderer.js';

/* ================= 全局状态 ================= */
const S = {
  occt: null,
  result: null,
  modelGroup: null,
  meshObjs: [],        // THREE.Mesh 数组（与 result.meshes 一一对应）
  meshMeta: [],        // { center, dir, dragOffset }
  edgeObjs: [],
  edgeLines: [],       // 轮廓线（挂在 mesh 下）
  wireframeOn: true,
  treeNodes: [],       // 平铺的树节点 { node, row, children, parent, solidIds, isLeaf }
  rootNode: null,
  selected: null,      // 当前高亮的树节点
  tool: 'select',
  modelCenter: new THREE.Vector3(),
  modelRadius: 1,
  bbox: new THREE.Box3(),
  explodeAmount: 0,
  sectionOn: false,
  sectionAxis: 'z',
  sectionOffset: 0,
  sectionPlane: new THREE.Plane(new THREE.Vector3(0, 0, 1), 0),
  sectionVisual: null, // 半透明剖切面指示器
  measurements: [],
  measurePts: [],      // 待连线的测量点
  dragMembers: null,   // 当前拖动的成员集合
  dragLastPos: new THREE.Vector3(),
  axesHelper: null,
  bboxHelper: null,
  viewIndex: 0,
  meshFaces: [],       // 每个 mesh 的 brep 面数据（{first,last,color}[]）
  colorOverrides: {},  // meshIndex -> '#rrggbb'
  partBboxHelper: null,
  thicknessFaces: [],
  thicknessMarkers: null,
  meshEntryMap: {},    // meshIndex -> 最具体的树行（零件行），视口点选/拖动用
  modelSource: { type: 'none', value: '' }, // 模型来源：local / file(同源相对) / url(远程)
  faceHighlight: null, // 面拾取高亮覆盖层列表（S.faceHighlights）
  faceHighlights: [],
};

// 网格细分精度档位（occt-import-js 参数）
const QUALITY = {
  low:    { linearDeflection: 0.5,  angularDeflection: 0.6, meshQuality: 'normal' },
  medium: { linearDeflection: 0.2,  angularDeflection: 0.4, meshQuality: 'normal' },
  high:   { linearDeflection: 0.05, angularDeflection: 0.2, meshQuality: 'fine' },
};

/* ================= DOM ================= */
const $ = (id) => document.getElementById(id);
const dom = {
  canvasWrap: $('canvasWrap'), tree: $('tree'), treeSearch: $('treeSearch'),
  toolbar: $('toolbar'), status: $('statusText'), modelInfo: $('modelInfo'),
  fileName: $('fileName'), fileInput: $('fileInput'), loading: $('loading'),
  loadingText: $('loadingText'), dropOverlay: $('dropOverlay'),
  explodePanel: $('explodePanel'), explodeSlider: $('explodeSlider'), explodeVal: $('explodeVal'),
  sectionPanel: $('sectionPanel'), sectionSlider: $('sectionSlider'), sectionVal: $('sectionVal'),
  qualitySelect: $('qualitySelect'),
};

/* ================= 场景初始化 ================= */
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.localClippingEnabled = true;
// 不使用色调映射：上色所见即所得（选红就是红），显示风格与最初版本一致
dom.canvasWrap.appendChild(renderer.domElement);

const cssRenderer = new CSS2DRenderer();
cssRenderer.domElement.style.position = 'absolute';
cssRenderer.domElement.style.inset = '0';
cssRenderer.domElement.style.pointerEvents = 'none';
dom.canvasWrap.appendChild(cssRenderer.domElement);

const scene = new THREE.Scene();
scene.background = new THREE.Color(0xf1f3f2);

const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 100000);
camera.position.set(100, 80, 120);

// 灯光：环境光 + 主光 + 补光（明亮清晰），整体亮度可调
const BRIGHTNESS = { ambient: 1.15, key: 1.35, fill: 0.55 };
const ambientLight = new THREE.AmbientLight(0xffffff, BRIGHTNESS.ambient);
scene.add(ambientLight);
const keyLight = new THREE.DirectionalLight(0xffffff, BRIGHTNESS.key);
keyLight.position.set(1, 1.6, 1);
scene.add(keyLight);
const fillLight = new THREE.DirectionalLight(0xffffff, BRIGHTNESS.fill);
fillLight.position.set(-1, -0.4, -0.8);
scene.add(fillLight);

// 亮度调节（0.5x ~ 2x）
function setBrightness(mult) {
  ambientLight.intensity = BRIGHTNESS.ambient * mult;
  keyLight.intensity = BRIGHTNESS.key * mult;
  fillLight.intensity = BRIGHTNESS.fill * mult;
  $('brightnessVal').textContent = Math.round(mult * 100) + '%';
}
$('brightnessSlider').addEventListener('input', () => {
  setBrightness(Number($('brightnessSlider').value) / 100);
});

const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.1;
// 按键映射：中键=旋转，Shift+中键=平移（左键旋转、右键平移保持默认）
controls.mouseButtons = {
  LEFT: THREE.MOUSE.ROTATE,
  MIDDLE: THREE.MOUSE.ROTATE,
  RIGHT: THREE.MOUSE.PAN,
};
// 捕获阶段先于 OrbitControls 内部监听器执行，按下中键时按 Shift 状态切换映射
renderer.domElement.addEventListener('pointerdown', (e) => {
  if (e.button === 1) {
    controls.mouseButtons.MIDDLE = e.shiftKey ? THREE.MOUSE.PAN : THREE.MOUSE.ROTATE;
  }
}, true);
renderer.domElement.addEventListener('pointerup', () => {
  controls.mouseButtons.MIDDLE = THREE.MOUSE.ROTATE;
}, true);

S.modelGroup = new THREE.Group();
scene.add(S.modelGroup);

const dragControls = new DragControls([], camera, renderer.domElement);
dragControls.enabled = false;

const raycaster = new THREE.Raycaster();

// 按需渲染：场景无变化时不重绘，空闲时零 GPU 占用，交互更流畅
let needsRender = true;
function invalidate() { needsRender = true; }
controls.addEventListener('change', invalidate);            // 旋转/缩放/惯性阻尼
renderer.domElement.addEventListener('pointermove', invalidate); // 悬停高亮
renderer.domElement.addEventListener('pointerdown', invalidate); // 选中/测量
renderer.domElement.addEventListener('pointerup', invalidate);   // 拖动零件
renderer.domElement.addEventListener('wheel', invalidate);
window.addEventListener('input', invalidate);               // 亮度/爆炸/剖切滑杆

function resize() {
  const w = dom.canvasWrap.clientWidth, h = dom.canvasWrap.clientHeight;
  camera.aspect = w / h;
  camera.updateProjectionMatrix();
  renderer.setSize(w, h);
  cssRenderer.setSize(w, h);
  invalidate();
}
window.addEventListener('resize', resize);
resize();

function animate() {
  requestAnimationFrame(animate);
  controls.update();
  if (!needsRender) return;
  needsRender = false;
  renderer.render(scene, camera);
  cssRenderer.render(scene, camera);
}
animate();

/* ================= 工具函数 ================= */
function setStatus(text) { dom.status.textContent = text; }

function defaultColor() { return new THREE.Color(0.82, 0.84, 0.86); }

// occt-import-js 0.0.23 的 attributes/index 是 { array: 普通数组 } 包装，转成 TypedArray
function toTypedArray(v, Ctor) {
  let a = v;
  while (a && a.array && !ArrayBuffer.isView(a) && !Array.isArray(a)) a = a.array;
  if (ArrayBuffer.isView(a)) return a;
  if (Array.isArray(a) || (a && typeof a.length === 'number')) return new Ctor(a);
  return null;
}

// 按角度平滑法线：相邻面夹角 < angleDeg 时合并法线（曲面光滑、锐边保持）
function smoothNormals(geo, angleDeg) {
  const cosTh = Math.cos(THREE.MathUtils.degToRad(angleDeg));
  const pos = geo.attributes.position;
  const idx = geo.index;
  const vertTris = new Map();
  const triNormals = [];
  const vA = new THREE.Vector3(), vB = new THREE.Vector3(), vC = new THREE.Vector3();
  const e1 = new THREE.Vector3(), e2 = new THREE.Vector3(), n = new THREE.Vector3();
  for (let t = 0; t < idx.count; t += 3) {
    const a = idx.getX(t), b = idx.getX(t + 1), c = idx.getX(t + 2);
    vA.fromBufferAttribute(pos, a); vB.fromBufferAttribute(pos, b); vC.fromBufferAttribute(pos, c);
    e1.subVectors(vB, vA); e2.subVectors(vC, vA);
    n.crossVectors(e1, e2);
    triNormals.push(n.lengthSq() > 1e-20 ? n.clone().normalize() : new THREE.Vector3(0, 0, 1));
    for (const v of [a, b, c]) {
      if (!vertTris.has(v)) vertTris.set(v, []);
      vertTris.get(v).push(t / 3);
    }
  }
  const normals = new Float32Array(pos.count);
  const acc = new THREE.Vector3();
  vertTris.forEach((tris, v) => {
    acc.set(0, 0, 0);
    const base = triNormals[tris[0]];
    for (const ti of tris) {
      const tn = triNormals[ti];
      if (tn.dot(base) >= cosTh) acc.add(tn);
    }
    if (acc.lengthSq() < 1e-12) acc.copy(base);
    acc.normalize();
    normals[v * 3] = acc.x; normals[v * 3 + 1] = acc.y; normals[v * 3 + 2] = acc.z;
  });
  geo.setAttribute('normal', new THREE.BufferAttribute(normals, 3));
}

// 过暗的 CAD 颜色提亮到可见中灰（保持色相），纯黑/近黑直接用默认灰，避免渲染成黑色区域
function liftDarkColor(c) {
  if (!Array.isArray(c) || c.length < 3) return null;
  let [r, g, b] = c;
  const m = Math.max(r, g, b);
  if (m < 0.01) return [0.52, 0.54, 0.57]; // 纯黑面 → 可见深灰
  if (m >= 0.45) return [r, g, b];
  const s = 0.45 / Math.max(m, 1e-4);
  return [Math.min(r * s, 1), Math.min(g * s, 1), Math.min(b * s, 1)];
}

function occtColor(c) {
  if (!Array.isArray(c) || c.length < 3) return defaultColor();
  const lifted = liftDarkColor(c) || [0.82, 0.84, 0.86];
  const col = new THREE.Color(lifted[0], lifted[1], lifted[2]);
  if (col.r === 0 && col.g === 0 && col.b === 0) return defaultColor();
  return col;
}

function disposeScene() {
  S.meshObjs.forEach(m => {
    m.geometry.dispose();
    m.material.dispose();
  });
  S.edgeObjs.forEach(e => {
    e.geometry.dispose();
    e.material.dispose();
  });
  S.edgeLines.forEach(l => {
    l.geometry.dispose();
    l.material.dispose();
  });
  while (S.modelGroup.children.length) S.modelGroup.remove(S.modelGroup.children[0]);
  clearMeasurements();
  clearHighlight();
  S.meshObjs = [];
  S.meshMeta = [];
  S.edgeObjs = [];
  S.edgeLines = [];
  S.meshFaces = [];
  S.colorOverrides = {};
  S.meshEntryMap = {};
  S.treeNodes = [];
  S.rootNode = null;
  S.selected = null;
  S.explodeAmount = 0;
  setSectionOn(false);
  dom.explodeSlider.value = 0;
  dom.explodeVal.textContent = '0%';
  if (S.bboxHelper) { scene.remove(S.bboxHelper); S.bboxHelper.geometry.dispose(); S.bboxHelper = null; }
  if (S.sectionVisual) { scene.remove(S.sectionVisual); S.sectionVisual.geometry.dispose(); S.sectionVisual.material.dispose(); S.sectionVisual = null; }
  dom.tree.innerHTML = '<div class="tree-empty">加载模型后显示零件树</div>';
  dom.modelInfo.textContent = '';
}

/* ================= STEP 加载 ================= */
function showLoading(text) {
  dom.loadingText.textContent = text;
  dom.loading.classList.remove('hidden');
}
function hideLoading() { dom.loading.classList.add('hidden'); }

async function loadFile(file, source) {
  S.modelSource = source || { type: 'local', value: file.name };
  const name = (file.name || 'model.step').toLowerCase();
  if (!/\.(step|stp)$/.test(name)) {
    setStatus('请选择 .step / .stp 文件');
    return;
  }
  showLoading('模型加载中...');
  try {
    if (!S.occt) {
      S.occt = await (typeof occtimportjs === 'function'
        ? occtimportjs({ locateFile: (p) => './libs/' + p })
        : occtimportjs.default({ locateFile: (p) => './libs/' + p }));
    }
    const buf = new Uint8Array(await file.arrayBuffer());
    const q = EMBED_MODE ? QUALITY.high : (QUALITY[dom.qualitySelect.value] || QUALITY.high);
    showLoading(`正在转换 STEP 模型（精度：${dom.qualitySelect.selectedOptions[0].textContent}）...`);

    // 转换期间把进度日志显示到遮罩上
    const origLog = console.log;
    console.log = (...args) => {
      origLog(...args);
      const msg = String(args.join(' '));
      if (/%|mesh|conver/i.test(msg)) dom.loadingText.textContent = msg.slice(0, 80);
    };
    const readFn = S.occt.ReadStepFileAsync || S.occt.ReadStepFile;
    const result = await readFn.call(S.occt, buf, {
      meshUnit: 'millimeter',
      linearDeflection: q.linearDeflection,
      angularDeflection: q.angularDeflection,
      meshQuality: q.meshQuality,
      enableProgressLog: true,
    });
    console.log = origLog;

    if (!result || result.error) {
      hideLoading();
      setStatus('转换失败：' + (result && result.error ? result.error : '未知错误'));
      return;
    }
    buildModel(result, file.name);
    hideLoading();
    invalidate();
    dom.fileName.textContent = file.name;
    // 本地文件留着引用，嵌入面板里点「上传当前模型」才上传（与 2D 查看器一致）
    if (S.modelSource.type === 'local') S.currentFile = file;
  } catch (err) {
    console.error(err);
    hideLoading();
    setStatus('加载失败：' + err.message);
  }
}

/* ================= 模型构建 ================= */
function buildModel(result, fileName) {
  disposeScene();
  S.result = result;

  // ---- 网格 ----
  // occt-import-js 0.0.23：attributes.position / index 是 { array: 普通数组 } 包装对象
  const meshes = result.meshes || [];
  let triCount = 0;
  S.meshFaces = [];
  meshes.forEach((m, i) => {
    const geo = new THREE.BufferGeometry();
    const posArr = toTypedArray(m.attributes.position, Float32Array);
    const nrmArr = toTypedArray(m.attributes.normal, Float32Array);
    const idxArr = toTypedArray(m.index, Uint32Array);
    if (!posArr) return;
    geo.setAttribute('position', new THREE.BufferAttribute(posArr, 3));
    geo.setAttribute('normal', new THREE.BufferAttribute(nrmArr, 3));
    geo.setIndex(new THREE.BufferAttribute(idxArr, 1));
    geo.computeBoundingBox();
    // 按角度平滑法线：曲面光滑、锐边保持（30° 阈值）
    smoothNormals(geo, 30);
    triCount += geo.index.count / 3;

    const baseColor = occtColor(m.color || (m.name && m.name.color));

    // B-rep 面数据：{first,last,color}，供面测量（面积/半径/壁厚）与面级配色
    const faces = Array.isArray(m.brep_faces) ? m.brep_faces : null;
    S.meshFaces[i] = faces;

    const mat = new THREE.MeshPhongMaterial({
      color: baseColor,
      specular: 0x222222,
      shininess: 30,
      side: THREE.DoubleSide,
    });

    // 面级颜色（CAD 原色，过暗自动提亮）：逐三角形展开成顶点颜色
    // 注意：顶点颜色不走材质 color 的 sRGB→线性转换，必须手动指定 sRGB 空间
    if (faces && faces.length > 0) {
      const colors = new Float32Array(idxArr.length);
      const col = new THREE.Color();
      for (const f of faces) {
        const raw = Array.isArray(f.color) && f.color.length >= 3 ? f.color : [0.82, 0.84, 0.86];
        const fc = liftDarkColor(raw) || [0.82, 0.84, 0.86];
        col.setRGB(fc[0], fc[1], fc[2], THREE.SRGBColorSpace);
        const triStart = f.first * 3;
        const triEnd = Math.min((f.last + 1) * 3, idxArr.length);
        for (let k = triStart; k < triEnd; k++) {
          const v = idxArr[k] * 3;
          colors[v] = col.r; colors[v + 1] = col.g; colors[v + 2] = col.b;
        }
      }
      geo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
      mat.vertexColors = true;
    }

    const obj = new THREE.Mesh(geo, mat);
    obj.userData.index = i;
    obj.userData.baseColor = baseColor.clone();
    S.modelGroup.add(obj);
    S.meshObjs.push(obj);
  });

  // ---- 轮廓线框（occt 0.0.23 不输出边线，用二面角 20° 提取锐边）----
  S.meshObjs.forEach((m) => {
    const edgeGeo = new THREE.EdgesGeometry(m.geometry, 20);
    const line = new THREE.LineSegments(edgeGeo, new THREE.LineBasicMaterial({
      color: 0x2b2b2b, transparent: true, opacity: 0.55,
    }));
    line.raycast = () => {};          // 不参与拾取
    line.userData.isEdge = true;
    line.visible = S.wireframeOn;
    m.add(line);                      // 挂在 mesh 下：随爆炸/拖动移动、随隐藏消失
    S.edgeLines.push(line);
  });

  // ---- 边线（装配体轮廓，更像 CAD 查看器）----
  const edges = result.edges || [];
  edges.forEach((e) => {
    const geo = new THREE.BufferGeometry();
    geo.setAttribute('position', new THREE.BufferAttribute(e.attributes.position, 3));
    const hasColor = e.attributes.color && e.attributes.color.length === e.attributes.position.length;
    if (hasColor) geo.setAttribute('color', new THREE.BufferAttribute(e.attributes.color, 3));
    const mat = new THREE.LineBasicMaterial({
      color: 0x333333,
      vertexColors: hasColor,
      transparent: true,
      opacity: 0.55,
    });
    const lines = new THREE.LineSegments(geo, mat);
    lines.userData.isEdge = true;
    S.modelGroup.add(lines);
    S.edgeObjs.push(lines);
  });

  // ---- 包围盒 / 中心 / 爆炸方向 ----
  S.bbox = new THREE.Box3();
  S.meshObjs.forEach((m) => S.bbox.expandByObject(m));
  if (S.bbox.isEmpty()) S.bbox.setFromCenterAndSize(new THREE.Vector3(), new THREE.Vector3(1, 1, 1));
  S.bbox.getCenter(S.modelCenter);
  S.modelRadius = S.bbox.getSize(new THREE.Vector3()).length() / 2;
  S.meshMeta = S.meshObjs.map((m) => {
    const c = new THREE.Vector3();
    m.geometry.boundingBox.getCenter(c);
    const dir = c.clone().sub(S.modelCenter);
    if (dir.lengthSq() < 1e-12) dir.set(0, 0, 1);
    return { center: c, dir: dir.normalize(), dragOffset: new THREE.Vector3() };
  });

  // ---- 零件树 ----
  buildTree(result.root, fileName);

  // ---- 剖切范围（按新模型包围盒重设）----
  const size = S.bbox.getSize(new THREE.Vector3());
  const maxHalf = Math.max(size.x, size.y, size.z) / 2;
  dom.sectionSlider.min = String(Math.round(-maxHalf));
  dom.sectionSlider.max = String(Math.round(maxHalf));
  S.sectionOffset = 0;
  dom.sectionSlider.value = 0;
  updateSectionVal();
  updateSectionPlane();
  if (S.sectionOn) updateSectionPlane();

  // ---- 相机适配 ----
  fitCamera();

  // ---- 状态 ----
  const partCount = S.meshObjs.length;
  const mm = (v) => Math.round(v) + ' mm';
  dom.modelInfo.textContent = `${partCount} 个零件 | ${(triCount / 1000).toFixed(0)}k 三角面 | 尺寸 ${mm(size.x)} × ${mm(size.y)} × ${mm(size.z)}`;
  setStatus(`模型加载完成：${fileName}`);
  updateDragControlsObjects();
}

function fitCamera() {
  const dir = new THREE.Vector3(1, 0.6, 1).normalize();
  camera.position.copy(S.modelCenter).addScaledVector(dir, S.modelRadius * 2.4);
  camera.near = Math.max(S.modelRadius / 1000, 0.1);
  camera.far = S.modelRadius * 100;
  camera.updateProjectionMatrix();
  controls.target.copy(S.modelCenter);
  controls.update();
}

/* ================= 零件树 ================= */
const ICONS = {
  root: './icons/tree-root.48094390.png',
  part: './icons/tree-Part.e3fb53a4.png',
  views: './icons/tree-views.7d4f57c8.png',
  captures: './icons/tree-captures.7dc1f3b5.png',
  open: './icons/tree-node-open.f45d6681.png',
  close: './icons/tree-node-close.9d625f70.png',
  check: './icons/ico-tree-check.7c4874a5.png',
  checked: './icons/ico-tree-checked.905447ca.png',
  partial: './icons/ico-tree-checking.bd34b24b.png',
};

function meshVisibility(meshIndex) {
  const m = S.meshObjs[meshIndex];
  return m ? m.visible : true;
}

function buildTree(root, fileName) {
  dom.tree.innerHTML = '';
  if (!root) { dom.tree.innerHTML = '<div class="tree-empty">无模型结构</div>'; return; }
  const rootName = (fileName || '').replace(/\.(step|stp)$/i, '');
  // 无名根节点且只有一个子节点时，直接用子节点作树根（贴合 STEP 原始结构）
  let treeRoot = root;
  if ((!root.name || !String(root.name).trim()) && root.children && root.children.length === 1) {
    treeRoot = root.children[0];
  }

  const createRow = (node, depth, parentEntry, isRoot) => {
    const row = document.createElement('div');
    row.className = 'tree-row';
    row.style.paddingLeft = (6 + depth * 16) + 'px';

    const nodeChildren = node.children || [];
    const nodeMeshes = node.meshes || node.solidIds || [];
    const hasChildren = nodeChildren.length > 0;
    const expander = document.createElement('span');
    expander.className = hasChildren ? 'tree-expander' : 'tree-spacer';
    const box = document.createElement('div');
    box.className = 'tree-children';
    if (hasChildren) {
      expander.innerHTML = `<img src="${ICONS.open}" alt="">`;
      expander.addEventListener('click', (ev) => {
        ev.stopPropagation();
        const collapsed = box.classList.toggle('hidden');
        expander.innerHTML = `<img src="${collapsed ? ICONS.close : ICONS.open}" alt="">`;
      });
    }
    row.appendChild(expander);

    // 图标（0.0.23 无 productType，用 children 判断装配/零件）
    const icon = document.createElement('img');
    icon.className = 'tree-icon';
    icon.src = isRoot ? ICONS.root : (hasChildren ? ICONS.views : ICONS.part);
    row.appendChild(icon);

    // 勾选框
    const entry = { node, row, parent: parentEntry, children: [], solidIds: nodeMeshes, depth, box, checkImg: null };
    const check = document.createElement('img');
    check.className = 'tree-check';
    check.src = ICONS.checked;
    check.addEventListener('click', (ev) => {
      ev.stopPropagation();
      const target = allVisible(entry);
      setNodeVisible(entry, !target);
    });
    row.appendChild(check);
    entry.checkImg = check;

    // 名称（保持 STEP 原名，只有完全无名时才兜底）
    const label = document.createElement('span');
    label.className = 'tree-label';
    const displayName = (node.name && String(node.name).trim()) || (isRoot ? rootName : '未命名');
    label.textContent = displayName;
    label.title = displayName;
    row.appendChild(label);

    row.addEventListener('click', () => selectNode(entry));

    // 挂载
    if (parentEntry) {
      parentEntry.box.appendChild(row);
      parentEntry.box.appendChild(box);
      parentEntry.children.push(entry);
    } else {
      dom.tree.appendChild(row);
      dom.tree.appendChild(box);
    }
    S.treeNodes.push(entry);
    return entry;
  };

  const walk = (node, depth, parentEntry, isRoot) => {
    const entry = createRow(node, depth, parentEntry, isRoot);
    // 每个实体一行，名称用 STEP 原始名（无名则「实体-N」），保证可独立选中/上色/隐藏
    const nodeMeshes = node.meshes || node.solidIds || [];
    nodeMeshes.forEach((mi) => {
      const m = (S.result && S.result.meshes) ? S.result.meshes[mi] : null;
      const raw = m ? m.name : null;
      const name = (typeof raw === 'string' && raw.trim()) || `实体-${mi + 1}`;
      const pe = createRow({ name, meshes: [mi], children: [] }, depth + 1, entry, false);
      S.meshEntryMap[mi] = pe;
    });
    (node.children || []).forEach((c) => walk(c, depth + 1, entry, false));
    return entry;
  };

  S.rootNode = walk(treeRoot, 0, null, true);
  refreshTreeChecks();
}

function allVisible(entry) {
  const ids = [];
  collectSolidIds(entry, ids);
  if (ids.length === 0) return true;
  return ids.every((i) => meshVisibility(i));
}

function collectSolidIds(entry, out) {
  // 有子行时只收集子行（父行的 solidIds 是子行并集，避免重复计数）
  if (entry.children.length === 0) {
    entry.solidIds.forEach((i) => out.push(i));
  } else {
    entry.children.forEach((c) => collectSolidIds(c, out));
  }
}

function setNodeVisible(entry, visible) {
  const ids = [];
  collectSolidIds(entry, ids);
  ids.forEach((i) => { if (S.meshObjs[i]) S.meshObjs[i].visible = visible; });
  if (entry.children.length === 0 && ids.length > 0) {
    // 叶子勾选后联动父级（父级全勾/半勾在 refreshTreeChecks 中处理）
  }
  refreshTreeChecks();
}

function refreshTreeChecks() {
  const visit = (entry) => {
    entry.children.forEach(visit);
    const ids = [];
    collectSolidIds(entry, ids);
    if (ids.length === 0) { entry.checkImg.src = ICONS.checked; return; }
    const shown = ids.filter((i) => meshVisibility(i)).length;
    entry.checkImg.src = shown === 0 ? ICONS.check : (shown === ids.length ? ICONS.checked : ICONS.partial);
  };
  visit(S.rootNode);
}

function selectNode(entry) {
  if (S.selected === entry) { clearHighlight(); return; }
  clearHighlight();
  S.selected = entry;
  entry.row.classList.add('highlight');
  const ids = [];
  collectSolidIds(entry, ids);
  ids.forEach((i) => {
    const m = S.meshObjs[i];
    if (m) {
      m.material.emissive = new THREE.Color(0x2b9ae8);
      m.material.emissiveIntensity = 0.4; // 深色零件也能看清选中高亮
    }
  });
  setStatus(`已选中：${entry.node.name || '未命名'}（${ids.length} 个几何体）`);
}

function clearHighlight() {
  if (S.selected) {
    S.selected.row.classList.remove('highlight');
    const ids = [];
    collectSolidIds(S.selected, ids);
    ids.forEach((i) => {
      const m = S.meshObjs[i];
      if (m) { m.material.emissive = new THREE.Color(0x000000); }
    });
  }
  S.selected = null;
}

// 树搜索
dom.treeSearch.addEventListener('input', () => {
  const q = dom.treeSearch.value.trim().toLowerCase();
  let firstMatch = null;
  S.treeNodes.forEach((entry) => {
    const name = (entry.node.name || '').toLowerCase();
    const hit = q !== '' && name.includes(q);
    entry.row.classList.toggle('matched', hit);
    if (hit && !firstMatch) firstMatch = entry;
    if (q === '') entry.row.style.display = '';
    else entry.row.style.display = hit ? '' : 'none';
    if (hit) {
      // 展开命中节点的祖先，使其可见
      let p = entry.parent;
      while (p) {
        p.box.classList.remove('hidden');
        const ex = p.row.querySelector('.tree-expander img');
        if (ex) ex.src = ICONS.open;
        p = p.parent;
      }
    }
  });
  if (firstMatch) firstMatch.row.scrollIntoView({ block: 'center' });
});

/* ================= 工具切换 ================= */
const toolButtons = document.querySelectorAll('.tool-btn[data-tool]');
function setTool(tool) {
  // 再次点击「测量」= 关闭测量，恢复工具栏
  if (tool === 'measure' && S.tool === 'measure') tool = 'select';
  S.tool = tool;
  toolButtons.forEach((b) => b.classList.toggle('active', b.dataset.tool === tool));
  dragControls.enabled = (tool === 'drag' || tool === 'singalDrag');
  controls.enabled = !dragControls.enabled;
  renderer.domElement.style.cursor =
    tool === 'measure' ? 'crosshair' : (dragControls.enabled ? 'grab' : 'default');
  // 测量激活时隐藏底部工具栏、显示水平单行测量面板；退出时恢复
  $('toolbar').classList.toggle('hidden', tool === 'measure');
  $('measurePanel').classList.toggle('hidden', tool !== 'measure');
  // 其他面板显隐
  dom.explodePanel.classList.toggle('hidden', tool !== 'explode');
  dom.sectionPanel.classList.toggle('hidden', tool !== 'section');
  if (tool !== 'measure') resetLengthTemp();
  if (tool === 'measure') setMeasureModeStatus();
  else if (tool === 'drag') setStatus('拖动：按住零件移动，松开结束；点空白处回到选择模式');
  else if (tool === 'singalDrag') setStatus('单件拖动：按住单个零件移动；点空白处回到选择模式');
  else if (tool === 'explode') setStatus('爆炸：拖动滑块分解装配体');
  else if (tool === 'section') setStatus('剖切：选择方向并拖动滑块定位剖面');
  else setStatus('选择：点击零件高亮，点击空白取消 | 左键/中键旋转 · Shift+中键/右键平移 · 滚轮缩放');
  S.measurePts = [];
  S.thicknessFaces = [];
  if (S.thicknessMarkers) { disposeGroup(S.thicknessMarkers); S.thicknessMarkers = null; }
  if (tool !== 'measure') clearFaceHighlight();
}
toolButtons.forEach((b) => b.addEventListener('click', () => setTool(b.dataset.tool)));

function setMeasureModeStatus() {
  const hints = {
    coordinate: '坐标：点击模型上的点，显示 X/Y/Z',
    distance: '距离：依次点击两个点',
    length: '长度：连续取点，双击或按 Enter 结束',
    angle: '角度：依次点击 顶点→边1点→边2点',
    radius: '半径：点击圆柱/圆弧面',
    ellipse: '椭圆：点击椭圆面',
    linear: '线性测量：依次点击两个点',
    thickness: '壁厚：依次点击两个相对的面',
    area: '面积：点击要测量的面',
    volume: '体积重量：点击实体零件（密度按钢 7.85 g/cm³）',
    bbox: '包围盒：点击零件显示其长宽高',
  };
  setStatus(`测量[${measureModeLabel(S.measureMode)}]：${hints[S.measureMode] || ''}`);
}

// 构建测量子面板（新迪同款 13 个测量按钮）
function buildMeasurePanel() {
  const box = $('measureModes');
  box.innerHTML = '';
  MEASURE_MODES.forEach((m) => {
    const b = document.createElement('button');
    b.className = 'measure-btn';
    b.dataset.mode = m.key;
    b.innerHTML = `<img src="./icons/${m.icon}" alt="${m.label}"><span>${m.label}</span>`;
    b.title = m.label;
    b.addEventListener('click', () => {
      S.measureMode = m.key;
      S.measurePts = [];
      S.thicknessFaces = [];
      if (S.thicknessMarkers) { disposeGroup(S.thicknessMarkers); S.thicknessMarkers = null; }
      resetLengthTemp();
      box.querySelectorAll('.measure-btn').forEach((x) => x.classList.toggle('active', x === b));
      setMeasureModeStatus();
    });
    if (m.key === S.measureMode) b.classList.add('active');
    box.appendChild(b);
  });
}

/* ================= 爆炸 ================= */
function applyExplode() {
  const amount = S.modelRadius * 0.6 * (S.explodeAmount / 100);
  S.meshMeta.forEach((meta, i) => {
    const m = S.meshObjs[i];
    m.position.copy(meta.dragOffset).addScaledVector(meta.dir, amount);
  });
}
dom.explodeSlider.addEventListener('input', () => {
  S.explodeAmount = Number(dom.explodeSlider.value);
  dom.explodeVal.textContent = S.explodeAmount + '%';
  applyExplode();
});

/* ================= 剖切 ================= */
function setSectionOn(on) {
  S.sectionOn = on;
  if (on) updateSectionPlane(); // 关键：开启时按当前模型包围盒重算平面位置
  S.meshObjs.forEach((m) => {
    m.material.clippingPlanes = on ? [S.sectionPlane] : null;
  });
  S.edgeLines.forEach((l) => {
    l.material.clippingPlanes = on ? [S.sectionPlane] : null;
  });
  updateSectionVisual();
  const btn = $('btnSectionToggle');
  btn.classList.toggle('on', on);
  btn.textContent = on ? '开启' : '关闭';
}

function updateSectionPlane() {
  const axisMap = { x: new THREE.Vector3(1, 0, 0), y: new THREE.Vector3(0, 1, 0), z: new THREE.Vector3(0, 0, 1) };
  const n = axisMap[S.sectionAxis] || axisMap.z;
  if (S.sectionPlane.normal.equals(n) === false) {
    S.sectionPlane.normal.copy(n);
    S.sectionOffset = 0;
    dom.sectionSlider.value = 0;
    updateSectionVal();
  }
  S.sectionPlane.constant = n.dot(S.modelCenter) + S.sectionOffset;
  updateSectionVisual();
}

// 半透明剖切面指示器（帮助看清剖切位置）
function updateSectionVisual() {
  if (!S.sectionOn || S.meshObjs.length === 0) {
    if (S.sectionVisual) S.sectionVisual.visible = false;
    return;
  }
  if (!S.sectionVisual) {
    const g = new THREE.PlaneGeometry(2, 2);
    const m = new THREE.MeshBasicMaterial({
      color: 0x2b9ae8, transparent: true, opacity: 0.22,
      side: THREE.DoubleSide, depthTest: false,
    });
    S.sectionVisual = new THREE.Mesh(g, m);
    S.sectionVisual.renderOrder = 998;
    scene.add(S.sectionVisual);
  }
  const r = Math.max(S.modelRadius * 1.2, 1);
  S.sectionVisual.scale.set(r, r, 1);
  // 平面过点 modelCenter + normal * offset（plane.constant = n·center + offset）
  S.sectionVisual.position.copy(S.modelCenter).addScaledVector(S.sectionPlane.normal, S.sectionOffset);
  S.sectionVisual.quaternion.setFromUnitVectors(new THREE.Vector3(0, 0, 1), S.sectionPlane.normal);
  S.sectionVisual.visible = true;
}

function updateSectionVal() {
  dom.sectionVal.textContent = String(Math.round(S.sectionOffset));
}

document.querySelectorAll('.axis-btn').forEach((b) => b.addEventListener('click', () => {
  document.querySelectorAll('.axis-btn').forEach((x) => x.classList.toggle('active', x === b));
  S.sectionAxis = b.dataset.axis;
  updateSectionPlane();
}));

dom.sectionSlider.addEventListener('input', () => {
  S.sectionOffset = Number(dom.sectionSlider.value);
  updateSectionVal();
  updateSectionPlane();
});

$('btnSectionFlip').addEventListener('click', () => {
  S.sectionPlane.normal.multiplyScalar(-1);
});

$('btnSectionToggle').addEventListener('click', () => setSectionOn(!S.sectionOn));

/* ================= 测量（全套件） ================= */
const MEASURE_MODES = [
  { key: 'coordinate', label: '坐标', icon: 'view-tool-coordinate.png', clicks: 1 },
  { key: 'distance', label: '距离', icon: 'view-tool-distance.png', clicks: 2 },
  { key: 'length', label: '长度', icon: 'view-tool-length.png', clicks: -1 },
  { key: 'angle', label: '角度', icon: 'view-tool-angel.png', clicks: 3 },
  { key: 'radius', label: '半径', icon: 'view-tool-radius.png', clicks: 1 },
  { key: 'ellipse', label: '椭圆', icon: 'view-tool-ellipse.png', clicks: 1 },
  { key: 'linear', label: '线性测量', icon: 'view-tool-Lineargauge.png', clicks: 2 },
  { key: 'thickness', label: '壁厚', icon: 'view-tool-wallThickness.png', clicks: 2 },
  { key: 'area', label: '面积', icon: 'view-tool-area.png', clicks: 1 },
  { key: 'volume', label: '体积重量', icon: 'view-tool-volumeWeight.png', clicks: 1 },
  { key: 'bbox', label: '包围盒', icon: 'view-tool-boundBox.png', clicks: 1 },
];
S.measureMode = 'distance';
S.results = [];
S.lengthTemp = null;
let resultSeq = 0;

function measureModeLabel(key) {
  const m = MEASURE_MODES.find((x) => x.key === key);
  return m ? m.label : key;
}

// ---- 结果统计 ----
function addResult(type, text, group) {
  const rec = { id: ++resultSeq, type, text, group };
  S.results.push(rec);
  renderStats();
  setStatus(`${type}：${text}`);
  return rec;
}

function disposeGroup(group) {
  if (!group) return;
  S.modelGroup.remove(group);
  group.traverse((o) => {
    if (o.geometry) o.geometry.dispose();
    if (o.material) o.material.dispose();
  });
}

function removeResult(id) {
  const i = S.results.findIndex((r) => r.id === id);
  if (i < 0) return;
  disposeGroup(S.results[i].group);
  S.results.splice(i, 1);
  renderStats();
}

function renderStats() {
  const box = $('statsList');
  box.innerHTML = '';
  if (S.results.length === 0) {
    box.innerHTML = '<div class="stats-empty">暂无测量结果</div>';
    return;
  }
  S.results.forEach((r) => {
    const row = document.createElement('div');
    row.className = 'stats-row';
    const t = document.createElement('span');
    t.className = 'stats-type';
    t.textContent = r.type;
    const v = document.createElement('span');
    v.className = 'stats-value';
    v.textContent = r.text;
    const d = document.createElement('span');
    d.className = 'stats-del';
    d.textContent = '✕';
    d.title = '删除该结果';
    d.addEventListener('click', () => removeResult(r.id));
    row.append(t, v, d);
    box.appendChild(row);
  });
}

function clearMeasurements() {
  S.results.slice().forEach((r) => disposeGroup(r.group));
  S.results = [];
  S.measurePts = [];
  resetLengthTemp();
  if (S.partBboxHelper) { disposeGroup(S.partBboxHelper); S.partBboxHelper = null; }
  clearFaceHighlight();
  dom.canvasWrap.querySelectorAll('.measure-label').forEach((e) => e.remove());
  renderStats();
}

// ---- 基础绘制 ----
function makeMarker(p) {
  const r = Math.max(S.modelRadius * 0.004, 0.5);
  const g = new THREE.SphereGeometry(r, 12, 12);
  const m = new THREE.Mesh(g, new THREE.MeshBasicMaterial({ color: 0x2b9ae8, depthTest: false }));
  m.renderOrder = 999;
  m.position.copy(p);
  return m;
}

function makeLabel(text, pos) {
  const el = document.createElement('div');
  el.className = 'measure-label';
  el.textContent = text;
  const label = new CSS2DObject(el);
  label.position.copy(pos);
  return label;
}

function makeLine(pts, color = 0x2b9ae8) {
  const geo = new THREE.BufferGeometry().setFromPoints(pts);
  const line = new THREE.Line(geo, new THREE.LineBasicMaterial({ color, depthTest: false }));
  line.renderOrder = 999;
  return line;
}

// ---- 拾取 ----
function pickPoint(event) {
  const rect = renderer.domElement.getBoundingClientRect();
  const mouse = new THREE.Vector2(
    ((event.clientX - rect.left) / rect.width) * 2 - 1,
    -((event.clientY - rect.top) / rect.height) * 2 + 1
  );
  raycaster.setFromCamera(mouse, camera);
  const hits = raycaster.intersectObjects(S.meshObjs, false).filter((h) => h.object.visible);
  return hits.length ? hits[0] : null;
}

// 三角形索引 → brep 面（first/last 有序，二分查找）
function triToFace(meshIndex, triIndex) {
  const faces = S.meshFaces[meshIndex];
  if (!faces || !faces.length) return null;
  let lo = 0, hi = faces.length - 1;
  while (lo <= hi) {
    const mid = (lo + hi) >> 1;
    if (triIndex < faces[mid].first) hi = mid - 1;
    else if (triIndex > faces[mid].last) lo = mid + 1;
    else return { face: faces[mid], faceIndex: mid };
  }
  return null;
}

// 面分析：面积 / 质心 / 法向 / 去重顶点
function faceAnalysis(meshIndex, face) {
  const mesh = S.meshObjs[meshIndex];
  const geo = mesh.geometry;
  const pos = geo.attributes.position;
  const idx = geo.index;
  const area = new THREE.Vector3();   // 面积加权法向累计
  let totalArea = 0;
  const centroid = new THREE.Vector3();
  const verts = new Map();
  const va = new THREE.Vector3(), vb = new THREE.Vector3(), vc = new THREE.Vector3();
  const e1 = new THREE.Vector3(), e2 = new THREE.Vector3(), n = new THREE.Vector3();
  const end = Math.min(face.last, idx.count / 3 - 1);
  for (let t = face.first; t <= end; t++) {
    const a = idx.getX(t * 3), b = idx.getX(t * 3 + 1), c = idx.getX(t * 3 + 2);
    va.fromBufferAttribute(pos, a); vb.fromBufferAttribute(pos, b); vc.fromBufferAttribute(pos, c);
    e1.subVectors(vb, va); e2.subVectors(vc, va);
    n.crossVectors(e1, e2);
    const a2 = n.length() * 0.5; // 三角形面积
    if (a2 <= 0) continue;
    n.normalize();
    area.addScaledVector(n, a2);
    totalArea += a2;
    centroid.addScaledVector(va.add(vb).add(vc), a2 / 3);
    const key = (x) => Math.round(x * 100);
    for (const p of [va, vb, vc]) verts.set(`${key(p.x)},${key(p.y)},${key(p.z)}`, p.clone());
  }
  if (totalArea > 0) centroid.multiplyScalar(1 / totalArea);
  const normal = area.clone();
  const len = normal.length();
  const planar = len > 0 && (area.dot(area) / (totalArea * totalArea)) > 0.85;
  normal.normalize();
  return { area: totalArea, centroid, normal, verts: Array.from(verts.values()), planar };
}

// 投影到过质心、法向为 normal 的平面，返回 2D 点
function projectToPlane(points, normal, centroid) {
  const u = new THREE.Vector3();
  const ref = Math.abs(normal.z) < 0.9 ? new THREE.Vector3(0, 0, 1) : new THREE.Vector3(1, 0, 0);
  u.crossVectors(normal, ref).normalize();
  const v = new THREE.Vector3().crossVectors(normal, u);
  return {
    basis: [u, v],
    pts: points.map((p) => {
      const d = p.clone().sub(centroid);
      return [d.dot(u), d.dot(v)];
    }),
  };
}

// Kasa 圆拟合（最小二乘，代数法）
function circleFit2D(pts) {
  const n = pts.length;
  if (n < 5) return null;
  let sx = 0, sy = 0, sx2 = 0, sy2 = 0, sx3 = 0, sy3 = 0, sxy = 0, sx2y = 0, sxy2 = 0;
  for (const [x, y] of pts) {
    sx += x; sy += y;
    const x2 = x * x, y2 = y * y;
    sx2 += x2; sy2 += y2;
    sx3 += x2 * x; sy3 += y2 * y;
    sxy += x * y; sx2y += x2 * y; sxy2 += x * y2;
  }
  const A = n * sx2 - sx * sx;
  const B = n * sxy - sx * sy;
  const C = n * sy2 - sy * sy;
  const D = 0.5 * (n * sx2y - sx2 * sy + n * sx3 - sx2 * sx);
  const E = 0.5 * (n * sxy2 - sy2 * sx + n * sy3 - sy2 * sy);
  const det = A * C - B * B;
  if (Math.abs(det) < 1e-12) return null;
  const cx = (D * C - B * E) / det;
  const cy = (A * E - B * D) / det;
  let r = 0;
  for (const [x, y] of pts) r += Math.hypot(x - cx, y - cy);
  r /= n;
  let rms = 0;
  for (const [x, y] of pts) {
    const d = Math.hypot(x - cx, y - cy) - r;
    rms += d * d;
  }
  rms = Math.sqrt(rms / n);
  return { cx, cy, r, rms };
}

// 归一化圆锥曲线拟合 → 椭圆参数（2x2 特征值闭式解）
function ellipseFit2D(pts) {
  const n = pts.length;
  if (n < 6) return null;
  // 最小二乘解 [x², xy, y², x, y, 1]·p = 0，约束 A+C=1
  const M = Array.from({ length: 5 }, () => new Array(5).fill(0));
  const b = new Array(5).fill(0);
  for (const [x, y] of pts) {
    const x2 = x * x, y2 = y * y, xy = x * y;
    const row = [x2 - y2, xy, x, y, 1];
    for (let i = 0; i < 5; i++) {
      for (let j = 0; j < 5; j++) M[i][j] += row[i] * row[j];
      b[i] += row[i] * (-y2);
    }
  }
  const p = gaussSolve(M, b);
  if (!p) return null;
  // A+C=1 → A = (p0+1)/2, C = (1-p0)/2
  const A = (p[0] + 1) / 2, Bc = p[1], C = (1 - p[0]) / 2, D = p[2], E = p[3], F = p[4];
  const disc = Bc * Bc - 4 * A * C;
  if (disc >= -1e-9) return null; // 非椭圆
  // 2x2 特征值闭式
  const tr = A + C;
  const dq = Math.sqrt(Math.max(tr * tr - 4 * (A * C - Bc * Bc / 4), 0));
  const l1 = (tr + dq) / 2, l2 = (tr - dq) / 2;
  if (l1 <= 0 || l2 <= 0) return null;
  const theta = 0.5 * Math.atan2(Bc, A - C);
  const cx = (Bc * E / 2 - C * D) / (A * C - Bc * Bc / 4);
  const cy = (Bc * D / 2 - A * E) / (A * C - Bc * Bc / 4);
  // 中心化后常数项：F' = A·cx² + B·cx·cy + C·cy² + D·cx + E·cy + F
  const Fp = A * cx * cx + Bc * cx * cy + C * cy * cy + D * cx + E * cy + F;
  const den = -Fp;
  if (den <= 0) return null;
  const a = Math.sqrt(den / l1), bb = Math.sqrt(den / l2);
  // 拟合残差
  let rms = 0;
  for (const [x, y] of pts) {
    const v = A * x * x + Bc * x * y + C * y * y + D * x + E * y + F;
    rms += v * v;
  }
  rms = Math.sqrt(rms / n);
  return { cx, cy, a: Math.max(a, bb), b: Math.min(a, bb), theta, rms };
}

// ---- 3x3 对称矩阵特征分解（Jacobi 旋转，特征值升序） ----
function jacobi3(M) {
  const A = [
    [M[0], M[1], M[2]],
    [M[1], M[3], M[4]],
    [M[2], M[4], M[5]],
  ];
  const V = [[1, 0, 0], [0, 1, 0], [0, 0, 1]];
  for (let iter = 0; iter < 50; iter++) {
    let off = Math.abs(A[0][1]) + Math.abs(A[0][2]) + Math.abs(A[1][2]);
    if (off < 1e-12) break;
    let p = 0, q = 1, mx = Math.abs(A[0][1]);
    if (Math.abs(A[0][2]) > mx) { mx = Math.abs(A[0][2]); p = 0; q = 2; }
    if (Math.abs(A[1][2]) > mx) { p = 1; q = 2; }
    const apq = A[p][q], app = A[p][p], aqq = A[q][q];
    const theta = 0.5 * Math.atan2(2 * apq, app - aqq);
    const c = Math.cos(theta), s = Math.sin(theta);
    for (let k = 0; k < 3; k++) {
      const akp = A[k][p], akq = A[k][q];
      A[k][p] = c * akp - s * akq;
      A[k][q] = s * akp + c * akq;
    }
    for (let k = 0; k < 3; k++) {
      const apk = A[p][k], aqk = A[q][k];
      A[p][k] = c * apk - s * aqk;
      A[q][k] = s * apk + c * aqk;
    }
    for (let k = 0; k < 3; k++) {
      const vkp = V[k][p], vkq = V[k][q];
      V[k][p] = c * vkp - s * vkq;
      V[k][q] = s * vkp + c * vkq;
    }
  }
  const idx = [0, 1, 2].sort((a, b) => A[a][a] - A[b][b]);
  const vals = idx.map((i) => A[i][i]);
  const vecs = idx.map((i) => new THREE.Vector3(V[0][i], V[1][i], V[2][i]));
  return { vals, vecs };
}

// 面三角形法向 → 圆柱轴（Σnnᵀ 最小特征向量）
function axisFromTriangleNormals(meshIndex, face) {
  const geo = S.meshObjs[meshIndex].geometry;
  const pos = geo.attributes.position;
  const idx = geo.index;
  const M = [0, 0, 0, 0, 0, 0];
  const va = new THREE.Vector3(), vb = new THREE.Vector3(), vc = new THREE.Vector3();
  const e1 = new THREE.Vector3(), e2 = new THREE.Vector3(), n = new THREE.Vector3();
  const end = Math.min(face.last, idx.count / 3 - 1);
  for (let t = face.first; t <= end; t++) {
    va.fromBufferAttribute(pos, idx.getX(t * 3));
    vb.fromBufferAttribute(pos, idx.getX(t * 3 + 1));
    vc.fromBufferAttribute(pos, idx.getX(t * 3 + 2));
    e1.subVectors(vb, va); e2.subVectors(vc, va);
    n.crossVectors(e1, e2);
    const l = n.length();
    if (l < 1e-20) continue;
    n.multiplyScalar(1 / l);
    M[0] += n.x * n.x; M[1] += n.x * n.y; M[2] += n.x * n.z;
    M[3] += n.y * n.y; M[4] += n.y * n.z; M[5] += n.z * n.z;
  }
  const { vals, vecs } = jacobi3(M);
  // 最小特征值方向 = 轴线（全圆柱时该值接近 0）
  return { axis: vecs[0], minEigen: vals[0], secondEigen: vals[1] };
}

// 平面面片的边界环（用于识别孔等圆边）
function faceBoundaryLoops(meshIndex, face) {
  const geo = S.meshObjs[meshIndex].geometry;
  const pos = geo.attributes.position;
  const idx = geo.index;
  const end = Math.min(face.last, idx.count / 3 - 1);
  const edgeCount = new Map();
  for (let t = face.first; t <= end; t++) {
    const a = idx.getX(t * 3), b = idx.getX(t * 3 + 1), c = idx.getX(t * 3 + 2);
    for (const [u, v] of [[a, b], [b, c], [c, a]]) {
      const k = u < v ? u + ',' + v : v + ',' + u;
      edgeCount.set(k, (edgeCount.get(k) || 0) + 1);
    }
  }
  const boundary = new Map();
  for (const [k, cnt] of edgeCount) {
    if (cnt !== 1) continue;
    const [u, v] = k.split(',').map(Number);
    if (!boundary.has(u)) boundary.set(u, []);
    if (!boundary.has(v)) boundary.set(v, []);
    boundary.get(u).push(v);
    boundary.get(v).push(u);
  }
  const loops = [];
  const visited = new Set();
  for (const start of boundary.keys()) {
    if (visited.has(start)) continue;
    const loop = [start];
    visited.add(start);
    let cur = start, prev = -1;
    while (true) {
      const nexts = boundary.get(cur).filter((n) => n !== prev);
      if (!nexts.length) break;
      const nxt = nexts[0];
      if (nxt === start) { loops.push(loop); break; }
      if (visited.has(nxt)) break;
      visited.add(nxt);
      loop.push(nxt);
      prev = cur; cur = nxt;
    }
  }
  return loops.map((loop) => loop.map((vi) => new THREE.Vector3().fromBufferAttribute(pos, vi)));
}

// 面高亮覆盖层（拾取反馈，可叠加多个面）
function highlightFaceOverlay(meshIndex, face) {
  const geo = S.meshObjs[meshIndex].geometry;
  const g = new THREE.BufferGeometry();
  const posAttr = geo.attributes.position;
  const idxAttr = geo.index;
  const end = Math.min(face.last, idxAttr.count / 3 - 1);
  const verts = [];
  const inds = [];
  const vmap = new Map();
  for (let t = face.first; t <= end; t++) {
    for (const vi of [idxAttr.getX(t * 3), idxAttr.getX(t * 3 + 1), idxAttr.getX(t * 3 + 2)]) {
      if (!vmap.has(vi)) {
        vmap.set(vi, verts.length / 3);
        verts.push(posAttr.getX(vi), posAttr.getY(vi), posAttr.getZ(vi));
      }
      inds.push(vmap.get(vi));
    }
  }
  g.setAttribute('position', new THREE.BufferAttribute(new Float32Array(verts), 3));
  g.setIndex(new THREE.BufferAttribute(new Uint32Array(inds), 1));
  const mat = new THREE.MeshBasicMaterial({
    color: 0x2b9ae8, transparent: true, opacity: 0.35,
    side: THREE.DoubleSide, depthTest: false,
  });
  const overlay = new THREE.Mesh(g, mat);
  overlay.renderOrder = 997;
  overlay.userData.isFaceOverlay = true;
  S.faceHighlights.push(overlay);
  S.modelGroup.add(overlay);
}

function clearFaceHighlight() {
  S.faceHighlights.forEach((o) => {
    o.geometry.dispose();
    o.material.dispose();
    S.modelGroup.remove(o);
  });
  S.faceHighlights = [];
}

// 高斯消元（5x5 对称正定近似）
function gaussSolve(M, b) {
  const n = b.length;
  const A = M.map((row, i) => row.concat(b[i]));
  for (let c = 0; c < n; c++) {
    let piv = c;
    for (let r = c + 1; r < n; r++) if (Math.abs(A[r][c]) > Math.abs(A[piv][c])) piv = r;
    if (Math.abs(A[piv][c]) < 1e-12) return null;
    [A[c], A[piv]] = [A[piv], A[c]];
    for (let r = c + 1; r < n; r++) {
      const f = A[r][c] / A[c][c];
      for (let k = c; k <= n; k++) A[r][k] -= f * A[c][k];
    }
  }
  const x = new Array(n).fill(0);
  for (let r = n - 1; r >= 0; r--) {
    let s = A[r][n];
    for (let k = r + 1; k < n; k++) s -= A[r][k] * x[k];
    x[r] = s / A[r][r];
  }
  return x;
}

// 网格体积（散度定理，实体需封闭）
function meshVolume(meshObj) {
  const geo = meshObj.geometry;
  const pos = geo.attributes.position;
  const idx = geo.index;
  let v = 0, va = 0;
  const p0 = new THREE.Vector3(), p1 = new THREE.Vector3(), p2 = new THREE.Vector3();
  for (let t = 0; t < idx.count; t += 3) {
    p0.fromBufferAttribute(pos, idx.getX(t));
    p1.fromBufferAttribute(pos, idx.getX(t + 1));
    p2.fromBufferAttribute(pos, idx.getX(t + 2));
    const d = p0.dot(p1.clone().cross(p2));
    v += d / 6;
    va += Math.abs(d) / 6;
  }
  return { volume: v, closure: va > 0 ? Math.abs(v) / va : 0 };
}

// ---- 圆/椭圆叠加线 ----
function drawCircleOnPlane(center3, basis, r) {
  const pts = [];
  for (let k = 0; k <= 64; k++) {
    const t = (k / 64) * Math.PI * 2;
    pts.push(center3.clone().addScaledVector(basis[0], Math.cos(t) * r).addScaledVector(basis[1], Math.sin(t) * r));
  }
  return makeLine(pts, 0xe67e22);
}

function drawEllipseOnPlane(center3, basis, a, b, theta) {
  const pts = [];
  for (let k = 0; k <= 64; k++) {
    const t = (k / 64) * Math.PI * 2;
    const x = Math.cos(t) * a, y = Math.sin(t) * b;
    const rx = x * Math.cos(theta) - y * Math.sin(theta);
    const ry = x * Math.sin(theta) + y * Math.cos(theta);
    pts.push(center3.clone().addScaledVector(basis[0], rx).addScaledVector(basis[1], ry));
  }
  return makeLine(pts, 0xe67e22);
}

// ---- 长度模式临时线段 ----
function resetLengthTemp() {
  if (S.lengthTemp) disposeGroup(S.lengthTemp);
  S.lengthTemp = new THREE.Group();
  S.modelGroup.add(S.lengthTemp);
}

// ---- 各测量模式 ----
function handleMeasurePointer(event) {
  const mode = MEASURE_MODES.find((m) => m.key === S.measureMode);
  if (!mode) return;

  if (S.measureMode === 'length') {
    const hit = pickPoint(event);
    if (!hit) return;
    const p = hit.point.clone();
    S.measurePts.push(p);
    if (S.measurePts.length > 1) {
      const prev = S.measurePts[S.measurePts.length - 2];
      S.lengthTemp.add(makeLine([prev, p]));
      const total = pathLength(S.measurePts);
      setStatus(`长度：已取 ${S.measurePts.length} 点，累计 ${total.toFixed(2)} mm（双击或按 Enter 结束）`);
    } else {
      setStatus('长度：继续取点（双击或按 Enter 结束）');
    }
    return;
  }

  if (S.measureMode === 'radius' || S.measureMode === 'ellipse' || S.measureMode === 'area') {
    const hit = pickPoint(event);
    if (!hit) { setStatus('测量：请点击模型上的面'); return; }
    const meshIndex = hit.object.userData.index;
    const tf = hit.faceIndex != null ? triToFace(meshIndex, hit.faceIndex) : null;
    if (!tf) { setStatus('所选位置没有面数据'); return; }
    const fa = faceAnalysis(meshIndex, tf.face);
    const group = new THREE.Group();
    group.add(makeMarker(hit.point));
    clearFaceHighlight();
    highlightFaceOverlay(meshIndex, tf.face); // 拾取反馈：高亮所选面
    if (S.measureMode === 'area') {
      const label = makeLabel(`${fa.area.toFixed(2)} mm²`, fa.centroid);
      group.add(label);
      S.modelGroup.add(group);
      addResult('面积', `${fa.area.toFixed(2)} mm²`, group);
    } else if (S.measureMode === 'radius') {
      if (fa.planar) {
        // 平面：边界环检测 → 识别圆面与孔（最小内环即孔）
        const loops = faceBoundaryLoops(meshIndex, tf.face);
        const cands = [];
        for (const loop of loops) {
          if (loop.length < 8) continue;
          const lp = projectToPlane(loop, fa.normal, fa.centroid);
          const fit = circleFit2D(lp.pts);
          if (fit && fit.rms <= Math.max(fit.r * 0.03, S.modelRadius * 0.001)) {
            const c3 = fa.centroid.clone().addScaledVector(lp.basis[0], fit.cx).addScaledVector(lp.basis[1], fit.cy);
            cands.push({ fit, c3, basis: lp.basis });
          }
        }
        if (!cands.length) { disposeGroup(group); setStatus('该平面没有可识别的圆边/孔'); return; }
        cands.sort((a, b) => a.fit.r - b.fit.r);
        const isHole = cands.length > 1; // 有外环+内环 → 最小环是孔
        const c = cands[0];
        group.add(drawCircleOnPlane(c.c3, c.basis, c.fit.r));
        const labelText = isHole ? `孔 R ${c.fit.r.toFixed(2)} mm` : `R ${c.fit.r.toFixed(2)} mm`;
        group.add(makeLabel(labelText, c.c3));
        S.modelGroup.add(group);
        addResult('半径', labelText, group);
      } else {
        // 曲面（圆柱面/孔壁）：法向张量最小特征向量 = 轴线，垂直轴平面拟合圆
        const { axis } = axisFromTriangleNormals(meshIndex, tf.face);
        const proj = projectToPlane(fa.verts, axis, fa.centroid);
        const fit = circleFit2D(proj.pts);
        if (!fit || fit.rms > Math.max(fit.r * 0.03, S.modelRadius * 0.001)) {
          disposeGroup(group);
          setStatus('该面不是圆柱/圆弧面，无法测量半径');
          return;
        }
        const center3 = fa.centroid.clone().addScaledVector(proj.basis[0], fit.cx).addScaledVector(proj.basis[1], fit.cy);
        group.add(drawCircleOnPlane(center3, proj.basis, fit.r));
        group.add(makeLabel(`R ${fit.r.toFixed(2)} mm`, center3));
        S.modelGroup.add(group);
        addResult('半径', `R ${fit.r.toFixed(2)} mm`, group);
      }
    } else {
      // 椭圆：平面用边界环拟合，曲面用全部顶点拟合
      let proj, loopFit = null;
      if (fa.planar) {
        const loops = faceBoundaryLoops(meshIndex, tf.face);
        const loop = loops.sort((a, b) => b.length - a.length)[0];
        if (loop && loop.length >= 8) {
          proj = projectToPlane(loop, fa.normal, fa.centroid);
          loopFit = ellipseFit2D(proj.pts);
        }
      } else {
        proj = projectToPlane(fa.verts, fa.normal, fa.centroid);
        loopFit = ellipseFit2D(proj.pts);
      }
      const fit = loopFit;
      if (!fit || fit.rms > Math.max(fit.a * 0.03, S.modelRadius * 0.001)) {
        disposeGroup(group);
        setStatus('该面不是椭圆面，无法测量');
        return;
      }
      const center3 = fa.centroid.clone().addScaledVector(proj.basis[0], fit.cx).addScaledVector(proj.basis[1], fit.cy);
      group.add(drawEllipseOnPlane(center3, proj.basis, fit.a, fit.b, fit.theta));
      group.add(makeLabel(`椭圆 a ${fit.a.toFixed(2)} / b ${fit.b.toFixed(2)} mm`, center3));
      S.modelGroup.add(group);
      addResult('椭圆', `长轴 ${fit.a.toFixed(2)} / 短轴 ${fit.b.toFixed(2)} mm`, group);
    }
    return;
  }

  if (S.measureMode === 'volume') {
    const hit = pickPoint(event);
    if (!hit) { setStatus('体积重量：请点击实体零件'); return; }
    const obj = hit.object;
    const v = meshVolume(obj);
    const volAbs = Math.abs(v.volume) / 1000; // cm³
    const weight = volAbs * 7.85;             // 钢 7.85 g/cm³
    const group = new THREE.Group();
    group.add(makeMarker(hit.point));
    const closureNote = v.closure > 0.95 ? '' : '（面未封闭，结果仅供参考）';
    const label = makeLabel(`V ${volAbs.toFixed(2)} cm³ | 重 ${weight.toFixed(2)} g${closureNote}`, hit.point);
    group.add(label);
    S.modelGroup.add(group);
    addResult('体积重量', `V ${volAbs.toFixed(2)} cm³ | ${weight.toFixed(2)} g（钢 7.85）${closureNote}`, group);
    return;
  }

  if (S.measureMode === 'bbox') {
    const hit = pickPoint(event);
    if (!hit) { setStatus('包围盒：请点击零件'); return; }
    const obj = hit.object;
    const bb = new THREE.Box3().setFromObject(obj);
    const size = bb.getSize(new THREE.Vector3());
    const c = bb.getCenter(new THREE.Vector3());
    if (S.partBboxHelper) disposeGroup(S.partBboxHelper);
    S.partBboxHelper = new THREE.Group();
    const helper = new THREE.Box3Helper(bb, 0xe67e22);
    S.partBboxHelper.add(helper);
    S.partBboxHelper.add(makeLabel(`${size.x.toFixed(1)} × ${size.y.toFixed(1)} × ${size.z.toFixed(1)} mm`, c));
    S.modelGroup.add(S.partBboxHelper);
    addResult('包围盒', `${size.x.toFixed(1)} × ${size.y.toFixed(1)} × ${size.z.toFixed(1)} mm`, S.partBboxHelper);
    return;
  }

  // 壁厚：两次点击取两个面，计算面间距
  if (S.measureMode === 'thickness') {
    const hit = pickPoint(event);
    if (!hit) {
      S.thicknessFaces = [];
      if (S.thicknessMarkers) { disposeGroup(S.thicknessMarkers); S.thicknessMarkers = null; }
      clearFaceHighlight();
      setStatus('壁厚：点击空白处已取消');
      return;
    }
    const meshIndex = hit.object.userData.index;
    const tf = hit.faceIndex != null ? triToFace(meshIndex, hit.faceIndex) : null;
    if (!tf) { setStatus('所选位置没有面数据'); return; }
    const fa = faceAnalysis(meshIndex, tf.face);
    highlightFaceOverlay(meshIndex, tf.face); // 两个面都高亮
    S.thicknessFaces.push({ fa, point: hit.point.clone() });
    if (S.thicknessFaces.length === 1) {
      S.thicknessMarkers = new THREE.Group();
      S.thicknessMarkers.add(makeMarker(hit.point));
      S.modelGroup.add(S.thicknessMarkers);
      setStatus('壁厚：已取第一面（高亮），请点击对面的面');
      return;
    }
    const [f1, f2] = S.thicknessFaces;
    S.thicknessFaces = [];
    const group = S.thicknessMarkers || new THREE.Group();
    S.thicknessMarkers = null;
    const n1 = f1.fa.normal, n2 = f2.fa.normal;
    const c1 = f1.fa.centroid, c2 = f2.fa.centroid;
    let thickness;
    if (n1.dot(n2) < -0.7) {
      thickness = Math.abs(c2.clone().sub(c1).dot(n1)); // 平行对面：法向间距
    } else {
      thickness = c1.distanceTo(c2);                    // 非平行：质心距离
    }
    group.add(makeMarker(f2.point));
    group.add(makeLine([c1, c2]));
    group.add(makeLabel(`${thickness.toFixed(2)} mm`, c1.clone().add(c2).multiplyScalar(0.5)));
    addResult('壁厚', `${thickness.toFixed(2)} mm`, group);
    return;
  }

  if (S.measureMode === 'coordinate') {
    const hit = pickPoint(event);
    if (!hit) return;
    const p = hit.point;
    const group = new THREE.Group();
    group.add(makeMarker(p));
    const text = `X ${p.x.toFixed(2)}  Y ${p.y.toFixed(2)}  Z ${p.z.toFixed(2)}`;
    group.add(makeLabel(text, p));
    S.modelGroup.add(group);
    addResult('坐标', text, group);
    return;
  }

  // 需要多点的模式：distance / angle / linear
  const hit = pickPoint(event);
  if (!hit) {
    S.measurePts = [];
    setStatus(`${measureModeLabel(S.measureMode)}：点击空白处已取消，重新取点`);
    return;
  }
  S.measurePts.push(hit.point.clone());

  const need = mode.clicks;
  if (S.measurePts.length < need) {
    const hints = { distance: '已取第一点，请点击第二点', angle: `已取 ${S.measurePts.length}/3 点（顶点→边1→边2）`, linear: '已取第一点，请点击第二点' };
    setStatus(hints[S.measureMode] || `已取 ${S.measurePts.length}/${need} 点`);
    return;
  }

  const pts = S.measurePts;
  S.measurePts = [];
  const group = new THREE.Group();
  pts.forEach((p) => group.add(makeMarker(p)));

  if (S.measureMode === 'distance') {
    group.add(makeLine(pts));
    const d = pts[0].distanceTo(pts[1]);
    group.add(makeLabel(`${d.toFixed(2)} mm`, pts[0].clone().add(pts[1]).multiplyScalar(0.5)));
    S.modelGroup.add(group);
    addResult('距离', `${d.toFixed(2)} mm`, group);
  } else if (S.measureMode === 'angle') {
    const [A, B, C] = pts;
    group.add(makeLine([A, B]));
    group.add(makeLine([A, C]));
    const v1 = B.clone().sub(A).normalize();
    const v2 = C.clone().sub(A).normalize();
    const ang = (Math.acos(THREE.MathUtils.clamp(v1.dot(v2), -1, 1)) * 180) / Math.PI;
    group.add(makeLabel(`${ang.toFixed(2)}°`, A));
    S.modelGroup.add(group);
    addResult('角度', `${ang.toFixed(2)}°`, group);
  } else if (S.measureMode === 'linear') {
    // 工程图风格尺寸：主尺寸线 + 两端延伸线 + 箭头
    const [p1, p2] = pts;
    const mid = p1.clone().add(p2).multiplyScalar(0.5);
    const d = p1.distanceTo(p2);
    group.add(makeLine([p1, p2], 0x2b9ae8));
    const ext = Math.max(S.modelRadius * 0.03, 3);
    const dir = p2.clone().sub(p1);
    if (dir.lengthSq() < 1e-12) { dir.set(1, 0, 0); }
    const up = new THREE.Vector3();
    const ref = Math.abs(dir.normalize().z) < 0.9 ? new THREE.Vector3(0, 0, 1) : new THREE.Vector3(1, 0, 0);
    up.crossVectors(dir, ref).normalize();
    group.add(makeLine([p1.clone().addScaledVector(up, ext), p1.clone().addScaledVector(up, -ext)]));
    group.add(makeLine([p2.clone().addScaledVector(up, ext), p2.clone().addScaledVector(up, -ext)]));
    group.add(makeLabel(`${d.toFixed(2)} mm`, mid.clone().addScaledVector(up, ext * 1.6)));
    S.modelGroup.add(group);
    addResult('线性测量', `${d.toFixed(2)} mm`, group);
  }
}

function pathLength(pts) {
  let sum = 0;
  for (let i = 1; i < pts.length; i++) sum += pts[i].distanceTo(pts[i - 1]);
  return sum;
}

function finishLengthMode() {
  if (S.measurePts.length >= 2) {
    const total = pathLength(S.measurePts);
    const group = S.lengthTemp;
    group.add(makeLabel(`总长 ${total.toFixed(2)} mm`, S.measurePts[S.measurePts.length - 1]));
    S.lengthTemp = null;
    addResult('长度', `${total.toFixed(2)} mm`, group);
    setStatus(`长度：${total.toFixed(2)} mm`);
  } else {
    resetLengthTemp();
    setStatus('长度：已取消');
  }
  S.measurePts = [];
}

// 测量子面板与统计/清空按钮（放在 MEASURE_MODES 定义之后初始化）
buildMeasurePanel();

$('btnStats').addEventListener('click', () => {
  $('statsPanel').classList.toggle('hidden');
  renderStats();
});

$('btnMeasureClear').addEventListener('click', () => {
  clearMeasurements();
  setStatus('测量结果已清空');
});

$('btnMeasureClose').addEventListener('click', () => {
  setTool('select');
  setStatus('已关闭测量 | 左键/中键旋转 · Shift+中键/右键平移 · 滚轮缩放');
});

/* ================= 拖动 ================= */
function updateDragControlsObjects() {
  // DragControls 内部持有传入的数组引用，原地更新即可
  const objs = dragControls.getObjects();
  objs.length = 0;
  S.meshObjs.forEach((m) => objs.push(m));
}

dragControls.addEventListener('dragstart', (e) => {
  const grab = e.object;
  const idx = grab.userData.index;
  const entry = S.meshEntryMap[idx] || null; // 精确命中零件行
  const members = [];
  if (S.tool === 'singalDrag') {
    members.push(grab);
  } else {
    const ids = [];
    if (entry) collectSolidIds(entry, ids);
    if (ids.length === 0) ids.push(idx);
    ids.forEach((i) => S.meshObjs[i] && members.push(S.meshObjs[i]));
  }
  S.dragMembers = members;
  S.dragLastPos.copy(grab.position);
  controls.enabled = false;
  renderer.domElement.style.cursor = 'grabbing';
});

dragControls.addEventListener('drag', (e) => {
  if (!S.dragMembers) return;
  const delta = e.object.position.clone().sub(S.dragLastPos);
  S.dragMembers.forEach((m) => { if (m !== e.object) m.position.add(delta); });
  S.dragLastPos.copy(e.object.position);
});

dragControls.addEventListener('dragend', () => {
  if (S.dragMembers) {
    const amount = S.modelRadius * 0.6 * (S.explodeAmount / 100);
    S.dragMembers.forEach((m) => {
      const idx = m.userData.index;
      const meta = S.meshMeta[idx];
      // 拖动位移 = 当前位置 - 爆炸偏移
      meta.dragOffset.copy(m.position).addScaledVector(meta.dir, -amount);
    });
  }
  S.dragMembers = null;
  controls.enabled = !dragControls.enabled;
  renderer.domElement.style.cursor = 'grab';
});

/* ================= 点击选择 / 测量 ================= */
// 记录按下位置，区分「点击」与「旋转拖动」
let downPos = null;

renderer.domElement.addEventListener('pointerdown', (event) => {
  if (event.button !== 0) return;
  downPos = { x: event.clientX, y: event.clientY };
  if (S.tool === 'measure') { handleMeasurePointer(event); return; }
  // 拖动模式下点击空白处 → 自动回到选择模式（恢复旋转/点选）
  if (S.tool === 'drag' || S.tool === 'singalDrag') {
    const hit = pickPoint(event);
    if (!hit) {
      setTool('select');
      setStatus('已回到选择模式 | 左键/中键旋转 · Shift+中键/右键平移 · 滚轮缩放');
    }
  }
});

renderer.domElement.addEventListener('dblclick', (event) => {
  if (S.tool === 'measure' && S.measureMode === 'length') finishLengthMode();
});

renderer.domElement.addEventListener('pointerup', (event) => {
  if (event.button !== 0) return;
  const moved = downPos && Math.hypot(event.clientX - downPos.x, event.clientY - downPos.y);
  downPos = null;
  if (S.tool !== 'select') return;
  if (moved > 5) return; // 是旋转/平移操作，不触发选择
  const rect = renderer.domElement.getBoundingClientRect();
  const mouse = new THREE.Vector2(
    ((event.clientX - rect.left) / rect.width) * 2 - 1,
    -((event.clientY - rect.top) / rect.height) * 2 + 1
  );
  raycaster.setFromCamera(mouse, camera);
  const hits = raycaster.intersectObjects(S.meshObjs, false).filter((h) => h.object.visible);
  if (hits.length === 0) { clearHighlight(); return; }
  const idx = hits[0].object.userData.index;
  const entry = S.meshEntryMap[idx]; // 精确命中零件行，而非父级
  if (entry) selectNode(entry);
});

/* ================= 零件右键菜单 ================= */
let ctxTarget = null;

function showCtxMenu(x, y) {
  const menu = $('ctxMenu');
  $('ctxMenuTitle').textContent = ctxTarget.node.name || '零件';
  menu.classList.remove('hidden');
  // 防止溢出屏幕
  const r = menu.getBoundingClientRect();
  menu.style.left = Math.min(x, window.innerWidth - r.width - 8) + 'px';
  menu.style.top = Math.min(y, window.innerHeight - r.height - 8) + 'px';
}

function hideCtxMenu() {
  $('ctxMenu').classList.add('hidden');
  ctxTarget = null;
}

function toggleTransparentEntry(entry) {
  const ids = [];
  collectSolidIds(entry, ids);
  if (!ids.length) return;
  const first = S.meshObjs[ids[0]];
  if (!first) return;
  const toTransparent = !first.material.transparent;
  ids.forEach((i) => {
    const m = S.meshObjs[i];
    if (m) {
      m.material.transparent = toTransparent;
      m.material.opacity = toTransparent ? 0.25 : 1;
      m.material.depthWrite = !toTransparent;
    }
  });
  return toTransparent;
}

renderer.domElement.addEventListener('contextmenu', (e) => {
  e.preventDefault();
  const hit = pickPoint(e);
  if (!hit) { hideCtxMenu(); return; }
  const entry = S.meshEntryMap[hit.object.userData.index];
  if (!entry) { hideCtxMenu(); return; }
  ctxTarget = entry;
  selectNode(entry);
  showCtxMenu(e.clientX, e.clientY);
});

document.querySelectorAll('#ctxMenu .ctx-item').forEach((item) => {
  item.addEventListener('click', () => {
    const entry = ctxTarget;
    const act = item.dataset.act;
    hideCtxMenu();
    if (!entry) return;
    if (act === 'hide') {
      setNodeVisible(entry, false);
      setStatus(`已隐藏：${entry.node.name}`);
    } else if (act === 'isolate') {
      S.meshObjs.forEach((m) => { m.visible = false; });
      setNodeVisible(entry, true);
      refreshTreeChecks();
      setStatus(`仅显示：${entry.node.name}（点「显示」按钮恢复全部）`);
    } else if (act === 'showAll') {
      S.meshObjs.forEach((m) => { m.visible = true; });
      refreshTreeChecks();
      setStatus('已全部显示');
    } else if (act === 'color') {
      $('colorPanel').classList.remove('hidden');
      $('colorPicker').focus();
      setStatus(`已选中「${entry.node.name}」，选色后点「应用到选中」`);
    } else if (act === 'transparent') {
      const on = toggleTransparentEntry(entry);
      setStatus(on ? '已透明显示，再次操作恢复' : '已恢复不透明');
    } else if (act === 'drag') {
      setTool('drag');
    } else if (act === 'reset') {
      const ids = [];
      collectSolidIds(entry, ids);
      ids.forEach((i) => { if (S.meshMeta[i]) S.meshMeta[i].dragOffset.set(0, 0, 0); });
      applyExplode();
      setStatus(`「${entry.node.name}」已复位`);
    }
  });
});

// 点击空白处 / Esc 关闭菜单
document.addEventListener('click', (e) => {
  if (!$('ctxMenu').contains(e.target)) hideCtxMenu();
});
window.addEventListener('keydown', (e) => { if (e.key === 'Escape') hideCtxMenu(); });
window.addEventListener('keydown', (e) => {
  if (e.key === 'Escape') {
    S.measurePts = [];
    S.thicknessFaces = [];
    if (S.thicknessMarkers) { disposeGroup(S.thicknessMarkers); S.thicknessMarkers = null; }
    resetLengthTemp();
    if (S.tool !== 'select') setTool('select');
    else clearHighlight();
  }
  if (e.key === 'Enter' && S.tool === 'measure' && S.measureMode === 'length') finishLengthMode();
});

/* ================= 上色 ================= */
const PALETTE = [
  '#2b9ae8', '#e74c3c', '#2ecc71', '#f39c12', '#9b59b6',
  '#1abc9c', '#e67e22', '#3498db', '#e91e63', '#00bcd4',
  '#8bc34a', '#ff9800', '#607d8b', '#ffc107', '#795548',
];

function applyColorOverrides() {
  S.meshObjs.forEach((m) => {
    const i = m.userData.index;
    const ov = S.colorOverrides[i];
    if (ov) {
      m.material.vertexColors = false;
      m.material.color.set(ov);
    } else {
      m.material.vertexColors = !!m.geometry.getAttribute('color');
      m.material.color.copy(m.userData.baseColor);
    }
  });
}

function colorNode(entry, hex) {
  const ids = [];
  collectSolidIds(entry, ids);
  ids.forEach((i) => { S.colorOverrides[i] = hex; });
  applyColorOverrides();
}

function colorAllParts() {
  // 一键上色：按树零件行依次取调色板颜色
  const partRows = S.treeNodes.filter((t) => t.solidIds.length > 0 && t.node.meshes && t.depth > 0);
  partRows.forEach((entry, k) => {
    const ids = [];
    collectSolidIds(entry, ids);
    const c = PALETTE[k % PALETTE.length];
    ids.forEach((i) => { S.colorOverrides[i] = c; });
  });
  applyColorOverrides();
  setStatus('已一键上色（按零件分配颜色）');
}

$('btnColorApply').addEventListener('click', () => {
  if (!S.selected) { setStatus('请先在模型树中选择零件'); return; }
  colorNode(S.selected, $('colorPicker').value);
  setStatus(`已上色：${S.selected.node.name || S.selected.row.querySelector('.tree-label').textContent}`);
});

$('btnColorOneClick').addEventListener('click', () => {
  if (!S.meshObjs.length) { setStatus('请先加载模型'); return; }
  colorAllParts();
});

$('btnColorReset').addEventListener('click', () => {
  S.colorOverrides = {};
  applyColorOverrides();
  setStatus('已恢复原始颜色');
});

// 上色面板开关
$('btnColorPanel').addEventListener('click', () => {
  $('colorPanel').classList.toggle('hidden');
});

// 结构树显隐：桌面默认显示、点击隐藏/显示；手机端是抽屉（默认隐藏，点按钮弹出）
$('btnTree').addEventListener('click', (e) => {
  e.stopPropagation();   // 别让「点击外部收起抽屉」把这次打开又关掉
  const p = $('treePanel');
  if (window.innerWidth <= 768) {
    const open = document.body.classList.toggle('tree-open');
    $('btnTree').classList.toggle('active', !open);
    setStatus(open ? '结构树已显示' : '结构树已隐藏');
  } else {
    const hidden = p.classList.toggle('hidden');
    $('viewport').style.left = hidden ? '0px' : '280px';
    $('btnTree').classList.toggle('active', hidden);
    setStatus(hidden ? '结构树已隐藏' : '结构树已显示');
  }
  requestAnimationFrame(resize);
});

// 视口切回桌面宽度时清掉手机上可能残留的内联 left
window.addEventListener('resize', () => {
  if (window.innerWidth <= 768) $('viewport').style.left = '';
});

// 手机抽屉：✕ 按钮关闭 + 点击抽屉外部自动收起
if ($('btnTreeClose')) $('btnTreeClose').addEventListener('click', () => document.body.classList.remove('tree-open'));
document.addEventListener('click', (e) => {
  if (window.innerWidth > 768 || !document.body.classList.contains('tree-open')) return;
  if (!$('treePanel').contains(e.target)) document.body.classList.remove('tree-open');
});

/* ================= 其他工具按钮 ================= */
$('btnWireframe').classList.add('active'); // 线框默认开启

$('btnWireframe').addEventListener('click', () => {
  S.wireframeOn = !S.wireframeOn;
  S.edgeLines.forEach((l) => { l.visible = S.wireframeOn; });
  $('btnWireframe').classList.toggle('active', S.wireframeOn);
  setStatus(S.wireframeOn ? '已显示轮廓线框' : '已隐藏轮廓线框');
});

$('btnShowAll').addEventListener('click', () => {
  S.meshObjs.forEach((m) => { m.visible = true; });
  refreshTreeChecks();
  setStatus('已全部显示');
});

$('btnTransparent').addEventListener('click', () => {
  if (!S.selected) { setStatus('请先选择要变透明的零件'); return; }
  const ids = [];
  collectSolidIds(S.selected, ids);
  const first = S.meshObjs[ids[0]];
  if (!first) return;
  const toTransparent = !first.material.transparent;
  ids.forEach((i) => {
    const m = S.meshObjs[i];
    if (m) {
      m.material.transparent = toTransparent;
      m.material.opacity = toTransparent ? 0.25 : 1;
      m.material.depthWrite = !toTransparent;
    }
  });
  setStatus(toTransparent ? '已透明显示，再次点击恢复' : '已恢复不透明');
});

$('btnBbox').addEventListener('click', () => {
  if (S.bboxHelper) {
    scene.remove(S.bboxHelper);
    S.bboxHelper.geometry.dispose();
    S.bboxHelper = null;
    setStatus('已关闭包围盒');
    return;
  }
  S.bboxHelper = new THREE.Box3Helper(S.bbox, 0x2b9ae8);
  scene.add(S.bboxHelper);
  setStatus('已显示整体包围盒');
});

$('btnClear').addEventListener('click', () => {
  clearMeasurements();
  setStatus('测量已清除');
});

$('btnSingleReset').addEventListener('click', () => {
  const targets = S.selected ? (() => {
    const ids = []; collectSolidIds(S.selected, ids); return ids;
  })() : null;
  const ids = targets && targets.length ? targets : S.meshMeta.map((_, i) => i);
  ids.forEach((i) => { if (S.meshMeta[i]) S.meshMeta[i].dragOffset.set(0, 0, 0); });
  applyExplode();
  setStatus(S.selected ? `「${S.selected.node.name}」已复位` : '所有拖动已复位');
});

$('btnAllReset').addEventListener('click', () => {
  S.meshMeta.forEach((m) => m.dragOffset.set(0, 0, 0));
  S.explodeAmount = 0;
  dom.explodeSlider.value = 0;
  dom.explodeVal.textContent = '0%';
  applyExplode();
  setSectionOn(false);
  S.sectionOffset = 0;
  dom.sectionSlider.value = 0;
  updateSectionVal();
  updateSectionPlane();
  clearMeasurements();
  clearHighlight();
  S.meshObjs.forEach((m) => { m.visible = true; m.material.transparent = false; m.material.opacity = 1; m.material.depthWrite = true; });
  refreshTreeChecks();
  setTool('select');
  fitCamera();
  setStatus('已全部复位');
});

$('btnVport').addEventListener('click', () => {
  const views = [
    { name: '等轴测', dir: new THREE.Vector3(1, 0.6, 1) },
    { name: '前视', dir: new THREE.Vector3(0, 0, 1) },
    { name: '后视', dir: new THREE.Vector3(0, 0, -1) },
    { name: '左视', dir: new THREE.Vector3(-1, 0, 0) },
    { name: '右视', dir: new THREE.Vector3(1, 0, 0) },
    { name: '俯视', dir: new THREE.Vector3(0, 1, 0) },
    { name: '仰视', dir: new THREE.Vector3(0, -1, 0) },
  ];
  const v = views[S.viewIndex % views.length];
  S.viewIndex++;
  camera.position.copy(S.modelCenter).addScaledVector(v.dir.normalize(), S.modelRadius * 2.4);
  controls.target.copy(S.modelCenter);
  controls.update();
  setStatus('视角：' + v.name);
});

$('btnAxes').addEventListener('click', () => {
  if (S.axesHelper) {
    scene.remove(S.axesHelper);
    S.axesHelper.geometry.dispose();
    S.axesHelper = null;
    setStatus('已关闭坐标系');
    return;
  }
  S.axesHelper = new THREE.AxesHelper(S.modelRadius * 0.5);
  scene.add(S.axesHelper);
  setStatus('已显示坐标系');
});

function toggleFullscreen() {
  if (document.fullscreenElement) document.exitFullscreen();
  else document.documentElement.requestFullscreen();
}
$('btnFullscreen').addEventListener('click', toggleFullscreen);
// 底部工具栏也放一个：嵌入模式下顶栏是隐藏的，那里点不到
$('btnFull').addEventListener('click', toggleFullscreen);

/* ================= 面板关闭按钮 ================= */
document.querySelectorAll('.panel-close').forEach((b) => b.addEventListener('click', () => {
  const p = b.dataset.panel;
  if (p === 'stats') $('statsPanel').classList.add('hidden');
  else if (p === 'color') $('colorPanel').classList.add('hidden');
  else if (p === 'embed') $('embedPanel').classList.add('hidden');
  else setTool('select');
}));

/* ================= 文件打开 / 拖拽 ================= */
$('btnOpen').addEventListener('click', () => dom.fileInput.click());
dom.fileInput.addEventListener('change', () => {
  if (dom.fileInput.files.length) loadFile(dom.fileInput.files[0]);
  dom.fileInput.value = '';
});

// 统一入口（../index.html）在 iframe 里把用户选的 STEP 文件传进来
window.addEventListener('message', (e) => {
  const data = e.data;
  if (!data || data.type !== 'viewer-file' || data.kind !== 'step') return;
  try { if (e.origin !== location.origin) return; } catch { return; }
  const f = new File([data.buffer], data.name || 'model.step');
  loadFile(f);
});
try { window.parent.postMessage({ type: 'viewer-ready' }, '*'); } catch { /* 没嵌在入口里 */ }

let dragDepth = 0;
window.addEventListener('dragenter', (e) => {
  e.preventDefault();
  if (e.dataTransfer.types.includes('Files')) {
    dragDepth++;
    dom.dropOverlay.classList.remove('hidden');
  }
});
window.addEventListener('dragleave', (e) => {
  e.preventDefault();
  if (--dragDepth <= 0) { dragDepth = 0; dom.dropOverlay.classList.add('hidden'); }
});
window.addEventListener('dragover', (e) => e.preventDefault());
window.addEventListener('drop', (e) => {
  e.preventDefault();
  dragDepth = 0;
  dom.dropOverlay.classList.add('hidden');
  const f = e.dataTransfer.files && e.dataTransfer.files[0];
  if (f) loadFile(f);
});

setStatus('就绪：点击「打开模型」或把 .step/.stp 文件拖入页面 | 左键/中键旋转 · Shift+中键/右键平移 · 滚轮缩放');

/* ================= URL 参数自动加载（iframe 嵌入用） =================
 * 用法：index.html?file=samples/xxx.stp   （同源相对路径）
 *       index.html?url=https://.../x.stp  （完整地址，跨域需对方服务器允许 CORS）
 *       index.html?embed=1                （嵌入模式：隐藏顶栏、强制高精度）
 *       index.html?embed=1&menu=min       （嵌入+精简菜单：只留 剖切/线框/透明/视角/测量, 欧霓博目录专用）
 */
const EMBED_MODE = new URLSearchParams(location.search).get('embed') === '1';
if (EMBED_MODE) document.body.classList.add('embed');
const MIN_MENU = new URLSearchParams(location.search).get('menu') === 'min';
if (MIN_MENU) document.body.classList.add('menu-min');

// API 端点自动探测：Python 版（/name）或 PHP 版（./api/name.php），404 时尝试下一个。
// name 可带查询串（如 "token.php?f=xxx"）——.php 必须拼在 ? 之前，否则 URL 变成
// "api/token?f=xxx.php"，PHP 服务器上永远 404，令牌流程整体失效。
async function apiRequest(name, opts) {
  const qi = name.indexOf('?');
  const base = (qi < 0 ? name : name.slice(0, qi)).replace(/\.php$/i, '');
  const query = qi < 0 ? '' : name.slice(qi);
  // 候选顺序：先看相对本页的 api/ 目录（合并入口下也正确），
  // 再试站点根（旧部署习惯），最后是相对于当前路径的 api/
  const candidates = [`./api/${base}.php${query}`, `api/${base}.php${query}`, `/${name}`];
  let lastErr = null;
  for (const url of candidates) {
    try {
      const resp = await fetch(url, opts);
      if (resp.status === 404) { lastErr = new Error('HTTP 404'); continue; }
      return resp;
    } catch (e) { lastErr = e; }
  }
  throw lastErr || new Error('no api endpoint');
}

// 受保护模型（.stp/.step/.enc）走令牌通道；服务器禁止直接下载
async function fetchModelBlob(path) {
  if (/^https?:\/\//i.test(path)) {
    const resp = await fetch(path);
    if (!resp.ok) throw new Error('HTTP ' + resp.status);
    return resp.blob();
  }
  // 站内路径优先走令牌接口；接口没部署（本地 python 服务器、或纯静态托管）
  // 就退回直接 fetch。判断依据是响应内容而不是状态码：python 服务器会把
  // token.php 源码当静态文件 200 发回来，只看状态码会误判。
  let tok = null;
  try {
    const tokResp = await apiRequest('token.php?f=' + encodeURIComponent(path));
    if (tokResp.ok && /application\/json/i.test(tokResp.headers.get('content-type') || '')) {
      tok = await tokResp.json();
    }
  } catch (e) { /* 接口不存在 */ }
  if (tok && tok.token) {
    const resp = await apiRequest(`model.php?f=${encodeURIComponent(path)}&t=${tok.token}&e=${tok.expires}`);
    if (!resp.ok) throw new Error('模型获取失败 HTTP ' + resp.status);
    return resp.blob();
  }
  const resp = await fetch(path);
  if (!resp.ok) throw new Error('HTTP ' + resp.status);
  return resp.blob();
}

async function loadFromUrlParams() {
  const q = new URLSearchParams(location.search);
  const file = q.get('file');
  const url = q.get('url');
  if (!file && !url) return;
  try {
    showLoading('正在下载模型文件...');
    const blob = await fetchModelBlob(url || file);
    hideLoading();
    const name = decodeURIComponent((url || file).split('/').pop()).replace(/\.enc$/i, '') || 'model.step';
    const source = url ? { type: 'url', value: url } : { type: 'file', value: file };
    await loadFile(new File([blob], name), source);
  } catch (e) {
    hideLoading();
    setStatus('自动加载失败：' + e.message);
  }
}
loadFromUrlParams();

// 调试钩子（供自动化测试使用，不影响正常功能）
window.__S = S;

/* ================= 上传当前模型（嵌入面板按钮，与 2D 查看器同流程） ================= */
async function uploadCurrentModel() {
  const file = S.currentFile;
  if (!file) throw new Error('还没有打开本地模型');
  const headers = { 'X-Filename': encodeURIComponent(file.name), 'Content-Type': 'application/octet-stream' };
  let body = await file.arrayBuffer();
  // STEP 是纯文本，gzip 可压到约 1/6；上传前先压缩，节省 80%+ 带宽
  if (typeof CompressionStream !== 'undefined') {
    try {
      const stream = file.stream().pipeThrough(new CompressionStream('gzip'));
      body = await new Response(stream).arrayBuffer();
      headers['X-Encoding'] = 'gzip';
    } catch (e) { console.warn('gzip 压缩失败，改传原始文件:', e); }
  }
  const resp = await apiRequest('upload.php', { method: 'POST', headers, body });
  const j = await resp.json().catch(() => ({}));
  if (!resp.ok) throw new Error(j.error || ('HTTP ' + resp.status));
  return j;
}

$('btnUploadModel').addEventListener('click', async () => {
  const btn = $('btnUploadModel');
  btn.disabled = true;
  btn.textContent = '上传中...';
  try {
    const j = await uploadCurrentModel();
    if (!j || !j.path) throw new Error('上传接口返回异常（接口未部署）');
    S.modelSource = { type: 'file', value: j.path };
    $('embedPath').value = j.path;
    $('embedHint').textContent = '';
    refreshEmbed();
    setStatus(`已加密上传：${j.path}`);
    // 嵌在入口页里时通知它刷新首页数模库
    try { window.parent.postMessage({ type: 'library-updated' }, '*') } catch (e) { /* 独立打开 */ }
  } catch (e) {
    $('embedHint').textContent = `上传失败：${e.message}（本地开发服务器没有 PHP，需要在正式站点上试）`;
    setStatus('上传失败：' + e.message);
  } finally {
    btn.disabled = false;
    btn.textContent = '上传当前模型';
  }
});

/* ================= 嵌入链接生成 ================= */
function viewerBaseUrl() {
  return location.origin + location.pathname; // 当前 index.html 的完整地址
}

function refreshEmbed() {
  const v = $('embedPath').value.trim();
  const base = viewerBaseUrl();
  if (!v) {
    $('embedUrl').value = '';
    $('embedCode').value = '';
    return;
  }
  const link = (/^https?:\/\//i.test(v)
    ? `${base}?url=${encodeURIComponent(v)}`
    : `${base}?file=${encodeURIComponent(v)}`) + '&embed=1';
  $('embedUrl').value = link;
  $('embedCode').value = `<iframe src="${link}" width="100%" height="600" allowFullScreen></iframe>`;
}

$('btnEmbed').addEventListener('click', () => {
  const src = S.modelSource;
  if (src.type === 'file') {
    $('embedPath').value = src.value;
    $('embedHint').textContent = '';
  } else if (src.type === 'url') {
    $('embedPath').value = src.value;
    $('embedHint').textContent = '跨域加载需对方服务器允许 CORS，同源部署最稳定。';
  } else if (src.type === 'local') {
    $('embedPath').value = '';
    $('embedHint').textContent = '当前模型是本地文件：点「上传当前模型」加密上传到服务器后自动生成链接；也可以手动填服务器上的路径（如 samples/我的模型.stp）。';
  } else {
    $('embedPath').value = '';
    $('embedHint').textContent = '还未加载模型。也可以手动填入模型路径（相对路径或 http(s) 完整地址）。';
  }
  $('embedPanel').classList.toggle('hidden');
  refreshEmbed();
});

$('embedPath').addEventListener('input', refreshEmbed);

async function copyText(text, label) {
  try {
    await navigator.clipboard.writeText(text);
    setStatus(label + '已复制到剪贴板');
  } catch (e) {
    // 剪贴板不可用（非安全上下文）时选中文本供手动复制
    setStatus('复制失败，请手动选择文本框内容复制');
  }
}
$('btnCopyUrl').addEventListener('click', () => copyText($('embedUrl').value, '嵌入链接'));
$('btnCopyCode').addEventListener('click', () => copyText($('embedCode').value, 'iframe 代码'));
