@fps-games/editor 0.2.1-beta.1 → 0.2.1-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. package/dist/agent-bridge/capabilities/ui-regions.d.ts.map +1 -1
  2. package/dist/agent-bridge/capabilities/ui-regions.js +16 -0
  3. package/dist/agent-bridge/capabilities/ui-regions.js.map +1 -1
  4. package/dist/build-info.json +43 -43
  5. package/dist/internal/material-preview.d.ts +22 -0
  6. package/dist/internal/material-preview.d.ts.map +1 -0
  7. package/dist/internal/material-preview.js +186 -0
  8. package/dist/internal/material-preview.js.map +1 -0
  9. package/dist/internal/project-model-preview.d.ts +4 -0
  10. package/dist/internal/project-model-preview.d.ts.map +1 -0
  11. package/dist/internal/project-model-preview.js +359 -0
  12. package/dist/internal/project-model-preview.js.map +1 -0
  13. package/dist/local-editor-agent-context.d.ts +12 -1
  14. package/dist/local-editor-agent-context.d.ts.map +1 -1
  15. package/dist/local-editor-agent-context.js +47 -0
  16. package/dist/local-editor-agent-context.js.map +1 -1
  17. package/dist/local-editor-harness.d.ts +15 -2
  18. package/dist/local-editor-harness.d.ts.map +1 -1
  19. package/dist/local-editor-harness.js +310 -21
  20. package/dist/local-editor-harness.js.map +1 -1
  21. package/dist/local-editor-tools.d.ts +1 -0
  22. package/dist/local-editor-tools.d.ts.map +1 -1
  23. package/dist/local-editor-tools.js +3 -0
  24. package/dist/local-editor-tools.js.map +1 -1
  25. package/dist/playable-local-editor-host.d.ts +4 -0
  26. package/dist/playable-local-editor-host.d.ts.map +1 -1
  27. package/dist/playable-local-editor-host.js +27 -0
  28. package/dist/playable-local-editor-host.js.map +1 -1
  29. package/dist/playable-product-scene-capabilities.d.ts.map +1 -1
  30. package/dist/playable-product-scene-capabilities.js +9 -3
  31. package/dist/playable-product-scene-capabilities.js.map +1 -1
  32. package/dist/playable-standard-project.d.ts.map +1 -1
  33. package/dist/playable-standard-project.js +5 -2
  34. package/dist/playable-standard-project.js.map +1 -1
  35. package/node_modules/@fps-games/babylon-renderer/package.json +7 -7
  36. package/node_modules/@fps-games/editor-babylon/dist/marker-overlay.js +24 -0
  37. package/node_modules/@fps-games/editor-babylon/dist/marker-overlay.js.map +1 -1
  38. package/node_modules/@fps-games/editor-babylon/dist/projection.d.ts +10 -1
  39. package/node_modules/@fps-games/editor-babylon/dist/projection.d.ts.map +1 -1
  40. package/node_modules/@fps-games/editor-babylon/dist/projection.js +96 -1
  41. package/node_modules/@fps-games/editor-babylon/dist/projection.js.map +1 -1
  42. package/node_modules/@fps-games/editor-babylon/dist/scene-view-spatial-overlay-controller.d.ts.map +1 -1
  43. package/node_modules/@fps-games/editor-babylon/dist/scene-view-spatial-overlay-controller.js +85 -22
  44. package/node_modules/@fps-games/editor-babylon/dist/scene-view-spatial-overlay-controller.js.map +1 -1
  45. package/node_modules/@fps-games/editor-babylon/dist/selection-presentation-controller.d.ts +2 -0
  46. package/node_modules/@fps-games/editor-babylon/dist/selection-presentation-controller.d.ts.map +1 -1
  47. package/node_modules/@fps-games/editor-babylon/dist/selection-presentation-controller.js +150 -4
  48. package/node_modules/@fps-games/editor-babylon/dist/selection-presentation-controller.js.map +1 -1
  49. package/node_modules/@fps-games/editor-babylon/dist/transform-gizmo-controller.d.ts +1 -0
  50. package/node_modules/@fps-games/editor-babylon/dist/transform-gizmo-controller.d.ts.map +1 -1
  51. package/node_modules/@fps-games/editor-babylon/dist/transform-gizmo-controller.js +249 -15
  52. package/node_modules/@fps-games/editor-babylon/dist/transform-gizmo-controller.js.map +1 -1
  53. package/node_modules/@fps-games/editor-babylon/dist/types.d.ts +1 -1
  54. package/node_modules/@fps-games/editor-babylon/dist/types.d.ts.map +1 -1
  55. package/node_modules/@fps-games/editor-babylon/dist/types.js.map +1 -1
  56. package/node_modules/@fps-games/editor-babylon/package.json +6 -6
  57. package/node_modules/@fps-games/editor-browser/dist/index.d.ts +5 -0
  58. package/node_modules/@fps-games/editor-browser/dist/index.d.ts.map +1 -1
  59. package/node_modules/@fps-games/editor-browser/dist/index.js +3 -0
  60. package/node_modules/@fps-games/editor-browser/dist/index.js.map +1 -1
  61. package/node_modules/@fps-games/editor-browser/dist/local-editor-asset-browser-continuity.d.ts +35 -0
  62. package/node_modules/@fps-games/editor-browser/dist/local-editor-asset-browser-continuity.d.ts.map +1 -0
  63. package/node_modules/@fps-games/editor-browser/dist/local-editor-asset-browser-continuity.js +136 -0
  64. package/node_modules/@fps-games/editor-browser/dist/local-editor-asset-browser-continuity.js.map +1 -0
  65. package/node_modules/@fps-games/editor-browser/dist/local-editor-asset-filesystem.d.ts +46 -0
  66. package/node_modules/@fps-games/editor-browser/dist/local-editor-asset-filesystem.d.ts.map +1 -0
  67. package/node_modules/@fps-games/editor-browser/dist/local-editor-asset-filesystem.js +283 -0
  68. package/node_modules/@fps-games/editor-browser/dist/local-editor-asset-filesystem.js.map +1 -0
  69. package/node_modules/@fps-games/editor-browser/dist/local-editor-preview-cleanup-registry.d.ts +12 -0
  70. package/node_modules/@fps-games/editor-browser/dist/local-editor-preview-cleanup-registry.d.ts.map +1 -0
  71. package/node_modules/@fps-games/editor-browser/dist/local-editor-preview-cleanup-registry.js +67 -0
  72. package/node_modules/@fps-games/editor-browser/dist/local-editor-preview-cleanup-registry.js.map +1 -0
  73. package/node_modules/@fps-games/editor-browser/dist/local-editor-project-file-open.d.ts +6 -0
  74. package/node_modules/@fps-games/editor-browser/dist/local-editor-project-file-open.d.ts.map +1 -0
  75. package/node_modules/@fps-games/editor-browser/dist/local-editor-project-file-open.js +9 -0
  76. package/node_modules/@fps-games/editor-browser/dist/local-editor-project-file-open.js.map +1 -0
  77. package/node_modules/@fps-games/editor-browser/dist/local-editor-project-file-preview.d.ts +7 -0
  78. package/node_modules/@fps-games/editor-browser/dist/local-editor-project-file-preview.d.ts.map +1 -0
  79. package/node_modules/@fps-games/editor-browser/dist/local-editor-project-file-preview.js +20 -0
  80. package/node_modules/@fps-games/editor-browser/dist/local-editor-project-file-preview.js.map +1 -0
  81. package/node_modules/@fps-games/editor-browser/dist/local-editor-project-media-thumbnail-cache.d.ts +9 -0
  82. package/node_modules/@fps-games/editor-browser/dist/local-editor-project-media-thumbnail-cache.d.ts.map +1 -0
  83. package/node_modules/@fps-games/editor-browser/dist/local-editor-project-media-thumbnail-cache.js +219 -0
  84. package/node_modules/@fps-games/editor-browser/dist/local-editor-project-media-thumbnail-cache.js.map +1 -0
  85. package/node_modules/@fps-games/editor-browser/dist/local-editor-tool-presentation.d.ts +14 -0
  86. package/node_modules/@fps-games/editor-browser/dist/local-editor-tool-presentation.d.ts.map +1 -0
  87. package/node_modules/@fps-games/editor-browser/dist/local-editor-tool-presentation.js +42 -0
  88. package/node_modules/@fps-games/editor-browser/dist/local-editor-tool-presentation.js.map +1 -0
  89. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-asset-kind.d.ts.map +1 -1
  90. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-asset-kind.js +2 -0
  91. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-asset-kind.js.map +1 -1
  92. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-console.d.ts +27 -0
  93. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-console.d.ts.map +1 -0
  94. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-console.js +288 -0
  95. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-console.js.map +1 -0
  96. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-icons.d.ts +1 -1
  97. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-icons.d.ts.map +1 -1
  98. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-icons.js +29 -0
  99. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-icons.js.map +1 -1
  100. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.d.ts +20 -1
  101. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.d.ts.map +1 -1
  102. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.js +321 -60
  103. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-panels.js.map +1 -1
  104. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-prefab-stage.d.ts +2 -1
  105. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-prefab-stage.d.ts.map +1 -1
  106. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-prefab-stage.js +4 -2
  107. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-prefab-stage.js.map +1 -1
  108. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-types.d.ts +63 -3
  109. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-types.d.ts.map +1 -1
  110. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-types.js.map +1 -1
  111. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-workbench.d.ts +9 -0
  112. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-workbench.d.ts.map +1 -1
  113. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-workbench.js +88 -16
  114. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui-workbench.js.map +1 -1
  115. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.d.ts +2 -1
  116. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.d.ts.map +1 -1
  117. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.js +1463 -356
  118. package/node_modules/@fps-games/editor-browser/dist/local-editor-ui.js.map +1 -1
  119. package/node_modules/@fps-games/editor-browser/dist/local-editor-viewport-orientation.d.ts +13 -0
  120. package/node_modules/@fps-games/editor-browser/dist/local-editor-viewport-orientation.d.ts.map +1 -0
  121. package/node_modules/@fps-games/editor-browser/dist/local-editor-viewport-orientation.js +15 -0
  122. package/node_modules/@fps-games/editor-browser/dist/local-editor-viewport-orientation.js.map +1 -0
  123. package/node_modules/@fps-games/editor-browser/package.json +7 -7
  124. package/node_modules/@fps-games/editor-core/dist/editor-tools.d.ts +1 -0
  125. package/node_modules/@fps-games/editor-core/dist/editor-tools.d.ts.map +1 -1
  126. package/node_modules/@fps-games/editor-core/dist/editor-tools.js +3 -1
  127. package/node_modules/@fps-games/editor-core/dist/editor-tools.js.map +1 -1
  128. package/node_modules/@fps-games/editor-core/dist/viewport-tools.d.ts +11 -2
  129. package/node_modules/@fps-games/editor-core/dist/viewport-tools.d.ts.map +1 -1
  130. package/node_modules/@fps-games/editor-core/dist/viewport-tools.js +12 -1
  131. package/node_modules/@fps-games/editor-core/dist/viewport-tools.js.map +1 -1
  132. package/node_modules/@fps-games/editor-core/package.json +6 -6
  133. package/node_modules/@fps-games/editor-forge-play/package.json +2 -2
  134. package/node_modules/@fps-games/editor-playable-sdk/dist/asset-library/catalog.d.ts +1 -0
  135. package/node_modules/@fps-games/editor-playable-sdk/dist/asset-library/catalog.d.ts.map +1 -1
  136. package/node_modules/@fps-games/editor-playable-sdk/dist/asset-library/catalog.js +5 -0
  137. package/node_modules/@fps-games/editor-playable-sdk/dist/asset-library/catalog.js.map +1 -1
  138. package/node_modules/@fps-games/editor-playable-sdk/dist/asset-library/index.d.ts +1 -1
  139. package/node_modules/@fps-games/editor-playable-sdk/dist/asset-library/index.d.ts.map +1 -1
  140. package/node_modules/@fps-games/editor-playable-sdk/dist/asset-library/index.js +1 -1
  141. package/node_modules/@fps-games/editor-playable-sdk/dist/asset-library/index.js.map +1 -1
  142. package/node_modules/@fps-games/editor-playable-sdk/dist/authoring-server/handler.d.ts.map +1 -1
  143. package/node_modules/@fps-games/editor-playable-sdk/dist/authoring-server/handler.js +15 -1
  144. package/node_modules/@fps-games/editor-playable-sdk/dist/authoring-server/handler.js.map +1 -1
  145. package/node_modules/@fps-games/editor-playable-sdk/dist/authoring-server/routes.d.ts.map +1 -1
  146. package/node_modules/@fps-games/editor-playable-sdk/dist/authoring-server/routes.js +3 -0
  147. package/node_modules/@fps-games/editor-playable-sdk/dist/authoring-server/routes.js.map +1 -1
  148. package/node_modules/@fps-games/editor-playable-sdk/dist/authoring-server/types.d.ts +6 -1
  149. package/node_modules/@fps-games/editor-playable-sdk/dist/authoring-server/types.d.ts.map +1 -1
  150. package/node_modules/@fps-games/editor-playable-sdk/dist/authoring-server/types.js.map +1 -1
  151. package/node_modules/@fps-games/editor-playable-sdk/dist/index.d.ts +1 -0
  152. package/node_modules/@fps-games/editor-playable-sdk/dist/index.d.ts.map +1 -1
  153. package/node_modules/@fps-games/editor-playable-sdk/dist/index.js +1 -0
  154. package/node_modules/@fps-games/editor-playable-sdk/dist/index.js.map +1 -1
  155. package/node_modules/@fps-games/editor-playable-sdk/dist/product/scene-source.d.ts +4 -0
  156. package/node_modules/@fps-games/editor-playable-sdk/dist/product/scene-source.d.ts.map +1 -1
  157. package/node_modules/@fps-games/editor-playable-sdk/dist/product/scene-source.js +10 -1
  158. package/node_modules/@fps-games/editor-playable-sdk/dist/product/scene-source.js.map +1 -1
  159. package/node_modules/@fps-games/editor-playable-sdk/dist/project-files/index.d.ts +4 -0
  160. package/node_modules/@fps-games/editor-playable-sdk/dist/project-files/index.d.ts.map +1 -0
  161. package/node_modules/@fps-games/editor-playable-sdk/dist/project-files/index.js +4 -0
  162. package/node_modules/@fps-games/editor-playable-sdk/dist/project-files/index.js.map +1 -0
  163. package/node_modules/@fps-games/editor-playable-sdk/dist/project-files/protocol.d.ts +4 -0
  164. package/node_modules/@fps-games/editor-playable-sdk/dist/project-files/protocol.d.ts.map +1 -0
  165. package/node_modules/@fps-games/editor-playable-sdk/dist/project-files/protocol.js +86 -0
  166. package/node_modules/@fps-games/editor-playable-sdk/dist/project-files/protocol.js.map +1 -0
  167. package/node_modules/@fps-games/editor-playable-sdk/dist/project-files/support.d.ts +9 -0
  168. package/node_modules/@fps-games/editor-playable-sdk/dist/project-files/support.d.ts.map +1 -0
  169. package/node_modules/@fps-games/editor-playable-sdk/dist/project-files/support.js +76 -0
  170. package/node_modules/@fps-games/editor-playable-sdk/dist/project-files/support.js.map +1 -0
  171. package/node_modules/@fps-games/editor-playable-sdk/dist/project-files/types.d.ts +34 -0
  172. package/node_modules/@fps-games/editor-playable-sdk/dist/project-files/types.d.ts.map +1 -0
  173. package/node_modules/@fps-games/editor-playable-sdk/dist/project-files/types.js +2 -0
  174. package/node_modules/@fps-games/editor-playable-sdk/dist/project-files/types.js.map +1 -0
  175. package/node_modules/@fps-games/editor-playable-sdk/dist/scene-source/client.d.ts +4 -0
  176. package/node_modules/@fps-games/editor-playable-sdk/dist/scene-source/client.d.ts.map +1 -1
  177. package/node_modules/@fps-games/editor-playable-sdk/dist/scene-source/client.js +40 -0
  178. package/node_modules/@fps-games/editor-playable-sdk/dist/scene-source/client.js.map +1 -1
  179. package/node_modules/@fps-games/editor-playable-sdk/dist/vite/index.d.ts +1 -0
  180. package/node_modules/@fps-games/editor-playable-sdk/dist/vite/index.d.ts.map +1 -1
  181. package/node_modules/@fps-games/editor-playable-sdk/dist/vite/index.js +1 -0
  182. package/node_modules/@fps-games/editor-playable-sdk/dist/vite/index.js.map +1 -1
  183. package/node_modules/@fps-games/editor-playable-sdk/dist/vite/product-authoring-api.d.ts +1 -0
  184. package/node_modules/@fps-games/editor-playable-sdk/dist/vite/product-authoring-api.d.ts.map +1 -1
  185. package/node_modules/@fps-games/editor-playable-sdk/dist/vite/product-authoring-api.js +94 -0
  186. package/node_modules/@fps-games/editor-playable-sdk/dist/vite/product-authoring-api.js.map +1 -1
  187. package/node_modules/@fps-games/editor-playable-sdk/dist/vite/project-file-handle.d.ts +14 -0
  188. package/node_modules/@fps-games/editor-playable-sdk/dist/vite/project-file-handle.d.ts.map +1 -0
  189. package/node_modules/@fps-games/editor-playable-sdk/dist/vite/project-file-handle.js +101 -0
  190. package/node_modules/@fps-games/editor-playable-sdk/dist/vite/project-file-handle.js.map +1 -0
  191. package/node_modules/@fps-games/editor-playable-sdk/dist/vite/project-files.d.ts +23 -0
  192. package/node_modules/@fps-games/editor-playable-sdk/dist/vite/project-files.d.ts.map +1 -0
  193. package/node_modules/@fps-games/editor-playable-sdk/dist/vite/project-files.js +180 -0
  194. package/node_modules/@fps-games/editor-playable-sdk/dist/vite/project-files.js.map +1 -0
  195. package/node_modules/@fps-games/editor-playable-sdk/package.json +6 -6
  196. package/node_modules/@fps-games/editor-protocol/dist/agent.d.ts +23 -3
  197. package/node_modules/@fps-games/editor-protocol/dist/agent.d.ts.map +1 -1
  198. package/node_modules/@fps-games/editor-protocol/dist/agent.js.map +1 -1
  199. package/node_modules/@fps-games/editor-protocol/package.json +2 -2
  200. package/node_modules/@fps-games/editor-tooling/package.json +3 -3
  201. package/node_modules/@fps-games/editor-ui/dist/editor-ui-theme.d.ts.map +1 -1
  202. package/node_modules/@fps-games/editor-ui/dist/editor-ui-theme.js +103 -10
  203. package/node_modules/@fps-games/editor-ui/dist/editor-ui-theme.js.map +1 -1
  204. package/node_modules/@fps-games/editor-ui/package.json +1 -1
  205. package/node_modules/@fps-games/playable-assets/package.json +1 -1
  206. package/node_modules/@fps-games/playable-contracts/package.json +1 -1
  207. package/node_modules/@fps-games/playable-rendering/package.json +2 -2
  208. package/node_modules/@fps-games/playable-runtime/package.json +12 -12
  209. package/node_modules/@fps-games/playable-runtime-data/package.json +4 -4
  210. package/node_modules/@fps-games/playable-scene/package.json +2 -2
  211. package/node_modules/@fps-games/plugin-api/package.json +1 -1
  212. package/node_modules/@fps-games/plugin-assets/dist/version.d.ts +1 -1
  213. package/node_modules/@fps-games/plugin-assets/dist/version.js +1 -1
  214. package/node_modules/@fps-games/plugin-assets/dist/version.js.map +1 -1
  215. package/node_modules/@fps-games/plugin-assets/package.json +5 -5
  216. package/node_modules/@fps-games/plugin-hierarchy/dist/version.d.ts +1 -1
  217. package/node_modules/@fps-games/plugin-hierarchy/dist/version.js +1 -1
  218. package/node_modules/@fps-games/plugin-hierarchy/dist/version.js.map +1 -1
  219. package/node_modules/@fps-games/plugin-hierarchy/package.json +5 -5
  220. package/node_modules/@fps-games/plugin-host/package.json +2 -2
  221. package/node_modules/@fps-games/plugin-markers/dist/version.d.ts +1 -1
  222. package/node_modules/@fps-games/plugin-markers/dist/version.js +1 -1
  223. package/node_modules/@fps-games/plugin-markers/dist/version.js.map +1 -1
  224. package/node_modules/@fps-games/plugin-markers/package.json +5 -5
  225. package/node_modules/@fps-games/plugin-materials/dist/version.d.ts +1 -1
  226. package/node_modules/@fps-games/plugin-materials/dist/version.js +1 -1
  227. package/node_modules/@fps-games/plugin-materials/dist/version.js.map +1 -1
  228. package/node_modules/@fps-games/plugin-materials/package.json +5 -5
  229. package/node_modules/@fps-games/plugin-rendering/dist/version.d.ts +1 -1
  230. package/node_modules/@fps-games/plugin-rendering/dist/version.js +1 -1
  231. package/node_modules/@fps-games/plugin-rendering/dist/version.js.map +1 -1
  232. package/node_modules/@fps-games/plugin-rendering/package.json +5 -5
  233. package/node_modules/@fps-games/plugin-scene/dist/version.d.ts +1 -1
  234. package/node_modules/@fps-games/plugin-scene/dist/version.js +1 -1
  235. package/node_modules/@fps-games/plugin-scene/dist/version.js.map +1 -1
  236. package/node_modules/@fps-games/plugin-scene/package.json +5 -5
  237. package/node_modules/@fps-games/plugin-shadow-map-experiment/dist/version.d.ts +1 -1
  238. package/node_modules/@fps-games/plugin-shadow-map-experiment/dist/version.js +1 -1
  239. package/node_modules/@fps-games/plugin-shadow-map-experiment/dist/version.js.map +1 -1
  240. package/node_modules/@fps-games/plugin-shadow-map-experiment/package.json +5 -5
  241. package/node_modules/@fps-games/plugin-shadows/dist/version.d.ts +1 -1
  242. package/node_modules/@fps-games/plugin-shadows/dist/version.js +1 -1
  243. package/node_modules/@fps-games/plugin-shadows/dist/version.js.map +1 -1
  244. package/node_modules/@fps-games/plugin-shadows/package.json +7 -7
  245. package/package.json +22 -22
@@ -8,15 +8,22 @@ import { createLocalEditorViewportRevealController } from './local-editor-ui-doc
8
8
  import { createLocalEditorModalDialog } from './local-editor-ui-modal-dialog.js';
9
9
  import { parseLocalEditorBrowserInspectorGroupValueClipboardPayload, } from './local-editor-inspector-clipboard.js';
10
10
  import { writeLocalEditorClipboardText } from './local-editor-clipboard.js';
11
+ import { captureLocalEditorAssetBrowserReturnFocus, createLocalEditorAssetBrowserContinuityController, resolveLocalEditorAssetBrowserContentKey, restoreLocalEditorAssetBrowserReturnFocus, } from './local-editor-asset-browser-continuity.js';
12
+ import { canSkipLocalEditorPreviewCleanupPass, createLocalEditorPreviewCleanupRegistry, } from './local-editor-preview-cleanup-registry.js';
11
13
  import { LOCAL_EDITOR_BROWSER_INSPECTOR_VEC3_AXES, resolveLocalEditorBrowserInspectorProportionalScaleValues, resolveLocalEditorBrowserInspectorVectorGroupAxisFromPropertyPath, resolveLocalEditorBrowserInspectorVectorGroupPathFromPropertyPath, } from './local-editor-inspector-groups.js';
12
14
  import { DEFAULT_EDITOR_TRANSFORM_OPERATION_SETTINGS, } from '@fps-games/editor-core';
13
15
  import { createEditorUiLayerHost } from '@fps-games/editor-ui';
16
+ import { isEditorProjectFilePreviewable, isEditorProjectMediaPreviewable, } from '@fps-games/editor-playable-sdk';
17
+ import { findLocalEditorProjectFileSearchMatches, toLocalEditorProjectRelativePath, } from './local-editor-project-file-preview.js';
18
+ import { resolveLocalEditorProjectFileOpenAction } from './local-editor-project-file-open.js';
14
19
  export { createLocalEditorViewportRevealController, isLocalEditorElementVisibleInViewport, resolveLocalEditorViewportRevealScrollTop, revealLocalEditorElementInViewport, } from './local-editor-ui-dock-panel.js';
15
20
  export { canUseHierarchyProviderReparent, createHierarchyEnabledOperationRequest, createLocalEditorHierarchyController, createHierarchyOperationMenuItems, createHierarchyToolbarCreateMenuItems, matchesHierarchyOperationShortcut, } from './local-editor-ui-hierarchy-controller.js';
16
21
  export { createLocalEditorHierarchyViewAdapter, } from './local-editor-ui-hierarchy-view-adapter.js';
17
22
  export { createLocalEditorContextMenuController, } from './local-editor-ui-context-menu.js';
18
23
  export { createLocalEditorTooltipController, } from './local-editor-ui-tooltip.js';
19
24
  import { createLocalEditorWorkbenchInputRouter } from './local-editor-ui-input-router.js';
25
+ import { createLocalEditorConsoleController } from './local-editor-ui-console.js';
26
+ import { synchronizeLocalEditorToolButtons } from './local-editor-tool-presentation.js';
20
27
  import { createLocalEditorPanelRegistry } from './local-editor-ui-panel-registry.js';
21
28
  import { createLocalEditorFormalSurfaceVisibilityController, disposeLocalEditorFormalSurfaces, shouldRenderLocalEditorInspectorPreview, } from './local-editor-ui-formal-surface-visibility.js';
22
29
  import * as LocalEditorShared from './local-editor-ui-shared.js';
@@ -28,13 +35,22 @@ import { createLocalEditorIcon, isLocalEditorIconName } from './local-editor-ui-
28
35
  import { createLocalEditorStatusCenterViewModel, resolveLocalEditorStatusDiagnosticLocatePlan, settleLocalEditorStatusDiagnosticRequest, resolveLocalEditorStatusDiagnostics, shouldCloseLocalEditorStatusCenter, } from './local-editor-ui-status-diagnostics.js';
29
36
  import { formatLocalEditorStatusDiagnosticDetails } from './local-editor-ui-status-format.js';
30
37
  import { resolveLocalEditorBrowserAssetKind, } from './local-editor-ui-asset-kind.js';
38
+ import { createLocalEditorAssetFilesystemSnapshot, resolveLocalEditorAssetFilesystemFolderPath, } from './local-editor-asset-filesystem.js';
39
+ import { createLocalEditorProjectModelThumbnailCache, } from './local-editor-project-media-thumbnail-cache.js';
31
40
  import { createLocalEditorPrefabStageTitle, findLocalEditorPrefabStageStructureItem, resolveLocalEditorRightDockVisibility, resolveLocalEditorPrefabStageWorkbenchLayout, resolveLocalEditorPreviewToolSupport, shouldLocalEditorClosePerformanceMonitor, } from './local-editor-ui-prefab-stage.js';
41
+ import { resolveLocalEditorCoordinateAxisActiveDirection, resolveLocalEditorCoordinateAxisDirection, } from './local-editor-viewport-orientation.js';
32
42
  export { applyLocalEditorBrowserInspectorControlBinding, createLocalEditorBrowserInspectorControlRegistry, formatLocalEditorBrowserInspectorNumberValue, formatLocalEditorBrowserInspectorValue, parseLocalEditorBrowserInspectorNumberValue, resolveLocalEditorBrowserInspectorSectionStatus, resolveLocalEditorBrowserInspectorControlRegistration, shouldShowPrefabModelAddAction, } from './local-editor-ui-panels.js';
33
43
  export { createLocalEditorBrowserInspectorGroupValueClipboardPayload, createLocalEditorBrowserInspectorVectorCopyPayload, parseLocalEditorBrowserInspectorGroupValueClipboardPayload, } from './local-editor-inspector-clipboard.js';
34
44
  export { LOCAL_EDITOR_BROWSER_INSPECTOR_VEC3_AXES, isLocalEditorBrowserInspectorTransformVectorGroupPath, resolveLocalEditorBrowserInspectorVectorGroupAxisFromPropertyPath, resolveLocalEditorBrowserInspectorVectorGroupPathFromPropertyPath, resolveLocalEditorBrowserInspectorVectorGroupPathFromProperties, } from './local-editor-inspector-groups.js';
45
+ export { findLocalEditorProjectFileSearchMatches, toLocalEditorProjectRelativePath, } from './local-editor-project-file-preview.js';
35
46
  export { DEFAULT_LOCAL_EDITOR_THEME, LOCAL_EDITOR_THEME_CLASS, applyLocalEditorTheme, normalizeLocalEditorThemeName, } from './local-editor-ui-theme.js';
36
47
  export { LOCAL_EDITOR_ICON_NAMES, createLocalEditorIcon, isLocalEditorIconName, resolveLocalEditorIconName, } from './local-editor-ui-icons.js';
37
48
  const PENDING_NUMERIC_PROPERTY_INPUT = Symbol('pending-numeric-property-input');
49
+ function syncLocalEditorInspectorPendingState(panel, pending) {
50
+ panel.dataset.editorInspectorPending = String(pending);
51
+ panel.setAttribute('aria-busy', String(pending));
52
+ panel.toggleAttribute('inert', pending);
53
+ }
38
54
  function readInspectorInputValue(input, source) {
39
55
  const control = input.dataset.serializedControl;
40
56
  const valueType = input.dataset.serializedValueType;
@@ -105,28 +121,6 @@ const PLACEMENT_MODE_DESCRIPTIONS = {
105
121
  ground: '放置到 XZ 地面',
106
122
  surface: '放置到场景表面',
107
123
  };
108
- const VIEW_PRESET_ICONS = {
109
- perspective: 'view-perspective',
110
- top: 'view-top',
111
- bottom: 'view-top',
112
- front: 'view-front',
113
- back: 'view-front',
114
- left: 'view-right',
115
- right: 'view-right',
116
- iso: 'view-perspective',
117
- threeQuarterTop: 'view-perspective',
118
- };
119
- const VIEW_PRESET_TOOLTIPS = {
120
- perspective: '透视视图',
121
- top: '顶视图 · 自动切换正交',
122
- bottom: '底视图 · 自动切换正交',
123
- front: '前视图 · 自动切换正交',
124
- back: '后视图 · 自动切换正交',
125
- left: '左视图 · 自动切换正交',
126
- right: '右视图 · 自动切换正交',
127
- iso: '等距视图 · 自动切换正交',
128
- threeQuarterTop: '斜 45° 俯视图',
129
- };
130
124
  const VIEWPORT_OVERLAY_TOGGLE_LABELS = {
131
125
  bounds: '包围盒',
132
126
  dimensions: '尺寸摘要',
@@ -137,6 +131,17 @@ const VIEWPORT_PROJECTION_LABELS = {
137
131
  perspective: '透视',
138
132
  orthographic: '正交',
139
133
  };
134
+ const VIEWPORT_VIEW_LABELS = {
135
+ perspective: '透视',
136
+ top: '顶视图',
137
+ bottom: '底视图',
138
+ front: '前视图',
139
+ back: '后视图',
140
+ left: '左视图',
141
+ right: '右视图',
142
+ iso: '等距正交',
143
+ threeQuarterTop: '斜 45° 俯视',
144
+ };
140
145
  const STATIC_SHADOW_BAKE_ACTION_ID = 'bake-static-shadows';
141
146
  const TRANSFORM_ACTION_GROUPS = [
142
147
  {
@@ -1223,96 +1228,6 @@ const PREFAB_IMPORT_STATS_METRICS = [
1223
1228
  format: stats => formatPerformanceInteger(stats?.animationCount ?? null),
1224
1229
  },
1225
1230
  ];
1226
- function createSceneFrameRateOverlay(doc) {
1227
- const root = doc.createElement('div');
1228
- root.classList.add(LOCAL_EDITOR_THEME_CLASS, 'fps-editor-ui-overlay-card');
1229
- root.dataset.editorSceneFrameRateOverlay = 'true';
1230
- root.style.cssText = [
1231
- 'position:absolute',
1232
- 'right:14px',
1233
- 'bottom:134px',
1234
- 'z-index:3',
1235
- 'display:none',
1236
- 'align-items:center',
1237
- 'gap:6px',
1238
- 'width:112px',
1239
- 'height:30px',
1240
- 'padding:0 8px',
1241
- 'box-sizing:border-box',
1242
- 'border:1px solid color-mix(in srgb, var(--fps-editor-border) 78%, transparent)',
1243
- 'border-radius:6px',
1244
- 'background:color-mix(in srgb, var(--fps-editor-chrome) 76%, transparent)',
1245
- 'box-shadow:var(--fps-editor-shadow-panel)',
1246
- 'font-family:var(--fps-editor-font)',
1247
- 'font-size:11px',
1248
- 'font-weight:900',
1249
- 'letter-spacing:0',
1250
- 'color:var(--fps-editor-text-strong)',
1251
- 'pointer-events:none',
1252
- 'backdrop-filter:blur(5px)',
1253
- 'overflow:hidden',
1254
- ].join(';');
1255
- const label = doc.createElement('span');
1256
- label.textContent = 'FPS';
1257
- label.style.cssText = [
1258
- 'color:var(--fps-editor-muted-strong)',
1259
- 'font-size:10px',
1260
- 'line-height:1',
1261
- ].join(';');
1262
- const fpsValue = doc.createElement('span');
1263
- fpsValue.textContent = '--';
1264
- fpsValue.style.cssText = [
1265
- 'width:26px',
1266
- 'flex:0 0 26px',
1267
- 'font-variant-numeric:tabular-nums',
1268
- 'font-size:13px',
1269
- 'line-height:1',
1270
- ].join(';');
1271
- const modeLabel = doc.createElement('span');
1272
- modeLabel.textContent = 'IDLE';
1273
- modeLabel.style.cssText = [
1274
- 'margin-left:auto',
1275
- 'padding:2px 4px',
1276
- 'flex:0 0 auto',
1277
- 'border:1px solid var(--fps-editor-border-soft)',
1278
- 'border-radius:4px',
1279
- 'background:color-mix(in srgb, var(--fps-editor-field) 72%, transparent)',
1280
- 'color:var(--fps-editor-muted)',
1281
- 'font-size:9px',
1282
- 'line-height:1',
1283
- ].join(';');
1284
- root.appendChild(label);
1285
- root.appendChild(fpsValue);
1286
- root.appendChild(modeLabel);
1287
- return { root, fpsValue, modeLabel };
1288
- }
1289
- function renderSceneFrameRateOverlay(overlay, stats) {
1290
- if (!stats) {
1291
- overlay.root.style.display = 'none';
1292
- return;
1293
- }
1294
- overlay.root.style.display = 'flex';
1295
- const fps = Number.isFinite(stats.fps) ? Math.round(stats.fps) : null;
1296
- const displayFps = stats.mode === 'idle' ? 0 : fps;
1297
- overlay.fpsValue.textContent = displayFps == null ? '--' : String(displayFps);
1298
- overlay.modeLabel.textContent = stats.mode === 'continuous' ? 'LIVE' : 'IDLE';
1299
- overlay.modeLabel.style.color = stats.mode === 'continuous'
1300
- ? 'var(--fps-editor-success)'
1301
- : 'var(--fps-editor-muted)';
1302
- overlay.fpsValue.style.color = displayFps == null || stats.mode === 'idle'
1303
- ? 'var(--fps-editor-muted)'
1304
- : displayFps >= 50
1305
- ? 'var(--fps-editor-success)'
1306
- : displayFps >= 30
1307
- ? 'var(--fps-editor-warn)'
1308
- : 'var(--fps-editor-danger-strong)';
1309
- overlay.root.title = [
1310
- `Scene FPS: ${displayFps == null ? '--' : displayFps}`,
1311
- `mode: ${stats.mode}`,
1312
- stats.lastFrameMs == null ? '' : `frame: ${stats.lastFrameMs.toFixed(1)}ms`,
1313
- stats.activeReasons.length ? `active: ${stats.activeReasons.join(', ')}` : '',
1314
- ].filter(Boolean).join(' | ');
1315
- }
1316
1231
  function createPerformanceMonitorOverlay(doc) {
1317
1232
  const root = doc.createElement('section');
1318
1233
  root.classList.add(LOCAL_EDITOR_THEME_CLASS, 'fps-editor-ui-overlay-card');
@@ -1612,154 +1527,211 @@ function isFinitePerformanceNumber(value) {
1612
1527
  }
1613
1528
  function createCoordinateAxesOverlay(doc) {
1614
1529
  const root = doc.createElement('div');
1530
+ root.classList.add(LOCAL_EDITOR_THEME_CLASS);
1615
1531
  root.dataset.editorCoordinateAxesOverlay = 'true';
1616
1532
  root.style.cssText = [
1617
1533
  'position:absolute',
1618
1534
  'right:14px',
1619
1535
  'bottom:14px',
1620
- 'width:112px',
1621
- 'height:112px',
1536
+ 'width:124px',
1537
+ 'height:124px',
1622
1538
  'display:none',
1623
1539
  'z-index:2',
1624
1540
  'box-sizing:border-box',
1625
- 'border:1px solid color-mix(in srgb, var(--fps-editor-border) 78%, transparent)',
1626
- 'border-radius:8px',
1627
- 'background:color-mix(in srgb, var(--fps-editor-chrome) 58%, transparent)',
1628
- 'box-shadow:var(--fps-editor-shadow-panel)',
1541
+ 'background:transparent',
1629
1542
  'pointer-events:none',
1630
- 'overflow:hidden',
1543
+ 'overflow:visible',
1631
1544
  ].join(';');
1632
1545
  const svg = doc.createElementNS('http://www.w3.org/2000/svg', 'svg');
1633
- svg.setAttribute('viewBox', '0 0 112 112');
1634
- svg.setAttribute('aria-hidden', 'true');
1546
+ svg.setAttribute('viewBox', '0 0 124 124');
1547
+ svg.setAttribute('role', 'group');
1548
+ svg.setAttribute('aria-label', '视口方向控制');
1635
1549
  svg.style.cssText = 'display:block;width:100%;height:100%';
1636
1550
  root.appendChild(svg);
1637
1551
  const lineLayer = doc.createElementNS('http://www.w3.org/2000/svg', 'g');
1638
1552
  const labelLayer = doc.createElementNS('http://www.w3.org/2000/svg', 'g');
1639
1553
  svg.appendChild(lineLayer);
1640
1554
  svg.appendChild(labelLayer);
1641
- const axisGroups = new Map();
1555
+ const axisDirectionGroups = new Map();
1556
+ const axisDirectionVisuals = new Map();
1642
1557
  const axisLines = new Map();
1643
- const axisLabels = new Map();
1644
- const axisDiscs = new Map();
1645
1558
  for (const axis of ['x', 'y', 'z']) {
1646
- const group = doc.createElementNS('http://www.w3.org/2000/svg', 'g');
1647
1559
  const line = doc.createElementNS('http://www.w3.org/2000/svg', 'line');
1648
1560
  line.setAttribute('stroke-width', '3.2');
1649
1561
  line.setAttribute('stroke-linecap', 'round');
1650
- line.setAttribute('x1', '56');
1651
- line.setAttribute('y1', '56');
1652
- const disc = doc.createElementNS('http://www.w3.org/2000/svg', 'circle');
1653
- disc.setAttribute('r', '7');
1654
- const label = doc.createElementNS('http://www.w3.org/2000/svg', 'text');
1655
- label.setAttribute('text-anchor', 'middle');
1656
- label.setAttribute('dominant-baseline', 'middle');
1657
- label.setAttribute('dy', '0.04em');
1658
- label.style.cssText = [
1659
- 'fill:#172033',
1660
- 'font-size:10px',
1661
- 'font-weight:900',
1662
- 'font-family:Arial, Helvetica, sans-serif',
1663
- 'letter-spacing:0',
1664
- ].join(';');
1665
- group.appendChild(disc);
1666
- group.appendChild(label);
1562
+ line.setAttribute('x1', '62');
1563
+ line.setAttribute('y1', '62');
1564
+ line.style.filter = 'drop-shadow(0 1px 1.5px rgba(0,0,0,0.88))';
1667
1565
  lineLayer.appendChild(line);
1668
- labelLayer.appendChild(group);
1669
- axisGroups.set(axis, group);
1670
1566
  axisLines.set(axis, line);
1671
- axisLabels.set(axis, label);
1672
- axisDiscs.set(axis, disc);
1567
+ for (const sign of ['positive', 'negative']) {
1568
+ const directionId = `${axis}-${sign}`;
1569
+ const direction = resolveLocalEditorCoordinateAxisDirection(directionId);
1570
+ const group = doc.createElementNS('http://www.w3.org/2000/svg', 'g');
1571
+ group.setAttribute('role', 'button');
1572
+ group.setAttribute('tabindex', '0');
1573
+ group.dataset.coordinateAxisDirection = directionId;
1574
+ group.dataset.viewportViewPreset = direction.preset;
1575
+ group.style.cssText = 'pointer-events:auto;cursor:pointer;outline:none';
1576
+ const title = doc.createElementNS('http://www.w3.org/2000/svg', 'title');
1577
+ title.textContent = `${direction.label} · 点击切换正交视图`;
1578
+ const hit = doc.createElementNS('http://www.w3.org/2000/svg', 'circle');
1579
+ hit.setAttribute('r', '14');
1580
+ hit.setAttribute('fill', 'transparent');
1581
+ const visual = doc.createElementNS('http://www.w3.org/2000/svg', 'circle');
1582
+ visual.setAttribute('r', sign === 'positive' ? '9' : '8.5');
1583
+ visual.style.cssText = 'transition:filter 120ms ease,opacity 120ms ease;filter:drop-shadow(0 1px 2.5px rgba(0,0,0,0.78))';
1584
+ group.append(title, hit, visual);
1585
+ if (sign === 'positive') {
1586
+ const label = doc.createElementNS('http://www.w3.org/2000/svg', 'text');
1587
+ label.setAttribute('text-anchor', 'middle');
1588
+ label.setAttribute('dominant-baseline', 'middle');
1589
+ label.setAttribute('dy', '0.04em');
1590
+ label.style.cssText = [
1591
+ 'fill:#172033',
1592
+ 'font-size:11px',
1593
+ 'font-weight:900',
1594
+ 'font-family:Arial, Helvetica, sans-serif',
1595
+ 'letter-spacing:0',
1596
+ 'pointer-events:none',
1597
+ ].join(';');
1598
+ label.textContent = axis.toUpperCase();
1599
+ group.appendChild(label);
1600
+ }
1601
+ const emphasize = () => {
1602
+ visual.style.filter = 'drop-shadow(0 0 4px rgba(255,255,255,0.78))';
1603
+ };
1604
+ const restore = () => {
1605
+ visual.style.filter = 'drop-shadow(0 1px 2.5px rgba(0,0,0,0.78))';
1606
+ };
1607
+ group.addEventListener('pointerenter', emphasize);
1608
+ group.addEventListener('pointerleave', restore);
1609
+ group.addEventListener('focus', emphasize);
1610
+ group.addEventListener('blur', restore);
1611
+ labelLayer.appendChild(group);
1612
+ axisDirectionGroups.set(directionId, group);
1613
+ axisDirectionVisuals.set(directionId, visual);
1614
+ }
1673
1615
  }
1674
1616
  const centerDot = doc.createElementNS('http://www.w3.org/2000/svg', 'circle');
1675
- centerDot.setAttribute('cx', '56');
1676
- centerDot.setAttribute('cy', '56');
1617
+ centerDot.setAttribute('cx', '62');
1618
+ centerDot.setAttribute('cy', '62');
1677
1619
  centerDot.setAttribute('r', '3.2');
1678
- centerDot.setAttribute('fill', 'color-mix(in srgb, var(--fps-editor-text) 82%, transparent)');
1620
+ centerDot.setAttribute('fill', '#f4f7fb');
1621
+ centerDot.setAttribute('stroke', 'rgba(16,20,26,0.82)');
1622
+ centerDot.setAttribute('stroke-width', '1.5');
1679
1623
  labelLayer.appendChild(centerDot);
1680
- const projectionIcon = doc.createElementNS('http://www.w3.org/2000/svg', 'g');
1681
- projectionIcon.setAttribute('transform', 'translate(8 8)');
1682
- projectionIcon.setAttribute('role', 'button');
1683
- projectionIcon.setAttribute('tabindex', '0');
1684
- projectionIcon.dataset.coordinateAxesProjectionToggle = 'true';
1685
- projectionIcon.style.cssText = 'opacity:0.92;pointer-events:auto;cursor:pointer;outline:none';
1686
- const projectionIconFrame = doc.createElementNS('http://www.w3.org/2000/svg', 'rect');
1687
- projectionIconFrame.setAttribute('x', '0');
1688
- projectionIconFrame.setAttribute('y', '0');
1689
- projectionIconFrame.setAttribute('width', '18');
1690
- projectionIconFrame.setAttribute('height', '18');
1691
- projectionIconFrame.setAttribute('rx', '4');
1692
- projectionIconFrame.setAttribute('fill', 'color-mix(in srgb, var(--fps-editor-chrome) 78%, transparent)');
1693
- projectionIconFrame.setAttribute('stroke', 'color-mix(in srgb, var(--fps-editor-border) 82%, transparent)');
1694
- projectionIconFrame.setAttribute('stroke-width', '1');
1695
- const projectionIconShape = doc.createElementNS('http://www.w3.org/2000/svg', 'path');
1696
- projectionIconShape.setAttribute('fill', 'none');
1697
- projectionIconShape.setAttribute('stroke', 'color-mix(in srgb, var(--fps-editor-accent) 82%, #ffffff)');
1698
- projectionIconShape.setAttribute('stroke-width', '1.6');
1699
- projectionIconShape.setAttribute('stroke-linejoin', 'round');
1700
- projectionIconShape.setAttribute('stroke-linecap', 'round');
1701
- projectionIcon.appendChild(projectionIconFrame);
1702
- projectionIcon.appendChild(projectionIconShape);
1703
- svg.appendChild(projectionIcon);
1624
+ const floatingButtonStyle = [
1625
+ 'position:absolute',
1626
+ 'top:4px',
1627
+ 'width:26px',
1628
+ 'height:26px',
1629
+ 'min-width:26px',
1630
+ 'padding:0',
1631
+ 'border-color:color-mix(in srgb, var(--fps-editor-border) 72%, transparent)',
1632
+ 'background:color-mix(in srgb, var(--fps-editor-chrome) 72%, transparent)',
1633
+ 'box-shadow:0 2px 6px rgba(0,0,0,0.34)',
1634
+ 'backdrop-filter:blur(5px)',
1635
+ 'pointer-events:auto',
1636
+ ].join(';');
1637
+ const projectionButton = createToolbarIconButton(doc, '投影模式', 'projection-perspective', '投影模式:透视', 'toggle');
1638
+ projectionButton.dataset.coordinateAxesProjectionToggle = 'true';
1639
+ projectionButton.style.cssText = `${floatingButtonStyle};left:4px`;
1640
+ root.appendChild(projectionButton);
1641
+ const viewMenuButton = createToolbarIconButton(doc, '更多视角', 'chevron-down', '更多视角');
1642
+ viewMenuButton.dataset.coordinateAxesViewMenu = 'true';
1643
+ viewMenuButton.setAttribute('aria-haspopup', 'menu');
1644
+ viewMenuButton.style.cssText = `${floatingButtonStyle};right:4px`;
1645
+ root.appendChild(viewMenuButton);
1704
1646
  return {
1705
1647
  root,
1706
1648
  lineLayer,
1707
1649
  labelLayer,
1708
- axisGroups,
1650
+ axisDirectionGroups,
1651
+ axisDirectionVisuals,
1709
1652
  axisLines,
1710
- axisLabels,
1711
- axisDiscs,
1712
1653
  centerDot,
1713
- projectionIcon,
1714
- projectionIconShape,
1654
+ projectionButton,
1655
+ viewMenuButton,
1715
1656
  };
1716
1657
  }
1717
- function renderCoordinateAxesOverlay(overlay, state) {
1658
+ function renderCoordinateAxesOverlay(overlay, state, viewPreset, busy) {
1718
1659
  if (!state?.axes?.length) {
1719
1660
  overlay.root.style.display = 'none';
1720
1661
  return;
1721
1662
  }
1722
1663
  overlay.root.style.display = '';
1723
- renderCoordinateAxesProjectionIcon(overlay, state.projectionMode, state.projectionToggleDisabled === true);
1724
- const center = 56;
1725
- const lineLength = 38;
1664
+ const controlsDisabled = state.projectionToggleDisabled === true || busy;
1665
+ renderCoordinateAxesProjectionButton(overlay, state.projectionMode, controlsDisabled);
1666
+ overlay.viewMenuButton.disabled = controlsDisabled;
1667
+ overlay.viewMenuButton.setAttribute('aria-disabled', controlsDisabled ? 'true' : 'false');
1668
+ setToolbarButtonTooltip(overlay.viewMenuButton, controlsDisabled
1669
+ ? `视角预设:${VIEWPORT_VIEW_LABELS[viewPreset]} · 当前不可切换`
1670
+ : `视角预设:${VIEWPORT_VIEW_LABELS[viewPreset]}`);
1671
+ const center = 62;
1672
+ const lineLength = 40;
1673
+ const activeDirectionId = resolveLocalEditorCoordinateAxisActiveDirection(viewPreset);
1726
1674
  const sortedAxes = [...state.axes].sort((left, right) => right.depth - left.depth);
1727
1675
  for (const axis of sortedAxes) {
1728
- const group = overlay.axisGroups.get(axis.id);
1729
1676
  const line = overlay.axisLines.get(axis.id);
1730
- const disc = overlay.axisDiscs.get(axis.id);
1731
- const label = overlay.axisLabels.get(axis.id);
1732
- if (!group || !line || !disc || !label)
1677
+ if (!line)
1733
1678
  continue;
1734
1679
  const length = lineLength * axis.scale;
1735
1680
  const endX = center + axis.x * length;
1736
1681
  const endY = center + axis.y * length;
1682
+ const oppositeX = center - axis.x * length;
1683
+ const oppositeY = center - axis.y * length;
1737
1684
  line.setAttribute('x2', formatSvgNumber(endX));
1738
1685
  line.setAttribute('y2', formatSvgNumber(endY));
1739
1686
  line.setAttribute('stroke', axis.color);
1740
- disc.setAttribute('cx', formatSvgNumber(endX));
1741
- disc.setAttribute('cy', formatSvgNumber(endY));
1742
- disc.setAttribute('fill', axis.color);
1743
- label.setAttribute('x', formatSvgNumber(endX));
1744
- label.setAttribute('y', formatSvgNumber(endY));
1745
- label.textContent = axis.label;
1746
- group.style.opacity = String(0.72 + axis.scale * 0.28);
1687
+ const positiveId = `${axis.id}-positive`;
1688
+ const negativeId = `${axis.id}-negative`;
1689
+ const positiveDirection = resolveLocalEditorCoordinateAxisDirection(positiveId);
1690
+ const negativeDirection = resolveLocalEditorCoordinateAxisDirection(negativeId);
1691
+ const positiveGroup = overlay.axisDirectionGroups.get(positiveId);
1692
+ const negativeGroup = overlay.axisDirectionGroups.get(negativeId);
1693
+ const positiveVisual = overlay.axisDirectionVisuals.get(positiveId);
1694
+ const negativeVisual = overlay.axisDirectionVisuals.get(negativeId);
1695
+ if (!positiveGroup || !negativeGroup || !positiveVisual || !negativeVisual)
1696
+ continue;
1697
+ positiveGroup.setAttribute('transform', `translate(${formatSvgNumber(endX)} ${formatSvgNumber(endY)})`);
1698
+ negativeGroup.setAttribute('transform', `translate(${formatSvgNumber(oppositeX)} ${formatSvgNumber(oppositeY)})`);
1699
+ positiveGroup.setAttribute('aria-label', `${positiveDirection.label} · 点击切换正交视图`);
1700
+ negativeGroup.setAttribute('aria-label', `${negativeDirection.label} · 点击切换正交视图`);
1701
+ positiveGroup.setAttribute('aria-disabled', controlsDisabled ? 'true' : 'false');
1702
+ negativeGroup.setAttribute('aria-disabled', controlsDisabled ? 'true' : 'false');
1703
+ positiveGroup.style.cursor = controlsDisabled ? 'not-allowed' : 'pointer';
1704
+ negativeGroup.style.cursor = controlsDisabled ? 'not-allowed' : 'pointer';
1705
+ positiveGroup.style.opacity = String((controlsDisabled ? 0.42 : 0.72) + axis.scale * (controlsDisabled ? 0.12 : 0.28));
1706
+ negativeGroup.style.opacity = controlsDisabled ? '0.3' : String(0.5 + axis.scale * 0.18);
1707
+ const positiveActive = positiveId === activeDirectionId;
1708
+ const negativeActive = negativeId === activeDirectionId;
1709
+ positiveGroup.setAttribute('aria-pressed', String(positiveActive));
1710
+ negativeGroup.setAttribute('aria-pressed', String(negativeActive));
1711
+ positiveVisual.setAttribute('fill', axis.color);
1712
+ positiveVisual.setAttribute('stroke', positiveActive ? '#ffffff' : 'color-mix(in srgb, var(--fps-editor-chrome) 72%, transparent)');
1713
+ positiveVisual.setAttribute('stroke-width', positiveActive ? '2.4' : '1.2');
1714
+ negativeVisual.setAttribute('fill', negativeActive ? `color-mix(in srgb, ${axis.color} 26%, transparent)` : 'color-mix(in srgb, var(--fps-editor-chrome) 62%, transparent)');
1715
+ negativeVisual.setAttribute('stroke', axis.color);
1716
+ negativeVisual.setAttribute('stroke-width', negativeActive ? '3.8' : '3');
1747
1717
  overlay.lineLayer.appendChild(line);
1748
- overlay.labelLayer.appendChild(group);
1718
+ const farGroup = axis.depth >= 0 ? negativeGroup : positiveGroup;
1719
+ const nearGroup = axis.depth >= 0 ? positiveGroup : negativeGroup;
1720
+ overlay.labelLayer.appendChild(farGroup);
1721
+ overlay.labelLayer.appendChild(nearGroup);
1749
1722
  }
1750
1723
  overlay.labelLayer.appendChild(overlay.centerDot);
1751
1724
  }
1752
- function renderCoordinateAxesProjectionIcon(overlay, mode, disabled) {
1753
- overlay.projectionIcon.dataset.viewportProjectionMode = mode;
1754
- overlay.projectionIcon.setAttribute('aria-label', disabled
1755
- ? `编辑视口投影:${VIEWPORT_PROJECTION_LABELS[mode]},Main Camera 预览中不可切换`
1756
- : `编辑视口投影:${VIEWPORT_PROJECTION_LABELS[mode]},点击切换`);
1757
- overlay.projectionIcon.setAttribute('aria-disabled', disabled ? 'true' : 'false');
1758
- overlay.projectionIcon.style.opacity = disabled ? '0.46' : '0.92';
1759
- overlay.projectionIcon.style.cursor = disabled ? 'not-allowed' : 'pointer';
1760
- overlay.projectionIconShape.setAttribute('d', mode === 'orthographic'
1761
- ? 'M5 6.5H12.5V14H5ZM7.5 4H15V11.5'
1762
- : 'M4.5 14H13.5L10.5 4H7.5ZM7 10.5H11');
1725
+ function renderCoordinateAxesProjectionButton(overlay, mode, disabled) {
1726
+ const nextMode = mode === 'orthographic' ? 'perspective' : 'orthographic';
1727
+ overlay.projectionButton.dataset.viewportProjectionMode = mode;
1728
+ overlay.projectionButton.disabled = disabled;
1729
+ overlay.projectionButton.setAttribute('aria-pressed', mode === 'orthographic' ? 'true' : 'false');
1730
+ setToolbarButtonIcon(overlay.projectionButton.ownerDocument, overlay.projectionButton, mode === 'orthographic' ? 'projection-ortho' : 'projection-perspective');
1731
+ setToolbarButtonTooltip(overlay.projectionButton, disabled
1732
+ ? `投影模式:${VIEWPORT_PROJECTION_LABELS[mode]} · 当前不可切换`
1733
+ : `投影模式:${VIEWPORT_PROJECTION_LABELS[mode]} · 点击切换为${VIEWPORT_PROJECTION_LABELS[nextMode]}`);
1734
+ LocalEditorShared.applyButtonActiveState(overlay.projectionButton, mode === 'orthographic');
1763
1735
  }
1764
1736
  function createSpatialOverlay(doc) {
1765
1737
  const root = doc.createElement('div');
@@ -1876,8 +1848,8 @@ function ensureSpatialOverlayLine(overlay, id) {
1876
1848
  if (existing)
1877
1849
  return existing;
1878
1850
  const line = overlay.root.ownerDocument.createElementNS('http://www.w3.org/2000/svg', 'line');
1879
- line.setAttribute('stroke', 'color-mix(in srgb, var(--fps-editor-drop-target) 82%, white)');
1880
- line.setAttribute('stroke-width', '1.4');
1851
+ line.setAttribute('stroke', 'color-mix(in srgb, var(--fps-editor-text) 82%, white)');
1852
+ line.setAttribute('stroke-width', '1.15');
1881
1853
  line.setAttribute('stroke-linecap', 'round');
1882
1854
  line.setAttribute('vector-effect', 'non-scaling-stroke');
1883
1855
  line.style.filter = 'drop-shadow(0 1px 2px rgba(0,0,0,0.45))';
@@ -2021,7 +1993,7 @@ function formatSvgNumber(value) {
2021
1993
  return Number.isFinite(value) ? value.toFixed(2) : '0';
2022
1994
  }
2023
1995
  function isLocalEditorAssetBrowserTab(value) {
2024
- return value === 'all' || value === 'models' || value === 'prefabs' || value === 'materials' || value === 'textures';
1996
+ return value === 'all' || value === 'models' || value === 'prefabs' || value === 'materials' || value === 'textures' || value === 'audio';
2025
1997
  }
2026
1998
  export function createAssetBrowserMenuItems(state, asset) {
2027
1999
  const assetKind = resolveLocalEditorBrowserAssetKind(asset);
@@ -2031,6 +2003,12 @@ export function createAssetBrowserMenuItems(state, asset) {
2031
2003
  const items = [];
2032
2004
  if (assetKind === 'material') {
2033
2005
  const deleteDisabledReason = getMaterialAssetDeleteDisabledReason(asset);
2006
+ if (asset.preview) {
2007
+ items.push({
2008
+ id: 'asset.preview',
2009
+ label: '预览材质',
2010
+ });
2011
+ }
2034
2012
  items.push({
2035
2013
  id: 'asset.edit-material',
2036
2014
  label: '编辑材质球',
@@ -2226,6 +2204,13 @@ export function resolveAssetBrowserDoubleClickAction(state, asset) {
2226
2204
  function copyTextToClipboard(win, text, onResult) {
2227
2205
  void writeLocalEditorClipboardText(win, text).then(onResult ?? (() => undefined));
2228
2206
  }
2207
+ function formatProjectFileByteSize(size) {
2208
+ if (size < 1024)
2209
+ return `${size} B`;
2210
+ if (size < 1024 * 1024)
2211
+ return `${(size / 1024).toFixed(size < 10 * 1024 ? 1 : 0)} KiB`;
2212
+ return `${(size / (1024 * 1024)).toFixed(1)} MiB`;
2213
+ }
2229
2214
  function copyEditorStatusText(button, win, text) {
2230
2215
  const idleLabel = button.getAttribute('aria-label') ?? '复制诊断';
2231
2216
  copyTextToClipboard(win, text, copied => {
@@ -2420,8 +2405,21 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
2420
2405
  let currentState = null;
2421
2406
  let pendingStatusDiagnosticRequest = null;
2422
2407
  let inspectorFilter = '';
2408
+ let lastRenderedInspectorContentKey = null;
2423
2409
  const lockedInspectorVectorGroups = new Set();
2424
2410
  let assetBrowserTab = 'all';
2411
+ let assetBrowserFolderPath = '/';
2412
+ const assetBrowserExpandedFolderPaths = new Set(['/']);
2413
+ let assetBrowserViewMode = 'grid';
2414
+ let selectedProjectEntryPath = null;
2415
+ const expandAssetBrowserFolderPath = (folderPath) => {
2416
+ assetBrowserExpandedFolderPaths.add('/');
2417
+ let currentPath = '';
2418
+ for (const segment of folderPath.split('/').filter(Boolean)) {
2419
+ currentPath = `${currentPath}/${segment}`;
2420
+ assetBrowserExpandedFolderPaths.add(currentPath);
2421
+ }
2422
+ };
2425
2423
  const hierarchySurface = options.hierarchy?.surface;
2426
2424
  if (!hierarchySurface)
2427
2425
  throw new Error('Hierarchy surface is required by the Local Editor workbench.');
@@ -2501,8 +2499,42 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
2501
2499
  let editorStatusCenterOpen = false;
2502
2500
  let performanceMonitorOpen = false;
2503
2501
  let assetPreviewOpen = false;
2502
+ let assetPreviewReturnFocus = null;
2504
2503
  let previewedAsset = null;
2505
2504
  let assetPreviewMode = 'preview';
2505
+ let projectMediaPreviewSource = null;
2506
+ let projectMediaPreviewRequestedPath = '';
2507
+ let projectMediaPreviewError = '';
2508
+ let projectMediaPreviewRequestId = 0;
2509
+ let uiDisposed = false;
2510
+ let projectModelPreviewCleanup = null;
2511
+ let projectModelPreviewAbortController = null;
2512
+ const projectModelPreviewCleanupRegistry = createLocalEditorPreviewCleanupRegistry({
2513
+ onTerminalFailure(error) {
2514
+ console.error('[fps-editor:model-preview] cleanup failed after bounded retries', error);
2515
+ },
2516
+ });
2517
+ const projectMediaThumbnailCache = new Map();
2518
+ const projectMediaThumbnailAbortControllers = new Set();
2519
+ const projectModelThumbnailCache = createLocalEditorProjectModelThumbnailCache(doc.defaultView);
2520
+ let projectMediaThumbnailGeneration = 0;
2521
+ let projectMediaThumbnailObserver = null;
2522
+ let materialPreviewRequestId = 0;
2523
+ let materialPreviewCleanup = null;
2524
+ const materialPreviewCleanupRegistry = createLocalEditorPreviewCleanupRegistry({
2525
+ onTerminalFailure(error) {
2526
+ console.error('[fps-editor:material-preview] cleanup failed after bounded retries', error);
2527
+ },
2528
+ });
2529
+ let projectFilePreviewOpen = false;
2530
+ let projectFilePreviewReturnFocus = null;
2531
+ let projectFilePreviewRequestId = 0;
2532
+ let projectFilePreviewContent = null;
2533
+ let projectFilePreviewRequestedPath = '';
2534
+ let projectFilePreviewError = '';
2535
+ let projectFilePreviewMatches = [];
2536
+ let projectFilePreviewActiveMatch = -1;
2537
+ let projectFilePreviewSearchRaf = null;
2506
2538
  let assetPickerOpen = false;
2507
2539
  let assetPickerFilter = '';
2508
2540
  let assetPickerInput = null;
@@ -2593,6 +2625,7 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
2593
2625
  const rightDockFormalSurfaceVisibility = createLocalEditorFormalSurfaceVisibilityController({
2594
2626
  disposeHidden() {
2595
2627
  disposeRightDockFormalSurfaces();
2628
+ lastRenderedInspectorContentKey = null;
2596
2629
  },
2597
2630
  rehydrateVisible() {
2598
2631
  disposeRightDockFormalSurfaces();
@@ -2605,6 +2638,7 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
2605
2638
  layerHost: editorUiLayerHost,
2606
2639
  lockedVectorGroups: lockedInspectorVectorGroups,
2607
2640
  });
2641
+ lastRenderedInspectorContentKey = resolveLocalEditorRightDockContentKey(currentState, 'inspector', inspectorFilter);
2608
2642
  }
2609
2643
  else if (activeRightTab === 'rendering') {
2610
2644
  LocalEditorPanels.renderRenderingPanel(doc, renderingPanel, currentState, { layerHost: editorUiLayerHost });
@@ -2630,13 +2664,18 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
2630
2664
  },
2631
2665
  });
2632
2666
  construction.addCleanup('workbench-layout-controller', () => workbenchLayoutController.dispose());
2667
+ const editorConsole = createLocalEditorConsoleController(doc, workbench.consoleDock, {
2668
+ isCollapsed: () => workbenchLayoutController.getLayout().consoleCollapsed,
2669
+ onCollapsedChange: collapsed => workbenchLayoutController.setConsoleCollapsed(collapsed, true),
2670
+ });
2671
+ construction.addCleanup('editor-console', () => editorConsole.dispose());
2633
2672
  const findAssetBrowserScrollViewport = () => (assetPanel.querySelector('[data-editor-asset-browser-scroll-viewport]'));
2634
2673
  const assetBrowserViewportReveal = createLocalEditorViewportRevealController({
2635
2674
  getViewport: findAssetBrowserScrollViewport,
2636
2675
  margin: 4,
2637
2676
  });
2638
- let lastRenderedAssetBrowserTab;
2639
- let lastRenderedBottomDockTab;
2677
+ const assetBrowserContinuity = createLocalEditorAssetBrowserContinuityController(() => assetPanel);
2678
+ let lastRenderedAssetBrowserContentKey = null;
2640
2679
  let lastRenderedSelectedAssetId;
2641
2680
  const sceneToolOverlay = createSceneHeaderToolbar(doc);
2642
2681
  const sceneTitle = doc.createElement('div');
@@ -2677,37 +2716,17 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
2677
2716
  'border-left:1px solid var(--fps-editor-divider)',
2678
2717
  ].join(';');
2679
2718
  const toolButtons = new Map();
2680
- const synchronizeToolButtons = (presentations) => {
2681
- const primaryTools = presentations.filter(tool => tool.kind === 'primary');
2682
- const nextIds = new Set(primaryTools.map(tool => tool.id));
2683
- for (const [toolId, button] of toolButtons) {
2684
- if (nextIds.has(toolId))
2685
- continue;
2686
- button.remove();
2687
- toolButtons.delete(toolId);
2688
- }
2689
- for (const tool of primaryTools) {
2690
- let button = toolButtons.get(tool.id);
2691
- const tooltip = tool.shortcut ? `${tool.label}工具 (${tool.shortcut})` : `${tool.label}工具`;
2692
- if (!button) {
2693
- button = LocalEditorShared.createButton(doc, tool.label, {
2694
- icon: tool.icon && isLocalEditorIconName(tool.icon) ? tool.icon : 'object',
2695
- labelMode: 'icon-only',
2696
- variant: 'toolbar-icon',
2697
- tooltip,
2698
- ariaLabel: tooltip,
2699
- });
2700
- button.dataset.editorToolId = tool.id;
2701
- toolButtons.set(tool.id, button);
2702
- }
2703
- setToolbarButtonTooltip(button, tool.availability.state === 'enabled'
2704
- ? tooltip
2705
- : `${tooltip} · ${tool.availability.reason}`);
2706
- button.style.display = tool.availability.state === 'hidden' ? 'none' : 'inline-flex';
2707
- button.disabled = tool.availability.state !== 'enabled';
2708
- LocalEditorShared.applyButtonActiveState(button, tool.active);
2709
- toolGroup.appendChild(button);
2710
- }
2719
+ const renderToolPresentationState = (state) => {
2720
+ synchronizeLocalEditorToolButtons({
2721
+ document: doc,
2722
+ group: toolGroup,
2723
+ buttons: toolButtons,
2724
+ presentations: state.tools ?? [],
2725
+ });
2726
+ toolGroup.style.display = state.mode === 'editor'
2727
+ && (state.tools ?? []).some(tool => tool.kind === 'primary' && tool.availability.state !== 'hidden')
2728
+ ? 'flex'
2729
+ : 'none';
2711
2730
  };
2712
2731
  const spaceGroup = doc.createElement('div');
2713
2732
  spaceGroup.className = 'fps-editor-ui-toolbar-group';
@@ -3065,16 +3084,6 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
3065
3084
  'padding-left:8px',
3066
3085
  'border-left:1px solid var(--fps-editor-divider)',
3067
3086
  ].join(';');
3068
- const viewportPresetButtons = new Map();
3069
- for (const preset of ['perspective', 'top', 'front', 'right']) {
3070
- const button = createToolbarIconButton(doc, VIEW_PRESET_TOOLTIPS[preset], VIEW_PRESET_ICONS[preset], VIEW_PRESET_TOOLTIPS[preset]);
3071
- button.dataset.viewportViewPreset = preset;
3072
- viewportPresetButtons.set(preset, button);
3073
- viewportToolsGroup.appendChild(button);
3074
- }
3075
- const projectionToggleButton = createToolbarIconButton(doc, '投影模式', 'projection-perspective', '切换投影模式');
3076
- projectionToggleButton.dataset.viewportProjectionToggle = 'true';
3077
- viewportToolsGroup.appendChild(projectionToggleButton);
3078
3087
  const markerVisibilityButton = createToolbarIconButton(doc, '标记视图', 'view-overlay', '隐藏标记与关系线', 'toggle');
3079
3088
  markerVisibilityButton.dataset.viewportMarkerVisibilityToggle = 'true';
3080
3089
  viewportToolsGroup.appendChild(markerVisibilityButton);
@@ -3155,7 +3164,6 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
3155
3164
  'pointer-events:auto',
3156
3165
  ].join(';');
3157
3166
  const coordinateAxesOverlay = createCoordinateAxesOverlay(doc);
3158
- const sceneFrameRateOverlay = createSceneFrameRateOverlay(doc);
3159
3167
  const performanceMonitorOverlay = createPerformanceMonitorOverlay(doc);
3160
3168
  const prefabImportStatsOverlay = createPrefabImportStatsOverlay(doc);
3161
3169
  const prefabStagePreviewLayout = createPrefabStagePreviewLayout(doc);
@@ -3182,7 +3190,6 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
3182
3190
  workbench.sceneHeader.appendChild(sceneToolOverlay);
3183
3191
  workbench.sceneFrame.appendChild(spatialOverlay.root);
3184
3192
  workbench.sceneFrame.appendChild(measurementOverlay.root);
3185
- workbench.sceneFrame.appendChild(sceneFrameRateOverlay.root);
3186
3193
  workbench.sceneFrame.appendChild(performanceMonitorOverlay.root);
3187
3194
  workbench.sceneFrame.appendChild(prefabStagePreviewLayout.root);
3188
3195
  workbench.sceneFrame.appendChild(coordinateAxesOverlay.root);
@@ -3299,6 +3306,112 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
3299
3306
  assetPreviewModal.appendChild(assetPreviewDialog);
3300
3307
  construction.appendRootChild(assetPreviewModal);
3301
3308
  tooltipSurfaces.add(assetPreviewModal);
3309
+ const projectFilePreviewModal = doc.createElement('div');
3310
+ projectFilePreviewModal.classList.add(LOCAL_EDITOR_THEME_CLASS, 'fps-editor-ui-modal-root');
3311
+ projectFilePreviewModal.dataset.editorProjectFilePreviewModal = 'true';
3312
+ projectFilePreviewModal.dataset.editorWorkbenchRegion = 'modal';
3313
+ projectFilePreviewModal.style.cssText = [
3314
+ 'position:fixed',
3315
+ 'inset:0',
3316
+ 'z-index:2147483642',
3317
+ 'display:none',
3318
+ 'align-items:center',
3319
+ 'justify-content:center',
3320
+ 'padding:24px',
3321
+ 'background:rgba(0,0,0,0.5)',
3322
+ 'font-family:var(--fps-editor-font)',
3323
+ 'color:var(--fps-editor-text)',
3324
+ 'pointer-events:auto',
3325
+ ].join(';');
3326
+ const projectFilePreviewDialog = doc.createElement('div');
3327
+ projectFilePreviewDialog.className = 'fps-editor-ui-modal';
3328
+ projectFilePreviewDialog.setAttribute('role', 'dialog');
3329
+ projectFilePreviewDialog.setAttribute('aria-modal', 'true');
3330
+ projectFilePreviewDialog.setAttribute('aria-label', '只读文件预览');
3331
+ projectFilePreviewDialog.style.cssText = [
3332
+ 'width:min(980px,calc(100vw - 48px))',
3333
+ 'height:min(720px,calc(100vh - 48px))',
3334
+ 'min-height:320px',
3335
+ 'display:grid',
3336
+ 'grid-template-rows:auto auto minmax(0,1fr) auto',
3337
+ 'gap:8px',
3338
+ 'padding:12px',
3339
+ 'border:1px solid var(--fps-editor-border)',
3340
+ 'border-radius:4px',
3341
+ 'background:var(--fps-editor-panel)',
3342
+ 'box-shadow:var(--fps-editor-shadow-modal)',
3343
+ 'overflow:hidden',
3344
+ ].join(';');
3345
+ const projectFilePreviewHeader = doc.createElement('div');
3346
+ projectFilePreviewHeader.className = 'fps-editor-ui-modal-header';
3347
+ projectFilePreviewHeader.style.cssText = 'display:flex;align-items:center;justify-content:space-between;gap:12px;min-width:0';
3348
+ const projectFilePreviewHeading = doc.createElement('div');
3349
+ projectFilePreviewHeading.style.cssText = 'display:flex;flex-direction:column;gap:2px;min-width:0';
3350
+ const projectFilePreviewTitle = doc.createElement('h2');
3351
+ projectFilePreviewTitle.className = 'fps-editor-ui-modal-title fps-editor-ui-ellipsis';
3352
+ projectFilePreviewTitle.style.cssText = 'margin:0;font-size:13px;font-weight:800;color:var(--fps-editor-text-strong)';
3353
+ const projectFilePreviewPath = doc.createElement('div');
3354
+ projectFilePreviewPath.className = 'fps-editor-ui-ellipsis';
3355
+ projectFilePreviewPath.style.cssText = 'font-size:11px;color:var(--fps-editor-muted)';
3356
+ projectFilePreviewHeading.append(projectFilePreviewTitle, projectFilePreviewPath);
3357
+ const projectFilePreviewCloseButton = createToolbarIconButton(doc, '关闭', 'close', '关闭文件预览');
3358
+ projectFilePreviewCloseButton.dataset.editorProjectFilePreviewClose = 'true';
3359
+ projectFilePreviewHeader.append(projectFilePreviewHeading, projectFilePreviewCloseButton);
3360
+ const projectFilePreviewSearchBar = doc.createElement('div');
3361
+ projectFilePreviewSearchBar.style.cssText = 'display:grid;grid-template-columns:minmax(160px,1fr) auto auto auto;align-items:center;gap:5px';
3362
+ const projectFilePreviewSearchShell = doc.createElement('div');
3363
+ projectFilePreviewSearchShell.className = 'fps-editor-ui-asset-search';
3364
+ const projectFilePreviewSearchIcon = createLocalEditorIcon(doc, 'search', { size: 15 });
3365
+ projectFilePreviewSearchIcon.classList.add('fps-editor-ui-asset-search-icon');
3366
+ const projectFilePreviewSearch = doc.createElement('input');
3367
+ projectFilePreviewSearch.className = 'fps-editor-ui-search';
3368
+ projectFilePreviewSearch.type = 'search';
3369
+ projectFilePreviewSearch.placeholder = '在文件中搜索';
3370
+ projectFilePreviewSearch.dataset.editorProjectFileSearch = 'true';
3371
+ projectFilePreviewSearch.style.paddingLeft = '28px';
3372
+ projectFilePreviewSearchShell.append(projectFilePreviewSearchIcon, projectFilePreviewSearch);
3373
+ const projectFilePreviewMatchCount = doc.createElement('span');
3374
+ projectFilePreviewMatchCount.dataset.editorProjectFileMatchCount = 'true';
3375
+ projectFilePreviewMatchCount.style.cssText = 'min-width:58px;text-align:center;font-size:11px;color:var(--fps-editor-muted-strong);font-variant-numeric:tabular-nums';
3376
+ const projectFilePreviewPrevious = createToolbarIconButton(doc, '上一个匹配项', 'arrow-up', '上一个匹配项');
3377
+ projectFilePreviewPrevious.dataset.editorProjectFileSearchPrevious = 'true';
3378
+ const projectFilePreviewNext = createToolbarIconButton(doc, '下一个匹配项', 'chevron-down', '下一个匹配项');
3379
+ projectFilePreviewNext.dataset.editorProjectFileSearchNext = 'true';
3380
+ projectFilePreviewSearchBar.append(projectFilePreviewSearchShell, projectFilePreviewMatchCount, projectFilePreviewPrevious, projectFilePreviewNext);
3381
+ const projectFilePreviewViewport = doc.createElement('div');
3382
+ projectFilePreviewViewport.dataset.editorProjectFilePreviewViewport = 'true';
3383
+ projectFilePreviewViewport.style.cssText = [
3384
+ 'min-width:0',
3385
+ 'min-height:0',
3386
+ 'overflow:auto',
3387
+ 'border:1px solid var(--fps-editor-divider)',
3388
+ 'border-radius:3px',
3389
+ 'background:var(--fps-editor-field)',
3390
+ ].join(';');
3391
+ const projectFilePreviewCode = doc.createElement('pre');
3392
+ projectFilePreviewCode.dataset.editorProjectFilePreviewCode = 'true';
3393
+ projectFilePreviewCode.setAttribute('aria-label', '只读文件内容');
3394
+ projectFilePreviewCode.style.cssText = [
3395
+ 'min-width:max-content',
3396
+ 'min-height:100%',
3397
+ 'margin:0',
3398
+ 'padding:12px 14px',
3399
+ 'box-sizing:border-box',
3400
+ 'white-space:pre',
3401
+ 'font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace',
3402
+ 'font-size:12px',
3403
+ 'line-height:1.55',
3404
+ 'tab-size:2',
3405
+ 'color:var(--fps-editor-text)',
3406
+ ].join(';');
3407
+ projectFilePreviewViewport.appendChild(projectFilePreviewCode);
3408
+ const projectFilePreviewMeta = doc.createElement('div');
3409
+ projectFilePreviewMeta.dataset.editorProjectFilePreviewMeta = 'true';
3410
+ projectFilePreviewMeta.style.cssText = 'min-height:16px;font-size:10px;color:var(--fps-editor-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap';
3411
+ projectFilePreviewDialog.append(projectFilePreviewHeader, projectFilePreviewSearchBar, projectFilePreviewViewport, projectFilePreviewMeta);
3412
+ projectFilePreviewModal.appendChild(projectFilePreviewDialog);
3413
+ construction.appendRootChild(projectFilePreviewModal);
3414
+ tooltipSurfaces.add(projectFilePreviewModal);
3302
3415
  const assetPickerModal = doc.createElement('div');
3303
3416
  assetPickerModal.classList.add(LOCAL_EDITOR_THEME_CLASS, 'fps-editor-ui-modal-root', 'fps-editor-ui-asset-modal');
3304
3417
  assetPickerModal.dataset.editorAssetPickerModal = 'true';
@@ -3390,12 +3503,12 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
3390
3503
  applyLocalEditorTheme(workbench.root, activeTheme);
3391
3504
  applyLocalEditorTheme(spatialOverlay.root, activeTheme);
3392
3505
  applyLocalEditorTheme(measurementOverlay.root, activeTheme);
3393
- applyLocalEditorTheme(sceneFrameRateOverlay.root, activeTheme);
3394
3506
  applyLocalEditorTheme(performanceMonitorOverlay.root, activeTheme);
3395
3507
  applyLocalEditorTheme(prefabStagePreviewLayout.root, activeTheme);
3396
3508
  applyLocalEditorTheme(prefabImportStatsOverlay.root, activeTheme);
3397
3509
  applyLocalEditorTheme(shortcutHelpPanel, activeTheme);
3398
3510
  applyLocalEditorTheme(assetPreviewModal, activeTheme);
3511
+ applyLocalEditorTheme(projectFilePreviewModal, activeTheme);
3399
3512
  applyLocalEditorTheme(assetPickerModal, activeTheme);
3400
3513
  applyLocalEditorTheme(boxSelectionOverlay, activeTheme);
3401
3514
  applyLocalEditorTheme(localTestMenu, activeTheme);
@@ -3504,6 +3617,11 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
3504
3617
  pendingStatusDiagnosticRequest = navigationRequest;
3505
3618
  if (locator.assetId) {
3506
3619
  assetBrowserTab = 'all';
3620
+ const asset = currentState?.assets.find(candidate => (candidate.id === locator.assetId || candidate.assetId === locator.assetId));
3621
+ if (asset && currentState) {
3622
+ assetBrowserFolderPath = resolveLocalEditorAssetFilesystemFolderPath(createLocalEditorAssetFilesystemSnapshot(currentState.assets, currentState.projectFiles), asset.id);
3623
+ expandAssetBrowserFolderPath(assetBrowserFolderPath);
3624
+ }
3507
3625
  panelRegistry.setBottomDockTab('assets');
3508
3626
  workbenchLayoutController.setBottomCollapsed(false);
3509
3627
  callbacks.onAssetFilterChange?.('');
@@ -4151,18 +4269,24 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
4151
4269
  enterEditorButton.addEventListener('click', () => callbacks.onEnterEditor?.());
4152
4270
  saveButton.addEventListener('click', () => {
4153
4271
  closeLocalTestMenu();
4154
- if (localTestActionsEnabled)
4272
+ if (localTestActionsEnabled) {
4273
+ editorConsole.append({ severity: 'info', source: 'save', message: '请求保存场景' });
4155
4274
  callbacks.onSaveScene?.();
4275
+ }
4156
4276
  });
4157
4277
  saveAndRunButton.addEventListener('click', () => {
4158
4278
  closeLocalTestMenu();
4159
- if (localTestActionsEnabled)
4279
+ if (localTestActionsEnabled) {
4280
+ editorConsole.append({ severity: 'info', source: 'run', message: '请求保存并运行' });
4160
4281
  callbacks.onSaveAndRunGame?.();
4282
+ }
4161
4283
  });
4162
4284
  discardRunButton.addEventListener('click', () => {
4163
4285
  closeLocalTestMenu();
4164
- if (localTestActionsEnabled)
4286
+ if (localTestActionsEnabled) {
4287
+ editorConsole.append({ severity: 'warning', source: 'run', message: '请求放弃更改并运行' });
4165
4288
  callbacks.onDiscardAndRunGame?.();
4289
+ }
4166
4290
  });
4167
4291
  undoButton.addEventListener('click', () => callbacks.onUndo?.());
4168
4292
  redoButton.addEventListener('click', () => callbacks.onRedo?.());
@@ -4197,31 +4321,99 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
4197
4321
  const nextMode = currentMode === 'orthographic' ? 'perspective' : 'orthographic';
4198
4322
  callbacks.onViewportProjectionModeChange?.(nextMode);
4199
4323
  if (event.type === 'click')
4200
- coordinateAxesOverlay.projectionIcon.blur();
4324
+ coordinateAxesOverlay.projectionButton.blur();
4201
4325
  };
4202
- coordinateAxesOverlay.projectionIcon.addEventListener('pointerdown', (event) => {
4326
+ coordinateAxesOverlay.projectionButton.addEventListener('pointerdown', (event) => {
4203
4327
  event.preventDefault();
4204
4328
  });
4205
- coordinateAxesOverlay.projectionIcon.addEventListener('click', toggleCoordinateAxesProjection);
4206
- coordinateAxesOverlay.projectionIcon.addEventListener('keydown', (event) => {
4329
+ coordinateAxesOverlay.projectionButton.addEventListener('click', toggleCoordinateAxesProjection);
4330
+ coordinateAxesOverlay.projectionButton.addEventListener('keydown', (event) => {
4207
4331
  if (event.key !== 'Enter' && event.key !== ' ')
4208
4332
  return;
4209
4333
  toggleCoordinateAxesProjection(event);
4210
4334
  });
4211
- viewportToolsGroup.addEventListener('click', (event) => {
4212
- const target = event.target instanceof HTMLElement ? event.target : null;
4213
- const preset = target?.closest('[data-viewport-view-preset]')?.dataset.viewportViewPreset;
4214
- if (preset) {
4215
- callbacks.onViewportViewPresetChange?.(preset);
4335
+ const activateCoordinateAxesViewPreset = (preset) => {
4336
+ const axesState = currentState?.coordinateAxes ?? null;
4337
+ if (!axesState || axesState.projectionToggleDisabled || currentState?.busy)
4216
4338
  return;
4217
- }
4218
- const projectionToggle = target?.closest('[data-viewport-projection-toggle]');
4219
- if (projectionToggle) {
4220
- const currentMode = currentState?.viewportTools?.projectionMode ?? 'perspective';
4221
- const nextMode = currentMode === 'orthographic' ? 'perspective' : 'orthographic';
4222
- callbacks.onViewportProjectionModeChange?.(nextMode);
4339
+ callbacks.onViewportViewPresetChange?.(preset);
4340
+ };
4341
+ coordinateAxesOverlay.root.addEventListener('pointerdown', (event) => {
4342
+ if (!(event.target instanceof Element))
4223
4343
  return;
4224
- }
4344
+ if (!event.target.closest('[data-coordinate-axis-direction],[data-coordinate-axes-projection-toggle],[data-coordinate-axes-view-menu]'))
4345
+ return;
4346
+ event.preventDefault();
4347
+ event.stopPropagation();
4348
+ });
4349
+ coordinateAxesOverlay.root.addEventListener('click', (event) => {
4350
+ const target = event.target instanceof Element ? event.target : null;
4351
+ const direction = target?.closest('[data-coordinate-axis-direction]');
4352
+ const preset = direction?.dataset.viewportViewPreset;
4353
+ if (!preset)
4354
+ return;
4355
+ event.preventDefault();
4356
+ event.stopPropagation();
4357
+ activateCoordinateAxesViewPreset(preset);
4358
+ });
4359
+ coordinateAxesOverlay.root.addEventListener('keydown', (event) => {
4360
+ if (event.key !== 'Enter' && event.key !== ' ')
4361
+ return;
4362
+ const target = event.target instanceof Element ? event.target : null;
4363
+ const direction = target?.closest('[data-coordinate-axis-direction]');
4364
+ const preset = direction?.dataset.viewportViewPreset;
4365
+ if (!preset)
4366
+ return;
4367
+ event.preventDefault();
4368
+ event.stopPropagation();
4369
+ activateCoordinateAxesViewPreset(preset);
4370
+ });
4371
+ coordinateAxesOverlay.viewMenuButton.addEventListener('click', (event) => {
4372
+ event.preventDefault();
4373
+ event.stopPropagation();
4374
+ const axesState = currentState?.coordinateAxes ?? null;
4375
+ if (!axesState || axesState.projectionToggleDisabled || currentState?.busy)
4376
+ return;
4377
+ const rect = coordinateAxesOverlay.viewMenuButton.getBoundingClientRect();
4378
+ const activePreset = currentState?.viewportTools?.viewPreset ?? 'perspective';
4379
+ const viewItem = (preset, label) => ({
4380
+ id: `viewport-view:${preset}`,
4381
+ label,
4382
+ ...(preset === activePreset ? { shortcut: '当前' } : {}),
4383
+ });
4384
+ contextMenu.open({
4385
+ x: rect.right,
4386
+ y: rect.bottom + 4,
4387
+ items: [
4388
+ viewItem('perspective', '透视视图'),
4389
+ viewItem('threeQuarterTop', '斜 45° 俯视'),
4390
+ viewItem('iso', '等距正交'),
4391
+ {
4392
+ id: 'viewport-view:fixed',
4393
+ label: '固定正交视图',
4394
+ separatorBefore: true,
4395
+ children: [
4396
+ viewItem('top', '顶视图 (+Y)'),
4397
+ viewItem('bottom', '底视图 (-Y)'),
4398
+ viewItem('front', '前视图 (-Z)'),
4399
+ viewItem('back', '后视图 (+Z)'),
4400
+ viewItem('left', '左视图 (-X)'),
4401
+ viewItem('right', '右视图 (+X)'),
4402
+ ],
4403
+ },
4404
+ ],
4405
+ onAction(item) {
4406
+ const presetId = item.id.startsWith('viewport-view:')
4407
+ ? item.id.slice('viewport-view:'.length)
4408
+ : null;
4409
+ if (presetId && presetId !== 'fixed') {
4410
+ activateCoordinateAxesViewPreset(presetId);
4411
+ }
4412
+ },
4413
+ });
4414
+ });
4415
+ viewportToolsGroup.addEventListener('click', (event) => {
4416
+ const target = event.target instanceof HTMLElement ? event.target : null;
4225
4417
  const markerVisibilityToggle = target?.closest('[data-viewport-marker-visibility-toggle]');
4226
4418
  if (markerVisibilityToggle) {
4227
4419
  const markersVisible = currentState?.viewportTools?.markersVisible ?? true;
@@ -4271,10 +4463,15 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
4271
4463
  if (notify)
4272
4464
  callbacks.onPerformanceMonitorOpenChange?.(performanceMonitorOpen);
4273
4465
  };
4274
- const openAssetPreviewModal = (asset) => {
4466
+ const captureAssetBrowserReturnFocus = (trigger) => {
4467
+ const active = doc.activeElement instanceof HTMLElement ? doc.activeElement : null;
4468
+ return captureLocalEditorAssetBrowserReturnFocus(assetPanel, trigger ?? active);
4469
+ };
4470
+ const openAssetPreviewModal = (asset, trigger) => {
4275
4471
  if (!asset.preview)
4276
4472
  return;
4277
4473
  contextMenu.close();
4474
+ assetPreviewReturnFocus = captureAssetBrowserReturnFocus(trigger);
4278
4475
  previewedAsset = asset;
4279
4476
  assetPreviewMode = 'preview';
4280
4477
  assetPreviewDialog.setAttribute('aria-label', '资产预览');
@@ -4283,10 +4480,11 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
4283
4480
  syncInputModalOpen();
4284
4481
  assetPreviewCloseButton.focus({ preventScroll: true });
4285
4482
  };
4286
- const openAssetMaterialEditorModal = (asset) => {
4483
+ const openAssetMaterialEditorModal = (asset, trigger) => {
4287
4484
  if (!asset.material)
4288
4485
  return;
4289
4486
  contextMenu.close();
4487
+ assetPreviewReturnFocus = captureAssetBrowserReturnFocus(trigger);
4290
4488
  previewedAsset = asset;
4291
4489
  assetPreviewMode = 'material-editor';
4292
4490
  assetPreviewDialog.setAttribute('aria-label', '编辑材质球');
@@ -4295,8 +4493,9 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
4295
4493
  syncInputModalOpen();
4296
4494
  assetPreviewCloseButton.focus({ preventScroll: true });
4297
4495
  };
4298
- const openAssetRenameModal = (asset) => {
4496
+ const openAssetRenameModal = (asset, trigger) => {
4299
4497
  contextMenu.close();
4498
+ assetPreviewReturnFocus = captureAssetBrowserReturnFocus(trigger);
4300
4499
  previewedAsset = asset;
4301
4500
  assetPreviewMode = 'rename';
4302
4501
  assetPreviewDialog.setAttribute('aria-label', '重命名资产');
@@ -4309,14 +4508,451 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
4309
4508
  input?.select();
4310
4509
  }, 0);
4311
4510
  };
4511
+ const disposeProjectModelPreview = () => {
4512
+ projectModelPreviewAbortController?.abort();
4513
+ projectModelPreviewAbortController = null;
4514
+ const cleanup = projectModelPreviewCleanup;
4515
+ projectModelPreviewCleanup = null;
4516
+ projectModelPreviewCleanupRegistry.disposeOwned(cleanup);
4517
+ const failures = projectModelPreviewCleanupRegistry.takeFailures();
4518
+ if (failures.length > 0) {
4519
+ const error = new Error('Project model preview cleanup failed after bounded retries.');
4520
+ error.errors = failures;
4521
+ throw error;
4522
+ }
4523
+ };
4524
+ const resetProjectMediaThumbnailCache = () => {
4525
+ projectMediaThumbnailGeneration += 1;
4526
+ for (const controller of projectMediaThumbnailAbortControllers)
4527
+ controller.abort();
4528
+ projectMediaThumbnailAbortControllers.clear();
4529
+ projectMediaThumbnailCache.clear();
4530
+ projectMediaThumbnailObserver?.disconnect();
4531
+ projectMediaThumbnailObserver = null;
4532
+ };
4533
+ const resolveProjectMediaThumbnail = (browserPath, cacheKey) => {
4534
+ const requestGeneration = projectMediaThumbnailGeneration;
4535
+ const relativePath = toLocalEditorProjectRelativePath(browserPath);
4536
+ const memoryKey = cacheKey || relativePath;
4537
+ const cached = projectMediaThumbnailCache.get(memoryKey);
4538
+ if (cached)
4539
+ return cached;
4540
+ const pending = Promise.resolve(callbacks.onProjectMediaResolve?.(relativePath) ?? null)
4541
+ .then(async (source) => {
4542
+ if (!source)
4543
+ return null;
4544
+ if (source.kind === 'image') {
4545
+ return { alt: source.name, kind: 'image', url: source.url };
4546
+ }
4547
+ if (source.kind !== 'model' || !callbacks.onProjectModelThumbnailResolve)
4548
+ return null;
4549
+ if (requestGeneration !== projectMediaThumbnailGeneration)
4550
+ return null;
4551
+ if (cacheKey) {
4552
+ const cachedUrl = await projectModelThumbnailCache.get(cacheKey);
4553
+ if (cachedUrl) {
4554
+ return { alt: `${source.name} 模型缩略图`, kind: 'model', url: cachedUrl };
4555
+ }
4556
+ }
4557
+ const controller = new AbortController();
4558
+ projectMediaThumbnailAbortControllers.add(controller);
4559
+ let url;
4560
+ try {
4561
+ url = await callbacks.onProjectModelThumbnailResolve(source, controller.signal);
4562
+ }
4563
+ finally {
4564
+ projectMediaThumbnailAbortControllers.delete(controller);
4565
+ }
4566
+ if (controller.signal.aborted || requestGeneration !== projectMediaThumbnailGeneration)
4567
+ return null;
4568
+ if (url && cacheKey)
4569
+ void projectModelThumbnailCache.put(cacheKey, url);
4570
+ return url ? { alt: `${source.name} 模型缩略图`, kind: 'model', url } : null;
4571
+ })
4572
+ .catch(() => null);
4573
+ projectMediaThumbnailCache.set(memoryKey, pending);
4574
+ return pending;
4575
+ };
4576
+ const hydrateProjectMediaThumbnails = () => {
4577
+ if (!callbacks.onProjectMediaResolve)
4578
+ return;
4579
+ const generation = projectMediaThumbnailGeneration;
4580
+ projectMediaThumbnailObserver?.disconnect();
4581
+ projectMediaThumbnailObserver = null;
4582
+ const hydrate = (frame) => {
4583
+ const path = frame.dataset.editorProjectMediaThumbnailPath;
4584
+ if (!path || frame.dataset.thumbnailRequested === 'true')
4585
+ return;
4586
+ const cacheKey = frame.dataset.editorProjectMediaThumbnailCacheKey;
4587
+ frame.dataset.thumbnailRequested = 'true';
4588
+ void resolveProjectMediaThumbnail(path, cacheKey).then((result) => {
4589
+ if (!frame.isConnected || generation !== projectMediaThumbnailGeneration)
4590
+ return;
4591
+ if (!result) {
4592
+ frame.dataset.thumbnailState = 'failed';
4593
+ return;
4594
+ }
4595
+ const image = doc.createElement('img');
4596
+ image.alt = result.alt;
4597
+ image.decoding = 'async';
4598
+ image.draggable = false;
4599
+ if (result.kind === 'image')
4600
+ image.loading = 'lazy';
4601
+ image.addEventListener('load', () => {
4602
+ if (frame.isConnected)
4603
+ frame.dataset.thumbnailState = 'ready';
4604
+ }, { once: true });
4605
+ image.addEventListener('error', () => {
4606
+ image.remove();
4607
+ if (frame.isConnected)
4608
+ frame.dataset.thumbnailState = 'failed';
4609
+ }, { once: true });
4610
+ image.src = result.url;
4611
+ frame.appendChild(image);
4612
+ });
4613
+ };
4614
+ const frames = assetPanel.querySelectorAll('[data-editor-project-media-thumbnail-path]');
4615
+ const Observer = doc.defaultView?.IntersectionObserver;
4616
+ if (!Observer) {
4617
+ for (const frame of frames)
4618
+ hydrate(frame);
4619
+ return;
4620
+ }
4621
+ const viewport = assetPanel.querySelector('[data-editor-asset-browser-scroll-viewport]');
4622
+ projectMediaThumbnailObserver = new Observer((entries, observer) => {
4623
+ for (const entry of entries) {
4624
+ if (!entry.isIntersecting || !(entry.target instanceof HTMLElement))
4625
+ continue;
4626
+ observer.unobserve(entry.target);
4627
+ hydrate(entry.target);
4628
+ }
4629
+ }, { root: viewport, rootMargin: '120px 0px' });
4630
+ for (const frame of frames)
4631
+ projectMediaThumbnailObserver.observe(frame);
4632
+ };
4633
+ const disposeMaterialPreview = () => {
4634
+ materialPreviewRequestId += 1;
4635
+ const cleanup = materialPreviewCleanup;
4636
+ materialPreviewCleanup = null;
4637
+ materialPreviewCleanupRegistry.disposeOwned(cleanup);
4638
+ const failures = materialPreviewCleanupRegistry.takeFailures();
4639
+ if (failures.length > 0) {
4640
+ const error = new Error('Material preview cleanup failed after bounded retries.');
4641
+ error.errors = failures;
4642
+ throw error;
4643
+ }
4644
+ };
4645
+ const createMaterialPreviewStage = (asset) => {
4646
+ const preview = asset.preview;
4647
+ if (preview?.kind !== 'material-sphere' || !callbacks.onMaterialPreviewMount) {
4648
+ return LocalEditorPanels.createAssetPreviewElement(doc, asset, 'modal');
4649
+ }
4650
+ disposeMaterialPreview();
4651
+ const requestId = materialPreviewRequestId;
4652
+ const stage = doc.createElement('div');
4653
+ stage.dataset.editorMaterialPreviewStage = 'true';
4654
+ stage.dataset.previewState = 'loading';
4655
+ stage.style.cssText = [
4656
+ 'position:relative',
4657
+ 'width:100%',
4658
+ 'aspect-ratio:16 / 10',
4659
+ 'min-height:180px',
4660
+ 'overflow:hidden',
4661
+ 'border:1px solid var(--fps-editor-border)',
4662
+ 'border-radius:4px',
4663
+ 'background:var(--fps-editor-field)',
4664
+ 'box-sizing:border-box',
4665
+ ].join(';');
4666
+ const fallback = LocalEditorPanels.createAssetPreviewElement(doc, asset, 'modal');
4667
+ fallback.dataset.editorMaterialPreviewFallback = 'true';
4668
+ fallback.style.position = 'absolute';
4669
+ fallback.style.left = '50%';
4670
+ fallback.style.top = '50%';
4671
+ fallback.style.transform = 'translate(-50%,-50%)';
4672
+ fallback.style.border = '0';
4673
+ fallback.style.background = 'transparent';
4674
+ const canvas = doc.createElement('canvas');
4675
+ canvas.dataset.editorMaterialPreviewCanvas = 'true';
4676
+ canvas.setAttribute('aria-label', `${asset.label} 材质球三维预览`);
4677
+ canvas.title = '材质球预览';
4678
+ canvas.style.cssText = [
4679
+ 'position:absolute',
4680
+ 'inset:0',
4681
+ 'display:block',
4682
+ 'width:100%',
4683
+ 'height:100%',
4684
+ 'opacity:0',
4685
+ 'cursor:grab',
4686
+ 'touch-action:none',
4687
+ 'outline:none',
4688
+ ].join(';');
4689
+ stage.append(fallback, canvas);
4690
+ const source = {
4691
+ id: asset.material?.id ?? asset.assetId ?? asset.id,
4692
+ name: asset.material?.name || asset.label,
4693
+ materialKind: asset.material?.materialKind,
4694
+ preview,
4695
+ };
4696
+ void Promise.resolve().then(() => callbacks.onMaterialPreviewMount(source, canvas)).then((cleanup) => {
4697
+ if (!assetPreviewOpen || requestId !== materialPreviewRequestId || !stage.isConnected) {
4698
+ materialPreviewCleanupRegistry.disposeOwned(cleanup);
4699
+ return undefined;
4700
+ }
4701
+ materialPreviewCleanupRegistry.own(cleanup);
4702
+ materialPreviewCleanup = cleanup;
4703
+ stage.dataset.previewState = 'ready';
4704
+ fallback.style.display = 'none';
4705
+ canvas.style.opacity = '1';
4706
+ return undefined;
4707
+ }).catch(() => {
4708
+ if (requestId !== materialPreviewRequestId || !stage.isConnected)
4709
+ return;
4710
+ stage.dataset.previewState = 'fallback';
4711
+ canvas.style.display = 'none';
4712
+ fallback.style.display = 'flex';
4713
+ });
4714
+ return stage;
4715
+ };
4716
+ const openProjectMediaPreview = async (browserPath, trigger) => {
4717
+ const relativePath = toLocalEditorProjectRelativePath(browserPath);
4718
+ if (!callbacks.onProjectMediaResolve || !isEditorProjectMediaPreviewable(relativePath))
4719
+ return;
4720
+ contextMenu.close();
4721
+ closeProjectFilePreview();
4722
+ disposeProjectModelPreview();
4723
+ assetPreviewReturnFocus = captureAssetBrowserReturnFocus(trigger);
4724
+ const requestId = ++projectMediaPreviewRequestId;
4725
+ projectMediaPreviewSource = null;
4726
+ projectMediaPreviewRequestedPath = relativePath;
4727
+ projectMediaPreviewError = '';
4728
+ previewedAsset = null;
4729
+ assetPreviewMode = 'project-media';
4730
+ assetPreviewDialog.setAttribute('aria-label', '媒体文件预览');
4731
+ assetPreviewOpen = true;
4732
+ renderAssetPreviewModal();
4733
+ syncInputModalOpen();
4734
+ try {
4735
+ const source = await callbacks.onProjectMediaResolve(relativePath);
4736
+ if (!assetPreviewOpen || assetPreviewMode !== 'project-media' || requestId !== projectMediaPreviewRequestId)
4737
+ return;
4738
+ if (!source)
4739
+ throw new Error('project_file_media_unsupported');
4740
+ projectMediaPreviewSource = source;
4741
+ renderAssetPreviewModal();
4742
+ if (source.kind === 'model') {
4743
+ const canvas = assetPreviewBody.querySelector('[data-editor-project-model-preview]');
4744
+ if (!canvas || !callbacks.onProjectModelPreviewMount)
4745
+ throw new Error('project_model_preview_unavailable');
4746
+ const controller = new AbortController();
4747
+ projectModelPreviewAbortController = controller;
4748
+ const cleanup = await callbacks.onProjectModelPreviewMount(source, canvas, controller.signal);
4749
+ if (uiDisposed
4750
+ || !assetPreviewDialog.isConnected
4751
+ || !assetPreviewOpen
4752
+ || assetPreviewMode !== 'project-media'
4753
+ || requestId !== projectMediaPreviewRequestId) {
4754
+ controller.abort();
4755
+ if (projectModelPreviewAbortController === controller)
4756
+ projectModelPreviewAbortController = null;
4757
+ projectModelPreviewCleanupRegistry.disposeOwned(cleanup);
4758
+ return;
4759
+ }
4760
+ projectModelPreviewCleanupRegistry.own(cleanup);
4761
+ projectModelPreviewCleanup = cleanup;
4762
+ }
4763
+ assetPreviewCloseButton.focus({ preventScroll: true });
4764
+ }
4765
+ catch (error) {
4766
+ if (uiDisposed
4767
+ || !assetPreviewDialog.isConnected
4768
+ || !assetPreviewOpen
4769
+ || assetPreviewMode !== 'project-media'
4770
+ || requestId !== projectMediaPreviewRequestId)
4771
+ return;
4772
+ disposeProjectModelPreview();
4773
+ projectMediaPreviewError = error instanceof Error ? error.message : String(error);
4774
+ renderAssetPreviewModal();
4775
+ assetPreviewCloseButton.focus({ preventScroll: true });
4776
+ }
4777
+ };
4312
4778
  const closeAssetPreviewModal = () => {
4313
4779
  if (!assetPreviewOpen)
4314
4780
  return;
4781
+ const returnFocus = assetPreviewReturnFocus;
4782
+ assetPreviewReturnFocus = null;
4783
+ projectMediaPreviewRequestId += 1;
4784
+ disposeProjectModelPreview();
4785
+ disposeMaterialPreview();
4315
4786
  assetPreviewOpen = false;
4316
4787
  previewedAsset = null;
4788
+ projectMediaPreviewSource = null;
4789
+ projectMediaPreviewRequestedPath = '';
4790
+ projectMediaPreviewError = '';
4317
4791
  assetPreviewMode = 'preview';
4318
4792
  renderAssetPreviewModal();
4319
4793
  syncInputModalOpen();
4794
+ restoreLocalEditorAssetBrowserReturnFocus(assetPanel, returnFocus);
4795
+ };
4796
+ const renderProjectFilePreview = () => {
4797
+ if (projectFilePreviewSearchRaf != null) {
4798
+ doc.defaultView?.cancelAnimationFrame(projectFilePreviewSearchRaf);
4799
+ projectFilePreviewSearchRaf = null;
4800
+ }
4801
+ projectFilePreviewModal.style.display = projectFilePreviewOpen ? 'flex' : 'none';
4802
+ if (!projectFilePreviewOpen)
4803
+ return;
4804
+ const content = projectFilePreviewContent;
4805
+ const displayPath = content?.path ?? projectFilePreviewRequestedPath;
4806
+ const displayPathSegments = displayPath.split('/');
4807
+ projectFilePreviewTitle.textContent = content?.name
4808
+ ?? displayPathSegments[displayPathSegments.length - 1]
4809
+ ?? '文件预览';
4810
+ projectFilePreviewPath.textContent = displayPath;
4811
+ projectFilePreviewPath.title = displayPath;
4812
+ projectFilePreviewSearch.disabled = !content;
4813
+ projectFilePreviewPrevious.disabled = projectFilePreviewMatches.length === 0;
4814
+ projectFilePreviewNext.disabled = projectFilePreviewMatches.length === 0;
4815
+ projectFilePreviewMatchCount.textContent = projectFilePreviewMatches.length > 0
4816
+ ? `${projectFilePreviewActiveMatch + 1} / ${projectFilePreviewMatches.length}`
4817
+ : projectFilePreviewSearch.value.trim() ? '0 / 0' : '0';
4818
+ projectFilePreviewMeta.textContent = projectFilePreviewError
4819
+ ? projectFilePreviewError
4820
+ : content
4821
+ ? [
4822
+ content.language,
4823
+ formatProjectFileByteSize(content.size),
4824
+ content.truncated ? '文件内容已截断' : '',
4825
+ '只读',
4826
+ ].filter(Boolean).join(' · ')
4827
+ : '正在读取文件...';
4828
+ while (projectFilePreviewCode.firstChild)
4829
+ projectFilePreviewCode.removeChild(projectFilePreviewCode.firstChild);
4830
+ if (projectFilePreviewError) {
4831
+ projectFilePreviewCode.textContent = `无法查看文件:${projectFilePreviewError}`;
4832
+ return;
4833
+ }
4834
+ if (!content) {
4835
+ projectFilePreviewCode.textContent = '正在读取文件...';
4836
+ return;
4837
+ }
4838
+ if (projectFilePreviewMatches.length === 0) {
4839
+ projectFilePreviewCode.textContent = content.text;
4840
+ return;
4841
+ }
4842
+ let cursor = 0;
4843
+ for (const [index, match] of projectFilePreviewMatches.entries()) {
4844
+ if (match.start > cursor) {
4845
+ projectFilePreviewCode.appendChild(doc.createTextNode(content.text.slice(cursor, match.start)));
4846
+ }
4847
+ const mark = doc.createElement('mark');
4848
+ mark.dataset.editorProjectFileSearchMatch = String(index);
4849
+ mark.dataset.active = index === projectFilePreviewActiveMatch ? 'true' : 'false';
4850
+ mark.textContent = content.text.slice(match.start, match.end);
4851
+ mark.style.cssText = index === projectFilePreviewActiveMatch
4852
+ ? 'background:var(--fps-editor-accent);color:var(--fps-editor-panel);outline:1px solid var(--fps-editor-accent-strong)'
4853
+ : 'background:var(--fps-editor-warn-soft);color:var(--fps-editor-text-strong);outline:1px solid var(--fps-editor-warn-border)';
4854
+ projectFilePreviewCode.appendChild(mark);
4855
+ cursor = match.end;
4856
+ }
4857
+ if (cursor < content.text.length) {
4858
+ projectFilePreviewCode.appendChild(doc.createTextNode(content.text.slice(cursor)));
4859
+ }
4860
+ const active = projectFilePreviewCode.querySelector('[data-editor-project-file-search-match][data-active="true"]');
4861
+ projectFilePreviewSearchRaf = doc.defaultView?.requestAnimationFrame(() => {
4862
+ projectFilePreviewSearchRaf = null;
4863
+ if (projectFilePreviewOpen && active?.isConnected) {
4864
+ active.scrollIntoView({ block: 'center', inline: 'nearest' });
4865
+ }
4866
+ }) ?? null;
4867
+ };
4868
+ const updateProjectFilePreviewSearch = (resetActive) => {
4869
+ projectFilePreviewMatches = projectFilePreviewContent
4870
+ ? findLocalEditorProjectFileSearchMatches(projectFilePreviewContent.text, projectFilePreviewSearch.value)
4871
+ : [];
4872
+ if (resetActive)
4873
+ projectFilePreviewActiveMatch = projectFilePreviewMatches.length > 0 ? 0 : -1;
4874
+ else if (projectFilePreviewMatches.length === 0)
4875
+ projectFilePreviewActiveMatch = -1;
4876
+ else
4877
+ projectFilePreviewActiveMatch = Math.min(Math.max(0, projectFilePreviewActiveMatch), projectFilePreviewMatches.length - 1);
4878
+ renderProjectFilePreview();
4879
+ };
4880
+ const moveProjectFilePreviewMatch = (delta) => {
4881
+ if (projectFilePreviewMatches.length === 0)
4882
+ return;
4883
+ projectFilePreviewActiveMatch = (projectFilePreviewActiveMatch + delta + projectFilePreviewMatches.length) % projectFilePreviewMatches.length;
4884
+ renderProjectFilePreview();
4885
+ };
4886
+ const closeProjectFilePreview = () => {
4887
+ if (!projectFilePreviewOpen)
4888
+ return;
4889
+ if (projectFilePreviewSearchRaf != null) {
4890
+ doc.defaultView?.cancelAnimationFrame(projectFilePreviewSearchRaf);
4891
+ projectFilePreviewSearchRaf = null;
4892
+ }
4893
+ const returnFocus = projectFilePreviewReturnFocus;
4894
+ projectFilePreviewReturnFocus = null;
4895
+ projectFilePreviewRequestId += 1;
4896
+ projectFilePreviewOpen = false;
4897
+ projectFilePreviewContent = null;
4898
+ projectFilePreviewRequestedPath = '';
4899
+ projectFilePreviewError = '';
4900
+ projectFilePreviewMatches = [];
4901
+ projectFilePreviewActiveMatch = -1;
4902
+ projectFilePreviewSearch.value = '';
4903
+ renderProjectFilePreview();
4904
+ syncInputModalOpen();
4905
+ restoreLocalEditorAssetBrowserReturnFocus(assetPanel, returnFocus);
4906
+ };
4907
+ const openProjectFilePreview = async (browserPath, trigger) => {
4908
+ const relativePath = toLocalEditorProjectRelativePath(browserPath);
4909
+ if (!callbacks.onProjectFileRead || !isEditorProjectFilePreviewable(relativePath))
4910
+ return;
4911
+ contextMenu.close();
4912
+ if (assetPreviewOpen)
4913
+ closeAssetPreviewModal();
4914
+ projectFilePreviewReturnFocus = captureAssetBrowserReturnFocus(trigger);
4915
+ const requestId = ++projectFilePreviewRequestId;
4916
+ projectFilePreviewOpen = true;
4917
+ projectFilePreviewContent = null;
4918
+ projectFilePreviewRequestedPath = relativePath;
4919
+ projectFilePreviewError = '';
4920
+ projectFilePreviewMatches = [];
4921
+ projectFilePreviewActiveMatch = -1;
4922
+ projectFilePreviewSearch.value = '';
4923
+ renderProjectFilePreview();
4924
+ syncInputModalOpen();
4925
+ try {
4926
+ const content = await callbacks.onProjectFileRead(relativePath);
4927
+ if (!projectFilePreviewOpen || requestId !== projectFilePreviewRequestId)
4928
+ return;
4929
+ projectFilePreviewContent = content;
4930
+ updateProjectFilePreviewSearch(true);
4931
+ projectFilePreviewSearch.focus({ preventScroll: true });
4932
+ }
4933
+ catch (error) {
4934
+ if (!projectFilePreviewOpen || requestId !== projectFilePreviewRequestId)
4935
+ return;
4936
+ projectFilePreviewError = error instanceof Error ? error.message : String(error);
4937
+ renderProjectFilePreview();
4938
+ projectFilePreviewCloseButton.focus({ preventScroll: true });
4939
+ }
4940
+ };
4941
+ const openProjectBrowserFile = (browserPath, trigger) => {
4942
+ const relativePath = toLocalEditorProjectRelativePath(browserPath);
4943
+ const action = resolveLocalEditorProjectFileOpenAction(relativePath, {
4944
+ mediaPreview: !!callbacks.onProjectMediaResolve,
4945
+ textPreview: !!callbacks.onProjectFileRead,
4946
+ });
4947
+ if (action === 'media-preview') {
4948
+ void openProjectMediaPreview(browserPath, trigger);
4949
+ return true;
4950
+ }
4951
+ if (action === 'text-preview') {
4952
+ void openProjectFilePreview(browserPath, trigger);
4953
+ return true;
4954
+ }
4955
+ return false;
4320
4956
  };
4321
4957
  const openInspectorAssetPicker = (input, property) => {
4322
4958
  contextMenu.close();
@@ -4328,6 +4964,7 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
4328
4964
  assetPickerFilter = '';
4329
4965
  assetPickerOpen = true;
4330
4966
  closeAssetPreviewModal();
4967
+ closeProjectFilePreview();
4331
4968
  renderAssetPickerModal();
4332
4969
  syncInputModalOpen();
4333
4970
  assetPickerSearch.focus({ preventScroll: true });
@@ -4372,7 +5009,7 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
4372
5009
  syncInputModalOpen();
4373
5010
  };
4374
5011
  function syncInputModalOpen() {
4375
- inputRouter.setModalOpen(currentState?.mode === 'editor' && (helpOpen || assetPreviewOpen || assetPickerOpen));
5012
+ inputRouter.setModalOpen(currentState?.mode === 'editor' && (helpOpen || assetPreviewOpen || projectFilePreviewOpen || assetPickerOpen));
4376
5013
  }
4377
5014
  function resolvePreviewedAssetFromState() {
4378
5015
  if (!previewedAsset)
@@ -4385,11 +5022,17 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
4385
5022
  return previewedAsset;
4386
5023
  }
4387
5024
  function renderAssetPreviewModal() {
5025
+ if (assetPreviewMode === 'project-media') {
5026
+ disposeMaterialPreview();
5027
+ renderProjectMediaPreviewModal();
5028
+ return;
5029
+ }
4388
5030
  const asset = resolvePreviewedAssetFromState();
4389
5031
  if (!assetPreviewOpen
4390
5032
  || !asset
4391
5033
  || (assetPreviewMode === 'preview' && !asset.preview)
4392
5034
  || (assetPreviewMode === 'material-editor' && !asset.material)) {
5035
+ disposeMaterialPreview();
4393
5036
  assetPreviewModal.style.display = 'none';
4394
5037
  assetPreviewTitle.textContent = '';
4395
5038
  assetPreviewMeta.textContent = '';
@@ -4403,6 +5046,7 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
4403
5046
  return;
4404
5047
  }
4405
5048
  if (assetPreviewMode === 'rename') {
5049
+ disposeMaterialPreview();
4406
5050
  renderAssetRenameModal(asset);
4407
5051
  assetPreviewModal.style.display = 'flex';
4408
5052
  return;
@@ -4424,8 +5068,81 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
4424
5068
  ].filter(Boolean).join(' · ');
4425
5069
  while (assetPreviewBody.firstChild)
4426
5070
  assetPreviewBody.removeChild(assetPreviewBody.firstChild);
4427
- assetPreviewBody.appendChild(LocalEditorPanels.createAssetPreviewElement(doc, asset, 'modal'));
5071
+ assetPreviewBody.appendChild(createMaterialPreviewStage(asset));
5072
+ assetPreviewModal.style.display = 'flex';
5073
+ }
5074
+ function renderProjectMediaPreviewModal() {
5075
+ if (!assetPreviewOpen) {
5076
+ assetPreviewModal.style.display = 'none';
5077
+ while (assetPreviewBody.firstChild)
5078
+ assetPreviewBody.removeChild(assetPreviewBody.firstChild);
5079
+ delete assetPreviewBody.dataset.editorProjectMediaPath;
5080
+ return;
5081
+ }
5082
+ const source = projectMediaPreviewSource;
5083
+ const path = source?.path ?? projectMediaPreviewRequestedPath;
5084
+ assetPreviewDialog.style.width = 'min(760px,calc(100vw - 64px))';
5085
+ const pathSegments = path.split('/');
5086
+ assetPreviewTitle.textContent = source?.name ?? pathSegments[pathSegments.length - 1] ?? '媒体预览';
5087
+ assetPreviewMeta.textContent = projectMediaPreviewError
5088
+ ? `无法预览:${projectMediaPreviewError}`
5089
+ : source
5090
+ ? [source.kind, source.extension.toUpperCase(), source.path, '只读'].join(' · ')
5091
+ : '正在准备预览...';
5092
+ assetPreviewBody.style.cssText = [
5093
+ 'display:flex',
5094
+ 'align-items:center',
5095
+ 'justify-content:center',
5096
+ 'min-height:360px',
5097
+ 'max-height:min(620px,calc(100vh - 170px))',
5098
+ 'overflow:hidden',
5099
+ 'border:1px solid var(--fps-editor-divider)',
5100
+ 'border-radius:4px',
5101
+ 'background:var(--fps-editor-field)',
5102
+ ].join(';');
4428
5103
  assetPreviewModal.style.display = 'flex';
5104
+ const previewKey = projectMediaPreviewError ? `error:${path}:${projectMediaPreviewError}` : source?.url ?? `loading:${path}`;
5105
+ if (assetPreviewBody.dataset.editorProjectMediaPath === previewKey && assetPreviewBody.firstChild)
5106
+ return;
5107
+ while (assetPreviewBody.firstChild)
5108
+ assetPreviewBody.removeChild(assetPreviewBody.firstChild);
5109
+ assetPreviewBody.dataset.editorProjectMediaPath = previewKey;
5110
+ if (projectMediaPreviewError) {
5111
+ const error = doc.createElement('div');
5112
+ error.style.cssText = 'padding:24px;color:var(--fps-editor-danger);font-size:12px;text-align:center';
5113
+ error.textContent = projectMediaPreviewError;
5114
+ assetPreviewBody.appendChild(error);
5115
+ return;
5116
+ }
5117
+ if (!source) {
5118
+ const loading = doc.createElement('div');
5119
+ loading.style.cssText = 'color:var(--fps-editor-muted);font-size:12px';
5120
+ loading.textContent = '正在准备预览...';
5121
+ assetPreviewBody.appendChild(loading);
5122
+ return;
5123
+ }
5124
+ if (source.kind === 'image') {
5125
+ const image = doc.createElement('img');
5126
+ image.src = source.url;
5127
+ image.alt = source.name;
5128
+ image.style.cssText = 'display:block;width:100%;height:100%;max-height:min(620px,calc(100vh - 170px));object-fit:contain';
5129
+ assetPreviewBody.appendChild(image);
5130
+ return;
5131
+ }
5132
+ if (source.kind === 'audio') {
5133
+ const audio = doc.createElement('audio');
5134
+ audio.controls = true;
5135
+ audio.preload = 'metadata';
5136
+ audio.src = source.url;
5137
+ audio.style.cssText = 'width:min(560px,calc(100% - 32px))';
5138
+ assetPreviewBody.appendChild(audio);
5139
+ return;
5140
+ }
5141
+ const canvas = doc.createElement('canvas');
5142
+ canvas.dataset.editorProjectModelPreview = 'true';
5143
+ canvas.setAttribute('aria-label', `${source.name} 三维模型预览`);
5144
+ canvas.style.cssText = 'display:block;width:100%;height:min(560px,calc(100vh - 190px));min-height:360px;touch-action:none;outline:none';
5145
+ assetPreviewBody.appendChild(canvas);
4429
5146
  }
4430
5147
  function renderAssetRenameModal(asset) {
4431
5148
  assetPreviewDialog.style.width = 'min(420px,calc(100vw - 64px))';
@@ -4477,7 +5194,7 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
4477
5194
  const emission = profile.emission ?? {};
4478
5195
  const readonly = material.readonly === true;
4479
5196
  const materialKind = material.materialKind === 'standard' ? 'standard' : 'pbr';
4480
- assetPreviewDialog.style.width = 'min(780px,calc(100vw - 64px))';
5197
+ assetPreviewDialog.style.width = 'min(840px,calc(100vw - 64px))';
4481
5198
  assetPreviewTitle.textContent = '编辑材质球';
4482
5199
  assetPreviewMeta.textContent = [
4483
5200
  material.name || asset.label,
@@ -4490,7 +5207,7 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
4490
5207
  assetPreviewBody.removeChild(assetPreviewBody.firstChild);
4491
5208
  assetPreviewBody.style.cssText = [
4492
5209
  'display:grid',
4493
- 'grid-template-columns:minmax(180px,250px) minmax(0,1fr)',
5210
+ 'grid-template-columns:repeat(auto-fit,minmax(min(280px,100%),1fr))',
4494
5211
  'gap:14px',
4495
5212
  'align-items:start',
4496
5213
  'min-height:360px',
@@ -4502,7 +5219,7 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
4502
5219
  ].join(';');
4503
5220
  const previewColumn = doc.createElement('div');
4504
5221
  previewColumn.style.cssText = 'display:flex;flex-direction:column;gap:10px;min-width:0';
4505
- previewColumn.appendChild(LocalEditorPanels.createAssetPreviewElement(doc, asset, 'modal'));
5222
+ previewColumn.appendChild(createMaterialPreviewStage(asset));
4506
5223
  const previewMeta = doc.createElement('div');
4507
5224
  previewMeta.textContent = readonly
4508
5225
  ? '系统默认材质不可编辑。'
@@ -4881,6 +5598,32 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
4881
5598
  if (target === assetPreviewModal)
4882
5599
  closeAssetPreviewModal();
4883
5600
  });
5601
+ projectFilePreviewModal.addEventListener('click', (event) => {
5602
+ const target = event.target instanceof HTMLElement ? event.target : null;
5603
+ if (target?.closest('[data-editor-project-file-preview-close]')) {
5604
+ closeProjectFilePreview();
5605
+ return;
5606
+ }
5607
+ if (target?.closest('[data-editor-project-file-search-previous]')) {
5608
+ moveProjectFilePreviewMatch(-1);
5609
+ return;
5610
+ }
5611
+ if (target?.closest('[data-editor-project-file-search-next]')) {
5612
+ moveProjectFilePreviewMatch(1);
5613
+ return;
5614
+ }
5615
+ if (target === projectFilePreviewModal)
5616
+ closeProjectFilePreview();
5617
+ });
5618
+ projectFilePreviewSearch.addEventListener('input', () => {
5619
+ updateProjectFilePreviewSearch(true);
5620
+ });
5621
+ projectFilePreviewSearch.addEventListener('keydown', (event) => {
5622
+ if (event.key !== 'Enter')
5623
+ return;
5624
+ event.preventDefault();
5625
+ moveProjectFilePreviewMatch(event.shiftKey ? -1 : 1);
5626
+ });
4884
5627
  assetPreviewBody.addEventListener('change', (event) => {
4885
5628
  const target = event.target instanceof HTMLElement ? event.target : null;
4886
5629
  if (assetPreviewMode !== 'material-editor')
@@ -5085,8 +5828,21 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
5085
5828
  : {}),
5086
5829
  };
5087
5830
  }
5831
+ function navigateAssetBrowserFolder(folderPath) {
5832
+ assetBrowserFolderPath = folderPath;
5833
+ expandAssetBrowserFolderPath(folderPath);
5834
+ selectedProjectEntryPath = null;
5835
+ if (currentState)
5836
+ render(currentState);
5837
+ }
5838
+ function selectProjectBrowserEntry(path, button) {
5839
+ selectedProjectEntryPath = path;
5840
+ for (const candidate of assetPanel.querySelectorAll('[data-editor-project-directory-path],[data-editor-project-file-path]')) {
5841
+ candidate.setAttribute('aria-selected', candidate === button ? 'true' : 'false');
5842
+ }
5843
+ }
5088
5844
  assetPanel.addEventListener('click', (event) => {
5089
- const target = event.target instanceof HTMLElement ? event.target : null;
5845
+ const target = event.target instanceof Element ? event.target : null;
5090
5846
  const tabButton = target?.closest('[data-editor-dock-tab]');
5091
5847
  const tab = tabButton?.dataset.editorDockTab;
5092
5848
  if (tab === 'assets' || tab === 'history') {
@@ -5095,6 +5851,61 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
5095
5851
  render(currentState);
5096
5852
  return;
5097
5853
  }
5854
+ const clearAssetFilterButton = target?.closest('[data-editor-asset-filter-clear]');
5855
+ if (clearAssetFilterButton) {
5856
+ callbacks.onAssetFilterChange?.('');
5857
+ return;
5858
+ }
5859
+ const refreshAssetBrowserButton = target?.closest('[data-editor-asset-browser-refresh]');
5860
+ if (refreshAssetBrowserButton) {
5861
+ resetProjectMediaThumbnailCache();
5862
+ void projectModelThumbnailCache.clearPathEntries()
5863
+ .catch((error) => {
5864
+ console.error('[fps-editor:asset-browser] thumbnail cache refresh cleanup failed', error);
5865
+ })
5866
+ .finally(() => callbacks.onAssetBrowserRefresh?.());
5867
+ return;
5868
+ }
5869
+ const assetFolderToggle = target?.closest('[data-editor-asset-folder-toggle]');
5870
+ const toggledFolderPath = assetFolderToggle?.dataset.editorAssetFolderToggle;
5871
+ if (toggledFolderPath) {
5872
+ if (assetBrowserExpandedFolderPaths.has(toggledFolderPath)) {
5873
+ assetBrowserExpandedFolderPaths.delete(toggledFolderPath);
5874
+ }
5875
+ else {
5876
+ assetBrowserExpandedFolderPaths.add(toggledFolderPath);
5877
+ }
5878
+ if (currentState)
5879
+ render(currentState);
5880
+ return;
5881
+ }
5882
+ const assetFolderButton = target?.closest('[data-editor-asset-folder-path]');
5883
+ if (assetFolderButton?.dataset.editorAssetFolderPath) {
5884
+ const treeFolderPath = assetFolderButton.dataset.editorAssetFolderTreePath;
5885
+ if (treeFolderPath) {
5886
+ assetBrowserFolderPath = treeFolderPath;
5887
+ selectedProjectEntryPath = null;
5888
+ if (assetBrowserExpandedFolderPaths.has(treeFolderPath)) {
5889
+ assetBrowserExpandedFolderPaths.delete(treeFolderPath);
5890
+ }
5891
+ else {
5892
+ assetBrowserExpandedFolderPaths.add(treeFolderPath);
5893
+ }
5894
+ if (currentState)
5895
+ render(currentState);
5896
+ return;
5897
+ }
5898
+ navigateAssetBrowserFolder(assetFolderButton.dataset.editorAssetFolderPath);
5899
+ return;
5900
+ }
5901
+ const assetViewButton = target?.closest('[data-editor-asset-view-mode]');
5902
+ const nextAssetViewMode = assetViewButton?.dataset.editorAssetViewMode;
5903
+ if (nextAssetViewMode === 'grid' || nextAssetViewMode === 'list') {
5904
+ assetBrowserViewMode = nextAssetViewMode;
5905
+ if (currentState)
5906
+ render(currentState);
5907
+ return;
5908
+ }
5098
5909
  const assetBrowserTabButton = target?.closest('[data-editor-asset-browser-tab]');
5099
5910
  const nextAssetBrowserTab = assetBrowserTabButton?.dataset.editorAssetBrowserTab;
5100
5911
  if (isLocalEditorAssetBrowserTab(nextAssetBrowserTab)) {
@@ -5125,26 +5936,93 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
5125
5936
  }
5126
5937
  const assetButton = target?.closest('[data-editor-asset-id]');
5127
5938
  if (assetButton?.dataset.editorAssetId) {
5939
+ selectedProjectEntryPath = null;
5128
5940
  suppressAssetBrowserRevealIfVisible(assetButton);
5129
5941
  callbacks.onSelectAsset?.(assetButton.dataset.editorAssetId);
5130
5942
  return;
5131
5943
  }
5944
+ const projectDirectoryButton = target?.closest('[data-editor-project-directory-path]');
5945
+ if (projectDirectoryButton?.dataset.editorProjectDirectoryPath) {
5946
+ selectProjectBrowserEntry(projectDirectoryButton.dataset.editorProjectDirectoryPath, projectDirectoryButton);
5947
+ return;
5948
+ }
5949
+ const projectFileButton = target?.closest('[data-editor-project-file-path]');
5950
+ if (projectFileButton?.dataset.editorProjectFilePath) {
5951
+ selectProjectBrowserEntry(projectFileButton.dataset.editorProjectFilePath, projectFileButton);
5952
+ return;
5953
+ }
5132
5954
  });
5133
5955
  assetPanel.addEventListener('dblclick', (event) => {
5134
- const target = event.target instanceof HTMLElement ? event.target : null;
5956
+ const target = event.target instanceof Element ? event.target : null;
5957
+ const projectDirectoryButton = target?.closest('[data-editor-project-directory-path]');
5958
+ const projectDirectoryPath = projectDirectoryButton?.dataset.editorProjectDirectoryPath;
5959
+ if (projectDirectoryPath) {
5960
+ event.preventDefault();
5961
+ event.stopPropagation();
5962
+ navigateAssetBrowserFolder(projectDirectoryPath);
5963
+ return;
5964
+ }
5135
5965
  const assetButton = target?.closest('[data-editor-asset-id]');
5136
5966
  const browserAssetId = assetButton?.dataset.editorAssetId;
5137
- if (!browserAssetId || !currentState)
5967
+ const projectFileButton = target?.closest('[data-editor-project-file-path]');
5968
+ const browserProjectFilePath = projectFileButton?.dataset.editorProjectFilePath;
5969
+ if (!currentState)
5138
5970
  return;
5139
- const asset = currentState.assets.find(candidate => candidate.id === browserAssetId);
5971
+ const asset = browserAssetId
5972
+ ? currentState.assets.find(candidate => candidate.id === browserAssetId)
5973
+ : undefined;
5140
5974
  const actionId = asset ? resolveAssetBrowserDoubleClickAction(currentState, asset) : null;
5141
- if (!asset || !actionId)
5975
+ if (asset && actionId && browserAssetId) {
5976
+ event.preventDefault();
5977
+ event.stopPropagation();
5978
+ suppressAssetBrowserRevealIfVisible(assetButton);
5979
+ callbacks.onSelectAsset?.(browserAssetId);
5980
+ callbacks.onAssetAction?.(createAssetBrowserActionInput(actionId, asset, createCurrentAssetBrowserActionContext()));
5981
+ return;
5982
+ }
5983
+ if (browserProjectFilePath && openProjectBrowserFile(browserProjectFilePath, projectFileButton)) {
5984
+ event.preventDefault();
5985
+ event.stopPropagation();
5986
+ }
5987
+ });
5988
+ assetPanel.addEventListener('keydown', (event) => {
5989
+ if (event.key !== 'Enter')
5990
+ return;
5991
+ const target = event.target instanceof Element ? event.target : null;
5992
+ const projectDirectoryButton = target?.closest('[data-editor-project-directory-path]');
5993
+ const projectDirectoryPath = projectDirectoryButton?.dataset.editorProjectDirectoryPath;
5994
+ if (projectDirectoryPath) {
5995
+ event.preventDefault();
5996
+ navigateAssetBrowserFolder(projectDirectoryPath);
5997
+ return;
5998
+ }
5999
+ const assetButton = target?.closest('[data-editor-asset-id]');
6000
+ const browserAssetId = assetButton?.dataset.editorAssetId;
6001
+ const asset = browserAssetId
6002
+ ? currentState?.assets.find(candidate => candidate.id === browserAssetId)
6003
+ : undefined;
6004
+ const assetActionId = asset && currentState
6005
+ ? resolveAssetBrowserDoubleClickAction(currentState, asset)
6006
+ : null;
6007
+ if (asset && browserAssetId && assetActionId) {
6008
+ event.preventDefault();
6009
+ suppressAssetBrowserRevealIfVisible(assetButton);
6010
+ callbacks.onSelectAsset?.(browserAssetId);
6011
+ callbacks.onAssetAction?.(createAssetBrowserActionInput(assetActionId, asset, createCurrentAssetBrowserActionContext()));
6012
+ return;
6013
+ }
6014
+ const projectFileButton = target?.closest('[data-editor-project-file-path]');
6015
+ const projectFilePath = projectFileButton?.dataset.editorProjectFilePath;
6016
+ if (projectFilePath && openProjectBrowserFile(projectFilePath, projectFileButton)) {
6017
+ event.preventDefault();
6018
+ return;
6019
+ }
6020
+ const treeFolderButton = target?.closest('[data-editor-asset-folder-tree-path]');
6021
+ const treeFolderPath = treeFolderButton?.dataset.editorAssetFolderTreePath;
6022
+ if (!treeFolderPath)
5142
6023
  return;
5143
6024
  event.preventDefault();
5144
- event.stopPropagation();
5145
- suppressAssetBrowserRevealIfVisible(assetButton);
5146
- callbacks.onSelectAsset?.(browserAssetId);
5147
- callbacks.onAssetAction?.(createAssetBrowserActionInput(actionId, asset, createCurrentAssetBrowserActionContext()));
6025
+ treeFolderButton.click();
5148
6026
  });
5149
6027
  assetPanel.addEventListener('contextmenu', (event) => {
5150
6028
  if (!inputRouter.shouldPreventBrowserContextMenu(event))
@@ -5152,31 +6030,119 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
5152
6030
  const target = inputRouter.getEventTarget(event);
5153
6031
  if (!target)
5154
6032
  return;
5155
- const assetButton = target?.closest('[data-editor-asset-id]');
6033
+ const projectFileButton = target.closest('[data-editor-project-file-path]');
6034
+ const browserProjectFilePath = projectFileButton?.dataset.editorProjectFilePath;
6035
+ const projectDirectoryButton = target.closest('[data-editor-project-directory-path],[data-editor-asset-folder-tree-path]');
6036
+ const browserProjectDirectoryPath = projectDirectoryButton?.dataset.editorProjectDirectoryPath
6037
+ ?? projectDirectoryButton?.dataset.editorAssetFolderTreePath;
6038
+ const assetButton = target.closest('[data-editor-asset-id]');
5156
6039
  const browserAssetId = assetButton?.dataset.editorAssetId;
5157
- if (!browserAssetId || !currentState)
6040
+ if ((!browserProjectFilePath && browserProjectDirectoryPath == null && !browserAssetId) || !currentState)
5158
6041
  return;
5159
- const asset = currentState.assets.find(candidate => candidate.id === browserAssetId);
5160
- if (!asset)
6042
+ const asset = browserAssetId
6043
+ ? currentState.assets.find(candidate => candidate.id === browserAssetId) ?? null
6044
+ : null;
6045
+ if (browserAssetId && !asset)
5161
6046
  return;
5162
6047
  inputRouter.claimContextMenu(event);
5163
- suppressAssetBrowserRevealIfVisible(assetButton);
5164
- callbacks.onSelectAsset?.(browserAssetId);
5165
- contextMenu.open({
6048
+ if (asset && assetButton) {
6049
+ suppressAssetBrowserRevealIfVisible(assetButton);
6050
+ callbacks.onSelectAsset?.(asset.id);
6051
+ }
6052
+ else if (browserProjectFilePath && projectFileButton) {
6053
+ selectProjectBrowserEntry(browserProjectFilePath, projectFileButton);
6054
+ }
6055
+ else if (browserProjectDirectoryPath != null && projectDirectoryButton) {
6056
+ selectProjectBrowserEntry(browserProjectDirectoryPath, projectDirectoryButton);
6057
+ }
6058
+ const relativeProjectFilePath = browserProjectFilePath
6059
+ ? toLocalEditorProjectRelativePath(browserProjectFilePath)
6060
+ : null;
6061
+ const relativeProjectDirectoryPath = browserProjectDirectoryPath != null
6062
+ ? toLocalEditorProjectRelativePath(browserProjectDirectoryPath) || '.'
6063
+ : null;
6064
+ const snapshot = createLocalEditorAssetFilesystemSnapshot(currentState.assets, currentState.projectFiles);
6065
+ const fileEntry = browserProjectFilePath
6066
+ ? snapshot.entries.find(entry => entry.path === browserProjectFilePath) ?? null
6067
+ : null;
6068
+ const directoryEntry = browserProjectDirectoryPath != null
6069
+ ? snapshot.entries.find(entry => entry.path === browserProjectDirectoryPath) ?? null
6070
+ : null;
6071
+ const directoryFolder = browserProjectDirectoryPath != null
6072
+ ? snapshot.folders.find(folder => folder.path === browserProjectDirectoryPath) ?? null
6073
+ : null;
6074
+ const projectItems = relativeProjectFilePath
6075
+ ? [
6076
+ ...(callbacks.onProjectFileRead && isEditorProjectFilePreviewable(relativeProjectFilePath)
6077
+ ? [{ id: 'project-file.preview', label: '查看文件' }]
6078
+ : []),
6079
+ ...(callbacks.onProjectMediaResolve && isEditorProjectMediaPreviewable(relativeProjectFilePath)
6080
+ ? [{ id: 'project-file.preview-media', label: '预览媒体' }]
6081
+ : []),
6082
+ ...(callbacks.onProjectEntrySendToAgent
6083
+ ? [{ id: 'project-entry.send-to-agent', label: '发送给 Agent' }]
6084
+ : []),
6085
+ { id: 'project-file.copy-path', label: '复制路径' },
6086
+ ]
6087
+ : relativeProjectDirectoryPath
6088
+ ? [
6089
+ ...(callbacks.onProjectEntrySendToAgent
6090
+ ? [{ id: 'project-entry.send-to-agent', label: '发送给 Agent' }]
6091
+ : []),
6092
+ { id: 'project-file.copy-path', label: '复制路径' },
6093
+ ]
6094
+ : [];
6095
+ const assetItems = asset ? createAssetBrowserMenuItems(currentState, asset) : [];
6096
+ if (projectItems.length > 0 && assetItems.length > 0)
6097
+ assetItems[0] = { ...assetItems[0], separatorBefore: true };
6098
+ const openContextMenu = () => contextMenu.open({
5166
6099
  x: event.clientX,
5167
6100
  y: event.clientY,
5168
- items: createAssetBrowserMenuItems(currentState, asset),
6101
+ items: [...projectItems, ...assetItems],
5169
6102
  onAction(item) {
6103
+ if (item.id === 'project-file.preview' && browserProjectFilePath) {
6104
+ void openProjectFilePreview(browserProjectFilePath, projectFileButton);
6105
+ return;
6106
+ }
6107
+ if (item.id === 'project-file.preview-media' && browserProjectFilePath) {
6108
+ void openProjectMediaPreview(browserProjectFilePath, projectFileButton);
6109
+ return;
6110
+ }
6111
+ if (item.id === 'project-entry.send-to-agent' && callbacks.onProjectEntrySendToAgent) {
6112
+ const path = relativeProjectFilePath ?? relativeProjectDirectoryPath;
6113
+ if (!path)
6114
+ return;
6115
+ const entryKind = relativeProjectFilePath ? 'file' : 'directory';
6116
+ const name = entryKind === 'file'
6117
+ ? fileEntry?.label ?? path.split('/').slice(-1)[0] ?? path
6118
+ : directoryEntry?.label ?? directoryFolder?.label ?? currentState?.projectFiles?.rootName ?? path;
6119
+ void Promise.resolve(callbacks.onProjectEntrySendToAgent({
6120
+ path,
6121
+ name,
6122
+ entryKind,
6123
+ ...(entryKind === 'file' && fileEntry?.extension ? { extension: fileEntry.extension } : {}),
6124
+ ...(asset ? { assetId: asset.assetId ?? asset.id, assetKind: asset.kind } : {}),
6125
+ })).catch(() => undefined);
6126
+ return;
6127
+ }
6128
+ if (item.id === 'project-file.copy-path') {
6129
+ const path = relativeProjectFilePath ?? relativeProjectDirectoryPath;
6130
+ if (path)
6131
+ copyTextToClipboard(doc.defaultView, path);
6132
+ return;
6133
+ }
6134
+ if (!asset)
6135
+ return;
5170
6136
  if (item.id === 'asset.preview') {
5171
- openAssetPreviewModal(asset);
6137
+ openAssetPreviewModal(asset, assetButton);
5172
6138
  return;
5173
6139
  }
5174
6140
  if (item.id === 'asset.edit-material') {
5175
- openAssetMaterialEditorModal(asset);
6141
+ openAssetMaterialEditorModal(asset, assetButton);
5176
6142
  return;
5177
6143
  }
5178
6144
  if (item.id === 'asset.rename') {
5179
- openAssetRenameModal(asset);
6145
+ openAssetRenameModal(asset, assetButton);
5180
6146
  return;
5181
6147
  }
5182
6148
  if (item.id === 'asset.edit-prefab' || item.id === 'asset.add-to-current-prefab') {
@@ -5190,6 +6156,15 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
5190
6156
  callbacks.onAssetAction?.(createAssetBrowserActionInput(item.id, asset, createCurrentAssetBrowserActionContext()));
5191
6157
  },
5192
6158
  });
6159
+ // Selecting a catalog asset rerenders the asset browser synchronously. Open
6160
+ // its menu on the next frame so the first right-click is not cleared by that
6161
+ // selection refresh.
6162
+ if (asset && assetButton && doc.defaultView?.requestAnimationFrame) {
6163
+ doc.defaultView.requestAnimationFrame(openContextMenu);
6164
+ }
6165
+ else {
6166
+ openContextMenu();
6167
+ }
5193
6168
  });
5194
6169
  workbench.rightDock.addEventListener('click', (event) => {
5195
6170
  const target = event.target instanceof HTMLElement ? event.target : null;
@@ -5236,6 +6211,8 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
5236
6211
  setShortcutHelpOpen(false);
5237
6212
  if (assetPreviewOpen)
5238
6213
  closeAssetPreviewModal();
6214
+ if (projectFilePreviewOpen)
6215
+ closeProjectFilePreview();
5239
6216
  },
5240
6217
  requestOperationInput: requestHierarchyOperationInput,
5241
6218
  });
@@ -5246,6 +6223,15 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
5246
6223
  return;
5247
6224
  callbacks.onAssetFilterChange?.(input.value);
5248
6225
  });
6226
+ assetPanel.addEventListener('change', (event) => {
6227
+ const select = event.target instanceof HTMLSelectElement ? event.target : null;
6228
+ const nextAssetBrowserTab = select?.dataset.editorAssetBrowserTypeFilter ? select.value : null;
6229
+ if (!isLocalEditorAssetBrowserTab(nextAssetBrowserTab))
6230
+ return;
6231
+ assetBrowserTab = nextAssetBrowserTab;
6232
+ if (currentState)
6233
+ render(currentState);
6234
+ });
5249
6235
  inspectorPanel.addEventListener('input', (event) => {
5250
6236
  const input = event.target instanceof HTMLInputElement ? event.target : null;
5251
6237
  if (input?.dataset.editorInspectorSearch != null) {
@@ -5254,6 +6240,8 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
5254
6240
  render(currentState);
5255
6241
  return;
5256
6242
  }
6243
+ if (currentState?.inspectorPending === true)
6244
+ return;
5257
6245
  if (input?.type === 'checkbox' || input?.type === 'color')
5258
6246
  return;
5259
6247
  if (!input?.dataset.serializedPath || !input.dataset.serializedTargetId)
@@ -5268,6 +6256,11 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
5268
6256
  callbacks.onPropertyInput?.(propertyInput);
5269
6257
  });
5270
6258
  inspectorPanel.addEventListener('paste', (event) => {
6259
+ if (currentState?.inspectorPending === true) {
6260
+ event.preventDefault();
6261
+ event.stopPropagation();
6262
+ return;
6263
+ }
5271
6264
  const input = event.target instanceof HTMLInputElement ? event.target : null;
5272
6265
  const text = event.clipboardData?.getData('text/plain') ?? event.clipboardData?.getData('text') ?? '';
5273
6266
  if (!input || !text)
@@ -5285,6 +6278,11 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
5285
6278
  callbacks.onPropertyInput?.(propertyInput);
5286
6279
  });
5287
6280
  inspectorPanel.addEventListener('click', (event) => {
6281
+ if (currentState?.inspectorPending === true) {
6282
+ event.preventDefault();
6283
+ event.stopPropagation();
6284
+ return;
6285
+ }
5288
6286
  const target = event.target instanceof HTMLElement ? event.target : null;
5289
6287
  const renderingActionButton = target?.closest('[data-editor-rendering-action]');
5290
6288
  if (renderingActionButton) {
@@ -5397,6 +6395,8 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
5397
6395
  openInspectorAssetPicker(input, property);
5398
6396
  });
5399
6397
  inspectorPanel.addEventListener('change', (event) => {
6398
+ if (currentState?.inspectorPending === true)
6399
+ return;
5400
6400
  const input = event.target instanceof HTMLInputElement || event.target instanceof HTMLSelectElement
5401
6401
  ? event.target
5402
6402
  : null;
@@ -5418,6 +6418,11 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
5418
6418
  callbacks.onPropertyInput?.(propertyInput);
5419
6419
  });
5420
6420
  inspectorPanel.addEventListener('keydown', (event) => {
6421
+ if (currentState?.inspectorPending === true) {
6422
+ event.preventDefault();
6423
+ event.stopPropagation();
6424
+ return;
6425
+ }
5421
6426
  const input = event.target instanceof HTMLInputElement ? event.target : null;
5422
6427
  if (!input || !isLocalEditorKeyboardCommitEvent(event, input))
5423
6428
  return;
@@ -5427,6 +6432,8 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
5427
6432
  event.stopPropagation();
5428
6433
  });
5429
6434
  inspectorPanel.addEventListener('focusout', (event) => {
6435
+ if (currentState?.inspectorPending === true)
6436
+ return;
5430
6437
  const input = event.target instanceof HTMLInputElement ? event.target : null;
5431
6438
  if (!input?.dataset.serializedPath || !input.dataset.serializedTargetId)
5432
6439
  return;
@@ -5568,6 +6575,11 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
5568
6575
  closeAssetPreviewModal();
5569
6576
  return;
5570
6577
  }
6578
+ if (key === 'escape' && projectFilePreviewOpen) {
6579
+ event.preventDefault();
6580
+ closeProjectFilePreview();
6581
+ return;
6582
+ }
5571
6583
  if (key === 'escape' && assetPickerOpen) {
5572
6584
  event.preventDefault();
5573
6585
  closeInspectorAssetPicker();
@@ -5621,6 +6633,7 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
5621
6633
  && primaryModifier
5622
6634
  && key === 's') {
5623
6635
  event.preventDefault();
6636
+ editorConsole.append({ severity: 'info', source: 'save', message: '请求保存场景' });
5624
6637
  callbacks.onSaveScene?.();
5625
6638
  return;
5626
6639
  }
@@ -5646,6 +6659,17 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
5646
6659
  && candidate.shortcut?.toLowerCase() === key));
5647
6660
  if (tool) {
5648
6661
  event.preventDefault();
6662
+ if (/(?:^|[?&])debugGizmo=(?:1|true)(?:&|$)/.test(doc.defaultView?.location?.search ?? '')) {
6663
+ console.info('[fps-editor:gizmo]', 'shortcut.dispatched', {
6664
+ timestamp: typeof performance !== 'undefined' ? Number(performance.now().toFixed(2)) : Date.now(),
6665
+ key,
6666
+ toolId: tool.id,
6667
+ availability: tool.availability.state,
6668
+ active: tool.active,
6669
+ eventTarget: event.target instanceof Element ? event.target.tagName : null,
6670
+ defaultPrevented: event.defaultPrevented,
6671
+ });
6672
+ }
5649
6673
  if (tool.availability.state === 'enabled')
5650
6674
  callbacks.onToolActivate?.(tool.id);
5651
6675
  return;
@@ -5693,27 +6717,31 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
5693
6717
  && getPreviewToolSupport(tool).support === 'enabled';
5694
6718
  }
5695
6719
  function applyPrefabStageWorkbenchLayoutOverride(layout = workbenchLayoutController.getLayout()) {
5696
- const { leftWidth, rightWidth, bottomEdge, bottomVisible } = resolveLocalEditorPrefabStageWorkbenchLayout(layout, readLocalEditorWorkbenchViewport(doc));
6720
+ const { leftWidth, rightWidth, consoleEdge, sceneBottomEdge, bottomVisible } = resolveLocalEditorPrefabStageWorkbenchLayout(layout, readLocalEditorWorkbenchViewport(doc));
5697
6721
  workbench.leftDock.style.display = 'flex';
5698
6722
  workbench.leftDock.style.width = `${leftWidth}px`;
5699
- workbench.leftDock.style.bottom = `${bottomEdge}px`;
6723
+ workbench.leftDock.style.bottom = `${consoleEdge}px`;
5700
6724
  workbench.leftDock.style.zIndex = '';
5701
6725
  workbench.leftDock.style.boxShadow = '';
5702
6726
  workbench.rightDock.style.display = 'flex';
5703
6727
  workbench.rightDock.style.width = `${rightWidth}px`;
6728
+ workbench.rightDock.style.bottom = `${consoleEdge}px`;
5704
6729
  workbench.rightDock.style.zIndex = '';
5705
6730
  workbench.rightDock.style.boxShadow = '';
5706
6731
  workbench.sceneFrame.style.left = `${leftWidth}px`;
5707
6732
  workbench.sceneFrame.style.right = `${rightWidth}px`;
5708
- workbench.sceneFrame.style.bottom = `${bottomEdge}px`;
6733
+ workbench.sceneFrame.style.bottom = `${sceneBottomEdge}px`;
5709
6734
  if (bottomVisible) {
6735
+ workbench.bottomDock.style.left = `${leftWidth}px`;
5710
6736
  workbench.bottomDock.style.right = `${rightWidth}px`;
6737
+ workbench.bottomDock.style.bottom = `${consoleEdge}px`;
6738
+ workbench.bottomResizer.style.left = `${leftWidth}px`;
5711
6739
  workbench.bottomResizer.style.right = `${rightWidth}px`;
5712
6740
  }
5713
6741
  workbench.leftToggle.style.display = 'none';
5714
6742
  workbench.rightToggle.style.display = 'none';
5715
- workbench.leftResizer.style.bottom = `${bottomEdge}px`;
5716
- workbench.rightResizer.style.bottom = '0';
6743
+ workbench.leftResizer.style.bottom = `${consoleEdge}px`;
6744
+ workbench.rightResizer.style.bottom = `${consoleEdge}px`;
5717
6745
  }
5718
6746
  function readLocalEditorWorkbenchViewport(docRef) {
5719
6747
  const win = docRef.defaultView;
@@ -5734,6 +6762,7 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
5734
6762
  emptyText: 'Select an item in Prefab Explorer.',
5735
6763
  lockedVectorGroups: lockedInspectorVectorGroups,
5736
6764
  });
6765
+ lastRenderedInspectorContentKey = resolveLocalEditorRightDockContentKey(currentState, 'inspector', inspectorFilter);
5737
6766
  return;
5738
6767
  }
5739
6768
  LocalEditorPanels.disposeInspectorPanelUi(inspectorPanel);
@@ -5750,6 +6779,9 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
5750
6779
  appendPrefabStageReadonlyRow(grid, 'Mode', stage.readonly ? '只读' : '可编辑');
5751
6780
  appendPrefabStageSelectedItemDetails(grid, selectedItem);
5752
6781
  inspectorPanel.appendChild(grid);
6782
+ lastRenderedInspectorContentKey = currentState
6783
+ ? resolveLocalEditorRightDockContentKey(currentState, 'inspector', inspectorFilter)
6784
+ : null;
5753
6785
  }
5754
6786
  function appendPrefabStageSelectedItemDetails(container, item) {
5755
6787
  if (!item)
@@ -5914,8 +6946,7 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
5914
6946
  ? 'var(--fps-editor-border)'
5915
6947
  : agentBridgeStatusColor;
5916
6948
  setToolbarButtonTooltip(agentBridgeStatusButton, createAgentBridgeStatusTooltip(state.agentBridge));
5917
- renderCoordinateAxesOverlay(coordinateAxesOverlay, inEditor ? state.coordinateAxes ?? null : null);
5918
- renderSceneFrameRateOverlay(sceneFrameRateOverlay, inEditor && !inPrefabStage ? state.sceneFrameStats ?? null : null);
6949
+ renderCoordinateAxesOverlay(coordinateAxesOverlay, inEditor ? state.coordinateAxes ?? null : null, state.viewportTools?.viewPreset ?? 'perspective', state.busy);
5919
6950
  renderPerformanceMonitorOverlay(performanceMonitorOverlay, inEditor && !inPrefabStage ? state.performanceStats ?? null : null, inEditor && !inPrefabStage && performanceMonitorOpen);
5920
6951
  prefabStagePreviewLayout.root.style.display = inEditor && inPrefabStage ? 'block' : 'none';
5921
6952
  renderPrefabImportStatsOverlay(prefabImportStatsOverlay, inEditor && inPrefabStage ? state.prefabStage ?? null : null);
@@ -5938,12 +6969,48 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
5938
6969
  const updatePreview = (preview) => {
5939
6970
  if (!currentState)
5940
6971
  return;
6972
+ const toolPresentationStartedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
6973
+ const shouldRenderToolPresentation = 'tools' in preview || 'activeToolId' in preview;
6974
+ const previousActiveToolId = currentState.activeToolId ?? null;
5941
6975
  const shouldRenderInspectorPreview = 'inspectorObject' in preview || 'inspectorMultiObject' in preview;
5942
- currentState = {
6976
+ const shouldRenderSelectionPresentation = 'selectedIds' in preview
6977
+ || 'activeId' in preview
6978
+ || 'selectionSummary' in preview;
6979
+ const nextState = {
5943
6980
  ...currentState,
5944
6981
  ...preview,
5945
6982
  };
6983
+ const nextInspectorContentKey = resolveLocalEditorRightDockContentKey(nextState, 'inspector', inspectorFilter);
6984
+ const inspectorPreviewScrollSnapshot = shouldRenderInspectorPreview
6985
+ && lastRenderedInspectorContentKey === nextInspectorContentKey
6986
+ ? captureLocalEditorRightDockScrollSnapshot(currentState, 'inspector', inspectorFilter, inspectorPanel, renderingPanel, markersPanel, pluginsPanel)
6987
+ : null;
6988
+ currentState = nextState;
6989
+ if ('inspectorPending' in preview) {
6990
+ syncLocalEditorInspectorPendingState(inspectorPanel, currentState.inspectorPending === true);
6991
+ }
6992
+ if (shouldRenderToolPresentation) {
6993
+ renderToolPresentationState(currentState);
6994
+ scheduleToolbarOverflowLayout();
6995
+ if (/(?:^|[?&])debugGizmo=(?:1|true)(?:&|$)/.test(doc.defaultView?.location?.search ?? '')) {
6996
+ console.info('[fps-editor:gizmo]', 'ui.tool-presentation.updated', {
6997
+ timestamp: typeof performance !== 'undefined' ? Number(performance.now().toFixed(2)) : Date.now(),
6998
+ durationMs: Number(((typeof performance !== 'undefined' ? performance.now() : Date.now()) - toolPresentationStartedAt).toFixed(2)),
6999
+ previousActiveToolId,
7000
+ activeToolId: currentState.activeToolId ?? null,
7001
+ toolCount: currentState.tools?.length ?? 0,
7002
+ });
7003
+ }
7004
+ }
7005
+ if (shouldRenderSelectionPresentation)
7006
+ hierarchyController.render(currentState);
5946
7007
  renderPreviewState(currentState);
7008
+ if (shouldRenderInspectorPreview
7009
+ && currentState.mode === 'editor'
7010
+ && !!currentState.prefabStage
7011
+ && rightDockFormalSurfaceVisibility.isVisible()) {
7012
+ renderPrefabStagePropertiesPanel(currentState.prefabStage);
7013
+ }
5947
7014
  if (shouldRenderLocalEditorInspectorPreview({
5948
7015
  hasInspectorPreview: shouldRenderInspectorPreview,
5949
7016
  mode: currentState.mode,
@@ -5951,20 +7018,33 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
5951
7018
  rightDockVisible: rightDockFormalSurfaceVisibility.isVisible(),
5952
7019
  activeRightTab: panelRegistry.getRightDockTab(),
5953
7020
  })) {
5954
- const rightDockScrollSnapshot = captureLocalEditorRightDockScrollSnapshot(currentState, 'inspector', inspectorFilter, inspectorPanel, renderingPanel, markersPanel, pluginsPanel);
5955
7021
  LocalEditorPanels.renderInspectorPanel(doc, inspectorPanel, currentState, inspectorFilter, {
5956
7022
  ...options.inspector,
5957
7023
  layerHost: editorUiLayerHost,
5958
7024
  lockedVectorGroups: lockedInspectorVectorGroups,
5959
7025
  });
5960
- restoreLocalEditorRightDockScrollSnapshot(rightDockScrollSnapshot, currentState, 'inspector', inspectorFilter, inspectorPanel, renderingPanel, markersPanel, pluginsPanel);
7026
+ lastRenderedInspectorContentKey = nextInspectorContentKey;
7027
+ restoreLocalEditorRightDockScrollSnapshot(inspectorPreviewScrollSnapshot, currentState, 'inspector', inspectorFilter, inspectorPanel, renderingPanel, markersPanel, pluginsPanel);
7028
+ }
7029
+ if ((shouldRenderSelectionPresentation || shouldRenderInspectorPreview)
7030
+ && /(?:^|[?&])debug(?:Gizmo|Selection)=(?:1|true)(?:&|$)/.test(doc.defaultView?.location?.search ?? '')) {
7031
+ console.info('[fps-editor:selection]', 'ui.selection-preview.updated', {
7032
+ timestamp: typeof performance !== 'undefined' ? Number(performance.now().toFixed(2)) : Date.now(),
7033
+ durationMs: Number(((typeof performance !== 'undefined' ? performance.now() : Date.now()) - toolPresentationStartedAt).toFixed(2)),
7034
+ phase: shouldRenderSelectionPresentation ? 'presentation' : 'inspector',
7035
+ activeId: currentState.activeId,
7036
+ selectedCount: currentState.selectedIds.length,
7037
+ });
5961
7038
  }
5962
7039
  };
5963
7040
  const render = (state) => {
5964
7041
  const previousState = currentState;
7042
+ const assetBrowserContinuitySnapshot = assetBrowserContinuity.capture(lastRenderedAssetBrowserContentKey);
5965
7043
  const previousRightTab = panelRegistry.getRightDockTab();
5966
7044
  const rightDockScrollSnapshot = captureLocalEditorRightDockScrollSnapshot(previousState, previousRightTab, inspectorFilter, inspectorPanel, renderingPanel, markersPanel, pluginsPanel);
5967
7045
  currentState = state;
7046
+ syncLocalEditorInspectorPendingState(inspectorPanel, state.inspectorPending === true);
7047
+ editorConsole.syncState(state, previousState);
5968
7048
  const focusSnapshot = captureLocalEditorEditableFocus(doc);
5969
7049
  const inEditor = state.mode === 'editor';
5970
7050
  const disabled = state.busy;
@@ -5993,14 +7073,29 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
5993
7073
  setPerformanceMonitorOpen(false);
5994
7074
  }
5995
7075
  if (!inEditor || disabled) {
7076
+ projectMediaPreviewRequestId += 1;
7077
+ disposeProjectModelPreview();
5996
7078
  assetPreviewOpen = false;
7079
+ assetPreviewReturnFocus = null;
5997
7080
  previewedAsset = null;
5998
7081
  assetPreviewMode = 'preview';
7082
+ projectMediaPreviewSource = null;
7083
+ projectMediaPreviewRequestedPath = '';
7084
+ projectMediaPreviewError = '';
7085
+ projectFilePreviewOpen = false;
7086
+ projectFilePreviewReturnFocus = null;
7087
+ projectFilePreviewRequestId += 1;
7088
+ projectFilePreviewContent = null;
7089
+ projectFilePreviewRequestedPath = '';
7090
+ projectFilePreviewError = '';
7091
+ projectFilePreviewMatches = [];
7092
+ projectFilePreviewActiveMatch = -1;
5999
7093
  assetPickerOpen = false;
6000
7094
  assetPickerInput = null;
6001
7095
  assetPickerProperty = null;
6002
7096
  settleHierarchyOperationInputRequest(null);
6003
7097
  }
7098
+ renderProjectFilePreview();
6004
7099
  syncInputModalOpen();
6005
7100
  const transformTool = state.transformTool ?? null;
6006
7101
  workbench.root.style.display = inEditor ? '' : 'none';
@@ -6066,21 +7161,7 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
6066
7161
  const viewportControlsTooltip = sceneCameraPreview.enabled
6067
7162
  ? '当前为 Main Camera 预览,请关闭后再使用视口工具'
6068
7163
  : '';
6069
- const viewportProjectionTooltip = sceneCameraPreview.enabled
6070
- ? '当前为 Main Camera 预览,投影由 Main Camera 决定;关闭预览后可切换编辑视口投影'
6071
- : '';
6072
7164
  viewportToolsGroup.style.display = isPreviewToolVisible('viewport-camera') ? 'flex' : 'none';
6073
- for (const [preset, button] of viewportPresetButtons) {
6074
- button.disabled = viewportControlsDisabled;
6075
- setToolbarButtonTooltip(button, viewportControlsTooltip || VIEW_PRESET_TOOLTIPS[preset]);
6076
- LocalEditorShared.applyButtonActiveState(button, viewportTools?.viewPreset === preset);
6077
- }
6078
- projectionToggleButton.disabled = viewportControlsDisabled;
6079
- const projectionMode = viewportTools?.projectionMode ?? 'perspective';
6080
- const nextProjectionMode = projectionMode === 'orthographic' ? 'perspective' : 'orthographic';
6081
- setToolbarButtonIcon(doc, projectionToggleButton, projectionMode === 'orthographic' ? 'projection-ortho' : 'projection-perspective');
6082
- setToolbarButtonTooltip(projectionToggleButton, viewportProjectionTooltip || `投影模式:${VIEWPORT_PROJECTION_LABELS[projectionMode]}。切换为${VIEWPORT_PROJECTION_LABELS[nextProjectionMode]}`);
6083
- LocalEditorShared.applyButtonActiveState(projectionToggleButton, projectionMode === 'orthographic');
6084
7165
  const markersVisible = viewportTools?.markersVisible ?? true;
6085
7166
  markerVisibilityButton.disabled = disabled;
6086
7167
  setToolbarButtonTooltip(markerVisibilityButton, markersVisible ? '隐藏标记与关系线' : '显示标记与关系线');
@@ -6120,11 +7201,7 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
6120
7201
  gridToggleButton.disabled = disabled || !gridState.available;
6121
7202
  setToolbarButtonTooltip(gridToggleButton, gridState.visible ? '隐藏 Scene View 网格' : '显示 Scene View 网格');
6122
7203
  LocalEditorShared.applyButtonActiveState(gridToggleButton, gridState.visible);
6123
- synchronizeToolButtons(state.tools ?? []);
6124
- toolGroup.style.display = inEditor
6125
- && (state.tools ?? []).some(tool => tool.kind === 'primary' && tool.availability.state !== 'hidden')
6126
- ? 'flex'
6127
- : 'none';
7204
+ renderToolPresentationState(state);
6128
7205
  spaceGroup.style.display = isPreviewToolVisible('transform-space') ? 'flex' : 'none';
6129
7206
  for (const [space, button] of Object.entries(spaceButtons)) {
6130
7207
  button.disabled = !isPreviewToolEnabled('transform-space');
@@ -6199,12 +7276,7 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
6199
7276
  activeRightTab,
6200
7277
  });
6201
7278
  const activeBottomTab = panelRegistry.getBottomDockTab();
6202
- const assetBrowserTabChanged = assetBrowserTab !== lastRenderedAssetBrowserTab;
6203
- const bottomDockTabChanged = activeBottomTab !== lastRenderedBottomDockTab;
6204
7279
  const selectedAssetChanged = state.selectedAssetId !== lastRenderedSelectedAssetId;
6205
- const assetBrowserScrollSnapshot = activeBottomTab === 'assets'
6206
- ? assetBrowserViewportReveal.captureScrollSnapshot()
6207
- : { scrollTop: null };
6208
7280
  leftDockTabs.style.display = 'none';
6209
7281
  rightDockTabs.style.display = rightDockVisibility.tabsDisplay;
6210
7282
  inspectorPanel.style.display = rightDockVisibility.inspectorDisplay;
@@ -6213,10 +7285,18 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
6213
7285
  pluginsPanel.style.display = rightDockVisibility.pluginsDisplay;
6214
7286
  hierarchyController.render(state);
6215
7287
  renderPrefabStagePropertiesPanel(prefabStage);
6216
- LocalEditorPanels.renderWorkbenchBottomDockPanel(doc, assetPanel, state, activeBottomTab, panelRegistry.getPanels('bottom'), assetBrowserTab);
6217
- if (activeBottomTab === 'assets' && !assetBrowserTabChanged && !bottomDockTabChanged) {
6218
- assetBrowserViewportReveal.restoreScrollSnapshot(assetBrowserScrollSnapshot);
7288
+ LocalEditorPanels.renderWorkbenchBottomDockPanel(doc, assetPanel, state, activeBottomTab, panelRegistry.getPanels('bottom'), assetBrowserTab, { expandedFolderPaths: assetBrowserExpandedFolderPaths, folderPath: assetBrowserFolderPath, viewMode: assetBrowserViewMode, selectedEntryPath: selectedProjectEntryPath });
7289
+ if (activeBottomTab === 'assets') {
7290
+ assetBrowserFolderPath = assetPanel.dataset.editorAssetBrowserFolderPath ?? assetBrowserFolderPath;
6219
7291
  }
7292
+ const assetBrowserContentKey = resolveLocalEditorAssetBrowserContentKey({
7293
+ assetFilter: state.assetFilter,
7294
+ folderPath: assetBrowserFolderPath,
7295
+ tab: assetBrowserTab,
7296
+ viewMode: assetBrowserViewMode,
7297
+ });
7298
+ hydrateProjectMediaThumbnails();
7299
+ assetBrowserContinuity.restore(assetBrowserContinuitySnapshot, assetBrowserContentKey, activeBottomTab === 'assets');
6220
7300
  const suppressAssetScroll = state.selectedAssetId
6221
7301
  ? assetBrowserViewportReveal.consumeRevealSuppression(state.selectedAssetId, { clearWhenDifferent: selectedAssetChanged })
6222
7302
  : false;
@@ -6224,8 +7304,7 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
6224
7304
  revealSelectedAssetBrowserItem(state.selectedAssetId);
6225
7305
  }
6226
7306
  revealPendingStatusDiagnosticAsset(state);
6227
- lastRenderedAssetBrowserTab = assetBrowserTab;
6228
- lastRenderedBottomDockTab = activeBottomTab;
7307
+ lastRenderedAssetBrowserContentKey = activeBottomTab === 'assets' ? assetBrowserContentKey : null;
6229
7308
  lastRenderedSelectedAssetId = state.selectedAssetId ?? null;
6230
7309
  restoreLocalEditorEditableFocus(doc, focusSnapshot);
6231
7310
  revealPendingStatusDiagnosticPath(state);
@@ -6240,12 +7319,7 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
6240
7319
  });
6241
7320
  rightDockTabs.style.display = rightDockVisibility.tabsDisplay;
6242
7321
  const activeBottomTab = panelRegistry.getBottomDockTab();
6243
- const assetBrowserTabChanged = assetBrowserTab !== lastRenderedAssetBrowserTab;
6244
- const bottomDockTabChanged = activeBottomTab !== lastRenderedBottomDockTab;
6245
7322
  const selectedAssetChanged = state.selectedAssetId !== lastRenderedSelectedAssetId;
6246
- const assetBrowserScrollSnapshot = activeBottomTab === 'assets'
6247
- ? assetBrowserViewportReveal.captureScrollSnapshot()
6248
- : { scrollTop: null };
6249
7323
  hierarchyPanel.dataset.editorPanelId = hierarchyDescriptor?.id ?? 'hierarchy';
6250
7324
  inspectorPanel.dataset.editorPanelId = activeRightTab === 'inspector' ? rightDescriptor?.id ?? 'inspector' : 'inspector';
6251
7325
  renderingPanel.dataset.editorPanelId = activeRightTab === 'rendering' ? rightDescriptor?.id ?? 'rendering' : 'rendering';
@@ -6257,10 +7331,18 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
6257
7331
  pluginsPanel.style.display = rightDockVisibility.pluginsDisplay;
6258
7332
  LocalEditorPanels.renderWorkbenchLeftDockTabs(doc, leftDockTabs, 'hierarchy', panelRegistry.getPanels('left'));
6259
7333
  hierarchyController.render(state);
6260
- LocalEditorPanels.renderWorkbenchBottomDockPanel(doc, assetPanel, state, activeBottomTab, panelRegistry.getPanels('bottom'), assetBrowserTab);
6261
- if (activeBottomTab === 'assets' && !assetBrowserTabChanged && !bottomDockTabChanged) {
6262
- assetBrowserViewportReveal.restoreScrollSnapshot(assetBrowserScrollSnapshot);
6263
- }
7334
+ LocalEditorPanels.renderWorkbenchBottomDockPanel(doc, assetPanel, state, activeBottomTab, panelRegistry.getPanels('bottom'), assetBrowserTab, { expandedFolderPaths: assetBrowserExpandedFolderPaths, folderPath: assetBrowserFolderPath, viewMode: assetBrowserViewMode, selectedEntryPath: selectedProjectEntryPath });
7335
+ if (activeBottomTab === 'assets') {
7336
+ assetBrowserFolderPath = assetPanel.dataset.editorAssetBrowserFolderPath ?? assetBrowserFolderPath;
7337
+ }
7338
+ const assetBrowserContentKey = resolveLocalEditorAssetBrowserContentKey({
7339
+ assetFilter: state.assetFilter,
7340
+ folderPath: assetBrowserFolderPath,
7341
+ tab: assetBrowserTab,
7342
+ viewMode: assetBrowserViewMode,
7343
+ });
7344
+ hydrateProjectMediaThumbnails();
7345
+ assetBrowserContinuity.restore(assetBrowserContinuitySnapshot, assetBrowserContentKey, activeBottomTab === 'assets');
6264
7346
  const suppressAssetScroll = state.selectedAssetId
6265
7347
  ? assetBrowserViewportReveal.consumeRevealSuppression(state.selectedAssetId, { clearWhenDifferent: selectedAssetChanged })
6266
7348
  : false;
@@ -6268,14 +7350,14 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
6268
7350
  revealSelectedAssetBrowserItem(state.selectedAssetId);
6269
7351
  }
6270
7352
  revealPendingStatusDiagnosticAsset(state);
6271
- lastRenderedAssetBrowserTab = assetBrowserTab;
6272
- lastRenderedBottomDockTab = activeBottomTab;
7353
+ lastRenderedAssetBrowserContentKey = activeBottomTab === 'assets' ? assetBrowserContentKey : null;
6273
7354
  lastRenderedSelectedAssetId = state.selectedAssetId;
6274
7355
  LocalEditorPanels.renderWorkbenchRightDockTabs(doc, rightDockTabs, activeRightTab, panelRegistry.getPanels('right'));
6275
7356
  const rightDockVisible = rightDockFormalSurfaceVisibility.isVisible();
6276
7357
  if (!rightDockVisible) {
6277
7358
  LocalEditorPanels.disposeInspectorPanelUi(inspectorPanel);
6278
7359
  LocalEditorPanels.disposeInspectorPanelUi(renderingPanel);
7360
+ lastRenderedInspectorContentKey = null;
6279
7361
  }
6280
7362
  else if (activeRightTab === 'inspector') {
6281
7363
  LocalEditorPanels.renderInspectorPanel(doc, inspectorPanel, state, inspectorFilter, {
@@ -6283,6 +7365,7 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
6283
7365
  layerHost: editorUiLayerHost,
6284
7366
  lockedVectorGroups: lockedInspectorVectorGroups,
6285
7367
  });
7368
+ lastRenderedInspectorContentKey = resolveLocalEditorRightDockContentKey(state, 'inspector', inspectorFilter);
6286
7369
  }
6287
7370
  else if (activeRightTab === 'rendering') {
6288
7371
  LocalEditorPanels.renderRenderingPanel(doc, renderingPanel, state, { layerHost: editorUiLayerHost });
@@ -6298,7 +7381,6 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
6298
7381
  revealPendingStatusDiagnosticPath(state);
6299
7382
  };
6300
7383
  const completedCleanup = new Set();
6301
- let uiDisposed = false;
6302
7384
  return {
6303
7385
  update(state) {
6304
7386
  render(state);
@@ -6307,7 +7389,6 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
6307
7389
  updateSceneFrameStats(stats) {
6308
7390
  if (currentState)
6309
7391
  currentState = { ...currentState, sceneFrameStats: stats };
6310
- renderSceneFrameRateOverlay(sceneFrameRateOverlay, currentState?.mode === 'editor' && !currentState.prefabStage ? stats : null);
6311
7392
  },
6312
7393
  updatePerformanceStats(stats) {
6313
7394
  if (currentState)
@@ -6330,7 +7411,10 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
6330
7411
  return activeTheme;
6331
7412
  },
6332
7413
  dispose() {
6333
- if (uiDisposed)
7414
+ if (canSkipLocalEditorPreviewCleanupPass(uiDisposed, [
7415
+ projectModelPreviewCleanupRegistry,
7416
+ materialPreviewCleanupRegistry,
7417
+ ]))
6334
7418
  return;
6335
7419
  const errors = [];
6336
7420
  const run = (key, operation) => {
@@ -6363,6 +7447,10 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
6363
7447
  run('toolbar-overflow-resize', () => {
6364
7448
  doc.defaultView?.removeEventListener('resize', scheduleToolbarOverflowLayout);
6365
7449
  });
7450
+ run('project-media-preview-request', () => {
7451
+ projectMediaPreviewRequestId += 1;
7452
+ assetPreviewOpen = false;
7453
+ });
6366
7454
  run('host-chrome', () => hostChrome.remove());
6367
7455
  run('inspector-ui', () => LocalEditorPanels.disposeInspectorPanelUi(inspectorPanel));
6368
7456
  run('rendering-ui', () => LocalEditorPanels.disposeInspectorPanelUi(renderingPanel));
@@ -6378,14 +7466,25 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
6378
7466
  run('overlay-settings-popover', () => overlaySettingsPopover.remove());
6379
7467
  run('spatial-overlay', () => spatialOverlay.root.remove());
6380
7468
  run('measurement-overlay', () => measurementOverlay.root.remove());
6381
- run('scene-frame-rate-overlay', () => sceneFrameRateOverlay.root.remove());
6382
7469
  run('performance-monitor-overlay', () => performanceMonitorOverlay.root.remove());
6383
7470
  run('prefab-stage-preview-layout', () => prefabStagePreviewLayout.root.remove());
6384
7471
  run('box-selection-overlay', () => boxSelectionOverlay.remove());
6385
7472
  run('shortcut-help-panel', () => shortcutHelpPanel.remove());
7473
+ run('project-model-preview', () => disposeProjectModelPreview());
7474
+ run('project-media-thumbnails', () => resetProjectMediaThumbnailCache());
7475
+ run('project-model-thumbnail-cache', () => projectModelThumbnailCache.dispose());
7476
+ run('material-preview', () => disposeMaterialPreview());
6386
7477
  run('asset-preview-modal', () => assetPreviewModal.remove());
7478
+ run('project-file-preview-modal', () => projectFilePreviewModal.remove());
7479
+ run('project-file-preview-search-frame', () => {
7480
+ if (projectFilePreviewSearchRaf != null) {
7481
+ doc.defaultView?.cancelAnimationFrame(projectFilePreviewSearchRaf);
7482
+ projectFilePreviewSearchRaf = null;
7483
+ }
7484
+ });
6387
7485
  run('asset-picker-modal', () => assetPickerModal.remove());
6388
7486
  run('hierarchy-controller', () => hierarchyController.dispose());
7487
+ run('editor-console', () => editorConsole.dispose());
6389
7488
  run('workbench-layout-controller', () => workbenchLayoutController.dispose());
6390
7489
  run('tooltip-controller', () => tooltipController.dispose());
6391
7490
  run('context-menu', () => contextMenu.dispose());
@@ -6393,12 +7492,20 @@ function createLocalEditorBrowserUiImplementation(options, construction) {
6393
7492
  run('input-router', () => inputRouter.dispose());
6394
7493
  run('document-keydown', () => doc.removeEventListener('keydown', onKeyDown));
6395
7494
  run('owned-root-children', () => construction.removeMountedRootChildren());
7495
+ projectModelPreviewCleanupRegistry.disposeOwned();
7496
+ materialPreviewCleanupRegistry.disposeOwned();
7497
+ const retainedPreviewCleanups = [
7498
+ ...projectModelPreviewCleanupRegistry.takeFailures(),
7499
+ ...materialPreviewCleanupRegistry.takeFailures(),
7500
+ ];
7501
+ errors.push(...retainedPreviewCleanups);
6396
7502
  if (errors.length > 0) {
6397
7503
  const error = new Error('Local Editor browser UI cleanup failed.');
6398
7504
  error.errors = errors;
6399
7505
  throw error;
6400
7506
  }
6401
- uiDisposed = true;
7507
+ uiDisposed = !projectModelPreviewCleanupRegistry.hasOwned()
7508
+ && !materialPreviewCleanupRegistry.hasOwned();
6402
7509
  },
6403
7510
  };
6404
7511
  }