diff --git a/web/README.md b/web/README.md new file mode 100644 index 0000000..3756510 --- /dev/null +++ b/web/README.md @@ -0,0 +1,23 @@ +# CTR Studio Web (early preview) + +A browser-first UI scaffold for the CTR Studio project. + +## Run locally + +```bash +cd web +npm install +npm run dev +``` + +## Current scope + +- Browser-based editor workspace and Three.js viewport. +- Drag/drop and local file selection. +- GLB, glTF and OBJ preview. +- Basic file metadata inspection and JSON inspection-report export. +- BCH and BCRES files are accepted as inputs for metadata inspection only. + +## Important porting work + +The desktop project targets .NET 6 and depends on OpenTK/OpenGL, ImGui.NET, native libraries, and the CtrLibrary project. Those components do not run directly in a normal browser. Real BCH/BCRES support requires porting the format readers, texture codecs, material editing and export pipeline to browser-compatible TypeScript/WebAssembly or a server-side service. The current preview does not claim to decode or edit BCH/BCRES assets. diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..e519035 --- /dev/null +++ b/web/index.html @@ -0,0 +1,2 @@ + +CTR Studio Web
\ No newline at end of file diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..51b0eea --- /dev/null +++ b/web/package.json @@ -0,0 +1,20 @@ +{ + "name": "ctr-studio-web", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite --host 0.0.0.0", + "build": "vite build", + "preview": "vite preview --host 0.0.0.0" + }, + "dependencies": { + "@vitejs/plugin-react": "latest", + "react": "latest", + "react-dom": "latest", + "three": "latest" + }, + "devDependencies": { + "vite": "latest" + } +} \ No newline at end of file diff --git a/web/src/main.jsx b/web/src/main.jsx new file mode 100644 index 0000000..16cbb9c --- /dev/null +++ b/web/src/main.jsx @@ -0,0 +1,22 @@ +import React,{useEffect,useRef,useState} from 'react'; +import{createRoot}from'react-dom/client'; +import*as THREE from'three'; +import{OrbitControls}from'three/addons/controls/OrbitControls.js'; +import{GLTFLoader}from'three/addons/loaders/GLTFLoader.js'; +import{OBJLoader}from'three/addons/loaders/OBJLoader.js'; +import './style.css'; + +function App(){ + const mount=useRef(null),sceneRef=useRef(null),[file,setFile]=useState(null),[status,setStatus]=useState('Ready'),[tab,setTab]=useState('Scene'),[tree,setTree]=useState([]),[selected,setSelected]=useState(''),[info,setInfo]=useState(null); + useEffect(()=>{const host=mount.current;const scene=new THREE.Scene();scene.background=new THREE.Color('#171a22');scene.fog=new THREE.Fog('#171a22',18,50);const camera=new THREE.PerspectiveCamera(45,1,.1,1000);camera.position.set(5,4,7);const renderer=new THREE.WebGLRenderer({antialias:true,alpha:false});renderer.setPixelRatio(Math.min(devicePixelRatio,2));renderer.outputColorSpace=THREE.SRGBColorSpace;host.appendChild(renderer.domElement);const controls=new OrbitControls(camera,renderer.domElement);controls.enableDamping=true;scene.add(new THREE.HemisphereLight(0xffffff,0x3c465b,2));const light=new THREE.DirectionalLight(0xffffff,2.2);light.position.set(5,8,5);scene.add(light);const grid=new THREE.GridHelper(20,20,0x48566a,0x303846);scene.add(grid);const axes=new THREE.AxesHelper(1.2);scene.add(axes);const placeholder=new THREE.Mesh(new THREE.IcosahedronGeometry(1.25,2),new THREE.MeshStandardMaterial({color:0x7c8da8,metalness:.25,roughness:.48,wireframe:false}));placeholder.name='Preview placeholder';scene.add(placeholder);sceneRef.current={scene,camera,renderer,controls,placeholder};const resize=()=>{const w=host.clientWidth,h=host.clientHeight;camera.aspect=w/h;camera.updateProjectionMatrix();renderer.setSize(w,h)};const ro=new ResizeObserver(resize);ro.observe(host);resize();let raf;const animate=()=>{raf=requestAnimationFrame(animate);controls.update();renderer.render(scene,camera)};animate();return()=>{cancelAnimationFrame(raf);ro.disconnect();controls.dispose();renderer.dispose();host.removeChild(renderer.domElement)}},[]); + const loadFile=async f=>{if(!f)return;setFile(f);setStatus('Inspecting file…');const ext=f.name.split('.').pop().toLowerCase();setInfo({name:f.name,size:f.size,format:ext.toUpperCase(),type:f.type||'Unknown'});if(['glb','gltf','obj'].includes(ext)){try{const url=URL.createObjectURL(f);let object;if(ext==='obj')object=await new OBJLoader().loadAsync(url);else{const gltf=await new GLTFLoader().loadAsync(url);object=gltf.scene}const s=sceneRef.current;if(s){s.scene.remove(s.placeholder);const old=s.scene.getObjectByName('Imported model');if(old)s.scene.remove(old);object.name='Imported model';s.scene.add(object);const box=new THREE.Box3().setFromObject(object);const size=box.getSize(new THREE.Vector3());const center=box.getCenter(new THREE.Vector3());object.position.sub(center);const max=Math.max(size.x,size.y,size.z)||1;s.camera.position.set(max*1.7,max*1.2,max*1.7);s.controls.target.set(0,0,0);s.controls.update();const rows=[];object.traverse(o=>{if(o!==object)rows.push({name:o.name||o.type,type:o.type})});setTree(rows.slice(0,100));}URL.revokeObjectURL(url);setStatus('Model loaded')}catch(e){setStatus('Could not load model: '+e.message)}}else if(['bch','bcres'].includes(ext)){setTree([]);setStatus('File received — BCH/BCRES decoder not connected yet')}else{setStatus('Unsupported preview format')}}; + const onDrop=e=>{e.preventDefault();loadFile(e.dataTransfer.files?.[0])}; + const exportInfo=()=>{if(!file)return;const blob=new Blob([JSON.stringify({file:file.name,size:file.size,format:info.format,status,tree},null,2)],{type:'application/json'});const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=file.name+'.inspection.json';a.click();URL.revokeObjectURL(a.href)}; + return
CTR
CTR STUDIOWEB EDITION PREVIEW
{file?file.name:'Untitled project'} • {status}
+ +
+
{['Scene','Textures','Materials'].map(t=>)}
Perspective▦◉⋯
e.preventDefault()} onDrop={onDrop}>
X Y Z
{!file&&
⇧

Bring your asset into the studio

Drop a model file here, or browse your device.

3D preview: GLB · GLTF · OBJ
BCH / BCRES: format inspection placeholder
}
DRAG TO ORBIT · SCROLL TO ZOOM · RIGHT-DRAG TO PAN
OUTPUT / CONSOLE{status}
● {status==='Ready'?'CTR Studio Web initialized.':status}
+
+ loadFile(e.target.files?.[0])}/>
+} +createRoot(document.getElementById('root')).render(); \ No newline at end of file diff --git a/web/src/style.css b/web/src/style.css new file mode 100644 index 0000000..84b9a2b --- /dev/null +++ b/web/src/style.css @@ -0,0 +1,2 @@ +@import url('https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Inter:wght@400;500;600;700&display=swap'); +:root{font-family:Inter,system-ui,sans-serif;color:#e6e9ef;background:#101116;font-synthesis:none}*{box-sizing:border-box}body{margin:0;min-width:850px;overflow:hidden}.app{height:100vh;display:flex;flex-direction:column;background:#101116}.top{height:62px;display:flex;align-items:center;padding:0 18px;border-bottom:1px solid #292c35;gap:28px;background:#15171e}.brand{display:flex;align-items:center;gap:10px;min-width:210px}.mark{height:34px;width:38px;border-radius:7px;background:#8c9bb7;color:#11151d;font-weight:800;display:grid;place-items:center;font-size:12px;letter-spacing:-.5px}.brand b{font-size:12px;letter-spacing:1.5px}.brand small{display:block;font:9px 'DM Mono';color:#788397;letter-spacing:1.2px;margin-top:3px}.brand small span{color:#a9bbd8;margin-left:6px}.topfile{font:11px 'DM Mono';color:#9ba4b4;flex:1}.topfile i{color:#68748a;margin:0 8px}.topactions{display:flex;gap:8px}button{font:600 11px Inter;border:1px solid #343946;border-radius:5px;padding:9px 13px;background:#20242d;color:#dce2ec;cursor:pointer}button:hover{border-color:#8494b2}button:disabled{opacity:.35;cursor:not-allowed}.primary{background:#9aabc8;color:#111722;border-color:#9aabc8}.primary:hover{background:#b3c0d6}.ghost{background:transparent}.menu{height:34px;border-bottom:1px solid #292c35;display:flex;align-items:center;padding:0 20px;gap:22px;color:#9ca5b4;font-size:10px}.menu-right{margin-left:auto;font:9px 'DM Mono';letter-spacing:.7px;color:#606b7e}.workspace{flex:1;min-height:0;display:grid;grid-template-columns:238px minmax(400px,1fr) 258px;gap:1px;background:#292c35}.panel{background:#171a21;min-width:0;display:flex;flex-direction:column}.panelhead{height:40px;padding:0 14px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #292d37;color:#9da8bb;font:10px 'DM Mono';letter-spacing:1px}.panelhead span{font-size:14px;color:#68748a}.project{display:flex;align-items:center;gap:10px;padding:14px;border-bottom:1px solid #292d37}.folder{width:30px;height:30px;border-radius:5px;background:#293244;color:#a9bad6;display:grid;place-items:center}.project b,.assetcard b{font-size:11px;display:block;max-width:170px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.project small,.assetcard small{display:block;font:9px 'DM Mono';color:#7e899c;margin-top:4px}.sectionlabel{padding:16px 14px 8px;color:#7e899c;font:9px 'DM Mono';letter-spacing:1px;display:flex;justify-content:space-between}.sectionlabel span{font-size:14px}.tree{padding:0 8px}.treeitem{height:29px;display:flex;align-items:center;gap:8px;padding:0 8px;border-radius:4px;color:#aab5c8;font-size:11px;cursor:pointer}.treeitem.active,.treeitem.chosen{background:#283244;color:#dce6f7}.treeitem.indent{padding-left:24px}.treeitem small{margin-left:auto;color:#68768d;font:9px 'DM Mono'}.empty{color:#687386;font-size:10px;line-height:1.6;padding:10px 14px}.leftbottom{margin-top:auto;border-top:1px solid #292d37;padding-bottom:12px}.infotile{margin:6px 12px;padding:9px;background:#20242d;border-radius:4px;display:flex;justify-content:space-between;align-items:center}.infotile small{font:9px 'DM Mono';color:#768298}.infotile b{font:10px 'DM Mono';color:#d5deed}.center{display:flex;flex-direction:column;min-width:0;background:#171a21}.viewportbar{height:40px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #292d37;padding:0 12px}.tabs{display:flex;gap:4px}.tab{border:0;background:transparent;color:#788397;padding:12px 10px;border-radius:0}.tab.on{color:#e2e8f2;border-bottom:2px solid #9aabc8}.viewtools{display:flex;gap:16px;color:#8a95a8;font-size:11px}.viewport{flex:1;min-height:280px;position:relative;overflow:hidden}.viewport canvas{display:block;width:100%;height:100%}.welcome{position:absolute;z-index:2;left:50%;top:45%;transform:translate(-50%,-50%);text-align:center;width:320px;pointer-events:none}.welcome button{pointer-events:auto}.uploadicon{margin:0 auto 14px;width:42px;height:42px;border:1px solid #46536a;border-radius:10px;display:grid;place-items:center;color:#b6c5de;font-size:21px;background:#202735}.welcome h2{font-size:16px;margin:0 0 8px}.welcome p{font-size:11px;color:#8994a7;margin:0 0 18px}.welcome small{display:block;color:#77849a;font:9px/1.8 'DM Mono';margin-top:16px}.axislabel{position:absolute;top:12px;left:14px;z-index:1;font:10px 'DM Mono';color:#e27e7e;pointer-events:none}.axislabel em{color:#8dc99c;font-style:normal;margin:0 5px}.viewhint{position:absolute;bottom:12px;left:14px;color:#768399;font:9px 'DM Mono';letter-spacing:.4px;pointer-events:none}.viewhint span{padding:0 5px;color:#4f5b70}.timeline{height:126px;border-top:1px solid #303541;background:#14171e}.timelinehead{height:34px;display:flex;align-items:center;justify-content:space-between;padding:0 14px;color:#8e9bb0;font:9px 'DM Mono';letter-spacing:.8px}.timelinehead span{color:#738098;letter-spacing:0}.log{padding:9px 14px;color:#9ca7b8;font:10px 'DM Mono'}.log i{color:#85c79c;font-style:normal;margin-right:7px}.inspecttabs{display:flex;border-bottom:1px solid #292d37}.inspecttabs button{flex:1;border:0;border-radius:0;background:transparent;color:#758198;padding:12px}.inspecttabs button.selected{color:#e0e6f1;border-bottom:2px solid #9aabc8}.assetcard{display:flex;align-items:center;gap:10px;padding:16px 14px;border-bottom:1px solid #292d37}.assetglyph{width:32px;height:32px;display:grid;place-items:center;border-radius:5px;background:#293244;color:#b3c4df;font-size:17px}.propgroup{padding:0 14px}.prophead{padding:16px 0 8px;color:#7d899e;font:9px 'DM Mono';letter-spacing:1px}.prop{display:flex;justify-content:space-between;gap:10px;padding:9px 0;border-bottom:1px solid #292d37;font-size:10px}.prop span{color:#8490a3}.prop b{font:9px 'DM Mono';color:#c8d1e0;text-align:right;overflow-wrap:anywhere}.notice{margin:18px 12px;padding:12px;border:1px solid #3b4351;border-radius:5px;background:#202630}.notice b{font-size:10px;color:#c4d0e4}.notice p{font-size:10px;line-height:1.6;color:#8e9aaf;margin:7px 0 0}.inspector-empty{margin-top:12px}footer{height:25px;display:flex;align-items:center;gap:20px;padding:0 14px;background:#12151b;border-top:1px solid #292c35;color:#69768b;font:8px 'DM Mono';letter-spacing:.7px}footer .green{display:inline-block;width:6px;height:6px;background:#81c49b;border-radius:50%;margin-right:5px}.footright{margin-left:auto}@media(max-width:1050px){.workspace{grid-template-columns:190px minmax(350px,1fr) 210px}.brand{min-width:180px}.top{gap:14px}} \ No newline at end of file