feat: dispatch clicks on the 3d models to the canvases

This commit is contained in:
2025-12-14 14:36:30 +01:00
parent 789a62ba90
commit cc4f2589ca
3 changed files with 92 additions and 9 deletions

View File

@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js"; import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import { useLoop } from "@tresjs/core"; import { useLoop, useTresContext } from "@tresjs/core";
import * as THREE from "three"; import * as THREE from "three";
const props = defineProps<{ const props = defineProps<{
@@ -8,6 +8,11 @@ const props = defineProps<{
bottomScreenCanvas: HTMLCanvasElement | null; bottomScreenCanvas: HTMLCanvasElement | null;
}>(); }>();
const emit = defineEmits<{
topScreenClick: [x: number, y: number];
bottomScreenClick: [x: number, y: number];
}>();
const { state: model } = useLoader( const { state: model } = useLoader(
GLTFLoader, GLTFLoader,
"/models/nintendo-ds/scene.gltf", "/models/nintendo-ds/scene.gltf",
@@ -17,6 +22,10 @@ const scene = computed(() => model.value?.scene);
let topScreenTexture: THREE.CanvasTexture | null = null; let topScreenTexture: THREE.CanvasTexture | null = null;
let bottomScreenTexture: THREE.CanvasTexture | null = null; let bottomScreenTexture: THREE.CanvasTexture | null = null;
let topScreenMesh: THREE.Mesh | null = null;
let bottomScreenMesh: THREE.Mesh | null = null;
const { camera, renderer } = useTresContext();
watch( watch(
() => [props.topScreenCanvas, props.bottomScreenCanvas], () => [props.topScreenCanvas, props.bottomScreenCanvas],
@@ -51,6 +60,7 @@ watch(scene, () => {
emissive: new THREE.Color(0x222222), emissive: new THREE.Color(0x222222),
emissiveIntensity: 0.5, emissiveIntensity: 0.5,
}); });
topScreenMesh = child;
} else if ( } else if (
material.name?.includes("screen_down") && material.name?.includes("screen_down") &&
bottomScreenTexture bottomScreenTexture
@@ -62,6 +72,7 @@ watch(scene, () => {
emissive: new THREE.Color(0x222222), emissive: new THREE.Color(0x222222),
emissiveIntensity: 0.5, emissiveIntensity: 0.5,
}); });
bottomScreenMesh = child;
} }
} }
}); });
@@ -74,7 +85,55 @@ onRender(() => {
if (bottomScreenTexture) bottomScreenTexture.needsUpdate = true; if (bottomScreenTexture) bottomScreenTexture.needsUpdate = true;
}); });
const handleClick = (event: MouseEvent) => {
const domElement = renderer.instance.domElement;
const rect = domElement.getBoundingClientRect();
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(
new THREE.Vector2(
((event.clientX - rect.left) / rect.width) * 2 - 1,
-((event.clientY - rect.top) / rect.height) * 2 + 1,
),
camera.activeCamera.value,
);
if (topScreenMesh) {
const intersects = raycaster.intersectObject(topScreenMesh);
if (intersects[0]) {
const uv = intersects[0].uv;
if (uv) {
const x = Math.floor(uv.x * 256);
const y = Math.floor(uv.y * (1024 / 404) * 192);
emit("topScreenClick", x, y);
return;
}
}
}
if (bottomScreenMesh) {
const intersects = raycaster.intersectObject(bottomScreenMesh);
if (intersects[0]) {
const uv = intersects[0].uv;
if (uv) {
const x = Math.floor(uv.x * 256);
const y = Math.floor(192 - (1 - uv.y) * (1024 / 532) * 192);
emit("bottomScreenClick", x, y);
}
}
}
};
onMounted(() => {
if (renderer) {
renderer.instance.domElement.addEventListener("click", handleClick);
}
});
onUnmounted(() => { onUnmounted(() => {
if (renderer) {
renderer.instance.domElement.removeEventListener("click", handleClick);
}
topScreenTexture?.dispose(); topScreenTexture?.dispose();
bottomScreenTexture?.dispose(); bottomScreenTexture?.dispose();
}); });

View File

@@ -83,17 +83,17 @@ const renderFrame = (timestamp: number) => {
animationFrameId = requestAnimationFrame(renderFrame); animationFrameId = requestAnimationFrame(renderFrame);
}; };
provide("registerUpdateCallback", registerUpdateCallback);
provide("registerRenderCallback", registerRenderCallback);
provide("registerScreenClickCallback", registerScreenClickCallback);
provide("registerScreenMouseWheelCallback", registerScreenMouseWheelCallback);
onMounted(() => { onMounted(() => {
if (!canvas.value) throw new Error("Missing canvas"); if (!canvas.value) throw new Error("Missing canvas");
ctx = canvas.value.getContext("2d"); ctx = canvas.value.getContext("2d");
if (!ctx) throw new Error("Missing 2d context"); if (!ctx) throw new Error("Missing 2d context");
provide("registerUpdateCallback", registerUpdateCallback);
provide("registerRenderCallback", registerRenderCallback);
provide("registerScreenClickCallback", registerScreenClickCallback);
provide("registerScreenMouseWheelCallback", registerScreenMouseWheelCallback);
canvas.value.addEventListener("click", handleCanvasClick); canvas.value.addEventListener("click", handleCanvasClick);
canvas.value.addEventListener("wheel", handleCanvasWheel, { passive: true }); canvas.value.addEventListener("wheel", handleCanvasWheel, { passive: true });

View File

@@ -6,6 +6,26 @@ const screen = computed(() => route.query.screen as string | undefined);
const topScreen = useTemplateRef("topScreen"); const topScreen = useTemplateRef("topScreen");
const bottomScreen = useTemplateRef("bottomScreen"); const bottomScreen = useTemplateRef("bottomScreen");
const topScreenCanvas = computed(() => topScreen.value?.canvas ?? null);
const bottomScreenCanvas = computed(() => bottomScreen.value?.canvas ?? null);
const handleScreenClick = (
canvas: HTMLCanvasElement | null,
x: number,
y: number,
) => {
if (!canvas) return;
const rect = canvas.getBoundingClientRect();
const clickEvent = new MouseEvent("click", {
bubbles: true,
cancelable: true,
clientX: (x / SCREEN_WIDTH) * rect.width + rect.left,
clientY: (y / SCREEN_HEIGHT) * rect.height + rect.top,
});
canvas.dispatchEvent(clickEvent);
};
</script> </script>
<template> <template>
@@ -18,9 +38,13 @@ const bottomScreen = useTemplateRef("bottomScreen");
<TresDirectionalLight /> <TresDirectionalLight />
<NDS <NDS
v-if="topScreen && bottomScreen" v-if="topScreenCanvas && bottomScreenCanvas"
:top-screen-canvas="topScreen.canvas" :top-screen-canvas="topScreenCanvas"
:bottom-screen-canvas="bottomScreen.canvas" :bottom-screen-canvas="bottomScreenCanvas"
@top-screen-click="(x, y) => handleScreenClick(topScreenCanvas, x, y)"
@bottom-screen-click="
(x, y) => handleScreenClick(bottomScreenCanvas, x, y)
"
/> />
</TresCanvas> </TresCanvas>