34 · 模型测量、最近点与装配吸附

返回系列目录

需求与最近点推导

装配工具需要将零件挂点吸附到一条导轨上,并使零件正面朝向装配面。必须分开处理挂点位置与零件朝向;若先移动再旋转,挂点可能再次离开目标位置。

线段端点为 A、B,待吸附点为 P。无限直线上最近点参数为:

t=(P−A)⋅(B−A)∣B−A∣2. t=\frac{(P-A)\cdot(B-A)}{|B-A|^2}.

将 t 限制到 [0,1],得到线段最近点 Q = A + t(B − A)。A 与 B 重合时分母为零,应返回 A。Three.js 的 Line3.closestPointToPoint(P, true, result) 已封装这项运算,不过业务层仍应明确退化语义。

完整核心代码

以下例子让零件局部 +Z 对齐装配面法线,再把偏离原点的挂点吸附到导轨。父节点只有旋转和平移,避免剪切影响世界四元数分解。

import { createDemo } from './runtime.js';

const { THREE, scene } = createDemo();
const parent = new THREE.Group();
parent.position.set(0.5, 0, 0);
parent.rotation.y = 0.3;
scene.add(parent);
const part = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshNormalMaterial());
part.position.set(1, 0, 1);
parent.add(part);
const anchorLocal = new THREE.Vector3(0.5, 0, 0);
const rail = new THREE.Line3(new THREE.Vector3(-3, 0, 0), new THREE.Vector3(3, 0, 0));
scene.add(new THREE.Line(
  new THREE.BufferGeometry().setFromPoints([rail.start, rail.end]),
  new THREE.LineBasicMaterial({ color: 0xff6644 })
));
const worldAnchor = new THREE.Vector3();
const nearest = new THREE.Vector3();
const worldOrigin = new THREE.Vector3();
const parentRotation = new THREE.Quaternion();
const desiredWorldRotation = new THREE.Quaternion();
const surfaceNormal = new THREE.Vector3(0, 1, 1).normalize();

function snapPart() {
  part.updateWorldMatrix(true, false);
  worldAnchor.copy(anchorLocal);
  part.localToWorld(worldAnchor);
  if (rail.start.distanceToSquared(rail.end) < 1e-12) nearest.copy(rail.start);
  else rail.closestPointToPoint(worldAnchor, true, nearest);
  const distance = worldAnchor.distanceTo(nearest);
  if (distance > 3) return false; // 业务吸附半径。

  desiredWorldRotation.setFromUnitVectors(new THREE.Vector3(0, 0, 1), surfaceNormal);
  parent.getWorldQuaternion(parentRotation);
  part.quaternion.copy(parentRotation.invert()).multiply(desiredWorldRotation);
  part.updateWorldMatrix(true, false);

  // 朝向改变后重新求挂点,平移整个零件使挂点落在 nearest。
  worldAnchor.copy(anchorLocal);
  part.localToWorld(worldAnchor);
  part.getWorldPosition(worldOrigin);
  worldOrigin.add(nearest).sub(worldAnchor);
  part.position.copy(parent.worldToLocal(worldOrigin));
  return true;
}

snapPart();

这里 nearest 根据旋转前挂点选择,之后保持这个目标不变。若产品要求“旋转后再寻找最近导轨点”,应调整步骤;两者是不同的交互规则。

验证与限制

执行后重新计算挂点世界坐标,应与 nearest 的距离小于浮点容差;验证端点吸附以及零长度导轨。加入父节点平移和旋转,结果不应依赖零件局部原点。

只对齐一根法线还剩绕法线的自由转角,不能唯一确定装配朝向。实际插槽通常需要第二个切向参考轴,叉积建立完整正交基底后再生成旋转矩阵和四元数。非均匀缩放、负缩放与剪切需要更明确的装配坐标框架。大量导轨应先做空间索引,不能每帧遍历所有边。测量值的单位来自项目约定,Three.js 不自动把 1 单位解释为米。

练习(暂不提供答案)

  1. 给导轨增加长度显示,并按毫米显示结果。
  2. 限制零件距导轨 0.2 单位以内才触发吸附。
  3. 加入第二参考方向,消除绕法线的旋转歧义。
  4. 用点到平面的投影实现面吸附,对比线段吸附。