/* 共享状态根:渲染器/场景/相机/灯光/栅格 + 关节链 + 网格构建 + 按需渲染标志 */ const TL = {}; for (const t of TRAVEL) TL[t.axis] = t; const V3 = THREE.Vector3; const VC = p => [p[0], p[2], -p[1]]; // CAD (x,y,z) -> viewer (x,z,-y); must precede use const cv = document.getElementById('cv'); const rend = new THREE.WebGLRenderer({canvas:cv, antialias:true, preserveDrawingBuffer:true}); rend.setPixelRatio(Math.min(window.devicePixelRatio || 1, 1.5)); const scene = new THREE.Scene(); scene.background = new THREE.Color(0x0d0f13); const cam = new THREE.PerspectiveCamera(38, 1, 1, 6000); scene.add(new THREE.HemisphereLight(0xcfe4ff, 0x22262e, 1.15)); const key = new THREE.DirectionalLight(0xffffff, 1.5); key.position.set(420, 620, 520); scene.add(key); const key2 = new THREE.DirectionalLight(0x9dc4ff, .7); key2.position.set(-520, 300, -420); scene.add(key2); const grid = new THREE.GridHelper(600, 30, 0x2b323e, 0x1b2028); grid.position.y = -0.5; scene.add(grid); let needRender = true; function requestRender() { needRender = true; } /* ---- kinematic groups: nested pivot matrices, serial chain ---- */ const ROOT = new THREE.Object3D(); scene.add(ROOT); const JG = {root: ROOT}; const JO = DATA.joints; const parentOf = {J1:'root', J2:'J1', J3:'J2', J4:'J3', J5:'J4', J6:'J5'}; const ang = {J1:0, J2:0, J3:0, J4:0, J5:0, J6:0}; for (const j of JO) { const g = new THREE.Object3D(); g.matrixAutoUpdate = false; JG[j.id] = g; JG[parentOf[j.id]].add(g); } /* 轴心 P / 轴向 D 只与 CAD 参数有关:预计算一次;矩阵合成改用 3 个共享实例。 旧版每次调用新建 2 个 Vector3 + 3 个 Matrix4(一次关节写入 = 6 次调用 = 30 个临时对象), 动画与实时追踪高频触发时纯属垃圾。数学与原式逐位相同:T(P)·R(D,θ)·T(−P)。 */ const JP = {}, JD = {}; for (const j of JO) { JP[j.id] = new V3(...VC(j.rear)); JD[j.id] = new V3(...VC(j.d)).normalize(); } const _jmT = new THREE.Matrix4(), _jmR = new THREE.Matrix4(), _jmTi = new THREE.Matrix4(); function applyJoint(id) { // geometry vertices were built in the viewer frame (x, z, -y), so the pivot point and the // axis must go through the same swap - using raw CAD coords rotates about the wrong line const P = JP[id], D = JD[id]; if (!P) return; const a = (ang[id] || 0) * Math.PI / 180; const m = _jmT.makeTranslation(P.x, P.y, P.z) .multiply(_jmR.makeRotationAxis(D, a)) .multiply(_jmTi.makeTranslation(-P.x, -P.y, -P.z)); JG[id].matrix.copy(m); JG[id].matrixWorldNeedsUpdate = true; if (window.updateHarnessBridges && window.isHarnessVisible && window.isHarnessVisible()) { updateHarnessBridges(); } requestRender(); } /* ---- meshes ---- */ const meshes = [], byName = {}; let expl = 0; for (const p of DATA.parts) { const geo = new THREE.BufferGeometry(); const pos = new Float32Array(p.v.length * 3); for (let i = 0; i < p.v.length; i++) { pos[i*3] = p.v[i][0]; pos[i*3+1] = p.v[i][2]; pos[i*3+2] = -p.v[i][1]; } p.triCount = p.t ? p.t.length : 0; p.vertCount = p.v.length; /* 外形三围:模型坐标系 X×Y×Z 包围盒(装配零位、打印朝向 Z=高),悬停面板显示用 */ { let x0=1e9,y0=1e9,z0=1e9,x1=-1e9,y1=-1e9,z1=-1e9; for (let i = 0; i < p.v.length; i++) { const a = p.v[i]; if (a[0]x1)x1=a[0]; if (a[1]y1)y1=a[1]; if (a[2]z1)z1=a[2]; } p.dims = [x1-x0, y1-y0, z1-z0]; } const isEnv = p.name.slice(0, 4) === 'ENV_'; // 可达包络:运动学边界,不随关节动 /* 索引按顶点数降位:单件 ≤65535 顶点时 Uint16 合法(92 件实测最大 47483), 索引内存与上传带宽减半;超大件保留 Uint32 兜底。 */ const IDX16 = p.v.length <= 65535; if (p.kind === 'line') { // 经纬线:LineSegments,无三角面 const sIdx = IDX16 ? new Uint16Array(p.s) : new Uint32Array(p.s); geo.setAttribute('position', new THREE.BufferAttribute(pos, 3)); geo.setIndex(new THREE.BufferAttribute(sIdx, 1)); geo.computeBoundingSphere(); geo.computeBoundingBox(); const lm = new THREE.LineBasicMaterial({color: p.color, transparent: true, opacity: 0.8}); const ls = new THREE.LineSegments(geo, lm); ls.userData = p; ls.position.set(p.ex[0], p.ex[2], -p.ex[1]); ls.base = ls.position.clone(); ls.visible = !isEnv; // 包络默认隐藏,走开关显示 JG[p.grp || 'root'].add(ls); meshes.push(ls); byName[p.name] = ls; delete p.v; delete p.s; continue; } const idx = IDX16 ? new Uint16Array(p.t.length * 3) : new Uint32Array(p.t.length * 3); // the vertex map (x,y,z)->(x,z,-y) has determinant +1, so triangle winding must be kept // as-is; swapping two indices here inverted every face and made the normals point inward for (let i = 0; i < p.t.length; i++) { idx[i*3] = p.t[i][0]; idx[i*3+1] = p.t[i][1]; idx[i*3+2] = p.t[i][2]; } geo.setAttribute('position', new THREE.BufferAttribute(pos, 3)); geo.setIndex(new THREE.BufferAttribute(idx, 1)); geo.computeVertexNormals(); geo.computeBoundingSphere(); geo.computeBoundingBox(); delete p.v; delete p.t; const isMo = !p.printed && p.name[0] === 'M'; const isScr = !!p.screw; // 紧固件(螺钉):金属亮色,可被 BOM 行高亮 const mat = new THREE.MeshStandardMaterial({ color: p.color, metalness: isScr ? .78 : (isMo ? .55 : .05), roughness: isScr ? .30 : (isMo ? .42 : .78), transparent: isEnv, opacity: isEnv ? .06 : 1, depthWrite: !isEnv, flatShading: false, side: isEnv ? THREE.DoubleSide : THREE.FrontSide}); const ms = new THREE.Mesh(geo, mat); ms.userData = p; ms.position.set(p.ex[0], p.ex[2], -p.ex[1]); ms.base = ms.position.clone(); ms.zero = [0,0,0]; if (isEnv) ms.visible = false; // 包络默认隐藏,走开关显示 JG[p.grp || 'root'].add(ms); meshes.push(ms); byName[p.name] = ms; }