fix: resolve workspace integration blockers (#5)

This commit is contained in:
2026-07-29 10:01:17 +09:00
parent 3279ac099e
commit cadbd5fb60
37 changed files with 1817 additions and 73 deletions
+1
View File
@@ -3,6 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'/%3E" />
<title>hmwebviewer — 3D Viewer</title>
<link rel="stylesheet" href="/src/style.css" />
</head>
+3
View File
@@ -6,7 +6,10 @@
"scripts": {
"dev": "vite",
"build": "tsc --noEmit && vite build",
"build:subpath:e2e": "vite build --base=/viewer-3d/ --outDir=dist-subpath",
"preview": "vite preview",
"preview:subpath:e2e": "vite preview --base=/viewer-3d/ --outDir=dist-subpath --host 127.0.0.1 --port 43174 --strictPort",
"serve:subpath:e2e": "npm run build:subpath:e2e && npm run preview:subpath:e2e",
"typecheck": "tsc --noEmit",
"stage:decoders": "node tools/copy-decoders.mjs"
},
+16
View File
@@ -0,0 +1,16 @@
# Hand-authored cube fixture
o Cube
v -1 -1 -1
v 1 -1 -1
v 1 1 -1
v -1 1 -1
v -1 -1 1
v 1 -1 1
v 1 1 1
v -1 1 1
f 1 4 3 2
f 5 6 7 8
f 1 2 6 5
f 2 3 7 6
f 3 4 8 7
f 4 1 5 8
View File
View File
+14 -3
View File
@@ -1,6 +1,7 @@
import { ThreeDViewer } from './viewer/ThreeDViewer';
import { initDropzone } from './dnd/dropzone';
import { setStatus } from './ui/progress';
import { publicAssetUrl } from './runtimeBase';
/**
* Entry point. Wires the ThreeDViewer (renderer + loaders + hydration) and the
@@ -62,11 +63,21 @@ if (modelUrl) {
// SSR placeholder: a pre-rendered 360° animated WebP (tools/prerender.mjs)
// shows instantly, then the hydration gate fades it out once WebGL + model ready.
const base = modelUrl.split('/').pop()!.replace(/\.(glb|gltf|obj|fbx|dae|ifc|ply)$/i, '');
previewEl.src = '/previews/' + base + '.webp';
previewEl.src = publicAssetUrl(`previews/${base}.webp`);
previewEl.classList.remove('hidden');
previewEl.onerror = () => previewEl.classList.add('hidden');
viewer.loadServerAsset(modelUrl);
}
// Exposed for tooling (tools/prerender.mjs captures rotating frames).
(window as unknown as { __viewer?: ThreeDViewer }).__viewer = viewer;
// tools/prerender.mjs가 회전 frame을 캡처할 때 사용하는 공개 reference입니다.
const viewerWindow = window as Window & { __viewer?: ThreeDViewer };
viewerWindow.__viewer = viewer;
window.addEventListener(
'pagehide',
() => {
viewer.dispose();
delete viewerWindow.__viewer;
},
{ once: true },
);
+12
View File
@@ -0,0 +1,12 @@
/**
* Vite의 배포 base 아래에 있는 public asset의 절대 URL을 반환합니다.
*/
export function publicAssetUrl(
path: string,
base = import.meta.env.BASE_URL,
origin = window.location.origin,
): string {
const normalizedBase = base.endsWith('/') ? base : `${base}/`;
const normalizedPath = path.replace(/^\/+/, '');
return new URL(normalizedPath, new URL(normalizedBase, `${origin}/`)).href;
}
+5 -2
View File
@@ -2,6 +2,7 @@ import * as THREE from 'three';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';
import { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader.js';
import { publicAssetUrl } from '../runtimeBase';
export interface ViewerLoaders {
gltf: GLTFLoader;
@@ -16,8 +17,10 @@ let _cache: ViewerLoaders | null = null;
export function getLoaders(renderer: THREE.WebGLRenderer): ViewerLoaders {
if (_cache) return _cache;
const draco = new DRACOLoader().setDecoderPath('/draco/');
const ktx2 = new KTX2Loader().setTranscoderPath('/basis/').detectSupport(renderer);
const draco = new DRACOLoader().setDecoderPath(publicAssetUrl('draco/'));
const ktx2 = new KTX2Loader()
.setTranscoderPath(publicAssetUrl('basis/'))
.detectSupport(renderer);
const gltf = new GLTFLoader().setDRACOLoader(draco).setKTX2Loader(ktx2);
_cache = { gltf, draco, ktx2 };
return _cache;
+2 -1
View File
@@ -1,4 +1,5 @@
import * as THREE from 'three';
import { publicAssetUrl } from '../runtimeBase';
import { getLoaders } from './loaders';
/**
@@ -244,7 +245,7 @@ async function loadIfc(url: string, onProgress?: ProgressCb): Promise<THREE.Obje
const WebIFC = await import('web-ifc');
if (!_ifcApi) {
_ifcApi = new WebIFC.IfcAPI();
_ifcApi.SetWasmPath('/web-ifc/', true);
_ifcApi.SetWasmPath(publicAssetUrl('web-ifc/'), true);
await _ifcApi.Init(undefined, true);
}
const api = _ifcApi;
+23 -3
View File
@@ -2,13 +2,30 @@
// runtime paths /draco/ and /basis/ resolve (version-matched to installed three).
// Also copies the web-ifc single-thread WASM to public/web-ifc/ for the IFC path.
// Runs automatically via `npm install` (postinstall). Safe to re-run.
import { cpSync, copyFileSync, existsSync, mkdirSync, rmSync } from 'node:fs';
import {
chmodSync,
cpSync,
copyFileSync,
existsSync,
mkdirSync,
readdirSync,
rmSync,
} from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const root = resolve(__dirname, '..');
function normalizePublicModes(directory) {
chmodSync(directory, 0o755);
for (const entry of readdirSync(directory, { withFileTypes: true })) {
const path = resolve(directory, entry.name);
if (entry.isDirectory()) normalizePublicModes(path);
else if (entry.isFile()) chmodSync(path, 0o644);
}
}
const threeEntry = fileURLToPath(import.meta.resolve('three'));
const threeRoot = resolve(dirname(threeEntry), '..');
const libs = resolve(threeRoot, 'examples/jsm/libs');
@@ -30,6 +47,7 @@ for (const [src, dest] of targets) {
rmSync(dest, { force: true, recursive: true });
mkdirSync(dest, { recursive: true });
cpSync(src, dest, { recursive: true });
normalizePublicModes(dest);
console.log(`[copy-decoders] ${src} -> ${dest}`);
}
@@ -42,8 +60,10 @@ if (existsSync(ifcSrcDir)) {
for (const wasm of ['web-ifc.wasm', 'web-ifc-mt.wasm']) {
const src = resolve(ifcSrcDir, wasm);
if (!existsSync(src)) continue;
copyFileSync(src, resolve(ifcDest, wasm));
console.log(`[copy-decoders] ${src} -> ${resolve(ifcDest, wasm)}`);
const dest = resolve(ifcDest, wasm);
copyFileSync(src, dest);
chmodSync(dest, 0o644);
console.log(`[copy-decoders] ${src} -> ${dest}`);
}
} else {
console.warn('[copy-decoders] web-ifc not installed yet — skipping IFC wasm.');
+1 -1
View File
@@ -14,7 +14,7 @@
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"types": ["three"]
"types": ["three", "vite/client"]
},
"include": ["src", "tools"]
}