@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.
- package/LICENSE +21 -0
- package/README.md +98 -0
- package/README.zh.md +100 -0
- package/client.js +122 -0
- package/cordis.patch.yml +8 -0
- package/dist/check-building.js +1669 -0
- package/dist/check-room.js +980 -0
- package/dist/index.js +2873 -0
- package/package.json +60 -0
- package/skills/specdev-building/SKILL.md +85 -0
- package/skills/specdev-building/assets/layouts/court.json +53 -0
- package/skills/specdev-building/assets/layouts/row.json +48 -0
- package/skills/specdev-building/assets/layouts/single.json +27 -0
- package/skills/specdev-building/assets/layouts/tower-9.json +71 -0
- package/skills/specdev-building/assets/layouts/tower.json +59 -0
- package/skills/specdev-building/references/blueprint-contract.md +126 -0
- package/skills/specdev-building/references/diagnostics.md +101 -0
- package/skills/specdev-building/references/layouts.md +112 -0
- package/skills/specdev-business/SKILL.md +91 -0
- package/skills/specdev-business/assets/plan-template.md +66 -0
- package/skills/specdev-business/assets/spec-template.md +111 -0
- package/skills/specdev-business/assets/tasks-template.md +36 -0
- package/skills/specdev-business/references/change.md +117 -0
- package/skills/specdev-business/references/evidence-review.md +125 -0
- package/skills/specdev-business/references/implementation.md +67 -0
- package/skills/specdev-business/references/project-contract.md +211 -0
- package/skills/specdev-business/references/source-map.md +110 -0
- package/skills/specdev-business/references/specification.md +93 -0
- package/skills/specdev-business/references/workflow.md +126 -0
- package/skills/specdev-room/SKILL.md +77 -0
- package/skills/specdev-room/assets/intro-room.json +14 -0
- package/skills/specdev-room/references/diagnostics.md +21 -0
- package/skills/specdev-room/references/room-contract.md +59 -0
- package/vendor/archify-renderer/VERSION.md +52 -0
- package/vendor/archify-renderer/archify/LICENSE +22 -0
- package/vendor/archify-renderer/archify/THIRD_PARTY_NOTICES.md +56 -0
- package/vendor/archify-renderer/archify/assets/template.html +14787 -0
- package/vendor/archify-renderer/archify/bin/archify.mjs +1990 -0
- package/vendor/archify-renderer/archify/renderers/shared/brand-marks.mjs +563 -0
- package/vendor/archify-renderer/archify/renderers/shared/cli.mjs +218 -0
- package/vendor/archify-renderer/archify/renderers/shared/desktop-readability.mjs +26 -0
- package/vendor/archify-renderer/archify/renderers/shared/diagnostics.mjs +127 -0
- package/vendor/archify-renderer/archify/renderers/shared/engineering-profiles.mjs +157 -0
- package/vendor/archify-renderer/archify/renderers/shared/generated-brand-marks.mjs +2003 -0
- package/vendor/archify-renderer/archify/renderers/shared/generated-validators.mjs +13 -0
- package/vendor/archify-renderer/archify/renderers/shared/geometry.mjs +1423 -0
- package/vendor/archify-renderer/archify/renderers/shared/i18n.mjs +594 -0
- package/vendor/archify-renderer/archify/renderers/shared/legend.mjs +217 -0
- package/vendor/archify-renderer/archify/renderers/shared/output-path.mjs +321 -0
- package/vendor/archify-renderer/archify/renderers/shared/repository-evidence.mjs +235 -0
- package/vendor/archify-renderer/archify/renderers/shared/text-fit.mjs +49 -0
- package/vendor/archify-renderer/archify/renderers/shared/utils.mjs +232 -0
- package/vendor/archify-renderer/archify/renderers/shared/validator.mjs +86 -0
- package/vendor/archify-renderer/archify/renderers/workflow/render-workflow.mjs +35 -0
- package/vendor/archify-renderer/archify/renderers/workflow/workflow-compiler.mjs +4400 -0
- package/vendor/archify-renderer/archify/renderers/workflow/workflow-migration-geometry.mjs +144 -0
- package/vendor/archify-renderer/archify/schemas/common.schema.json +115 -0
- package/vendor/archify-renderer/archify/schemas/workflow.schema.json +428 -0
- package/vendor/archify-renderer/archify/scripts/check-render-output.mjs +835 -0
- package/vendor/archify-renderer/shims/process.mjs +18 -0
- package/vendor/archify-renderer/stubs/child_process.mjs +2 -0
- package/vendor/archify-renderer/stubs/crypto.mjs +3 -0
- package/vendor/archify-renderer/stubs/dns-promises.mjs +2 -0
- package/vendor/archify-renderer/stubs/fs.mjs +3 -0
- package/vendor/archify-renderer/stubs/http.mjs +3 -0
- package/vendor/archify-renderer/stubs/https.mjs +3 -0
- package/vendor/archify-renderer/stubs/net.mjs +3 -0
- package/vendor/archify-renderer/stubs/path.mjs +3 -0
- package/vendor/archify-renderer/stubs/url.mjs +3 -0
- package/web/assets/building-kit.js +1157 -0
- package/web/assets/building-look.js +185 -0
- package/web/assets/building-plan.js +495 -0
- package/web/assets/building-scene.js +529 -0
- package/web/assets/building-walk.js +248 -0
- package/web/assets/building.js +859 -0
- package/web/assets/business.js +124 -0
- package/web/assets/chart-link.js +165 -0
- package/web/assets/common.js +302 -0
- package/web/assets/details.js +197 -0
- package/web/assets/evidence.js +30 -0
- package/web/assets/furniture/SOURCES.md +24 -0
- package/web/assets/furniture/assets/ceramic-display-stand-01/asset.json +25 -0
- package/web/assets/furniture/assets/ceramic-display-stand-01/model.js +116 -0
- package/web/assets/furniture/assets/ceramic-display-stand-01/preview.webp +0 -0
- package/web/assets/furniture/assets/ceramic-floor-lamp-01/asset.json +61 -0
- package/web/assets/furniture/assets/ceramic-floor-lamp-01/model.js +56 -0
- package/web/assets/furniture/assets/ceramic-floor-lamp-01/preview.webp +0 -0
- package/web/assets/furniture/assets/ceramic-low-cabinet-01/asset.json +81 -0
- package/web/assets/furniture/assets/ceramic-low-cabinet-01/model.js +67 -0
- package/web/assets/furniture/assets/ceramic-low-cabinet-01/preview.webp +0 -0
- package/web/assets/furniture/assets/ceramic-vase-01/asset.json +21 -0
- package/web/assets/furniture/assets/ceramic-vase-01/model.js +119 -0
- package/web/assets/furniture/assets/ceramic-vase-01/preview.webp +0 -0
- package/web/assets/furniture/assets/fairy-bed-01/asset.json +61 -0
- package/web/assets/furniture/assets/fairy-bed-01/model.js +63 -0
- package/web/assets/furniture/assets/fairy-bed-01/preview.webp +0 -0
- package/web/assets/furniture/assets/fairy-book-stack-01/asset.json +21 -0
- package/web/assets/furniture/assets/fairy-book-stack-01/model.js +71 -0
- package/web/assets/furniture/assets/fairy-book-stack-01/preview.webp +0 -0
- package/web/assets/furniture/assets/fairy-bookshelf-01/asset.json +32 -0
- package/web/assets/furniture/assets/fairy-bookshelf-01/model.js +168 -0
- package/web/assets/furniture/assets/fairy-bookshelf-01/preview.webp +0 -0
- package/web/assets/furniture/assets/fairy-desk-01/asset.json +25 -0
- package/web/assets/furniture/assets/fairy-desk-01/model.js +132 -0
- package/web/assets/furniture/assets/fairy-desk-01/preview.webp +0 -0
- package/web/assets/furniture/assets/fairy-notice-board-01/asset.json +23 -0
- package/web/assets/furniture/assets/fairy-notice-board-01/model.js +125 -0
- package/web/assets/furniture/assets/fairy-notice-board-01/preview.webp +0 -0
- package/web/assets/furniture/assets/fairy-potted-plant-01/asset.json +21 -0
- package/web/assets/furniture/assets/fairy-potted-plant-01/model.js +128 -0
- package/web/assets/furniture/assets/fairy-potted-plant-01/preview.webp +0 -0
- package/web/assets/furniture/assets/fairy-reading-chair-01/asset.json +61 -0
- package/web/assets/furniture/assets/fairy-reading-chair-01/model.js +66 -0
- package/web/assets/furniture/assets/fairy-reading-chair-01/preview.webp +0 -0
- package/web/assets/furniture/assets/fairy-table-lamp-01/asset.json +21 -0
- package/web/assets/furniture/assets/fairy-table-lamp-01/model.js +182 -0
- package/web/assets/furniture/assets/fairy-table-lamp-01/preview.webp +0 -0
- package/web/assets/furniture/catalog.js +137 -0
- package/web/assets/furniture/index.json +110 -0
- package/web/assets/furniture/registry.js +72 -0
- package/web/assets/furniture-page.css +68 -0
- package/web/assets/furniture-page.js +302 -0
- package/web/assets/home.js +65 -0
- package/web/assets/node-detail.js +148 -0
- package/web/assets/panel-link.js +47 -0
- package/web/assets/read.js +746 -0
- package/web/assets/room-scene.js +386 -0
- package/web/assets/room.css +287 -0
- package/web/assets/room.js +437 -0
- package/web/assets/rooms/bindings.js +449 -0
- package/web/assets/rooms/ceramic-reflection.js +193 -0
- package/web/assets/rooms/ceramic-room.js +302 -0
- package/web/assets/rooms/door.js +35 -0
- package/web/assets/rooms/fairy-room.js +492 -0
- package/web/assets/rooms/furniture-assembly.js +113 -0
- package/web/assets/rooms/navigation.js +262 -0
- package/web/assets/rooms/placeholder-catalog.json +71 -0
- package/web/assets/rooms/placeholder-furniture.js +76 -0
- package/web/assets/rooms/placement.js +588 -0
- package/web/assets/rooms/reader.js +441 -0
- package/web/assets/rooms/reading-gate.js +157 -0
- package/web/assets/rooms/room-lighting.js +50 -0
- package/web/assets/rooms/templates.json +50 -0
- package/web/assets/rooms/walk-frame.js +64 -0
- package/web/assets/rooms/walk-session.js +138 -0
- package/web/assets/save-result.js +89 -0
- package/web/assets/screenshots/business.png +0 -0
- package/web/assets/screenshots/node-detail.png +0 -0
- package/web/assets/screenshots/project-home.png +0 -0
- package/web/assets/screenshots/workflow.png +0 -0
- package/web/assets/showcase.js +232 -0
- package/web/assets/style.css +836 -0
- package/web/assets/template.js +7 -0
- package/web/assets/theme.js +29 -0
- package/web/assets/three/OrbitControls.js +1972 -0
- package/web/assets/three/VERSION.md +12 -0
- package/web/assets/three/three.core.js +60586 -0
- package/web/assets/three/three.module.js +19719 -0
- package/web/assets/workspaces.css +21 -0
- package/web/assets/workspaces.js +81 -0
- package/web/building.html +131 -0
- package/web/business.html +43 -0
- package/web/furniture.html +47 -0
- package/web/index.html +57 -0
- package/web/read.html +107 -0
- package/web/room.html +146 -0
- package/web/showcase.html +50 -0
- 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
|
+
}
|