Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added RobotExpressive.glb
Binary file not shown.
708 changes: 701 additions & 7 deletions client/package-lock.json

Large diffs are not rendered by default.

15 changes: 9 additions & 6 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,29 @@
"type": "module",
"scripts": {
"lint": "eslint .",
"dev": "eslint . && vite",
"dev": "vite",
"build": "eslint . && vite build",
"preview": "eslint . && vite preview"
},
"dependencies": {
"@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.4.2",
"axios": "^1.13.2",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-router": "^7.9.6",
"axios": "^1.13.2"
"three": "^0.182.0"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@vitejs/plugin-react": "^5.1.1",
"eslint": "^9.39.1",
"brace-expansion": "^1.1.12",
"@eslint/js": "^9.39.1",
"eslint": "^9.39.1",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"globals": "^16.5.0",
"vite": "^7.2.4"
}
Expand Down
Binary file added client/public/models/RobotExpressive.glb
Binary file not shown.
25 changes: 4 additions & 21 deletions client/src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,13 @@
import { BrowserRouter, Routes, Route, Link } from 'react-router'

Check failure on line 1 in client/src/App.jsx

View workflow job for this annotation

GitHub Actions / client / test-npm

'Link' is defined but never used

Check failure on line 1 in client/src/App.jsx

View workflow job for this annotation

GitHub Actions / client / test-npm

'Route' is defined but never used

Check failure on line 1 in client/src/App.jsx

View workflow job for this annotation

GitHub Actions / client / test-npm

'Routes' is defined but never used

Check failure on line 1 in client/src/App.jsx

View workflow job for this annotation

GitHub Actions / client / test-npm

'BrowserRouter' is defined but never used
import Home from './pages/HomePage/HomePage';

Check failure on line 2 in client/src/App.jsx

View workflow job for this annotation

GitHub Actions / client / test-npm

'Home' is defined but never used
import styles from './styles/App.module.css';

Check failure on line 3 in client/src/App.jsx

View workflow job for this annotation

GitHub Actions / client / test-npm

'styles' is defined but never used

import projectLogo from './assets/project-logo.png'

Check failure on line 4 in client/src/App.jsx

View workflow job for this annotation

GitHub Actions / client / test-npm

'projectLogo' is defined but never used
import ThreeDemo from "./ThreeDemo";

function App() {
return (
<BrowserRouter>
<div className={styles.app}>
<header className={styles.appHeader}>
<img src={projectLogo} alt="Logo" className={styles.appLogo} />
<nav className={styles.appNav}>
<Link to="/" className={styles.appLink}>Home</Link>
</nav>
</header>
<main className={styles.main}>
<Routes>
<Route path="/" element={<Home />} />
</Routes>
</main>
<footer className={styles.footer}>
<p>&copy; 2024 My App</p>
</footer>
</div>
</BrowserRouter>
);
return <ThreeDemo />;
}

export default App;


88 changes: 88 additions & 0 deletions client/src/Robot.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { useEffect, useMemo, forwardRef } from "react";
import * as THREE from "three";
import { useFrame } from "@react-three/fiber";
import { useGLTF, useAnimations } from "@react-three/drei";

// מוסיף forwardRef כדי ש-ThreeDemo יוכל לשלוט עליו
const Robot = forwardRef((props, ref) => {

Check failure on line 7 in client/src/Robot.jsx

View workflow job for this annotation

GitHub Actions / client / test-npm

Component definition is missing display name
const gltf = useGLTF("/models/RobotExpressive.glb");
const { scene, animations } = gltf;
const { actions, names } = useAnimations(animations, scene);

// חומרים בסגנון Toon
const toonMaterial = useMemo(
() =>
new THREE.MeshToonMaterial({
color: new THREE.Color("#ffd6ff"), // פסטלי-ורדרד
skinning: true,
}),
[]
);

// החלת חומרים וחישול הצללות
useEffect(() => {
scene.traverse((obj) => {
if (obj.isMesh) {
obj.castShadow = true;
obj.receiveShadow = true;
const skinned = obj.isSkinnedMesh;
obj.material = obj.material.clone();
obj.material.color.multiplyScalar(1.1); // קצת יותר בהיר
if (skinned) obj.material.skinning = true;
if (skinned) obj.material.skinning = true;
}
});
}, [scene, toonMaterial]);

// === שליטה באנימציות (Idle / Walking) ===
useEffect(() => {
if (actions && names.length > 0) {
let currentAction = null;

// פונקציה לשינוי מצב האנימציה
const setAction = (name) => {
const next =
actions[name] ||
actions[
names.find((n) => n.toLowerCase().includes(name.toLowerCase()))
];

if (next && next !== currentAction) {
if (currentAction) currentAction.fadeOut(0.3);
next.reset().fadeIn(0.3).play();
currentAction = next;
}
};

// שומר את הפונקציה ב־userData כדי ש-ThreeDemo תוכל להפעיל אותה
scene.userData.setAction = setAction;

// ברירת מחדל: Idle
setAction("Idle");
}
}, [actions, names, scene]);

// חיוך + מצמוץ עדין (כמו קודם)
useFrame(({ clock }) => {
const t = clock.getElapsedTime();
scene.traverse((obj) => {
if (!obj.morphTargetInfluences || !obj.morphTargetDictionary) return;
const dict = obj.morphTargetDictionary;
const infl = obj.morphTargetInfluences;
const smileKey = Object.keys(dict).find((k) => /smile|happy/i.test(k));
const blinkKey = Object.keys(dict).find((k) => /blink/i.test(k));

if (smileKey) infl[dict[smileKey]] = 0.55;
if (blinkKey) {
const blink = Math.max(0, Math.sin(t * 1.8));
infl[dict[blinkKey]] = blink > 0.98 ? 1 : 0;
}
});
});

// מחזיר את הסצנה
return <primitive ref={ref} object={scene} {...props} />;
});

useGLTF.preload("/models/RobotExpressive.glb");
export default Robot;
150 changes: 150 additions & 0 deletions client/src/ThreeDemo.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/* eslint-disable react/no-unknown-property */
import { Canvas, useFrame } from "@react-three/fiber";
import { OrbitControls } from "@react-three/drei";
import Robot from "./Robot";
import * as THREE from "three";
import { useRef, useEffect } from "react";
import { useKeyboard } from "./useKeyboard";

// תנועת הרובוט
function MovingRobot({ robotRef }) {

Check failure on line 10 in client/src/ThreeDemo.jsx

View workflow job for this annotation

GitHub Actions / client / test-npm

'robotRef' is missing in props validation
const keys = useKeyboard();
const velocity = useRef(new THREE.Vector3());

useFrame((_, delta) => {
if (!robotRef.current) return;

const moveSpeed = 3.5;
const direction = new THREE.Vector3();

if (keys.current.KeyW || keys.current.ArrowUp) direction.z -= 1;
if (keys.current.KeyS || keys.current.ArrowDown) direction.z += 1;
if (keys.current.KeyA || keys.current.ArrowLeft) direction.x -= 1;
if (keys.current.KeyD || keys.current.ArrowRight) direction.x += 1;

direction.normalize();

if (direction.length() > 0) {
if (robotRef.current.userData.setAction) {
robotRef.current.userData.setAction("Walking");
}

velocity.current.copy(direction).multiplyScalar(moveSpeed * delta);
robotRef.current.position.add(velocity.current);

const angle = Math.atan2(direction.x, direction.z);
robotRef.current.rotation.y = angle;
} else {
if (robotRef.current.userData.setAction) {
robotRef.current.userData.setAction("Idle");
}
}
});

return <Robot ref={robotRef} scale={1} position={[0, -1.15, 0]} />;
}

// מצלמה שעוקבת אחרי הרובוט
function FollowCamera({ targetRef }) {
useFrame(({ camera }) => {
if (!targetRef.current) return;

const targetPos = targetRef.current.position;

// מיקום יחסי של המצלמה מאחורי הרובוט
const offset = new THREE.Vector3(0, 3, 8);
const desiredPos = targetPos.clone().add(offset);

// תנועה רכה למיקום הרצוי
camera.position.lerp(desiredPos, 0.05);
camera.lookAt(targetPos.x, targetPos.y + 1.5, targetPos.z);
});

return null;
}

export default function ThreeDemo() {
const robotRef = useRef();

// ביטול גלילת חיצים
useEffect(() => {
const preventArrowScroll = (e) => {
if (
["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.code)
) {
e.preventDefault();
}
};
window.addEventListener("keydown", preventArrowScroll);
return () => window.removeEventListener("keydown", preventArrowScroll);
}, []);

return (
<div style={{ height: "100vh", width: "100%" }}>
<Canvas
camera={{ position: [0, 2.6, 14], fov: 60, near: 0.1, far: 400 }}
shadows
gl={{ antialias: true }}
onCreated={({ scene }) => {
scene.background = new THREE.Color("#ffffff");
scene.fog = new THREE.Fog("#e0eefe", 10, 90);
}}
>
{/* קירות */}
<mesh position={[0, 5, 0]}>
<boxGeometry args={[100, 40, 100]} />
<meshStandardMaterial
color="#ffffff"
roughness={0.95}
metalness={0}
side={THREE.BackSide}
/>
</mesh>

{/* תאורה */}
<hemisphereLight
intensity={1.1}
skyColor="#ffffff"
groundColor="#E4EBF7"
/>
<directionalLight
position={[6, 10, 8]}
intensity={0.55}
color="#ffffff"
castShadow
/>
<directionalLight
position={[-8, 6, -6]}
intensity={0.35}
color="#ffffff"
/>

{/* רצפה */}
<mesh
rotation={[-Math.PI / 2, 0, 0]}
position={[0, -1.15, 0]}
receiveShadow
>
<planeGeometry args={[150, 150]} />
<meshStandardMaterial
color="#9db5e3"
roughness={0.9}
metalness={0}
/>
</mesh>

{/* הרובוט + מצלמה עוקבת */}
<MovingRobot robotRef={robotRef} />
<FollowCamera targetRef={robotRef} />

{/* ביטול שליטה עם העכבר */}
<OrbitControls
enablePan={false}
enableZoom={false}
enableRotate={false} // ❌ זה העיקרי: מבטל סיבוב עם העכבר
enableKeys={false}
/>
</Canvas>
</div>
);
}
32 changes: 32 additions & 0 deletions client/src/useKeyboard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useEffect, useRef } from "react";

export function useKeyboard() {
const keys = useRef({
ArrowUp: false,
ArrowDown: false,
ArrowLeft: false,
ArrowRight: false,
KeyW: false,
KeyS: false,
KeyA: false,
KeyD: false,
});

useEffect(() => {
const down = (e) => {
if (e.code in keys.current) keys.current[e.code] = true;
};
const up = (e) => {
if (e.code in keys.current) keys.current[e.code] = false;
};

window.addEventListener("keydown", down);
window.addEventListener("keyup", up);
return () => {
window.removeEventListener("keydown", down);
window.removeEventListener("keyup", up);
};
}, []);

return keys;
}
Loading