写了一款3D可视化编辑器模版,开源!

大家好, 我是徐小夕.

上周和大家分享了多维表格 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采用了组件化、模块化的架构设计,主要分为以下几个部分:

架构图

数据流

  1. 状态管理使用Zustand管理应用状态,包括场景元素、选中元素等
  2. 用户交互用户通过界面进行交互,如拖拽组件、选择元素、调整属性等
  3. 状态更新交互触发状态更新,通过Zustand的actions修改状态
  4. UI渲染状态变化触发UI重新渲染,包括3D场景和编辑界面
  5. 历史记录状态变化被记录到历史栈中,支持撤销/重做操作

核心功能实现

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 &lt; 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

相关推荐
codingandsleeping16 分钟前
Express入门
javascript·后端·node.js
Vaclee19 分钟前
JavaScript-基础语法
开发语言·javascript·ecmascript
拉不动的猪41 分钟前
前端常见数组分析
前端·javascript·面试
小吕学编程1 小时前
ES练习册
java·前端·elasticsearch
Asthenia04121 小时前
Netty编解码器详解与实战
前端
袁煦丞1 小时前
每天省2小时!这个网盘神器让我告别云存储混乱(附内网穿透神操作)
前端·程序员·远程工作
Gladiator5752 小时前
博客记录-day154-面试
github
Gladiator5752 小时前
博客记录-day155-力扣+面试
github
一个专注写代码的程序媛2 小时前
vue组件间通信
前端·javascript·vue.js
一笑code2 小时前
美团社招一面
前端·javascript·vue.js