Fix blank drawings, drop parse-time animation cost, add render diagnostics
Accuracy / completeness - docViews: split parsed scene into model space / paper space / free-standing blocks (geometry hidden in uninserted blocks no longer shows as a blank canvas); auto-select the first block view when model space is empty - viewer: view switcher panel listing every detected view with expanded entity counts; paper space is now parsed (includePaperSpace) and kept apart - SHX: missing .shx references are surfaced with an import control (addReferenceFile), linetypes/text redraw afterwards - unsupported/proxy entities: markers on canvas + diagnostics block showing proxy / wipeout counts and missing SHX names Speed - the loading overlay used an infinite CSS spinner + backdrop blur while the worker parsed; on software rendering that starved the parse thread (TELECOM 18MB: 71s -> 30s wall clock, normalize 33s -> 14s). Replaced with a determinate progress bar fed by loader progress events - transferInputBuffer for worker loads, maxInsertDepth 32, longer worker timeout Tooling - scripts/probe-dwg.mjs / probe-app.mjs: headless render + end-to-end probes (entity/layer/block/layout/proxy stats, timings, screenshots) - tools/render-probe.html, scripts/profile-worker.mjs - version 0.6.0 (versionCode 8)
This commit is contained in:
@@ -0,0 +1,261 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user