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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions web/README.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 2 additions & 0 deletions web/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<!doctype html>
<html lang="en"><head><meta charset="UTF-8"/><meta name="viewport" content="width=device-width, initial-scale=1.0"/><meta name="theme-color" content="#101116"/><title>CTR Studio Web</title></head><body><div id="root"></div><script type="module" src="/src/main.jsx"></script></body></html>
20 changes: 20 additions & 0 deletions web/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
22 changes: 22 additions & 0 deletions web/src/main.jsx
Original file line number Diff line number Diff line change
@@ -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 <div className="app"><header className="top"><div className="brand"><div className="mark">CTR</div><div><b>CTR STUDIO</b><small>WEB EDITION <span>PREVIEW</span></small></div></div><div className="topfile">{file?file.name:'Untitled project'} <i>•</i> {status}</div><div className="topactions"><button className="ghost" onClick={()=>document.getElementById('file').click()}>Open file</button><button className="primary" onClick={exportInfo} disabled={!file}>Export report</button></div></header>
<nav className="menu"><span>File</span><span>Edit</span><span>View</span><span>Tools</span><span>Help</span><div className="menu-right">LOCAL SESSION · FILES STAY IN YOUR BROWSER</div></nav>
<main className="workspace"><aside className="left panel"><div className="panelhead"><b>PROJECT</b><span>⌄</span></div><div className="project"><div className="folder">▣</div><div><b>{file?.name||'No project loaded'}</b><small>{file?info.format+' asset':'Open an asset to begin'}</small></div></div><div className="sectionlabel">HIERARCHY <span>+</span></div><div className="tree"><div className="treeitem active">◈ <span>Scene</span></div>{tree.length?tree.map((n,i)=><div className={'treeitem indent '+(selected===n.name?'chosen':'')} key={i} onClick={()=>setSelected(n.name)}>◇ <span>{n.name}</span><small>{n.type}</small></div>):<div className="empty">Scene objects will appear here.</div>}</div><div className="leftbottom"><div className="sectionlabel">ASSET INFO</div>{info?<><div className="infotile"><small>FORMAT</small><b>{info.format}</b></div><div className="infotile"><small>FILE SIZE</small><b>{(info.size/1024).toFixed(1)} KB</b></div></>:<div className="empty">No asset selected</div>}</div></aside>
<section className="center"><div className="viewportbar"><div className="tabs">{['Scene','Textures','Materials'].map(t=><button className={tab===t?'tab on':'tab'} onClick={()=>setTab(t)} key={t}>{t}</button>)}</div><div className="viewtools"><span>Perspective</span><span>▦</span><span>◉</span><span>⋯</span></div></div><div className="viewport" ref={mount} onDragOver={e=>e.preventDefault()} onDrop={onDrop}><div className="axislabel">X <em>Y</em> Z</div>{!file&&<div className="welcome"><div className="uploadicon">⇧</div><h2>Bring your asset into the studio</h2><p>Drop a model file here, or browse your device.</p><button className="primary" onClick={()=>document.getElementById('file').click()}>Choose a file</button><small>3D preview: GLB · GLTF · OBJ<br/>BCH / BCRES: format inspection placeholder</small></div>}<div className="viewhint">DRAG TO ORBIT <span>·</span> SCROLL TO ZOOM <span>·</span> RIGHT-DRAG TO PAN</div></div><div className="timeline"><div className="timelinehead"><b>OUTPUT / CONSOLE</b><span>{status}</span></div><div className="log"><i>●</i> {status==='Ready'?'CTR Studio Web initialized.':status}</div></div></section>
<aside className="right panel"><div className="panelhead"><b>INSPECTOR</b><span>⌄</span></div><div className="inspecttabs"><button className="selected">Properties</button><button>Data</button></div>{info?<><div className="assetcard"><div className="assetglyph">◈</div><div><b>{selected||info.name}</b><small>{info.format} ASSET</small></div></div><div className="propgroup"><div className="prophead">FILE DETAILS</div><div className="prop"><span>Name</span><b>{info.name}</b></div><div className="prop"><span>Format</span><b>{info.format}</b></div><div className="prop"><span>Size</span><b>{(info.size/1024).toFixed(1)} KB</b></div><div className="prop"><span>MIME type</span><b>{info.type}</b></div></div><div className="notice"><b>Browser port in progress</b><p>The original editor uses native OpenGL and C# libraries. BCH/BCRES parsing and editing must be ported before these assets can be rendered or modified here.</p></div></>:<div className="empty inspector-empty">Select an object or open an asset to inspect its properties.</div>}</aside></main>
<footer><span><i className="green"/> WEB PREVIEW</span><span>THREE.JS VIEWPORT</span><span>LOCAL FILE MODE</span><span className="footright">CTR STUDIO WEB · 0.1.0</span></footer><input id="file" type="file" hidden accept=".glb,.gltf,.obj,.bch,.bcres" onChange={e=>loadFile(e.target.files?.[0])}/></div>
}
createRoot(document.getElementById('root')).render(<App/>);
2 changes: 2 additions & 0 deletions web/src/style.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.