@pioneer_zmc/dsh-workbench 0.2.1

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 (168) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +98 -0
  3. package/README.zh.md +100 -0
  4. package/client.js +122 -0
  5. package/cordis.patch.yml +8 -0
  6. package/dist/check-building.js +1669 -0
  7. package/dist/check-room.js +980 -0
  8. package/dist/index.js +2873 -0
  9. package/package.json +60 -0
  10. package/skills/specdev-building/SKILL.md +85 -0
  11. package/skills/specdev-building/assets/layouts/court.json +53 -0
  12. package/skills/specdev-building/assets/layouts/row.json +48 -0
  13. package/skills/specdev-building/assets/layouts/single.json +27 -0
  14. package/skills/specdev-building/assets/layouts/tower-9.json +71 -0
  15. package/skills/specdev-building/assets/layouts/tower.json +59 -0
  16. package/skills/specdev-building/references/blueprint-contract.md +126 -0
  17. package/skills/specdev-building/references/diagnostics.md +101 -0
  18. package/skills/specdev-building/references/layouts.md +112 -0
  19. package/skills/specdev-business/SKILL.md +91 -0
  20. package/skills/specdev-business/assets/plan-template.md +66 -0
  21. package/skills/specdev-business/assets/spec-template.md +111 -0
  22. package/skills/specdev-business/assets/tasks-template.md +36 -0
  23. package/skills/specdev-business/references/change.md +117 -0
  24. package/skills/specdev-business/references/evidence-review.md +125 -0
  25. package/skills/specdev-business/references/implementation.md +67 -0
  26. package/skills/specdev-business/references/project-contract.md +211 -0
  27. package/skills/specdev-business/references/source-map.md +110 -0
  28. package/skills/specdev-business/references/specification.md +93 -0
  29. package/skills/specdev-business/references/workflow.md +126 -0
  30. package/skills/specdev-room/SKILL.md +77 -0
  31. package/skills/specdev-room/assets/intro-room.json +14 -0
  32. package/skills/specdev-room/references/diagnostics.md +21 -0
  33. package/skills/specdev-room/references/room-contract.md +59 -0
  34. package/vendor/archify-renderer/VERSION.md +52 -0
  35. package/vendor/archify-renderer/archify/LICENSE +22 -0
  36. package/vendor/archify-renderer/archify/THIRD_PARTY_NOTICES.md +56 -0
  37. package/vendor/archify-renderer/archify/assets/template.html +14787 -0
  38. package/vendor/archify-renderer/archify/bin/archify.mjs +1990 -0
  39. package/vendor/archify-renderer/archify/renderers/shared/brand-marks.mjs +563 -0
  40. package/vendor/archify-renderer/archify/renderers/shared/cli.mjs +218 -0
  41. package/vendor/archify-renderer/archify/renderers/shared/desktop-readability.mjs +26 -0
  42. package/vendor/archify-renderer/archify/renderers/shared/diagnostics.mjs +127 -0
  43. package/vendor/archify-renderer/archify/renderers/shared/engineering-profiles.mjs +157 -0
  44. package/vendor/archify-renderer/archify/renderers/shared/generated-brand-marks.mjs +2003 -0
  45. package/vendor/archify-renderer/archify/renderers/shared/generated-validators.mjs +13 -0
  46. package/vendor/archify-renderer/archify/renderers/shared/geometry.mjs +1423 -0
  47. package/vendor/archify-renderer/archify/renderers/shared/i18n.mjs +594 -0
  48. package/vendor/archify-renderer/archify/renderers/shared/legend.mjs +217 -0
  49. package/vendor/archify-renderer/archify/renderers/shared/output-path.mjs +321 -0
  50. package/vendor/archify-renderer/archify/renderers/shared/repository-evidence.mjs +235 -0
  51. package/vendor/archify-renderer/archify/renderers/shared/text-fit.mjs +49 -0
  52. package/vendor/archify-renderer/archify/renderers/shared/utils.mjs +232 -0
  53. package/vendor/archify-renderer/archify/renderers/shared/validator.mjs +86 -0
  54. package/vendor/archify-renderer/archify/renderers/workflow/render-workflow.mjs +35 -0
  55. package/vendor/archify-renderer/archify/renderers/workflow/workflow-compiler.mjs +4400 -0
  56. package/vendor/archify-renderer/archify/renderers/workflow/workflow-migration-geometry.mjs +144 -0
  57. package/vendor/archify-renderer/archify/schemas/common.schema.json +115 -0
  58. package/vendor/archify-renderer/archify/schemas/workflow.schema.json +428 -0
  59. package/vendor/archify-renderer/archify/scripts/check-render-output.mjs +835 -0
  60. package/vendor/archify-renderer/shims/process.mjs +18 -0
  61. package/vendor/archify-renderer/stubs/child_process.mjs +2 -0
  62. package/vendor/archify-renderer/stubs/crypto.mjs +3 -0
  63. package/vendor/archify-renderer/stubs/dns-promises.mjs +2 -0
  64. package/vendor/archify-renderer/stubs/fs.mjs +3 -0
  65. package/vendor/archify-renderer/stubs/http.mjs +3 -0
  66. package/vendor/archify-renderer/stubs/https.mjs +3 -0
  67. package/vendor/archify-renderer/stubs/net.mjs +3 -0
  68. package/vendor/archify-renderer/stubs/path.mjs +3 -0
  69. package/vendor/archify-renderer/stubs/url.mjs +3 -0
  70. package/web/assets/building-kit.js +1157 -0
  71. package/web/assets/building-look.js +185 -0
  72. package/web/assets/building-plan.js +495 -0
  73. package/web/assets/building-scene.js +529 -0
  74. package/web/assets/building-walk.js +248 -0
  75. package/web/assets/building.js +859 -0
  76. package/web/assets/business.js +124 -0
  77. package/web/assets/chart-link.js +165 -0
  78. package/web/assets/common.js +302 -0
  79. package/web/assets/details.js +197 -0
  80. package/web/assets/evidence.js +30 -0
  81. package/web/assets/furniture/SOURCES.md +24 -0
  82. package/web/assets/furniture/assets/ceramic-display-stand-01/asset.json +25 -0
  83. package/web/assets/furniture/assets/ceramic-display-stand-01/model.js +116 -0
  84. package/web/assets/furniture/assets/ceramic-display-stand-01/preview.webp +0 -0
  85. package/web/assets/furniture/assets/ceramic-floor-lamp-01/asset.json +61 -0
  86. package/web/assets/furniture/assets/ceramic-floor-lamp-01/model.js +56 -0
  87. package/web/assets/furniture/assets/ceramic-floor-lamp-01/preview.webp +0 -0
  88. package/web/assets/furniture/assets/ceramic-low-cabinet-01/asset.json +81 -0
  89. package/web/assets/furniture/assets/ceramic-low-cabinet-01/model.js +67 -0
  90. package/web/assets/furniture/assets/ceramic-low-cabinet-01/preview.webp +0 -0
  91. package/web/assets/furniture/assets/ceramic-vase-01/asset.json +21 -0
  92. package/web/assets/furniture/assets/ceramic-vase-01/model.js +119 -0
  93. package/web/assets/furniture/assets/ceramic-vase-01/preview.webp +0 -0
  94. package/web/assets/furniture/assets/fairy-bed-01/asset.json +61 -0
  95. package/web/assets/furniture/assets/fairy-bed-01/model.js +63 -0
  96. package/web/assets/furniture/assets/fairy-bed-01/preview.webp +0 -0
  97. package/web/assets/furniture/assets/fairy-book-stack-01/asset.json +21 -0
  98. package/web/assets/furniture/assets/fairy-book-stack-01/model.js +71 -0
  99. package/web/assets/furniture/assets/fairy-book-stack-01/preview.webp +0 -0
  100. package/web/assets/furniture/assets/fairy-bookshelf-01/asset.json +32 -0
  101. package/web/assets/furniture/assets/fairy-bookshelf-01/model.js +168 -0
  102. package/web/assets/furniture/assets/fairy-bookshelf-01/preview.webp +0 -0
  103. package/web/assets/furniture/assets/fairy-desk-01/asset.json +25 -0
  104. package/web/assets/furniture/assets/fairy-desk-01/model.js +132 -0
  105. package/web/assets/furniture/assets/fairy-desk-01/preview.webp +0 -0
  106. package/web/assets/furniture/assets/fairy-notice-board-01/asset.json +23 -0
  107. package/web/assets/furniture/assets/fairy-notice-board-01/model.js +125 -0
  108. package/web/assets/furniture/assets/fairy-notice-board-01/preview.webp +0 -0
  109. package/web/assets/furniture/assets/fairy-potted-plant-01/asset.json +21 -0
  110. package/web/assets/furniture/assets/fairy-potted-plant-01/model.js +128 -0
  111. package/web/assets/furniture/assets/fairy-potted-plant-01/preview.webp +0 -0
  112. package/web/assets/furniture/assets/fairy-reading-chair-01/asset.json +61 -0
  113. package/web/assets/furniture/assets/fairy-reading-chair-01/model.js +66 -0
  114. package/web/assets/furniture/assets/fairy-reading-chair-01/preview.webp +0 -0
  115. package/web/assets/furniture/assets/fairy-table-lamp-01/asset.json +21 -0
  116. package/web/assets/furniture/assets/fairy-table-lamp-01/model.js +182 -0
  117. package/web/assets/furniture/assets/fairy-table-lamp-01/preview.webp +0 -0
  118. package/web/assets/furniture/catalog.js +137 -0
  119. package/web/assets/furniture/index.json +110 -0
  120. package/web/assets/furniture/registry.js +72 -0
  121. package/web/assets/furniture-page.css +68 -0
  122. package/web/assets/furniture-page.js +302 -0
  123. package/web/assets/home.js +65 -0
  124. package/web/assets/node-detail.js +148 -0
  125. package/web/assets/panel-link.js +47 -0
  126. package/web/assets/read.js +746 -0
  127. package/web/assets/room-scene.js +386 -0
  128. package/web/assets/room.css +287 -0
  129. package/web/assets/room.js +437 -0
  130. package/web/assets/rooms/bindings.js +449 -0
  131. package/web/assets/rooms/ceramic-reflection.js +193 -0
  132. package/web/assets/rooms/ceramic-room.js +302 -0
  133. package/web/assets/rooms/door.js +35 -0
  134. package/web/assets/rooms/fairy-room.js +492 -0
  135. package/web/assets/rooms/furniture-assembly.js +113 -0
  136. package/web/assets/rooms/navigation.js +262 -0
  137. package/web/assets/rooms/placeholder-catalog.json +71 -0
  138. package/web/assets/rooms/placeholder-furniture.js +76 -0
  139. package/web/assets/rooms/placement.js +588 -0
  140. package/web/assets/rooms/reader.js +441 -0
  141. package/web/assets/rooms/reading-gate.js +157 -0
  142. package/web/assets/rooms/room-lighting.js +50 -0
  143. package/web/assets/rooms/templates.json +50 -0
  144. package/web/assets/rooms/walk-frame.js +64 -0
  145. package/web/assets/rooms/walk-session.js +138 -0
  146. package/web/assets/save-result.js +89 -0
  147. package/web/assets/screenshots/business.png +0 -0
  148. package/web/assets/screenshots/node-detail.png +0 -0
  149. package/web/assets/screenshots/project-home.png +0 -0
  150. package/web/assets/screenshots/workflow.png +0 -0
  151. package/web/assets/showcase.js +232 -0
  152. package/web/assets/style.css +836 -0
  153. package/web/assets/template.js +7 -0
  154. package/web/assets/theme.js +29 -0
  155. package/web/assets/three/OrbitControls.js +1972 -0
  156. package/web/assets/three/VERSION.md +12 -0
  157. package/web/assets/three/three.core.js +60586 -0
  158. package/web/assets/three/three.module.js +19719 -0
  159. package/web/assets/workspaces.css +21 -0
  160. package/web/assets/workspaces.js +81 -0
  161. package/web/building.html +131 -0
  162. package/web/business.html +43 -0
  163. package/web/furniture.html +47 -0
  164. package/web/index.html +57 -0
  165. package/web/read.html +107 -0
  166. package/web/room.html +146 -0
  167. package/web/showcase.html +50 -0
  168. package/web/workspaces.html +32 -0
@@ -0,0 +1,441 @@
1
+ // 房间阅读面板(E3b-2;E9c 扩展流程图):点书架、告示牌或"业务资料"后在房间之上
2
+ // 打开的资料集合与阅读区。文档继续本模块正文阅读;流程图复用既有 read 页面装进
3
+ // iframe(不开新标签、不另造图渲染)。本模块只负责 DOM 面板、资料请求、焦点与请求
4
+ // 失效;不 import Three、场景、旧建筑模型或业务清单读取代码。解析内核复用 details.js
5
+ //(parseDocument/splitInline),正文与行内标记的 DOM 做法迁用 building.js 的
6
+ // renderDocumentBody/appendInline(不经 innerHTML,业务数据里的 HTML 字符一律按文字显示)。
7
+ // 流程图的打开做法迁用 building.js 的 openWorkflowReader:先 fetch 同工作区阅读入口,
8
+ // 取不到如实报错并可重试;取到再设 iframe 地址——预检 200 只证明 read.html 可达,
9
+ // 图文件缺失、坏 JSON 和编译问题由阅读页自己显示,不用 iframe 的 error 事件判断。
10
+ //
11
+ // 暂停与恢复不在本模块:open/close 时经 onReadingChange 通知页面(页面调场景句柄
12
+ // setReading 并同步按钮),重复 open 不重复暂停,dispose 不触发恢复。
13
+ // iframe 里的阅读页按既有约定(panel-link.js)把 ESC 转发回来关面板;它自己的
14
+ // 节点详情等 dialog 开着时先按原行为关 dialog,不提前退出面板——本模块不改 read.js。
15
+ import { el, wsUrl } from '../common.js'
16
+ import { parseDocument, splitInline } from '../details.js'
17
+ import { listenMaterialClose } from '../panel-link.js'
18
+
19
+ /** 接口失败时优先说后端给的中文原因(JSON 错误体的 error 字段),拿不到再按 HTTP 状态说。
20
+ * 与 building.js 同口径(迁用,不 import 整个页面脚本)。 */
21
+ async function failureReason(response) {
22
+ try {
23
+ const body = await response.json()
24
+ if (body && typeof body.error === 'string' && body.error) return body.error
25
+ } catch { /* 正文不是 JSON:按状态码说。 */ }
26
+ return `HTTP ${response.status}`
27
+ }
28
+
29
+ /**
30
+ * 房间阅读面板。
31
+ * @param root room.html 里唯一的阅读覆盖层(面板放在 viewport 外、roomStage 内,
32
+ * 背景视口加 inert 不会波及面板)
33
+ * @param onReadingChange 开/关面板时各通知一次(true=进入阅读,false=退出);页面负责
34
+ * 暂停场景与按钮同步
35
+ * @param focusFallback 归还焦点时的兜底元素(触发元素已不在文档里时用)
36
+ * @returns { open({ title, entries, emptyText, returnFocus }), close(), isOpen(), dispose() }
37
+ * entries 条目来自 resolver 已验证的目录({ kind: 'document'|'workflow', … }),本模块
38
+ * 不自行造链接;文档条目带 path,图条目带 chartId(unavailableReason 的登记图不发请求)。
39
+ */
40
+
41
+ /** 资料条目的稳定身份(与绑定层同口径):文档=登记路径、流程图=图 ID;不按标题区分。 */
42
+ const materialKey = (entry) => (entry.kind === 'workflow' ? `chart:${entry.chartId}` : `doc:${entry.path}`)
43
+
44
+ /** 清单与标题里的人话前缀。 */
45
+ const kindLabel = (entry) => (entry.kind === 'workflow' ? '流程图' : '文档')
46
+
47
+ export function createRoomReader({ root, onReadingChange = () => {}, focusFallback = null } = {}) {
48
+ const panel = root.querySelector('#readerPanel')
49
+ const title = root.querySelector('#readerTitle')
50
+ const closeBtn = root.querySelector('#readerClose')
51
+ const listLabel = root.querySelector('#readerListLabel')
52
+ const list = root.querySelector('#readerList')
53
+ const count = root.querySelector('#readerCount')
54
+ const listEmpty = root.querySelector('#readerEmpty')
55
+ const hint = root.querySelector('#readerHint')
56
+ const docTitle = root.querySelector('#readerDocTitle')
57
+ const status = root.querySelector('#readerStatus')
58
+ const body = root.querySelector('#readerBody')
59
+ const retryLine = root.querySelector('#readerRetryLine')
60
+ const retryBtn = root.querySelector('#readerRetry')
61
+ const frameHost = root.querySelector('#readerFrameHost')
62
+ const listToggle = root.querySelector('#readerListToggle')
63
+ const sizeToggle = root.querySelector('#readerSizeToggle')
64
+ const side = root.querySelector('.material-side')
65
+ const startGuard = root.querySelector('#readerFocusStart')
66
+ const endGuard = root.querySelector('#readerFocusEnd')
67
+
68
+ let openState = false
69
+ let disposed = false
70
+ // 左侧清单收缩(作者令 2026-10-01):换集合(open)时重置为展开——新集合要看清单;
71
+ // 同一次打开内自由切换。面板放大跨 open 保持——窗口大小是阅读偏好,不随换家具回弹。
72
+ let listCollapsed = false
73
+ let enlarged = false
74
+ // 单调请求序号=最终写入条件:换资料、换集合(resetReader)、关闭、dispose 都使旧序号
75
+ // 失效;成功与失败分支在异步正文/错误体读取之后都复查"当前请求、面板仍开着、控制器仍存活"。
76
+ let requestSeq = 0
77
+ let currentEntry = null
78
+ let returnFocus = null
79
+ // 当前集合的条目表(按钮 → 已验证 entry,E3b-2 验收补齐 R1):条目点击经 list 一次性
80
+ // 委托分发,只认这张表——不从 DOM 的 dataset/path 重建资料链接;换集合清空、销毁清空。
81
+ const entryByButton = new Map()
82
+ // 图阅读页(E9c):同时最多一个 iframe。每次选图新建节点,切走/换集合/关闭/销毁时
83
+ // 清空导航(about:blank)、移除节点并解绑关闭消息监听——旧 load/消息不能改写新资料,
84
+ // 也不能在关闭后复活面板。
85
+ let frame = null
86
+ let unlistenClose = null
87
+
88
+ const setNote = (kind, text) => {
89
+ status.hidden = !text
90
+ status.textContent = text || ''
91
+ status.dataset.kind = kind || ''
92
+ }
93
+
94
+ /** 撤掉当前图 iframe:先解绑关闭消息,再清空导航并移除节点。幂等。 */
95
+ function clearFrame() {
96
+ if (unlistenClose) {
97
+ unlistenClose()
98
+ unlistenClose = null
99
+ }
100
+ if (frame) {
101
+ frame.setAttribute('src', 'about:blank')
102
+ frame.remove()
103
+ frame = null
104
+ }
105
+ frameHost.textContent = ''
106
+ frameHost.hidden = true
107
+ }
108
+
109
+ const markActive = () => {
110
+ const currentKey = currentEntry ? materialKey(currentEntry) : ''
111
+ for (const button of list.querySelectorAll('.material-item')) {
112
+ if (currentKey && button.dataset.material === currentKey) button.setAttribute('aria-current', 'true')
113
+ else button.removeAttribute('aria-current')
114
+ }
115
+ }
116
+
117
+ /** 换集合、换资料、关闭都走这里:使在途请求失效并回到"还没挑资料"。 */
118
+ const resetReader = () => {
119
+ requestSeq += 1
120
+ currentEntry = null
121
+ clearFrame()
122
+ body.textContent = ''
123
+ body.hidden = true
124
+ retryLine.hidden = true
125
+ docTitle.hidden = true
126
+ setNote('', '')
127
+ hint.textContent = '选择一份资料开始阅读'
128
+ hint.hidden = false
129
+ markActive()
130
+ }
131
+
132
+ const renderList = (entries, emptyText) => {
133
+ list.textContent = '' // 旧按钮随清空移除(脱离文档的引用无法再冒泡到委托层)
134
+ entryByButton.clear() // 换集合:旧条目表整体作废
135
+ const empty = entries.length === 0
136
+ listLabel.hidden = empty
137
+ list.hidden = empty
138
+ listEmpty.hidden = !empty
139
+ listEmpty.textContent = empty ? emptyText : ''
140
+ count.textContent = empty ? '' : `${entries.length} 份`
141
+ for (const entry of entries) {
142
+ const item = el('li')
143
+ const button = el('button', 'material-item')
144
+ button.type = 'button'
145
+ button.dataset.kind = entry.kind
146
+ button.dataset.material = materialKey(entry)
147
+ button.textContent = `${kindLabel(entry)} · ${entry.title}`
148
+ entryByButton.set(button, entry)
149
+ item.appendChild(button)
150
+ list.appendChild(item)
151
+ }
152
+ }
153
+
154
+ const showFailure = (reason) => {
155
+ setNote('bad', `读不到这份资料:${reason}。可以点“重试”再取一次。`)
156
+ retryLine.hidden = false
157
+ }
158
+
159
+ /** 取一份文档的正文(不缓存:每次点击都重新请求,服务端仍负责当时的登记与文件权限)。
160
+ * 入口先于一切 DOM 写入与请求检查存活与打开:销毁后或面板已关时,旧按钮引用
161
+ * (含已脱离文档的)不能再触发加载状态或发起新请求(E3b-2 验收补齐 R1)。 */
162
+ async function openEntry(entry) {
163
+ if (disposed || !openState) return
164
+ requestSeq += 1 // 所有切换都先作废旧请求,包括不发请求的不可用图。
165
+ currentEntry = entry
166
+ markActive()
167
+ hint.hidden = true
168
+ docTitle.textContent = `${kindLabel(entry)} · ${entry.title}`
169
+ docTitle.hidden = false
170
+ body.textContent = ''
171
+ body.hidden = true
172
+ retryLine.hidden = true
173
+ clearFrame() // 换目标:旧图先让位(含解绑监听),文档与图互不残留
174
+ return entry.kind === 'workflow' ? openChart(entry) : openDocument(entry)
175
+ }
176
+
177
+ /** 文档正文:错误体读取也是等待,期间用户可能已换目标/关面板。 */
178
+ async function openDocument(entry) {
179
+ setNote('info', '正在读取文档…')
180
+ const seq = ++requestSeq
181
+ try {
182
+ const response = await fetch(wsUrl(entry.href))
183
+ if (seq !== requestSeq || !openState || disposed) return
184
+ if (!response.ok) {
185
+ const reason = await failureReason(response) // 异步读错误体期间用户可能已换目标
186
+ if (seq !== requestSeq || !openState || disposed) return
187
+ return showFailure(reason)
188
+ }
189
+ const text = await response.text()
190
+ if (seq !== requestSeq || !openState || disposed) return
191
+ setNote('', '')
192
+ renderDocumentBody(text)
193
+ } catch (error) {
194
+ if (seq !== requestSeq || !openState || disposed) return
195
+ showFailure(error instanceof Error ? error.message : String(error))
196
+ }
197
+ }
198
+
199
+ /** 流程图(E9c):入口预检(同 building.js 做法)成功后新建 iframe 装既有阅读页;
200
+ * 重试按当前资料类型重新走本入口。坏登记条目(unavailableReason)不发网络请求,
201
+ * 只显示说明——刷新房间后可重新读取登记状态。 */
202
+ async function openChart(entry) {
203
+ if (entry.unavailableReason) {
204
+ setNote('bad', `这张流程图当前不可用:${entry.unavailableReason}。刷新房间后可重新读取登记状态。`)
205
+ retryLine.hidden = true // 不发请求的说明不是网络失败,不给重试按钮
206
+ return
207
+ }
208
+ setNote('info', '正在打开阅读页…')
209
+ const seq = ++requestSeq
210
+ const url = wsUrl(entry.href)
211
+ let response
212
+ try {
213
+ response = await fetch(url)
214
+ } catch (error) {
215
+ if (seq !== requestSeq || !openState || disposed) return
216
+ return showFailure(`阅读页没有打开(${error instanceof Error ? error.message : String(error)})`)
217
+ }
218
+ if (seq !== requestSeq || !openState || disposed) return
219
+ if (!response.ok) {
220
+ const reason = await failureReason(response) // 异步读错误体期间用户可能已换目标
221
+ if (seq !== requestSeq || !openState || disposed) return
222
+ return showFailure(`阅读页打不开(${reason})`)
223
+ }
224
+ if (seq !== requestSeq || !openState || disposed) return
225
+ // 说明行留到用户换资料/关闭为止:iframe 导航失败不触发 error 事件,"正在打开"
226
+ // 比一片空白诚实;图内容问题由阅读页自己那套说法显示(缺图、坏 JSON、编号冲突)。
227
+ const node = el('iframe', 'material-reader-frame')
228
+ node.title = `流程图阅读:${entry.title}`
229
+ frameHost.textContent = ''
230
+ frameHost.appendChild(node)
231
+ frameHost.hidden = false
232
+ frame = node
233
+ node.setAttribute('src', url)
234
+ // 阅读页里按 ESC 会经 panel-link.js 转发关面板;只认当前 iframe 的同源消息。
235
+ unlistenClose = listenMaterialClose(window, frame, () => close())
236
+ }
237
+
238
+ /** 正文按块呈现(迁用 building.js 的 DOM 做法):标题成标题、列表成列表、空文件给空态。 */
239
+ function renderDocumentBody(text) {
240
+ body.textContent = ''
241
+ const blocks = parseDocument(text)
242
+ if (!blocks.length) {
243
+ const empty = el('p', 'material-doc-empty')
244
+ empty.textContent = '这份文档没有正文(文件是空的)。'
245
+ body.appendChild(empty)
246
+ }
247
+ let listNode = null
248
+ for (const block of blocks) {
249
+ if (block.kind === 'bullet') {
250
+ if (!listNode) {
251
+ listNode = el('ul', 'material-doc-list')
252
+ body.appendChild(listNode)
253
+ }
254
+ const item = el('li')
255
+ appendInline(item, block.text)
256
+ listNode.appendChild(item)
257
+ continue
258
+ }
259
+ listNode = null
260
+ if (block.kind === 'heading') {
261
+ const heading = el('h3', 'material-doc-heading')
262
+ heading.dataset.level = String(block.level)
263
+ appendInline(heading, block.text)
264
+ body.appendChild(heading)
265
+ continue
266
+ }
267
+ const paragraph = el('p', 'material-doc-paragraph')
268
+ appendInline(paragraph, block.text)
269
+ body.appendChild(paragraph)
270
+ }
271
+ body.hidden = false
272
+ }
273
+
274
+ /** 行内 **加粗**/`代码` 拼成 DOM,不经 innerHTML(迁用 building.js)。 */
275
+ function appendInline(host, text) {
276
+ for (const part of splitInline(text)) {
277
+ if (part.strong) {
278
+ const node = el('strong')
279
+ node.textContent = part.text
280
+ host.appendChild(node)
281
+ } else if (part.code) {
282
+ const node = el('code')
283
+ node.textContent = part.text
284
+ host.appendChild(node)
285
+ } else {
286
+ host.appendChild(document.createTextNode(part.text))
287
+ }
288
+ }
289
+ }
290
+
291
+ /** 清单收缩与面板放大(作者令 2026-10-01)只改 root 的 data 属性,样式在 room.css;
292
+ * 按钮文案与 aria-pressed 同步,两按钮照常参与 Tab 焦点循环。 */
293
+ const syncListToggle = () => {
294
+ side.hidden = listCollapsed // DOM 与样式同步隐藏,焦点循环也能排除收起清单。
295
+ if (listCollapsed) root.dataset.list = 'collapsed'
296
+ else delete root.dataset.list
297
+ listToggle.textContent = listCollapsed ? '展开清单' : '收起清单'
298
+ listToggle.setAttribute('aria-pressed', String(listCollapsed))
299
+ }
300
+ const syncSizeToggle = () => {
301
+ if (enlarged) root.dataset.size = 'large'
302
+ else delete root.dataset.size
303
+ sizeToggle.textContent = enlarged ? '还原' : '放大'
304
+ sizeToggle.setAttribute('aria-pressed', String(enlarged))
305
+ }
306
+
307
+ function onListToggleClick() {
308
+ if (disposed) return
309
+ listCollapsed = !listCollapsed
310
+ syncListToggle()
311
+ }
312
+
313
+ function onSizeToggleClick() {
314
+ if (disposed) return
315
+ enlarged = !enlarged
316
+ syncSizeToggle()
317
+ }
318
+
319
+ function open({ title: heading = '', entries = [], emptyText = '', returnFocus: focusTarget = null } = {}) {
320
+ if (disposed) return
321
+ const wasOpen = openState
322
+ openState = true
323
+ returnFocus = focusTarget || focusFallback
324
+ title.textContent = heading
325
+ renderList(Array.isArray(entries) ? entries : [], emptyText)
326
+ listCollapsed = false // 换集合重置清单为展开(新集合要看清单);放大状态保持
327
+ syncListToggle()
328
+ resetReader()
329
+ root.hidden = false
330
+ if (wasOpen) return // 已打开时换集合:不重复暂停、不动焦点
331
+ onReadingChange(true)
332
+ closeBtn.focus()
333
+ }
334
+
335
+ function close() {
336
+ if (disposed || !openState) return
337
+ openState = false
338
+ requestSeq += 1 // 关闭后晚到的响应不能重新打开面板或写正文
339
+ currentEntry = null
340
+ clearFrame() // 关面板:停掉图阅读页与它的关闭消息监听(dispose 也不再补发恢复)
341
+ root.hidden = true
342
+ onReadingChange(false) // 页面先解除背景 inert,焦点才能落回触发元素
343
+ const target = returnFocus
344
+ returnFocus = null
345
+ if (target && target.isConnected) target.focus?.()
346
+ else if (focusFallback && focusFallback.isConnected) focusFallback.focus?.()
347
+ }
348
+
349
+ function dispose() {
350
+ if (disposed) return
351
+ disposed = true
352
+ openState = false
353
+ requestSeq += 1
354
+ currentEntry = null
355
+ returnFocus = null
356
+ clearFrame() // 销毁后旧条目引用/委托不再请求或写 DOM(幂等:clearFrame 重复调用安全)
357
+ root.removeEventListener('keydown', onRootKeydown)
358
+ closeBtn.removeEventListener('click', onCloseClick)
359
+ retryBtn.removeEventListener('click', onRetryClick)
360
+ list.removeEventListener('click', onListClick)
361
+ listToggle.removeEventListener('click', onListToggleClick)
362
+ sizeToggle.removeEventListener('click', onSizeToggleClick)
363
+ startGuard.removeEventListener('focus', onStartGuardFocus)
364
+ endGuard.removeEventListener('focus', onEndGuardFocus)
365
+ entryByButton.clear()
366
+ }
367
+
368
+ function onCloseClick() { close() }
369
+
370
+ function onRetryClick() {
371
+ if (disposed || !openState || !currentEntry) return
372
+ openEntry(currentEntry) // 重试取当前资料类型:文档重取正文、图重走入口预检
373
+ }
374
+
375
+ const visibleFocusables = () => [...root.querySelectorAll('button:not([disabled]), iframe')]
376
+ .filter((node) => !node.closest('[hidden]'))
377
+
378
+ // 子文档键盘事件不会冒泡到父页:靠原生 Tab 离开 iframe 后到达的边界守卫回环。
379
+ function onStartGuardFocus() {
380
+ if (disposed || !openState) return
381
+ const nodes = visibleFocusables()
382
+ nodes.at(-1)?.focus()
383
+ }
384
+ function onEndGuardFocus() {
385
+ if (disposed || !openState) return
386
+ visibleFocusables()[0]?.focus()
387
+ }
388
+
389
+ /** 只在阅读开启时拦截 Escape/Tab,不影响原页面键盘操作。
390
+ * 焦点循环收可见按钮与可见图 iframe(E9c):键盘可以进图、离图、到关闭按钮并回环,
391
+ * 不深入改子页控件。iframe 里的 ESC 由 read.js 经 panel-link.js 转发回来,不在这里处理。 */
392
+ function onRootKeydown(event) {
393
+ if (disposed || !openState) return
394
+ if (event.key === 'Escape') {
395
+ event.preventDefault()
396
+ close()
397
+ return
398
+ }
399
+ if (event.key !== 'Tab') return
400
+ // 只算真正露着的控件(隐藏的重试行、空目录与收起的 iframe 不参与循环)。
401
+ const focusables = visibleFocusables()
402
+ if (focusables.length === 0) return
403
+ const first = focusables[0]
404
+ const last = focusables[focusables.length - 1]
405
+ const inside = root.contains(document.activeElement)
406
+ if (event.shiftKey && (document.activeElement === first || !inside)) {
407
+ event.preventDefault()
408
+ last.focus()
409
+ } else if (!event.shiftKey && (document.activeElement === last || !inside)) {
410
+ event.preventDefault()
411
+ first.focus()
412
+ }
413
+ }
414
+
415
+ /** 条目点击在 list 上一次性委托分发(E3b-2 验收补齐 R1):只认当前条目表里的
416
+ * 按钮——换集合后残留的旧按钮、销毁后保存的按钮引用都不处理;资料链接不从
417
+ * DOM 重建,entry 永远来自页面交入并经 resolver 验证的集合。 */
418
+ function onListClick(event) {
419
+ const button = event.target instanceof Element ? event.target.closest('.material-item') : null
420
+ if (!button || !list.contains(button)) return
421
+ const entry = entryByButton.get(button)
422
+ if (!entry) return
423
+ void openEntry(entry)
424
+ }
425
+
426
+ root.addEventListener('keydown', onRootKeydown)
427
+ closeBtn.addEventListener('click', onCloseClick)
428
+ retryBtn.addEventListener('click', onRetryClick)
429
+ list.addEventListener('click', onListClick)
430
+ listToggle.addEventListener('click', onListToggleClick)
431
+ sizeToggle.addEventListener('click', onSizeToggleClick)
432
+ startGuard.addEventListener('focus', onStartGuardFocus)
433
+ endGuard.addEventListener('focus', onEndGuardFocus)
434
+
435
+ return {
436
+ open,
437
+ close,
438
+ isOpen: () => !disposed && openState,
439
+ dispose,
440
+ }
441
+ }
@@ -0,0 +1,157 @@
1
+ // 独立房间的阅读暂停闸门(E3b-1):阅读开始时停住场景的全部操作输入,
2
+ // 阅读结束后仍在原模式、原位置、原视线继续。动画氛围与渲染由场景照常推进,
3
+ // 本模块不冻结画面。只协调输入与帧推进——不读配置、不访问文档、不计算碰撞、
4
+ // 不保存第二份人物位置、不建 Three 渲染器;站位与视线始终留在会话/帧推进器,
5
+ // 模式始终由场景函数切换(本模块只按 getMode() 读,不改模式)。
6
+ //
7
+ // 为什么不能只在页面遮住画面或隐藏按钮:键盘监听仍在(WASD 会继续走动);
8
+ // OrbitControls 的 enabled=false 也拦不住程序继续 update() 推进阻尼——
9
+ // r186 的 update 不检查 enabled。因此暂停做三件事:
10
+ // 1) 输入器 setEnabled(false)(键盘/拖动/虚拟按键同一道闸)+clear+resetClock;
11
+ // 2) OrbitControls disconnect() 结束未完成手势并拆监听,随后用公开接口
12
+ // 消耗残余阻尼并同步回原机位(见 pauseOverview 的步骤注释);
13
+ // 3) 场景侧 pointerdown/up 在阅读中直接忽略;clearPick 在暂停时清掉未完成
14
+ // 拾取,恢复后不把暂停前的按下与恢复后的抬起拼成新点击。
15
+
16
+ /**
17
+ * 建一个阅读暂停闸门。依赖均由场景传入:原版 createWalkInput 句柄、
18
+ * 已创建并连接的 OrbitControls、行走帧推进器 walk-frame 句柄、读场景当前
19
+ * 模式的 getMode('walk'|'overview')、清掉未完成拾取的 clearPick。
20
+ * 返回 { setReading, isReading, syncMode, advance, dispose }。
21
+ */
22
+ export function createRoomReadingGate({ input, controls, walkFrame, getMode, clearPick }) {
23
+ let reading = false
24
+ let disposed = false
25
+ // 场景创建 controls 时构造函数已自动 connect;这里只记录是否由本闸门断开,
26
+ // 恢复时按记录重连原 domElement,避免重复连接。
27
+ let controlsConnected = true
28
+
29
+ /** 总览机位快照:相机 position/quaternion/zoom 与 controls.target(暂停前后保持)。 */
30
+ function snapshotCamera() {
31
+ const camera = controls.object
32
+ return {
33
+ position: camera.position.clone(),
34
+ quaternion: camera.quaternion.clone(),
35
+ zoom: camera.zoom,
36
+ target: controls.target.clone(),
37
+ }
38
+ }
39
+
40
+ function restoreCamera(snapshot) {
41
+ const camera = controls.object
42
+ camera.position.copy(snapshot.position)
43
+ camera.quaternion.copy(snapshot.quaternion)
44
+ camera.zoom = snapshot.zoom
45
+ camera.updateProjectionMatrix?.()
46
+ controls.target.copy(snapshot.target)
47
+ }
48
+
49
+ /**
50
+ * 停住总览操作并清掉残余运动,全程同步执行、不渲染中间机位:
51
+ * 1. 保存机位与设置 → enabled=false → disconnect()(结束未完成手势、
52
+ * 清指针列表,r186 的 disconnect 会拆监听并把 state 置回 NONE);
53
+ * 2. 暂时关掉 autoRotate/enableDamping,update() 一次——无阻尼分支会把
54
+ * 残余旋转/平移一次应用并在末尾清零,缩放比例重置;残余消耗在临时机位上;
55
+ * 3. 相机恢复到保存的机位,再以无阻尼/无自动旋转 update() 一次,让控制器
56
+ * 的内部球坐标与原机位重新对齐——此后没有任何可推进的残余;
57
+ * 4. 恢复原 enableDamping/autoRotate 设置。只用公开接口,不读写私有字段,
58
+ * 不调用 reset()/saveState(),不重建相机。
59
+ */
60
+ function pauseOverview() {
61
+ const savedDamping = controls.enableDamping
62
+ const savedAutoRotate = controls.autoRotate
63
+ const snapshot = snapshotCamera()
64
+ controls.enabled = false
65
+ if (controlsConnected) {
66
+ controls.disconnect()
67
+ controlsConnected = false
68
+ }
69
+ controls.autoRotate = false
70
+ controls.enableDamping = false
71
+ controls.update() // 消耗残余运动(此步可能临时移动相机,不渲染)
72
+ restoreCamera(snapshot)
73
+ controls.update() // 从原机位重建控制器内部状态,无残余可推进
74
+ controls.enableDamping = savedDamping
75
+ controls.autoRotate = savedAutoRotate
76
+ }
77
+
78
+ /** 按当前模式与阅读状态设输入启用:walk 未阅读→行走进启用、总览停;
79
+ * overview 未阅读→OrbitControls 启用、行走停。阅读中一律停用。模式由场景切。 */
80
+ function syncMode() {
81
+ if (disposed) return
82
+ if (reading) {
83
+ input.setEnabled(false)
84
+ controls.enabled = false
85
+ return
86
+ }
87
+ if (getMode() === 'walk') {
88
+ input.setEnabled(true)
89
+ controls.enabled = false
90
+ } else {
91
+ input.setEnabled(false)
92
+ controls.enabled = true
93
+ }
94
+ }
95
+
96
+ /**
97
+ * 暂停(true)或恢复(false)场景操作。重复设置同一值无副作用(不重新保存
98
+ * 机位、不重复断开/连接监听);非 boolean 明确抛参数错误且不改变状态。
99
+ * 恢复不重置机位:总览机位在暂停时原样锁定,行走站位与视线一直在会话/
100
+ * 帧推进器里;恢复只重连控制器、清空输入并重置计时(首帧 dt=0,不补阅读
101
+ * 时长,阅读中按下的键不许在恢复后补算)。
102
+ */
103
+ function setReading(value) {
104
+ if (disposed) return
105
+ if (typeof value !== 'boolean') {
106
+ throw new TypeError(`setReading 只接受布尔值(实际是 ${value === null ? 'null' : typeof value})`)
107
+ }
108
+ if (value === reading) return
109
+ reading = value
110
+ if (value) {
111
+ input.setEnabled(false) // 键盘/拖动/虚拟按键同一道闸,停用即清空
112
+ input.clear()
113
+ walkFrame.resetClock()
114
+ clearPick?.()
115
+ pauseOverview()
116
+ } else {
117
+ if (!controlsConnected) {
118
+ controls.connect(controls.domElement)
119
+ controlsConnected = true
120
+ }
121
+ input.clear()
122
+ walkFrame.resetClock()
123
+ syncMode()
124
+ }
125
+ }
126
+
127
+ /** 场景渲染循环唯一的相机推进入口:阅读时两种推进都不做;
128
+ * 非阅读时 walk 交帧推进器(人物与视线),overview 交 OrbitControls。 */
129
+ function advance(now) {
130
+ if (disposed || reading) return
131
+ if (getMode() === 'walk') walkFrame.frame(now)
132
+ else controls.update()
133
+ }
134
+
135
+ function isReading() {
136
+ return !disposed && reading
137
+ }
138
+
139
+ /** 只使闸门失效并停用全部交互——含总览控制器:enabled=false 并主动断开已连接的
140
+ * 监听(销毁自 overview/非阅读状态时鼠标事件不得再绕过失效的 advance 驱动它);
141
+ * 已在阅读中(先前已断开)则不重复主动断开。不调用 controls.dispose、不释放
142
+ * 场景/渲染器/底座/家具——那些归场景的既有释放路径;不恢复相机、不重连、
143
+ * 不改模式与站位。销毁后一切调用均为无操作,isReading 返回 false。 */
144
+ function dispose() {
145
+ if (disposed) return
146
+ disposed = true
147
+ reading = false
148
+ input.setEnabled(false)
149
+ controls.enabled = false
150
+ if (controlsConnected) {
151
+ controls.disconnect()
152
+ controlsConnected = false
153
+ }
154
+ }
155
+
156
+ return { setReading, isReading, syncMode, advance, dispose }
157
+ }
@@ -0,0 +1,50 @@
1
+ // 空房间风格的公共光照;场景模块不依赖旧建筑蓝图或业务资料。
2
+ import * as THREE from 'three';
3
+
4
+ export function createRoomLighting(renderer,scene){
5
+ const group=new THREE.Group(); group.name='room-style-lighting'; scene.add(group);
6
+ const hemi=new THREE.HemisphereLight(0xe6efff,0x9a896e,2);group.add(hemi);
7
+ const key=new THREE.DirectionalLight(0xffeed2,3.7);key.position.set(-5,9,4);key.castShadow=true;
8
+ key.shadow.mapSize.set(2048,2048); Object.assign(key.shadow.camera,{left:-8,right:8,top:7,bottom:-7,near:.1,far:35});
9
+ key.shadow.bias=-.00025;key.shadow.normalBias=.025;key.shadow.radius=3;group.add(key);group.add(key.target);
10
+ const fill=new THREE.DirectionalLight(0xd9e7ff,1.4);fill.position.set(4,5,-1);group.add(fill);
11
+ const bounce=new THREE.DirectionalLight(0xffdbaa,.5);bounce.position.set(1,2,6);group.add(bounce);
12
+ const studio=new THREE.Scene();studio.background=new THREE.Color(0xbfc4ce);
13
+ const panel=(w,h,x,y,z,power)=>{const m=new THREE.Mesh(new THREE.PlaneGeometry(w,h),new THREE.MeshBasicMaterial({color:new THREE.Color(power,power*.98,power*.93),side:THREE.DoubleSide}));m.position.set(x,y,z);m.lookAt(0,1,0);studio.add(m);};
14
+ panel(9,6,-5,8,2,5);panel(5,7,6,3,0,2.2);panel(8,3,0,3,-8,1.5);
15
+ const pmrem=new THREE.PMREMGenerator(renderer);const env=pmrem.fromScene(studio,.04,.1,40);pmrem.dispose();
16
+ studio.traverse(o=>{o.geometry?.dispose();o.material?.dispose();});
17
+ const previousEnv=scene.environment;scene.environment=env.texture;
18
+ const skyMaterial=new THREE.ShaderMaterial({side:THREE.BackSide,depthWrite:false,uniforms:{top:{value:new THREE.Color('#c9dfed')},horizon:{value:new THREE.Color('#eff0df')},bottom:{value:new THREE.Color('#cddcc4')}},vertexShader:'varying vec3 vWorld; void main(){ vWorld=(modelMatrix*vec4(position,1.)).xyz; gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.); }',fragmentShader:'varying vec3 vWorld; uniform vec3 top; uniform vec3 horizon; uniform vec3 bottom; void main(){float h=normalize(vWorld).y;vec3 c=mix(horizon,top,smoothstep(0.,.72,h));c=mix(c,bottom,1.-smoothstep(-.5,0.,h));gl_FragColor=vec4(c,1.);\n#include <tonemapping_fragment>\n#include <colorspace_fragment>\n}'});
19
+ const sky=new THREE.Mesh(new THREE.SphereGeometry(90,32,16),skyMaterial);sky.renderOrder=-10;group.add(sky);
20
+ const stage=new THREE.Mesh(new THREE.PlaneGeometry(180,180),new THREE.MeshStandardMaterial({color:0xe2e4dc,roughness:1}));stage.rotation.x=-Math.PI/2;stage.position.y=-.46;stage.receiveShadow=true;group.add(stage);
21
+ const glowCanvas=document.createElement('canvas');glowCanvas.width=128;glowCanvas.height=128;const ctx=glowCanvas.getContext('2d');const grad=ctx.createRadialGradient(64,64,0,64,64,64);grad.addColorStop(0,'rgba(255,244,205,.6)');grad.addColorStop(.18,'rgba(255,238,186,.28)');grad.addColorStop(.5,'rgba(255,220,165,.08)');grad.addColorStop(1,'rgba(255,220,165,0)');ctx.fillStyle=grad;ctx.fillRect(0,0,128,128);
22
+ const glowTexture=new THREE.CanvasTexture(glowCanvas);glowTexture.colorSpace=THREE.SRGBColorSpace;
23
+ const celestial=new THREE.Group();celestial.position.set(-6,8,-12);group.add(celestial);
24
+ const moonMaterial=new THREE.MeshBasicMaterial({color:0xffe5ac});
25
+ const orb=new THREE.Mesh(new THREE.SphereGeometry(1,48,32),moonMaterial);celestial.add(orb);
26
+ const halo=new THREE.Sprite(new THREE.SpriteMaterial({map:glowTexture,transparent:true,depthWrite:false,blending:THREE.AdditiveBlending,toneMapped:false}));halo.scale.set(8,8,1);celestial.add(halo);
27
+ // 月面细节是建筑外的低对比点缀,不作为室内物品。
28
+ const craters=new THREE.Group();celestial.add(craters);
29
+ [[-.3,.35,.89,.16],[.32,-.23,.91,.22],[-.2,-.46,.86,.1],[.43,.42,.78,.12],[.05,.65,.75,.08]].forEach(([x,y,z,r])=>{const crater=new THREE.Mesh(new THREE.CircleGeometry(r,24),new THREE.MeshBasicMaterial({color:0xd3be8e,transparent:true,opacity:.21,depthWrite:false}));crater.position.set(x,y,z+.02);crater.lookAt(new THREE.Vector3(x,y,z).multiplyScalar(2));craters.add(crater);});
30
+ let seed=478;const random=()=>{seed=(seed*1664525+1013904223)>>>0;return seed/4294967296;};
31
+ const positions=[];for(let i=0;i<120;i++){const a=random()*Math.PI*2,r=25+random()*25;positions.push(Math.cos(a)*r,9+random()*24,Math.sin(a)*r);}
32
+ const stars=new THREE.Points(new THREE.BufferGeometry().setAttribute('position',new THREE.Float32BufferAttribute(positions,3)),new THREE.PointsMaterial({color:0xffecc4,size:.095,transparent:true,opacity:.75,depthWrite:false,toneMapped:false}));group.add(stars);
33
+ let style='ceramic',period='day';
34
+ function set(nextStyle,nextPeriod='day'){
35
+ style=nextStyle;period=nextPeriod;const fairy=style==='fairy',night=fairy&&period==='night';
36
+ sky.visible=fairy;stage.visible=!fairy;celestial.visible=fairy;stars.visible=night;craters.visible=night;
37
+ scene.background=new THREE.Color(night?0x1c263b:fairy?0xd6e3dc:0xe5e6df);
38
+ scene.fog=fairy?new THREE.Fog(night?0x26364b:0xd7e3d5,26,76):new THREE.Fog(0xe5e6df,35,90);
39
+ scene.environmentIntensity=night?.20:fairy?.45:.75;
40
+ hemi.color.set(night?0x92aeed:fairy?0xdbefff:0xe8f1ff);hemi.groundColor.set(night?0x3f3b52:0xb4a27f);hemi.intensity=night?.85:fairy?1.7:2.2;
41
+ key.color.set(night?0xb4c9ff:fairy?0xffd99d:0xfff1df);key.intensity=night?1.5:fairy?3.1:3.3;
42
+ key.position.set(night?-5:-4,night?7:9,night?-6:5);key.target.position.set(0,0,0);
43
+ fill.color.set(night?0x969bde:0xdeeeff);fill.intensity=night?.7:1.1;bounce.color.set(night?0xeebd76:0xffdfa8);bounce.intensity=night?.35:.55;
44
+ renderer.toneMappingExposure=night?1.1:fairy?1.08:1.02;
45
+ skyMaterial.uniforms.top.value.set(night?'#111b37':'#a9cfe4');skyMaterial.uniforms.horizon.value.set(night?'#566078':'#f4e6c6');skyMaterial.uniforms.bottom.value.set(night?'#283f47':'#bbcfad');
46
+ moonMaterial.color.set(night?0xffe5af:0xfff4cb);celestial.scale.setScalar(night?1.05:.7);halo.material.opacity=night?.58:.7;
47
+ }
48
+ set('ceramic');
49
+ return {set,update(t){if(style==='fairy'&&period==='night')stars.material.opacity=.68+Math.sin(t*.4)*.07;},dispose(){scene.remove(group);if(scene.environment===env.texture)scene.environment=previousEnv;env.dispose();const g=new Set(),m=new Set();group.traverse(o=>{if(o.geometry)g.add(o.geometry);if(o.material)m.add(o.material);});g.forEach(v=>v.dispose());m.forEach(v=>v.dispose());glowTexture.dispose();key.shadow.map?.dispose();}};
50
+ }