Pascal Editor 深度实战:当 WebGPU 遇见 3D 建筑可视化——从浏览器零安装到生产级架构的完全指南(2026)
前言:浏览器里的工业级 3D 编辑器
想象一下:你打开 Chrome,在网页里像搭乐高一样画墙、铺地板、摆家具,实时渲染出完整的三维建筑模型——不用安装任何软件,不用配置任何显卡驱动,转动视角时连卡帧都没有。这听起来像是科幻小说的场景,但 Pascal Editor 已经把它变成了现实。
截至 2026 年 6 月,这个开源项目在 GitHub 上已经积累了 7.9k Star、1k Fork,最新版本 v0.3.1(2026年3月发布),MIT 协议,完全免费。它的技术栈堪称 2026 年前端工程化的标杆样本:React 19 + Next.js 16 做界面层,Three.js(WebGPU 渲染器)+ React Three Fiber 做 3D 渲染,Zustand + Zundo 做状态管理(带撤销/重做),Turborepo + Bun 做 Monorepo 工程化,Supabase 做本地/云端数据持久化,three-bvh-csg 做 CSG 布尔运算。
更重要的是,它的架构设计本身就是一个值得深度解剖的生产级范本——解耦渲染与编辑逻辑、Monorepo 多包协作、WebGPU 硬件加速、CSG 几何运算,每一项单独拎出来都是值得写一篇文章的技术深度点。本文将从架构设计、核心实现、性能优化三个维度,把 Pascal Editor 的里里外外全部拆干净。
一、背景:WebGPU 凭什么让浏览器能跑 3D 工业软件?
1.1 WebGL 的局限与 WebGPU 的崛起
要理解 Pascal Editor 为什么选择 WebGPU,先得理解它的前身 WebGL 存在哪些根本性缺陷。
WebGL 基于 OpenGL ES 规范设计,最初发布于 2011 年。十多年过去了,它在浏览器端确实做了很多了不起的事情——Google Maps 的 3D 地图、Three.js 的大量 Demo、Figma 的 3D 变换效果——但 WebGL 的架构瓶颈在复杂场景面前越来越明显:
Draw Call 问题:WebGL 每次绘制都需要 CPU 发起一次 Draw Call,场景中物体越多,CPU 和 GPU 之间的通信开销就越大。一个有 1000 个家具模型的室内场景,WebGL 可能在 Draw Call 上消耗的时间比实际渲染还多。
资源管理模式落后:WebGL 的 GPU 资源管理基于 OpenGL ES 的"命名对象"模型,没有现代 GPU API 的 explicit resource management(显式资源管理)能力。纹理、缓冲区、着色器程序的生命周期完全依赖 JavaScript 垃圾回收器,频繁的创建/销毁会导致显存碎片化。
缺乏计算着色器:WebGL 没有 Compute Shader,所有通用计算都得挤在顶点/片元着色器里。这意味着物理模拟、布料模拟、大规模粒子系统等计算密集型任务,在 WebGL 里要么跑不动,要么性能惨不忍睹。
WebGPU 则完全不同。它基于 Vulkan(Linux/Android)、Metal(macOS/iOS)、DirectX 12(Windows)三大现代 GPU API 的共同特性设计,提供了:
- Bind Group:现代资源绑定模型,比 WebGL 的 uniform 更灵活,支持多组资源同时绑定到着色器
- Command Encoder:批量提交渲染命令,减少 Draw Call 开销
- Compute Pass:原生计算着色器支持,通用计算直接在 GPU 上跑
- Explicit Resource Management:资源的创建、生命周期管理全部显式控制,告别隐式 GC
用一个不精确但直观的比喻:WebGL 像是手动挡的老爷车,换挡(Draw Call)全靠司机(CPU)操作;而 WebGPU 是一台自动挡+ECU 行车电脑的车,CPU 只管发指令,车子自己优化内部流程。
1.2 为什么现在的时间点是 WebGPU 爆发的前夜?
2026 年,WebGPU 的浏览器支持已经到达了一个关键临界点:
| 浏览器 | 最低版本要求 | WebGPU 支持状态 |
|---|---|---|
| Chrome | 113+ | ✅ 完整支持 |
| Edge | 113+ | ✅ 完整支持 |
| Firefox | Nightly + 开发者预览 | ✅ 实验性支持 |
| Safari | 17.4+ | ✅ 自 Safari 17.4 起正式支持 |
换句话说,全球主流浏览器在 2026 年已经全部支持 WebGPU(Firefox 还在 Nightly 阶段,但开发者预览版体验已经足够稳定)。Pascal Editor 明确要求 Chrome 113+ 或 Edge 113+,实际上覆盖了绝大多数用户群体。
更重要的是,硬件支持也已经不是问题。Pascal Editor 的技术文档明确列出:只要显卡支持 Vulkan(Linux)/ Metal(macOS)/ D3D12(Windows)三者之一,就能运行 WebGPU。而这三者几乎涵盖了 2018 年以后发布的所有现代独立显卡和集成显卡。
1.3 建筑可视化:WebGPU 的杀手级应用场景
建筑信息模型(BIM)和室内设计工具,传统上都是桌面软件的领地——SketchUp、Revit、AutoCAD、Blender 统治了几十年。为什么 WebGPU 在这个领域特别有潜力?
零安装、跨平台:用户打开链接就能用,不用下载 500MB 的安装包,不用配置显卡驱动。Pascal Editor 的在线版本直接在内嵌浏览器里打开就能用。
协作天然优势:浏览器天然支持多标签页、多窗口,配合 Supabase 的实时同步,多人在线协作室内设计变得异常简单。
与 Web 技术栈深度集成:设计师可以在同一个浏览器里一边查产品目录,一边搭建 3D 模型,一边和客户视频会议——传统桌面软件根本做不到这种体验。
迭代速度:前端工程化的迭代速度远超桌面软件。Pascal Editor 的 WebGPU 渲染器、React 界面、状态管理层都可以通过 CDN 无感更新,用户永远用的是最新版本。
二、架构解析:Monorepo 架构如何支撑渲染与编辑的彻底解耦
2.1 整体架构总览
Pascal Editor 采用了 Turborepo + Bun 的 Monorepo 架构,这是 2026 年前端工程化的主流范式之一。整个项目分为三层:
┌─────────────────────────────────────────────┐
│ apps/editor/ │
│ Next.js 应用:编辑器 UI、工具栏、API 路由 │
├─────────────────────────────────────────────┤
│ packages/core/ │
│ @pascal-app/core:节点定义、场景状态、 │
│ 几何系统、CSG 布尔运算核心逻辑 │
├─────────────────────────────────────────────┤
│ packages/viewer/ │
│ @pascal-app/viewer:3D 渲染组件、 │
│ WebGPU 渲染管线、材质系统 │
└─────────────────────────────────────────────┘
这种分层的核心理念是:编辑逻辑与渲染逻辑彻底解耦。core 包完全不依赖任何渲染框架,只关心"场景里有什么东西"(数据模型);viewer 包完全不关心"东西怎么被编辑",只关心"东西该怎么画出来"(渲染管线)。这带来的好处是:
- 可以单独替换渲染引擎(比如从 Three.js 换成 R3F 以外的引擎)
- 可以单独升级编辑器 UI,不影响 3D 渲染性能
- 可以在 Node.js 环境里直接运行
core做服务端验证(SSR/SSG 场景)
2.2 packages/core:场景状态的核心抽象
@pascal-app/core 是整个系统的数据中枢。它定义了 Pascal Editor 的场景模型:
// packages/core/src/types/scene.ts
export interface SceneNode {
id: string;
type: 'wall' | 'floor' | 'roof' | 'door' | 'window' | 'furniture';
transform: {
position: [number, number, number];
rotation: [number, number, number];
scale: [number, number, number];
};
geometry: GeometryParams;
material: MaterialParams;
children?: SceneNode[];
}
export interface GeometryParams {
type: 'box' | 'cylinder' | 'extrusion';
dimensions: {
width?: number;
height?: number;
depth?: number;
radius?: number;
segments?: number;
path?: Array<[number, number]>; // 用于 extrusion 的 2D 路径
};
csgOperations?: CsgOperation[]; // CSG 布尔运算定义
}
export interface CsgOperation {
operation: 'union' | 'subtract' | 'intersect';
targetId: string; // 参与运算的目标节点 ID
}
场景状态通过 Zustand 管理,这是目前 React 生态里最流行的轻量级状态管理库:
// packages/core/src/store/sceneStore.ts
import { create } from 'zustand';
import { subscribeWithSelector } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
interface SceneState {
nodes: Map<string, SceneNode>;
selectedIds: Set<string>;
history: HistoryState;
// 核心操作
addNode: (node: SceneNode) => void;
updateNode: (id: string, updates: Partial<SceneNode>) => void;
removeNode: (id: string) => void;
executeCsg: (operation: CsgOperation) => void;
// 选择管理
select: (id: string, additive?: boolean) => void;
deselect: (id: string) => void;
// 撤销/重做
undo: () => void;
redo: () => void;
}
export const useSceneStore = create<SceneState>()(
subscribeWithSelector(
immer((set, get) => ({
nodes: new Map(),
selectedIds: new Set(),
history: { past: [], future: [] },
addNode: (node) => set((state) => {
state.nodes.set(node.id, node);
}),
updateNode: (id, updates) => set((state) => {
const node = state.nodes.get(id);
if (node) {
Object.assign(node, updates);
}
}),
executeCsg: (operation) => set((state) => {
const { nodes } = state;
const sourceNode = nodes.get(operation.targetId);
if (!sourceNode) return;
// 遍历所有节点,找到需要与 sourceNode 做 CSG 的节点
nodes.forEach((node, nodeId) => {
if (nodeId === operation.targetId) return;
// CSG 运算:对节点进行布尔运算,更新几何参数
});
}),
undo: () => {
const { history } = get();
if (history.past.length === 0) return;
const previous = history.past[history.past.length - 1];
set((state) => {
state.history.future.unshift(cloneDeep(state.nodes));
state.nodes = previous;
state.history.past.pop();
});
},
redo: () => {
const { history } = get();
if (history.future.length === 0) return;
const next = history.future[0];
set((state) => {
state.history.past.push(cloneDeep(state.nodes));
state.nodes = next;
state.history.future.shift();
});
},
}))
)
);
Zustand 的 subscribeWithSelector 中间件和 immer 中间件的组合是 2026 年的最佳实践:前者允许精细化的订阅(只订阅特定字段变化),后者提供了不可变更新的语法糖(直接修改 state 而 zustand 自动做 immutable clone)。
特别值得注意的是 history 字段的实现——Pascal Editor 使用了 Zundo 库来实现撤销/重做。Zundo 是专门为 Zustand 设计的 undo/redo 中间件,它的核心思路是:
// 利用 zustand/middleware 的 temporal 特性
import { temporal } from 'zundo';
const useEditorStore = create(
temporal(
(set) => ({
nodes: new Map(),
// ... 其他状态
}),
{
// temporal 会自动追踪指定字段的变化历史
partialize: (state) => ({
nodes: state.nodes,
selectedIds: state.selectedIds,
}),
limit: 50, // 最多保留 50 步历史
}
)
);
// 使用时:
const { undo, redo } = useEditorStore.temporal.getters;
这种实现比 Redux 的 time-travel 机制轻量得多——不需要 Action/Reducer 模板,直接在 store 层面做快照。
2.3 packages/viewer:WebGPU 渲染管线深度解析
@pascal-app/viewer 是 Pascal Editor 的渲染引擎核心。它基于 Three.js 构建,但真正发挥威力的不是 Three.js 本身,而是 Three.js 的 WebGPU 渲染器(WebGPURenderer)。
Three.js 从 r152 版本开始提供 WebGPURenderer,虽然名字叫"Three.js 的 WebGPU 渲染器",但它并不是简单地把 WebGL 代码翻译成 WebGPU——而是从底层重新设计了一套渲染管线,利用 WebGPU 的现代 GPU API 特性:
// packages/viewer/src/renderer/WebGPURenderer.ts
import * as THREE from 'three';
import { WebGPURenderer } from 'three/examples/jsm/renderers/WebGPURenderer.js';
import { RoomEnvironment } from 'three/examples/jsm/environments/RoomEnvironment.js';
export class PascalRenderer {
private renderer: THREE.WebGPURenderer;
private scene: THREE.Scene;
private camera: THREE.PerspectiveCamera;
private controls: THREE.OrbitControls;
constructor(canvas: HTMLCanvasElement) {
// 创建 WebGPU 渲染器
this.renderer = new WebGPURenderer({
canvas,
antialias: true,
alpha: false,
powerPreference: 'high-performance',
// WebGPU 特有的属性
trackTimestamp: true, // 用于 GPU 性能分析
});
this.renderer.setPixelRatio(window.devicePixelRatio);
this.renderer.setSize(canvas.clientWidth, canvas.clientHeight);
this.renderer.toneMapping = THREE.ACESFilmicToneMapping;
this.renderer.toneMappingExposure = 1.0;
this.renderer.outputColorSpace = THREE.SRGBColorSpace;
// 设置环境光
const pmremGenerator = new THREE.PMREMGenerator(this.renderer);
const envTexture = pmremGenerator.fromScene(new RoomEnvironment()).texture;
this.scene.environment = envTexture;
// 初始化相机
this.camera = new THREE.PerspectiveCamera(
45,
canvas.clientWidth / canvas.clientHeight,
0.1,
1000
);
this.camera.position.set(5, 5, 10);
// 轨道控制器
this.controls = new OrbitControls(this.camera, canvas);
this.controls.enableDamping = true;
this.controls.dampingFactor = 0.05;
}
async initialize(): Promise<void> {
// WebGPU 需要在初始化时检查适配器可用性
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) {
throw new Error('WebGPU not supported on this device');
}
const device = await adapter.requestDevice();
// 通知 Three.js WebGPURenderer 使用该设备
this.renderer.setDevice(device);
}
render(): void {
// 在每次动画帧中调用
this.controls.update();
this.renderer.render(this.scene, this.camera);
}
}
2.4 CSG 布尔运算:门窗自动切割墙体的实现原理
Pascal Editor 最实用的功能之一是 CSG 布尔运算:当你往墙上放门窗时,系统会自动在墙体几何体上"挖洞",不需要手动绘制精确的形状。这背后的实现依赖于 three-bvh-csg 库。
CSG(Constructive Solid Geometry,构建实体几何)是一种通过基本体之间的并集(union)、差集(subtract)、交集(intersect)运算来构建复杂几何体的技术。在建筑场景中,最典型的应用就是"用门洞从墙体上切出一个开口":
// packages/core/src/geometry/csg.ts
import { CSG } from 'three-csg-ts';
import * as THREE from 'three';
export interface CsgCutResult {
resultGeometry: THREE.BufferGeometry;
// 额外元数据:用于后续编辑(如门洞尺寸、位置)
metadata: {
cutType: 'door' | 'window' | 'opening';
cutDimensions: { width: number; height: number; depth: number };
sourceId: string; // 被切割的墙体 ID
toolId: string; // 执行切割的门/窗 ID
};
}
/**
* 在墙体上执行布尔差集运算,创建门/窗开口
*
* 工作原理:
* 1. 将墙体几何体转换为 CSG 格式(多边形三角化)
* 2. 将门/窗几何体转换为 CSG 格式
* 3. 执行 subtract(差集):墙体 - 门/窗
* 4. 将结果转换回 Three.js 几何体
*/
export function performCsgCut(
wallMesh: THREE.Mesh,
openingMesh: THREE.Mesh,
metadata: CsgCutResult['metadata']
): CsgCutResult {
// Step 1: 将 Three.js 几何体转换为 CSG 多边形
const wallCSG = CSG.fromMesh(wallMesh);
const openingCSG = CSG.fromMesh(openingMesh);
// Step 2: 执行差集运算(墙体 - 门窗 = 带洞的墙体)
const resultCSG = wallCSG.subtract(openingCSG);
// Step 3: 将 CSG 结果转换回 Three.js 几何体
const resultMesh = CSG.toMesh(resultCSG, wallMesh.material);
resultMesh.geometry.computeVertexNormals(); // 重新计算法向量
return {
resultGeometry: resultMesh.geometry,
metadata,
};
}
/**
* 批量处理场景中所有需要 CSG 运算的节点
* 优化策略:按几何体类型分组,减少重复计算
*/
export function processSceneCsg(nodes: SceneNode[]): Map<string, CsgCutResult> {
const results = new Map<string, CsgCutResult>();
// 筛选出所有有 CSG 操作定义的节点
const csgNodes = nodes.filter(n => n.geometry.csgOperations?.length > 0);
for (const node of csgNodes) {
for (const operation of node.geometry.csgOperations!) {
const sourceNode = nodes.find(n => n.id === operation.targetId);
if (!sourceNode) continue;
const sourceMesh = buildThreeMesh(sourceNode);
const toolMesh = buildThreeMesh(node);
const result = performCsgCut(sourceMesh, toolMesh, {
cutType: node.type === 'door' ? 'door' : 'window',
cutDimensions: node.geometry.dimensions as any,
sourceId: sourceNode.id,
toolId: node.id,
});
results.set(sourceNode.id, result);
}
}
return results;
}
CSG 的计算开销随几何体复杂度指数级增长。为了保证实时交互体验,Pascal Editor 做了两件事:
- BVH 加速:
three-bvh-csg使用 BVH(Bounding Volume Hierarchy,包围盒层次结构)对多边形集合做空间索引,将 CSG 的计算复杂度从 O(n²) 降低到 O(n log n)。 - 增量更新:只有在门窗位置/尺寸变化时才重新计算 CSG,而不是每次渲染都重新运算。
三、核心实现:从画墙到实时 3D 渲染的完整链路
3.1 编辑器的数据流架构
Pascal Editor 的数据流设计非常清晰,整个链路是单向数据流(Unidirectional Data Flow):
用户操作(鼠标/键盘)
↓
编辑 UI 层(React 组件)
↓
Zustand Store(场景状态)
↓
@pascal-app/core(场景数据模型 + CSG 运算)
↓
@pascal-app/viewer(Three.js WebGPU 渲染管线)
↓
Canvas(WebGPU 输出)
这种单向数据流的好处是可预测性强。当状态变化时,数据的流向是唯一确定的,不会出现 React 里常见的"状态不知道从哪里变了"的问题。
3.2 React Three Fiber 的响应式绑定
Pascal Editor 在 React 层使用 React Three Fiber(R3F),它是 React 的 Three.js 绑定层,允许用 React 组件的方式声明 3D 场景:
// packages/viewer/src/components/Scene.tsx
import { Canvas } from '@react-three/fiber';
import { OrbitControls, Grid, Environment } from '@react-three/drei';
import { Suspense } from 'react';
import { PascalRenderer } from '../renderer/WebGPURenderer';
import { useSceneStore } from '@pascal-app/core';
function SceneContent() {
const nodes = useSceneStore((state) => state.nodes);
const selectedIds = useSceneStore((state) => state.selectedIds);
return (
<>
{/* 地板网格 */}
<Grid
args={[50, 50]}
cellSize={1}
cellThickness={0.5}
cellColor="#404040"
sectionSize={5}
sectionThickness={1}
sectionColor="#606060"
fadeDistance={50}
fadeStrength={1}
followCamera={false}
infiniteGrid
/>
{/* 环境光 */}
<Environment preset="apartment" />
{/* 场景节点 */}
<Suspense fallback={null}>
{Array.from(nodes.values()).map((node) => (
<SceneNodeMesh
key={node.id}
node={node}
isSelected={selectedIds.has(node.id)}
/>
))}
</Suspense>
{/* 相机控制器 */}
<OrbitControls
enableDamping
dampingFactor={0.05}
minDistance={1}
maxDistance={200}
maxPolarAngle={Math.PI / 2}
/>
</>
);
}
export function SceneViewer() {
return (
<Canvas
gl={(canvas) => new PascalRenderer(canvas)}
camera={{ position: [5, 5, 10], fov: 45 }}
dpr={[1, 2]} // 设备像素比,支持 Retina
shadows
>
<SceneContent />
</Canvas>
);
}
R3F 的核心价值在于它将 Three.js 的场景图(Scene Graph)映射成了 React 的虚拟 DOM 模型——Canvas 组件创建一个 WebGL/WebGPU 上下文,SceneContent 里的 JSX 直接对应 Three.js 的 Object3D 树。这种映射带来的最大好处是 React 的 diffing 算法可以直接作用于 3D 场景,状态变化时只更新需要重新渲染的 3D 节点,而不是重绘整个场景。
3.3 编辑工具栏:建筑编辑的核心交互
Pascal Editor 的编辑工具栏提供了五类核心操作,对应五个编辑模式:
// apps/editor/src/components/Toolbar.tsx
type ToolMode = 'select' | 'wall' | 'floor' | 'place' | 'measure';
interface ToolbarProps {
activeTool: ToolMode;
onToolChange: (tool: ToolMode) => void;
}
function Toolbar({ activeTool, onToolChange }: ToolbarProps) {
const tools: Array<{ id: ToolMode; icon: string; label: string }> = [
{ id: 'select', icon: '🖱', label: '选择' },
{ id: 'wall', icon: '🧱', label: '画墙' },
{ id: 'floor', icon: '🏠', label: '铺地板' },
{ id: 'place', icon: '🪑', label: '摆家具' },
{ id: 'measure', icon: '📏', label: '测量' },
];
return (
<div className="toolbar">
{tools.map((tool) => (
<button
key={tool.id}
className={`tool-button ${activeTool === tool.id ? 'active' : ''}`}
onClick={() => onToolChange(tool.id)}
title={tool.label}
>
<span>{tool.icon}</span>
<span>{tool.label}</span>
</button>
))}
</div>
);
}
// 画墙模式的核心逻辑:鼠标点按 → 记录起点 → 拖动 → 记录终点 → 生成墙体节点
function useWallTool() {
const addNode = useSceneStore((state) => state.addNode);
const [wallPoints, setWallPoints] = useState<Array<[number, number]>>([]);
const handleMouseDown = useCallback((event: ThreeEvent<PointerEvent>) => {
const point = [event.point.x, event.point.z] as [number, number];
setWallPoints([point]);
}, []);
const handleMouseMove = useCallback((event: ThreeEvent<PointerEvent>) => {
if (wallPoints.length === 0) return;
const currentPoint = [event.point.x, event.point.z] as [number, number];
// 实时预览墙体
}, [wallPoints]);
const handleMouseUp = useCallback((event: ThreeEvent<PointerEvent>) => {
if (wallPoints.length === 0) return;
const endPoint = [event.point.x, event.point.z] as [number, number];
// 从两点计算墙体几何参数
const dx = endPoint[0] - wallPoints[0][0];
const dz = endPoint[1] - wallPoints[0][1];
const length = Math.sqrt(dx * dx + dz * dz);
const angle = Math.atan2(dz, dx);
const wallNode: SceneNode = {
id: crypto.randomUUID(),
type: 'wall',
transform: {
position: [
(wallPoints[0][0] + endPoint[0]) / 2,
1.5, // 默认层高 3m,墙体中心在 1.5m 处
(wallPoints[0][1] + endPoint[1]) / 2,
],
rotation: [0, -angle, 0],
scale: [1, 1, length],
},
geometry: {
type: 'box',
dimensions: {
width: 0.2, // 墙体厚度 20cm
height: 3, // 墙体高度 3m
depth: 0.2,
},
},
material: { type: 'standard', color: '#ffffff' },
};
addNode(wallNode);
setWallPoints([]);
}, [wallPoints, addNode]);
return { handleMouseDown, handleMouseMove, handleMouseUp };
}
四、性能优化:让浏览器跑出专业软件的帧率
4.1 渲染性能优化
Pascal Editor 在浏览器里能跑出流畅的 3D 帧率,背后有多个性能优化策略:
1. 实例化渲染(Instanced Rendering)
对于重复出现的几何体(如墙砖、地板砖),Pascal Editor 使用 Three.js 的 InstancedMesh 将多个相同几何体合并为一个 Draw Call:
// packages/viewer/src/optimizations/InstancedRenderer.ts
/**
* 实例化渲染:将 N 个相同几何体合并为 1 个 Draw Call
* 适用场景:地板砖、墙砖重复图案、批量家具
*/
export function createInstancedWalls(
scene: THREE.Scene,
wallNodes: SceneNode[],
material: THREE.Material
): THREE.InstancedMesh {
const wallGeometry = new THREE.BoxGeometry(0.2, 3, 1); // 标准墙单元
// 实例化网格:一次性渲染所有相同几何体
const instancedMesh = new THREE.InstancedMesh(
wallGeometry,
material,
wallNodes.length // 实例数量
);
// 为每个实例设置变换矩阵
const matrix = new THREE.Matrix4();
wallNodes.forEach((node, index) => {
matrix.compose(
new THREE.Vector3(...node.transform.position),
new THREE.Quaternion().setFromEuler(
new THREE.Euler(...node.transform.rotation)
),
new THREE.Vector3(...node.transform.scale)
);
instancedMesh.setMatrixAt(index, matrix);
});
instancedMesh.instanceMatrix.needsUpdate = true;
scene.add(instancedMesh);
return instancedMesh;
}
2. 视锥体剔除(Frustum Culling)
Pascal Editor 在大型场景中自动启用 Three.js 的视锥体剔除——只有相机视野内的物体才会发送给 GPU 渲染:
// 启用自动视锥体剔除
wallMesh.frustumCulled = true; // Three.js 默认开启
// 对于超大几何体可以手动设置包围盒
instancedMesh.boundingBox = new THREE.Box3().setFromCenterAndSize(
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(100, 10, 100)
);
3. 延迟渲染材质更新
CSG 运算的结果会导致几何体重建,如果每次都立即触发 React 重新渲染,整个编辑器会卡顿。Pascal Editor 使用了 React 的 startTransition 来标记非紧急更新:
import { startTransition } from 'react';
function onGeometryUpdate(newGeometry: THREE.BufferGeometry) {
// 将几何体更新标记为非紧急更新
// React 会异步处理,不会阻塞主线程
startTransition(() => {
setCurrentGeometry(newGeometry);
});
}
4.2 状态管理性能优化
选择状态的精细化订阅
Zustand 的 selector 机制允许组件只订阅它真正需要的状态字段,避免不必要的重渲染:
// ❌ 不推荐:订阅整个 store,任何字段变化都触发重渲染
const { nodes, selectedIds } = useSceneStore();
// ✅ 推荐:精确订阅,只有 nodes 变化才重渲染
const nodes = useSceneStore((state) => state.nodes);
// ✅ 推荐:使用 shallow 比较器
import { shallow } from 'zustand/shallow';
const { nodes, selectedIds } = useSceneStore(
(state) => ({ nodes: state.nodes, selectedIds: state.selectedIds }),
shallow // 只在引用变化时触发重渲染
);
五、工程化:Turborepo + Bun 的 Monorepo 最佳实践
5.1 为什么选择 Turborepo?
Pascal Editor 的 packages/ 目录下有三个相互独立但共享依赖的包。如果用传统的 npm workspaces,三个包在构建时无法感知彼此的依赖关系——修改 core 包后,viewer 包不会自动重新构建。
Turborepo 通过 任务管道(Task Pipeline) 解决了这个问题:
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"], // ^ 表示依赖前置包的 build
"outputs": ["dist/**", ".next/**"]
},
"dev": {
"cache": false,
"persistent": true // 常驻进程,不缓存
},
"lint": {
"dependsOn": ["^build"]
},
"test": {
"dependsOn": ["^build"]
}
}
}
"dependsOn": ["^build"] 这行配置的意思是:当执行 turbo run build 时,先确保所有前置依赖包(^ 表示"依赖")构建完成,再构建当前包。也就是说,viewer 包的构建会自动等待 core 包先构建好。
5.2 Bun 的速度优势
Pascal Editor 推荐使用 Bun 作为运行时。Bun 的安装速度比 npm 快 20 倍,依赖安装速度比 pnpm 快 5 倍。在 Pascal Editor 的开发流程中,使用 Bun 带来的体验提升是:
# 使用 Bun 启动整个 Monorepo(包含 core + viewer + editor 三个包)
bun install # 约 1-2 秒(npm 需要 30-60 秒)
bun run dev # 启动热重载开发服务器
Bun 的 bun.lockb 锁文件格式也比 npm/pnpm 的 lock 文件更紧凑,Git 仓库体积更小。
5.3 数据持久化:Supabase 的本地/云端双轨
Pascal Editor 的数据存储策略非常实用:本地优先,云端同步。
// packages/core/src/storage/StorageAdapter.ts
import { createClient } from '@supabase/supabase-js';
import { IndexedDB } from 'idb-keyval';
interface StorageAdapter {
saveScene(sceneId: string, data: SceneData): Promise<void>;
loadScene(sceneId: string): Promise<SceneData | null>;
listScenes(): Promise<SceneMeta[]>;
syncToCloud(sceneId: string): Promise<void>;
}
/**
* 本地存储适配器:IndexedDB
* 优先存储到本地,断网也能用
*/
const localStorage = {
async saveScene(sceneId: string, data: SceneData): Promise<void> {
await set(sceneId, data); // idb-keyval 的 set API
},
async loadScene(sceneId: string): Promise<SceneData | null> {
return await get(sceneId);
},
async listScenes(): Promise<SceneMeta[]> {
const keys = await keys();
return keys.map((key) => ({
id: key,
lastModified: Date.now(),
}));
},
};
/**
* 云端存储适配器:Supabase
* 登录后自动同步,支持多人协作
*/
const cloudStorage = {
supabase: createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
),
async saveScene(sceneId: string, data: SceneData): Promise<void> {
const { error } = await this.supabase
.from('scenes')
.upsert({ id: sceneId, data, updated_at: new Date().toISOString() });
if (error) throw new Error(`Cloud save failed: ${error.message}`);
},
async syncToCloud(sceneId: string): Promise<void> {
const localData = await localStorage.loadScene(sceneId);
if (localData) {
await cloudStorage.saveScene(sceneId, localData);
}
},
};
这种"本地优先"的设计哲学(Offline-First)非常值得借鉴:用户的作品立即保存到 IndexedDB(延迟几乎为零),在后台静默同步到 Supabase(网络条件允许时)。即使完全离线,用户也能继续工作,下次联网时自动同步。
六、深度思考:Pascal Editor 的架构设计给我们的启示
6.1 渲染与数据分离的价值
Pascal Editor 最值得学习的架构决策,是把 core(数据模型)和 viewer(渲染引擎)拆成了两个独立的 npm 包。在很多团队的项目里,这两个职责往往混在一起——React 组件里直接操作 Three.js 对象,数据层和渲染层互相耦合。
这种耦合在小型项目里可以接受,但一旦项目规模增长,就会面临几个典型问题:
- 想换 3D 渲染引擎?几乎不可能,所有组件都直接引用 Three.js
- 想加服务端渲染(SSR)?Three.js 的 WebGL 依赖无法在 Node.js 里运行
- 想写单元测试?必须启动整个渲染上下文,测试运行极慢
Pascal Editor 的架构从根本上避免了这些问题。core 包是纯 TypeScript,不依赖任何浏览器 API,可以在 Node.js、浏览器、Web Worker 甚至 React Native 环境里运行。
6.2 WebGPU 给前端工程化带来的新机遇
Pascal Editor 的 7.9k Star 和 v0.3.1 版本告诉我们一件事:WebGPU 的浏览器生态已经足够成熟,可以支撑起真正的生产级应用了。这意味着什么?
通用计算的前端化:过去只能在服务端跑的计算密集型任务(图像处理、物理模拟、路径规划),现在可以在浏览器里用 Compute Shader 实现,享受 GPU 的并行计算能力。
工程软件 Web 化:AutoCAD、SketchUp、Revit 这些传统桌面软件的 Web 化窗口已经打开。Pascal Editor 只是一个开始,未来会有更多垂直领域的专业工具走向浏览器。
跨平台的一致体验:Mac/Windows/Linux 用户打开同一个链接,得到完全一致的体验——这是 Web 的天然优势。Pascal Editor 的技术文档明确支持三个平台,背后的实现成本几乎为零(只依赖 Web 标准 API)。
6.3 开源 3D 编辑器的产品化路径
Pascal Editor 的 GitHub 页面显示它有 7.9k Star,但 Star 数量不等于用户数量。它的产品化路径值得观察:
- 当前阶段:开源工具,技术爱好者试用
- 下一阶段:通过 Supabase 云同步打造在线协作体验,引入团队版付费
- 长期愿景:成为室内设计师、建筑师的浏览器端首选工具,对标 Figma 在 UI 设计领域的地位
Figma 的成功已经证明:浏览器端的工具只要体验足够好,用户愿意放弃桌面软件。Pascal Editor 正在用 WebGPU 和现代前端工程化向这个目标迈进。
七、总结与展望
Pascal Editor 是一个技术密度极高的开源项目。它的价值不仅仅在于"在浏览器里做了一个 3D 建筑编辑器",更在于它展示了一套完整的 2026 年前端工程化最佳实践:
| 技术维度 | Pascal Editor 的实践 | 行业参考价值 |
|---|---|---|
| 架构设计 | Turborepo Monorepo(core + viewer 解耦) | ⭐⭐⭐⭐⭐ 多包协作的标杆 |
| 状态管理 | Zustand + Zundo + temporal middleware | ⭐⭐⭐⭐⭐ 轻量级 undo/redo 实现 |
| 3D 渲染 | Three.js + React Three Fiber + WebGPU | ⭐⭐⭐⭐⭐ WebGPU 渲染管线实践 |
| 几何运算 | three-bvh-csg(BVH 加速的 CSG) | ⭐⭐⭐⭐ 工程级几何运算 |
| 数据持久化 | IndexedDB(本地)+ Supabase(云端) | ⭐⭐⭐⭐⭐ Offline-First 架构 |
| 工程化 | Bun + Turborepo + Next.js 16 + React 19 | ⭐⭐⭐⭐⭐ 2026 前端工具链 |
| 类型安全 | TypeScript 完整类型定义 | ⭐⭐⭐⭐⭐ 全链路 TypeScript |
WebGPU 的到来让浏览器端的 3D 应用终于有了一战桌面软件的可能性。Pascal Editor 的出现不是终点,而是一个开始——它证明了用 React + TypeScript + Bun + Turborepo 这套"前端工具链",完全能构建出性能足以满足专业场景的 3D 应用。
未来,我们可以期待:
- WebGPU Compute Shader 在浏览器端的物理模拟和 AI 推理应用
- 更多垂直领域(机械设计、工业建模、医学可视化)的 WebGPU 应用
- WebGPU 与 WebXR 的结合,让 VR/AR 场景直接在浏览器里运行
Pascal Editor 用 7.9k Star 告诉我们:浏览器不只是信息的载体,它正在成为专业工具的新舞台。
相关资源:
- GitHub:https://github.com/pascalorg/editor
- 在线体验:https://pascaleditor.org
- 技术栈:React 19 + Next.js 16 + Three.js WebGPU + React Three Fiber + Zustand + Zundo + Turborepo + Bun + Supabase + three-bvh-csg