25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

138 lines
6.2 KiB

  1. /**
  2. * 采集原型 DEMO 的模型工程出厂基线,作为「正式环境要和 DEMO 一致」的唯一基准物。
  3. *
  4. * 产物落到 `unreal_tran_web/scripts/fixtures/bridge-erector-demo-baseline.json`,
  5. * 由 `unreal_tran_web/scripts/emit-model-bridge-erector-seed.mjs` 读取生成数据库种子。
  6. *
  7. * 为什么必须从浏览器采集,不能从 profile 推导:
  8. * 文档里 `model.objects`(187 个部件语义 + 114 条材质)与
  9. * `environment.objects`(70 个环境对象布局)是 `captureModelObjects()` /
  10. * `captureOutdoorScene()` 从活的 three.js 场景里采下来的,
  11. * `equipmentModelProfiles.ts` 只有机构轨道,推不出这两块。
  12. * 丢了它们,下游 `applyModelEditorOverrides` 就回放不出部件语义与 PBR 质感。
  13. *
  14. * 关键点:必须用全新 browser context。
  15. * 原型启动流程是 loadModel -> restoreAutoSave()(读 dt-editor-project-v2),
  16. * 有存档就恢复存档、没存档才走 createBaselineProject()。
  17. * 带着上一次的 localStorage 打开,采到的是那份存档而不是出厂基线。
  18. * 报告里的 `hadAutosaveBeforeDump` 必须是 false 才算有效采集。
  19. *
  20. * 前置:`unreal_tran_web` 的 dev server 或 preview 在 6180 上跑着,
  21. * 且 `public/legacy-model-editor/` 已由 stage-legacy-model-editor.mjs 生成。
  22. *
  23. * 用法(在 ute2e 下):
  24. * node tools/dump-model-demo-baseline.mjs
  25. * node tools/dump-model-demo-baseline.mjs # 自定义地址用 TARGET 环境变量
  26. *
  27. * 只覆盖模型制作,不涉及场景制作。
  28. */
  29. import { chromium } from '@playwright/test'
  30. import { mkdirSync, writeFileSync } from 'node:fs'
  31. const URL = process.env.TARGET || 'http://127.0.0.1:6180/legacy-model-editor/editor.html'
  32. const OUT = 'artifacts/model-demo-baseline'
  33. /** 基准物落在 Web 项目的 fixtures 下,由种子生成器读取。 */
  34. const FIXTURE = '../unreal_tran_web/scripts/fixtures/bridge-erector-demo-baseline.json'
  35. mkdirSync(OUT, { recursive: true })
  36. const browser = await chromium.launch({
  37. args: ['--enable-unsafe-swiftshader', '--use-gl=angle', '--use-angle=swiftshader'],
  38. })
  39. // 全新 context,天然没有 localStorage / IndexedDB 残留,拿到的是出厂基线工程
  40. const context = await browser.newContext({ viewport: { width: 1920, height: 1080 } })
  41. const page = await context.newPage()
  42. const failures = []
  43. page.on('requestfailed', (r) => failures.push(`${r.method()} ${r.url()} :: ${r.failure()?.errorText}`))
  44. page.on('response', (r) => { if (r.status() >= 400) failures.push(`HTTP ${r.status()} ${r.url()}`) })
  45. const report = {}
  46. try {
  47. await page.goto(URL, { waitUntil: 'domcontentloaded', timeout: 60000 })
  48. await page.waitForSelector('.editor-shell', { timeout: 60000 })
  49. await page.waitForSelector('#loading-overlay.hidden', { timeout: 180000 })
  50. // 基准动作是在 loadModel 之后注入的,等一拍确保时间轴已就位
  51. await page.waitForTimeout(1500)
  52. const dump = await page.evaluate(() => {
  53. const app = window.editorApp
  54. if (!app) throw new Error('window.editorApp 不存在')
  55. return {
  56. project: app.serializeProject(),
  57. runtime: {
  58. activeDescriptorId: app.activeDescriptor?.id ?? null,
  59. activeDescriptorName: app.activeDescriptor?.name ?? null,
  60. timelineTrackCount: app.timeline?.tracks?.length ?? 0,
  61. blueprintNodeCount: app.blueprint?.nodes?.length ?? 0,
  62. blueprintEdgeCount: app.blueprint?.edges?.length ?? 0,
  63. environmentMeta: { ...(app.outdoorSceneMeta ?? {}) },
  64. modelTargetKeys: [...(app.modelTargets?.keys?.() ?? [])],
  65. },
  66. // 存档路径也一起记下来,确认这次确实走的是出厂基线而不是恢复存档
  67. hadAutosave: Boolean(window.localStorage.getItem('dt-editor-project-v2')),
  68. }
  69. })
  70. report.runtime = dump.runtime
  71. report.hadAutosaveBeforeDump = dump.hadAutosave
  72. const project = dump.project
  73. if (dump.hadAutosave) {
  74. throw new Error('采集到的是 localStorage 存档而不是出厂基线,请用全新 context 重跑')
  75. }
  76. writeFileSync(`${OUT}/demo-project.json`, `${JSON.stringify(project, null, 2)}\n`, 'utf8')
  77. writeFileSync(`${OUT}/demo-project.compact.json`, JSON.stringify(project), 'utf8')
  78. // 缩进版进仓库:122 KB 的单行 JSON 没法 code review,缩进后 diff 可读
  79. writeFileSync(FIXTURE, `${JSON.stringify(project, null, 2)}\n`, 'utf8')
  80. report.summary = {
  81. schema: project.schema,
  82. version: project.version,
  83. name: project.name,
  84. modelId: project.modelId,
  85. meta: project.meta,
  86. timeline: {
  87. duration: project.timeline?.duration,
  88. currentTime: project.timeline?.currentTime,
  89. loop: project.timeline?.loop,
  90. zoom: project.timeline?.zoom,
  91. trackCount: project.timeline?.tracks?.length,
  92. trackIds: project.timeline?.tracks?.map((t) => t.id),
  93. trackTargets: project.timeline?.tracks?.map((t) => t.targetId),
  94. keyframeTotal: project.timeline?.tracks?.reduce((s, t) => s + t.keyframes.length, 0),
  95. },
  96. blueprint: {
  97. name: project.blueprint?.name,
  98. viewport: project.blueprint?.viewport,
  99. nodeCount: project.blueprint?.nodes?.length,
  100. edgeCount: project.blueprint?.edges?.length,
  101. nodes: project.blueprint?.nodes?.map((n) => ({ id: n.id, type: n.type, title: n.title, params: n.params })),
  102. edges: project.blueprint?.edges,
  103. },
  104. camera: project.camera,
  105. modelResource: project.modelResource,
  106. model: project.model ? { objectKeys: Object.keys(project.model.objects ?? {}).length } : null,
  107. environment: {
  108. ...project.environment,
  109. objects: `<${Object.keys(project.environment?.objects ?? {}).length} 个对象,已折叠>`,
  110. },
  111. runtime: project.runtime,
  112. publish: project.publish,
  113. topLevelKeys: Object.keys(project),
  114. }
  115. report.failedRequests = failures
  116. report.bytesCompact = Buffer.byteLength(JSON.stringify(project), 'utf8')
  117. report.ok = failures.length === 0 && project.modelId === 'bridge-erector-vehicle'
  118. } catch (error) {
  119. report.error = error?.message || String(error)
  120. report.failedRequests = failures
  121. report.ok = false
  122. } finally {
  123. await browser.close()
  124. }
  125. writeFileSync(`${OUT}/dump-report.json`, `${JSON.stringify(report, null, 2)}\n`, 'utf8')
  126. process.stdout.write(`报告已写入 ${OUT}/dump-report.json ok=${report.ok}\n`)
  127. process.exit(report.ok ? 0 : 1)