From 787d0b2e35414d3cd36519afa374d991725a44a7 Mon Sep 17 00:00:00 2001 From: faithia-anastasia <211831874+faithia-anastasia@users.noreply.github.com> Date: Thu, 19 Feb 2026 12:06:00 +0900 Subject: [PATCH 1/5] feat(collision): add function to detect collisions --- src/utils/isColliding.ts | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/utils/isColliding.ts diff --git a/src/utils/isColliding.ts b/src/utils/isColliding.ts new file mode 100644 index 0000000..82adb57 --- /dev/null +++ b/src/utils/isColliding.ts @@ -0,0 +1,9 @@ +import type { Planet } from "@/types/planet"; + +export function isColliding(planet1: Planet, planet2: Planet): boolean { + const distanceSquared = planet1.position.distanceToSquared(planet2.position); + + const radiusSum = planet1.radius + planet2.radius; + + return distanceSquared <= radiusSum ** 2; +} From 9ca10492c08f64ace050b6b99c47e8cc41cd3f9f Mon Sep 17 00:00:00 2001 From: faithia-anastasia <211831874+faithia-anastasia@users.noreply.github.com> Date: Thu, 19 Feb 2026 13:58:02 +0900 Subject: [PATCH 2/5] feat(collision): add temporary explosion effect for planets --- src/components/Explosion.tsx | 102 +++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 src/components/Explosion.tsx diff --git a/src/components/Explosion.tsx b/src/components/Explosion.tsx new file mode 100644 index 0000000..ee02219 --- /dev/null +++ b/src/components/Explosion.tsx @@ -0,0 +1,102 @@ +import { useRef, useState, useEffect } from "react"; +import { useFrame } from "@react-three/fiber"; +import * as THREE from "three"; +import type { Planet } from "@/types/planet"; + +export type Fragment = { + mesh: THREE.Mesh; + velocity: THREE.Vector3; + rotationAxis: THREE.Vector3; + lifetime: number; +}; + +type ExplosionProps = { + planet: Planet; + fragmentCount?: number; + onComplete?: () => void; +}; + +export const Explosion: React.FC = ({ + planet, + fragmentCount = 50, + onComplete, +}: ExplosionProps) => { + const groupRef = useRef(null); + const [fragments, setFragments] = useState([]); + + // 爆発初期化 + useEffect(() => { + const newFragments: Fragment[] = []; + for (let i = 0; i < fragmentCount; i++) { + const size = Math.random() * (planet.radius * 0.2) + 0.05; + const geometry = new THREE.SphereGeometry(size, 6, 6); + const material = new THREE.MeshStandardMaterial({ + color: 0xffaa33, + emissive: 0xff5500, + }); + const mesh = new THREE.Mesh(geometry, material); + + // 初期位置は惑星中心 + mesh.position.copy(planet.position); + + // ランダム方向に飛ぶ速度 + const velocity = new THREE.Vector3( + (Math.random() - 0.5) * 4, + (Math.random() - 0.5) * 4, + (Math.random() - 0.5) * 4, + ); + + // 回転軸 + const rotationAxis = new THREE.Vector3( + Math.random(), + Math.random(), + Math.random(), + ).normalize(); + + newFragments.push({ + mesh, + velocity, + rotationAxis, + lifetime: Math.random() * 2 + 1, // 1~3秒で消える + }); + } + setFragments(newFragments); + }, [planet, fragmentCount]); + + // フレームごとの更新 + useFrame((_, delta) => { + if (fragments.length === 0) return; + + setFragments((prev) => { + const alive: Fragment[] = []; + prev.forEach((f) => { + // 位置更新 + f.mesh.position.add(f.velocity.clone().multiplyScalar(delta)); + + // 回転 + f.mesh.rotateOnAxis(f.rotationAxis, delta * 5); + + // 減速(摩擦的) + f.velocity.multiplyScalar(0.98); + + // 減衰 + f.lifetime -= delta; + if (f.lifetime > 0) alive.push(f); + else f.mesh.parent?.remove(f.mesh); // Group から削除 + }); + + // 爆発完了通知 + if (alive.length === 0 && onComplete) onComplete(); + + return alive; + }); + }); + + return ( + + {fragments.map((f, i) => ( + + ))} + + ); +}; From 8dabb410dd9738f26a847c82b1b9457b6a3f29b5 Mon Sep 17 00:00:00 2001 From: faithia-anastasia <211831874+faithia-anastasia@users.noreply.github.com> Date: Thu, 19 Feb 2026 14:38:39 +0900 Subject: [PATCH 3/5] feat(collision): add temporary collision detection --- src/data/planets.ts | 26 ++++++--- src/pages/Simulation/index.tsx | 101 ++++++++++++++++++++++++++------- src/types/planet.ts | 13 +++-- 3 files changed, 105 insertions(+), 35 deletions(-) diff --git a/src/data/planets.ts b/src/data/planets.ts index c72ea55..c8da761 100644 --- a/src/data/planets.ts +++ b/src/data/planets.ts @@ -2,13 +2,25 @@ import * as THREE from "three"; import type { Planet } from "@/types/planet"; export const earth: Planet = { - texturePath: - "https://raw.githubusercontent.com/mrdoob/three.js/master/examples/textures/planets/earth_atmos_2048.jpg", - rotationSpeedY: 2, - radius: 2, - width: 64, - height: 64, - position: new THREE.Vector3(0, 0, 0), + texturePath: + "https://raw.githubusercontent.com/mrdoob/three.js/master/examples/textures/planets/earth_atmos_2048.jpg", + rotationSpeedY: 2, + radius: 2, + width: 64, + height: 64, + position: new THREE.Vector3(0, 0, 0), + velocity: new THREE.Vector3(0, 0, 0), +}; + +export const test1: Planet = { + texturePath: + "https://raw.githubusercontent.com/mrdoob/three.js/master/examples/textures/planets/earth_atmos_2048.jpg", + rotationSpeedY: 2, + radius: 2, + width: 64, + height: 64, + position: new THREE.Vector3(100, 0, 0), + velocity: new THREE.Vector3(-10, 0, 0), }; // Easy to add more planets later: diff --git a/src/pages/Simulation/index.tsx b/src/pages/Simulation/index.tsx index 912ab62..1f4a302 100644 --- a/src/pages/Simulation/index.tsx +++ b/src/pages/Simulation/index.tsx @@ -1,9 +1,13 @@ import { OrbitControls, Stars, useTexture } from "@react-three/drei"; import { Canvas, useFrame } from "@react-three/fiber"; -import { useRef } from "react"; -import type * as THREE from "three"; -import { earth } from "@/data/planets"; +import { useRef, useState } from "react"; +import * as THREE from "three"; import type { Planet } from "@/types/planet"; +import { earth, test1 } from "@/data/planets"; +import { Explosion } from "@/components/Explosion"; +import { isColliding } from "@/utils/isColliding"; + +const planets: Planet[] = [earth, test1]; interface PlanetMeshProps { planet: Planet; @@ -15,13 +19,15 @@ function PlanetMesh({ planet }: PlanetMeshProps) { // Load the texture (you can use any public Earth texture URL) const [colorMap] = useTexture([planet.texturePath]); - // This hook runs every frame (approx 60fps) - useFrame((_state, delta) => { - if (meshRef.current) { - // Rotate the planet on its Y-axis - meshRef.current.rotation.y += delta * planet.rotationSpeedY; - } - }); + // This hook runs every frame (approx 60fps) + useFrame((_state, delta) => { + if (meshRef.current) { + // Rotate the planet on its Y-axis + meshRef.current.rotation.y += delta * planet.rotationSpeedY; + // 位置を planet.position に同期 + meshRef.current.position.set(planet.position.x, planet.position.y, planet.position.z); + } + }); return ( ); } +interface SimulationProps { + planets: Planet[]; + setExplosions: React.Dispatch>; +} + +export function Simulation({ planets, setExplosions }: SimulationProps) { + // 前フレームの衝突ペアを記録して、連続爆発を防ぐ + const collidedPairsRef = useRef>(new Set()); + + useFrame((_state, delta) => { + // 並進運動 + planets.forEach((planet) => { + if (planet.velocity) { + planet.position.addScaledVector(planet.velocity, delta); + } + }); + + // 衝突判定 + for (let i = 0; i < planets.length; i++) { + for (let j = i + 1; j < planets.length; j++) { + const a = planets[i]; + const b = planets[j]; + const key = `${i}-${j}`; + + if (isColliding(a, b)) { + if (!collidedPairsRef.current.has(key)) { + collidedPairsRef.current.add(key); + + // 衝突したら爆発を追加 + setExplosions((prev) => [...prev, a, b]); + + console.log(`Collision detected between planet ${i} and ${j}`); + } + } else { + // 衝突していない場合は記録を削除 + collidedPairsRef.current.delete(key); + } + } + } + }); + + return null; +} export default function Page() { - return ( - { - gl.setClearColor("#000000", 1); - }} - style={{ width: "100vw", height: "100vh" }} - > - {/* Adds ambient and directional light so we can see the 3D shape */} - - + const [explosions, setExplosions] = useState([]); + + return ( + { + gl.setClearColor("#000000", 1); + }} + style={{ width: "100vw", height: "100vh" }} + > + {/* Adds ambient and directional light so we can see the 3D shape */} + + - + {planets.map((planet, idx) => ( + + ))} + + {explosions.map((exp, idx) => ( + + ))} {/* Optional background and controls */} Date: Fri, 20 Feb 2026 12:07:01 +0900 Subject: [PATCH 4/5] chore: run npm check --- src/components/Explosion.tsx | 178 ++++++++++++++++----------------- src/data/planets.ts | 32 +++--- src/pages/Simulation/index.tsx | 146 ++++++++++++++------------- src/types/planet.ts | 14 +-- src/utils/isColliding.ts | 6 +- 5 files changed, 190 insertions(+), 186 deletions(-) diff --git a/src/components/Explosion.tsx b/src/components/Explosion.tsx index ee02219..8d50ecd 100644 --- a/src/components/Explosion.tsx +++ b/src/components/Explosion.tsx @@ -1,102 +1,102 @@ -import { useRef, useState, useEffect } from "react"; import { useFrame } from "@react-three/fiber"; +import { useEffect, useRef, useState } from "react"; import * as THREE from "three"; import type { Planet } from "@/types/planet"; export type Fragment = { - mesh: THREE.Mesh; - velocity: THREE.Vector3; - rotationAxis: THREE.Vector3; - lifetime: number; + mesh: THREE.Mesh; + velocity: THREE.Vector3; + rotationAxis: THREE.Vector3; + lifetime: number; }; type ExplosionProps = { - planet: Planet; - fragmentCount?: number; - onComplete?: () => void; + planet: Planet; + fragmentCount?: number; + onComplete?: () => void; }; export const Explosion: React.FC = ({ - planet, - fragmentCount = 50, - onComplete, + planet, + fragmentCount = 50, + onComplete, }: ExplosionProps) => { - const groupRef = useRef(null); - const [fragments, setFragments] = useState([]); - - // 爆発初期化 - useEffect(() => { - const newFragments: Fragment[] = []; - for (let i = 0; i < fragmentCount; i++) { - const size = Math.random() * (planet.radius * 0.2) + 0.05; - const geometry = new THREE.SphereGeometry(size, 6, 6); - const material = new THREE.MeshStandardMaterial({ - color: 0xffaa33, - emissive: 0xff5500, - }); - const mesh = new THREE.Mesh(geometry, material); - - // 初期位置は惑星中心 - mesh.position.copy(planet.position); - - // ランダム方向に飛ぶ速度 - const velocity = new THREE.Vector3( - (Math.random() - 0.5) * 4, - (Math.random() - 0.5) * 4, - (Math.random() - 0.5) * 4, - ); - - // 回転軸 - const rotationAxis = new THREE.Vector3( - Math.random(), - Math.random(), - Math.random(), - ).normalize(); - - newFragments.push({ - mesh, - velocity, - rotationAxis, - lifetime: Math.random() * 2 + 1, // 1~3秒で消える - }); - } - setFragments(newFragments); - }, [planet, fragmentCount]); - - // フレームごとの更新 - useFrame((_, delta) => { - if (fragments.length === 0) return; - - setFragments((prev) => { - const alive: Fragment[] = []; - prev.forEach((f) => { - // 位置更新 - f.mesh.position.add(f.velocity.clone().multiplyScalar(delta)); - - // 回転 - f.mesh.rotateOnAxis(f.rotationAxis, delta * 5); - - // 減速(摩擦的) - f.velocity.multiplyScalar(0.98); - - // 減衰 - f.lifetime -= delta; - if (f.lifetime > 0) alive.push(f); - else f.mesh.parent?.remove(f.mesh); // Group から削除 - }); - - // 爆発完了通知 - if (alive.length === 0 && onComplete) onComplete(); - - return alive; - }); - }); - - return ( - - {fragments.map((f, i) => ( - - ))} - - ); + const groupRef = useRef(null); + const [fragments, setFragments] = useState([]); + + // 爆発初期化 + useEffect(() => { + const newFragments: Fragment[] = []; + for (let i = 0; i < fragmentCount; i++) { + const size = Math.random() * (planet.radius * 0.2) + 0.05; + const geometry = new THREE.SphereGeometry(size, 6, 6); + const material = new THREE.MeshStandardMaterial({ + color: 0xffaa33, + emissive: 0xff5500, + }); + const mesh = new THREE.Mesh(geometry, material); + + // 初期位置は惑星中心 + mesh.position.copy(planet.position); + + // ランダム方向に飛ぶ速度 + const velocity = new THREE.Vector3( + (Math.random() - 0.5) * 4, + (Math.random() - 0.5) * 4, + (Math.random() - 0.5) * 4, + ); + + // 回転軸 + const rotationAxis = new THREE.Vector3( + Math.random(), + Math.random(), + Math.random(), + ).normalize(); + + newFragments.push({ + mesh, + velocity, + rotationAxis, + lifetime: Math.random() * 2 + 1, // 1~3秒で消える + }); + } + setFragments(newFragments); + }, [planet, fragmentCount]); + + // フレームごとの更新 + useFrame((_, delta) => { + if (fragments.length === 0) return; + + setFragments((prev) => { + const alive: Fragment[] = []; + prev.forEach((f) => { + // 位置更新 + f.mesh.position.add(f.velocity.clone().multiplyScalar(delta)); + + // 回転 + f.mesh.rotateOnAxis(f.rotationAxis, delta * 5); + + // 減速(摩擦的) + f.velocity.multiplyScalar(0.98); + + // 減衰 + f.lifetime -= delta; + if (f.lifetime > 0) alive.push(f); + else f.mesh.parent?.remove(f.mesh); // Group から削除 + }); + + // 爆発完了通知 + if (alive.length === 0 && onComplete) onComplete(); + + return alive; + }); + }); + + return ( + + {fragments.map((f, i) => ( + + ))} + + ); }; diff --git a/src/data/planets.ts b/src/data/planets.ts index c8da761..55a2d41 100644 --- a/src/data/planets.ts +++ b/src/data/planets.ts @@ -2,25 +2,25 @@ import * as THREE from "three"; import type { Planet } from "@/types/planet"; export const earth: Planet = { - texturePath: - "https://raw.githubusercontent.com/mrdoob/three.js/master/examples/textures/planets/earth_atmos_2048.jpg", - rotationSpeedY: 2, - radius: 2, - width: 64, - height: 64, - position: new THREE.Vector3(0, 0, 0), - velocity: new THREE.Vector3(0, 0, 0), + texturePath: + "https://raw.githubusercontent.com/mrdoob/three.js/master/examples/textures/planets/earth_atmos_2048.jpg", + rotationSpeedY: 2, + radius: 2, + width: 64, + height: 64, + position: new THREE.Vector3(0, 0, 0), + velocity: new THREE.Vector3(0, 0, 0), }; export const test1: Planet = { - texturePath: - "https://raw.githubusercontent.com/mrdoob/three.js/master/examples/textures/planets/earth_atmos_2048.jpg", - rotationSpeedY: 2, - radius: 2, - width: 64, - height: 64, - position: new THREE.Vector3(100, 0, 0), - velocity: new THREE.Vector3(-10, 0, 0), + texturePath: + "https://raw.githubusercontent.com/mrdoob/three.js/master/examples/textures/planets/earth_atmos_2048.jpg", + rotationSpeedY: 2, + radius: 2, + width: 64, + height: 64, + position: new THREE.Vector3(100, 0, 0), + velocity: new THREE.Vector3(-10, 0, 0), }; // Easy to add more planets later: diff --git a/src/pages/Simulation/index.tsx b/src/pages/Simulation/index.tsx index 1f4a302..909087a 100644 --- a/src/pages/Simulation/index.tsx +++ b/src/pages/Simulation/index.tsx @@ -1,10 +1,10 @@ import { OrbitControls, Stars, useTexture } from "@react-three/drei"; import { Canvas, useFrame } from "@react-three/fiber"; import { useRef, useState } from "react"; -import * as THREE from "three"; -import type { Planet } from "@/types/planet"; -import { earth, test1 } from "@/data/planets"; +import type * as THREE from "three"; import { Explosion } from "@/components/Explosion"; +import { earth, test1 } from "@/data/planets"; +import type { Planet } from "@/types/planet"; import { isColliding } from "@/utils/isColliding"; const planets: Planet[] = [earth, test1]; @@ -19,15 +19,19 @@ function PlanetMesh({ planet }: PlanetMeshProps) { // Load the texture (you can use any public Earth texture URL) const [colorMap] = useTexture([planet.texturePath]); - // This hook runs every frame (approx 60fps) - useFrame((_state, delta) => { - if (meshRef.current) { - // Rotate the planet on its Y-axis - meshRef.current.rotation.y += delta * planet.rotationSpeedY; - // 位置を planet.position に同期 - meshRef.current.position.set(planet.position.x, planet.position.y, planet.position.z); - } - }); + // This hook runs every frame (approx 60fps) + useFrame((_state, delta) => { + if (meshRef.current) { + // Rotate the planet on its Y-axis + meshRef.current.rotation.y += delta * planet.rotationSpeedY; + // 位置を planet.position に同期 + meshRef.current.position.set( + planet.position.x, + planet.position.y, + planet.position.z, + ); + } + }); return ( >; + planets: Planet[]; + setExplosions: React.Dispatch>; } export function Simulation({ planets, setExplosions }: SimulationProps) { - // 前フレームの衝突ペアを記録して、連続爆発を防ぐ - const collidedPairsRef = useRef>(new Set()); - - useFrame((_state, delta) => { - // 並進運動 - planets.forEach((planet) => { - if (planet.velocity) { - planet.position.addScaledVector(planet.velocity, delta); - } - }); - - // 衝突判定 - for (let i = 0; i < planets.length; i++) { - for (let j = i + 1; j < planets.length; j++) { - const a = planets[i]; - const b = planets[j]; - const key = `${i}-${j}`; - - if (isColliding(a, b)) { - if (!collidedPairsRef.current.has(key)) { - collidedPairsRef.current.add(key); - - // 衝突したら爆発を追加 - setExplosions((prev) => [...prev, a, b]); - - console.log(`Collision detected between planet ${i} and ${j}`); - } - } else { - // 衝突していない場合は記録を削除 - collidedPairsRef.current.delete(key); - } - } - } - }); - - return null; + // 前フレームの衝突ペアを記録して、連続爆発を防ぐ + const collidedPairsRef = useRef>(new Set()); + + useFrame((_state, delta) => { + // 並進運動 + planets.forEach((planet) => { + if (planet.velocity) { + planet.position.addScaledVector(planet.velocity, delta); + } + }); + + // 衝突判定 + for (let i = 0; i < planets.length; i++) { + for (let j = i + 1; j < planets.length; j++) { + const a = planets[i]; + const b = planets[j]; + const key = `${i}-${j}`; + + if (isColliding(a, b)) { + if (!collidedPairsRef.current.has(key)) { + collidedPairsRef.current.add(key); + + // 衝突したら爆発を追加 + setExplosions((prev) => [...prev, a, b]); + + console.log(`Collision detected between planet ${i} and ${j}`); + } + } else { + // 衝突していない場合は記録を削除 + collidedPairsRef.current.delete(key); + } + } + } + }); + + return null; } export default function Page() { - const [explosions, setExplosions] = useState([]); - - return ( - { - gl.setClearColor("#000000", 1); - }} - style={{ width: "100vw", height: "100vh" }} - > - {/* Adds ambient and directional light so we can see the 3D shape */} - - - - {planets.map((planet, idx) => ( - - ))} - - {explosions.map((exp, idx) => ( - - ))} + const [explosions, setExplosions] = useState([]); + + return ( + { + gl.setClearColor("#000000", 1); + }} + style={{ width: "100vw", height: "100vh" }} + > + {/* Adds ambient and directional light so we can see the 3D shape */} + + + + {planets.map((planet, idx) => ( + + ))} + + {explosions.map((exp, idx) => ( + + ))} {/* Optional background and controls */} Date: Fri, 20 Feb 2026 23:06:55 +0900 Subject: [PATCH 5/5] fix: address some review comments --- src/components/Explosion.tsx | 2 +- src/data/planets.ts | 4 +++- src/pages/Simulation/index.tsx | 18 +++++++++--------- src/types/planet.ts | 1 + 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/components/Explosion.tsx b/src/components/Explosion.tsx index 8d50ecd..01636fd 100644 --- a/src/components/Explosion.tsx +++ b/src/components/Explosion.tsx @@ -3,7 +3,7 @@ import { useEffect, useRef, useState } from "react"; import * as THREE from "three"; import type { Planet } from "@/types/planet"; -export type Fragment = { +type Fragment = { mesh: THREE.Mesh; velocity: THREE.Vector3; rotationAxis: THREE.Vector3; diff --git a/src/data/planets.ts b/src/data/planets.ts index 55a2d41..5093f3b 100644 --- a/src/data/planets.ts +++ b/src/data/planets.ts @@ -2,6 +2,7 @@ import * as THREE from "three"; import type { Planet } from "@/types/planet"; export const earth: Planet = { + name: "Earth", texturePath: "https://raw.githubusercontent.com/mrdoob/three.js/master/examples/textures/planets/earth_atmos_2048.jpg", rotationSpeedY: 2, @@ -12,7 +13,8 @@ export const earth: Planet = { velocity: new THREE.Vector3(0, 0, 0), }; -export const test1: Planet = { +export const testPlanet: Planet = { + name: "TestPlanet", texturePath: "https://raw.githubusercontent.com/mrdoob/three.js/master/examples/textures/planets/earth_atmos_2048.jpg", rotationSpeedY: 2, diff --git a/src/pages/Simulation/index.tsx b/src/pages/Simulation/index.tsx index 909087a..b733116 100644 --- a/src/pages/Simulation/index.tsx +++ b/src/pages/Simulation/index.tsx @@ -3,11 +3,11 @@ import { Canvas, useFrame } from "@react-three/fiber"; import { useRef, useState } from "react"; import type * as THREE from "three"; import { Explosion } from "@/components/Explosion"; -import { earth, test1 } from "@/data/planets"; +import { earth, testPlanet } from "@/data/planets"; import type { Planet } from "@/types/planet"; import { isColliding } from "@/utils/isColliding"; -const planets: Planet[] = [earth, test1]; +const testPlanets: Planet[] = [earth, testPlanet]; interface PlanetMeshProps { planet: Planet; @@ -57,11 +57,11 @@ export function Simulation({ planets, setExplosions }: SimulationProps) { useFrame((_state, delta) => { // 並進運動 - planets.forEach((planet) => { - if (planet.velocity) { - planet.position.addScaledVector(planet.velocity, delta); + for (let i = 0; i < planets.length; i++) { + if (planets[i].velocity) { + planets[i].position.addScaledVector(planets[i].velocity, delta); } - }); + } // 衝突判定 for (let i = 0; i < planets.length; i++) { @@ -105,10 +105,10 @@ export default function Page() { - {planets.map((planet, idx) => ( - + {testPlanets.map((planet) => ( + ))} - + {explosions.map((exp, idx) => ( ))} diff --git a/src/types/planet.ts b/src/types/planet.ts index 0e91449..1946a65 100644 --- a/src/types/planet.ts +++ b/src/types/planet.ts @@ -1,5 +1,6 @@ import type * as THREE from "three"; export type Planet = { + name: string; texturePath: string; rotationSpeedY: number; radius: number;