Skip to content

93. 实战:Three.js Editor(十)

Published:

功能做完后,这节我们来优化一下细节。

首先,现在页面一刷新编辑的场景就没了:

2025-05-17 22.59.00.gif

这样肯定不行。

zustand 内置了持久化的功能:

这是之前的 store:

image.png

我们用 persist 中间件包裹一层,加上持久化功能:

image.png

import { persist } from "zustand/middleware";
const useThreeStore = create(persist((set, get) => {
    return {
    }
}, {
    name: 'xxx'
});

这个 name 是 localStorage 里的 key。

试一下:

2025-05-17 23.07.47.gif

这样,刷新之后,编辑的 3D 场景依然能恢复(甚至你还可以把它保存到服务端)。

在 localStorage 看下:

image.png

可以看到,json 被持久化到了 localStorage 里,key 就是我们指定的 xxx。

然后我们把右边的布局改一下:

image.png

把右边的 json 单独用一个 tab 来展示,并且可以用 monaco editor 来做代码高亮。

tab 我们用 antd 的这个 Segmented 组件来做:

2025-05-17 23.15.50.gif

改下 Properties/index.jsx

image.png

key 为 json 和属性的时候,分别渲染不同的内容。

import { useEffect, useState } from "react";
import { useThreeStore } from "../../store";
import { Segmented, Tree } from "antd";
import Info from "./Info";

function Properties() {
    const { setSelectedObjName, selectedObj, data, scene } = useThreeStore();

    const [treeData, setTreeData] = useState();
    useEffect(() => {
        if(scene?.children) {
            const tree = scene.children.map(item => {
                if(item.isTransformControlsRoot) {
                    return null;
                }

                return {
                    title: item.isMesh ?  item.geometry.type : item.type,
                    key: item.type + item.name + item.id,
                    name: item.name
                }
            }).filter(item => item !== null);

            setTreeData([
                {
                    title: 'Scene',
                    key: 'root',
                    children: tree
                }
            ]);
        }
    }, [scene]);

    function handleSelect(selectKeys, info) {
        const name = info.node.name;
        
        setSelectedObjName(name);
    }

    const [key, setKey] = useState('属性');

    return <div className="Properties">
        <Segmented value={key} onChange={setKey} block options={['属性', 'json']} />
        {
            key === '属性' ? <div>
                <Tree treeData={treeData} expandedKeys={['root']} onSelect={handleSelect}/>
                <Info/>
            </div> : null
        }
        { key === 'json' ? 
            <pre>
                {JSON.stringify(data, null, 2)}
            </pre>: null
        }
    </div>
}

export default Properties;

2025-05-17 23.23.56.gif

然后用 monaco editor 来高亮下 json

安装下:

npm install --save @monaco-editor/react

代码里替换 pre 为 MonacoEditor

image.png

import MonacoEditor from '@monaco-editor/react'
{ key === 'json' ? 
    // <pre>
    //     {JSON.stringify(data, null, 2)}
    // </pre>: null
    <MonacoEditor
        height={'90%'}
        path='code.json'
        language='json'
        value={JSON.stringify(data, null, 2)}
    /> : null
}

看下效果:

2025-05-17 23.32.19.gif

还有一点,为了展示这个场景树,我们把 scene clone 了一份放到 store:

image.png

image.png 其实这是没必要的,性能消耗很高。

其实我们只要遍历一下,把场景树摘出来再放到 store 就可以了。

把 Properties 组件里的遍历逻辑拿过来:

image.png

image.png

const tree = scene.children.map(item => {
if(item.isTransformControlsRoot) {
    return null;
}

return {
    title: item.isMesh ?  item.geometry.type : item.type,
    key: item.type + item.name + item.id,
    name: item.name
}
}).filter(item => item !== null);

setScene(tree);

遍历结果放到 store 里。

初次渲染这里也要改下:

image.png

然后取的地方取出来直接用就可以了:

image.png

useEffect(() => {
    if(scene) {
        setTreeData([
            {
                title: 'Scene',
                key: 'root',
                children: scene
            }
        ]);
    }
}, [scene]);

因为都计算好了,取出来直接用就好了。

2025-06-01 08.50.26.gif

功能正常,但性能提升了很多。

案例代码上传了小册仓库

总结

这节我们优化了下细节的功能。

首先,用 zustand 的 persist 中间件实现了持久化,这样刷新页面,编辑后的场景依然在,它会把 json 保存到 localStorage 里。

然后用 antd 的 Segmented 组件来做了 tab,并且用 MonacoEditor 做了 json 的高亮。

这样,react 结合 Three.js 的 3D 编辑器就完成了。

评论