大家好, 我是徐小夕.
上周和大家分享了多维表格 flowmix/mute 相关的内容,也上线了第一个版本供大家参考体验:

最近抽空又写了一个3D可视化编辑器的模版 i3D-Editor,我们可以使用它轻松构建3D可视化编辑器,目前已经在github上开源,文末会附带github地址。

接下来就和大家分享一下这款开源3D可视化编辑器模版以及核心功能实现。
demo演示

之所以写这个项目,是因为之前有小伙伴对我的专栏中分享的3D可视化项目比较感兴趣,想学习一下设计思路,所以我就写了一个基础的,并且流程完整的3D可视化搭建项目,大家可以基于我设计的这套框架,来二次扩展自己的3D可视化搭建平台。
整个搭建平台核心模块包含如下几个部分:
- 3D场景渲染
- 组件拖拽系统
- 元素编辑功能
- 状态管理
- 历史记录与撤销/重做
技术栈
前端框架与库
- React 18 用于构建用户界面的JavaScript库
- Next.js 14 React框架,提供服务端渲染、路由等功能
- TypeScript 静态类型检查的JavaScript超集
- Three.js 3D图形库,用于在浏览器中渲染3D场景
- React Three Fiber Three.js的React渲染器
- React Three Drei Three.js的React组件集合
- Tailwind CSS 实用优先的CSS框架
- Lucide React 现代图标库
状态管理与工具
- Zustand 轻量级状态管理库
- UUID 用于生成唯一标识符
- HTML5 Drag and Drop API 原生拖放功能实现
技术架构

i3D Editor采用了组件化、模块化的架构设计,主要分为以下几个部分:
架构图

数据流
- 状态管理使用Zustand管理应用状态,包括场景元素、选中元素等
- 用户交互用户通过界面进行交互,如拖拽组件、选择元素、调整属性等
- 状态更新交互触发状态更新,通过Zustand的actions修改状态
- UI渲染状态变化触发UI重新渲染,包括3D场景和编辑界面
- 历史记录状态变化被记录到历史栈中,支持撤销/重做操作
核心功能实现
3D场景渲染
i3D Editor使用React Three Fiber和React Three Drei来简化Three.js的使用,实现3D场景的渲染。
jsx
// Canvas设置
<Canvas
camera={{ position: viewMode === "3D" ? [5, 5, 5] : [0, 5, 0], fov: 50 }}
shadows
className="w-full h-full"
>
<ambientLight intensity={0.5} />
<directionalLight position={[10, 10, 10]} intensity={1} castShadow />
{/* 网格 */}
<Grid
args={[100, 100]}
cellSize={1}
cellThickness={0.5}
cellColor="#a0a0ff"
sectionSize={5}
sectionThickness={1}
sectionColor="#2080ff"
fadeDistance={50}
fadeStrength={1.5}
followCamera={false}
infiniteGrid
/>
{/* 场景对象 */}
{elements.map((element) => (
<SceneObject
key={element.id}
element={element}
isSelected={selectedElement?.id === element.id}
onClick={() => setSelectedElement(element)}
viewMode={viewMode}
activeMode={activeMode}
/>
))}
{/* 控制器 */}
<OrbitControls makeDefault enabled={!selectedElement || activeMode !== "select"} />
<Environment preset="studio" />
</Canvas>
组件拖拽系统
i3D Editor实现了一个基于HTML5 Drag and Drop API的拖拽系统,允许用户从组件库拖拽元素到3D场景中。
javascript
// 拖拽开始
const handleDragStart = (event, elementType) => {
event.dataTransfer.setData("application/element-type", elementType);
event.dataTransfer.effectAllowed = "copy";
// 通知父组件拖拽开始
onStartDrag(elementType);
};
// 拖拽结束
const handleDrop = (event) => {
event.preventDefault();
if (!isDragging || !draggedElementType) return;
// 获取Canvas容器的位置和尺寸
const rect = canvasContainerRef.current.getBoundingClientRect();
// 计算鼠标在Canvas中的相对位置
const x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
const y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
// 创建射线并计算与平面的交点
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(
new THREE.Vector2(x, y),
new THREE.PerspectiveCamera(50, rect.width / rect.height, 0.1, 1000)
);
const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
const target = new THREE.Vector3();
raycaster.ray.intersectPlane(plane, target);
// 添加元素到场景
addElement({
type: draggedElementType,
position: { x: target.x, y: 0, z: target.z },
rotation: { x: 0, y: 0, z: 0 },
scale: 1,
material: "standard",
color: getDefaultColor(draggedElementType),
name: getElementName(draggedElementType),
displayTip: false,
});
// 重置拖拽状态
setIsDragging(false);
setDraggedElementType(null);
};
### 元素编辑功能
i3D Editor提供了浮动工具栏,允许用户对选中的元素进行编辑操作,如移动、旋转、复制和删除等。
```jsx
// 浮动工具栏定位
<div
ref={toolbarRef}
className={`absolute bg-white rounded-lg shadow-lg z-20 flex ${isHorizontalLayout ? "flex-row" : "flex-col"} items-center p-2`}
style={{
left: `${toolbarPosition.x}px`,
top: `${toolbarPosition.y}px`,
transform: "translate(-50%, -120%)",
transition: "left 0.2s ease, top 0.2s ease",
}}
>
{/* 工具按钮 */}
<button
className={`p-1.5 rounded-full hover:bg-blue-100 ${activeMode === "select" ? "text-blue-500" : ""}`}
onClick={() => handleToolClick("select")}
title="选择工具"
>
<MousePointer className="h-4 w-4" />
</button>
{/* 更多工具按钮... */}
</div>
状态管理实现
i3D Editor使用Zustand进行状态管理,包括场景元素、选中元素等。
typescript
// 元素存储
export const useElementStore = create<ElementStore>((set) => ({
elements: [],
selectedElement: null,
addElement: (element) =>
set((state) => ({
elements: [...state.elements, { ...element, id: uuidv4() }],
})),
updateElement: (id, updates) =>
set((state) => ({
elements: state.elements.map((element) =>
element.id === id ? { ...element, ...updates } : element
),
selectedElement:
state.selectedElement?.id === id
? { ...state.selectedElement, ...updates }
: state.selectedElement,
})),
removeElement: (id) =>
set((state) => ({
elements: state.elements.filter((element) => element.id !== id),
selectedElement: state.selectedElement?.id === id ? null : state.selectedElement,
})),
setSelectedElement: (element) => set({ selectedElement: element }),
// 更多actions...
}));
历史记录与撤销/重做

i3D Editor实现了历史记录功能,支持撤销和重做操作。
typescript
// 历史记录状态
const [history, setHistory] = useState<HistoryState[]>([]);
const [historyIndex, setHistoryIndex] = useState(-1);
const [isUndoRedo, setIsUndoRedo] = useState(false);
// 监听元素变化,更新历史记录
useEffect(() => {
if (isUndoRedo) {
setIsUndoRedo(false);
return;
}
if (historyIndex >= 0) {
const currentState: HistoryState = {
elements: JSON.parse(JSON.stringify(elements)),
selectedElementId: selectedElement?.id || null,
};
// 检查是否与当前历史记录状态相同
const lastState = history[historyIndex];
const isEqual =
JSON.stringify(lastState.elements) === JSON.stringify(currentState.elements) &&
lastState.selectedElementId === currentState.selectedElementId;
if (!isEqual) {
// 如果在历史记录中间进行了操作,则删除后面的历史记录
const newHistory = history.slice(0, historyIndex + 1);
setHistory([...newHistory, currentState]);
setHistoryIndex(historyIndex + 1);
}
}
}, [elements, selectedElement, history, historyIndex]);
// 撤销操作
const handleUndo = () => {
if (historyIndex > 0) {
setIsUndoRedo(true);
const prevState = history[historyIndex - 1];
// 应用历史状态
loadElements(prevState.elements);
// 恢复选中状态
if (prevState.selectedElementId) {
const selectedElement = prevState.elements.find(
(el) => el.id === prevState.selectedElementId
);
if (selectedElement) {
setSelectedElement(selectedElement);
}
} else {
setSelectedElement(null);
}
setHistoryIndex(historyIndex - 1);
}
};
// 重做操作
const handleRedo = () => {
if (historyIndex < history.length - 1) {
setIsUndoRedo(true);
const nextState = history[historyIndex + 1];
// 应用历史状态
loadElements(nextState.elements);
// 恢复选中状态
if (nextState.selectedElementId) {
const selectedElement = nextState.elements.find(
(el) => el.id === nextState.selectedElementId
);
if (selectedElement) {
setSelectedElement(selectedElement);
}
} else {
setSelectedElement(null);
}
setHistoryIndex(historyIndex + 1);
}
};
上面就是核心功能实现,当然项目还有很多需要优化,这里只是给大家提供一个方案思路参考,如果感兴趣可以在github上下载代码学习。
github地址:github.com/MrXujiang/3...
最后
我们最近研发的 flowmix/docx多模态文档引擎,目前也在持续更新中,欢迎体验参考:

文档地址:flowmix.turntip.cn