From 9fbde885ecbd47504c90de11cddf0f20972bfc09 Mon Sep 17 00:00:00 2001 From: Xiang Silian Date: Tue, 25 Nov 2025 13:16:57 +0800 Subject: [PATCH] init --- html/unity-coordinate-visualizer/.gitignore | 24 ++ html/unity-coordinate-visualizer/App.tsx | 379 ++++++++++++++++++ html/unity-coordinate-visualizer/README.md | 20 + html/unity-coordinate-visualizer/index.html | 41 ++ html/unity-coordinate-visualizer/index.tsx | 15 + .../unity-coordinate-visualizer/metadata.json | 5 + html/unity-coordinate-visualizer/package.json | 22 + .../unity-coordinate-visualizer/tsconfig.json | 29 ++ .../vite.config.ts | 23 ++ 9 files changed, 558 insertions(+) create mode 100644 html/unity-coordinate-visualizer/.gitignore create mode 100644 html/unity-coordinate-visualizer/App.tsx create mode 100644 html/unity-coordinate-visualizer/README.md create mode 100644 html/unity-coordinate-visualizer/index.html create mode 100644 html/unity-coordinate-visualizer/index.tsx create mode 100644 html/unity-coordinate-visualizer/metadata.json create mode 100644 html/unity-coordinate-visualizer/package.json create mode 100644 html/unity-coordinate-visualizer/tsconfig.json create mode 100644 html/unity-coordinate-visualizer/vite.config.ts diff --git a/html/unity-coordinate-visualizer/.gitignore b/html/unity-coordinate-visualizer/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/html/unity-coordinate-visualizer/.gitignore @@ -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? diff --git a/html/unity-coordinate-visualizer/App.tsx b/html/unity-coordinate-visualizer/App.tsx new file mode 100644 index 0000000..81dbe7e --- /dev/null +++ b/html/unity-coordinate-visualizer/App.tsx @@ -0,0 +1,379 @@ +import React, { useEffect, useRef, useState, useMemo } from 'react'; +import * as THREE from 'three'; +import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; + +// --- Types & Constants --- +interface CoordinateState { + theta: number; // Yaw + phi: number; // Pitch + r: number; // Radius +} + +const AXIS_LEN = 12; + +const App: React.FC = () => { + // --- State --- + const [coords, setCoords] = useState({ + theta: 45, + phi: 30, + r: 8 + }); + + // --- Refs for Three.js objects --- + const mountRef = useRef(null); + const sceneRef = useRef<{ + scene: THREE.Scene; + camera: THREE.PerspectiveCamera; + renderer: THREE.WebGLRenderer; + sphereMesh: THREE.Mesh; + radiusLine: THREE.Line; + projLine: THREE.Line; + heightLine: THREE.Line; + controls: OrbitControls; + } | null>(null); + + // --- Math Logic (Extracted for UI & 3D sync) --- + const mathResult = useMemo(() => { + const { theta, phi, r } = coords; + + // --- CHANGE HERE: Invert the Phi (Pitch) direction --- + // Original was: const radPhi = phi * Math.PI / 180; + const radTheta = theta * Math.PI / 180; + const radPhi = -phi * Math.PI / 180; // Inverted rotation direction + + const sinT = Math.sin(radTheta); + const cosT = Math.cos(radTheta); + const sinP = Math.sin(radPhi); + const cosP = Math.cos(radPhi); + + // Unity Coordinate Calculation + const r_xz = r * cosP; // Projection length on XZ plane + const valX = r_xz * sinT; + const valY = r * sinP; + const valZ = r_xz * cosT; + + return { + radTheta, + radPhi, + sinT, cosT, sinP, cosP, + r_xz, + valX, valY, valZ + }; + }, [coords]); + + // --- Initialize Three.js Scene --- + useEffect(() => { + if (!mountRef.current) return; + + // 1. Scene Setup + const scene = new THREE.Scene(); + scene.background = new THREE.Color(0x1a1a1a); + + const width = mountRef.current.clientWidth; + const height = mountRef.current.clientHeight; + const camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 1000); + camera.position.set(0, 5, 20); + + const renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setSize(width, height); + renderer.setPixelRatio(window.devicePixelRatio); + mountRef.current.appendChild(renderer.domElement); + + const controls = new OrbitControls(camera, renderer.domElement); + controls.enableDamping = true; + + // 2. Helpers + const gridHelper = new THREE.GridHelper(30, 30, 0x444444, 0x222222); + scene.add(gridHelper); + + // 3. Axes Creation Helper + const createLine = (start: THREE.Vector3, end: THREE.Vector3, color: number, dashed = false) => { + const points = [start, end]; + const geo = new THREE.BufferGeometry().setFromPoints(points); + let mat: THREE.Material; + if (dashed) { + mat = new THREE.LineDashedMaterial({ color, dashSize: 0.5, gapSize: 0.3, scale: 1 }); + } else { + mat = new THREE.LineBasicMaterial({ color, linewidth: 2 }); + } + const line = new THREE.Line(geo, mat); + if (dashed) line.computeLineDistances(); + return line; + }; + + // Axes + scene.add(createLine(new THREE.Vector3(0, 0, 0), new THREE.Vector3(AXIS_LEN, 0, 0), 0xff0000)); // X + scene.add(createLine(new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, AXIS_LEN, 0), 0x00ff00)); // Y + scene.add(createLine(new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, 0, -AXIS_LEN), 0x0066ff)); // Z (Visualized into screen) + + // Labels (Using Sprites) + const addLabel = (text: string, pos: THREE.Vector3, colorStr: string) => { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + if (ctx) { + canvas.width = 64; canvas.height = 64; + ctx.fillStyle = colorStr; + ctx.font = "bold 48px Arial"; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillText(text, 32, 32); + const texture = new THREE.CanvasTexture(canvas); + const spriteMat = new THREE.SpriteMaterial({ map: texture }); + const sprite = new THREE.Sprite(spriteMat); + sprite.position.copy(pos); + sprite.scale.set(1.5, 1.5, 1.5); + scene.add(sprite); + } + }; + + addLabel("X", new THREE.Vector3(AXIS_LEN + 1, 0, 0), "#ff5555"); + addLabel("Y", new THREE.Vector3(0, AXIS_LEN + 1, 0), "#55ff55"); + addLabel("Z", new THREE.Vector3(0, 0, -AXIS_LEN - 1), "#5588ff"); + + // 4. Dynamic Objects + const sphereMesh = new THREE.Mesh( + new THREE.SphereGeometry(0.4, 32, 32), + new THREE.MeshBasicMaterial({ color: 0xffff00 }) + ); + scene.add(sphereMesh); + + const radiusLine = createLine(new THREE.Vector3(), new THREE.Vector3(), 0xffff00, true); + const projLine = createLine(new THREE.Vector3(), new THREE.Vector3(), 0x00aa00); + const heightLine = createLine(new THREE.Vector3(), new THREE.Vector3(), 0x00aaff); + scene.add(radiusLine, projLine, heightLine); + + // Store refs + sceneRef.current = { + scene, camera, renderer, sphereMesh, radiusLine, projLine, heightLine, controls + }; + + // Animation Loop + const animate = () => { + requestAnimationFrame(animate); + controls.update(); + renderer.render(scene, camera); + }; + animate(); + + // Cleanup + return () => { + if (mountRef.current) { + mountRef.current.removeChild(renderer.domElement); + } + renderer.dispose(); + }; + }, []); + + // --- Update Scene when Coordinates Change --- + useEffect(() => { + if (!sceneRef.current) return; + + const { sphereMesh, radiusLine, projLine, heightLine } = sceneRef.current; + const { valX, valY, valZ } = mathResult; + + // Unity Z+ is "Forward". In Three.js (WebGL), "Forward" into the screen is Z-. + // To visually match the Unity inspector logic where Z+ goes "into" the scene relative to camera start: + const renderX = valX; + const renderY = valY; + const renderZ = -valZ; + + sphereMesh.position.set(renderX, renderY, renderZ); + + const origin = new THREE.Vector3(0, 0, 0); + const target = new THREE.Vector3(renderX, renderY, renderZ); + const ground = new THREE.Vector3(renderX, 0, renderZ); + + // Update lines + radiusLine.geometry.setFromPoints([origin, target]); + radiusLine.computeLineDistances(); // Necessary for dashed lines + radiusLine.geometry.attributes.position.needsUpdate = true; + + projLine.geometry.setFromPoints([origin, ground]); + projLine.geometry.attributes.position.needsUpdate = true; + + heightLine.geometry.setFromPoints([ground, target]); + heightLine.geometry.attributes.position.needsUpdate = true; + + }, [mathResult]); + + // --- Resize Handler --- + useEffect(() => { + const handleResize = () => { + if (!mountRef.current || !sceneRef.current) return; + const width = window.innerWidth; + const height = window.innerHeight; + const { camera, renderer } = sceneRef.current; + + camera.aspect = width / height; + camera.updateProjectionMatrix(); + renderer.setSize(width, height); + }; + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); + + // --- UI Handlers --- + const handleChange = (key: keyof CoordinateState, value: number) => { + setCoords(prev => ({ ...prev, [key]: value })); + }; + + return ( +
+ {/* 3D Canvas Container */} +
+ + {/* UI Overlay */} +
+ +
+

Unity Coordinates

+

+ System: X-Right, Y-Up, Z-Forward (Into Screen) +

+

+ * Pitch direction inverted +

+
+ + {/* Controls */} +
+ handleChange('theta', v)} + /> + handleChange('phi', v)} + /> + handleChange('r', v)} + /> +
+ + {/* Calculations Display */} +
+

Calculation Logic

+ +
+ {/* Step 1 */} +
+
// 1. Deg to Rad (Pitch Inverted)
+
θ = {coords.theta}° ⇒ sin: {mathResult.sinT.toFixed(3)}, cos: {mathResult.cosT.toFixed(3)}
+
φ = -({coords.phi})° ⇒ sin: {mathResult.sinP.toFixed(3)}, cos: {mathResult.cosP.toFixed(3)}
+
+ + {/* Step 2 */} +
+
// 2. XZ Plane Projection
+
+ r_xz = r * cos(φ) +
+
+ = {coords.r} * {mathResult.cosP.toFixed(3)} = {mathResult.r_xz.toFixed(3)} +
+
+ + {/* Step 3 */} +
+
// 3. Unity Vectors
+ + {/* Y */} +
+ Y (Up) = r * sin(φ) +
+
+ = {coords.r} * {mathResult.sinP.toFixed(3)} = {mathResult.valY.toFixed(2)} +
+ + {/* X */} +
+ X (Right) = r_xz * sin(θ) +
+
+ = {mathResult.r_xz.toFixed(3)} * {mathResult.sinT.toFixed(3)} = {mathResult.valX.toFixed(2)} +
+ + {/* Z */} +
+ Z (Fwd) = r_xz * cos(θ) +
+
+ = {mathResult.r_xz.toFixed(3)} * {mathResult.cosT.toFixed(3)} = {mathResult.valZ.toFixed(2)} +
+
+
+
+ + {/* Result Grid */} +
+
+ X: {mathResult.valX.toFixed(2)} +
+
+ Y: {mathResult.valY.toFixed(2)} +
+
+ Z: {mathResult.valZ.toFixed(2)} +
+
+ +
+ + {/* Footer Tooltip */} +
+ + Left Click: Rotate · Right Click: Pan · Scroll: Zoom + +
+
+ ); +}; + +// --- Sub-component for Slider+Input --- +interface ControlRowProps { + label: string; + value: number; + min: number; + max: number; + step: number; + onChange: (val: number) => void; +} + +const ControlRow: React.FC = ({ label, value, min, max, step, onChange }) => { + return ( +
+ +
+ onChange(parseFloat(e.target.value))} + className="flex-grow h-1 bg-neutral-600 rounded-lg appearance-none cursor-pointer accent-sky-500 hover:accent-sky-400 transition-all" + /> + { + let v = parseFloat(e.target.value); + if (isNaN(v)) return; + if (v < min) v = min; + if (v > max) v = max; + onChange(v); + }} + className="w-16 bg-neutral-950 border border-neutral-600 text-white text-right text-sm p-1 rounded focus:outline-none focus:border-sky-500 font-mono" + /> +
+
+ ); +}; + +export default App; diff --git a/html/unity-coordinate-visualizer/README.md b/html/unity-coordinate-visualizer/README.md new file mode 100644 index 0000000..c83afef --- /dev/null +++ b/html/unity-coordinate-visualizer/README.md @@ -0,0 +1,20 @@ +
+GHBanner +
+ +# Run and deploy your AI Studio app + +This contains everything you need to run your app locally. + +View your app in AI Studio: https://ai.studio/apps/drive/1xK1SY-nHdgcy_n6gMkYNyMPdNrp-RAif + +## Run Locally + +**Prerequisites:** Node.js + + +1. Install dependencies: + `npm install` +2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key +3. Run the app: + `npm run dev` diff --git a/html/unity-coordinate-visualizer/index.html b/html/unity-coordinate-visualizer/index.html new file mode 100644 index 0000000..370cd95 --- /dev/null +++ b/html/unity-coordinate-visualizer/index.html @@ -0,0 +1,41 @@ + + + + + + Unity Coordinate Visualizer + + + + + + +
+ + + \ No newline at end of file diff --git a/html/unity-coordinate-visualizer/index.tsx b/html/unity-coordinate-visualizer/index.tsx new file mode 100644 index 0000000..6ca5361 --- /dev/null +++ b/html/unity-coordinate-visualizer/index.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +const rootElement = document.getElementById('root'); +if (!rootElement) { + throw new Error("Could not find root element to mount to"); +} + +const root = ReactDOM.createRoot(rootElement); +root.render( + + + +); \ No newline at end of file diff --git a/html/unity-coordinate-visualizer/metadata.json b/html/unity-coordinate-visualizer/metadata.json new file mode 100644 index 0000000..f52b9c9 --- /dev/null +++ b/html/unity-coordinate-visualizer/metadata.json @@ -0,0 +1,5 @@ +{ + "name": "Unity Coordinate Visualizer", + "description": "A 3D interactive visualization of Unity coordinate system (Left-handed, Z-forward) demonstrating the relationship between spherical inputs (Yaw/Pitch/Radius) and Cartesian coordinates.", + "requestFramePermissions": [] +} \ No newline at end of file diff --git a/html/unity-coordinate-visualizer/package.json b/html/unity-coordinate-visualizer/package.json new file mode 100644 index 0000000..03a5d78 --- /dev/null +++ b/html/unity-coordinate-visualizer/package.json @@ -0,0 +1,22 @@ +{ + "name": "unity-coordinate-visualizer", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.2.0", + "three": "^0.181.2", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@types/node": "^22.14.0", + "@vitejs/plugin-react": "^5.0.0", + "typescript": "~5.8.2", + "vite": "^6.2.0" + } +} diff --git a/html/unity-coordinate-visualizer/tsconfig.json b/html/unity-coordinate-visualizer/tsconfig.json new file mode 100644 index 0000000..2c6eed5 --- /dev/null +++ b/html/unity-coordinate-visualizer/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "ES2022", + "experimentalDecorators": true, + "useDefineForClassFields": false, + "module": "ESNext", + "lib": [ + "ES2022", + "DOM", + "DOM.Iterable" + ], + "skipLibCheck": true, + "types": [ + "node" + ], + "moduleResolution": "bundler", + "isolatedModules": true, + "moduleDetection": "force", + "allowJs": true, + "jsx": "react-jsx", + "paths": { + "@/*": [ + "./*" + ] + }, + "allowImportingTsExtensions": true, + "noEmit": true + } +} \ No newline at end of file diff --git a/html/unity-coordinate-visualizer/vite.config.ts b/html/unity-coordinate-visualizer/vite.config.ts new file mode 100644 index 0000000..ee5fb8d --- /dev/null +++ b/html/unity-coordinate-visualizer/vite.config.ts @@ -0,0 +1,23 @@ +import path from 'path'; +import { defineConfig, loadEnv } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, '.', ''); + return { + server: { + port: 3000, + host: '0.0.0.0', + }, + plugins: [react()], + define: { + 'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY), + 'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY) + }, + resolve: { + alias: { + '@': path.resolve(__dirname, '.'), + } + } + }; +});