Files
PhoneViewer/tools/render-probe.html
T

262 lines
10 KiB
HTML
Raw Normal View History

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<title>EasyView · DWG render probe</title>
<style>
html,
body {
margin: 0;
height: 100%;
background: #0d1117;
overflow: hidden;
}
#stage {
position: fixed;
inset: 0;
}
</style>
</head>
<body>
<div id="stage"></div>
<script type="module">
import { CadViewer } from '@flyfish-dev/cad-viewer'
const stage = document.getElementById('stage')
let current = null
function makeViewer(canvasOptions = {}, extra = {}) {
return new CadViewer({
container: stage,
renderer: 'auto',
wasmPath: new URL('/wasm/', location.href).href,
workerUrl: new URL('/wasm/dwg-worker.js', location.href).href,
dwfWasmUrl: new URL('/wasm/dwfv-render.wasm', location.href).href,
useWorker: true,
autoFit: true,
...extra,
canvasOptions: {
background: '#0d1117',
foreground: '#f4f7fb',
contrastMode: 'adaptive',
enableSpatialIndex: true,
spatialIndexCellCount: 96,
maxCurveSegments: 72,
maxVisibleTextLabels: 2400,
...canvasOptions
}
})
}
function summarizeTypes(entities) {
const byType = {}
for (const e of entities) {
const t = e.type || 'UNKNOWN'
byType[t] = (byType[t] || 0) + 1
}
return byType
}
function proxyReport(entities) {
const proxies = entities.filter(
(e) => /proxy/i.test(e.type || '') || /proxy/i.test(String(e.subType || ''))
)
const byType = {}
for (const e of proxies) {
const t = e.type || 'PROXY'
byType[t] = (byType[t] || 0) + 1
}
return { total: proxies.length, byType }
}
window.__probe = async (url, options = {}) => {
if (current) {
try {
current.destroy()
} catch {}
current = null
}
stage.innerHTML = ''
const viewer = makeViewer(options.canvasOptions || {}, options.viewerOptions || {})
current = viewer
const stats = []
let loadProgress = []
viewer.onRenderStats?.(() => {})
const res = await fetch(url)
const buf = await res.arrayBuffer()
const name = decodeURIComponent(url.split('/').pop())
const file = new File([buf], name)
let tPreload = 0
if (options.preload) {
const tp = performance.now()
await viewer.preloadDwg()
tPreload = performance.now() - tp
}
const t0 = performance.now()
const progressTimeline = []
const result = await viewer.loadFile(file, {
onProgress: (p) => progressTimeline.push({ ms: Math.round(performance.now() - t0), phase: p.phase, message: p.message })
})
const tLoad = performance.now() - t0
await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)))
const doc = result.document
const firstView = viewer.renderer.render()
// pan/zoom interaction timing (操作速度)
const frameTimes = []
const renderer = viewer.renderer
let prev = performance.now()
for (let i = 0; i < 40; i++) {
renderer.panByScreenDelta(8, 6)
renderer.render()
const now = performance.now()
frameTimes.push(now - prev)
prev = now
}
const zoomTimes = []
for (let i = 0; i < 20; i++) {
const t = performance.now()
viewer.zoomIn()
zoomTimes.push(performance.now() - t)
}
viewer.fit('auto')
const perf = (arr) => {
const s = [...arr].sort((a, b) => a - b)
return {
avg: +(arr.reduce((a, b) => a + b, 0) / arr.length).toFixed(2),
p50: +s[Math.floor(s.length * 0.5)].toFixed(2),
p95: +s[Math.floor(s.length * 0.95)].toFixed(2),
max: +s[s.length - 1].toFixed(2)
}
}
const proxySample = doc.entities
.filter((e) => /proxy/i.test(e.type || ''))
.slice(0, 6)
.map((e) => ({
keys: Object.keys(e),
originalDxfName: e.originalDxfName,
graphicsDataSize: e.graphicsDataSize,
entityDataSize: e.entityDataSize,
hasPreview: !!e.graphicsData,
classId: e.applicationEntityClassId
}))
const proxyWithGraphics = doc.entities.filter(
(e) => /proxy/i.test(e.type || '') && (e.graphicsDataSize > 0 || e.entityDataSize > 0)
).length
// 复现 buildCadViews 的判定,便于对比 App 面板
const nameOf = (e) => (e.type === 'INSERT' ? e.effectiveBlockName || e.blockName || e.name : undefined)
const byName = new Map()
for (const b of Object.values(doc.blocks || {})) {
const k = String(b.name || '').trim().toLowerCase()
if (!k) continue
const cur = byName.get(k)
if (!cur || (b.entities || []).length > cur.entities.length) byName.set(k, b)
}
const sceneRefs = new Set(doc.entities.map(nameOf).filter(Boolean).map((n) => n.toLowerCase()))
const candidates = [...byName.entries()].filter(([k, b]) => !sceneRefs.has(k) && !/^\*?(model|paper)_space/i.test(b.name) && (b.entities || []).length)
const candRefs = new Map()
for (const [k, b] of candidates) {
for (const e of b.entities || []) {
const n = nameOf(e)
if (n) candRefs.set(n.toLowerCase(), (candRefs.get(n.toLowerCase()) || 0) + 1)
}
}
const viewCandidates = candidates
.filter(([k]) => !candRefs.has(k))
.sort((a, b) => b[1].entities.length - a[1].entities.length)
.map(([k, b]) => `${b.name}:${b.entities.length}`)
const viewBlockParents = [...candRefs.entries()]
.filter(([k]) => /视图/.test(k))
.map(([k, c]) => `${k}<-${c}`)
return {
file: name,
blockGraph: Object.values(doc.blocks || {})
.filter((b) => (b.entities || []).length >= 15)
.map((b) => ({ n: b.name, size: b.entities.length, ins: [...new Set((b.entities || []).map(nameOf).filter(Boolean))].slice(0, 8) })),
viewCandidates: viewCandidates.slice(0, 12),
viewBlockParents: viewBlockParents.slice(0, 12),
proxySample,
proxyWithGraphics,
bytes: result.bytes,
parseElapsedMs: Math.round(result.elapsedMs),
preloadMs: Math.round(tPreload),
progressTimeline,
paperSpaceEntities: doc.entities.filter((e) => e.isInPaperSpace === true).length,
metadataKeys: Object.keys(doc.metadata || {}),
headerKeys: Object.keys(doc.header || {}).slice(0, 40),
loadElapsedMs: Math.round(tLoad),
backend: firstView.backend || 'unknown',
entityCount: doc.entities.length,
entityTypes: summarizeTypes(doc.entities),
layerCount: Object.keys(doc.layers || {}).length,
blockCount: Object.keys(doc.blocks || {}).length,
spaceBlocks: Object.entries(doc.blocks || {})
.filter(([name]) => /space/i.test(name))
.map(([name, b]) => ({ name, entities: (b.entities || []).length })),
blockNames: Object.keys(doc.blocks || {}),
sceneInserts: [...new Set(doc.entities.filter((e) => e.type === 'INSERT').map((e) => e.effectiveBlockName || e.blockName || e.name).filter(Boolean))],
blockEntityTotal: Object.values(doc.blocks || {}).reduce((n, b) => n + (b.entities || []).length, 0),
topBlocks: Object.entries(doc.blocks || {})
.sort((a, b) => (b[1].entities || []).length - (a[1].entities || []).length)
.slice(0, 5)
.map(([name, b]) => `${name}:${(b.entities || []).length}`),
pages: (doc.pages || []).map((p) => ({ name: p.name, type: p.type })),
pageCount: (doc.pages || []).length,
proxies: proxyReport(doc.entities),
missingReferences: (viewer.getMissingReferences?.() || []).map((r) => r.name || r.fileName || r),
loadedReferences: (viewer.getLoadedReferences?.() || []).length,
warnings: [...(result.warnings || []), ...(doc.warnings || [])].slice(0, 40),
render: {
total: firstView.total,
drawn: firstView.drawn,
skipped: firstView.skipped,
unsupported: firstView.unsupported,
renderElapsedMs: +firstView.renderElapsedMs.toFixed(2),
primitiveCount: firstView.primitiveCount,
visiblePrimitiveCount: firstView.visiblePrimitiveCount,
culledPrimitiveCount: firstView.culledPrimitiveCount,
gpuMemoryMB: firstView.gpuMemoryBytes ? +(firstView.gpuMemoryBytes / 1048576).toFixed(1) : undefined,
buildElapsedMs: firstView.buildElapsedMs ? Math.round(firstView.buildElapsedMs) : undefined
},
interaction: { panFrameMs: perf(frameTimes), zoomCallMs: perf(zoomTimes) },
zoomPercent: viewer.getZoomPercent(),
loadProgressCount: loadProgress.length
}
}
// 供 CDP 剖析用:先建 viewer + 预编译 wasm(不解析),再单独触发解析
window.__prepare = async (url) => {
if (current) {
try { current.destroy() } catch {}
current = null
}
stage.innerHTML = ''
const viewer = makeViewer()
current = viewer
const res = await fetch(url)
const buf = await res.arrayBuffer()
const name = decodeURIComponent(url.split('/').pop())
const file = new File([buf], name)
const t = performance.now()
await viewer.preloadDwg()
window.__pendingFile = file
return { preloadMs: Math.round(performance.now() - t) }
}
window.__loadPrepared = async () => {
const t = performance.now()
const result = await current.loadFile(window.__pendingFile)
return { parseMs: Math.round(performance.now() - t), entities: result.document.entities.length }
}
window.__capture = async (url) => url
window.__ready = true
</script>
</body>
</html>