@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,437 @@
|
|
|
1
|
+
// 业务房间页逻辑(P1b-2):从业务页进入,读 /api/room 拿该业务的 room.json,
|
|
2
|
+
// 用插件固定资源(模板+占位目录+家具库)选模板,交给产品 placement 校验,
|
|
3
|
+
// 只有校验通过(ok:true)才准备家具批次并交给场景模块——页面不自己算几何、
|
|
4
|
+
// 不建 Three.js 对象、不静态导入 furniture-assembly。
|
|
5
|
+
// 数据到场景的唯一链路:URL 业务 ID → API 配置 → 固定模板/目录(占位+正式合并)→
|
|
6
|
+
// validatePlacement → prepareRoomFurniture → 场景。
|
|
7
|
+
// 本轮只有初次装载:改配置后刷新页面即重新读取;不做页内切换、热更新或全局缓存。
|
|
8
|
+
import {
|
|
9
|
+
$,
|
|
10
|
+
el,
|
|
11
|
+
wsUrl,
|
|
12
|
+
workspaceParamEmpty,
|
|
13
|
+
renderEmptyWorkspaceParam,
|
|
14
|
+
fetchJson,
|
|
15
|
+
renderGuide,
|
|
16
|
+
renderRepoState,
|
|
17
|
+
renderRepoLine,
|
|
18
|
+
pageTitle,
|
|
19
|
+
setStatus,
|
|
20
|
+
showError,
|
|
21
|
+
} from './common.js'
|
|
22
|
+
import { validatePlacement } from './rooms/placement.js'
|
|
23
|
+
import { resolveRoomBindings } from './rooms/bindings.js'
|
|
24
|
+
import { createRoomReader } from './rooms/reader.js'
|
|
25
|
+
import { loadFurnitureCatalog } from './furniture/catalog.js'
|
|
26
|
+
|
|
27
|
+
// 模板与占位目录是插件固定资源:不从 query、room.json 或外部 URL 替换。
|
|
28
|
+
const ASSET_BASE = '/specdev-workbench/assets/rooms'
|
|
29
|
+
|
|
30
|
+
// 底座风格字幕(2026-09-28 作者令保留并照抄旧页样式):文案逐字来自旧底座实验页面
|
|
31
|
+
//(experiments/2026-09-27-空房间风格底座,作者圈定保留;按作者令去掉数字编号 01/02),
|
|
32
|
+
// 按 template.styleId 分派;只有童话底座有昼夜之分(写实底座夜里无变化,不显示昼夜开关)。
|
|
33
|
+
const BASE_COPY = {
|
|
34
|
+
ceramic: {
|
|
35
|
+
eyebrow: '陶瓷 · 日光',
|
|
36
|
+
title: '釉白之间',
|
|
37
|
+
desc: { day: ['让光落在地面上,', '也让空间慢下来。'] },
|
|
38
|
+
material: '珍珠釉面 / 暖白灰泥',
|
|
39
|
+
},
|
|
40
|
+
fairy: {
|
|
41
|
+
eyebrow: { day: '森林 · 晴昼', night: '森林 · 星夜' },
|
|
42
|
+
title: '林间来信',
|
|
43
|
+
desc: { day: ['风经过藤蔓,', '留下一个安静的下午。'], night: ['月亮升起时,', '小小的光开始游走。'] },
|
|
44
|
+
material: '奶油灰泥 / 蜂蜜木 / 苔藓',
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 字幕一行或多行(行间用 DOM 换行,不拼 HTML)。 */
|
|
49
|
+
function renderLines(node, lines) {
|
|
50
|
+
node.textContent = ''
|
|
51
|
+
for (const [index, line] of lines.entries()) {
|
|
52
|
+
if (index > 0) node.appendChild(document.createElement('br'))
|
|
53
|
+
node.appendChild(document.createTextNode(line))
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** 按风格与昼夜填字幕块;文案是产品静态文案,不掺业务数据。
|
|
58
|
+
* body[data-style]/[data-period] 供 room.css 切换旧页同款变量(含童话星夜配色)。 */
|
|
59
|
+
function renderCaption(styleId, period) {
|
|
60
|
+
document.body.dataset.style = styleId
|
|
61
|
+
document.body.dataset.period = period
|
|
62
|
+
const copy = BASE_COPY[styleId]
|
|
63
|
+
if (!copy) return
|
|
64
|
+
$('captionEyebrow').textContent = typeof copy.eyebrow === 'string' ? copy.eyebrow : copy.eyebrow[period]
|
|
65
|
+
$('captionTitle').textContent = copy.title
|
|
66
|
+
renderLines($('captionDesc'), copy.desc[period] ?? copy.desc.day)
|
|
67
|
+
$('captionMaterial').textContent = copy.material
|
|
68
|
+
$('styleCaption').hidden = false
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** 昼夜开关(作者令保留,只有童话底座显示):切场景昼夜并同步字幕与按钮态。 */
|
|
72
|
+
function wirePeriodSwitch(scene, styleId) {
|
|
73
|
+
if (styleId !== 'fairy') return
|
|
74
|
+
const day = $('periodDay')
|
|
75
|
+
const night = $('periodNight')
|
|
76
|
+
const choose = (next) => {
|
|
77
|
+
scene.setPeriod(next)
|
|
78
|
+
renderCaption(styleId, next)
|
|
79
|
+
day.setAttribute('aria-pressed', String(next === 'day'))
|
|
80
|
+
night.setAttribute('aria-pressed', String(next === 'night'))
|
|
81
|
+
}
|
|
82
|
+
day.addEventListener('click', () => choose('day'))
|
|
83
|
+
night.addEventListener('click', () => choose('night'))
|
|
84
|
+
$('periodSwitch').hidden = false
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 行走/总览的操作提示(P1c-2c-2):随模式切换;门的结果说明也临时走这一行。
|
|
88
|
+
const WALK_HINT = 'WASD/方向键走动 · Shift 加速 · 拖动环视 · 点家具看身份'
|
|
89
|
+
const OVERVIEW_HINT = '拖动旋转 · 滚轮缩放 · 点家具看身份'
|
|
90
|
+
|
|
91
|
+
/** 从会话状态同步按钮态与提示:门按钮文案跟随返回后的实际 doorOpen;
|
|
92
|
+
* 当前模式的按钮禁用(已在行走就没有"继续行走"可点)。 */
|
|
93
|
+
function syncWalkUI(scene) {
|
|
94
|
+
const state = scene.getWalkState()
|
|
95
|
+
const walking = state.mode === 'walk'
|
|
96
|
+
$('enterWalk').disabled = walking
|
|
97
|
+
$('resetView').disabled = !walking
|
|
98
|
+
$('doorToggle').disabled = false
|
|
99
|
+
$('doorToggle').textContent = state.doorOpen ? '关门' : '开门'
|
|
100
|
+
$('sceneHint').textContent = walking ? WALK_HINT : OVERVIEW_HINT
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** 行走/总览/门按钮(P1c-2c-2):页面只调场景句柄,不直接碰门、导航或人物位置;
|
|
104
|
+
* 也不每帧改 DOM——只在点击与初始化时同步一次。
|
|
105
|
+
* E3b-2 起阅读中回调一律无操作(按钮已禁用、视口已 inert,这里再挡一层,
|
|
106
|
+
* 不只靠 CSS 遮住);门防夹语义不动。 */
|
|
107
|
+
function wireWalkControls(scene) {
|
|
108
|
+
$('resetView').addEventListener('click', () => {
|
|
109
|
+
if (scene?.isReading()) return
|
|
110
|
+
scene?.resetView()
|
|
111
|
+
if (scene) syncWalkUI(scene)
|
|
112
|
+
})
|
|
113
|
+
$('enterWalk').addEventListener('click', () => {
|
|
114
|
+
if (scene?.isReading()) return
|
|
115
|
+
scene?.enterWalk()
|
|
116
|
+
if (scene) syncWalkUI(scene)
|
|
117
|
+
})
|
|
118
|
+
$('doorToggle').addEventListener('click', () => {
|
|
119
|
+
if (!scene || scene.isReading()) return
|
|
120
|
+
const open = !scene.getWalkState().doorOpen
|
|
121
|
+
let result
|
|
122
|
+
try {
|
|
123
|
+
result = scene.setDoorOpen(open)
|
|
124
|
+
} catch (error) {
|
|
125
|
+
$('sceneHint').textContent = `门切换没有完成:${error instanceof Error ? error.message : String(error)}`
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
if (result.ok) {
|
|
129
|
+
syncWalkUI(scene)
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
if (result.code === 'door-blocked-by-player') {
|
|
133
|
+
$('sceneHint').textContent = `站位挡住了门板,请先离开门边,再${open ? '开' : '关'}门。`
|
|
134
|
+
return // 拒绝时门没动:按钮文案保持原样
|
|
135
|
+
}
|
|
136
|
+
$('sceneHint').textContent = '门切换没有完成,请再试一次。'
|
|
137
|
+
})
|
|
138
|
+
syncWalkUI(scene)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* 场景模块按需动态加载:空态、错误与诊断页不拉 Three.js。
|
|
143
|
+
* 加载口可注入(页面回归在 jsdom 里替换它,动态 import 在那里不可用);
|
|
144
|
+
* 生产路径用浏览器动态 import,importmap 解析裸名 'three'(与建筑页同模式)。
|
|
145
|
+
*/
|
|
146
|
+
function loadSceneModule() {
|
|
147
|
+
if (typeof globalThis.__loadRoomScene === 'function') return globalThis.__loadRoomScene()
|
|
148
|
+
return import('/specdev-workbench/assets/room-scene.js')
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function businessIdFromLocation() {
|
|
152
|
+
const parts = location.pathname.split('/').filter(Boolean) // ['specdev-workbench','room','<id>']
|
|
153
|
+
return parts[2] || ''
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let scene = null
|
|
157
|
+
let reader = null
|
|
158
|
+
// 真正离开页面后,晚到的数据/模块不许再创建场景;进入往返缓存(bfcache,
|
|
159
|
+
// pagehide.persisted=true)只是冻结:不终止异步链、不销毁场景——恢复的页面
|
|
160
|
+
// 脚本不会重跑,若在这里置了终止标记,加载中的页面回来后就永久停在加载状态。
|
|
161
|
+
// 阅读面板同口径:冻结时不销毁 reader(面板照常开着),真正离开先 reader.dispose
|
|
162
|
+
// (使请求与监听失效,不触发 setReading(false)——离页不该重新启用场景)再走场景释放。
|
|
163
|
+
let pageAlive = true
|
|
164
|
+
window.addEventListener('pagehide', (event) => {
|
|
165
|
+
if (event.persisted) return
|
|
166
|
+
pageAlive = false
|
|
167
|
+
reader?.dispose()
|
|
168
|
+
scene?.dispose()
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
/** 场景建不起来(资源下载、动态导入、WebGL 创建失败):结束加载状态并说明原因,返回链接不受影响。 */
|
|
172
|
+
function showSceneFailure(reason) {
|
|
173
|
+
setStatus('房间暂时无法显示', 'bad')
|
|
174
|
+
$('roomStage').hidden = false
|
|
175
|
+
$('sceneFallback').hidden = false
|
|
176
|
+
$('sceneFallbackReason').textContent = reason instanceof Error ? reason.message : String(reason)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** 点击家具的身份一行:textContent 填充,业务数据里的 HTML 字符按文字显示。 */
|
|
180
|
+
function showPick({ instanceId, assetId, name }) {
|
|
181
|
+
const info = $('pickInfo')
|
|
182
|
+
info.hidden = false
|
|
183
|
+
info.textContent = `${name} · 实例 ${instanceId} · 资产 ${assetId}`
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** 家具资料绑定有误(E3b-2):不装任何家具阅读动作,场景照常创建;
|
|
187
|
+
* 这里给出提示与可展开诊断(code、fieldPath、实例、中文原因,全 textContent)。 */
|
|
188
|
+
function showBindingNote(diagnostics) {
|
|
189
|
+
const note = $('bindingNote')
|
|
190
|
+
note.hidden = false
|
|
191
|
+
const list = $('bindingDiagList')
|
|
192
|
+
list.textContent = ''
|
|
193
|
+
for (const item of diagnostics) {
|
|
194
|
+
const li = el('li')
|
|
195
|
+
const head = el('p', 'diag-code')
|
|
196
|
+
const who = (item.instanceIds ?? []).join(' ')
|
|
197
|
+
head.textContent = `${item.code}${who ? ` · ${who}` : ''}${item.fieldPath ? ` · ${item.fieldPath}` : ''}`
|
|
198
|
+
const body = el('p')
|
|
199
|
+
body.textContent = item.message
|
|
200
|
+
li.append(head, body)
|
|
201
|
+
list.append(li)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** 阅读暂停与恢复(E3b-2):只经场景句柄 setReading,不用 resetView/enterWalk 恢复。
|
|
206
|
+
* 进入阅读:停用三个场景按钮并给背景视口加 inert(面板在 viewport 外,不受波及);
|
|
207
|
+
* 退出阅读:先撤 inert,再按当前模式经既有 syncWalkUI 恢复按钮态(不整体设 enabled)。 */
|
|
208
|
+
function handleReadingChange(reading) {
|
|
209
|
+
if (!scene) return
|
|
210
|
+
scene.setReading(reading)
|
|
211
|
+
if (reading) {
|
|
212
|
+
$('viewport').setAttribute('inert', '')
|
|
213
|
+
$('enterWalk').disabled = true
|
|
214
|
+
$('resetView').disabled = true
|
|
215
|
+
$('doorToggle').disabled = true
|
|
216
|
+
return
|
|
217
|
+
}
|
|
218
|
+
$('viewport').removeAttribute('inert')
|
|
219
|
+
syncWalkUI(scene)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** "业务资料"入口(E3b-2;E9c 起含流程图):只使用 resolver 交付的 directory,不从原始
|
|
223
|
+
* business.docs/business.charts 另造未校验链接;directory 为 null(业务输入坏)时如实
|
|
224
|
+
* 说明并指向绑定诊断。 */
|
|
225
|
+
function wireBizDocs({ directory, emptyText }) {
|
|
226
|
+
$('bizDocs').disabled = false
|
|
227
|
+
$('bizDocs').addEventListener('click', () => {
|
|
228
|
+
if (reader?.isOpen()) return
|
|
229
|
+
reader.open({ title: '业务资料', entries: directory ?? [], emptyText, returnFocus: $('bizDocs') })
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** 摆放校验/家具目录没过:完整列出 code、涉及实例/目标、字段路径与中文原因;不渲染半份家具、不建场景。 */
|
|
234
|
+
function showDiagnostics(diagnostics) {
|
|
235
|
+
setStatus('房间布置未通过校验', 'bad')
|
|
236
|
+
$('diagPanel').hidden = false
|
|
237
|
+
const list = $('diagList')
|
|
238
|
+
list.textContent = ''
|
|
239
|
+
for (const item of diagnostics) {
|
|
240
|
+
const li = el('li')
|
|
241
|
+
const head = el('p', 'diag-code')
|
|
242
|
+
const who = (item.instanceIds ?? []).join(' ')
|
|
243
|
+
head.textContent = `${item.code}${who ? ` · ${who}` : ''}${item.targetId ? ` → ${item.targetId}` : ''}${item.fieldPath ? ` · ${item.fieldPath}` : ''}`
|
|
244
|
+
const body = el('p')
|
|
245
|
+
body.textContent = item.message
|
|
246
|
+
li.append(head, body)
|
|
247
|
+
list.append(li)
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** 家具库读取失败(E4b-2):同一诊断面板列出 code/fieldPath/中文原因;不建场景、
|
|
252
|
+
* 不静默只渲染占位家具——正式家具配置读不到时如实说明。 */
|
|
253
|
+
function showLibraryFailure(diagnostics) {
|
|
254
|
+
showDiagnostics(diagnostics)
|
|
255
|
+
setStatus('家具目录读取失败', 'bad')
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** 占位目录容器形状不合法(E4b-2 补齐):同一诊断面板列出 CATALOG_INPUT/字段路径/
|
|
259
|
+
* 中文原因与资源来源;不建场景、不静默跳过占位家具——目录容器坏了如实说明。 */
|
|
260
|
+
function showCatalogInputFailure(fieldPath) {
|
|
261
|
+
showDiagnostics([{
|
|
262
|
+
code: 'CATALOG_INPUT',
|
|
263
|
+
fieldPath,
|
|
264
|
+
message: `占位目录必须是带 assets 数组的对象(资源来源:插件固定占位目录 ${ASSET_BASE}/placeholder-catalog.json)`,
|
|
265
|
+
}])
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function main() {
|
|
269
|
+
if (workspaceParamEmpty) return renderEmptyWorkspaceParam()
|
|
270
|
+
const id = businessIdFromLocation()
|
|
271
|
+
if (!id) return showError('缺少业务 id(路径应为 /specdev-workbench/room/<业务id>)')
|
|
272
|
+
// 返回业务页的链接先绑好(带工作区标识):读取失败、场景失败时都要能点回去。
|
|
273
|
+
// 面包屑那份在页头;全窗口形态下页头隐藏,工具条里的返回入口常驻(同一地址)。
|
|
274
|
+
const backHref = wsUrl(`/specdev-workbench/business/${encodeURIComponent(id)}`)
|
|
275
|
+
$('bizLink').href = backHref
|
|
276
|
+
$('bizLink').textContent = '业务'
|
|
277
|
+
$('backLink').href = backHref
|
|
278
|
+
|
|
279
|
+
let data
|
|
280
|
+
try {
|
|
281
|
+
data = await fetchJson(wsUrl(`/specdev-workbench/api/room?business=${encodeURIComponent(id)}`))
|
|
282
|
+
} catch (error) {
|
|
283
|
+
if (!pageAlive) return // 真正离开后的晚到失败只清理,不改写已离开的页面(对齐 prepare 守卫口径)
|
|
284
|
+
if (renderRepoState(error)) return
|
|
285
|
+
if (error.code === 'no-room-layout') {
|
|
286
|
+
setStatus('尚未配置房间', 'warn')
|
|
287
|
+
const box = $('errorBox')
|
|
288
|
+
box.hidden = false
|
|
289
|
+
box.textContent = `此业务尚未配置房间(${error.message})。管理页只读,不会自动创建配置;可从上面的「业务」链接返回。`
|
|
290
|
+
return
|
|
291
|
+
}
|
|
292
|
+
return showError(error.message)
|
|
293
|
+
}
|
|
294
|
+
if (!pageAlive) return // 成功返回也一样:先判存活,再渲染 repo-not-configured 等早退
|
|
295
|
+
if (data.code === 'repo-not-configured') return renderGuide(data)
|
|
296
|
+
|
|
297
|
+
renderRepoLine(data.repo)
|
|
298
|
+
document.title = pageTitle(`${data.business.name} · 房间`, data.repo)
|
|
299
|
+
$('title').textContent = `${data.business.name}的房间`
|
|
300
|
+
$('intro').textContent = data.business.intro || '按 room.json 配置显示的独立房间:底座与家具。'
|
|
301
|
+
setStatus('正在核对房间布置…', 'info')
|
|
302
|
+
|
|
303
|
+
// 固定资源:模板与占位目录只从插件固定地址读取;家具库目录(E4b-2)经真实
|
|
304
|
+
// loadFurnitureCatalog 读取——固定静态资源,不加工作区参数、不接受外部目录地址。
|
|
305
|
+
let templates, placeholderCatalog, furnitureLibrary
|
|
306
|
+
try {
|
|
307
|
+
;[templates, placeholderCatalog, furnitureLibrary] = await Promise.all([
|
|
308
|
+
fetchJson(`${ASSET_BASE}/templates.json`),
|
|
309
|
+
fetchJson(`${ASSET_BASE}/placeholder-catalog.json`),
|
|
310
|
+
loadFurnitureCatalog({ fetchJson }),
|
|
311
|
+
])
|
|
312
|
+
} catch (error) {
|
|
313
|
+
if (!pageAlive) return // 离页后的晚到失败不改写页面;活页面的失败仍完整显示原因
|
|
314
|
+
return showSceneFailure(`房间资源读取失败:${error.message}`)
|
|
315
|
+
}
|
|
316
|
+
if (!pageAlive) return
|
|
317
|
+
// 家具库读取失败:如实列出 code/fieldPath/中文原因,不建场景、不静默只渲染占位家具。
|
|
318
|
+
if (!furnitureLibrary.ok) return showLibraryFailure(furnitureLibrary.diagnostics)
|
|
319
|
+
|
|
320
|
+
// 占位目录容器检查(E4b-2 补齐):合并前只验容器形状——非 null、非数组的对象且
|
|
321
|
+
// assets 是数组。此前直接展开 .assets,目录为 null/{}/assets 非数组时在拼接行抛
|
|
322
|
+
// TypeError 且 main 无捕获,状态停在"正在核对房间布置…"。不把坏形状默认成空数组、
|
|
323
|
+
// 不用可迭代性代替数组判断;逐项校验(重复 ID、坏条目)仍归真实 placement。
|
|
324
|
+
const placeholderIsContainer = placeholderCatalog !== null
|
|
325
|
+
&& typeof placeholderCatalog === 'object' && !Array.isArray(placeholderCatalog)
|
|
326
|
+
if (!placeholderIsContainer || !Array.isArray(placeholderCatalog.assets)) {
|
|
327
|
+
return showCatalogInputFailure(placeholderIsContainer ? 'placeholderCatalog.assets' : 'placeholderCatalog')
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// 两目录成功后以数组拼接合并(E4b-2):不用按 ID 覆盖的 Map/对象展开——重复
|
|
331
|
+
// assetId 交真实 placement 拒绝,不让后者覆盖前者。同一合并目录给摆放与内容绑定。
|
|
332
|
+
const catalog = { schemaVersion: 1, assets: [...placeholderCatalog.assets, ...furnitureLibrary.catalog.assets] }
|
|
333
|
+
|
|
334
|
+
// 按 templateRef 精确匹配模板:未知模板明确报错,不用数组第一项或默认写实模板兜底。
|
|
335
|
+
const known = Array.isArray(templates?.templates) ? templates.templates : []
|
|
336
|
+
const template = known.find((item) => item?.templateId === data.layout.templateRef)
|
|
337
|
+
if (!template) {
|
|
338
|
+
setStatus('房间模板缺失', 'bad')
|
|
339
|
+
showError(`room.json 指定的模板「${data.layout.templateRef}」不在插件模板清单里(现有:${known.map((item) => item.templateId).join('、') || '无'})。请修改配置里的 templateRef。`)
|
|
340
|
+
return
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const result = validatePlacement(template, catalog, data.layout)
|
|
344
|
+
if (!result.ok) return showDiagnostics(result.diagnostics)
|
|
345
|
+
if (!pageAlive) return
|
|
346
|
+
|
|
347
|
+
// 家具资料绑定(E3a resolver,E3b-2 接线;E9c 起目录含流程图):placement 成功后校验,
|
|
348
|
+
// 不复制规则。任何绑定错误:不安装任何家具阅读动作(不用半份绑定),只提示+可展开诊断;
|
|
349
|
+
// 业务可信而绑定有错时 directory 仍完整("业务资料"备用入口照常可读)。
|
|
350
|
+
const resolved = resolveRoomBindings({ business: data.business, layout: data.layout, catalog })
|
|
351
|
+
const bindingByInstance = new Map()
|
|
352
|
+
if (resolved.ok) {
|
|
353
|
+
for (const binding of resolved.bindings) bindingByInstance.set(binding.instanceId, binding)
|
|
354
|
+
} else {
|
|
355
|
+
showBindingNote(resolved.diagnostics)
|
|
356
|
+
}
|
|
357
|
+
// 空目录两种情形分开说:没登记=正常空态;解析有错且目录为空=不可用,指向诊断。
|
|
358
|
+
const docDirectory = resolved.ok || resolved.directory.length > 0 ? resolved.directory : null
|
|
359
|
+
const docEmptyText = docDirectory === null
|
|
360
|
+
? '当前没有可用的资料目录,请查看绑定诊断'
|
|
361
|
+
: '此业务尚未登记文档或流程图'
|
|
362
|
+
|
|
363
|
+
let module
|
|
364
|
+
try {
|
|
365
|
+
module = await loadSceneModule()
|
|
366
|
+
} catch (error) {
|
|
367
|
+
if (!pageAlive) return // 离页后的晚到模块失败不改写页面
|
|
368
|
+
return showSceneFailure(error)
|
|
369
|
+
}
|
|
370
|
+
if (!pageAlive) return
|
|
371
|
+
|
|
372
|
+
// 先异步备好家具批次,再同步建场景(E4b-2 固定设计):等待模型期间不持有半初始化
|
|
373
|
+
// 的 renderer/输入。准备期归 prepare 所有(页面离开时其 isActive 检查自会释放全部
|
|
374
|
+
// 句柄并中止);返回后、交给场景前归本页所有,页面不再存活则立即释放。
|
|
375
|
+
let furniture
|
|
376
|
+
try {
|
|
377
|
+
furniture = await module.prepareRoomFurniture(result.assembly, { isActive: () => pageAlive })
|
|
378
|
+
} catch (error) {
|
|
379
|
+
if (!pageAlive) return // 真正离开后的晚到错误只清理(prepare 内已释放),不写 DOM
|
|
380
|
+
return showSceneFailure(error)
|
|
381
|
+
}
|
|
382
|
+
if (!pageAlive) {
|
|
383
|
+
furniture.dispose()
|
|
384
|
+
return
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
try {
|
|
388
|
+
scene = module.createRoomScene({
|
|
389
|
+
container: $('viewport'),
|
|
390
|
+
template,
|
|
391
|
+
assembly: result.assembly,
|
|
392
|
+
furniture, // 调用瞬间所有权整体转交场景:此后任何失败由场景 releaseAll 释放批次
|
|
393
|
+
// 点击分派(E3b-2):先保留身份显示,再按 instanceId 查已验证绑定;
|
|
394
|
+
// 阅读已打开时迟到的点击不切换集合;未绑定家具只显身份,不猜文档。
|
|
395
|
+
onPick: (picked) => {
|
|
396
|
+
showPick(picked)
|
|
397
|
+
if (!reader || reader.isOpen()) return
|
|
398
|
+
const binding = bindingByInstance.get(picked.instanceId)
|
|
399
|
+
if (!binding) return
|
|
400
|
+
const canvas = $('viewport').querySelector('canvas')
|
|
401
|
+
reader.open({
|
|
402
|
+
title: picked.name,
|
|
403
|
+
entries: binding.entries,
|
|
404
|
+
emptyText: `这件${picked.name}还没有放入资料`,
|
|
405
|
+
returnFocus: canvas ?? $('viewport'),
|
|
406
|
+
})
|
|
407
|
+
},
|
|
408
|
+
})
|
|
409
|
+
} catch (error) {
|
|
410
|
+
scene = null // 批次已由场景释放(转交即接管),本页不再重复释放
|
|
411
|
+
return showSceneFailure(error)
|
|
412
|
+
}
|
|
413
|
+
if (!pageAlive) {
|
|
414
|
+
scene.dispose()
|
|
415
|
+
scene = null
|
|
416
|
+
return
|
|
417
|
+
}
|
|
418
|
+
$('roomStage').hidden = false
|
|
419
|
+
// 全窗口形态(作者令:房间占满全屏,不留其他东西):场景创建成功才切;
|
|
420
|
+
// 错误、空态与场景失败不加这个类,仍是普通滚动文档页。
|
|
421
|
+
document.body.classList.add('room-live')
|
|
422
|
+
// 底座字幕与昼夜开关(作者令保留):字幕默认白天;开关只有童话底座有。
|
|
423
|
+
renderCaption(template.styleId, 'day')
|
|
424
|
+
wirePeriodSwitch(scene, template.styleId)
|
|
425
|
+
// 阅读面板(E3b-2):资料入口只在场景成功装配后存在;面板打开=暂停,关闭=原位恢复。
|
|
426
|
+
reader = createRoomReader({
|
|
427
|
+
root: $('readerRoot'),
|
|
428
|
+
focusFallback: $('viewport'),
|
|
429
|
+
onReadingChange: handleReadingChange,
|
|
430
|
+
})
|
|
431
|
+
wireBizDocs({ directory: docDirectory, emptyText: docEmptyText })
|
|
432
|
+
// 行走/总览/门按钮(P1c-2c-2):默认门外行走,此时才启用新控件并同步提示。
|
|
433
|
+
wireWalkControls(scene)
|
|
434
|
+
setStatus(`${data.business.name} · ${template.name}`, 'ok')
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
main()
|