be34c630bb
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)
262 lines
10 KiB
HTML
262 lines
10 KiB
HTML
<!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>
|