@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,529 @@
1
+ // 建筑总览页的三维场景(外观重建版):Three.js 消费服务端算好的 model 与 present。
2
+ //
3
+ // 与旧版相同的边界(接口、行为一条不改):
4
+ // · createBuildingScene(container, callbacks) → 句柄,句柄操作与回调同旧版;
5
+ // · 可见的东西来自 present.parts,能走的与挡路的来自 present.surfaces / present.colliders,
6
+ // 两套数据互不串:外观派生件(整面墙、檐口、台基……)一律不进碰撞与行走,
7
+ // 漫游判定仍全部在 building-walk.js(本模块只把结果同步到相机);
8
+ // · 楼层剖看是可见性过滤,不是重算;
9
+ // · 所有 GPU 资源登记后逐项 dispose,换建筑与销毁不泄漏。
10
+ // 新增的只有外观:building-plan.js 规划(纯函数),building-kit.js 造网格,
11
+ // building-look.js 给两套预设(由 model.style 驱动)。
12
+ // 第三个参数 options 可选(页面不传即默认):{ style, look, debug }——原型用来调试外观,
13
+ // 页面与测试替身仍然只依赖句柄。
14
+ import * as THREE from 'three'
15
+ import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
16
+ import { createNavigator, createWalkInput, canSpawn, entryPoint, pickSign } from './building-walk.js'
17
+ import { resolveLook } from './building-look.js'
18
+ import { planBuilding } from './building-plan.js'
19
+ import { createKit, buildSign, baseBoard, groundCanvas, skyTexture, environmentTexture } from './building-kit.js'
20
+
21
+ // 漫游:眼高、视场角与速度沿用旧版手感;俯仰限制在水平线上下约 79°。
22
+ const EYE_HEIGHT = 1.65
23
+ const WALK_FOV = 66
24
+ const PITCH_LIMIT = Math.PI * 0.44
25
+ const WALK_SPEED = 2.6
26
+ const WALK_RUN_SPEED = 4.6
27
+
28
+ /**
29
+ * 建一个建筑场景挂进 container。句柄与回调同旧版(见文档"接口"一节):
30
+ * show / enterWalk / exitWalk / getMode / walkKey / pauseWalk / resumeWalk /
31
+ * setFloor / focusSpace / resetView / dispose;
32
+ * onWalkMode / onWalkMove / onWalkBlocked / onSignClick。
33
+ */
34
+ export function createBuildingScene(container, { onWalkMode = () => {}, onWalkMove = () => {}, onWalkBlocked = () => {}, onSignClick = () => {} } = {}, options = {}) {
35
+ const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false, preserveDrawingBuffer: Boolean(options.debug) })
36
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 1.8))
37
+ renderer.shadowMap.enabled = true
38
+ // r186 已移除 PCFSoftShadowMap(会告警并回落为 PCF);PCF+radius 给柔边。
39
+ renderer.shadowMap.type = THREE.PCFShadowMap
40
+ renderer.outputColorSpace = THREE.SRGBColorSpace
41
+ renderer.toneMapping = THREE.ACESFilmicToneMapping
42
+ container.append(renderer.domElement)
43
+ renderer.domElement.tabIndex = 0
44
+ renderer.domElement.setAttribute('aria-label', '建筑视图,拖动环绕,滚轮缩放;进入探索后用 WASD 或方向键移动')
45
+ const maxAnisotropy = Math.min(8, renderer.capabilities.getMaxAnisotropy())
46
+
47
+ const scene = new THREE.Scene()
48
+ const camera = new THREE.PerspectiveCamera(40, 1, 0.05, 1000)
49
+ const controls = new OrbitControls(camera, renderer.domElement)
50
+ controls.enableDamping = true
51
+ controls.dampingFactor = 0.075
52
+ controls.maxPolarAngle = Math.PI * 0.47
53
+ controls.minDistance = 2
54
+ controls.maxDistance = 300
55
+ const hemisphere = new THREE.HemisphereLight('#ffffff', '#cccccc', 1)
56
+ const sunlight = new THREE.DirectionalLight('#ffffff', 2.5)
57
+ sunlight.castShadow = true
58
+ scene.add(hemisphere, sunlight, sunlight.target)
59
+ const root = new THREE.Group()
60
+ scene.add(root)
61
+
62
+ let model = null
63
+ let presentation = null
64
+ let navigator = null
65
+ let look = null
66
+ let floorId = 'all'
67
+ let disposed = false
68
+ // 资源分两层:building(换建筑即清)与 stage(换外观预设时才清:环境贴图、天空)。
69
+ let ownedResources = []
70
+ let stageResources = []
71
+ const stageCache = new Map()
72
+ let site = null
73
+ let lastPlan = null
74
+ let lastTimings = null
75
+ const own = (resource) => {
76
+ ownedResources.push(resource)
77
+ return resource
78
+ }
79
+
80
+ // 漫游状态:mode 决定相机归 OrbitControls 还是行走控制;player 是脚底位置,不是眼睛位置。
81
+ let mode = 'orbit'
82
+ let player = { x: 0, y: 0, z: 0 }
83
+ let yaw = 0
84
+ let pitch = 0
85
+ let lastLocation = ''
86
+ let lastTime = 0
87
+ const input = createWalkInput(renderer.domElement)
88
+
89
+ // ── 点牌子(同旧版口径):只在"按下与松开几乎同一点"时算点击;射线打全部看得见的构件,
90
+ // 由 pickSign 按距离判定——被墙、楼板挡住的牌子点不到,剖看藏起的牌子也点不到。
91
+ // 构件现在是分组的(一面墙=墙体+墙裙),命中后回溯到 root 下的顶层对象取身份,
92
+ // 可见性按"自己与所有祖先都可见"判定(Three.js 的射线本身不看 visible)。
93
+ const raycaster = new THREE.Raycaster()
94
+ const pointerNdc = new THREE.Vector2()
95
+ let pressedAt = null
96
+
97
+ function topLevel(object) {
98
+ let node = object
99
+ while (node.parent && node.parent !== root) node = node.parent
100
+ return node.parent === root ? node : null
101
+ }
102
+
103
+ function effectivelyVisible(object) {
104
+ for (let node = object; node; node = node.parent) if (!node.visible) return false
105
+ return true
106
+ }
107
+
108
+ function pickables() {
109
+ const list = []
110
+ for (const child of root.children) {
111
+ if (child.userData.environment) continue
112
+ child.traverse((node) => { if (node.isMesh) list.push(node) })
113
+ }
114
+ return list
115
+ }
116
+
117
+ function pointerDownForSign(event) {
118
+ if (event.button !== 0) return
119
+ pressedAt = { x: event.clientX, y: event.clientY }
120
+ }
121
+
122
+ function pointerUpForSign(event) {
123
+ const start = pressedAt
124
+ pressedAt = null
125
+ if (event.button !== 0 || !start) return
126
+ if (Math.hypot(event.clientX - start.x, event.clientY - start.y) > 6) return
127
+ const rect = renderer.domElement.getBoundingClientRect()
128
+ if (!rect.width || !rect.height) return
129
+ pointerNdc.set(((event.clientX - rect.left) / rect.width) * 2 - 1, -((event.clientY - rect.top) / rect.height) * 2 + 1)
130
+ raycaster.setFromCamera(pointerNdc, camera)
131
+ const hits = raycaster.intersectObjects(pickables(), false).map((hit) => {
132
+ const top = topLevel(hit.object)
133
+ return {
134
+ kind: top?.userData.kind,
135
+ visible: effectivelyVisible(hit.object),
136
+ opacity: hit.object.userData.opacity ?? top?.userData.opacity ?? 1,
137
+ spaceId: top?.userData.spaceIds?.[0] ?? null,
138
+ }
139
+ })
140
+ const picked = pickSign(hits)
141
+ if (picked) onSignClick(picked)
142
+ }
143
+
144
+ const forgetPress = () => { pressedAt = null }
145
+ renderer.domElement.addEventListener('pointerdown', pointerDownForSign)
146
+ renderer.domElement.addEventListener('pointerup', pointerUpForSign)
147
+ renderer.domElement.addEventListener('pointercancel', forgetPress)
148
+
149
+ function cleanupBuilding() {
150
+ root.clear()
151
+ for (const resource of ownedResources) resource.dispose()
152
+ ownedResources = []
153
+ site = null
154
+ }
155
+
156
+ function cleanupStage() {
157
+ for (const resource of stageResources) resource.dispose()
158
+ stageResources = []
159
+ stageCache.clear()
160
+ scene.background = null
161
+ scene.environment = null
162
+ scene.fog = null
163
+ }
164
+
165
+ /** 舞台(天空、环境反射)按预设缓存:来回切换 style 不重新生成环境贴图,销毁时一并释放。 */
166
+ function applyStage() {
167
+ const key = options.look ? `${look.id}|${JSON.stringify(options.look)}` : look.id
168
+ if (!stageCache.has(key)) {
169
+ const sky = skyTexture(look)
170
+ const environment = environmentTexture(renderer, look)
171
+ stageResources.push(sky, environment)
172
+ stageCache.set(key, { sky, environment })
173
+ }
174
+ const stage = stageCache.get(key)
175
+ scene.background = stage.sky
176
+ scene.environment = stage.environment.texture
177
+ scene.environmentIntensity = look.light.envIntensity
178
+ renderer.toneMappingExposure = look.light.exposure
179
+ hemisphere.color.set(look.light.hemiSky)
180
+ hemisphere.groundColor.set(look.light.hemiGround)
181
+ hemisphere.intensity = look.light.hemiIntensity
182
+ sunlight.color.set(look.light.sun)
183
+ sunlight.intensity = look.light.sunIntensity
184
+ if (sunlight.shadow.mapSize.x !== look.shadow.mapSize) {
185
+ sunlight.shadow.mapSize.set(look.shadow.mapSize, look.shadow.mapSize)
186
+ sunlight.shadow.map?.dispose()
187
+ sunlight.shadow.map = null
188
+ }
189
+ sunlight.shadow.radius = look.shadow.radius
190
+ sunlight.shadow.bias = look.shadow.bias
191
+ sunlight.shadow.normalBias = look.shadow.normalBias
192
+ camera.fov = mode === 'walk' ? WALK_FOV : look.camera.orbitFov
193
+ camera.updateProjectionMatrix()
194
+ }
195
+
196
+ function sunDirection() {
197
+ const el = THREE.MathUtils.degToRad(look.light.sunElevation)
198
+ const az = THREE.MathUtils.degToRad(look.light.sunAzimuth)
199
+ return new THREE.Vector3(Math.cos(el) * Math.sin(az), Math.sin(el), Math.cos(el) * Math.cos(az))
200
+ }
201
+
202
+ /** 阴影相机:总览时框住整块底座;漫游时跟着人走、只框周边,近处阴影才够清楚。 */
203
+ function fitShadow(center, half) {
204
+ const direction = sunDirection()
205
+ const distance = half * 3 + 20
206
+ sunlight.target.position.copy(center)
207
+ sunlight.position.copy(center).addScaledVector(direction, distance)
208
+ Object.assign(sunlight.shadow.camera, { left: -half, right: half, top: half, bottom: -half, near: 0.5, far: distance + half * 2 })
209
+ sunlight.shadow.camera.updateProjectionMatrix()
210
+ }
211
+
212
+ function fitShadowOverview() {
213
+ if (!site) return
214
+ fitShadow(site.center, site.radius * 1.05)
215
+ }
216
+
217
+ /** 楼层剖看(同旧版语义):选中某层时隐藏其上方构件与压在该层上的屋顶(山墙填充随屋顶走)。 */
218
+ function updateFloor() {
219
+ const floor = model?.floors.find((item) => item.id === floorId)
220
+ const floorY = floor ? (floor.y ?? floor.elevation) : 0
221
+ const ceiling = floor ? floorY + floor.height + 0.25 : Infinity
222
+ for (const object of root.children) {
223
+ if (object.userData.environment) continue
224
+ const ids = object.userData.spaceIds || []
225
+ const spaces = ids.map((id) => model.spaces.find((item) => item.id === id)).filter(Boolean)
226
+ object.visible = !floor
227
+ || ((object.userData.lowY ?? 0) < ceiling && (spaces.length ? spaces.some((space) => space.y <= floorY + 0.01) : true))
228
+ const roofLike = ['roof', 'roof-frame'].includes(object.userData.kind) || object.userData.cutAs === 'roof'
229
+ if (floor && roofLike && spaces.some((space) => space.floorId === floor.id)) object.visible = false
230
+ }
231
+ }
232
+
233
+ /**
234
+ * 总览机位:沿固定的"模型摄影"方向,二分出让建筑包络(含底座)刚好占满画面约 look.camera.fill
235
+ * 的距离——不同尺寸、不同长宽比的建筑都得到同样饱满的构图,不靠写死的倍数。
236
+ */
237
+ function home() {
238
+ if (!model || !site) return
239
+ const b = model.bounds
240
+ const center = new THREE.Vector3((b.x0 + b.x1) / 2, (b.y0 + b.y1) * 0.35, (b.z0 + b.z1) / 2)
241
+ const direction = new THREE.Vector3(...look.camera.overviewDirection).normalize()
242
+ const r = site.rect
243
+ const corners = []
244
+ for (const x of [r.x0, r.x0 + r.width]) for (const z of [r.z0, r.z0 + r.depth]) {
245
+ corners.push(new THREE.Vector3(x, site.center.y, z), new THREE.Vector3(x, b.y1, z))
246
+ }
247
+ const probe = camera.clone()
248
+ probe.fov = look.camera.orbitFov
249
+ probe.aspect = camera.aspect
250
+ probe.near = 0.1
251
+ probe.far = 1e5
252
+ probe.updateProjectionMatrix()
253
+ const extent = (distance) => {
254
+ probe.position.copy(center).addScaledVector(direction, distance)
255
+ probe.lookAt(center)
256
+ probe.updateMatrixWorld()
257
+ let max = 0
258
+ for (const corner of corners) {
259
+ const v = corner.clone().project(probe)
260
+ max = Math.max(max, Math.abs(v.x), Math.abs(v.y))
261
+ }
262
+ return max
263
+ }
264
+ let lo = 1
265
+ let hi = 2000
266
+ for (let i = 0; i < 40; i += 1) {
267
+ const mid = (lo + hi) / 2
268
+ if (extent(mid) > look.camera.fill) lo = mid
269
+ else hi = mid
270
+ }
271
+ const distance = hi
272
+ controls.target.copy(center)
273
+ camera.position.copy(center).addScaledVector(direction, distance)
274
+ controls.maxDistance = distance * 3
275
+ camera.far = Math.max(500, distance * 8)
276
+ camera.updateProjectionMatrix()
277
+ camera.lookAt(center)
278
+ controls.update()
279
+ }
280
+
281
+ function resize() {
282
+ if (disposed) return
283
+ const width = Math.max(1, container.clientWidth)
284
+ const height = Math.max(1, container.clientHeight)
285
+ renderer.setSize(width, height, false)
286
+ camera.aspect = width / height
287
+ camera.updateProjectionMatrix()
288
+ }
289
+
290
+ function syncWalk() {
291
+ camera.position.set(player.x, player.y + EYE_HEIGHT, player.z)
292
+ camera.rotation.set(pitch, yaw, 0, 'YXZ')
293
+ const space = navigator?.locate(player) || null
294
+ const key = space?.id || 'stairs'
295
+ if (key !== lastLocation) {
296
+ lastLocation = key
297
+ onWalkMove(space, { ...player })
298
+ }
299
+ }
300
+
301
+ function locate(point, lookAt) {
302
+ if (!model || !point) return
303
+ const space = model.spaces.find((item) => item.id === point.spaceId)
304
+ player = { x: point.x, y: point.y ?? space?.y ?? 0, z: point.z }
305
+ const target = lookAt || { x: (model.bounds.x0 + model.bounds.x1) / 2, z: (model.bounds.z0 + model.bounds.z1) / 2 }
306
+ if (Math.hypot(target.x - player.x, target.z - player.z) > 0.01) yaw = Math.atan2(-(target.x - player.x), -(target.z - player.z))
307
+ pitch = 0
308
+ lastLocation = ''
309
+ syncWalk()
310
+ }
311
+
312
+ function blocked(reason, whileWalking) {
313
+ onWalkBlocked({ reason, whileWalking })
314
+ }
315
+
316
+ function enterWalk() {
317
+ if (!model || !navigator || !presentation?.surfaces?.length) {
318
+ blocked('no-walkable-ground', false)
319
+ return false
320
+ }
321
+ const spawn = entryPoint(model)
322
+ if (!canSpawn(navigator, spawn)) {
323
+ blocked('entry-blocked', false)
324
+ return false
325
+ }
326
+ floorId = 'all'
327
+ updateFloor()
328
+ mode = 'walk'
329
+ controls.enabled = false
330
+ camera.fov = WALK_FOV
331
+ camera.updateProjectionMatrix()
332
+ input.setEnabled(true)
333
+ locate(spawn)
334
+ renderer.domElement.focus({ preventScroll: true })
335
+ onWalkMode(mode)
336
+ return true
337
+ }
338
+
339
+ function exitWalk() {
340
+ if (mode !== 'walk') return false
341
+ mode = 'orbit'
342
+ input.setEnabled(false)
343
+ controls.enabled = true
344
+ camera.fov = look?.camera.orbitFov ?? 40
345
+ camera.updateProjectionMatrix()
346
+ fitShadowOverview()
347
+ home()
348
+ onWalkMode(mode)
349
+ return true
350
+ }
351
+
352
+ /** 场地:底座、接触阴影、雾——都是环境(不参与剖看与拾取)。 */
353
+ function buildSite(plan) {
354
+ const b = model.bounds
355
+ const margin = look.base.margin
356
+ const rect = { x0: b.x0 - margin, z0: b.z0 - margin, width: b.x1 - b.x0 + margin * 2, depth: b.z1 - b.z0 + margin * 2 }
357
+ const footprints = plan.plinths.map((p) => ({ x0: p.x0 - look.plinth.outset, x1: p.x1 + look.plinth.outset, z0: p.z0 - look.plinth.outset, z1: p.z1 + look.plinth.outset }))
358
+ const groundTexture = own(new THREE.CanvasTexture(groundCanvas(look, rect, footprints)))
359
+ groundTexture.colorSpace = THREE.SRGBColorSpace
360
+ groundTexture.anisotropy = maxAnisotropy
361
+ const board = baseBoard(look, rect, plan.groundY, own, groundTexture)
362
+ board.userData.environment = true
363
+ root.add(board)
364
+ const center = new THREE.Vector3(rect.x0 + rect.width / 2, plan.groundY, rect.z0 + rect.depth / 2)
365
+ const radius = 0.5 * Math.hypot(rect.width, rect.depth)
366
+ site = { center, radius, rect }
367
+ const span = Math.max(b.x1 - b.x0, b.z1 - b.z0, 12)
368
+ scene.fog = new THREE.Fog(look.sky.horizon, span * look.sky.fogNear, span * look.sky.fogFar)
369
+ }
370
+
371
+ function show({ model: nextModel, present }) {
372
+ model = nextModel
373
+ presentation = present
374
+ navigator = createNavigator(model, present)
375
+ floorId = 'all'
376
+ const t0 = performance.now()
377
+ cleanupBuilding()
378
+ const t1 = performance.now()
379
+ look = resolveLook(options.style || model.style, options.look || null)
380
+ applyStage()
381
+ const t2 = performance.now()
382
+ const plan = planBuilding(model, present, look)
383
+ const kit = createKit({ look, own, maxAnisotropy })
384
+ buildSite(plan)
385
+ for (const object of kit.build(plan, model, present)) root.add(object)
386
+ for (const sign of model.signs || []) root.add(buildSign(sign, { project: !sign.spaceId, look, own, maxAnisotropy }))
387
+ if (options.debug) lastTimings = { cleanup: Math.round(t1 - t0), stage: Math.round(t2 - t1), build: Math.round(performance.now() - t2) }
388
+ lastPlan = options.debug ? plan : null
389
+ fitShadowOverview()
390
+ updateFloor()
391
+ if (mode === 'walk') {
392
+ const spawn = entryPoint(model)
393
+ if (!presentation?.surfaces?.length) {
394
+ exitWalk()
395
+ blocked('no-walkable-ground', true)
396
+ } else if (!canSpawn(navigator, spawn)) {
397
+ exitWalk()
398
+ blocked('entry-blocked', true)
399
+ } else locate(spawn)
400
+ } else home()
401
+ }
402
+
403
+ function focusSpace(spaceId) {
404
+ if (!model) return
405
+ const space = model.spaces.find((item) => item.id === spaceId)
406
+ if (!space) return
407
+ const width = space.bounds.x1 - space.bounds.x0
408
+ const depth = space.bounds.z1 - space.bounds.z0
409
+ const target = new THREE.Vector3((space.bounds.x0 + space.bounds.x1) / 2, space.y + space.height * 0.35, (space.bounds.z0 + space.bounds.z1) / 2)
410
+ const direction = camera.position.clone().sub(controls.target)
411
+ if (direction.lengthSq() < 1e-6) direction.set(0.85, 0.72, 1.04)
412
+ direction.normalize()
413
+ const distance = Math.max(Math.max(width, depth) * 2.4, 12)
414
+ controls.target.copy(target)
415
+ camera.position.copy(target).add(direction.multiplyScalar(distance))
416
+ controls.update()
417
+ }
418
+
419
+ const observer = new ResizeObserver(resize)
420
+ observer.observe(container)
421
+ resize()
422
+ const shadowCenter = new THREE.Vector3()
423
+ renderer.setAnimationLoop((time) => {
424
+ const dt = Math.min((time - lastTime) / 1000 || 0, 0.05)
425
+ lastTime = time
426
+ if (mode === 'walk') {
427
+ const lookDelta = input.takeLook()
428
+ if (lookDelta.yaw || lookDelta.pitch) {
429
+ yaw += lookDelta.yaw
430
+ pitch = Math.max(-PITCH_LIMIT, Math.min(PITCH_LIMIT, pitch + lookDelta.pitch))
431
+ }
432
+ const { forward, right, fast } = input.axis()
433
+ const length = Math.hypot(forward, right)
434
+ if (length > 0 && navigator) {
435
+ const step = dt * (fast ? WALK_RUN_SPEED : WALK_SPEED)
436
+ player = navigator.move(
437
+ player,
438
+ (-Math.sin(yaw) * forward + Math.cos(yaw) * right) / length * step,
439
+ (-Math.cos(yaw) * forward - Math.sin(yaw) * right) / length * step,
440
+ )
441
+ }
442
+ syncWalk()
443
+ if (look) fitShadow(shadowCenter.set(player.x, player.y, player.z), look.shadow.walkHalfSize)
444
+ } else controls.update()
445
+ if (model) renderer.render(scene, camera)
446
+ })
447
+
448
+ const handle = {
449
+ show,
450
+ enterWalk,
451
+ exitWalk,
452
+ getMode: () => mode,
453
+ walkKey(code, down) {
454
+ input.press(code, down)
455
+ },
456
+ pauseWalk() {
457
+ if (mode !== 'walk') return false
458
+ input.setEnabled(false)
459
+ return true
460
+ },
461
+ resumeWalk() {
462
+ if (mode !== 'walk') return false
463
+ input.setEnabled(true)
464
+ renderer.domElement.focus({ preventScroll: true })
465
+ return true
466
+ },
467
+ setFloor(id) {
468
+ if (mode === 'walk' && id !== 'all') exitWalk()
469
+ floorId = id
470
+ updateFloor()
471
+ },
472
+ focusSpace,
473
+ resetView: home,
474
+ dispose() {
475
+ if (disposed) return
476
+ disposed = true
477
+ renderer.setAnimationLoop(null)
478
+ observer.disconnect()
479
+ input.dispose()
480
+ controls.dispose()
481
+ renderer.domElement.removeEventListener('pointerdown', pointerDownForSign)
482
+ renderer.domElement.removeEventListener('pointerup', pointerUpForSign)
483
+ renderer.domElement.removeEventListener('pointercancel', forgetPress)
484
+ cleanupBuilding()
485
+ cleanupStage()
486
+ sunlight.shadow.map?.dispose()
487
+ hemisphere.dispose?.()
488
+ sunlight.dispose?.()
489
+ renderer.dispose()
490
+ renderer.domElement.remove()
491
+ },
492
+ }
493
+ // 调试口只在 options.debug 时出现(原型与自动检查用);页面不传,句柄与旧版一致。
494
+ if (options.debug) {
495
+ handle.debug = {
496
+ info: () => ({ ...renderer.info.memory, programs: renderer.info.programs?.length ?? 0, calls: renderer.info.render.calls, triangles: renderer.info.render.triangles }),
497
+ plan: () => lastPlan,
498
+ timings: () => lastTimings,
499
+ look: () => look,
500
+ camera,
501
+ controls,
502
+ scene,
503
+ sunlight,
504
+ root,
505
+ renderNow: () => renderer.render(scene, camera),
506
+ canvas: renderer.domElement,
507
+ player: () => ({ ...player, yaw, pitch }),
508
+ setPlayer(next) {
509
+ player = { x: next.x, y: next.y, z: next.z }
510
+ yaw = next.yaw ?? yaw
511
+ pitch = next.pitch ?? pitch
512
+ syncWalk()
513
+ },
514
+ clickAt(x, y) {
515
+ const rect = renderer.domElement.getBoundingClientRect()
516
+ const down = new PointerEvent('pointerdown', { button: 0, clientX: rect.left + x, clientY: rect.top + y, bubbles: true })
517
+ const up = new PointerEvent('pointerup', { button: 0, clientX: rect.left + x, clientY: rect.top + y, bubbles: true })
518
+ renderer.domElement.dispatchEvent(down)
519
+ renderer.domElement.dispatchEvent(up)
520
+ },
521
+ project(point) {
522
+ const v = new THREE.Vector3(point.x, point.y, point.z).project(camera)
523
+ const rect = renderer.domElement.getBoundingClientRect()
524
+ return { x: (v.x + 1) / 2 * rect.width, y: (1 - v.y) / 2 * rect.height, z: v.z }
525
+ },
526
+ }
527
+ }
528
+ return handle
529
+ }