feat: multi-format 3D viewer with large-file OBJ streaming, Z-up, projection/outline UI
Three.js viewer supporting glb/gltf/obj/fbx/dae/ifc/ply via both server (SSR) and drag&drop (CSR) paths. - Streaming OBJ parser (src/viewer/objStream.ts) for files past the V8 max string length (>~1GB text) that OBJLoader can't handle; indexed geometry, per-vertex color from MTL Kd, float64 recenter baked in. - In-viewer float64 recenter (objRecenter.ts) for huge CAD/survey coordinates (~1e8) so float32 vertex buffers keep precision (no cracked faces). - Z-up right-handed world; Y-up formats rotated on load. - OBJ+MTL+texture drag&drop (LoadingManager URL-modifier maps dropped images). - OrbitControls ground-plane panning (road/rail alignment workflow). - UI: Zoom Fit, perspective/orthographic toggle, feature-edge outline. - DoubleSide for CAD OBJ; PLY mesh + point-cloud support. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* tools/adaptive-smoke.mjs — empirically trigger the adaptive-quality downstep.
|
||||
*
|
||||
* Loads a heavy sample in headless Chrome (software GL = slow) + applies a CDP
|
||||
* CPU throttle so the REAL measured fps sustains < 60. Captures the genuine
|
||||
* `[hmwebviewer] adaptive quality -> tier N (pr=..)` console logs emitted by
|
||||
* src/viewer/adaptiveQuality.ts, plus the live #.fps overlay text. Proves the
|
||||
* <60fps → optimize path fires on real frame measurement (no logic patched).
|
||||
*
|
||||
* USAGE: node tools/adaptive-smoke.mjs [--url URL] [--asset PATH] [--throttle N] [--seconds S]
|
||||
*/
|
||||
import puppeteer from 'puppeteer-core';
|
||||
import { existsSync } from 'node:fs';
|
||||
|
||||
const CHROME_CANDIDATES = [
|
||||
'C:/Program Files/Google/Chrome/Application/chrome.exe',
|
||||
'C:/Program Files (x86)/Google/Chrome/Application/chrome.exe',
|
||||
'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe',
|
||||
];
|
||||
function findBrowser() {
|
||||
if (process.env.PUPPETEER_EXECUTABLE_PATH && existsSync(process.env.PUPPETEER_EXECUTABLE_PATH)) return process.env.PUPPETEER_EXECUTABLE_PATH;
|
||||
for (const p of CHROME_CANDIDATES) if (existsSync(p)) return p;
|
||||
return null;
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const a = argv.slice(2);
|
||||
let url = 'http://127.0.0.1:4173', asset = '/samples/ABeautifulGame.ktx2.glb', throttle = 6, seconds = 25;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (a[i] === '--url') url = a[++i];
|
||||
else if (a[i] === '--asset') asset = a[++i];
|
||||
else if (a[i] === '--throttle') throttle = Number(a[++i]);
|
||||
else if (a[i] === '--seconds') seconds = Number(a[++i]);
|
||||
}
|
||||
return { url, asset, throttle, seconds };
|
||||
}
|
||||
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
async function main() {
|
||||
const o = parseArgs(process.argv);
|
||||
const exe = findBrowser();
|
||||
if (!exe) { console.error('[adaptive-smoke] No Chrome/Edge found.'); process.exit(1); }
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
executablePath: exe,
|
||||
headless: 'new',
|
||||
args: ['--no-sandbox', '--use-gl=swiftshader', '--enable-webgl', '--ignore-gpu-blocklist'],
|
||||
});
|
||||
const page = await browser.newPage();
|
||||
|
||||
const tierLogs = [];
|
||||
page.on('console', (m) => {
|
||||
const t = m.text();
|
||||
if (t.includes('adaptive quality')) { tierLogs.push(t); console.log(' LOG ' + t); }
|
||||
});
|
||||
|
||||
await page.evaluateOnNewDocument(() => {
|
||||
window.__hmwReady = null;
|
||||
window.addEventListener('hmw:ready', (e) => { window.__hmwReady = e.detail; });
|
||||
});
|
||||
|
||||
const target = o.url + '?model=' + o.asset;
|
||||
console.log('[adaptive-smoke] ' + target + ' throttle=' + o.throttle + 'x watch=' + o.seconds + 's browser=' + exe);
|
||||
await page.goto(target, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
||||
await page.waitForFunction('window.__hmwReady != null', { timeout: 60000 });
|
||||
console.log('[adaptive-smoke] model loaded; applying CPU throttle + watching fps...');
|
||||
|
||||
const client = await page.target().createCDPSession();
|
||||
await client.send('Emulation.setCPUThrottlingRate', { rate: o.throttle });
|
||||
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < o.seconds * 1000) {
|
||||
await sleep(2000);
|
||||
const state = await page.evaluate(() => {
|
||||
const el = document.querySelector('.fps');
|
||||
return { fps: el ? el.textContent : '(no overlay)', pr: window.__viewer ? undefined : undefined };
|
||||
});
|
||||
console.log(' t+' + Math.round((Date.now() - start) / 1000) + 's overlay=' + state.fps + ' tierSteps=' + tierLogs.length);
|
||||
if (tierLogs.length >= 3) break; // enough proof
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
|
||||
console.log('\n[adaptive-smoke] tier-change log lines captured: ' + tierLogs.length);
|
||||
tierLogs.forEach((l) => console.log(' ' + l));
|
||||
if (tierLogs.length === 0) {
|
||||
console.error('[adaptive-smoke] NO downstep observed — fps stayed >=60 (raise --throttle or use a heavier asset).');
|
||||
process.exit(2);
|
||||
}
|
||||
console.log('[adaptive-smoke] PASS — adaptive quality degraded on sustained <60fps.');
|
||||
}
|
||||
|
||||
main().catch((e) => { console.error('[adaptive-smoke] Fatal: ' + (e && e.stack ? e.stack : e)); process.exit(1); });
|
||||
Reference in New Issue
Block a user