3D 装配仿真平台
工业级 Web3D 装配仿真工程:Vue3+Babylon 实时装配、SimEngine 门面、同一份干涉算法前后端复用、性能治理与可观测。
3D 装配仿真平台
工业级 Web3D 装配仿真工程:浏览器实时装配 + 服务端离线预检,同一套算法驱动两端。覆盖 SimEngine 门面、干涉分析两阶段、模型轻量化、性能治理与工程化交付。
平台架构总览

接入层统一鉴权与限流路由 · 前端经 SimEngine 门面访问 Babylon(业务不直引引擎)· 后端无状态微服务多副本 · 数据层 MySQL 主从 + Redis + CDN · 安全/可观测/质量保障纵切。
技术选型:为什么 Babylon.js 而非 Three.js
- 渲染:Vue3 + Babylon.js(非 Three.js)
- 理由:① Vue3 + TS 强类型技术栈,Babylon 本身就是强类型 TS 引擎;② 装配仿真对碰撞检测和装配约束需求重,Babylon 内置碰撞系统、父级关系树、glTF 管线更全;③ 企业级复杂工程场景,官方 Inspector 场景调试器显著降排查成本
⚠️ 简历自洽口径:核心能力写「Vue3+Babylon.js 为主,熟悉 Three.js、原理通用」;别把两个引擎的 API 混着说(会暴露「只背了博客」)。
仓库分层:Monorepo 单向依赖
工程是 pnpm Monorepo,三种角色分目录,依赖方向强制单向:
flowchart TD
subgraph Apps["apps/ · 浏览器应用(Vue3 + TS)"]
SP["sim-platform<br/>仿真工作台<br/>经 SimEngine 门面访问 Babylon"]
SA["sim-admin<br/>管理后台(规划)"]
end
subgraph Services["services/ · 后端微服务<br/>无状态 · 端口隔离 · 可独立启动"]
GW["gateway :80<br/>子进程编排 + 静态托管 + 反代"]
AS["assembly-svc :7101<br/>产线 / BOM / 装配约束 / 工艺步骤"]
IS["interference-svc :7102<br/>服务端离线整线批量预检"]
MS["model-svc :7103<br/>glTF 元数据 / 资产版本 / CDN 签名"]
TS2["takt-svc :7104<br/>节拍计算 / 瓶颈识别"]
AU["auth-svc :7105<br/>OIDC / RBAC / 审计"]
end
subgraph Packages["packages/ · 唯一可被前后端同时 import 的层"]
DOM["<b>@assemble/domain</b><br/>领域类型契约(全工程单一事实源)"]
CC["<b>@assemble/clearance-core</b><br/>干涉分析核心算法(前后端复用)"]
SU["sim-utils / storage / http / observability"]
end
SP -->|HTTP| GW
SA -.->|HTTP| GW
GW --> AS & IS & MS & TS2 & AU
SP -->|"① 前端实时判定"| CC
IS -->|"② 后端离线复用<br/>同一份算法"| CC
CC --> SU
CC --> DOM
style CC fill:#ffe6cc,stroke:#d79b00
style DOM fill:#d5e8d4,stroke:#82b366
依赖方向强制规则(写代码必须遵守):
- 业务入口(apps/services)不得被其它入口依赖
- apps 之间、services 之间互不
import:服务间通过网关 HTTP 解耦,跨服务数据一律用@assemble/domain描述 - 共享算法/类型只放
packages/,保证浏览器与 Node 同源、判定一致
为什么用 Monorepo:算法与类型要在浏览器和 Node 之间共享,同一个仓库可以直接以源码引用(TS 路径映射),无需发包、无需版本对齐——这是「前后端复用同一份算法」这个需求的直接结果。
SimEngine 门面:前端核心设计决策
业务组件禁止直接 import '@babylonjs/core',一律经 SimEngine 门面:
Vue3 业务组件层(views/stores/components) ← 只依赖 SimEngine 公开接口
│
SimEngine Facade (apps/sim-platform/src/engine)
│ 抽象:SceneManager / AssetManager / InteractionManager
│ / AssemblyController / ClearanceController
▼
@babylonjs/core(可替换引擎层,业务不 touch)
架构红线:
- 业务组件禁止直接
import '@babylonjs/core';一律经SimEngine门面 - SimEngine 的接口稳定(对未来切 Three.js/升级引擎不敏感),实现细节隔离在门面内部
- 门面需可被 mock —— 单测装配控制器时注入假门面,不启动真实 WebGL
对外能力:
| 能力 | 职责 | 关联 domain |
|---|---|---|
| 实时装配 | 自动/手动/回放三模式,约束贴合用 slerp 平滑过渡 | assembly.ts |
| 实时干涉 | 拖拽装配时的实时碰撞检测,命中即高亮/拦截 | clearance-core |
| 产线运行态 | GLB 设备运动节点、物料沿 BOM 工位路径流转 | rhythm.ts + runtime |
干涉分析:同一套算法前后端复用
同一套 @assemble/clearance-core 被两端复用,保证判定一致——这是本工程最具含金量的设计。
@assemble/clearance-core
├─ bvh.ts # Broad Phase:BVH 空间索引(构建 + selfIntersect + queryAgainstSingle)
├─ obbSat.ts # Narrow Phase:OBB-SAT,15 分离轴短路判定
├─ detector.ts # ClearanceDetector 门面:交互式(增量)与离线(批量)两条路径
└─ aabb.ts / obb.ts # AABB 运算、AABB→OBB 派生
flowchart LR
subgraph Core["⭐ @assemble/clearance-core<br/>同一份 TS 算法"]
direction TB
BVH["bvh.ts · Broad Phase<br/>BVH 空间索引"]
SAT["obbSat.ts · Narrow Phase<br/>OBB-SAT 15 分离轴"]
BVH --> SAT
end
subgraph FE["🖥 前端 sim-platform"]
direction TB
A1["交互拖拽 / 单步装配"]
A2["queryInteractive<br/>BVH 树 vs 单件"]
A3["实时红高亮<br/>+ 阻断非法放置"]
A1 --> A2 --> A3
end
subgraph BE["🖧 服务端 interference-svc"]
direction TB
B1["整线全量预检<br/>/ 出干涉报告"]
B2["loadAll + runFull<br/>BVH 自碰撞 selfIntersect"]
B3["干涉清单 → 3D 定位<br/>→ 调整工位 → 复检归零"]
B1 --> B2 --> B3
end
A2 -.->|"复用同一算法"| Core
B2 -.->|"复用同一算法"| Core
两阶段判定(Broad + Narrow):
graph TD
A["200 零件两两组合<br/>19,900 对候选"] --> B["Broad Phase<br/>BVH 粗筛"]
B -->|包围盒相交| C["Narrow Phase<br/>OBB-SAT 精判"]
B -->|不相交| E["排除,跳过"]
C -->|"15 条分离轴短路"| D["判定干涉/无干涉"]
| 场景 | 执行端 | 路径 | 为什么 |
|---|---|---|---|
| 交互拖拽、单步装配(实时) | 前端 | queryInteractive(BVH 树 vs 单件) | 需 <200ms 满足拖拽手感,本地零往返 |
| 整线全量预检、出干涉报告 | 服务端 | loadAll + runFull(BVH 自碰撞) | 数千零件组合校验太重,放离线队列 |
OBB-SAT 15 条分离轴 = A 的 3 个面法线 + B 的 3 个面法线 + 9 条边对叉积(3+3+9=15)。
- SAT 短路特性:15 条轴只要找到一条投影不重叠就立即返回「不相交」,不用测完——这是它快的原因,面试一定要说「短路」这个词
- 为什么 OBB 不用 AABB:AABB 轴对齐,旋转件(机械臂/传送带)包不紧、误报多;OBB 跟着零件旋转仍贴合
为什么这个设计是加分项:前端要的是低延迟(拖拽手感,本地零往返),后端要的是吞吐与完整性(几千零件组合、可长时间跑)。把算法抽成
packages/clearance-core后,两端调用的是同一份代码——判定逻辑、边界处理、数值精度完全一致,从根本上消除了「前后端结论不一致」这类最难排查的 bug。这是「工程正确性优先于实现便利」的典型取舍。
性能门禁:200 零部件 runFull < 200ms(不可退让),已固化为 CI 性能门禁。
模型轻量化:两道压缩互补
| 压缩器 | 管什么 | 优点 | 代价 | 场景 |
|---|---|---|---|---|
| Draco | 压几何(顶点/索引/法线量化) | 体积降最多 | CPU 解压耗时 | 体积敏感的大静态模型 |
| Meshopt | 优化缓冲布局 + 顺带压缩 | 又小又快、GPU 友好 | 压缩率略低 | 动态/反复加载的模型 |
为什么两个一起用(-75% 的关键):互补——Draco 把体积砍到底,Meshopt 保证砍完体积后 GPU 还能读得快。
首屏秒开四招:按需/懒加载(只加载当前视角必需)、资源池化 + InstancedMesh 复用、LOD 渐进加载、Web Worker 解压(Draco 解码不阻主线程)。
// Babylon.js 正确写法(别写 THREE.GLTFLoader/DRACOLoader)
import { DracoCompression } from '@babylonjs/core/Meshes/Compression/dracoCompression'
import { MeshoptCompression } from '@babylonjs/core/Meshes/Compression/meshoptCompression'
const { meshes } = await SceneLoader.ImportMeshAsync('', '/assets/', 'line.glb', scene)
装配约束与三模式动画
零件装配本质是约束求解——把子件贴合到父件的指定约束位:
- 四种约束:对齐 Coincident、共面 Coplanar、同轴 Concentric、距离 Distance
- 实现:维护父子层级树 + 约束参数表,用矩阵变换(平移 + 四元数 slerp)贴合
- 三种模式:
- 自动装配:按 BOM/约束表自动贴合
- 手动装配:拖拽 + 磁吸吸附,命中即高亮拦截
- 步骤回放:关键帧倒放,时间轴可 scrub 拖动
- 关键帧动画:每个零件一条轨道编进 AnimationGroup 统一播放;位置/缩放用 lerp,旋转用 四元数 slerp(避免万向锁)
- 一键拆解(爆炸图):零件沿轴心分离、与装配动画互逆,共用同一套关键帧数据
性能治理体系
1. 性能预算分层
flowchart TD
SLI["用户可感知目标<br/>仿真页面 3s 内可交互 · 拖拽跟手不卡顿"] --> B1["网络与加载预算"]
SLI --> B2["渲染帧预算"]
SLI --> B3["交互响应预算"]
SLI --> B4["服务端接口预算"]
B1 --> B1a["首屏 JS + CSS gzip < 800KB"]
B1 --> B1b["模型资产首屏 < 3MB<br/>(12MB → 3MB,-75%)"]
B2 --> B2a["稳定 60fps,单帧预算 16.7ms"]
B2 --> B2b["掉帧帧占比 < 5%"]
B3 --> B3a["拖拽干涉判定本地零往返<br/>单次 < 16ms"]
B4 --> B4a["普通接口 P95 < 200ms"]
B4 --> B4b["整线离线预检 < 200ms / 200 件"]
预算的三条铁律:
- 预算是「总额」不是「单点」:模型资产省下来的额度不能被首屏 JS 吃掉——分层预算、分层守住
- 预算必须可自动判定:不能进 CI 的预算只是愿望。
runFull < 200ms之所以可信,是因为它在 merge 门禁里跑 - 预算超了要有取舍流程:不是「下次再优化」,而是当场决定——降 LOD?砍后处理?走分级渲染(Tier A/B/C)
2. 并发形态:三种混合负载分开打
| 负载类型 | 特征 | 主要瓶颈 | 应对 |
|---|---|---|---|
| 静态与模型资产 | 大文件、可缓存、读多写零 | 带宽 | CDN + 强缓存 + 版本指纹(绕开源站) |
| 交互式 API | 高频、轻量、低延迟 | 事件循环与连接数 | 无状态多实例 + 连接复用 |
| CPU 密集批量任务 | 整线预检/节拍仿真,秒级、吃满单核 | CPU(单进程单核) | 独立进程 + 队列 + 限并发,绝不同进程抢主线程 |
第一原则:
interference-svc的离线预检是 CPU 密集,Node 单进程单事件循环意味着一个预检任务就能拖慢同进程的所有请求。所以它必须被隔离——这是本项目并发设计的第一原则。
3. 六层保护机制
flowchart LR
U["浏览器<br/>SimEngine"] --> CDN["CDN<br/>静态资源 / GLB 资产"]
U --> GW["gateway :80<br/>子进程编排 + 反代"]
GW --> L1{"① 入口限流<br/>令牌桶·按 IP/租户"}
L1 -->|超限| R429["429 + Retry-After"]
L1 -->|通过| SVC["上游服务<br/>assembly/model/takt"]
SVC --> POOL["② 连接池<br/>MySQL / Redis"]
SVC --> Q["③ 任务队列<br/>整线预检 / 节拍仿真"]
Q --> W["独立 Worker 进程<br/>限并发 = CPU 核数"]
W --> BUD["④ 任务级预算<br/>超时 + 件数上限"]
SVC -.->|连续失败| CB["⑤ 熔断<br/>快速失败 · 半开探测"]
SVC -.->|DB 不可用| DG["⑥ 降级读<br/>内存/文件仓储"]
4. 瓶颈定位决策树
flowchart TD
P["现象:卡 / 慢"] --> Q1{"范围?"}
Q1 -->|"只有 3D 卡"| F1["前端渲染路径<br/>Performance / Spector.js / Stats.js"]
Q1 -->|"接口慢"| F2["服务端路径<br/>分段耗时 + /metrics"]
Q1 -->|"切换产线后越来越卡"| F3["内存泄漏路径<br/>堆快照对比"]
F1 --> D1{"帧时间尖刺在哪?"}
D1 -->|JS 执行长| D1a["长任务 → 拆帧/Worker"]
D1 -->|draw call 高| D1b["实例化 / 合批 / LOD"]
D1 -->|GPU 时间高| D1c["纹理 / 后处理 / 抗锯齿降级"]
F2 --> D2{"哪一段最慢?"}
D2 -->|"事件循环延迟高"| D2a["同步阻塞主线程<br/>CPU 密集混进交互进程"]
D2 -->|"连接池等待"| D2b["池太小 / 慢查询占连接"]
D2 -->|"下游服务慢"| D2c["看该服务 /metrics 与日志"]
D2 -->|"网关自身慢"| D2d["反代/序列化/日志开销"]
F3 --> D3{"堆快照增量对象"}
D3 --> D3a["Babylon 资源未 dispose<br/>场景/材质/纹理/回调"]
四类高频陷阱:
- 「服务端慢」其实是 CPU 密集任务混进了交互进程——本项目最可能的性能事故:预检跑在 gateway 子进程里,事件循环被 OBB-SAT 计算占满。修法是进程隔离,不是「优化算法」
- 「越来越卡」其实是资源没释放——切换产线只
dispose()了 Mesh,漏了材质/纹理/AnimationGroup/事件回调/ResizeObserver - 「接口慢」其实是日志与序列化开销——大对象
JSON.stringify、同步写日志、请求体全量打点 - 「缓存没生效」其实是 key 不稳定或缓存了不该缓存的——GLB 走强缓存必须带版本指纹,否则改了模型拿不到新版
工程治理覆盖
平台沉淀了完整的工程治理文档族:
| 文档 | 内容 |
|---|---|
| ARCHITECTURE | 分层架构、依赖方向红线、SimEngine 门面、干涉算法前后端复用、数据契约 |
| PERFORMANCE | 性能预算分层、三种并发形态、六层保护、瓶颈定位决策树、性能回归门禁 |
| SECURITY | HS256 JWT、AES-256-GCM、RBAC 矩阵、审计哈希链、传输/静态加密 |
| TESTING | Vitest 单测 + 服务层集成 + 200 件性能门禁 + 趋势报告 |
| OBSERVABILITY | Counter/Histogram 指标库 + Prometheus 文本 + 全链路 X-Request-Id |
| HA | 多副本 + 故障转移 + 健康检查 |
| DISASTER_RECOVERY | 备份策略与恢复 RTO/RPO |
| ALERTING | 滑动窗口告警 + 阈值可配 + webhook 推送 |
| VERSIONING | SemVer + 里程碑(M0~M5) + 版本路线 |
| DEPLOYMENT | CloudBase 单容器聚合镜像 + Git 自动构建发布 |
| FRESHCUT_ASSET_SPEC | 净菜设备 GLB 资产接入规范 |
| HARDCODE_AUDIT | 硬编码审计清单 |
运维与交付治理要点
平台把「高可用 / 可观测 / 告警 / 容灾」拆成代码层前置件 + 部署层 checklist 两层推进(本机无 docker,容器编排留部署层):
① 高可用(HA)—— 代码层前置件已全部落地
| 能力 | 实现 | 面试话术 |
|---|---|---|
| 优雅停机 | SIGTERM/SIGINT → drain 3s → 关连接 → 退出 | 滚动发布不切断在途请求 |
| 双探针分离 | /healthz liveness 恒 200,/readyz readiness 依赖不健康 503 | 编排器标准 liveness/readiness 双探针 |
| 上游健康池 | gateway 内 UpstreamHealthPool(多副本轮询 + 故障摘流 + 5s 周期恢复) | 单容器内即表达「多副本 + failover」 |
| 进程自愈 | 子进程异常退出指数退避重启(500ms→15s,上限 10 次),不再整体 process.exit(1) | 单机自愈,整体崩溃留给容器重启 |
| 副本配置化 | UPSTREAM_TARGETS env 逗号分隔 host:port | 不写死 127.0.0.1:7101–7105 |
② 可观测性(OBSERVABILITY)—— 轻量自研三件套
- 自研
/metrics(Prometheus 文本格式,不引 Prometheus/OTel)—— Counter/Histogram 纯 TS 指标库,curl直看,未来接 Prometheus 无缝迁移 X-Request-Id透传跨服务调用,日志串成一条链路- 前端 SimMonitor SDK:Web Vitals(LCP/INP/CLS)+ FPS + 内存采样,节流批量上报 gateway
/telemetry
③ 告警(ALERTING)—— 规则引擎 + 状态机 + webhook
flowchart LR
A["健康池快照<br/>telemetry 直方图<br/>上游 /metrics"] --> B["规则引擎<br/>静态阈值 / 持续增长 / 健康状态"]
B --> C["告警状态机<br/>P0/P1/P2 · 去重 · 冷却 · 恢复"]
C --> D["webhook 推送<br/>企微 / 钉钉群机器人"]
- 评估位置选 gateway 单点(单容器入口,天然聚合上游指标 + telemetry + 健康快照,零新增服务)
- 防告警疲劳:去重 + 冷却 + 恢复通知(最小集,不做完整告警平台)
- 未配置
ALERT_WEBHOOK_URL→ 只记结构化日志,静默降级不报错
④ 容灾(DR)—— 部署层 checklist
| 维度 | 口径 |
|---|---|
| 数据备份 | MySQL 主从 + 半同步,每日全量 + binlog 增量,PITR 时间点恢复 |
| RPO / RTO | RPO ≤ 15min、RTO ≤ 30min |
| 对象存储 | glTF 资产多区冗余 + CDN 多源容灾 |
| 恢复演练 | 每季度 Restore Drill(不演练 = 没备份) |
面试金句:HA 与容灾是两个互补方向——HA 管「单机自愈 + 摘流 failover」,容灾管「跨机/跨区数据不丢」。代码层前置件(优雅停机/双探针/健康池/退避重启)本机可闭环验证,容器编排(k8s/compose)属部署层交付物。
量化结果
- GLB 体积 12MB → 3MB(-75%)(Draco + Meshopt 双压缩)
- 200 零部件检测耗时 < 200ms(BVH 粗筛 + OBB-SAT 短路,已固化 CI 门禁)
- 稳定 60fps / 单帧 16.7ms(Tier A/B/C 分级渲染适配低端集显办公机)
- 新产线建模到上线 ↓50%,复用 3 类产线(净菜清洗 / 切配 / 果蔬)
- 拖拽装配本地零往返(同一份
clearance-core复用,避免前后端结论不一致)
一句话收尾:「这工程的核心不是算法本身,而是三件事——SimEngine 门面把引擎封装掉、同一份
clearance-core被前后端复用、性能靠『预算 + 度量 + 门禁』守出来。」面试时把每一件事的具体证据(架构图、门禁值、门禁 CI 位置)准备好,比背任何 API 都管用。