demo-int-table/front/src/components/Promo/load_models.vue

380 lines
13 KiB
Vue

<script setup lang="ts">
import { onMounted, onUnmounted, reactive, Ref, ref, watch } from 'vue';
import {
Box3, Color, DoubleSide, Group, Mesh, PlaneGeometry,
MeshStandardMaterial, MeshStandardMaterialParameters,
Vector2, Vector3,
CircleGeometry, MeshBasicMaterial,
Quaternion,
} from 'three';
import { useTresContext, useSeek, useTexture, useLoop } from '@tresjs/core';
import { useGLTF } from '@tresjs/cientos'
import Env from './env.vue'
import { IMAGE_URL, PROMOBG, SERVER_URL, } from '../../constants'
import { usePromoSidebar } from '../../stores/promo_sidebar';
import { usePromoScene } from '../../stores/promo_scene';
import { useClickable } from '../../stores/clickable';
import { useLoading } from '../../stores/loading';
import { mobileAndTabletCheck } from '../../helpers';
import { useTimer } from '../../stores/timer';
const props = defineProps(['source', 'loaded', 'loaded_pan'])
const CON_MOVETO_COUNT = 150;
const CAM_MOVETO_COUNT = 100;
const models = ref<model3DType[]>([])
const clickable_items = ref<any[]>([])
const clickable_refs = ref<any[]>([])
const envVars = reactive({}) as EnvVars
const def_distance = reactive({ max: 10, min: 1 })
const controls_targetto = ref() as Ref<Vector3 | undefined>;
const controls_targetto_count = ref(CON_MOVETO_COUNT)
const camera_moveto = ref() as Ref<Vector3 | undefined>;
const camera_moveto_count = ref(CON_MOVETO_COUNT)
const camera_rotatetoto = ref() as Ref<Quaternion | undefined>;
const camera_rotatetoto_count = ref(CAM_MOVETO_COUNT)
const sidebar = usePromoSidebar();
const sidebar_scene = usePromoScene();
const clickable = useClickable()
const loading_store = useLoading()
const { controls, camera, scene, raycaster, renderer } = useTresContext()
const { seekByName, seekAllByName } = useSeek()
const groundTexture = await useTexture({
displacementMap: '/ground_displacement.jpg',
})
const pointerTexture = await useTexture({
map: '/pointer_texture.png'
})
const loadModels = async () => {
const res = await fetch(`${SERVER_URL}/api/obj/scene/${props.source}`)
const raw_data = await res.json() as scene3D
loading_store.status = 'other'
def_distance.max = raw_data.max_distance
def_distance.min = raw_data.min_distance
envVars.focus = raw_data.max_distance * 0.5
if (raw_data.env) {
Object.assign(envVars, raw_data.env)
} else {
delete envVars.env_displacementmap
delete envVars.env_normalmap
delete envVars.hdr_gainmap
delete envVars.hdr_json
delete envVars.hdr_webp
envVars.clear_color = PROMOBG
}
loading_store.status = 'env'
const data = raw_data.elements
if (!controls.value) return;
controls.value.enabled = false;
(controls.value as any).minDistance = mobileAndTabletCheck() ? raw_data.min_distance * 0.5 : raw_data.min_distance;
(controls.value as any).maxDistance = raw_data.max_distance;
camera.value?.position.set(
(controls.value as any).maxDistance * 0.5,
(controls.value as any).maxDistance * 0.5,
(controls.value as any).maxDistance * 0.5
);
(controls.value as any).target = new Vector3(0, 0, 0);
(controls.value as any).autoRotate = false;
(controls.value as any)._needsUpdate = true;
(controls.value as any).update()
const sidebar_items = [] as PromoScene[]
clickable_items.value = []
loading_store.status = 'model'
for (let index = 0; index < data.length; index++) {
loading_store.count = index
const element = data[index];
const item = {} as model3DType
item.modelUrl = `${IMAGE_URL}/${element.model_file}`
let { scene: loaded_scene } = await useGLTF(item.modelUrl)
item.modelFile = loaded_scene
item.name = element.name
if (!element.is_enabled) {
item.modelFile.visible = false
}
if (item.modelFile.children[0]) {
item.modelFile.children[0].position.set(
item.modelFile.children[0].position.x + element.x_pos,
item.modelFile.children[0].position.y + element.y_pos,
item.modelFile.children[0].position.z + element.z_pos
)
item.modelFile.children[0].updateMatrixWorld(true)
}
item.modelFile.updateMatrixWorld(true)
models.value.push(item)
const res = await fetch(`${SERVER_URL}/api/obj/clickable/?source=${element.id}`)
const clickable_areas = await res.json()
clickable.list.push(...clickable_areas)
}
let c = new Color()
if (envVars.clear_color) {
c.set(envVars.clear_color)
} else {
renderer.value.getClearColor(c)
}
const tex = {} as any
if (envVars.env_displacementmap) { tex.displacementMap = `${IMAGE_URL}/${envVars.env_displacementmap}` }
if (envVars.env_normalmap) { tex.normalMap = `${IMAGE_URL}/${envVars.env_normalmap}` }
let addTexture: any
if (Object.keys(tex).length > 0) {
addTexture = await useTexture(tex)
}
if (!models.value.find(el => el.name == 'ground')) {
loading_store.status = 'ground'
const mesh = {
color: c.offsetHSL(0, 0.5, -0.33),
displacementScale: envVars.focus * 0.33,
roughness: 100,
side: DoubleSide
} as MeshStandardMaterialParameters
if (envVars.env_displacementmap) {
mesh.displacementMap = addTexture.displacementMap
} else {
mesh.displacementMap = groundTexture.displacementMap
}
if (envVars.env_normalmap) {
mesh.normalMap = addTexture.normalMap
}
const ground = new Mesh(
new PlaneGeometry(envVars.focus * 7, envVars.focus * 7, 1024, 1024),
new MeshStandardMaterial(mesh)
)
ground.position.y = -0.33 * envVars.focus
ground.rotateX(-Math.PI / 2)
ground.name = "ground"
models.value.push({ name: 'ground', modelFile: ground })
}
for (let index = 0; index < clickable.list.length; index++) {
loading_store.status = 'clickable'
const element = clickable.list[index];
const find_element = seekByName(scene.value, element.object_name)
if (!find_element) continue
if (find_element && !(find_element as Group).isGroup) {
const world_position = new Vector3();
((find_element as Mesh).geometry.boundingBox as any).getCenter(world_position);
(find_element as Mesh).localToWorld(world_position);
const p = raw_data.min_distance * 0.05
const point_mesh = new Mesh(
new CircleGeometry(p, 32),
new MeshBasicMaterial({ map: pointerTexture.map })
)
point_mesh.rotateX(-0.5 * Math.PI)
const point = new Group()
point.add(point_mesh)
point.position.set(world_position.x, p * 3, world_position.z * 2)
point.name = `${element.id}_clickable`
point.updateMatrixWorld()
if (clickable_items.value.find(el => el.name == point.name)) continue
clickable_items.value.push(point)
clickable_refs.value.push(ref(`${element.id}_clickable`))
sidebar_items.push({
id: element.id,
name: element.name
})
}
}
sidebar_scene.name = raw_data.name;
sidebar_scene.setData(sidebar_items)
loading_store.status = 'boxes'
const box = new Box3();
models.value.forEach(element => {
if (element.name !== 'ground') {
box.expandByObject(element.modelFile.clone());
}
});
const box_size = new Vector3();
box.getSize(box_size)
console.log(box_size)
props.loaded_pan(
new Vector3(box_size.x * 0.5, box_size.y * 0.5, box_size.z * 0.5),
new Vector3(box_size.x * -0.25, box_size.y * -0.25, box_size.z * -0.25),
)
controls.value.enabled = true;
props.loaded(false)
timer.startTimer()
if (controls.value && (controls.value as any).autoRotate) {
(controls.value as any).autoRotate = false;
}
}
const gotoCenterAndDistance = () => {
(controls.value as any).minDistance = mobileAndTabletCheck() ? def_distance.min * 0.5 : def_distance.min;
(controls.value as any).maxDistance = def_distance.max;
controls_targetto.value = new Vector3(0, 0, 0);
camera_moveto.value = new Vector3(
def_distance.max * 0.5,
def_distance.max * 0.5,
def_distance.max * 0.5
);
}
loadModels()
watch(() => props.source, () => {
const loaded = seekByName(scene.value, 'loaded')
if (loaded) {
loaded.children = []
}
sidebar.close()
loadModels()
})
watch(() => sidebar, () => {
if (sidebar.is_open == false) {
gotoCenterAndDistance()
}
if (sidebar.is_open && sidebar.id_clickable) {
const clickable = useClickable()
const target = clickable.list.find(el => el.id == sidebar.id_clickable)
if (!target) return
const el = seekByName(scene.value, `${sidebar.id_clickable}_clickable`);
if (el) {
(controls.value as any).maxDistance = 10;
(controls.value as any).minDistance = 1;
const target_vector = new Vector3();
el.getWorldPosition(target_vector);
controls_targetto.value = target_vector;
camera_rotatetoto.value = new Quaternion(0, 1, 0.25, -0.25)
}
}
}, { deep: true })
const { onAfterRender } = useLoop()
onAfterRender(() => {
clickable_refs.value.map(el => {
if (el.value[0] && el.value[0].children) {
el.value[0].children[0].lookAt(camera.value?.position)
}
})
if (controls.value) {
if (timer.seconds_left == 0) {
(controls.value as any).update();
}
}
if (controls_targetto.value) {
timer.stopTimer();
(controls.value as any).target.lerp(controls_targetto.value, 0.01);
controls_targetto_count.value -= 1
if (controls_targetto_count.value == 0) {
controls_targetto_count.value = CON_MOVETO_COUNT
controls_targetto.value = undefined
}
(controls.value as any).update()
}
if (camera_moveto.value) {
timer.stopTimer();
camera.value?.position.lerp(camera_moveto.value, 0.01);
camera_moveto_count.value -= 1
if (camera_moveto_count.value == 0) {
camera_moveto_count.value = CON_MOVETO_COUNT
camera_moveto.value = undefined
}
}
if (camera_rotatetoto.value) {
timer.stopTimer();
camera.value?.quaternion.slerp(camera_rotatetoto.value, 0.01);
camera_rotatetoto_count.value -= 1;
if (camera_rotatetoto_count.value == 0) {
camera_rotatetoto_count.value = CAM_MOVETO_COUNT
camera_rotatetoto.value = undefined
}
}
})
const timer = useTimer()
timer.timer_func = () => {
if (timer.seconds_left == 0 && !(controls.value as any).autoRotate && (controls.value as any).enabled) {
gotoCenterAndDistance();
(controls.value as any).autoRotate = true;
(controls.value as any).autoRotateSpeed = 0.5;
}
}
const stopTimer = () => {
timer.resetTimer()
if ((controls.value as any).autoRotate) {
(controls.value as any).autoRotate = false
}
}
const pointer = reactive({ x: 0, y: 0 })
const clickEvent = (event: MouseEvent) => {
const x = (event.clientX / window.innerWidth) * 2 - 1
const y = - (event.clientY / window.innerHeight) * 2 + 1
if (x == pointer.x && y == pointer.y) return
if (!camera.value) return
pointer.x = x
pointer.y = y
raycaster.value.setFromCamera(new Vector2(pointer.x, pointer.y), camera.value);
const clickable_objects = seekAllByName(scene.value, '_clickable');
const intersects = raycaster.value.intersectObjects(clickable_objects);
const names = intersects.map(el => (el.object.parent ? el.object.parent.name : el.object.name) ?? false).filter(Boolean)
if (names.length) {
sidebar.open(parseInt(names[0].replace('_clickable', '')))
}
}
const timerEvent = ['click', 'contextmenu', 'mousedown', 'mouseup', 'touchstart', 'touchend', 'touchmove']
onMounted(() => {
document.addEventListener('click', clickEvent)
timerEvent.map((event: string) => {
document.addEventListener(event, stopTimer)
})
if (sidebar.is_open) {
sidebar.close()
}
})
onUnmounted(() => {
document.removeEventListener('click', clickEvent)
timerEvent.map((event: string) => {
document.removeEventListener(event, stopTimer)
})
})
</script>
<template>
<TresGroup name="loaded">
<Env v-bind="envVars" />
<!-- <PostProcessing /> -->
<template v-for="item in models">
<TresGroup :name="item.name">
<TresObject3D v-bind="item.modelFile.clone()" />
</TresGroup>
</template>
<template v-for="(item, i) in clickable_items">
<TresObject3D v-bind="item.clone()" :ref="clickable_refs[i]" />
</template>
</TresGroup>
</template>