Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 
 
 

191 lignes
8.3 KiB

  1. import assert from 'node:assert/strict'
  2. import { readFile } from 'node:fs/promises'
  3. import test from 'node:test'
  4. import ts from 'typescript'
  5. const sourceUrl = new URL('../../unreal_tran_web/src/features/scene-legacy/sceneCoverCapture.ts', import.meta.url)
  6. const source = await readFile(sourceUrl, 'utf8')
  7. const output = ts.transpileModule(source, {
  8. compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 },
  9. reportDiagnostics: true,
  10. })
  11. assert.deepEqual(output.diagnostics, [])
  12. const { captureSceneCover } = await import(`data:text/javascript;base64,${Buffer.from(output.outputText).toString('base64')}`)
  13. function fixture({ width = 1600, height = 900, renderError, drawError, encodeError, blob } = {}) {
  14. const events = []
  15. const transformHelper = { visible: true }
  16. const grid = { visible: false }
  17. const boundsHelper = { visible: true }
  18. const materials = [
  19. { original: { id: 'single-original' }, current: { id: 'single-highlighted' } },
  20. { original: [{ id: 'array-original-1' }, { id: 'array-original-2' }], current: [{ id: 'array-highlighted-1' }, { id: 'array-highlighted-2' }] },
  21. ]
  22. const entries = materials.map(({ original, current }) => ({ child: { material: current }, original, highlighted: current }))
  23. const sourceCanvas = { width, height }
  24. const scene = { children: [transformHelper, grid, boundsHelper, ...entries.map(entry => entry.child)] }
  25. const camera = Object.freeze({ position: Object.freeze({ x: 1, y: 2, z: 3 }) })
  26. const selected = entries[0].child
  27. const visibility = () => [transformHelper.visible, grid.visible, boundsHelper.visible]
  28. const snapshot = operation => ({ operation, visibility: visibility(), materials: entries.map(entry => entry.child.material) })
  29. let renders = 0
  30. const neverCalled = () => assert.fail('capture must not reset, detach, or dispose selection')
  31. const editor = {
  32. scene, camera, destroyed: false, selected,
  33. controls: { target: Object.freeze({ x: 0, y: 0, z: 0 }), update: neverCalled },
  34. transform: { getHelper: () => transformHelper, detach: neverCalled },
  35. grid,
  36. selectionHighlight: {
  37. helper: boundsHelper, materialEntries: entries, object: selected,
  38. setObject: neverCalled, clear: neverCalled, update: neverCalled,
  39. },
  40. renderer: {
  41. domElement: sourceCanvas,
  42. render(actualScene, actualCamera) {
  43. assert.equal(actualScene, scene)
  44. assert.equal(actualCamera, camera)
  45. events.push(snapshot('render'))
  46. renders += 1
  47. if (renders === 1 && renderError) throw renderError
  48. },
  49. },
  50. }
  51. const canvas = {
  52. width: 0, height: 0,
  53. getContext(kind) {
  54. assert.equal(kind, '2d')
  55. return {
  56. drawImage(...args) {
  57. assert.deepEqual(args, [sourceCanvas, 0, 0, canvas.width, canvas.height])
  58. events.push(snapshot('copy'))
  59. if (drawError) throw drawError
  60. },
  61. }
  62. },
  63. toBlob(callback, type, quality) {
  64. events.push({ ...snapshot('encode'), type, quality })
  65. if (encodeError) throw encodeError
  66. queueMicrotask(() => callback(blob === undefined ? new Blob(['jpeg'], { type }) : blob))
  67. },
  68. }
  69. const document = { createElement: tag => { assert.equal(tag, 'canvas'); return canvas } }
  70. const assertRestored = () => {
  71. assert.deepEqual(visibility(), [true, false, true])
  72. entries.forEach((entry, index) => {
  73. assert.equal(entry.child.material, materials[index].current)
  74. assert.equal(entry.original, materials[index].original)
  75. assert.equal(entry.highlighted, materials[index].current)
  76. })
  77. assert.equal(editor.selectionHighlight.materialEntries, entries)
  78. assert.equal(editor.selectionHighlight.object, selected)
  79. assert.equal(editor.selected, selected)
  80. assert.equal(scene.children.length, 5)
  81. assert.deepEqual(camera.position, { x: 1, y: 2, z: 3 })
  82. assert.deepEqual(editor.controls.target, { x: 0, y: 0, z: 0 })
  83. }
  84. return { editor, document, events, canvas, materials, entries, assertRestored }
  85. }
  86. test('copies synchronously without bounds, gizmo, grid or selection glow, then restores before JPEG encoding', async () => {
  87. const f = fixture()
  88. const pending = captureSceneCover(f.editor, { document: f.document })
  89. assert.deepEqual(f.events.map(event => event.operation), ['render', 'copy', 'render', 'encode'])
  90. for (const event of f.events.slice(0, 2)) {
  91. assert.deepEqual(event.visibility, [false, false, false])
  92. event.materials.forEach((material, index) => assert.equal(material, f.materials[index].original))
  93. }
  94. for (const event of f.events.slice(2)) {
  95. assert.deepEqual(event.visibility, [true, false, true])
  96. event.materials.forEach((material, index) => assert.equal(material, f.materials[index].current))
  97. }
  98. f.assertRestored()
  99. assert.equal(f.events[3].type, 'image/jpeg')
  100. assert.equal(f.events[3].quality, 0.85)
  101. const result = await pending
  102. assert.equal(result.type, 'image/jpeg')
  103. assert.equal(result.size, 4)
  104. })
  105. for (const { width, height, maxWidth, expected } of [
  106. { width: 1600, height: 900, expected: [640, 360] },
  107. { width: 900, height: 1600, expected: [360, 640] },
  108. { width: 320, height: 180, expected: [320, 180] },
  109. { width: 1600, height: 900, maxWidth: 320, expected: [320, 180] },
  110. ]) {
  111. test(`preserves the ${width} x ${height} ratio within ${maxWidth ?? 640}px and never upscales`, async () => {
  112. const f = fixture({ width, height })
  113. await captureSceneCover(f.editor, { document: f.document, maxWidth })
  114. assert.deepEqual([f.canvas.width, f.canvas.height], expected)
  115. f.assertRestored()
  116. })
  117. }
  118. for (const failure of ['drawError', 'renderError', 'encodeError']) {
  119. test(`restores helper visibility, scalar/array materials and selection after ${failure}`, async () => {
  120. const error = new Error(failure)
  121. const f = fixture({ [failure]: error })
  122. await assert.rejects(captureSceneCover(f.editor, { document: f.document }), actual => actual === error)
  123. f.assertRestored()
  124. assert.equal(f.events.filter(event => event.operation === 'render').length, 2)
  125. if (failure !== 'encodeError') assert.equal(f.events.some(event => event.operation === 'encode'), false)
  126. })
  127. }
  128. test('restores the actual material reference even if it differs from the recorded highlighted clone', async () => {
  129. const f = fixture()
  130. const replacement = { id: 'material-changed-after-selection' }
  131. f.entries[0].child.material = replacement
  132. await captureSceneCover(f.editor, { document: f.document })
  133. assert.equal(f.entries[0].child.material, replacement)
  134. assert.equal(f.events[0].materials[0], f.materials[0].original)
  135. })
  136. test('supports a ready scene without selection helpers', async () => {
  137. const f = fixture()
  138. delete f.editor.transform
  139. delete f.editor.grid
  140. delete f.editor.selectionHighlight
  141. assert.equal((await captureSceneCover(f.editor, { document: f.document })).type, 'image/jpeg')
  142. })
  143. test('deduplicates shared helper objects before recording visibility', async () => {
  144. const f = fixture()
  145. f.editor.transform.getHelper = () => f.editor.selectionHighlight.helper
  146. await captureSceneCover(f.editor, { document: f.document })
  147. f.assertRestored()
  148. })
  149. for (const blob of [null, new Blob([])]) {
  150. test(`rejects ${blob === null ? 'missing' : 'empty'} encoded images without disturbing the editor`, async () => {
  151. const f = fixture({ blob })
  152. await assert.rejects(captureSceneCover(f.editor, { document: f.document }), /编码结果为空/)
  153. f.assertRestored()
  154. })
  155. }
  156. test('refuses an unready or destroyed renderer before changing any scene state', async () => {
  157. for (const change of [
  158. f => { f.editor.destroyed = true },
  159. f => { f.editor.renderer = null },
  160. f => { f.editor.renderer.domElement.width = 0 },
  161. f => { f.editor.renderer.domElement.height = Number.NaN },
  162. f => { f.editor.scene = null },
  163. f => { f.editor.camera = null },
  164. ]) {
  165. const f = fixture()
  166. change(f)
  167. await assert.rejects(captureSceneCover(f.editor, { document: f.document }), /视口尚未准备好/)
  168. f.assertRestored()
  169. assert.equal(f.events.length, 0)
  170. }
  171. })
  172. test('refuses invalid dimensions and unavailable 2D canvas before changing scene state', async () => {
  173. const f = fixture()
  174. await assert.rejects(captureSceneCover(f.editor, { document: f.document, maxWidth: 0 }), /正数像素/)
  175. f.canvas.getContext = () => null
  176. await assert.rejects(captureSceneCover(f.editor, { document: f.document }), /不支持场景封面编码/)
  177. f.assertRestored()
  178. assert.equal(f.events.length, 0)
  179. })