Newer
Older
InforSystem / simulation / src / App.jsx
import React, { useRef, useState, useEffect } from "react";
import { Canvas, useFrame, useThree } from "@react-three/fiber";
import { TextureLoader } from "three/src/loaders/TextureLoader";
import { useLoader } from "@react-three/fiber";
import * as THREE from "three";

function Sun() {
  const texture = useLoader(TextureLoader, "/texture/sun_texture.jpg");
  const ref = useRef();

  useFrame(() => {
    ref.current.rotation.y += 0.005;
  });

  return (
    <mesh ref={ref} position={[0, 0, 0]}>
      <sphereGeometry args={[1, 32, 32]} />
      <meshStandardMaterial
        map={texture}
        emissive={"#ff6600"}
        emissiveIntensity={1.1}
      />
    </mesh>
  );
}

function Earth({ G, resetTrigger }) {
  const texture = useLoader(TextureLoader, "/texture/earth_texture.jpg");
  const ref = useRef();

  const initialPos = [10, 0, 0];
  const initialVel = [0, 0, 10];

  const pos = useRef([...initialPos]);
  const vel = useRef([...initialVel]);

  const trailPoints = useRef([]);
  const trailRef = useRef();

  useEffect(() => {
    // 位置・速度・軌跡をリセット
    pos.current = [...initialPos];
    vel.current = [...initialVel];
    trailPoints.current = [];

    if (ref.current) {
      ref.current.position.set(...initialPos);
      ref.current.rotation.set(0, 0, 0);
    }
    if (trailRef.current) {
      trailRef.current.geometry.setDrawRange(0, 0);
    }
  }, [G, resetTrigger]);

  const M = 1000;
  const dt = 0.01; // タイムスケールなし固定

  useFrame(() => {
    const rVec = [-pos.current[0], -pos.current[1], -pos.current[2]];
    const dist = Math.sqrt(
      rVec[0] ** 2 + rVec[1] ** 2 + rVec[2] ** 2
    );

    const acc = [
      (G * M * rVec[0]) / dist ** 3,
      (G * M * rVec[1]) / dist ** 3,
      (G * M * rVec[2]) / dist ** 3,
    ];

    vel.current[0] += acc[0] * dt;
    vel.current[1] += acc[1] * dt;
    vel.current[2] += acc[2] * dt;

    pos.current[0] += vel.current[0] * dt;
    pos.current[1] += vel.current[1] * dt;
    pos.current[2] += vel.current[2] * dt;

    if (ref.current) {
      ref.current.position.set(...pos.current);
      ref.current.rotation.y += 0.01;
    }

    trailPoints.current.push(new THREE.Vector3(...pos.current));
    if (trailPoints.current.length > 200) trailPoints.current.shift();

    if (trailRef.current) {
      const positions = new Float32Array(trailPoints.current.length * 3);
      trailPoints.current.forEach((v, i) => {
        positions[i * 3] = v.x;
        positions[i * 3 + 1] = v.y;
        positions[i * 3 + 2] = v.z;
      });
      trailRef.current.geometry.setAttribute(
        "position",
        new THREE.BufferAttribute(positions, 3)
      );
      trailRef.current.geometry.setDrawRange(0, trailPoints.current.length);
      trailRef.current.geometry.attributes.position.needsUpdate = true;
    }
  });

  return (
    <>
      <mesh ref={ref}>
        <sphereGeometry args={[0.3, 32, 32]} />
        <meshStandardMaterial
          map={texture}
          emissive="#555555"
          emissiveIntensity={0.3}
        />
      </mesh>

      <line ref={trailRef}>
        <bufferGeometry />
        <lineBasicMaterial color="lightblue" linewidth={2} />
      </line>
    </>
  );
}

function CameraController() {
  const { camera } = useThree();

  useEffect(() => {
    camera.position.set(18, 8, 8);
    camera.lookAt(0, 0, 0);
  }, [camera]);

  return null;
}

export default function App() {
  const [G, setG] = useState(1.0);
  const [resetKey, setResetKey] = useState(0);

  const handleReset = () => {
    setG(1.0);       // Gを1に戻す
    setResetKey((k) => k + 1);  // リセットトリガー更新
  };

  return (
    <>
      <div
        style={{
          position: "absolute",
          top: 10,
          left: 10,
          zIndex: 1,
          background: "rgba(0,0,0,0.6)",
          color: "white",
          padding: 15,
          borderRadius: 8,
          width: 260,
          fontFamily: "Arial, sans-serif",
          userSelect: "none",
        }}
      >
        <label>
          <strong>重力定数 G:</strong> {G.toFixed(2)}
          <br />
          <input
            type="range"
            min="0.1"
            max="10.0"
            step="0.01"
            value={G}
            onChange={(e) => setG(parseFloat(e.target.value))}
            style={{ width: "100%" }}
          />
        </label>

        <br />
        <br />

        <button
          onClick={handleReset}
          style={{
            width: "100%",
            padding: "8px 0",
            borderRadius: 5,
            border: "none",
            backgroundColor: "#ff6600",
            color: "white",
            fontWeight: "bold",
            cursor: "pointer",
            fontSize: "16px",
          }}
        >
          リセット(G=1に戻す)
        </button>
      </div>

      <Canvas
        camera={{ fov: 50 }}
        style={{ background: "#0b0d17", height: "100vh", width: "100vw" }}
      >
        <ambientLight intensity={1.3} />
        <pointLight
          position={[0, 0, 0]}
          intensity={1.8}
          decay={1}
          distance={0}
          color="#ffaa33"
        />
        <CameraController />
        <Sun />
        <Earth G={G} resetTrigger={resetKey} />
      </Canvas>
    </>
  );
}