bx-865-apps #1
|
@ -18,14 +18,10 @@ class Element3D(models.Model):
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
description = models.TextField()
|
description = models.TextField()
|
||||||
min_distance = models.IntegerField(
|
min_distance = models.IntegerField(
|
||||||
validators=[MinValueValidator(1), MaxValueValidator(200)],
|
validators=[MinValueValidator(1), MaxValueValidator(200)], blank=True, null=True
|
||||||
blank=True,
|
|
||||||
null=True
|
|
||||||
)
|
)
|
||||||
max_distance = models.IntegerField(
|
max_distance = models.IntegerField(
|
||||||
validators=[MinValueValidator(2), MaxValueValidator(200)],
|
validators=[MinValueValidator(2), MaxValueValidator(200)], blank=True, null=True
|
||||||
blank=True,
|
|
||||||
null=True
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
@ -37,7 +33,15 @@ class ClickableArea(models.Model):
|
||||||
description = models.TextField()
|
description = models.TextField()
|
||||||
object_name = models.CharField(max_length=255)
|
object_name = models.CharField(max_length=255)
|
||||||
target = models.ForeignKey(
|
target = models.ForeignKey(
|
||||||
Element3D, on_delete=models.CASCADE, related_name="clickable_areas"
|
Element3D,
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name="clickable_areas",
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
)
|
||||||
|
source = models.ForeignKey(
|
||||||
|
Element3D,
|
||||||
|
on_delete=models.PROTECT,
|
||||||
)
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
|
|
@ -12,7 +12,10 @@ class NumberInFilter(django_filters.BaseInFilter, django_filters.NumberFilter):
|
||||||
class ClickableAreaViewSet(viewsets.ModelViewSet):
|
class ClickableAreaViewSet(viewsets.ModelViewSet):
|
||||||
queryset = ClickableArea.objects.all()
|
queryset = ClickableArea.objects.all()
|
||||||
serializer_class = ClickableAreaSerializer
|
serializer_class = ClickableAreaSerializer
|
||||||
filterset_fields = ("target",)
|
filterset_fields = (
|
||||||
|
"target",
|
||||||
|
"object_name",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Element3DFilter(django_filters.rest_framework.FilterSet):
|
class Element3DFilter(django_filters.rest_framework.FilterSet):
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { IMAGE_URL, SERVER_URL, } from '../../constants'
|
import { IMAGE_URL, SERVER_URL, } from '../../constants'
|
||||||
import { onMounted, reactive, ref } from 'vue';
|
import { onMounted, onUnmounted, reactive, ref } from 'vue';
|
||||||
import { Box3, Vector3 } from 'three';
|
import { Box3, Vector2, Vector3 } from 'three';
|
||||||
|
|
||||||
import { useTresContext } from '@tresjs/core';
|
import { useTresContext } from '@tresjs/core';
|
||||||
import { useGLTF } from '@tresjs/cientos'
|
import { useGLTF } from '@tresjs/cientos'
|
||||||
|
@ -23,8 +23,17 @@ interface model3DType {
|
||||||
modelUrl?: string,
|
modelUrl?: string,
|
||||||
modelFile?: any
|
modelFile?: any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shadows_and_pos(scene: any) {
|
||||||
|
scene.children.forEach((el: any) => {
|
||||||
|
el.receiveShadow = true
|
||||||
|
el.castShadow = true
|
||||||
|
shadows_and_pos(el)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const models = ref<model3DType[]>([])
|
const models = ref<model3DType[]>([])
|
||||||
const { camera, controls } = useTresContext()
|
const { controls, raycaster, camera, scene } = useTresContext()
|
||||||
const loadModels = async () => {
|
const loadModels = async () => {
|
||||||
const res = await fetch(`${SERVER_URL}/api/obj/element/?parent__isnull=True`)
|
const res = await fetch(`${SERVER_URL}/api/obj/element/?parent__isnull=True`)
|
||||||
const data = await res.json() as element3DType[]
|
const data = await res.json() as element3DType[]
|
||||||
|
@ -32,14 +41,13 @@ const loadModels = async () => {
|
||||||
min: controls.value.minDistance,
|
min: controls.value.minDistance,
|
||||||
max: controls.value.maxDistance == Infinity ? 1 : controls.value.maxDistance,
|
max: controls.value.maxDistance == Infinity ? 1 : controls.value.maxDistance,
|
||||||
}
|
}
|
||||||
console.log(camera)
|
|
||||||
console.log(controls)
|
|
||||||
for (let index = 0; index < data.length; index++) {
|
for (let index = 0; index < data.length; index++) {
|
||||||
const element = data[index];
|
const element = data[index];
|
||||||
const item = {} as model3DType
|
const item = {} as model3DType
|
||||||
|
|
||||||
item.modelUrl = `${IMAGE_URL}/${element.model_file}`
|
item.modelUrl = `${IMAGE_URL}/${element.model_file}`
|
||||||
let { scene: loaded_scene } = await useGLTF(item.modelUrl)
|
let { scene: loaded_scene } = await useGLTF(item.modelUrl)
|
||||||
|
shadows_and_pos(loaded_scene)
|
||||||
item.modelFile = loaded_scene
|
item.modelFile = loaded_scene
|
||||||
|
|
||||||
models.value.push(item)
|
models.value.push(item)
|
||||||
|
@ -59,16 +67,36 @@ const loadModels = async () => {
|
||||||
controls.value.minDistance = distance.min
|
controls.value.minDistance = distance.min
|
||||||
controls.value._needsUpdate = true
|
controls.value._needsUpdate = true
|
||||||
controls.value.update(1)
|
controls.value.update(1)
|
||||||
|
|
||||||
}
|
}
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadModels()
|
loadModels()
|
||||||
|
document.addEventListener('click', clickEvent)
|
||||||
})
|
})
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('click', clickEvent)
|
||||||
|
})
|
||||||
|
const pointer = reactive({ x: 0, y: 0 })
|
||||||
|
const clickEvent = (event: MouseEvent) => {
|
||||||
|
console.time('raycaster')
|
||||||
|
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 intersects = raycaster.value.intersectObjects(scene.value.children);
|
||||||
|
const names = intersects.map(el => el.object.name ?? false).filter(Boolean)
|
||||||
|
if (names.length) {
|
||||||
|
console.log({ pointer, names })
|
||||||
|
}
|
||||||
|
console.timeEnd('raycaster')
|
||||||
|
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<TresGroup v-for="item in models">
|
<TresGroup v-for="item in models">
|
||||||
<Suspense>
|
<TresObject3D v-bind="item.modelFile.clone()" />
|
||||||
<ModelItem v-bind="item" />
|
|
||||||
</Suspense>
|
|
||||||
</TresGroup>
|
</TresGroup>
|
||||||
</template>
|
</template>
|
|
@ -6,9 +6,9 @@ const props = defineProps(['modelUrl', 'modelFile', 'onClick'])
|
||||||
let scene: any
|
let scene: any
|
||||||
if (props.modelUrl) {
|
if (props.modelUrl) {
|
||||||
let { scene: loaded_scene } = await useGLTF(props.modelUrl)
|
let { scene: loaded_scene } = await useGLTF(props.modelUrl)
|
||||||
scene = loaded_scene
|
scene = loaded_scene.clone()
|
||||||
} else if (props.modelFile) {
|
} else if (props.modelFile) {
|
||||||
scene = props.modelFile
|
scene = props.modelFile.clone()
|
||||||
}
|
}
|
||||||
function shadows_and_pos(scene: any) {
|
function shadows_and_pos(scene: any) {
|
||||||
scene.children.forEach((el: any) => {
|
scene.children.forEach((el: any) => {
|
||||||
|
@ -21,7 +21,7 @@ function shadows_and_pos(scene: any) {
|
||||||
shadows_and_pos(scene)
|
shadows_and_pos(scene)
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<TresMesh>
|
<TresMesh v-if="scene.children[0].isObject3D">
|
||||||
<primitive :object="scene" />
|
<TresObject3D v-bind="scene.children[0].clone()"/>
|
||||||
</TresMesh>
|
</TresMesh>
|
||||||
</template>
|
</template>
|
Loading…
Reference in New Issue