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 STUDIO WEB EDITION PREVIEW
{file?file.name:'Untitled project'} • {status}
document.getElementById('file').click()}>Open file Export report
+
File Edit View Tools Help LOCAL SESSION · FILES STAY IN YOUR BROWSER
+
PROJECT ⌄
▣
{file?.name||'No project loaded'} {file?info.format+' asset':'Open an asset to begin'}
HIERARCHY +
◈ Scene
{tree.length?tree.map((n,i)=>
setSelected(n.name)}>◇ {n.name} {n.type}
):
Scene objects will appear here.
}
ASSET INFO
{info?<>
FORMAT {info.format}
FILE SIZE {(info.size/1024).toFixed(1)} KB
>:
No asset selected
}
+ {['Scene','Textures','Materials'].map(t=>setTab(t)} key={t}>{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.
document.getElementById('file').click()}>Choose a file 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}
+ INSPECTOR ⌄
Properties Data
{info?<>◈
{selected||info.name} {info.format} ASSET
FILE DETAILS
Name {info.name}
Format {info.format}
Size {(info.size/1024).toFixed(1)} KB
MIME type {info.type}
Browser port in progress 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.
>:Select an object or open an asset to inspect its properties.
}
+
WEB PREVIEWTHREE.JS VIEWPORT LOCAL FILE MODE CTR STUDIO WEB · 0.1.0 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