// Real Cesium/DEM screenshots, with camera range readback. No live config is modified. import { readFileSync, writeFileSync, mkdirSync } from 'node:fs' import { resolve } from 'node:path' import { PNG } from 'pngjs' const BASE = process.env.MODEL_REVIEW_BASE || 'http://127.0.0.1:8092' const DIR = resolve('tools/city-massing/district/generated') const OUT = resolve('.scratch/cockpit-shell-panel-layout/verify/artifacts/t35/district') mkdirSync(OUT, { recursive: true }) const plan = JSON.parse(readFileSync(resolve(DIR, 'placement-report.json'))) const puppeteer = ( await import( 'file:///C:/Users/ASUS/.dsh/profiles/desktop/node_modules/puppeteer-core/lib/puppeteer/puppeteer-core.js' ) ).default const browser = await puppeteer.launch({ executablePath: 'C:/Program Files/Google/Chrome/Application/chrome.exe', headless: true, defaultViewport: { width: 1500, height: 1000 }, args: ['--no-sandbox'], }) const images = [], requests = [], errors = [] const page = await browser.newPage() page.on('pageerror', (e) => errors.push(e.message)) page.on('response', (r) => { if (r.url().endsWith('.glb')) requests.push({ url: r.url(), status: r.status() }) }) try { await page.goto(BASE + '/preview.html#automation', { waitUntil: 'domcontentloaded' }) await page.waitForFunction(() => typeof window.startPreview === 'function') await page.evaluate(() => window.startPreview({ models: [ '/review-assets/district/lod-preview/tileset.json', '/review-assets/city/regular-estate/lod-preview/tileset.json', '/review-assets/city/oldtown/lod-preview/tileset.json', ], })) const apartment = plan.buildings.find((b) => b.sourceId === 'osm-1285336673') || plan.buildings.find((b) => b.asset.includes('slab')) const tower = plan.buildings.find((b) => b.asset.includes('midrise')) const cases = [ { id: '01-satellite-control', lon: 110.395, lat: 28.454, height: 135, range: 1500, pitch: -89, heading: 0, show: false, label: '原始卫星影像 · 屋顶对照', }, { id: '02-neighborhood-overview', lon: 110.395, lat: 28.454, height: 135, range: 1300, pitch: -50, heading: 10, show: true, label: '示范街区 · 有来源的逐栋布置 · 未覆盖区域保留原影像', }, { id: '03-near-apartment', ...apartment.center, height: apartment.floorHeight + 9, range: 230, pitch: -30, heading: 200, show: true, label: '多层住宅 · 230m 实际视距 · 门窗 / 阳台 / 屋檐', }, { id: '04-near-tower', ...tower.center, height: tower.floorHeight + 18, range: 300, pitch: -25, heading: 170, show: true, label: '较高住宅 · 300m 实际视距 · 台阶屋顶 / 层次变化', }, { id: '05-top-built', lon: 110.395, lat: 28.454, height: 135, range: 1500, pitch: -89, heading: 0, show: true, label: '同视角顶视 · 与原始影像轮廓核对', }, { id: '06-near-facade', ...apartment.center, height: apartment.floorHeight + 8, range: 150, pitch: -22, heading: 200, show: true, label: '住宅立面 · 150m 实际视距 · 非特写近景', }, { id: '07-mobile-near', ...apartment.center, height: apartment.floorHeight + 8, range: 230, pitch: -26, heading: 200, show: true, mobile: true, label: '窄屏样片 · 230m 实际视距', }, ] for (const a of cases) { if (a.mobile) { await page.setViewport({ width: 430, height: 932 }) await page.evaluate(() => window.scene35.v.resize()) } await page.evaluate((a) => { const { C, v, tiles } = window.scene35 tiles.forEach((t) => (t.show = a.show)) v.camera.lookAtTransform( C.Transforms.eastNorthUpToFixedFrame(C.Cartesian3.fromDegrees(a.lon, a.lat, a.height)), new C.HeadingPitchRange(C.Math.toRadians(a.heading), C.Math.toRadians(a.pitch), a.range) ) document.querySelector('#caption').textContent = a.label + ' | 立面与楼层为估计,非测绘数据' }, a) await page.waitForFunction(() => window.scene35.v.scene.globe._surface._tilesToRender.length > 0, { timeout: 45000, }) if (a.show) await page.waitForFunction(() => window.scene35.tiles.some(tile => tile.root?.content && tile.tilesLoaded), { timeout: 45000, }) const f = await page.evaluate(() => window.frameCount) await page.waitForFunction((f) => window.frameCount > f + 60, { timeout: 20000 }, f) const state = await page.evaluate(() => { const { v, tiles, E, C } = window.scene35 return { range: C.Cartesian3.magnitude(v.camera.position), depth: v.scene.globe.depthTestAgainstTerrain, groundTiles: v.scene.globe._surface._tilesToRender.length, tiles: E.readTilesetDiagnostics(tiles), } }) if (Math.abs(state.range - a.range) > 1) throw Error('Camera range was not applied') const file = resolve(OUT, a.id + '.png') const png = PNG.sync.read(Buffer.from(await page.screenshot({ path: file }))) // Capture the same canvas with the model hidden; actual model pixels must differ in the central scene. let modelPixels = null if (a.show) { await page.evaluate(() => window.scene35.tiles.forEach((t) => (t.show = false))) const hiddenFrame = await page.evaluate(() => window.frameCount) await page.waitForFunction((f) => window.frameCount > f + 3, {}, hiddenFrame) const hidden = PNG.sync.read(Buffer.from(await page.screenshot())) modelPixels = 0 for (let y = 60; y < png.height - 100; y++) for (let x = 10; x < png.width - 10; x++) { const i = (y * png.width + x) * 4 if (Math.abs(png.data[i] - hidden.data[i]) + Math.abs(png.data[i + 1] - hidden.data[i + 1]) + Math.abs(png.data[i + 2] - hidden.data[i + 2]) > 45) modelPixels++ } if (modelPixels < 100) throw Error('Model has no visible rendered canvas pixels at ' + a.id) await page.evaluate(() => window.scene35.tiles.forEach((t) => (t.show = true))) } images.push({ ...a, ...state, modelPixels, file }) console.log(a.id, JSON.stringify(state)) } writeFileSync(resolve(OUT, 'report.json'), JSON.stringify({ images, requests, errors }, null, 2)) if (errors.length) throw Error(errors.join('\n')) } catch (error) { await page.screenshot({ path: resolve(OUT, 'failure.png') }) throw error } finally { await browser.close() }