Files
cbb f713355351 refactor(viewer): app.js 按职责切成 8 个普通脚本(纯切片,零语义改动)
state / harness / camera / pick / ui / sheets / render / urlparams。
经典脚本的顶层 const/let 落在同一个全局词法作用域,所以切片本身不改变任何行为——
这一步先让"文件边界"与"模块语义"两件事分离:翻 ESM 时若出问题,责任清楚在
import/export,而不是切片切错了行。

切片踩到并修掉的:urlparams.js 被切掉了 IIFE 的收尾 })();(我按 len-2 收边界),
node --check 当场报 Unexpected end of input。

截图基线分级也在这一步收敛:工程图/装配向导视图带 feGaussianBlur 发光滤镜,
同一构建两轮就有 ±1 抖动(先前扫描那两轮碰巧逐字节相同,才漏进"稳定"清单,
于是随机假 FAIL)。把 8 张工程图/向导视图连同 3 张线束 x-ray、boss_p07、
p03_mount_loose 一起转像素容差(存参考像素,阈值 mean<=0.02、单像素<=32,
约实测 20x 余量),其余 92 张仍要求逐字节相同。

关卡:92 张逐字节一致 + 13 张在噪声阈内;数据哈希与 IK exact 段一致。
2026-10-15 04:45:56 +08:00

116 lines
5.9 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 共享状态根:渲染器/场景/相机/灯光/栅格 + 关节链 + 网格构建 + 按需渲染标志 */
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]<x0)x0=a[0]; if (a[0]>x1)x1=a[0];
if (a[1]<y0)y0=a[1]; if (a[1]>y1)y1=a[1];
if (a[2]<z0)z0=a[2]; 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;
}