449 lines
15 KiB
Vue
449 lines
15 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, AdditiveBlending,
|
|
} from 'three';
|
|
|
|
import { useTresContext, useSeek, useTexture, useLoop } from '@tresjs/core';
|
|
// @ts-ignore
|
|
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';
|
|
import { useRawData } from '../../stores/raw_data';
|
|
|
|
const props = defineProps(['source', 'loaded_pan'])
|
|
|
|
const models = ref<model3DType[]>([])
|
|
const clickable_items = ref<any[]>([])
|
|
const clickable_refs = ref<any[]>([])
|
|
const envVars = reactive({}) as EnvVars
|
|
const process_loading = ref(null)
|
|
const targetDistance = reactive({ max: 10, min: 0 })
|
|
let sidebar_clickable = [] as PromoScene[]
|
|
let sidebar_visible = [] as PromoScene[]
|
|
let addTexture: any
|
|
|
|
const COUNT = 100
|
|
const controls_targetto = ref() as Ref<Vector3 | undefined>;
|
|
const controls_targetto_count = ref(COUNT)
|
|
const camera_moveto = ref() as Ref<Vector3 | undefined>;
|
|
const camera_moveto_count = ref(COUNT)
|
|
const camera_rotatetoto = ref() as Ref<Quaternion | undefined>;
|
|
const camera_rotatetoto_count = ref(COUNT)
|
|
|
|
const sidebar = usePromoSidebar();
|
|
const sidebar_scene = usePromoScene();
|
|
const clickable = useClickable()
|
|
const loading_store = useLoading()
|
|
const raw_dataStore = useRawData()
|
|
|
|
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 setEnv = async () => {
|
|
envVars.focus = raw_dataStore.data.max_distance * 0.5
|
|
if (raw_dataStore.data.env) {
|
|
Object.assign(envVars, raw_dataStore.data.env)
|
|
} else {
|
|
delete envVars.env_displacementmap
|
|
delete envVars.env_normalmap
|
|
envVars.clear_color = PROMOBG
|
|
}
|
|
|
|
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}` }
|
|
if (Object.keys(tex).length > 0) {
|
|
addTexture = await useTexture(tex)
|
|
}
|
|
}
|
|
|
|
const setControls = () => {
|
|
if (!controls.value) return;
|
|
controls.value.enabled = false;
|
|
targetDistance.max = raw_dataStore.data.max_distance;
|
|
targetDistance.min = mobileAndTabletCheck() ? raw_dataStore.data.min_distance * 0.5 : raw_dataStore.data.min_distance;
|
|
// (controls.value as any).minDistance = mobileAndTabletCheck() ? raw_dataStore.data.min_distance * 0.5 : raw_dataStore.data.min_distance;
|
|
(controls.value as any).maxDistance = raw_dataStore.data.max_distance;
|
|
|
|
const d = targetDistance.max * 0.5
|
|
camera.value?.position.set(d, d, d);
|
|
(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 makeGround = () => {
|
|
const mesh = {
|
|
color: new Color(envVars.clear_color).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 })
|
|
}
|
|
|
|
const clearValues = () => {
|
|
clickable_items.value = []
|
|
clickable_refs.value = []
|
|
sidebar_clickable = []
|
|
sidebar_visible = []
|
|
}
|
|
|
|
const loadModels = async () => {
|
|
if (!props.source) return
|
|
if (!raw_dataStore.data) return
|
|
|
|
console.log(`load models ${props.source} ${process_loading.value}`)
|
|
clearValues()
|
|
|
|
loading_store.status = 'loading'
|
|
process_loading.value = props.source
|
|
await raw_dataStore.load(props)
|
|
raw_dataStore.data.loading = true
|
|
|
|
loading_store.status = 'env'
|
|
await setEnv()
|
|
|
|
loading_store.status = 'other'
|
|
setControls()
|
|
|
|
sidebar_scene.setName({ name: raw_dataStore.data.name, description: raw_dataStore.data.name })
|
|
|
|
loading_store.status = 'model'
|
|
for (let index = 0; index < raw_dataStore.data.elements.length; index++) {
|
|
if (process_loading.value !== props.source) return
|
|
loading_store.count = index
|
|
const element = raw_dataStore.data.elements[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.id = element.id
|
|
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)
|
|
}
|
|
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)
|
|
|
|
if (!element.can_not_disable) {
|
|
sidebar_visible.push(element)
|
|
}
|
|
}
|
|
sidebar_scene.setVisible(sidebar_visible)
|
|
|
|
if (!models.value.find(el => el.name == 'ground')) {
|
|
loading_store.status = 'ground'
|
|
makeGround()
|
|
}
|
|
|
|
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_dataStore.data.min_distance * 0.05
|
|
|
|
const point_mesh = new Mesh(
|
|
new CircleGeometry(2, 32),
|
|
new MeshBasicMaterial({
|
|
color: new Color(envVars.clear_color),
|
|
map: pointerTexture.map,
|
|
transparent: true,
|
|
blending: AdditiveBlending
|
|
})
|
|
)
|
|
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`
|
|
|
|
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_clickable.push({
|
|
is_enabled: true,
|
|
id: element.id,
|
|
name: element.name
|
|
})
|
|
}
|
|
}
|
|
|
|
sidebar_scene.setClickable(sidebar_clickable)
|
|
|
|
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);
|
|
|
|
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 as any).enabled = true;
|
|
raw_dataStore.data.loading = false;
|
|
|
|
timer.startTimer()
|
|
if (controls.value && (controls.value as any).autoRotate) {
|
|
(controls.value as any).autoRotate = false;
|
|
}
|
|
|
|
// process_loading.value = null
|
|
}
|
|
|
|
const gotoCenterAndDistance = () => {
|
|
targetDistance.min = mobileAndTabletCheck() ? raw_dataStore.data.min_distance * 0.5 : raw_dataStore.data.min_distance;
|
|
targetDistance.max = raw_dataStore.data.max_distance;
|
|
controls_targetto.value = new Vector3(0, 0, 0);
|
|
camera_moveto.value = new Vector3(
|
|
raw_dataStore.data.max_distance * 0.5,
|
|
raw_dataStore.data.max_distance * 0.5,
|
|
raw_dataStore.data.max_distance * 0.5
|
|
);
|
|
}
|
|
|
|
watch(() => props.source, () => {
|
|
if (props.source) {
|
|
raw_dataStore.$reset()
|
|
const loaded = seekByName(scene.value, 'loaded')
|
|
if (loaded) {
|
|
loaded.children = []
|
|
}
|
|
console.log('props change')
|
|
sidebar.close()
|
|
// loadModels()
|
|
} else {
|
|
renderer.value.dispose()
|
|
}
|
|
}, { deep: true })
|
|
|
|
watch(() => sidebar.is_open, () => {
|
|
if (sidebar.is_open == false) {
|
|
gotoCenterAndDistance();
|
|
}
|
|
})
|
|
watch(() => sidebar.is_open && sidebar.id_clickable, () => {
|
|
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) {
|
|
targetDistance.max = 10
|
|
targetDistance.min = 1
|
|
const target_vector = new Vector3();
|
|
|
|
el.getWorldPosition(target_vector);
|
|
target_vector.y = 10;
|
|
controls_targetto.value = target_vector;
|
|
|
|
const quaternion = new Quaternion();
|
|
quaternion.setFromAxisAngle(new Vector3(1, 0, 0), -45 * 4 * (Math.PI / 180));
|
|
camera_rotatetoto.value = quaternion
|
|
camera.value.rotation.z += Math.PI/2
|
|
camera_moveto.value = target_vector;
|
|
}
|
|
}
|
|
}, { 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)
|
|
|
|
const dis_to_cam = camera.value?.position.distanceTo(el.value[0].position);
|
|
if (dis_to_cam) {
|
|
const scaling = (1 * dis_to_cam) / 100
|
|
el.value[0].children[0].scale.set(scaling, scaling, scaling);
|
|
el.value[0].updateMatrixWorld()
|
|
}
|
|
}
|
|
})
|
|
const koef = 0.02
|
|
if (controls_targetto.value) {
|
|
timer.stopTimer();
|
|
(controls.value as any).target.lerp(controls_targetto.value, koef);
|
|
controls_targetto_count.value -= 1
|
|
if (controls_targetto_count.value == 0) {
|
|
controls_targetto_count.value = COUNT;
|
|
controls_targetto.value = undefined;
|
|
}
|
|
}
|
|
if (camera_moveto.value) {
|
|
timer.stopTimer();
|
|
camera.value?.position.lerp(camera_moveto.value, koef);
|
|
camera_moveto_count.value -= 1
|
|
if (camera_moveto_count.value == 0) {
|
|
camera_moveto.value = undefined;
|
|
camera_moveto_count.value = COUNT;
|
|
|
|
// (controls.value as any).maxDistance = targetDistance.max;
|
|
// (controls.value as any).minDistance = targetDistance.min;
|
|
}
|
|
}
|
|
if (!camera_moveto.value && !controls_targetto.value && camera_rotatetoto.value) {
|
|
timer.stopTimer();
|
|
camera.value?.quaternion.slerp(camera_rotatetoto.value, koef);
|
|
// camera.value?.quaternion.normalize();
|
|
camera_rotatetoto_count.value -= 1;
|
|
if (camera_rotatetoto_count.value == 0) {
|
|
camera_rotatetoto_count.value = COUNT;
|
|
camera_rotatetoto.value = undefined
|
|
}
|
|
}
|
|
(controls.value as any).update()
|
|
})
|
|
|
|
const timer = useTimer()
|
|
timer.timer_func = () => {
|
|
if (timer.seconds_left == 0 && !(controls.value as any).autoRotate && (controls.value as any).enabled) {
|
|
gotoCenterAndDistance();
|
|
sidebar.close();
|
|
(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) => {
|
|
if (event.target && !(event.target as HTMLElement).closest('canvas')) return
|
|
|
|
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', '')))
|
|
sidebar.toggleAccordion('clickable')
|
|
}
|
|
}
|
|
const timerEvent = ['click', 'contextmenu', 'mousedown', 'mouseup', 'touchstart', 'touchend', 'touchmove']
|
|
onMounted(() => {
|
|
console.log('mount')
|
|
clearValues()
|
|
loadModels()
|
|
|
|
document.addEventListener('click', clickEvent)
|
|
|
|
timerEvent.map((event: string) => {
|
|
document.addEventListener(event, stopTimer)
|
|
})
|
|
|
|
if (sidebar.is_open) {
|
|
sidebar.close()
|
|
}
|
|
})
|
|
onUnmounted(() => {
|
|
console.log('unmount load models')
|
|
clearValues()
|
|
document.removeEventListener('click', clickEvent)
|
|
timerEvent.map((event: string) => {
|
|
document.removeEventListener(event, stopTimer)
|
|
})
|
|
renderer.value.dispose()
|
|
})
|
|
</script>
|
|
<template>
|
|
<TresGroup name="loaded" :key="props.source" ref="loaded">
|
|
<Env v-bind="envVars" />
|
|
<!-- <PostProcessing /> -->
|
|
<template v-for="item in models">
|
|
<TresGroup :name="item.name"
|
|
:visible="sidebar_scene.visible.find(el => el.id == item.id)?.is_enabled ?? true">
|
|
<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> |