Skip to content

99. 实战:酷家乐装修编辑器(三)

Published:

这节我们来初始化 3D 部分:

2025-05-21 19.43.06.gif

它分为两块,2D 的平面户型图、3D 的立体房屋。

平面户型图我们也用 Three.js 来画。

所以一共要在两个 dom 元素下挂载 2 个 canvas

点击 2D、3D 切换按钮的时候,切换显示隐藏。

我们先来写第一个:

改下 components/Main/index.tsx

import { useEffect } from "react";
import { init3D } from "./init-3d";

function Main() {

    useEffect(() => {
        const dom = document.getElementById('threejs-3d-container')!;
        const { scene } = init3D(dom);
          
        return () => {
          dom.innerHTML = '';
        }
    }, []);

    return <div className="Main">
         <div id="threejs-3d-container"></div>
    </div>
}

export default Main;

在 useEffect 里拿到 dom 元素,传给 init3D 方法来做初始化。

当组件销毁的时候,把 innerHTML 清空,也就是销毁 threejs 的 canvas。

然后写一下 Main/init-3d.ts

import * as THREE from 'three';
import {
    OrbitControls
} from 'three/addons/controls/OrbitControls.js';

export function init3D(dom: HTMLElement) {
    const scene = new THREE.Scene();

    const axesHelper = new THREE.AxesHelper(500);
    scene.add(axesHelper);

    const directionalLight = new THREE.DirectionalLight(0xffffff);
    directionalLight.position.set(500, 400, 300);
    scene.add(directionalLight);

    const ambientLight = new THREE.AmbientLight(0xffffff);
    scene.add(ambientLight);

    const width = window.innerWidth;
    const height = window.innerHeight - 60;

    const camera = new THREE.PerspectiveCamera(60, width / height, 1, 10000);
    camera.position.set(500, 500, 500);
    camera.lookAt(0, 0, 0);

    const renderer = new THREE.WebGLRenderer({
        antialias: true
    });
    renderer.setSize(width, height);
    renderer.setClearColor('lightyellow');

    function render() {
        renderer.render(scene, camera);
        requestAnimationFrame(render);
    }

    render();

    dom.append(renderer.domElement);

    window.onresize = function () {
        const width = window.innerWidth;
        const height = window.innerHeight - 60;

        renderer.setSize(width,height);

        camera.aspect = width / height;
        camera.updateProjectionMatrix();
    };
    
    const controls = new OrbitControls(camera, renderer.domElement);

    return {
        scene
    }
}

和前面的 threejs editor 项目一样,创建 Scene、Camera、Renderer、OrbitControls

这次 renderer.setClearColor 设置背景颜色是浅黄色。

宽度是 window.innerWidth 高度是 window.innerHeight - 60

看下效果:

2025-05-21 20.22.00.gif

这样,第一个 scene 就初始化完成了。

然后再写一个,用来渲染平面户型图:

image.png

import { useEffect } from "react";
import { init3D } from "./init-3d";
import { init2D } from "./init-2d";

function Main() {

    useEffect(() => {
        const dom = document.getElementById('threejs-3d-container')!;
        const { scene } = init3D(dom);
          
        return () => {
          dom.innerHTML = '';
        }
    }, []);

    useEffect(() => {
        const dom = document.getElementById('threejs-2d-container')!;
        const { scene } = init2D(dom);
          
        return () => {
          dom.innerHTML = '';
        }
    }, []);

    return <div className="Main">
         <div id="threejs-3d-container"></div>
         <div id="threejs-2d-container"></div>
    </div>
}

export default Main;

初始化 3D 场景的代码和之前一样,只是我们把 clearColor 改成淡蓝色:

Main/init-2d.ts

import * as THREE from 'three';
import {
    OrbitControls
} from 'three/addons/controls/OrbitControls.js';

export function init2D(dom: HTMLElement) {
    const scene = new THREE.Scene();

    const axesHelper = new THREE.AxesHelper(500);
    scene.add(axesHelper);

    const directionalLight = new THREE.DirectionalLight(0xffffff);
    directionalLight.position.set(500, 400, 300);
    scene.add(directionalLight);

    const ambientLight = new THREE.AmbientLight(0xffffff);
    scene.add(ambientLight);

    const width = window.innerWidth;
    const height = window.innerHeight - 60;

    const camera = new THREE.PerspectiveCamera(60, width / height, 1, 10000);
    camera.position.set(500, 500, 500);
    camera.lookAt(0, 0, 0);

    const renderer = new THREE.WebGLRenderer({
        antialias: true
    });
    renderer.setSize(width, height);
    renderer.setClearColor('lightblue');

    function render() {
        renderer.render(scene, camera);
        requestAnimationFrame(render);
    }

    render();

    dom.append(renderer.domElement);

    window.onresize = function () {
        const width = window.innerWidth;
        const height = window.innerHeight - 60;

        renderer.setSize(width,height);

        camera.aspect = width / height;
        camera.updateProjectionMatrix();
    };
    
    const controls = new OrbitControls(camera, renderer.domElement);

    return {
        scene
    }
}

然后在 App.scss 里改下样式:

image.png

.Main {
  position: relative;
  #threejs-3d-container {
    position: absolute;
    z-index: 1;
  }
  #threejs-2d-container {
    position: absolute;
    z-index: 2;
  }
}

两个 canvas 重合,让 2D 的在上面。

看下效果:

2025-05-21 20.30.24.gif

初始化没问题。

但这样把两边的面板挡住了,那俩也要设置 z-index

image.png

2025-05-21 20.31.40.gif

这样就好了。

然后我们来做这个切换按钮:

2025-05-21 20.33.22.gif

用 antd 的 Button 来写:

npm install --save antd

image.png

const [curMode, setCurMode] = useState('2d');

return <div className="Main">
     <div id="threejs-3d-container"></div>
     <div id="threejs-2d-container"></div>
     <div className="mode-change-btns">
        <Button 
            type={curMode === '2d' ? "primary" : 'default'} 
            onClick={() => setCurMode('2d')}
            >2D</Button>
        <Button 
            type={curMode === '3d' ? "primary" : 'default'}
            onClick={() => setCurMode('3d')}
            >3D</Button>
     </div>
</div>

加一个状态来保存 mode,根据状态来设置按钮的 type

然后写下样式:

image.png

height: calc(100vh - 60px);

.mode-change-btns {
    position: absolute;
    z-index: 3;
    bottom: 20px;
    left: 20px;
}

设置整体的高度和按钮的绝对定位。

试下效果:

2025-05-21 20.48.05.gif

这样,切换按钮就完成了。

只是现在展开左侧面板的时候,会把切换按钮盖住,这个我们后面再处理。

然后点击切换按钮的时候,切换两个 canvas 的显示隐藏:

image.png

<div id="threejs-3d-container" style={{display: curMode ==='3d' ? 'block': 'none'}}></div>
<div id="threejs-2d-container" style={{display: curMode ==='2d' ? 'block': 'none'}}></div>

2025-05-24 18.46.46.gif

这样,两个 3D 场景的切换就完成了。

案例代码上传了小册仓库

总结

这节我们初始化了 threejs 部分。

我们需要创建两个场景,分别渲染在两个 canvas 上。

并且添加了一个切换按钮,来切换 2d、3d 的两个场景的显示隐藏。

初始化完 threejs 部分,下节我们来写 store 部分。

评论