起因
前段时间做项目需要一个「能在浏览器里跑的 3D 平面图编辑器」。调研了一圈, 发现市面上几乎没有现代化的开源方案。
倒是找到了一个叫 blueprint3d 的项目,功能设计得挺完整:2D 平面图编辑 + 3D 实时预览 + 家具拖拽摆放 + 墙面纹理。但打开一看——
- 构建工具:Grunt + Bower
- 语言:CoffeeScript
- DOM 操作:jQuery
- Three.js:使用的是早已废弃的
Geometry/Face3API - 最近一次有效维护:好几年前
直接用是不可能的,改也很难改。于是决定从头重写一遍,顺手开源出来。
重写版地址:github.com/charmlinn/b…
整体架构
项目分两层:
blueprint3d-modern/
├── src/ # 核心库:纯 TypeScript ES Module,不依赖任何框架
└── app/ # Demo 应用:Next.js 15,消费核心库
这种分层的好处是核心库可以独立使用,不绑定 React 或任何框架。
核心库重写(src/)
1. 废弃 API 迁移
原版大量使用了 Three.js 的旧 API,迁移的主要工作量在这里。
Geometry → BufferGeometry
原版:
const geometry = new THREE.Geometry()
geometry.vertices.push(new THREE.Vector3(...))
geometry.faces.push(new THREE.Face3(0, 1, 2))
新版:
const geometry = new THREE.BufferGeometry()
const vertices = new Float32Array([...])
geometry.setAttribute('position',
new THREE.BufferAttribute(vertices, 3))
BufferGeometry 直接操作类型化数组,内存布局更紧凑,GPU 上传效率更高。 对于户型图这种需要频繁更新墙体几何体的场景,性能差异是可感知的。
贴图 UV 映射
墙面和地板的纹理 UV 在迁移后需要重新计算,原来基于 Face3 的
faceVertexUvs 全部改成了 BufferGeometry 的 uv attribute:
geometry.setAttribute('uv',
new THREE.BufferAttribute(new Float32Array(uvs), 2))
2. 动画:tween.js → anime.js v4
原版用 tween.js 做相机切换动画。anime.js v4 的 API 更现代, 支持 Promise,方便配合 async/await 控制动画时序:
import { animate } from 'animejs'
await animate(camera.position, {
x: target.x,
y: target.y,
z: target.z,
duration: 600,
easing: 'easeInOutQuad'
})
3. Item 系统:Factory 模式加载 OBJ 模型
家具 Item 按放置位置分为几种子类型:
| 类型 | 说明 |
|---|---|
FloorItem | 落地家具:床、沙发、桌子 |
WallItem | 挂墙家具:挂画、电视 |
InWallItem | 嵌墙物件:门、窗 |
CeilingItem | 吸顶:灯具 |
加载流程:
// 每个家具有一个 metadata JSON
{
"itemName": "Single Bed",
"itemType": 1, // 1 = FloorItem
"modelUrl": "/models/bed.obj",
"materialUrl": "/models/bed.mtl"
}
// Factory 根据 itemType 实例化对应子类
class Factory {
static async loadItem(metadata: ItemMetadata, scene: THREE.Scene) {
const obj = await loadOBJMTL(metadata.modelUrl, metadata.materialUrl)
switch (metadata.itemType) {
case ItemType.FloorItem: return new FloorItem(obj, scene)
case ItemType.WallItem: return new WallItem(obj, scene)
// ...
}
}
}
Raycasting 选中逻辑也按子类型做了区分,WallItem 只响应打在墙面上的
射线,FloorItem 只响应打在地面上的,避免误选。
4. 2D 平面图编辑器
这部分是纯 Canvas 2D,不用 Three.js,独立于 3D 渲染器。
核心数据结构是 Corner-Wall 图:
class Floorplan {
corners: Corner[] // 墙角顶点
walls: Wall[] // 连接两个 Corner 的边
rooms: Room[] // 由 Wall 围合而成的面
}
Wall 从两个 Corner 派生出几何体,Room 用多边形填充算法从封闭的 Wall 环 中计算得出。每次 Corner 移动,相关的 Wall 和 Room 会自动重新计算。
吸附逻辑:
// 新 Corner 在一定距离内自动吸附到已有 Corner
const SNAP_DISTANCE = 20 // px
const nearest = findNearestCorner(mousePos, floorplan.corners)
if (distance(mousePos, nearest) < SNAP_DISTANCE) {
return nearest.position
}
5. IndexedDB 持久化
原版完全没有存档功能。新版用浏览器原生 IndexedDB 实现, 无需任何后端,无需账号:
// 保存户型
const plan = await blueprintStorage.create({
name: 'My Bedroom',
roomType: 'bedroom',
layoutData: blueprint.model.exportSerialized(), // JSON 序列化
thumbnailBase64: canvas.toDataURL() // canvas 截图作为缩略图
})
// 读取列表
const plans = await blueprintStorage.list()
// 加载
const plan = await blueprintStorage.get(id)
blueprint.model.loadSerialized(plan.layoutData)
Demo 应用(app/)
核心库搞定之后,Demo 应用主要是 UI 工程。
技术栈:
| 层 | 技术 |
|---|---|
| 框架 | Next.js 15 App Router |
| UI Runtime | React 19 |
| 样式 | Tailwind CSS v4 |
| 组件 | shadcn/ui + Radix UI |
| 状态 | Zustand |
| 动画 | Framer Motion |
| i18n | next-intl(en / zh / tw) |
踩坑记录
坑 1:Three.js 不能跑在服务端
App Router 下组件默认是 Server Component,Three.js 依赖 window /
document,直接 import 会在 SSR 阶段报错。
解法:顶层组件用 dynamic 包裹:
const Blueprint3DApp = dynamic(
() => import('./Blueprint3DAppBase'),
{ ssr: false }
)
坑 2:monorepo 下 Three.js ESM 路径解析错误
核心库在 src/,Demo 在 app/,两个目录各有自己的 node_modules。
three/examples/jsm 的 import 会解析到错误的版本,导致类型不匹配。
解法:在 next.config.js 里加 webpack alias 强制指向同一份:
config.resolve.alias['three/examples/jsm'] =
path.resolve(__dirname, '../node_modules/three/examples/jsm')
坑 3:Canvas ref 初始化时序
Three.js 需要拿到真实 DOM 的 canvas 元素才能初始化。
在 React 里必须在 useEffect 里做,不能在渲染阶段:
useEffect(() => {
if (!canvasRef.current) return
const blueprint = new Blueprint3d({
threeContainer: canvasRef.current,
floorplanContainer: floorplanRef.current,
textureDir: '/textures/'
})
blueprintRef.current = blueprint
return () => blueprint.destroy()
}, []) // 空依赖,只跑一次
还没做的
- GLB / GLTF 模型支持(目前只有 OBJ/MTL)
- 撤销 / 重做历史栈
- 核心库发布到 npm
- 墙体厚度独立配置
- 多房间 / 多楼层
欢迎 PR,也欢迎在评论区讨论 Three.js 相关的问题。