左边的编辑器告一段落,这节我们来做右边的部分:

右边上面是场景的对象树的展示,下面是属性的编辑。
我们先写上面部分:

如何拿到这样的场景中所有的对象呢?
用 scene.traverse 就可以

但 threejs 的场景是在 Main 组件里,我们要把它传到 Properties 组件里,明显需要通过 store。
我们在 store 里加个属性方法:

scene: null,
setScene(obj) {
set({
scene: obj
})
},
然后在 Main 组件里用 setScene 把 scene 放入 store

接着在 Properties 组件里取出来:

const { selectedObj, data, scene } = useThreeStore();
useEffect(() => {
if(scene?.traverse) {
scene.traverse(obj => {
console.log(obj);
})
}
}, [scene]);
拿到 store 里的 scene,遍历打印所有的对象

可以看到,它会遍历打印所有的对象,比如 GridHelper 下的每一条 Line。
其实没必要用 traverse,直接用 scene.children 取就行,因为我们只有一层。
useEffect(() => {
if(scene?.children) {
scene.children.forEach(item => {
console.log(item);
});
}
}, [scene]);
第一层只有这 4 个对象:

我们在渲染完物体之后也重新更新一下 scene:

react 会浅层对比 scene 有没有变化,这里 clone 一下来触发更新。
当我添加一个立方体、一个圆柱之后,拿到的对象都是对的:

接下来把它用树形组件展示出来就行。


引入 Tree 组件,它的 treeData 是数据,expandedKeys 指定哪些 key 展开。
创建一个 state,在 scene 更新的时候,设置 state 数据。
这里要注意的是,如果是 Mesh,title 就展示几何体的类型,比如 BoxGeometry,否则展示对象的类型
import { useEffect, useState } from "react";
import { useThreeStore } from "../../store";
import { Tree } from "antd";
function Properties() {
const { selectedObj, data, scene } = useThreeStore();
const [treeData, setTreeData] = useState();
useEffect(() => {
if(scene?.children) {
const tree = scene.children.map(item => {
return {
title: item.isMesh ? item.geometry.type : item.type,
key: item.type + item.name + item.id
}
});
setTreeData([
{
title: 'Scene',
key: 'root',
children: tree
}
]);
}
}, [scene]);
return <div className="Properties">
<Tree treeData={treeData} expandedKeys={['root']}/>
{/* <div>selectedObj: {selectedObj?.name}</div>
<pre>
{JSON.stringify(data, null, 2)}
</pre> */}
</div>
}
export default Properties;
试下效果:

可以看到,对象树展示出来的,只是多了一个 Object3D,这是 TransformControls 的 helper 加的
我们过滤下:

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
}
}).filter(item => item !== null);
这样就好了:

添加一个圆柱:

右边也同步变化了。
然后点击右边的 item 的时候,也要选中左边对应的物体。
这个就是在全局 store 存一个 selectedObjName,变化的时候,从 scene 种找到对应的物体来 attach 就好了。

selectedObjName: null,
setSelectedObjName(name) {
set({
selectedObjName: name
})
},
store 里添加这个属性和方法。
然后在 Properties 组件里点击 tree 的 item 的时候,设置 selectedObjName:


import { useEffect, useState } from "react";
import { useThreeStore } from "../../store";
import { Tree } from "antd";
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);
}
return <div className="Properties">
<Tree treeData={treeData} expandedKeys={['root']} onSelect={handleSelect}/>
{/* <div>selectedObj: {selectedObj?.name}</div>
<pre>
{JSON.stringify(data, null, 2)}
</pre> */}
</div>
}
export default Properties;
然后在 Main 组件里,在 selectedObjName 变化的时候,遍历找对应的 obj,然后让 TransformControls attach 它。
我们先暴露下 attach 方法:

function transformControlsAttachObj(obj) {
transformControls.attach(obj);
}
和之前切换 mode 一样,这里也是导出一个方法,在组件利用 ref 保存。
当然,这里直接把 TransformControls 导出会更方便。

const transformControlsAttachObjRef = useRef();
transformControlsAttachObjRef.current = transformControlsAttachObj;
然后在 name 切换的时候,遍历找到 obj,让 TransformControls attach 它:

useEffect(() => {
if(selectedObjName) {
const obj = sceneRef.current.getObjectByName(selectedObjName);
setSelectedObj(obj);
transformControlsAttachObjRef.current(obj);
}
}, [selectedObjName]);
试下效果:

这样,点击 tree,选中对应的对象就完成了。
最后,我们来修一个 bug:

选中物体点击 delete 的时候,物体删除了,对应的 helper 还在,并且控制台报错了。
说是 helper attach 到的物体不在场景树中。
这个问题的解决也比较简单,就是先让 helper detach,之后再删就好了。
改一下暴露的 attach 方法,如果 obj 是空,就 detach

if(!obj) {
transformControls.detach();
return;
}
然后在删除物体之前,先 detach 一下:

if(selectedObj) {
transformControlsAttachObjRef.current(null);
sceneRef.current.remove(selectedObj);
removeMesh(selectedObj.name);
}
试下效果:

这样就好了。
案例代码上传了小册仓库
总结
这节我们实现了场景树的展示,点击树选中对应的对象。
首先我们把 scene 保存到了 store,在 Properties 组件里取出来遍历。
因为只有一层,我们直接用 scene.chilren 拿到数据,用 antd 的 Tree 组件展示。
之后添加了一个 selectedObjName 的 store 属性,点击树的 item 的时候,修改它。
然后在 Main 组件里查找对应的 obj,让 TransformControls attach 上它就好了。
这样,场景树就完成了。