feat(nds): physical dpad

This commit is contained in:
2025-12-14 18:58:22 +01:00
parent e720f5a6c7
commit 03bea6a641
2 changed files with 170 additions and 48 deletions

View File

@@ -22,8 +22,23 @@ const scene = computed(() => model.value?.scene);
let topScreenTexture: THREE.CanvasTexture | null = null;
let bottomScreenTexture: THREE.CanvasTexture | null = null;
let topScreenMesh: THREE.Mesh | null = null;
let bottomScreenMesh: THREE.Mesh | null = null;
/// meshes ///
// screens
const TOP_SCREEN = "Object_9";
const BOTTOM_SCREEN = "Object_28";
// buttons
const CROSS_BUTTON = "Object_21";
const meshes = new Map<string, THREE.Mesh>();
const requireMesh = (key: string): THREE.Mesh => {
const mesh = meshes.get(key);
if (!mesh) throw new Error(`Missing mesh '${key}'`);
return mesh;
};
const { camera, renderer } = useTresContext();
@@ -36,11 +51,15 @@ watch(
topScreenTexture.minFilter = THREE.NearestFilter;
topScreenTexture.magFilter = THREE.NearestFilter;
topScreenTexture.flipY = false;
topScreenTexture.repeat.set(1, 1024 / 404);
topScreenTexture.offset.set(0, -4 / 1024);
bottomScreenTexture = new THREE.CanvasTexture(props.bottomScreenCanvas);
bottomScreenTexture.minFilter = THREE.NearestFilter;
bottomScreenTexture.magFilter = THREE.NearestFilter;
bottomScreenTexture.flipY = false;
bottomScreenTexture.repeat.set(1, 1024 / 532);
bottomScreenTexture.offset.set(0, -1024 / 532 + 1);
},
{ immediate: true },
);
@@ -48,44 +67,86 @@ watch(
watch(scene, () => {
if (!scene.value) return;
meshes.clear();
scene.value.scale.set(100, 100, 100);
scene.value.traverse((child) => {
if (child instanceof THREE.Mesh) {
const material = child.material as THREE.Material;
if (material.name?.includes("screen_up") && topScreenTexture) {
topScreenTexture.repeat.set(1, 1024 / 404);
topScreenTexture.offset.set(0, -4 / 1024);
child.material = new THREE.MeshStandardMaterial({
map: topScreenTexture,
emissive: new THREE.Color(0x222222),
emissiveIntensity: 0.5,
});
topScreenMesh = child;
} else if (
material.name?.includes("screen_down") &&
bottomScreenTexture
) {
bottomScreenTexture.repeat.set(1, 1024 / 532);
bottomScreenTexture.offset.set(0, -1024 / 532 + 1);
child.material = new THREE.MeshStandardMaterial({
map: bottomScreenTexture,
emissive: new THREE.Color(0x222222),
emissiveIntensity: 0.5,
});
bottomScreenMesh = child;
}
meshes.set(child.name, child);
}
});
if (!topScreenTexture || !bottomScreenTexture)
throw new Error(
"topScreenTexture and bottomScreenTexture should be initialized",
);
requireMesh(TOP_SCREEN).material = new THREE.MeshStandardMaterial({
map: topScreenTexture,
emissive: new THREE.Color(0x222222),
emissiveIntensity: 0.5,
});
requireMesh(BOTTOM_SCREEN).material = new THREE.MeshStandardMaterial({
map: bottomScreenTexture,
emissive: new THREE.Color(0x222222),
emissiveIntensity: 0.5,
});
});
const { onRender } = useLoop();
onRender(() => {
const physicalButtonsDown = new Set<string>();
let mousePressedButton: string | null = null;
const keyButtonMappings: [key: string, physicalButton: string][] = [
["ArrowUp", "UP"],
["ArrowDown", "DOWN"],
["ArrowLeft", "LEFT"],
["ArrowRight", "RIGHT"],
];
const keyToButton = new Map(keyButtonMappings);
const buttonToKey = new Map(keyButtonMappings.map(([k, b]) => [b, k]));
useKeyDown((key) => {
const button = keyToButton.get(key);
if (button) physicalButtonsDown.add(button);
});
useKeyUp((key) => {
const button = keyToButton.get(key);
if (button) physicalButtonsDown.delete(button);
});
onRender(({ delta }) => {
if (topScreenTexture) topScreenTexture.needsUpdate = true;
if (bottomScreenTexture) bottomScreenTexture.needsUpdate = true;
const crossButton = meshes.get(CROSS_BUTTON);
if (crossButton) {
const PRESS_ANGLE = Math.PI / 28;
const PRESS_SPEED = 150;
const targetRotation = new THREE.Vector3(0);
if (physicalButtonsDown.has("UP")) targetRotation.setX(-PRESS_ANGLE);
if (physicalButtonsDown.has("RIGHT")) targetRotation.setZ(-PRESS_ANGLE);
if (physicalButtonsDown.has("DOWN")) targetRotation.setX(PRESS_ANGLE);
if (physicalButtonsDown.has("LEFT")) targetRotation.setZ(PRESS_ANGLE);
const lerpFactor = Math.min(delta * PRESS_SPEED, 1);
const currentRotation = new THREE.Vector3().setFromEuler(
crossButton.rotation,
);
currentRotation.lerp(targetRotation, lerpFactor);
crossButton.rotation.setFromVector3(currentRotation);
}
});
const handleClick = (event: MouseEvent) => {
if (!scene.value) return;
const domElement = renderer.instance.domElement;
const rect = domElement.getBoundingClientRect();
@@ -98,41 +159,87 @@ const handleClick = (event: MouseEvent) => {
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;
}
}
}
const intersects = raycaster.intersectObjects(scene.value.children, true);
const intersection = intersects[0];
if (!intersection?.uv) 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);
switch (intersection.object.name) {
case TOP_SCREEN:
case BOTTOM_SCREEN: {
const x = Math.floor(intersection.uv.x * 256);
if (intersection.object.name === TOP_SCREEN) {
const y = Math.floor(intersection.uv.y * (1024 / 404) * 192);
emit("topScreenClick", x, y);
} else if (intersection.object.name === BOTTOM_SCREEN) {
const y = Math.floor(
192 - (1 - intersection.uv.y) * (1024 / 532) * 192,
);
emit("bottomScreenClick", x, y);
}
break;
}
case CROSS_BUTTON: {
const localPos = intersection.point
.clone()
.sub(intersection.object.getWorldPosition(new THREE.Vector3()));
const MIN_DIST = 0.45;
let button: string | null = null;
if (localPos.z <= -MIN_DIST) button = "UP";
else if (localPos.x >= MIN_DIST) button = "RIGHT";
else if (localPos.z >= MIN_DIST) button = "DOWN";
else if (localPos.x <= -MIN_DIST) button = "LEFT";
if (button) {
if (mousePressedButton) {
physicalButtonsDown.delete(mousePressedButton);
const prevKey = buttonToKey.get(mousePressedButton);
if (prevKey) {
window.dispatchEvent(new KeyboardEvent("keyup", { key: prevKey }));
}
}
physicalButtonsDown.add(button);
mousePressedButton = button;
const key = buttonToKey.get(button);
if (key) {
window.dispatchEvent(new KeyboardEvent("keydown", { key }));
}
}
break;
}
}
};
const handleMouseUp = () => {
if (mousePressedButton) {
physicalButtonsDown.delete(mousePressedButton);
const key = buttonToKey.get(mousePressedButton);
if (key) {
window.dispatchEvent(new KeyboardEvent("keyup", { key }));
}
mousePressedButton = null;
}
};
onMounted(() => {
if (renderer) {
renderer.instance.domElement.addEventListener("click", handleClick);
renderer.instance.domElement.addEventListener("mousedown", handleClick);
renderer.instance.domElement.addEventListener("mouseup", handleMouseUp);
}
});
onUnmounted(() => {
if (renderer) {
renderer.instance.domElement.removeEventListener("click", handleClick);
renderer.instance.domElement.removeEventListener("mousedown", handleClick);
renderer.instance.domElement.removeEventListener("mouseup", handleMouseUp);
}
topScreenTexture?.dispose();
bottomScreenTexture?.dispose();

View File

@@ -0,0 +1,15 @@
export type KeyUpCallback = (key: string) => void;
export const useKeyUp = (callback: KeyUpCallback) => {
const handleKeyUp = (event: KeyboardEvent) => {
callback(event.key);
};
onMounted(() => {
window.addEventListener("keyup", handleKeyUp);
});
onUnmounted(() => {
window.removeEventListener("keyup", handleKeyUp);
});
};