add: week3 project setup with previous tasks included

This commit is contained in:
anshk 2026-04-07 14:57:54 +05:30
parent 2f2d5e6573
commit 2724f4e475
59 changed files with 7054 additions and 0 deletions

View 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?

View 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?

View File

24
Week-3/Task-1/r3f/Cammera/.gitignore vendored Normal file
View 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?

View 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.

View 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_]' }],
},
},
])

View 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>

View 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"
}
}

View 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

View 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;
}

View 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>,
)

View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
})

File diff suppressed because it is too large Load Diff

View 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"
}
}

View 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>

View 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);
});

View 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"
}
}

View 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?

View 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?

View 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.

View 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
View 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?

View 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.

View 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_]' }],
},
},
])

View 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

File diff suppressed because it is too large Load Diff

View 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"
}
}

View 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

View 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;
}

View 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>,
)

View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
})

View 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>

View 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)
})

View 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"
}
}

View 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?

View 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?

View 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.

View 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>

View 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"
}
}

View 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>
)
}

View 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;
}

View 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>,
)

View File

@ -0,0 +1,6 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
})

View 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>

View 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)
})

View 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"
}
}

View 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?

View 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?

View 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.

View 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>

View 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"
}
}

View 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>
)
}

View 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;
}

View 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>,
)

View File

@ -0,0 +1,6 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
})

View 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>

View 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)
})

View 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"
}
}

View 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: