add: week3 project setup with previous tasks included
This commit is contained in:
parent
2f2d5e6573
commit
2724f4e475
5
Week-3/Task-1/ProductLens.md
Normal file
5
Week-3/Task-1/ProductLens.md
Normal file
@ -0,0 +1,5 @@
|
||||
## Product Lens
|
||||
- Is this pattern useful for real customers? Yes / Partial / No
|
||||
- What kind of customer use case does this support?
|
||||
- Does Thob feel strong enough for this use case?
|
||||
- What would improve the experience?
|
||||
60
Week-3/Task-1/TaskNotes.md
Normal file
60
Week-3/Task-1/TaskNotes.md
Normal file
@ -0,0 +1,60 @@
|
||||
# Task: [Feature Name]
|
||||
|
||||
## Objective
|
||||
What is the feature trying to do?
|
||||
|
||||
## Vanilla three.js
|
||||
-Possible: Yes / Partial / No
|
||||
-Notes:
|
||||
-Key concepts:
|
||||
-Complexity: Easy / Medium / Hard
|
||||
|
||||
## R3F
|
||||
-Possible: Yes / Partial / No
|
||||
-Notes:
|
||||
-What R3F abstracted:
|
||||
-Complexity: Easy / Medium / Hard
|
||||
|
||||
## Thob Page Builder
|
||||
-Possible: Yes / Partial / No
|
||||
-Notes:
|
||||
-Builder steps:
|
||||
-Complexity: Easy / Medium / Hard
|
||||
|
||||
## Comparison Summary
|
||||
-Possible in all 3? Yes / Partial / No
|
||||
-Main differences:
|
||||
-Where Thob is better:
|
||||
-Where Thob is weaker:
|
||||
-What feels awkward or unclear:
|
||||
|
||||
## Limitation Type (if any)
|
||||
-[ ] Editor UX limitation
|
||||
-[ ] Runtime limitation
|
||||
-[ ] Schema / data model limitation
|
||||
-[ ] Component limitation
|
||||
-[ ] Event system limitation
|
||||
-[ ] Asset pipeline limitation
|
||||
-[ ] Unknown / needs investigation
|
||||
|
||||
## Workaround
|
||||
-Is there a workaround?
|
||||
-If yes, what is it?
|
||||
|
||||
## Suggested Improvement
|
||||
-What should improve in Thob?
|
||||
-Is it:
|
||||
-editor
|
||||
-runtime
|
||||
-component
|
||||
-UX
|
||||
-schema/data
|
||||
|
||||
## Difficulty Estimate
|
||||
-Easy / Medium / Hard
|
||||
|
||||
## Business Value
|
||||
-Low / Medium / High
|
||||
|
||||
## Recommendation
|
||||
Should Thob support this better? Why?
|
||||
0
Week-3/Task-1/builder/BuilderNotes.md
Normal file
0
Week-3/Task-1/builder/BuilderNotes.md
Normal file
24
Week-3/Task-1/r3f/Cammera/.gitignore
vendored
Normal file
24
Week-3/Task-1/r3f/Cammera/.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
16
Week-3/Task-1/r3f/Cammera/README.md
Normal file
16
Week-3/Task-1/r3f/Cammera/README.md
Normal file
@ -0,0 +1,16 @@
|
||||
# React + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
|
||||
|
||||
## React Compiler
|
||||
|
||||
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
|
||||
29
Week-3/Task-1/r3f/Cammera/eslint.config.js
Normal file
29
Week-3/Task-1/r3f/Cammera/eslint.config.js
Normal file
@ -0,0 +1,29 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{js,jsx}'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
reactHooks.configs.flat.recommended,
|
||||
reactRefresh.configs.vite,
|
||||
],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
ecmaFeatures: { jsx: true },
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
|
||||
},
|
||||
},
|
||||
])
|
||||
13
Week-3/Task-1/r3f/Cammera/index.html
Normal file
13
Week-3/Task-1/r3f/Cammera/index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Task1 R3f</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
30
Week-3/Task-1/r3f/Cammera/package.json
Normal file
30
Week-3/Task-1/r3f/Cammera/package.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "cammera",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"packageManager": "yarn@1.22.22",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-three/fiber": "^9.5.0",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"three": "^0.183.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.4",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.0",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.2",
|
||||
"globals": "^17.4.0",
|
||||
"vite": "^8.0.0"
|
||||
}
|
||||
}
|
||||
95
Week-3/Task-1/r3f/Cammera/src/App.jsx
Normal file
95
Week-3/Task-1/r3f/Cammera/src/App.jsx
Normal file
@ -0,0 +1,95 @@
|
||||
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { Canvas, useFrame, useThree } from '@react-three/fiber'
|
||||
import * as THREE from 'three'
|
||||
|
||||
const PRESETS = {
|
||||
front: [0, 1.2, 6.5],
|
||||
side: [6.5, 1.4, 0],
|
||||
topAngled: [3.8, 5.4, 4.2],
|
||||
}
|
||||
|
||||
function CameraController({ preset }) {
|
||||
const { camera } = useThree()
|
||||
const target = useMemo(() => new THREE.Vector3(0, 0, 0), [])
|
||||
const targetPosition = useMemo(() => new THREE.Vector3(), [])
|
||||
|
||||
useEffect(() => {
|
||||
const [x, y, z] = PRESETS[preset]
|
||||
targetPosition.set(x, y, z)
|
||||
}, [preset, targetPosition])
|
||||
|
||||
useFrame(() => {
|
||||
camera.position.lerp(targetPosition, 0.08)
|
||||
camera.lookAt(target)
|
||||
})
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function Scene({ preset }) {
|
||||
return (
|
||||
<>
|
||||
<ambientLight intensity={0.85} />
|
||||
<directionalLight intensity={1} position={[3, 4, 5]} />
|
||||
|
||||
<mesh>
|
||||
<boxGeometry args={[1.6, 1.6, 1.6]} />
|
||||
<meshStandardMaterial color="#2a9d8f" roughness={0.45} metalness={0.1} />
|
||||
</mesh>
|
||||
|
||||
<CameraController preset={preset} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [preset, setPreset] = useState('front')
|
||||
|
||||
useEffect(() => {
|
||||
function onKeyDown(event) {
|
||||
if (event.key === '1') setPreset('front')
|
||||
if (event.key === '2') setPreset('side')
|
||||
if (event.key === '3') setPreset('topAngled')
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', onKeyDown)
|
||||
return () => window.removeEventListener('keydown', onKeyDown)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="app-shell">
|
||||
<div id="preset-panel">
|
||||
<h2>Camera Presets</h2>
|
||||
<div className="preset-buttons">
|
||||
<button
|
||||
className={`preset-btn ${preset === 'front' ? 'active' : ''}`}
|
||||
onClick={() => setPreset('front')}
|
||||
>
|
||||
Front (1)
|
||||
</button>
|
||||
<button
|
||||
className={`preset-btn ${preset === 'side' ? 'active' : ''}`}
|
||||
onClick={() => setPreset('side')}
|
||||
>
|
||||
Side (2)
|
||||
</button>
|
||||
<button
|
||||
className={`preset-btn ${preset === 'topAngled' ? 'active' : ''}`}
|
||||
onClick={() => setPreset('topAngled')}
|
||||
>
|
||||
Top Angled (3)
|
||||
</button>
|
||||
</div>
|
||||
<p id="hint">Press 1, 2, or 3 to switch view.</p>
|
||||
</div>
|
||||
|
||||
<Canvas camera={{ fov: 55, near: 0.1, far: 100, position: PRESETS.front }}>
|
||||
<color attach="background" args={["#f3f5f8"]} />
|
||||
<Scene preset={preset} />
|
||||
</Canvas>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
81
Week-3/Task-1/r3f/Cammera/src/index.css
Normal file
81
Week-3/Task-1/r3f/Cammera/src/index.css
Normal file
@ -0,0 +1,81 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow: hidden;
|
||||
font-family: "Avenir Next", "Segoe UI", sans-serif;
|
||||
background: #f3f5f8;
|
||||
}
|
||||
|
||||
.app-shell {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#preset-panel {
|
||||
position: fixed;
|
||||
top: 16px;
|
||||
left: 16px;
|
||||
z-index: 10;
|
||||
background: rgba(255, 255, 255, 0.82);
|
||||
border: 1px solid rgba(0, 0, 0, 0.12);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
|
||||
#preset-panel h2 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.03em;
|
||||
color: #1b2530;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.preset-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.preset-btn {
|
||||
border: 1px solid #cfd8e3;
|
||||
background: #ffffff;
|
||||
color: #203448;
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.preset-btn:hover {
|
||||
border-color: #8ea2b8;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.preset-btn.active {
|
||||
background: #203448;
|
||||
border-color: #203448;
|
||||
color: #f4f8fb;
|
||||
}
|
||||
|
||||
#hint {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: #4f647a;
|
||||
}
|
||||
10
Week-3/Task-1/r3f/Cammera/src/main.jsx
Normal file
10
Week-3/Task-1/r3f/Cammera/src/main.jsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.jsx'
|
||||
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
7
Week-3/Task-1/r3f/Cammera/vite.config.js
Normal file
7
Week-3/Task-1/r3f/Cammera/vite.config.js
Normal file
@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||
1292
Week-3/Task-1/r3f/Cammera/yarn.lock
Normal file
1292
Week-3/Task-1/r3f/Cammera/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
12
Week-3/Task-1/r3f/package.json
Normal file
12
Week-3/Task-1/r3f/package.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "week-1-task-1-r3f",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"packageManager": "yarn@1.22.22",
|
||||
"scripts": {
|
||||
"dev": "echo 'Add your React Three Fiber dev script here'",
|
||||
"build": "echo 'Add your React Three Fiber build script here'",
|
||||
"lint": "echo 'Add your lint script here'",
|
||||
"clean": "rm -rf dist build .next"
|
||||
}
|
||||
}
|
||||
87
Week-3/Task-1/vanilla/index.html
Normal file
87
Week-3/Task-1/vanilla/index.html
Normal file
@ -0,0 +1,87 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Task1 vanilla </title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
background: #f3f5f8;
|
||||
overflow: hidden;
|
||||
font-family: "Avenir Next", "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
#preset-panel {
|
||||
position: fixed;
|
||||
top: 16px;
|
||||
left: 16px;
|
||||
z-index: 10;
|
||||
background: rgba(255, 255, 255, 0.82);
|
||||
border: 1px solid rgba(0, 0, 0, 0.12);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
|
||||
#preset-panel h2 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.03em;
|
||||
color: #1b2530;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.preset-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.preset-btn {
|
||||
border: 1px solid #cfd8e3;
|
||||
background: #ffffff;
|
||||
color: #203448;
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.preset-btn:hover {
|
||||
border-color: #8ea2b8;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.preset-btn.active {
|
||||
background: #203448;
|
||||
border-color: #203448;
|
||||
color: #f4f8fb;
|
||||
}
|
||||
|
||||
#hint {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: #4f647a;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="preset-panel">
|
||||
<h2>Camera Presets</h2>
|
||||
<div class="preset-buttons">
|
||||
<button class="preset-btn" data-preset="front">Front (1)</button>
|
||||
<button class="preset-btn" data-preset="side">Side (2)</button>
|
||||
<button class="preset-btn" data-preset="topAngled">Top Angled (3)</button>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="/main.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
99
Week-3/Task-1/vanilla/main.js
Normal file
99
Week-3/Task-1/vanilla/main.js
Normal file
@ -0,0 +1,99 @@
|
||||
import * as THREE from "three";
|
||||
|
||||
const scene = new THREE.Scene();
|
||||
scene.background = new THREE.Color(0xf3f5f8);
|
||||
|
||||
const camera = new THREE.PerspectiveCamera(
|
||||
55,
|
||||
window.innerWidth / window.innerHeight,
|
||||
0.1,
|
||||
100
|
||||
);
|
||||
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
document.body.appendChild(renderer.domElement);
|
||||
|
||||
const ambientLight = new THREE.AmbientLight(0xffffff, 0.85);
|
||||
scene.add(ambientLight);
|
||||
|
||||
const keyLight = new THREE.DirectionalLight(0xffffff, 1);
|
||||
keyLight.position.set(3, 4, 5);
|
||||
scene.add(keyLight);
|
||||
|
||||
const cube = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(1.6, 1.6, 1.6),
|
||||
new THREE.MeshStandardMaterial({ color: 0x2a9d8f, roughness: 0.45, metalness: 0.1 })
|
||||
);
|
||||
scene.add(cube);
|
||||
|
||||
const target = new THREE.Vector3(0, 0, 0);
|
||||
|
||||
const cameraPresets = {
|
||||
front: new THREE.Vector3(0, 1.2, 6.5),
|
||||
side: new THREE.Vector3(6.5, 1.4, 0),
|
||||
topAngled: new THREE.Vector3(3.8, 5.4, 4.2),
|
||||
};
|
||||
|
||||
let activePreset = "front";
|
||||
let desiredPosition = cameraPresets.front.clone();
|
||||
camera.position.copy(desiredPosition);
|
||||
camera.lookAt(target);
|
||||
|
||||
const presetButtons = Array.from(document.querySelectorAll(".preset-btn"));
|
||||
|
||||
function setActiveButton(presetName) {
|
||||
presetButtons.forEach((button) => {
|
||||
const isActive = button.dataset.preset === presetName;
|
||||
button.classList.toggle("active", isActive);
|
||||
});
|
||||
}
|
||||
|
||||
function switchCameraPreset(presetName) {
|
||||
const nextPreset = cameraPresets[presetName];
|
||||
if (!nextPreset) {
|
||||
return;
|
||||
}
|
||||
|
||||
activePreset = presetName;
|
||||
desiredPosition = nextPreset.clone();
|
||||
setActiveButton(activePreset);
|
||||
}
|
||||
|
||||
presetButtons.forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
const presetName = button.dataset.preset;
|
||||
switchCameraPreset(presetName);
|
||||
});
|
||||
});
|
||||
|
||||
window.addEventListener("keydown", (event) => {
|
||||
if (event.key === "1") {
|
||||
switchCameraPreset("front");
|
||||
}
|
||||
if (event.key === "2") {
|
||||
switchCameraPreset("side");
|
||||
}
|
||||
if (event.key === "3") {
|
||||
switchCameraPreset("topAngled");
|
||||
}
|
||||
});
|
||||
|
||||
setActiveButton(activePreset);
|
||||
|
||||
function animate() {
|
||||
|
||||
camera.position.lerp(desiredPosition, 0.08);
|
||||
camera.lookAt(target);
|
||||
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
|
||||
renderer.setAnimationLoop(animate);
|
||||
|
||||
window.addEventListener("resize", () => {
|
||||
camera.aspect = window.innerWidth / window.innerHeight;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
});
|
||||
18
Week-3/Task-1/vanilla/package.json
Normal file
18
Week-3/Task-1/vanilla/package.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "week-1-task-1-vanilla",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"packageManager": "yarn@1.22.22",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "echo 'Add your lint script here'",
|
||||
"clean": "rm -rf dist build"
|
||||
},
|
||||
"dependencies": {
|
||||
"three": "^0.183.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^8.0.2"
|
||||
}
|
||||
}
|
||||
5
Week-3/Task-2/ProductLens.md
Normal file
5
Week-3/Task-2/ProductLens.md
Normal file
@ -0,0 +1,5 @@
|
||||
## Product Lens
|
||||
- Is this pattern useful for real customers? Yes / Partial / No
|
||||
- What kind of customer use case does this support?
|
||||
- Does Thob feel strong enough for this use case?
|
||||
- What would improve the experience?
|
||||
60
Week-3/Task-2/TaskNotes.md
Normal file
60
Week-3/Task-2/TaskNotes.md
Normal file
@ -0,0 +1,60 @@
|
||||
# Task: [Feature Name]
|
||||
|
||||
## Objective
|
||||
What is the feature trying to do?
|
||||
|
||||
## Vanilla three.js
|
||||
-Possible: Yes / Partial / No
|
||||
-Notes:
|
||||
-Key concepts:
|
||||
-Complexity: Easy / Medium / Hard
|
||||
|
||||
## R3F
|
||||
-Possible: Yes / Partial / No
|
||||
-Notes:
|
||||
-What R3F abstracted:
|
||||
-Complexity: Easy / Medium / Hard
|
||||
|
||||
## Thob Page Builder
|
||||
-Possible: Yes / Partial / No
|
||||
-Notes:
|
||||
-Builder steps:
|
||||
-Complexity: Easy / Medium / Hard
|
||||
|
||||
## Comparison Summary
|
||||
-Possible in all 3? Yes / Partial / No
|
||||
-Main differences:
|
||||
-Where Thob is better:
|
||||
-Where Thob is weaker:
|
||||
-What feels awkward or unclear:
|
||||
|
||||
## Limitation Type (if any)
|
||||
-[ ] Editor UX limitation
|
||||
-[ ] Runtime limitation
|
||||
-[ ] Schema / data model limitation
|
||||
-[ ] Component limitation
|
||||
-[ ] Event system limitation
|
||||
-[ ] Asset pipeline limitation
|
||||
-[ ] Unknown / needs investigation
|
||||
|
||||
## Workaround
|
||||
-Is there a workaround?
|
||||
-If yes, what is it?
|
||||
|
||||
## Suggested Improvement
|
||||
-What should improve in Thob?
|
||||
-Is it:
|
||||
-editor
|
||||
-runtime
|
||||
-component
|
||||
-UX
|
||||
-schema/data
|
||||
|
||||
## Difficulty Estimate
|
||||
-Easy / Medium / Hard
|
||||
|
||||
## Business Value
|
||||
-Low / Medium / High
|
||||
|
||||
## Recommendation
|
||||
Should Thob support this better? Why?
|
||||
77
Week-3/Task-2/builder/BuilderNotes.md
Normal file
77
Week-3/Task-2/builder/BuilderNotes.md
Normal file
@ -0,0 +1,77 @@
|
||||
# Builder Notes (Thob) — Task 2: Scene State Switching
|
||||
|
||||
## Thob Observations from Task Notes
|
||||
|
||||
- **Possible:** Partial
|
||||
- **Implementation used:** Combinations of object visibility states and Perspective Camera positions, with button-based switching attempts.
|
||||
- **What worked as expected:**
|
||||
- Visibility states can be configured for multiple objects.
|
||||
- Camera position presets can be prepared for different scene views.
|
||||
- Base scene composition is quick to assemble in the editor.
|
||||
- **Main limitation observed:**
|
||||
- One button updating multiple targets (visibility + transforms + camera) is not consistently reliable.
|
||||
- Updates should happen simultaneously (or fast enough to feel instant), but current behavior can feel staggered/inconsistent.
|
||||
- Button binding flow remains hard to reason about for multi-prop state orchestration.
|
||||
- **Builder flow used:**
|
||||
1. Create scene objects and set initial visibility/transform states.
|
||||
2. Add Perspective Camera and set target positions per scene state.
|
||||
3. Add state buttons in UI.
|
||||
4. Attempt to bind each button to all required prop updates.
|
||||
5. Validate whether transitions are synchronized and repeatable.
|
||||
- **Complexity:** Hard
|
||||
- **Main limitation signals:** Editor UX + Event system + Runtime stability concerns.
|
||||
- **Workaround status:** Partial workaround only (manual or simplified transitions; full one-click synchronized switching is not dependable yet).
|
||||
|
||||
## Console Warnings/Errors Seen (Deduplicated) and Probable Meaning
|
||||
|
||||
### warn: `Found both blacklist and siteRules — using siteRules`
|
||||
- **Type:** Configuration precedence warning.
|
||||
- **Probable meaning:** Multiple rule sets are available and runtime is choosing one path (`siteRules`).
|
||||
- **Impact:** Usually low, but indicates overlapping configuration paths.
|
||||
|
||||
### warn: `... changing from uncontrolled to controlled` and `RadioGroup is changing from uncontrolled to controlled`
|
||||
- **Type:** React state-management warning.
|
||||
- **Probable meaning:** UI controls start with unstable values and later switch to controlled mode.
|
||||
- **Impact:** Property panel/control behavior can become inconsistent during binding setup.
|
||||
|
||||
### warn: `Permissions-Policy header: Unrecognized feature: 'browsing-topics'`
|
||||
- **Type:** Browser/header compatibility warning.
|
||||
- **Probable meaning:** Response headers include unsupported policy directives for current browser.
|
||||
- **Impact:** Low direct impact on scene logic; mostly environment noise.
|
||||
|
||||
### warn: `Unchecked runtime.lastError: The message port closed before a response was received`
|
||||
- **Type:** Browser runtime/extension messaging warning.
|
||||
- **Probable meaning:** A message channel closed before callback completion.
|
||||
- **Impact:** Usually non-fatal, but adds noise and can complicate debugging.
|
||||
|
||||
### warn: `GetBindingData<id> method already registered` (repeated)
|
||||
- **Type:** Duplicate registration warning.
|
||||
- **Probable meaning:** Binding handlers are being registered repeatedly across rerenders/remounts.
|
||||
- **Impact:** High relevance for this task; can cause duplicate triggers and unreliable button-driven state updates.
|
||||
|
||||
### warn: `update-static-component-prop method already registered`
|
||||
- **Type:** Duplicate update pipeline warning.
|
||||
- **Probable meaning:** Static prop update handler is attached more than once.
|
||||
- **Impact:** Can produce repeated writes and non-atomic multi-prop transitions.
|
||||
|
||||
### warn: `resetPOI method already registered`
|
||||
- **Type:** Duplicate command registration warning.
|
||||
- **Probable meaning:** Camera/POI reset command is mounted multiple times.
|
||||
- **Impact:** Camera behavior may drift or feel inconsistent during state switching.
|
||||
|
||||
### error: `Failed to load resource: 404`
|
||||
- **Type:** Network/resource error.
|
||||
- **Probable meaning:** Missing/stale project asset or endpoint.
|
||||
- **Impact:** Can partially break expected editor/preview behavior.
|
||||
|
||||
### error: `THREE.WebGLRenderer: Context Lost`
|
||||
- **Type:** Graphics runtime error.
|
||||
- **Probable meaning:** WebGL context dropped due to resource pressure, remount loops, or browser/GPU reset.
|
||||
- **Impact:** Preview instability can invalidate transition testing.
|
||||
|
||||
## Overall Read
|
||||
|
||||
- Task 2 concept is achievable in thob at a basic level: visibility and camera state combinations can be authored.
|
||||
- The key product gap is synchronized execution: one button should apply all linked updates simultaneously, but current binding behavior is not reliably atomic.
|
||||
- Recurring duplicate-registration warnings strongly match the interaction issues seen during multi-prop button setup.
|
||||
- Improving binding lifecycle stability and one-click bundled updates should be the highest priority for this task pattern.
|
||||
12
Week-3/Task-2/r3f/package.json
Normal file
12
Week-3/Task-2/r3f/package.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "week-1-task-2-r3f",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"packageManager": "yarn@1.22.22",
|
||||
"scripts": {
|
||||
"dev": "echo 'Add your React Three Fiber dev script here'",
|
||||
"build": "echo 'Add your React Three Fiber build script here'",
|
||||
"lint": "echo 'Add your lint script here'",
|
||||
"clean": "rm -rf dist build .next"
|
||||
}
|
||||
}
|
||||
24
Week-3/Task-2/r3f/sceene/.gitignore
vendored
Normal file
24
Week-3/Task-2/r3f/sceene/.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
16
Week-3/Task-2/r3f/sceene/README.md
Normal file
16
Week-3/Task-2/r3f/sceene/README.md
Normal file
@ -0,0 +1,16 @@
|
||||
# React + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
|
||||
|
||||
## React Compiler
|
||||
|
||||
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
|
||||
29
Week-3/Task-2/r3f/sceene/eslint.config.js
Normal file
29
Week-3/Task-2/r3f/sceene/eslint.config.js
Normal file
@ -0,0 +1,29 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{js,jsx}'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
reactHooks.configs.flat.recommended,
|
||||
reactRefresh.configs.vite,
|
||||
],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
ecmaFeatures: { jsx: true },
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
|
||||
},
|
||||
},
|
||||
])
|
||||
13
Week-3/Task-2/r3f/sceene/index.html
Normal file
13
Week-3/Task-2/r3f/sceene/index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Task2 R3f</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
2815
Week-3/Task-2/r3f/sceene/package-lock.json
generated
Normal file
2815
Week-3/Task-2/r3f/sceene/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
29
Week-3/Task-2/r3f/sceene/package.json
Normal file
29
Week-3/Task-2/r3f/sceene/package.json
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "sceene",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-three/fiber": "^9.5.0",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"three": "^0.183.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.4",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.0",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.2",
|
||||
"globals": "^17.4.0",
|
||||
"vite": "^8.0.0"
|
||||
}
|
||||
}
|
||||
148
Week-3/Task-2/r3f/sceene/src/App.jsx
Normal file
148
Week-3/Task-2/r3f/sceene/src/App.jsx
Normal file
@ -0,0 +1,148 @@
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Canvas, useFrame } from '@react-three/fiber'
|
||||
import * as THREE from 'three'
|
||||
|
||||
const SCENE_STATES = {
|
||||
state1: {
|
||||
center: { position: [0, 0, 0], rotation: [0, 0, 0], visible: true },
|
||||
left: { position: [0, 0, 0], rotation: [0, 0, 0], visible: false },
|
||||
right: { position: [0, 0, 0], rotation: [0, 0, 0], visible: false },
|
||||
},
|
||||
state2: {
|
||||
center: { position: [0, 0, 0], rotation: [0.25, 0.4, 0], visible: true },
|
||||
left: { position: [-2.1, 0, 0.2], rotation: [0, 0.2, 0], visible: true },
|
||||
right: { position: [2.1, 0, -0.2], rotation: [0, -0.2, 0], visible: true },
|
||||
},
|
||||
state3: {
|
||||
center: { position: [0, 1.3, 0], rotation: [0.6, 0.9, 0], visible: true },
|
||||
left: { position: [-1.2, -1, 1.2], rotation: [0.3, 0.3, 0.1], visible: true },
|
||||
right: { position: [0, 0, 0], rotation: [0, 0, 0], visible: false },
|
||||
},
|
||||
}
|
||||
|
||||
function SceneObjects({ stateKey }) {
|
||||
const centerRef = useRef()
|
||||
const leftRef = useRef()
|
||||
const rightRef = useRef()
|
||||
|
||||
const targets = useMemo(
|
||||
() => ({
|
||||
center: { position: new THREE.Vector3(), rotation: new THREE.Euler() },
|
||||
left: { position: new THREE.Vector3(), rotation: new THREE.Euler() },
|
||||
right: { position: new THREE.Vector3(), rotation: new THREE.Euler() },
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const state = SCENE_STATES[stateKey]
|
||||
const refs = {
|
||||
center: centerRef.current,
|
||||
left: leftRef.current,
|
||||
right: rightRef.current,
|
||||
}
|
||||
|
||||
Object.keys(refs).forEach((name) => {
|
||||
const object = refs[name]
|
||||
if (!object) return
|
||||
|
||||
const config = state[name]
|
||||
targets[name].position.set(...config.position)
|
||||
targets[name].rotation.set(...config.rotation)
|
||||
object.visible = config.visible
|
||||
})
|
||||
}, [stateKey, targets])
|
||||
|
||||
useFrame(() => {
|
||||
const refs = {
|
||||
center: centerRef.current,
|
||||
left: leftRef.current,
|
||||
right: rightRef.current,
|
||||
}
|
||||
|
||||
Object.keys(refs).forEach((name) => {
|
||||
const object = refs[name]
|
||||
if (!object) return
|
||||
|
||||
const target = targets[name]
|
||||
object.position.lerp(target.position, 0.1)
|
||||
object.rotation.x += (target.rotation.x - object.rotation.x) * 0.1
|
||||
object.rotation.y += (target.rotation.y - object.rotation.y) * 0.1
|
||||
object.rotation.z += (target.rotation.z - object.rotation.z) * 0.1
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<ambientLight intensity={0.9} />
|
||||
<directionalLight intensity={1} position={[3, 4, 5]} />
|
||||
|
||||
<mesh ref={centerRef}>
|
||||
<boxGeometry args={[1.2, 1.2, 1.2]} />
|
||||
<meshStandardMaterial color="#2a9d8f" />
|
||||
</mesh>
|
||||
|
||||
<mesh ref={leftRef}>
|
||||
<boxGeometry args={[0.9, 0.9, 0.9]} />
|
||||
<meshStandardMaterial color="#e76f51" />
|
||||
</mesh>
|
||||
|
||||
<mesh ref={rightRef}>
|
||||
<boxGeometry args={[0.9, 0.9, 0.9]} />
|
||||
<meshStandardMaterial color="#457b9d" />
|
||||
</mesh>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [stateKey, setStateKey] = useState('state1')
|
||||
|
||||
useEffect(() => {
|
||||
function onKeyDown(event) {
|
||||
if (event.key === '1') setStateKey('state1')
|
||||
if (event.key === '2') setStateKey('state2')
|
||||
if (event.key === '3') setStateKey('state3')
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', onKeyDown)
|
||||
return () => window.removeEventListener('keydown', onKeyDown)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="app-shell">
|
||||
<div id="state-panel">
|
||||
<h2>Scene States</h2>
|
||||
<div className="state-buttons">
|
||||
<button
|
||||
className={`state-btn ${stateKey === 'state1' ? 'active' : ''}`}
|
||||
onClick={() => setStateKey('state1')}
|
||||
>
|
||||
State 1 (1)
|
||||
</button>
|
||||
<button
|
||||
className={`state-btn ${stateKey === 'state2' ? 'active' : ''}`}
|
||||
onClick={() => setStateKey('state2')}
|
||||
>
|
||||
State 2 (2)
|
||||
</button>
|
||||
<button
|
||||
className={`state-btn ${stateKey === 'state3' ? 'active' : ''}`}
|
||||
onClick={() => setStateKey('state3')}
|
||||
>
|
||||
State 3 (3)
|
||||
</button>
|
||||
</div>
|
||||
<p id="hint">Press 1, 2, or 3 to switch state.</p>
|
||||
</div>
|
||||
|
||||
<Canvas camera={{ fov: 55, near: 0.1, far: 100, position: [0, 2, 7] }}>
|
||||
<color attach="background" args={["#f3f5f8"]} />
|
||||
<SceneObjects stateKey={stateKey} />
|
||||
</Canvas>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
81
Week-3/Task-2/r3f/sceene/src/index.css
Normal file
81
Week-3/Task-2/r3f/sceene/src/index.css
Normal file
@ -0,0 +1,81 @@
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow: hidden;
|
||||
font-family: "Avenir Next", "Segoe UI", sans-serif;
|
||||
background: #f3f5f8;
|
||||
}
|
||||
|
||||
canvas {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.app-shell {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#state-panel {
|
||||
position: fixed;
|
||||
top: 16px;
|
||||
left: 16px;
|
||||
z-index: 10;
|
||||
background: rgba(255, 255, 255, 0.82);
|
||||
border: 1px solid rgba(0, 0, 0, 0.12);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
|
||||
#state-panel h2 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.03em;
|
||||
color: #1b2530;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.state-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.state-btn {
|
||||
border: 1px solid #cfd8e3;
|
||||
background: #ffffff;
|
||||
color: #203448;
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.state-btn:hover {
|
||||
border-color: #8ea2b8;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.state-btn.active {
|
||||
background: #203448;
|
||||
border-color: #203448;
|
||||
color: #f4f8fb;
|
||||
}
|
||||
|
||||
#hint {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: #4f647a;
|
||||
}
|
||||
10
Week-3/Task-2/r3f/sceene/src/main.jsx
Normal file
10
Week-3/Task-2/r3f/sceene/src/main.jsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.jsx'
|
||||
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
7
Week-3/Task-2/r3f/sceene/vite.config.js
Normal file
7
Week-3/Task-2/r3f/sceene/vite.config.js
Normal file
@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||
97
Week-3/Task-2/vanilla/index.html
Normal file
97
Week-3/Task-2/vanilla/index.html
Normal file
@ -0,0 +1,97 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Task2 vanilla</title>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
background: #f3f5f8;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
font-family: "Avenir Next", "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#state-panel {
|
||||
position: fixed;
|
||||
top: 16px;
|
||||
left: 16px;
|
||||
z-index: 10;
|
||||
background: rgba(255, 255, 255, 0.82);
|
||||
border: 1px solid rgba(0, 0, 0, 0.12);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
|
||||
#state-panel h2 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.03em;
|
||||
color: #1b2530;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.state-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.state-btn {
|
||||
border: 1px solid #cfd8e3;
|
||||
background: #ffffff;
|
||||
color: #203448;
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.state-btn:hover {
|
||||
border-color: #8ea2b8;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.state-btn.active {
|
||||
background: #203448;
|
||||
border-color: #203448;
|
||||
color: #f4f8fb;
|
||||
}
|
||||
|
||||
#hint {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: #4f647a;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="state-panel">
|
||||
<h2>Scene States</h2>
|
||||
<div class="state-buttons">
|
||||
<button class="state-btn" data-state="state1">State 1 (1)</button>
|
||||
<button class="state-btn" data-state="state2">State 2 (2)</button>
|
||||
<button class="state-btn" data-state="state3">State 3 (3)</button>
|
||||
</div>
|
||||
<p id="hint">Press 1, 2, or 3 to switch state.</p>
|
||||
</div>
|
||||
<script type="module" src="/main.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
130
Week-3/Task-2/vanilla/main.js
Normal file
130
Week-3/Task-2/vanilla/main.js
Normal file
@ -0,0 +1,130 @@
|
||||
import * as THREE from 'three'
|
||||
|
||||
const scene = new THREE.Scene()
|
||||
scene.background = new THREE.Color(0xf3f5f8)
|
||||
|
||||
const camera = new THREE.PerspectiveCamera(
|
||||
55,
|
||||
window.innerWidth / window.innerHeight,
|
||||
0.1,
|
||||
100
|
||||
)
|
||||
camera.position.set(0, 2, 7)
|
||||
camera.lookAt(0, 0, 0)
|
||||
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: true })
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
|
||||
renderer.setSize(window.innerWidth, window.innerHeight)
|
||||
document.body.appendChild(renderer.domElement)
|
||||
|
||||
scene.add(new THREE.AmbientLight(0xffffff, 0.9))
|
||||
|
||||
const keyLight = new THREE.DirectionalLight(0xffffff, 1)
|
||||
keyLight.position.set(3, 4, 5)
|
||||
scene.add(keyLight)
|
||||
|
||||
const objects = {
|
||||
center: new THREE.Mesh(
|
||||
new THREE.BoxGeometry(1.2, 1.2, 1.2),
|
||||
new THREE.MeshStandardMaterial({ color: 0x2a9d8f })
|
||||
),
|
||||
left: new THREE.Mesh(
|
||||
new THREE.BoxGeometry(0.9, 0.9, 0.9),
|
||||
new THREE.MeshStandardMaterial({ color: 0xe76f51 })
|
||||
),
|
||||
right: new THREE.Mesh(
|
||||
new THREE.BoxGeometry(0.9, 0.9, 0.9),
|
||||
new THREE.MeshStandardMaterial({ color: 0x457b9d })
|
||||
),
|
||||
}
|
||||
|
||||
scene.add(objects.center, objects.left, objects.right)
|
||||
|
||||
const states = {
|
||||
state1: {
|
||||
center: { position: [0, 0, 0], rotation: [0, 0, 0], visible: true },
|
||||
left: { position: [0, 0, 0], rotation: [0, 0, 0], visible: false },
|
||||
right: { position: [0, 0, 0], rotation: [0, 0, 0], visible: false },
|
||||
},
|
||||
state2: {
|
||||
center: { position: [0, 0, 0], rotation: [0.25, 0.4, 0], visible: true },
|
||||
left: { position: [-2.1, 0, 0.2], rotation: [0, 0.2, 0], visible: true },
|
||||
right: { position: [2.1, 0, -0.2], rotation: [0, -0.2, 0], visible: true },
|
||||
},
|
||||
state3: {
|
||||
center: { position: [0, 1.3, 0], rotation: [0.6, 0.9, 0], visible: true },
|
||||
left: { position: [-1.2, -1, 1.2], rotation: [0.3, 0.3, 0.1], visible: true },
|
||||
right: { position: [0, 0, 0], rotation: [0, 0, 0], visible: false },
|
||||
},
|
||||
}
|
||||
|
||||
const targets = {
|
||||
center: { position: new THREE.Vector3(), rotation: new THREE.Euler() },
|
||||
left: { position: new THREE.Vector3(), rotation: new THREE.Euler() },
|
||||
right: { position: new THREE.Vector3(), rotation: new THREE.Euler() },
|
||||
}
|
||||
|
||||
const buttons = Array.from(document.querySelectorAll('.state-btn'))
|
||||
let activeState = 'state1'
|
||||
|
||||
function updateButtonState(stateName) {
|
||||
buttons.forEach((button) => {
|
||||
button.classList.toggle('active', button.dataset.state === stateName)
|
||||
})
|
||||
}
|
||||
|
||||
function applyState(stateName) {
|
||||
const nextState = states[stateName]
|
||||
if (!nextState) {
|
||||
return
|
||||
}
|
||||
|
||||
activeState = stateName
|
||||
|
||||
Object.keys(objects).forEach((name) => {
|
||||
const object = objects[name]
|
||||
const config = nextState[name]
|
||||
|
||||
targets[name].position.set(...config.position)
|
||||
targets[name].rotation.set(...config.rotation)
|
||||
object.visible = config.visible
|
||||
})
|
||||
|
||||
updateButtonState(stateName)
|
||||
}
|
||||
|
||||
buttons.forEach((button) => {
|
||||
button.addEventListener('click', () => {
|
||||
applyState(button.dataset.state)
|
||||
})
|
||||
})
|
||||
|
||||
window.addEventListener('keydown', (event) => {
|
||||
if (event.key === '1') applyState('state1')
|
||||
if (event.key === '2') applyState('state2')
|
||||
if (event.key === '3') applyState('state3')
|
||||
})
|
||||
|
||||
applyState(activeState)
|
||||
|
||||
function animate() {
|
||||
Object.keys(objects).forEach((name) => {
|
||||
const object = objects[name]
|
||||
const target = targets[name]
|
||||
|
||||
object.position.lerp(target.position, 0.1)
|
||||
object.rotation.x += (target.rotation.x - object.rotation.x) * 0.1
|
||||
object.rotation.y += (target.rotation.y - object.rotation.y) * 0.1
|
||||
object.rotation.z += (target.rotation.z - object.rotation.z) * 0.1
|
||||
})
|
||||
|
||||
renderer.render(scene, camera)
|
||||
}
|
||||
|
||||
renderer.setAnimationLoop(animate)
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
camera.aspect = window.innerWidth / window.innerHeight
|
||||
camera.updateProjectionMatrix()
|
||||
renderer.setSize(window.innerWidth, window.innerHeight)
|
||||
})
|
||||
18
Week-3/Task-2/vanilla/package.json
Normal file
18
Week-3/Task-2/vanilla/package.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "week-1-task-2-vanilla",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"packageManager": "yarn@1.22.22",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "echo 'Add your lint script here'",
|
||||
"clean": "rm -rf dist build"
|
||||
},
|
||||
"dependencies": {
|
||||
"three": "^0.183.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^8.0.3"
|
||||
}
|
||||
}
|
||||
5
Week-3/Task-3/ProductLens.md
Normal file
5
Week-3/Task-3/ProductLens.md
Normal file
@ -0,0 +1,5 @@
|
||||
## Product Lens
|
||||
- Is this pattern useful for real customers? Yes / Partial / No
|
||||
- What kind of customer use case does this support?
|
||||
- Does Thob feel strong enough for this use case?
|
||||
- What would improve the experience?
|
||||
60
Week-3/Task-3/TaskNotes.md
Normal file
60
Week-3/Task-3/TaskNotes.md
Normal file
@ -0,0 +1,60 @@
|
||||
# Task: [Feature Name]
|
||||
|
||||
## Objective
|
||||
What is the feature trying to do?
|
||||
|
||||
## Vanilla three.js
|
||||
-Possible: Yes / Partial / No
|
||||
-Notes:
|
||||
-Key concepts:
|
||||
-Complexity: Easy / Medium / Hard
|
||||
|
||||
## R3F
|
||||
-Possible: Yes / Partial / No
|
||||
-Notes:
|
||||
-What R3F abstracted:
|
||||
-Complexity: Easy / Medium / Hard
|
||||
|
||||
## Thob Page Builder
|
||||
-Possible: Yes / Partial / No
|
||||
-Notes:
|
||||
-Builder steps:
|
||||
-Complexity: Easy / Medium / Hard
|
||||
|
||||
## Comparison Summary
|
||||
-Possible in all 3? Yes / Partial / No
|
||||
-Main differences:
|
||||
-Where Thob is better:
|
||||
-Where Thob is weaker:
|
||||
-What feels awkward or unclear:
|
||||
|
||||
## Limitation Type (if any)
|
||||
-[ ] Editor UX limitation
|
||||
-[ ] Runtime limitation
|
||||
-[ ] Schema / data model limitation
|
||||
-[ ] Component limitation
|
||||
-[ ] Event system limitation
|
||||
-[ ] Asset pipeline limitation
|
||||
-[ ] Unknown / needs investigation
|
||||
|
||||
## Workaround
|
||||
-Is there a workaround?
|
||||
-If yes, what is it?
|
||||
|
||||
## Suggested Improvement
|
||||
-What should improve in Thob?
|
||||
-Is it:
|
||||
-editor
|
||||
-runtime
|
||||
-component
|
||||
-UX
|
||||
-schema/data
|
||||
|
||||
## Difficulty Estimate
|
||||
-Easy / Medium / Hard
|
||||
|
||||
## Business Value
|
||||
-Low / Medium / High
|
||||
|
||||
## Recommendation
|
||||
Should Thob support this better? Why?
|
||||
77
Week-3/Task-3/builder/BuilderNotes.md
Normal file
77
Week-3/Task-3/builder/BuilderNotes.md
Normal file
@ -0,0 +1,77 @@
|
||||
# Builder Notes (Thob) — Task 3: Step-Guided Camera and Object State Flow
|
||||
|
||||
## Thob Observations from Task Notes
|
||||
|
||||
- **Possible:** Partial
|
||||
- **Implementation used:** Perspective Camera with Make Default toggle, object visibility toggles, and attempted button bindings for step transitions.
|
||||
- **What worked as expected:**
|
||||
- Camera can be set as default quickly.
|
||||
- Object visibility/state can be adjusted manually.
|
||||
- Basic static scene states can be assembled in the editor.
|
||||
- **Main limitation observed:**
|
||||
- One button driving multiple prop updates (camera + objects) is difficult to configure reliably.
|
||||
- Button-to-prop mapping is not intuitive for multi-step behavior.
|
||||
- Could not complete a fully reliable end-to-end guided step interaction purely in current builder flow.
|
||||
- **Builder flow used:**
|
||||
1. Add Perspective Camera and toggle Make Default.
|
||||
2. Add scene objects and configure visibility/transform states.
|
||||
3. Create step buttons.
|
||||
4. Attempt to bind each button to camera and object prop changes.
|
||||
5. Validate step transitions and check repeatability.
|
||||
- **Complexity:** Hard
|
||||
- **Main limitation signals:** Editor UX + Event system + Runtime stability concerns.
|
||||
- **Workaround status:** Partial workaround only (manual/static states are possible; robust step orchestration via button bindings remains difficult).
|
||||
|
||||
## Console Warnings/Errors Seen (Deduplicated) and Probable Meaning
|
||||
|
||||
### warn: `Found both blacklist and siteRules — using siteRules`
|
||||
- **Type:** Configuration precedence warning.
|
||||
- **Probable meaning:** Two policy/rule sources are present and runtime is choosing `siteRules`.
|
||||
- **Impact:** Usually non-blocking, but indicates overlapping config paths.
|
||||
|
||||
### warn: `undefined is changing from uncontrolled to controlled` and `RadioGroup is changing from uncontrolled to controlled`
|
||||
- **Type:** React state-management warning.
|
||||
- **Probable meaning:** Controls mount with unstable/default values, then switch to controlled values.
|
||||
- **Impact:** Can cause inspector/control jitter and unreliable interaction setup.
|
||||
|
||||
### warn: `Permissions-Policy header: Unrecognized feature: 'browsing-topics'`
|
||||
- **Type:** Browser/header compatibility warning.
|
||||
- **Probable meaning:** Response header contains a policy directive unsupported by the current browser.
|
||||
- **Impact:** Low direct impact on scene logic; mostly environmental noise.
|
||||
|
||||
### warn: `Unchecked runtime.lastError: The message port closed before a response was received`
|
||||
- **Type:** Browser runtime/extension messaging warning.
|
||||
- **Probable meaning:** A message channel closed before callback completion.
|
||||
- **Impact:** Typically low for core feature behavior; adds noise during debugging.
|
||||
|
||||
### warn: `GetBindingData<id> method already registered` (repeated)
|
||||
- **Type:** Duplicate registration warning.
|
||||
- **Probable meaning:** Binding handlers are re-registered on rerenders/remounts/reconnects without cleanup.
|
||||
- **Impact:** High for this task because step-driven interactions depend on stable bindings; duplicates can trigger repeated or inconsistent updates.
|
||||
|
||||
### warn: `update-static-component-prop method already registered`
|
||||
- **Type:** Duplicate update pipeline warning.
|
||||
- **Probable meaning:** Static prop update method is attached multiple times.
|
||||
- **Impact:** Can cause repeated writes and make multi-prop step transitions unreliable.
|
||||
|
||||
### warn: `resetPOI method already registered`
|
||||
- **Type:** Duplicate command registration warning.
|
||||
- **Probable meaning:** Camera/POI reset command handler is mounted repeatedly.
|
||||
- **Impact:** Camera state may jump/drift, reducing confidence in guided camera steps.
|
||||
|
||||
### error: `Failed to load resource: the server responded with a status of 404 ()`
|
||||
- **Type:** Network/resource error.
|
||||
- **Probable meaning:** A required project/resource endpoint is missing or stale.
|
||||
- **Impact:** Missing resources can break expected editor behavior and complicate reproduction.
|
||||
|
||||
### error: `THREE.WebGLRenderer: Context Lost.`
|
||||
- **Type:** Graphics runtime error.
|
||||
- **Probable meaning:** WebGL context dropped due to resource pressure, remount loops, or browser/GPU reset.
|
||||
- **Impact:** Preview instability/blackouts can invalidate interactive step-flow testing.
|
||||
|
||||
## Overall Read
|
||||
|
||||
- Task 3 behavior is partially achievable in thob: static setup and manual state toggles work, but robust multi-step interaction wiring is still difficult.
|
||||
- The repeated registration warnings (`GetBindingData`, `update-static-component-prop`, `resetPOI`) strongly align with the core issue: unstable button-driven multi-prop updates.
|
||||
- Controlled/uncontrolled warnings indicate editor form-state instability, which likely contributes to confusion during binding setup.
|
||||
- Runtime instability signals (`404`, `Context Lost`) reduce trust during validation and should be addressed alongside interaction UX improvements.
|
||||
15
Week-3/Task-3/r3f/index.html
Normal file
15
Week-3/Task-3/r3f/index.html
Normal file
@ -0,0 +1,15 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Task 3 R3F</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
24
Week-3/Task-3/r3f/package.json
Normal file
24
Week-3/Task-3/r3f/package.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "week-1-task-3-r3f",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"packageManager": "yarn@1.22.22",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "echo 'Lint not configured yet'",
|
||||
"preview": "vite preview",
|
||||
"clean": "rm -rf dist build .next"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-three/fiber": "^9.5.0",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"three": "^0.183.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react": "^6.0.0",
|
||||
"vite": "^8.0.3"
|
||||
}
|
||||
}
|
||||
177
Week-3/Task-3/r3f/src/App.jsx
Normal file
177
Week-3/Task-3/r3f/src/App.jsx
Normal file
@ -0,0 +1,177 @@
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Canvas, useFrame, useThree } from '@react-three/fiber'
|
||||
import * as THREE from 'three'
|
||||
|
||||
const STEPS = [
|
||||
{
|
||||
label: 'Step 1: Focus on object A',
|
||||
detail: 'Camera frames object A only.',
|
||||
cameraPosition: [0, 1.4, 5],
|
||||
lookAt: [-1.2, 0, 0],
|
||||
a: { position: [-1.2, 0, 0], rotation: [0, 0.25, 0], visible: true },
|
||||
b: { position: [1.4, 0, 0], rotation: [0, 0, 0], visible: false },
|
||||
},
|
||||
{
|
||||
label: 'Step 2: Move camera + reveal object B',
|
||||
detail: 'Camera shifts right and object B becomes visible.',
|
||||
cameraPosition: [2.2, 1.4, 5],
|
||||
lookAt: [1.4, 0, 0],
|
||||
a: { position: [-1.2, 0, 0], rotation: [0.2, 0.8, 0], visible: true },
|
||||
b: { position: [1.4, 0, 0], rotation: [0, 0.2, 0], visible: true },
|
||||
},
|
||||
{
|
||||
label: 'Step 3: Final combined scene',
|
||||
detail: 'Both objects stay visible in one balanced view.',
|
||||
cameraPosition: [0.3, 2.2, 7],
|
||||
lookAt: [0, 0, 0],
|
||||
a: { position: [-1.5, 0, 0], rotation: [0.3, 1.1, 0], visible: true },
|
||||
b: { position: [1.5, 0, 0], rotation: [0.1, 0.6, 0], visible: true },
|
||||
},
|
||||
]
|
||||
|
||||
function GuidedScene({ stepIndex }) {
|
||||
const { camera } = useThree()
|
||||
const objectARef = useRef()
|
||||
const objectBRef = useRef()
|
||||
const initializedRef = useRef(false)
|
||||
|
||||
const targets = useMemo(
|
||||
() => ({
|
||||
cameraPosition: new THREE.Vector3(),
|
||||
cameraLookAt: new THREE.Vector3(),
|
||||
lookCurrent: new THREE.Vector3(),
|
||||
aPosition: new THREE.Vector3(),
|
||||
aRotation: new THREE.Euler(),
|
||||
bPosition: new THREE.Vector3(),
|
||||
bRotation: new THREE.Euler(),
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const step = STEPS[stepIndex]
|
||||
|
||||
targets.cameraPosition.set(...step.cameraPosition)
|
||||
targets.cameraLookAt.set(...step.lookAt)
|
||||
|
||||
targets.aPosition.set(...step.a.position)
|
||||
targets.aRotation.set(...step.a.rotation)
|
||||
|
||||
targets.bPosition.set(...step.b.position)
|
||||
targets.bRotation.set(...step.b.rotation)
|
||||
|
||||
if (objectARef.current) objectARef.current.visible = step.a.visible
|
||||
if (objectBRef.current) objectBRef.current.visible = step.b.visible
|
||||
|
||||
if (!initializedRef.current && objectARef.current && objectBRef.current) {
|
||||
camera.position.copy(targets.cameraPosition)
|
||||
targets.lookCurrent.copy(targets.cameraLookAt)
|
||||
objectARef.current.position.copy(targets.aPosition)
|
||||
objectARef.current.rotation.copy(targets.aRotation)
|
||||
objectBRef.current.position.copy(targets.bPosition)
|
||||
objectBRef.current.rotation.copy(targets.bRotation)
|
||||
initializedRef.current = true
|
||||
}
|
||||
}, [camera, stepIndex, targets])
|
||||
|
||||
useFrame(() => {
|
||||
if (!objectARef.current || !objectBRef.current) return
|
||||
|
||||
objectARef.current.position.lerp(targets.aPosition, 0.1)
|
||||
objectBRef.current.position.lerp(targets.bPosition, 0.1)
|
||||
|
||||
objectARef.current.rotation.x += (targets.aRotation.x - objectARef.current.rotation.x) * 0.1
|
||||
objectARef.current.rotation.y += (targets.aRotation.y - objectARef.current.rotation.y) * 0.1
|
||||
objectARef.current.rotation.z += (targets.aRotation.z - objectARef.current.rotation.z) * 0.1
|
||||
|
||||
objectBRef.current.rotation.x += (targets.bRotation.x - objectBRef.current.rotation.x) * 0.1
|
||||
objectBRef.current.rotation.y += (targets.bRotation.y - objectBRef.current.rotation.y) * 0.1
|
||||
objectBRef.current.rotation.z += (targets.bRotation.z - objectBRef.current.rotation.z) * 0.1
|
||||
|
||||
camera.position.lerp(targets.cameraPosition, 0.08)
|
||||
targets.lookCurrent.lerp(targets.cameraLookAt, 0.08)
|
||||
camera.lookAt(targets.lookCurrent)
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<ambientLight intensity={0.8} />
|
||||
<directionalLight intensity={1} position={[3, 4, 5]} />
|
||||
|
||||
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, -0.8, 0]}>
|
||||
<planeGeometry args={[12, 12]} />
|
||||
<meshStandardMaterial color="#dbe6f3" roughness={0.95} />
|
||||
</mesh>
|
||||
|
||||
<mesh ref={objectARef}>
|
||||
<boxGeometry args={[1.2, 1.2, 1.2]} />
|
||||
<meshStandardMaterial color="#2a9d8f" />
|
||||
</mesh>
|
||||
|
||||
<mesh ref={objectBRef}>
|
||||
<sphereGeometry args={[0.7, 32, 32]} />
|
||||
<meshStandardMaterial color="#e76f51" />
|
||||
</mesh>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const [stepIndex, setStepIndex] = useState(0)
|
||||
const step = STEPS[stepIndex]
|
||||
|
||||
useEffect(() => {
|
||||
function onKeyDown(event) {
|
||||
if (event.key === '1') setStepIndex(0)
|
||||
if (event.key === '2') setStepIndex(1)
|
||||
if (event.key === '3') setStepIndex(2)
|
||||
if (event.key === 'ArrowLeft') setStepIndex((value) => Math.max(value - 1, 0))
|
||||
if (event.key === 'ArrowRight') setStepIndex((value) => Math.min(value + 1, STEPS.length - 1))
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', onKeyDown)
|
||||
return () => window.removeEventListener('keydown', onKeyDown)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="app-shell">
|
||||
<div id="guide">
|
||||
<h2>Step Guided Flow</h2>
|
||||
<p id="step-label">{step.label}</p>
|
||||
<p id="step-detail">{step.detail}</p>
|
||||
|
||||
<div className="row">
|
||||
{STEPS.map((_, index) => (
|
||||
<button
|
||||
key={index}
|
||||
className={`step-btn ${stepIndex === index ? 'active' : ''}`}
|
||||
onClick={() => setStepIndex(index)}
|
||||
>
|
||||
Step {index + 1}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="row">
|
||||
<button onClick={() => setStepIndex((value) => Math.max(value - 1, 0))} disabled={stepIndex === 0}>
|
||||
Previous
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setStepIndex((value) => Math.min(value + 1, STEPS.length - 1))}
|
||||
disabled={stepIndex === STEPS.length - 1}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p id="hint">Use buttons or keys 1, 2, 3.</p>
|
||||
</div>
|
||||
|
||||
<Canvas camera={{ fov: 55, near: 0.1, far: 100, position: [0, 1.4, 5] }}>
|
||||
<color attach="background" args={['#eaf0f7']} />
|
||||
<GuidedScene stepIndex={stepIndex} />
|
||||
</Canvas>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
101
Week-3/Task-3/r3f/src/index.css
Normal file
101
Week-3/Task-3/r3f/src/index.css
Normal file
@ -0,0 +1,101 @@
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow: hidden;
|
||||
background: #eaf0f7;
|
||||
font-family: "Avenir Next", "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.app-shell {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#guide {
|
||||
position: fixed;
|
||||
top: 14px;
|
||||
left: 14px;
|
||||
z-index: 10;
|
||||
max-width: 290px;
|
||||
background: rgba(255, 255, 255, 0.88);
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
#guide h2 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: #203448;
|
||||
}
|
||||
|
||||
#step-label {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #1b2530;
|
||||
}
|
||||
|
||||
#step-detail {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: #4f647a;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
button {
|
||||
border: 1px solid #cfd8e3;
|
||||
background: #fff;
|
||||
color: #203448;
|
||||
border-radius: 8px;
|
||||
padding: 7px 9px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
border-color: #8ea2b8;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
button.step-btn.active {
|
||||
background: #203448;
|
||||
border-color: #203448;
|
||||
color: #f4f8fb;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
#hint {
|
||||
margin: 0;
|
||||
font-size: 11px;
|
||||
color: #566f86;
|
||||
}
|
||||
10
Week-3/Task-3/r3f/src/main.jsx
Normal file
10
Week-3/Task-3/r3f/src/main.jsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import App from './App.jsx'
|
||||
import './index.css'
|
||||
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
6
Week-3/Task-3/r3f/vite.config.js
Normal file
6
Week-3/Task-3/r3f/vite.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||
125
Week-3/Task-3/vanilla/index.html
Normal file
125
Week-3/Task-3/vanilla/index.html
Normal file
@ -0,0 +1,125 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Task 3 Vanilla</title>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: #eaf0f7;
|
||||
font-family: "Avenir Next", "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#guide {
|
||||
position: fixed;
|
||||
top: 14px;
|
||||
left: 14px;
|
||||
z-index: 10;
|
||||
max-width: 290px;
|
||||
background: rgba(255, 255, 255, 0.88);
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
#guide h2 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: #203448;
|
||||
}
|
||||
|
||||
#step-label {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #1b2530;
|
||||
}
|
||||
|
||||
#step-detail {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: #4f647a;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
button {
|
||||
border: 1px solid #cfd8e3;
|
||||
background: #fff;
|
||||
color: #203448;
|
||||
border-radius: 8px;
|
||||
padding: 7px 9px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
border-color: #8ea2b8;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
button.active {
|
||||
background: #203448;
|
||||
border-color: #203448;
|
||||
color: #f4f8fb;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
#hint {
|
||||
margin: 0;
|
||||
font-size: 11px;
|
||||
color: #566f86;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="guide">
|
||||
<h2>Step Guided Flow</h2>
|
||||
<p id="step-label"></p>
|
||||
<p id="step-detail"></p>
|
||||
|
||||
<div class="row">
|
||||
<button class="step-btn" data-step="0">Step 1</button>
|
||||
<button class="step-btn" data-step="1">Step 2</button>
|
||||
<button class="step-btn" data-step="2">Step 3</button>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<button id="prev-btn">Previous</button>
|
||||
<button id="next-btn">Next</button>
|
||||
</div>
|
||||
|
||||
<p id="hint">Use buttons or keys 1, 2, 3.</p>
|
||||
</div>
|
||||
<script type="module" src="/main.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
163
Week-3/Task-3/vanilla/main.js
Normal file
163
Week-3/Task-3/vanilla/main.js
Normal file
@ -0,0 +1,163 @@
|
||||
import * as THREE from 'three'
|
||||
|
||||
const scene = new THREE.Scene()
|
||||
scene.background = new THREE.Color(0xeaf0f7)
|
||||
|
||||
const camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 0.1, 100)
|
||||
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: true })
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
|
||||
renderer.setSize(window.innerWidth, window.innerHeight)
|
||||
document.body.appendChild(renderer.domElement)
|
||||
|
||||
scene.add(new THREE.AmbientLight(0xffffff, 0.8))
|
||||
|
||||
const dirLight = new THREE.DirectionalLight(0xffffff, 1)
|
||||
dirLight.position.set(3, 4, 5)
|
||||
scene.add(dirLight)
|
||||
|
||||
const floor = new THREE.Mesh(
|
||||
new THREE.PlaneGeometry(12, 12),
|
||||
new THREE.MeshStandardMaterial({ color: 0xdbe6f3, roughness: 0.95 })
|
||||
)
|
||||
floor.rotation.x = -Math.PI / 2
|
||||
floor.position.y = -0.8
|
||||
scene.add(floor)
|
||||
|
||||
const objectA = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(1.2, 1.2, 1.2),
|
||||
new THREE.MeshStandardMaterial({ color: 0x2a9d8f })
|
||||
)
|
||||
|
||||
const objectB = new THREE.Mesh(
|
||||
new THREE.SphereGeometry(0.7, 32, 32),
|
||||
new THREE.MeshStandardMaterial({ color: 0xe76f51 })
|
||||
)
|
||||
|
||||
scene.add(objectA, objectB)
|
||||
|
||||
const STEPS = [
|
||||
{
|
||||
label: 'Step 1: Focus on object A',
|
||||
detail: 'Camera frames object A only.',
|
||||
cameraPosition: [0, 1.4, 5],
|
||||
lookAt: [-1.2, 0, 0],
|
||||
a: { position: [-1.2, 0, 0], rotation: [0, 0.25, 0], visible: true },
|
||||
b: { position: [1.4, 0, 0], rotation: [0, 0, 0], visible: false },
|
||||
},
|
||||
{
|
||||
label: 'Step 2: Move camera + reveal object B',
|
||||
detail: 'Camera shifts right and object B becomes visible.',
|
||||
cameraPosition: [2.2, 1.4, 5],
|
||||
lookAt: [1.4, 0, 0],
|
||||
a: { position: [-1.2, 0, 0], rotation: [0.2, 0.8, 0], visible: true },
|
||||
b: { position: [1.4, 0, 0], rotation: [0, 0.2, 0], visible: true },
|
||||
},
|
||||
{
|
||||
label: 'Step 3: Final combined scene',
|
||||
detail: 'Both objects stay visible in one balanced view.',
|
||||
cameraPosition: [0.3, 2.2, 7],
|
||||
lookAt: [0, 0, 0],
|
||||
a: { position: [-1.5, 0, 0], rotation: [0.3, 1.1, 0], visible: true },
|
||||
b: { position: [1.5, 0, 0], rotation: [0.1, 0.6, 0], visible: true },
|
||||
},
|
||||
]
|
||||
|
||||
const targets = {
|
||||
cameraPosition: new THREE.Vector3(),
|
||||
cameraLookAt: new THREE.Vector3(),
|
||||
aPosition: new THREE.Vector3(),
|
||||
aRotation: new THREE.Euler(),
|
||||
bPosition: new THREE.Vector3(),
|
||||
bRotation: new THREE.Euler(),
|
||||
}
|
||||
|
||||
let currentStep = 0
|
||||
let initialized = false
|
||||
const lookCurrent = new THREE.Vector3()
|
||||
|
||||
const stepLabel = document.getElementById('step-label')
|
||||
const stepDetail = document.getElementById('step-detail')
|
||||
const stepButtons = Array.from(document.querySelectorAll('.step-btn'))
|
||||
const prevButton = document.getElementById('prev-btn')
|
||||
const nextButton = document.getElementById('next-btn')
|
||||
|
||||
function applyStep(index) {
|
||||
currentStep = THREE.MathUtils.clamp(index, 0, STEPS.length - 1)
|
||||
const step = STEPS[currentStep]
|
||||
|
||||
targets.cameraPosition.set(...step.cameraPosition)
|
||||
targets.cameraLookAt.set(...step.lookAt)
|
||||
|
||||
targets.aPosition.set(...step.a.position)
|
||||
targets.aRotation.set(...step.a.rotation)
|
||||
objectA.visible = step.a.visible
|
||||
|
||||
targets.bPosition.set(...step.b.position)
|
||||
targets.bRotation.set(...step.b.rotation)
|
||||
objectB.visible = step.b.visible
|
||||
|
||||
if (!initialized) {
|
||||
camera.position.copy(targets.cameraPosition)
|
||||
lookCurrent.copy(targets.cameraLookAt)
|
||||
objectA.position.copy(targets.aPosition)
|
||||
objectA.rotation.copy(targets.aRotation)
|
||||
objectB.position.copy(targets.bPosition)
|
||||
objectB.rotation.copy(targets.bRotation)
|
||||
initialized = true
|
||||
}
|
||||
|
||||
stepLabel.textContent = step.label
|
||||
stepDetail.textContent = step.detail
|
||||
|
||||
stepButtons.forEach((button, buttonIndex) => {
|
||||
button.classList.toggle('active', buttonIndex === currentStep)
|
||||
})
|
||||
|
||||
prevButton.disabled = currentStep === 0
|
||||
nextButton.disabled = currentStep === STEPS.length - 1
|
||||
}
|
||||
|
||||
stepButtons.forEach((button) => {
|
||||
button.addEventListener('click', () => applyStep(Number(button.dataset.step)))
|
||||
})
|
||||
|
||||
prevButton.addEventListener('click', () => applyStep(currentStep - 1))
|
||||
nextButton.addEventListener('click', () => applyStep(currentStep + 1))
|
||||
|
||||
window.addEventListener('keydown', (event) => {
|
||||
if (event.key === '1') applyStep(0)
|
||||
if (event.key === '2') applyStep(1)
|
||||
if (event.key === '3') applyStep(2)
|
||||
if (event.key === 'ArrowLeft') applyStep(currentStep - 1)
|
||||
if (event.key === 'ArrowRight') applyStep(currentStep + 1)
|
||||
})
|
||||
|
||||
applyStep(0)
|
||||
|
||||
function animate() {
|
||||
objectA.position.lerp(targets.aPosition, 0.1)
|
||||
objectB.position.lerp(targets.bPosition, 0.1)
|
||||
|
||||
objectA.rotation.x += (targets.aRotation.x - objectA.rotation.x) * 0.1
|
||||
objectA.rotation.y += (targets.aRotation.y - objectA.rotation.y) * 0.1
|
||||
objectA.rotation.z += (targets.aRotation.z - objectA.rotation.z) * 0.1
|
||||
|
||||
objectB.rotation.x += (targets.bRotation.x - objectB.rotation.x) * 0.1
|
||||
objectB.rotation.y += (targets.bRotation.y - objectB.rotation.y) * 0.1
|
||||
objectB.rotation.z += (targets.bRotation.z - objectB.rotation.z) * 0.1
|
||||
|
||||
camera.position.lerp(targets.cameraPosition, 0.08)
|
||||
lookCurrent.lerp(targets.cameraLookAt, 0.08)
|
||||
camera.lookAt(lookCurrent)
|
||||
|
||||
renderer.render(scene, camera)
|
||||
}
|
||||
|
||||
renderer.setAnimationLoop(animate)
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
camera.aspect = window.innerWidth / window.innerHeight
|
||||
camera.updateProjectionMatrix()
|
||||
renderer.setSize(window.innerWidth, window.innerHeight)
|
||||
})
|
||||
18
Week-3/Task-3/vanilla/package.json
Normal file
18
Week-3/Task-3/vanilla/package.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "week-1-task-3-vanilla",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"packageManager": "yarn@1.22.22",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "echo 'Add your lint script here'",
|
||||
"clean": "rm -rf dist build"
|
||||
},
|
||||
"dependencies": {
|
||||
"three": "^0.183.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^8.0.3"
|
||||
}
|
||||
}
|
||||
5
Week-3/Task-4/ProductLens.md
Normal file
5
Week-3/Task-4/ProductLens.md
Normal file
@ -0,0 +1,5 @@
|
||||
## Product Lens
|
||||
- Is this pattern useful for real customers? Yes / Partial / No
|
||||
- What kind of customer use case does this support?
|
||||
- Does Thob feel strong enough for this use case?
|
||||
- What would improve the experience?
|
||||
60
Week-3/Task-4/TaskNotes.md
Normal file
60
Week-3/Task-4/TaskNotes.md
Normal file
@ -0,0 +1,60 @@
|
||||
# Task: [Feature Name]
|
||||
|
||||
## Objective
|
||||
What is the feature trying to do?
|
||||
|
||||
## Vanilla three.js
|
||||
-Possible: Yes / Partial / No
|
||||
-Notes:
|
||||
-Key concepts:
|
||||
-Complexity: Easy / Medium / Hard
|
||||
|
||||
## R3F
|
||||
-Possible: Yes / Partial / No
|
||||
-Notes:
|
||||
-What R3F abstracted:
|
||||
-Complexity: Easy / Medium / Hard
|
||||
|
||||
## Thob Page Builder
|
||||
-Possible: Yes / Partial / No
|
||||
-Notes:
|
||||
-Builder steps:
|
||||
-Complexity: Easy / Medium / Hard
|
||||
|
||||
## Comparison Summary
|
||||
-Possible in all 3? Yes / Partial / No
|
||||
-Main differences:
|
||||
-Where Thob is better:
|
||||
-Where Thob is weaker:
|
||||
-What feels awkward or unclear:
|
||||
|
||||
## Limitation Type (if any)
|
||||
-[ ] Editor UX limitation
|
||||
-[ ] Runtime limitation
|
||||
-[ ] Schema / data model limitation
|
||||
-[ ] Component limitation
|
||||
-[ ] Event system limitation
|
||||
-[ ] Asset pipeline limitation
|
||||
-[ ] Unknown / needs investigation
|
||||
|
||||
## Workaround
|
||||
-Is there a workaround?
|
||||
-If yes, what is it?
|
||||
|
||||
## Suggested Improvement
|
||||
-What should improve in Thob?
|
||||
-Is it:
|
||||
-editor
|
||||
-runtime
|
||||
-component
|
||||
-UX
|
||||
-schema/data
|
||||
|
||||
## Difficulty Estimate
|
||||
-Easy / Medium / Hard
|
||||
|
||||
## Business Value
|
||||
-Low / Medium / High
|
||||
|
||||
## Recommendation
|
||||
Should Thob support this better? Why?
|
||||
67
Week-3/Task-4/builder/BuilderNotes.md
Normal file
67
Week-3/Task-4/builder/BuilderNotes.md
Normal file
@ -0,0 +1,67 @@
|
||||
# Builder Notes (Thob) — Task 4: Parent-Child Group Motion
|
||||
|
||||
## Thob Observations from Task Notes
|
||||
|
||||
- **Possible:** Partial
|
||||
- **Implementation used:** Mesh-inside-mesh (parent with children offsets), parent rotation/motion, and Perspective Camera setup.
|
||||
- **What worked as expected:**
|
||||
- Parent-child hierarchy setup is straightforward.
|
||||
- Rotating the parent makes children inherit rotation while keeping offset distance.
|
||||
- Basic grouped motion behavior is achievable visually.
|
||||
- **Main limitation observed:**
|
||||
- Perspective Camera rotation appears unreliable: objects disappear regardless of rotation axis changes.
|
||||
- Because camera rotation is unstable, full parity with the planned step-guided camera behavior is blocked.
|
||||
- Multi-step orchestration remains harder to validate when preview state is unstable.
|
||||
- **Builder flow used:**
|
||||
1. Create parent mesh/group container.
|
||||
2. Add child meshes with local offsets.
|
||||
3. Rotate/move parent to verify inheritance.
|
||||
4. Configure Perspective Camera and test camera orientation adjustments.
|
||||
5. Validate whether grouped motion plus camera framing can be repeated reliably.
|
||||
- **Complexity:** Easy for hierarchy behavior, Medium for complete camera-guided flow due runtime issues.
|
||||
- **Main limitation signals:** Runtime + Editor UX + Unknown investigation needed.
|
||||
- **Workaround status:** Partial workaround only (keep parent-driven motion, avoid direct camera rotation, prefer camera position/look target adjustments).
|
||||
|
||||
## Console Warnings/Errors Seen (Deduplicated) and Probable Meaning
|
||||
|
||||
### warn: `Permissions-Policy header: Unrecognized feature: 'browsing-topics'`
|
||||
- **Type:** Browser/header compatibility warning.
|
||||
- **Probable meaning:** Response includes a policy directive unsupported by current browser/runtime.
|
||||
- **Impact:** Usually low for core feature behavior; mainly environment-level noise.
|
||||
|
||||
### error: `GET https://builder.thob.studio/builder/<id> 404 (Not Found)`
|
||||
- **Type:** Network/resource error.
|
||||
- **Probable meaning:** Builder project/resource URL is stale, missing, or inaccessible in the current session.
|
||||
- **Impact:** High for workflow continuity; can interrupt loading and testing stability.
|
||||
|
||||
### warn: `Unchecked runtime.lastError: The message port closed before a response was received`
|
||||
- **Type:** Browser runtime/extension messaging warning.
|
||||
- **Probable meaning:** Background/bridge message channel closed before callback completion.
|
||||
- **Impact:** Usually non-fatal for scene logic, but adds debugging noise.
|
||||
|
||||
### warn: `No HydrateFallback element provided to render during initial hydration`
|
||||
- **Type:** Hydration/lifecycle warning.
|
||||
- **Probable meaning:** Hydration path expects a fallback UI but none is configured.
|
||||
- **Impact:** Can cause unstable initial render state in editor/preview panels.
|
||||
|
||||
### warn: `Found both blacklist and siteRules — using siteRules`
|
||||
- **Type:** Configuration precedence warning.
|
||||
- **Probable meaning:** Two rule sources are present, runtime picks one (`siteRules`).
|
||||
- **Impact:** Generally non-blocking, but indicates overlapping configuration surfaces.
|
||||
|
||||
### warn: `undefined is changing from uncontrolled to controlled` and `RadioGroup is changing from uncontrolled to controlled`
|
||||
- **Type:** React state-management warning.
|
||||
- **Probable meaning:** Controls mount with undefined/default state and later become controlled.
|
||||
- **Impact:** Property controls may behave inconsistently, making camera/property tuning harder.
|
||||
|
||||
### warn: `GetBindingData<id> method already registered` (repeated)
|
||||
- **Type:** Duplicate registration warning.
|
||||
- **Probable meaning:** Binding handlers are being registered repeatedly across rerenders/remounts without cleanup.
|
||||
- **Impact:** High log noise and risk of duplicate event executions, especially problematic for interactive scene controls.
|
||||
|
||||
## Overall Read
|
||||
|
||||
- Task 4 core hierarchy behavior works in builder: parent rotation correctly drives child motion with preserved local offsets.
|
||||
- The major blocker is camera reliability: direct Perspective Camera rotation can make objects disappear, reducing feature parity confidence.
|
||||
- Repeated binding-registration and controlled/uncontrolled warnings suggest editor/runtime instability that can amplify interaction and camera issues.
|
||||
- Product priority for this task should focus on camera rotation reliability first, then interaction-state stability for repeatable multi-step scene authoring.
|
||||
15
Week-3/Task-4/r3f/index.html
Normal file
15
Week-3/Task-4/r3f/index.html
Normal file
@ -0,0 +1,15 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Task 4 R3F</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
24
Week-3/Task-4/r3f/package.json
Normal file
24
Week-3/Task-4/r3f/package.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "week-1-task-4-r3f",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"packageManager": "yarn@1.22.22",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "echo 'Lint not configured yet'",
|
||||
"preview": "vite preview",
|
||||
"clean": "rm -rf dist build .next"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-three/fiber": "^9.5.0",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"three": "^0.183.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react": "^6.0.0",
|
||||
"vite": "^8.0.3"
|
||||
}
|
||||
}
|
||||
172
Week-3/Task-4/r3f/src/App.jsx
Normal file
172
Week-3/Task-4/r3f/src/App.jsx
Normal file
@ -0,0 +1,172 @@
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Canvas, useFrame, useThree } from '@react-three/fiber'
|
||||
import * as THREE from 'three'
|
||||
|
||||
const STEPS = [
|
||||
{
|
||||
label: 'Step 1: Group at rest',
|
||||
detail: 'Parent group is centered and children keep their offsets.',
|
||||
cameraPosition: [0, 1.7, 6],
|
||||
lookAt: [0, 0, 0],
|
||||
groupPosition: [0, 0, 0],
|
||||
groupRotation: [0, 0, 0],
|
||||
spinSpeed: 0,
|
||||
},
|
||||
{
|
||||
label: 'Step 2: Parent rotation',
|
||||
detail: 'Only parent rotates; children move together without changing local layout.',
|
||||
cameraPosition: [1.7, 1.8, 5.6],
|
||||
lookAt: [0, 0, 0],
|
||||
groupPosition: [0, 0, 0],
|
||||
groupRotation: [0.15, 0.55, 0],
|
||||
spinSpeed: 0.015,
|
||||
},
|
||||
{
|
||||
label: 'Step 3: Parent move + rotate',
|
||||
detail: 'Parent shifts and rotates as one unit while child spacing stays intact.',
|
||||
cameraPosition: [0.6, 2.4, 7],
|
||||
lookAt: [0.4, 0.35, 0],
|
||||
groupPosition: [0.7, 0.4, -0.4],
|
||||
groupRotation: [0.35, 1.2, 0.2],
|
||||
spinSpeed: 0.03,
|
||||
},
|
||||
]
|
||||
|
||||
function GuidedGroupScene({ stepIndex }) {
|
||||
const { camera } = useThree()
|
||||
const groupRef = useRef()
|
||||
|
||||
const targets = useMemo(
|
||||
() => ({
|
||||
cameraPosition: new THREE.Vector3(),
|
||||
cameraLookAt: new THREE.Vector3(),
|
||||
lookCurrent: new THREE.Vector3(),
|
||||
groupPosition: new THREE.Vector3(),
|
||||
groupRotation: new THREE.Euler(),
|
||||
spinSpeed: 0,
|
||||
initialized: false,
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const step = STEPS[stepIndex]
|
||||
|
||||
targets.cameraPosition.set(...step.cameraPosition)
|
||||
targets.cameraLookAt.set(...step.lookAt)
|
||||
targets.groupPosition.set(...step.groupPosition)
|
||||
targets.groupRotation.set(...step.groupRotation)
|
||||
targets.spinSpeed = step.spinSpeed
|
||||
|
||||
if (groupRef.current && !targets.initialized) {
|
||||
camera.position.copy(targets.cameraPosition)
|
||||
targets.lookCurrent.copy(targets.cameraLookAt)
|
||||
groupRef.current.position.copy(targets.groupPosition)
|
||||
groupRef.current.rotation.copy(targets.groupRotation)
|
||||
targets.initialized = true
|
||||
}
|
||||
}, [camera, stepIndex, targets])
|
||||
|
||||
useFrame(() => {
|
||||
if (!groupRef.current) return
|
||||
|
||||
groupRef.current.position.lerp(targets.groupPosition, 0.1)
|
||||
|
||||
groupRef.current.rotation.x += (targets.groupRotation.x - groupRef.current.rotation.x) * 0.1
|
||||
groupRef.current.rotation.y += (targets.groupRotation.y - groupRef.current.rotation.y) * 0.1
|
||||
groupRef.current.rotation.z += (targets.groupRotation.z - groupRef.current.rotation.z) * 0.1
|
||||
|
||||
groupRef.current.rotation.y += targets.spinSpeed * 0.2
|
||||
|
||||
camera.position.lerp(targets.cameraPosition, 0.08)
|
||||
targets.lookCurrent.lerp(targets.cameraLookAt, 0.08)
|
||||
camera.lookAt(targets.lookCurrent)
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<ambientLight intensity={0.8} />
|
||||
<directionalLight intensity={1} position={[4, 5, 4]} />
|
||||
|
||||
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, -1.2, 0]}>
|
||||
<planeGeometry args={[12, 12]} />
|
||||
<meshStandardMaterial color="#dde7f3" roughness={0.95} />
|
||||
</mesh>
|
||||
|
||||
<group ref={groupRef}>
|
||||
<mesh position={[-1.1, 0, 0]}>
|
||||
<boxGeometry args={[0.9, 0.9, 0.9]} />
|
||||
<meshStandardMaterial color="#2a9d8f" />
|
||||
</mesh>
|
||||
|
||||
<mesh position={[1.1, 0, 0]}>
|
||||
<sphereGeometry args={[0.55, 24, 24]} />
|
||||
<meshStandardMaterial color="#e76f51" />
|
||||
</mesh>
|
||||
|
||||
<mesh position={[0, 0.95, 0]}>
|
||||
<coneGeometry args={[0.42, 0.85, 24]} />
|
||||
<meshStandardMaterial color="#457b9d" />
|
||||
</mesh>
|
||||
</group>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const [stepIndex, setStepIndex] = useState(0)
|
||||
const step = STEPS[stepIndex]
|
||||
|
||||
useEffect(() => {
|
||||
function onKeyDown(event) {
|
||||
if (event.key === '1') setStepIndex(0)
|
||||
if (event.key === '2') setStepIndex(1)
|
||||
if (event.key === '3') setStepIndex(2)
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', onKeyDown)
|
||||
return () => window.removeEventListener('keydown', onKeyDown)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="app-shell">
|
||||
<div id="guide">
|
||||
<h2>Task 4: Group Motion</h2>
|
||||
<p id="step-label">{step.label}</p>
|
||||
<p id="step-detail">{step.detail}</p>
|
||||
|
||||
<div className="row">
|
||||
{STEPS.map((_, index) => (
|
||||
<button
|
||||
key={index}
|
||||
className={`step-btn ${stepIndex === index ? 'active' : ''}`}
|
||||
onClick={() => setStepIndex(index)}
|
||||
>
|
||||
Step {index + 1}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="row">
|
||||
<button onClick={() => setStepIndex((value) => Math.max(value - 1, 0))} disabled={stepIndex === 0}>
|
||||
Previous
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setStepIndex((value) => Math.min(value + 1, STEPS.length - 1))}
|
||||
disabled={stepIndex === STEPS.length - 1}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p id="hint">Simple parent group animation. Keys: 1, 2, 3.</p>
|
||||
</div>
|
||||
|
||||
<Canvas camera={{ fov: 55, near: 0.1, far: 100, position: [0, 1.7, 6] }}>
|
||||
<color attach="background" args={['#edf3fa']} />
|
||||
<GuidedGroupScene stepIndex={stepIndex} />
|
||||
</Canvas>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
101
Week-3/Task-4/r3f/src/index.css
Normal file
101
Week-3/Task-4/r3f/src/index.css
Normal file
@ -0,0 +1,101 @@
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow: hidden;
|
||||
background: #edf3fa;
|
||||
font-family: "Avenir Next", "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.app-shell {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#guide {
|
||||
position: fixed;
|
||||
top: 14px;
|
||||
left: 14px;
|
||||
z-index: 10;
|
||||
max-width: 300px;
|
||||
background: rgba(255, 255, 255, 0.88);
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
#guide h2 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: #203448;
|
||||
}
|
||||
|
||||
#step-label {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #1b2530;
|
||||
}
|
||||
|
||||
#step-detail {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: #4f647a;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
button {
|
||||
border: 1px solid #cfd8e3;
|
||||
background: #fff;
|
||||
color: #203448;
|
||||
border-radius: 8px;
|
||||
padding: 7px 9px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
border-color: #8ea2b8;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
button.step-btn.active {
|
||||
background: #203448;
|
||||
border-color: #203448;
|
||||
color: #f4f8fb;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
#hint {
|
||||
margin: 0;
|
||||
font-size: 11px;
|
||||
color: #566f86;
|
||||
}
|
||||
10
Week-3/Task-4/r3f/src/main.jsx
Normal file
10
Week-3/Task-4/r3f/src/main.jsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import App from './App.jsx'
|
||||
import './index.css'
|
||||
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
6
Week-3/Task-4/r3f/vite.config.js
Normal file
6
Week-3/Task-4/r3f/vite.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||
124
Week-3/Task-4/vanilla/index.html
Normal file
124
Week-3/Task-4/vanilla/index.html
Normal file
@ -0,0 +1,124 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Task4 vanilla </title>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: #edf3fa;
|
||||
font-family: "Avenir Next", "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#guide {
|
||||
position: fixed;
|
||||
top: 14px;
|
||||
left: 14px;
|
||||
z-index: 10;
|
||||
max-width: 300px;
|
||||
background: rgba(255, 255, 255, 0.88);
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
#guide h2 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: #203448;
|
||||
}
|
||||
|
||||
#step-label {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #1b2530;
|
||||
}
|
||||
|
||||
#step-detail {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: #4f647a;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
button {
|
||||
border: 1px solid #cfd8e3;
|
||||
background: #fff;
|
||||
color: #203448;
|
||||
border-radius: 8px;
|
||||
padding: 7px 9px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
border-color: #8ea2b8;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
button.active {
|
||||
background: #203448;
|
||||
border-color: #203448;
|
||||
color: #f4f8fb;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
#hint {
|
||||
margin: 0;
|
||||
font-size: 11px;
|
||||
color: #566f86;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="guide">
|
||||
<h2>Task 4: Group Motion</h2>
|
||||
<p id="step-label"></p>
|
||||
<p id="step-detail"></p>
|
||||
|
||||
<div class="row">
|
||||
<button class="step-btn" data-step="0">Step 1</button>
|
||||
<button class="step-btn" data-step="1">Step 2</button>
|
||||
<button class="step-btn" data-step="2">Step 3</button>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<button id="prev-btn">Previous</button>
|
||||
<button id="next-btn">Next</button>
|
||||
</div>
|
||||
|
||||
<p id="hint">Simple parent group animation. Keys: 1, 2, 3.</p>
|
||||
</div>
|
||||
<script type="module" src="/main.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
166
Week-3/Task-4/vanilla/main.js
Normal file
166
Week-3/Task-4/vanilla/main.js
Normal file
@ -0,0 +1,166 @@
|
||||
import * as THREE from 'three'
|
||||
|
||||
const scene = new THREE.Scene()
|
||||
scene.background = new THREE.Color(0xedf3fa)
|
||||
|
||||
const camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 0.1, 100)
|
||||
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: true })
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
|
||||
renderer.setSize(window.innerWidth, window.innerHeight)
|
||||
document.body.appendChild(renderer.domElement)
|
||||
|
||||
scene.add(new THREE.AmbientLight(0xffffff, 0.8))
|
||||
|
||||
const keyLight = new THREE.DirectionalLight(0xffffff, 1)
|
||||
keyLight.position.set(4, 5, 4)
|
||||
scene.add(keyLight)
|
||||
|
||||
const floor = new THREE.Mesh(
|
||||
new THREE.PlaneGeometry(12, 12),
|
||||
new THREE.MeshStandardMaterial({ color: 0xdde7f3, roughness: 0.95 })
|
||||
)
|
||||
floor.rotation.x = -Math.PI / 2
|
||||
floor.position.y = -1.2
|
||||
scene.add(floor)
|
||||
|
||||
const parentGroup = new THREE.Group()
|
||||
|
||||
const leftBox = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(0.9, 0.9, 0.9),
|
||||
new THREE.MeshStandardMaterial({ color: 0x2a9d8f })
|
||||
)
|
||||
leftBox.position.x = -1.1
|
||||
|
||||
const rightSphere = new THREE.Mesh(
|
||||
new THREE.SphereGeometry(0.55, 24, 24),
|
||||
new THREE.MeshStandardMaterial({ color: 0xe76f51 })
|
||||
)
|
||||
rightSphere.position.x = 1.1
|
||||
|
||||
const topCone = new THREE.Mesh(
|
||||
new THREE.ConeGeometry(0.42, 0.85, 24),
|
||||
new THREE.MeshStandardMaterial({ color: 0x457b9d })
|
||||
)
|
||||
topCone.position.set(0, 0.95, 0)
|
||||
|
||||
parentGroup.add(leftBox, rightSphere, topCone)
|
||||
scene.add(parentGroup)
|
||||
|
||||
const STEPS = [
|
||||
{
|
||||
label: 'Step 1: Group at rest',
|
||||
detail: 'Parent group is centered and children keep their offsets.',
|
||||
cameraPosition: [0, 1.7, 6],
|
||||
lookAt: [0, 0, 0],
|
||||
groupPosition: [0, 0, 0],
|
||||
groupRotation: [0, 0, 0],
|
||||
spinSpeed: 0,
|
||||
},
|
||||
{
|
||||
label: 'Step 2: Parent rotation',
|
||||
detail: 'Only parent rotates; children move together without changing local layout.',
|
||||
cameraPosition: [1.7, 1.8, 5.6],
|
||||
lookAt: [0, 0, 0],
|
||||
groupPosition: [0, 0, 0],
|
||||
groupRotation: [0.15, 0.55, 0],
|
||||
spinSpeed: 0.015,
|
||||
},
|
||||
{
|
||||
label: 'Step 3: Parent move + rotate',
|
||||
detail: 'Parent shifts and rotates as one unit while child spacing stays intact.',
|
||||
cameraPosition: [0.6, 2.4, 7],
|
||||
lookAt: [0.4, 0.35, 0],
|
||||
groupPosition: [0.7, 0.4, -0.4],
|
||||
groupRotation: [0.35, 1.2, 0.2],
|
||||
spinSpeed: 0.03,
|
||||
},
|
||||
]
|
||||
|
||||
const targets = {
|
||||
cameraPosition: new THREE.Vector3(),
|
||||
cameraLookAt: new THREE.Vector3(),
|
||||
lookCurrent: new THREE.Vector3(),
|
||||
groupPosition: new THREE.Vector3(),
|
||||
groupRotation: new THREE.Euler(),
|
||||
}
|
||||
|
||||
let currentStepIndex = 0
|
||||
let spinSpeed = 0
|
||||
let initialized = false
|
||||
|
||||
const stepLabel = document.getElementById('step-label')
|
||||
const stepDetail = document.getElementById('step-detail')
|
||||
const stepButtons = Array.from(document.querySelectorAll('.step-btn'))
|
||||
const prevButton = document.getElementById('prev-btn')
|
||||
const nextButton = document.getElementById('next-btn')
|
||||
|
||||
function applyStep(stepIndex) {
|
||||
currentStepIndex = THREE.MathUtils.clamp(stepIndex, 0, STEPS.length - 1)
|
||||
const step = STEPS[currentStepIndex]
|
||||
|
||||
targets.cameraPosition.set(...step.cameraPosition)
|
||||
targets.cameraLookAt.set(...step.lookAt)
|
||||
targets.groupPosition.set(...step.groupPosition)
|
||||
targets.groupRotation.set(...step.groupRotation)
|
||||
spinSpeed = step.spinSpeed
|
||||
|
||||
if (!initialized) {
|
||||
camera.position.copy(targets.cameraPosition)
|
||||
targets.lookCurrent.copy(targets.cameraLookAt)
|
||||
parentGroup.position.copy(targets.groupPosition)
|
||||
parentGroup.rotation.copy(targets.groupRotation)
|
||||
initialized = true
|
||||
}
|
||||
|
||||
stepLabel.textContent = step.label
|
||||
stepDetail.textContent = step.detail
|
||||
|
||||
stepButtons.forEach((button, index) => {
|
||||
button.classList.toggle('active', index === currentStepIndex)
|
||||
})
|
||||
|
||||
prevButton.disabled = currentStepIndex === 0
|
||||
nextButton.disabled = currentStepIndex === STEPS.length - 1
|
||||
}
|
||||
|
||||
stepButtons.forEach((button) => {
|
||||
button.addEventListener('click', () => {
|
||||
applyStep(Number(button.dataset.step))
|
||||
})
|
||||
})
|
||||
|
||||
prevButton.addEventListener('click', () => applyStep(currentStepIndex - 1))
|
||||
nextButton.addEventListener('click', () => applyStep(currentStepIndex + 1))
|
||||
|
||||
window.addEventListener('keydown', (event) => {
|
||||
if (event.key === '1') applyStep(0)
|
||||
if (event.key === '2') applyStep(1)
|
||||
if (event.key === '3') applyStep(2)
|
||||
})
|
||||
|
||||
applyStep(0)
|
||||
|
||||
function animate() {
|
||||
parentGroup.position.lerp(targets.groupPosition, 0.1)
|
||||
|
||||
parentGroup.rotation.x += (targets.groupRotation.x - parentGroup.rotation.x) * 0.1
|
||||
parentGroup.rotation.y += (targets.groupRotation.y - parentGroup.rotation.y) * 0.1
|
||||
parentGroup.rotation.z += (targets.groupRotation.z - parentGroup.rotation.z) * 0.1
|
||||
|
||||
parentGroup.rotation.y += spinSpeed * 0.2
|
||||
|
||||
camera.position.lerp(targets.cameraPosition, 0.08)
|
||||
targets.lookCurrent.lerp(targets.cameraLookAt, 0.08)
|
||||
camera.lookAt(targets.lookCurrent)
|
||||
|
||||
renderer.render(scene, camera)
|
||||
}
|
||||
|
||||
renderer.setAnimationLoop(animate)
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
camera.aspect = window.innerWidth / window.innerHeight
|
||||
camera.updateProjectionMatrix()
|
||||
renderer.setSize(window.innerWidth, window.innerHeight)
|
||||
})
|
||||
18
Week-3/Task-4/vanilla/package.json
Normal file
18
Week-3/Task-4/vanilla/package.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "week-1-task-4-vanilla",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"packageManager": "yarn@1.22.22",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "echo 'Add your lint script here'",
|
||||
"clean": "rm -rf dist build"
|
||||
},
|
||||
"dependencies": {
|
||||
"three": "^0.183.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^8.0.3"
|
||||
}
|
||||
}
|
||||
56
Week-3/Week-3-PersonalSummary.md
Normal file
56
Week-3/Week-3-PersonalSummary.md
Normal file
@ -0,0 +1,56 @@
|
||||
# Ansh — Week 2 Summary
|
||||
|
||||
## Tasks Completed
|
||||
- Task 1:
|
||||
- Task 2:
|
||||
- Task 3:
|
||||
- Task 4:
|
||||
|
||||
## Strongest Product Flow In Thob
|
||||
-
|
||||
|
||||
## Weakest / Most Awkward Product Flow In Thob
|
||||
-
|
||||
|
||||
## Top 2 High-Value Discoveries
|
||||
- 1.
|
||||
- 2.
|
||||
|
||||
## Top 1 Quick Win Recommendation
|
||||
-
|
||||
|
||||
## Top 1 Deeper Architecture Concern
|
||||
-
|
||||
|
||||
## If A Customer Wanted A Guided 3D Product Story Or Multi-State Scene, Could Thob Support It Well?
|
||||
- Yes / Partial / No
|
||||
- Why:
|
||||
|
||||
# Divya — Week 3 Summary
|
||||
|
||||
## Tasks Completed
|
||||
- Task 1:
|
||||
- Task 2:
|
||||
- Task 3:
|
||||
- Task 4:
|
||||
|
||||
## Most Reliable Pattern In Thob
|
||||
-
|
||||
|
||||
## Most Fragile / Misleading Pattern In Thob
|
||||
-
|
||||
|
||||
## Top 3 Visual / Configurator Gaps
|
||||
- 1.
|
||||
- 2.
|
||||
- 3.
|
||||
|
||||
## Top 1 Quick Win
|
||||
-
|
||||
|
||||
## Top 1 Deeper Architecture Concern
|
||||
-
|
||||
|
||||
## Can Thob Support Product-Configurator Style Visual State Changes Reliably?
|
||||
- Yes / Partial / No
|
||||
- Why:
|
||||
Loading…
x
Reference in New Issue
Block a user