Skip to content

案例展示

模型吸附(点 / 线 / 面)

TIP

绿色为模型 A(目标),橙色为模型 B(主动吸附)。球体为吸附点,亮线为吸附边,半透明面为吸附面。B 主动对齐到 A。

模型吸附(B → A)
<template>
    <div style="position: relative">
        <div id="TO"></div>
        <el-form id="formSnap" label-width="0px">
            <div class="form-title">模型吸附(B → A)</div>
            <el-form-item>
                <el-button type="primary" size="small" @click="doSnap('point')">点吸附</el-button>
                <el-button type="primary" size="small" @click="doSnap('line')">线吸附</el-button>
                <el-button type="primary" size="small" @click="doSnap('face')">面吸附</el-button>
            </el-form-item>
            <el-form-item>
                <el-button size="small" @click="resetPose()">复位</el-button>
            </el-form-item>
        </el-form>
    </div>
</template>

<script setup>
import { onMounted } from 'vue'
import { ElForm, ElFormItem, ElButton } from 'element-plus'

let TO = null
let itemA = null
let itemB = null
let poseB = null

onMounted(async () => {
    TO = new ThingOrigin('snapScene', document.getElementById('TO'), {
        scene: {
            background: { type: 'color', color: { alpha: 1, color: '#1f2430' } }
        },
        camera: { position: { x: 20, y: 40, z: 70 } },
        helper: {
            axes: { active: true },
            grid: { active: true, size: 200, divisions: 40 }
        }
    })

    // A:目标模型(绿);B:主动吸附模型(橙)
    itemA = await makeCube({
        name: 'modelA',
        pos: { x: -18, y: 5, z: 0 },
        rot: { x: 0, y: 0, z: 0 },
        color: '#57a359',
        ptColor: '#ffffff',
        lineColor: '#36cfc9',
        faceColor: '#95de64'
    })

    itemB = await makeCube({
        name: 'modelB',
        pos: { x: 28, y: 8, z: 12 },
        rot: { x: 0.7, y: 1.1, z: 0.4 },
        color: '#e6a23c',
        ptColor: '#ffd666',
        lineColor: '#ffc53d',
        faceColor: '#ffe58f'
    })

    // 记录 B 初始位姿,供复位
    poseB = {
        pos: itemB.cube.position.clone(),
        quat: itemB.cube.quaternion.clone()
    }
})

// 立方体 + 吸附点(球)+ 吸附线 + 吸附面,均挂在本地坐标
// initCube 默认 1×1×1,initPlane 尺寸在 base.rect 下,需显式指定与辅助几何一致
async function makeCube(cfg) {
    const size = 10
    const half = size / 2

    const cube = TO.model.initCube({
        position: cfg.pos,
        rotation: cfg.rot,
        base: { width: size, height: size, depth: size },
        material: { color: cfg.color }
    })
    cube.name = cfg.name

    const pt = TO.model.initSphere({
        position: { x: half, y: half, z: half },
        base: { radius: 0.8, widthSegments: 12, heightSegments: 12 },
        material: { color: cfg.ptColor }
    })
    cube.add(pt)

    // 两点直线,供 snapLine / getLineCenter 使用
    const line = TO.line.initLine({
        base: {
            p1: { x: -half, y: half, z: half + 0.15 },
            p2: { x: half, y: half, z: half + 0.15 }
        },
        material: { color: cfg.lineColor }
    })
    cube.add(line)

    // segments=1,保证前 3 顶点构成有效三角面;尺寸与立方体正面一致
    const face = await TO.model.initPlane({
        position: { x: 0, y: 0, z: half + 0.01 },
        rotation: { x: 0, y: 0, z: 0 },
        base: {
            type: 'rect',
            rect: {
                width: size,
                height: size,
                widthSegments: 1,
                heightSegments: 1
            }
        },
        material: {
            type: 'color',
            color: { color: cfg.faceColor, opacity: 0.4 },
            doubleSide: true
        }
    })
    if (face.material) {
        face.material.transparent = true
        face.material.opacity = 0.4
    }
    cube.add(face)

    TO.scene.add(cube)
    return { cube, pt, line, face }
}

// 面法线:优先 TO.tool.getWorldDirection,旧版 bundle 回退 Three.js 原生 API
function getNormal(face) {
    return TO.tool.getWorldDirection(face)
}

// 更新世界矩阵
function updateWorld() {
    itemA.cube.updateMatrixWorld(true)
    itemB.cube.updateMatrixWorld(true)
}

// B 主动吸附 A;每次先复位便于对比
function doSnap(type) {
    if (!TO || !itemA || !itemB) return
    resetPose()
    updateWorld()

    if (type === 'point') {
        TO.snap.snapPoint(
            { model: itemA.cube, position: TO.tool.getWorldPosition(itemA.pt) },
            { model: itemB.cube, position: TO.tool.getWorldPosition(itemB.pt) }
        )
        return
    }

    if (type === 'line') {
        TO.snap.snapLine({ model: itemA.cube, line: itemA.line }, { model: itemB.cube, line: itemB.line })
        return
    }

    TO.snap.snapFace(
        { model: itemA.cube, face: itemA.face, normal: getNormal(itemA.face) },
        { model: itemB.cube, face: itemB.face, normal: getNormal(itemB.face) }
    )
}

function resetPose() {
    if (!itemB || !poseB) return
    itemB.cube.position.copy(poseB.pos)
    itemB.cube.quaternion.copy(poseB.quat)
    itemB.cube.updateMatrixWorld(true)
}
</script>

<style scoped>
#TO {
    width: 100%;
    height: 400px;
    position: relative;
}

#formSnap {
    position: absolute;
    top: 10px;
    right: 10px;
    min-width: 240px;
    box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.3);
    z-index: 1000;
    background: rgba(0, 0, 0, 0.6);
    color: #fff;
    padding: 12px 16px;
    border-radius: 4px;
    font-size: 12px;
}

.form-title {
    font-size: 13px;
    font-weight: 700;
    margin-bottom: 8px;
    color: #fff;
}

#formSnap :deep(.el-form-item__label) {
    color: #fff;
    font-size: 12px;
}
</style>

API 介绍

snap.snapPoint

方法签名返回值描述
snapPoint(modelA: snapPointParams, modelB: snapPointParams)-点吸附:将 B 的指定点平移到 A 的指定点

参数说明:

参数名说明类型必填默认值
modelA模型 A 及点(目标)snapPointParams-
modelB模型 B 及点(主动吸附)snapPointParams-

snapPointParams

属性说明类型必填默认值
model待吸附的模型对象Object3D-
position吸附点世界坐标(需支持 clone()Vector3-

snap.snapLine

方法签名返回值描述
snapLine(modelA: snapLineParams, modelB: snapLineParams)-线吸附:将 B 的线段方向对齐到 A,并平移使线段中心重合

参数说明:

参数名说明类型必填默认值
modelA模型 A 及线(目标)snapLineParams-
modelB模型 B 及线(主动吸附)snapLineParams-

snapLineParams

属性说明类型必填默认值
model待吸附的模型对象Object3D-
line吸附线(Line,几何需为两点直线)Line-

snap.snapFace

方法签名返回值描述
snapFace(modelA: snapFaceParams, modelB: snapFaceParams)-面吸附:将 B 的面法线对齐到 A,并平移使面中心重合

参数说明:

参数名说明类型必填默认值
modelA模型 A 及面(目标)snapFaceParams-
modelB模型 B 及面(主动吸附)snapFaceParams-

snapFaceParams

属性说明类型必填默认值
model待吸附的模型对象Object3D-
face吸附面网格(使用几何前 3 个顶点计算面中心)Mesh-
normal面法线(世界坐标,需支持 clone() / normalize()Vector3-

使用要点

  • 三个方法均为 B 主动去吸附 A:只变换 modelB.model 的位置 / 旋转。
  • snapPoint 计算 positionA - positionB 作为偏移,加到 B 的 position。吸附点世界坐标可用 TO.tool.getWorldPosition(obj) 获取。
  • snapLine 要求 line 为两点直线(几何 position 长度为 6)。方向对齐后按线段中心补偿位移,可用 TO.line.getLineCenter(line) 获取世界坐标中点。
  • snapFacenormal 做法线对齐,面中心可用 TO.tool.getFaceWorldCenter(face)(取几何前 3 顶点世界坐标均值)。面法线可用 TO.tool.getWorldDirection(face)
  • 吸附点 / 线 / 面建议作为模型子对象挂在本地坐标,吸附前调用 updateMatrixWorld(true)