291 lines
7.4 KiB
Vue
291 lines
7.4 KiB
Vue
|
<template>
|
||
|
<div class="container">
|
||
|
<div ref="threeContainer" class="three-container"></div>
|
||
|
</div>
|
||
|
</template>
|
||
|
|
||
|
<script setup>
|
||
|
import {
|
||
|
onMounted,
|
||
|
ref
|
||
|
} from 'vue'
|
||
|
import * as THREE from 'three'
|
||
|
import {
|
||
|
OrbitControls
|
||
|
} from 'three/examples/jsm/controls/OrbitControls.js'
|
||
|
|
||
|
const threeContainer = ref(null)
|
||
|
|
||
|
onMounted(() => {
|
||
|
const container = threeContainer.value
|
||
|
const W = container.clientWidth,
|
||
|
H = 400
|
||
|
|
||
|
// Scene, Camera, Renderer
|
||
|
const scene = new THREE.Scene()
|
||
|
scene.background = new THREE.Color(0xf5f5f5)
|
||
|
const camera = new THREE.PerspectiveCamera(60, W / H, 0.1, 1000)
|
||
|
camera.position.set(0, 20, 30)
|
||
|
const renderer = new THREE.WebGLRenderer({
|
||
|
antialias: true
|
||
|
})
|
||
|
renderer.setSize(W, H)
|
||
|
renderer.shadowMap.enabled = true
|
||
|
container.appendChild(renderer.domElement)
|
||
|
|
||
|
// Lights
|
||
|
scene.add(new THREE.AmbientLight(0xffffff, 0.5))
|
||
|
const dirL = new THREE.DirectionalLight(0xffffff, 0.8)
|
||
|
dirL.position.set(-10, 20, 10)
|
||
|
dirL.castShadow = true
|
||
|
scene.add(dirL)
|
||
|
|
||
|
// Floor
|
||
|
const ground = new THREE.Mesh(
|
||
|
new THREE.BoxGeometry(50, 0.02, 50),
|
||
|
new THREE.MeshStandardMaterial({
|
||
|
color: 0xdddddd
|
||
|
})
|
||
|
)
|
||
|
ground.position.y = -0.01
|
||
|
ground.receiveShadow = true
|
||
|
scene.add(ground)
|
||
|
|
||
|
// Materials
|
||
|
const wallMat = new THREE.MeshStandardMaterial({
|
||
|
color: 0xf8f8f8,
|
||
|
roughness: 0.7,
|
||
|
metalness: 0.1,
|
||
|
side: THREE.DoubleSide
|
||
|
})
|
||
|
const lintelMat = new THREE.MeshStandardMaterial({
|
||
|
color: 0xe0e0e0,
|
||
|
roughness: 0.6,
|
||
|
metalness: 0.1
|
||
|
})
|
||
|
const doorMat = new THREE.MeshStandardMaterial({
|
||
|
color: 0x8b4513,
|
||
|
roughness: 0.6,
|
||
|
metalness: 0.2
|
||
|
})
|
||
|
const bedFrameMat = new THREE.MeshStandardMaterial({
|
||
|
color: 0x555555,
|
||
|
roughness: 0.5,
|
||
|
metalness: 0.8
|
||
|
})
|
||
|
const mattressMat = new THREE.MeshStandardMaterial({
|
||
|
color: 0xffffff,
|
||
|
roughness: 0.8,
|
||
|
metalness: 0.1
|
||
|
})
|
||
|
const tvMat = new THREE.MeshStandardMaterial({
|
||
|
color: 0x000000,
|
||
|
roughness: 0.4,
|
||
|
metalness: 0.3
|
||
|
})
|
||
|
|
||
|
let selectedRoom = null
|
||
|
|
||
|
function markOriginal(mesh) {
|
||
|
mesh.userData.originalMaterial = mesh.material.clone()
|
||
|
}
|
||
|
|
||
|
function createRoom({
|
||
|
w,
|
||
|
h,
|
||
|
d,
|
||
|
x,
|
||
|
z,
|
||
|
door
|
||
|
}) {
|
||
|
const room = new THREE.Group()
|
||
|
room.position.set(x, h / 2, z)
|
||
|
room.userData.isRoom = true
|
||
|
room.userData.selected = false
|
||
|
|
||
|
const planeH = new THREE.PlaneGeometry(w, h)
|
||
|
const planeD = new THREE.PlaneGeometry(d, h)
|
||
|
|
||
|
// South with door
|
||
|
const upH = h - door.height - door.sill
|
||
|
const upWall = new THREE.Mesh(new THREE.PlaneGeometry(w, upH), wallMat.clone())
|
||
|
upWall.position.set(0, door.height + door.sill + upH / 2 - h / 2, d / 2)
|
||
|
markOriginal(upWall);
|
||
|
room.add(upWall)
|
||
|
|
||
|
const sideW = (w - door.width) / 2
|
||
|
const leftWall = new THREE.Mesh(new THREE.PlaneGeometry(sideW, door.height), wallMat.clone())
|
||
|
leftWall.position.set(-w / 2 + sideW / 2, door.sill + door.height / 2 - h / 2, d / 2)
|
||
|
markOriginal(leftWall);
|
||
|
room.add(leftWall)
|
||
|
const rightWall = leftWall.clone()
|
||
|
rightWall.position.x = w / 2 - sideW / 2
|
||
|
markOriginal(rightWall);
|
||
|
room.add(rightWall)
|
||
|
|
||
|
const lintel = new THREE.Mesh(new THREE.BoxGeometry(door.width, door.lintelThk, 0.1), lintelMat
|
||
|
.clone())
|
||
|
lintel.position.set(0, door.sill + door.height + door.lintelThk / 2 - h / 2, d / 2 + 0.05)
|
||
|
markOriginal(lintel);
|
||
|
room.add(lintel)
|
||
|
|
||
|
const doorGroup = new THREE.Group()
|
||
|
const doorMesh = new THREE.Mesh(new THREE.BoxGeometry(door.width, door.height, 0.05), doorMat.clone())
|
||
|
markOriginal(doorMesh);
|
||
|
doorMesh.position.set(door.width / 2, door.height / 2 - h / 2, 0)
|
||
|
doorGroup.add(doorMesh)
|
||
|
doorGroup.position.set(-w / 2 + sideW + 0.01, 0, d / 2 + 0.025)
|
||
|
doorGroup.userData.isDoor = true
|
||
|
doorGroup.userData.open = false
|
||
|
room.add(doorGroup)
|
||
|
|
||
|
// North, East, West walls
|
||
|
const north = new THREE.Mesh(planeH, wallMat.clone())
|
||
|
north.position.set(0, 0, -d / 2)
|
||
|
north.rotation.y = Math.PI
|
||
|
markOriginal(north);
|
||
|
room.add(north)
|
||
|
const east = new THREE.Mesh(planeD, wallMat.clone())
|
||
|
east.position.set(w / 2, 0, 0)
|
||
|
east.rotation.y = -Math.PI / 2
|
||
|
markOriginal(east);
|
||
|
room.add(east)
|
||
|
const west = east.clone()
|
||
|
west.position.x = -w / 2
|
||
|
west.rotation.y = Math.PI / 2
|
||
|
markOriginal(west);
|
||
|
room.add(west)
|
||
|
|
||
|
// Single Bed flush
|
||
|
const bedW = w * 0.6,
|
||
|
bedH = 0.5,
|
||
|
bedD = d * 0.4
|
||
|
const bed = new THREE.Group()
|
||
|
bed.userData.isBed = true
|
||
|
const frame = new THREE.Mesh(new THREE.BoxGeometry(bedW, 0.1, bedD), bedFrameMat.clone())
|
||
|
frame.position.y = -h / 2 + 0.05
|
||
|
markOriginal(frame);
|
||
|
bed.add(frame)
|
||
|
const mat = new THREE.Mesh(new THREE.BoxGeometry(bedW * 0.95, bedH, bedD * 0.95), mattressMat.clone())
|
||
|
mat.position.y = -h / 2 + bedH / 2 + 0.05
|
||
|
markOriginal(mat);
|
||
|
bed.add(mat)
|
||
|
bed.position.set(0, 0, d / 2 - bedD / 2 - 0.05)
|
||
|
room.add(bed)
|
||
|
|
||
|
// TV flush opposite
|
||
|
const tvWidth = bedW * 0.6
|
||
|
const tvHeight = tvWidth * 9 / 16
|
||
|
const tv = new THREE.Mesh(new THREE.BoxGeometry(tvWidth, tvHeight, 0.05), tvMat.clone())
|
||
|
tv.userData.isTV = true
|
||
|
markOriginal(tv);
|
||
|
tv.position.set(0, bedH + tvHeight / 2, -d / 2 + 0.05)
|
||
|
room.add(tv)
|
||
|
|
||
|
room.traverse(o => {
|
||
|
if (o.isMesh) {
|
||
|
o.castShadow = true
|
||
|
o.receiveShadow = true
|
||
|
}
|
||
|
})
|
||
|
return room
|
||
|
}
|
||
|
|
||
|
// Build grid
|
||
|
const building = new THREE.Group()
|
||
|
const cols = 5,
|
||
|
rows = 4,
|
||
|
sx = 6,
|
||
|
sz = 8
|
||
|
for (let i = 0; i < 20; i++) {
|
||
|
const col = i % cols,
|
||
|
row = Math.floor(i / cols)
|
||
|
const x = -((cols - 1) * sx) / 2 + col * sx
|
||
|
const z = ((rows - 1) * sz) / 2 - row * sz
|
||
|
building.add(createRoom({
|
||
|
w: 4,
|
||
|
h: 3,
|
||
|
d: 5,
|
||
|
x,
|
||
|
z,
|
||
|
door: {
|
||
|
width: 1,
|
||
|
height: 2,
|
||
|
sill: 0.1,
|
||
|
lintelThk: 0.2
|
||
|
}
|
||
|
}))
|
||
|
}
|
||
|
scene.add(building)
|
||
|
|
||
|
const controls = new OrbitControls(camera, renderer.domElement)
|
||
|
controls.enableDamping = true
|
||
|
|
||
|
const ray = new THREE.Raycaster(),
|
||
|
mouse = new THREE.Vector2()
|
||
|
renderer.domElement.addEventListener('click', e => {
|
||
|
const rect = renderer.domElement.getBoundingClientRect()
|
||
|
mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1
|
||
|
mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1
|
||
|
ray.setFromCamera(mouse, camera)
|
||
|
const hits = ray.intersectObjects(building.children, true)
|
||
|
if (!hits.length) return
|
||
|
|
||
|
// Door
|
||
|
let obj = hits[0].object
|
||
|
const doorGrp = obj.userData.isDoor ? obj : obj.parent.userData.isDoor ? obj.parent : null
|
||
|
if (doorGrp) {
|
||
|
const open = !doorGrp.userData.open
|
||
|
doorGrp.rotation.y = open ? -Math.PI / 2 : 0
|
||
|
doorGrp.userData.open = open
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Room selection
|
||
|
let tgt = hits[0].object
|
||
|
while (tgt && !tgt.userData.isRoom) tgt = tgt.parent
|
||
|
if (!tgt) return
|
||
|
|
||
|
// Deselect previous
|
||
|
if (selectedRoom && selectedRoom !== tgt) {
|
||
|
selectedRoom.userData.selected = false
|
||
|
selectedRoom.traverse(n => {
|
||
|
if (n.isMesh && n.userData.originalMaterial) {
|
||
|
n.material = n.userData.originalMaterial.clone()
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
|
||
|
// Toggle
|
||
|
const sel = !tgt.userData.selected
|
||
|
tgt.userData.selected = sel
|
||
|
tgt.traverse(n => {
|
||
|
if (n.isMesh && n.userData.originalMaterial) {
|
||
|
let matClone = n.userData.originalMaterial.clone()
|
||
|
if (sel && !n.userData.isTV && !n.userData.isDoor) matClone.color.set(0x3377ff)
|
||
|
n.material = matClone
|
||
|
}
|
||
|
})
|
||
|
selectedRoom = sel ? tgt : null
|
||
|
})
|
||
|
|
||
|
const animate = () => {
|
||
|
requestAnimationFrame(animate);
|
||
|
controls.update();
|
||
|
renderer.render(scene, camera)
|
||
|
}
|
||
|
animate()
|
||
|
})
|
||
|
</script>
|
||
|
|
||
|
<style scoped>
|
||
|
.container {
|
||
|
width: 100%;
|
||
|
height: 400px
|
||
|
}
|
||
|
|
||
|
.three-container {
|
||
|
width: 100%;
|
||
|
height: 100%
|
||
|
}
|
||
|
</style>
|