@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,449 @@
|
|
|
1
|
+
// 家具资料绑定校验(E3a;E9c 扩展流程图):确认某个家具实例绑定的文档与流程图,
|
|
2
|
+
// 确实属于当前业务的登记清单——文档来自 business.json 的 docs,图来自本业务目录内
|
|
3
|
+
// 按既有规则扫描出的图目录(/api/room 响应的 business.charts 投影;business.json
|
|
4
|
+
// 没有 charts 登记数组)。纯逻辑模块:不算坐标、不调用 Three、不 fetch、不读磁盘、
|
|
5
|
+
// 不引入 Node API,也不复制 placement——空间仍归 placement 校验,本模块只管
|
|
6
|
+
// "动作与资料身份"这半边。输入是 /api/room 响应里的 business/layout 与插件固定
|
|
7
|
+
// catalog;生产页面先跑真实 validatePlacement,成功后再调用本模块。
|
|
8
|
+
//
|
|
9
|
+
// 文档身份=当前工作区该业务 business.json docs 里登记的原始路径:不 trim、不正规化、
|
|
10
|
+
// 不当 URL、不拼文件系统;读取安全继续由 /api/doc 提供,这里只生成固定接口链接。
|
|
11
|
+
// 图身份=当前业务的图 ID(图目录名):只收字符串 ID,不收路径、URL、workspace、
|
|
12
|
+
// 版本或对象;阅读链接固定生成 /specdev-workbench/read/<编码业务ID>/<编码图ID>,
|
|
13
|
+
// 不含 workspace,由页面侧补。
|
|
14
|
+
|
|
15
|
+
/** 与 src/core/room.ts 的 ID_PATTERN 同规则(core 不 import dsh,两处同步)。 */
|
|
16
|
+
const ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/
|
|
17
|
+
|
|
18
|
+
/** 首版唯一动作:打开资料集合。E9c 起该动作代表"文档+流程图"的资料集合,
|
|
19
|
+
* 动作名与十二款资产的 capabilities 不批量改名;不因此让灯、桌或展示台自动具有资料能力。 */
|
|
20
|
+
export const BINDING_ACTION = 'open-document-collection'
|
|
21
|
+
|
|
22
|
+
/** 绑定条目的必填字段(instanceId、action、documents)+唯一可选字段 charts(E9c)。 */
|
|
23
|
+
const BINDING_REQUIRED_FIELDS = ['instanceId', 'action', 'documents']
|
|
24
|
+
const BINDING_ALLOWED_FIELDS = [...BINDING_REQUIRED_FIELDS, 'charts']
|
|
25
|
+
|
|
26
|
+
const isObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
27
|
+
const isNonEmptyString = (value) => typeof value === 'string' && value !== ''
|
|
28
|
+
|
|
29
|
+
function diag(code, message, { instanceIds = [], fieldPath = '' } = {}) {
|
|
30
|
+
return { code, instanceIds, fieldPath, message }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 解析并校验房间资料绑定(文档+流程图)。
|
|
35
|
+
* @returns {{ ok: boolean,
|
|
36
|
+
* diagnostics: Array<{ code: string, instanceIds: string[], fieldPath: string, message: string }>,
|
|
37
|
+
* directory: Array<{ kind: 'document', businessId: string, path: string, title: string, href: string } | { kind: 'workflow', businessId: string, chartId: string, title: string, href: string, unavailableReason?: string }>,
|
|
38
|
+
* bindings: Array<{ instanceId: string, assetId: string, action: string,
|
|
39
|
+
* entries: Array<{ kind: 'document', businessId: string, path: string, title: string, href: string } | { kind: 'workflow', businessId: string, chartId: string, title: string, href: string, unavailableReason?: string }> }> }}
|
|
40
|
+
* 成功 ok:true、diagnostics 空;任何绑定错误 ok:false 且 bindings 整体为空(不交付部分有效动作)。
|
|
41
|
+
* 业务身份、docs 与 charts 目录可信、仅绑定条目有错时 directory 仍完整返回(供"业务资料"备用入口);
|
|
42
|
+
* 业务输入坏(含 business.charts 坏形状)、身份不匹配时 directory 也为空。图表中带
|
|
43
|
+
* descriptorError/idConflict 的不可用图保留身份并给条目 unavailableReason(不静默过滤);
|
|
44
|
+
* 绑定到这类图才报 BINDING_CHART_UNAVAILABLE,未绑定时不再连带。
|
|
45
|
+
*/
|
|
46
|
+
export function resolveRoomBindings(input) {
|
|
47
|
+
const diagnostics = []
|
|
48
|
+
const directory = []
|
|
49
|
+
const bindings = []
|
|
50
|
+
|
|
51
|
+
// ⓪ 最外层参数:普通对象才继续(null/数组/字符串/数字等调用错误统一返回诊断,
|
|
52
|
+
// 不在参数解构位置抛 TypeError)。边界限于 JSON 能得到的普通数据与常规调用错误。
|
|
53
|
+
if (!isObject(input)) {
|
|
54
|
+
diagnostics.push(diag(
|
|
55
|
+
'BINDING_INPUT',
|
|
56
|
+
`参数必须是包含 business、layout、catalog 的对象(实际是 ${input === null ? 'null' : Array.isArray(input) ? '数组' : typeof input})`,
|
|
57
|
+
{ fieldPath: 'input' },
|
|
58
|
+
))
|
|
59
|
+
return { ok: false, diagnostics, directory, bindings }
|
|
60
|
+
}
|
|
61
|
+
const { business, layout, catalog } = input
|
|
62
|
+
|
|
63
|
+
// ① 业务前提:对象、合法 id、说明文件无错、docs 为非空字符串数组。前提坏了 directory 也为空。
|
|
64
|
+
if (!isObject(business) || !isNonEmptyString(business.id) || !ID_PATTERN.test(business.id)) {
|
|
65
|
+
diagnostics.push(diag(
|
|
66
|
+
'BINDING_INPUT',
|
|
67
|
+
`business 必须是带合法 id(字母数字开头,可含 . _ -)的对象,实际 id:${JSON.stringify(isObject(business) ? business.id : business)}`,
|
|
68
|
+
{ fieldPath: 'business.id' },
|
|
69
|
+
))
|
|
70
|
+
return { ok: false, diagnostics, directory, bindings }
|
|
71
|
+
}
|
|
72
|
+
if (business.descriptorError) {
|
|
73
|
+
diagnostics.push(diag(
|
|
74
|
+
'BINDING_INPUT',
|
|
75
|
+
`业务 ${business.id} 的说明文件有问题,不能当作可用业务:${business.descriptorError}`,
|
|
76
|
+
{ fieldPath: 'business.descriptorError' },
|
|
77
|
+
))
|
|
78
|
+
return { ok: false, diagnostics, directory, bindings }
|
|
79
|
+
}
|
|
80
|
+
if (!Array.isArray(business.docs) || !business.docs.every(isNonEmptyString)) {
|
|
81
|
+
diagnostics.push(diag(
|
|
82
|
+
'BINDING_INPUT',
|
|
83
|
+
`业务 ${business.id} 的 docs 必须是非空字符串组成的数组(登记清单本身有问题,不能据此校验绑定)`,
|
|
84
|
+
{ fieldPath: 'business.docs' },
|
|
85
|
+
))
|
|
86
|
+
return { ok: false, diagnostics, directory, bindings }
|
|
87
|
+
}
|
|
88
|
+
// business.charts 投影(E9c):省略按 [] 兼容旧调用;显式存在就必须是合法形状。
|
|
89
|
+
// 坏形状(null/非数组/条目非对象/非法 ID/重复 ID/非字符串名称或错误标记)返回
|
|
90
|
+
// BINDING_INPUT 与准确字段位置——业务前提坏了,目录也不交付(不交半份业务目录)。
|
|
91
|
+
const chartList = business.charts === undefined ? [] : business.charts
|
|
92
|
+
if (!Array.isArray(chartList)) {
|
|
93
|
+
diagnostics.push(diag(
|
|
94
|
+
'BINDING_INPUT',
|
|
95
|
+
`业务 ${business.id} 的 charts 必须是图目录数组(省略视为没有流程图;实际是 ${chartList === null ? 'null' : typeof chartList})`,
|
|
96
|
+
{ fieldPath: 'business.charts' },
|
|
97
|
+
))
|
|
98
|
+
return { ok: false, diagnostics, directory, bindings }
|
|
99
|
+
}
|
|
100
|
+
const chartById = new Map()
|
|
101
|
+
for (const [index, chart] of chartList.entries()) {
|
|
102
|
+
const at = `business.charts[${index}]`
|
|
103
|
+
if (!isObject(chart) || !isNonEmptyString(chart.id) || !ID_PATTERN.test(chart.id)) {
|
|
104
|
+
diagnostics.push(diag(
|
|
105
|
+
'BINDING_INPUT',
|
|
106
|
+
`${at} 必须是带合法 id(字母数字开头,可含 . _ -)的图目录对象,实际 id:${JSON.stringify(isObject(chart) ? chart.id : chart)}`,
|
|
107
|
+
{ fieldPath: `${at}.id` },
|
|
108
|
+
))
|
|
109
|
+
return { ok: false, diagnostics, directory, bindings }
|
|
110
|
+
}
|
|
111
|
+
if (chartById.has(chart.id)) {
|
|
112
|
+
diagnostics.push(diag(
|
|
113
|
+
'BINDING_INPUT',
|
|
114
|
+
`图目录里 ${chart.id} 重复登记(${at})`,
|
|
115
|
+
{ fieldPath: `${at}.id` },
|
|
116
|
+
))
|
|
117
|
+
return { ok: false, diagnostics, directory, bindings }
|
|
118
|
+
}
|
|
119
|
+
if (!isNonEmptyString(chart.name)) {
|
|
120
|
+
diagnostics.push(diag(
|
|
121
|
+
'BINDING_INPUT',
|
|
122
|
+
`${at}.name 必须是非空字符串(实际是 ${JSON.stringify(chart.name)})`,
|
|
123
|
+
{ fieldPath: `${at}.name` },
|
|
124
|
+
))
|
|
125
|
+
return { ok: false, diagnostics, directory, bindings }
|
|
126
|
+
}
|
|
127
|
+
for (const marker of ['descriptorError', 'idConflict']) {
|
|
128
|
+
if (chart[marker] !== undefined && !isNonEmptyString(chart[marker])) {
|
|
129
|
+
diagnostics.push(diag(
|
|
130
|
+
'BINDING_INPUT',
|
|
131
|
+
`${at}.${marker} 存在时必须是非空字符串(实际是 ${JSON.stringify(chart[marker])})`,
|
|
132
|
+
{ fieldPath: `${at}.${marker}` },
|
|
133
|
+
))
|
|
134
|
+
return { ok: false, diagnostics, directory, bindings }
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
chartById.set(chart.id, chart)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ② 归属前提:layout 是对象且 businessId 与 business.id 严格相同(不静默对齐)。
|
|
141
|
+
if (!isObject(layout)) {
|
|
142
|
+
diagnostics.push(diag('BINDING_INPUT', 'layout 必须是对象', { fieldPath: 'layout' }))
|
|
143
|
+
return { ok: false, diagnostics, directory, bindings }
|
|
144
|
+
}
|
|
145
|
+
if (layout.businessId !== business.id) {
|
|
146
|
+
diagnostics.push(diag(
|
|
147
|
+
'BINDING_BUSINESS_MISMATCH',
|
|
148
|
+
`布局归属的业务(${JSON.stringify(layout.businessId)})与当前业务(${business.id})不一致,绑定只在归属业务内生效`,
|
|
149
|
+
{ fieldPath: 'layout.businessId' },
|
|
150
|
+
))
|
|
151
|
+
return { ok: false, diagnostics, directory, bindings }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 业务可信:先逐项确认登记路径能按现有方法编码(JSON 可表示未配对的 UTF-16 代理
|
|
155
|
+
// 码元,encodeURIComponent 会对其抛 URIError——配置输入错误不能变成页面模块异常)。
|
|
156
|
+
// 全部通过才交付目录(局部候选数组,中途失败不返回半份);不用替换字符、丢弃坏项
|
|
157
|
+
// 或重新正规化来"修好"路径,登记身份不得悄悄改变;成对码元(如 emoji)正常编码。
|
|
158
|
+
// 目录按登记顺序去重,href 走固定 /api/doc 接口(不带 workspace,由页面侧补)。
|
|
159
|
+
const candidates = []
|
|
160
|
+
const seenPaths = new Set()
|
|
161
|
+
for (const [index, docPath] of business.docs.entries()) {
|
|
162
|
+
if (seenPaths.has(docPath)) continue
|
|
163
|
+
seenPaths.add(docPath)
|
|
164
|
+
let href
|
|
165
|
+
try {
|
|
166
|
+
href = `/specdev-workbench/api/doc?business=${encodeURIComponent(business.id)}&path=${encodeURIComponent(docPath)}`
|
|
167
|
+
} catch {
|
|
168
|
+
diagnostics.push(diag(
|
|
169
|
+
'BINDING_INPUT',
|
|
170
|
+
`登记路径包含无法编码的字符(如未配对的 UTF-16 代理码元),不能生成阅读链接:${JSON.stringify(docPath)}`,
|
|
171
|
+
{ fieldPath: `business.docs[${index}]` },
|
|
172
|
+
))
|
|
173
|
+
return { ok: false, diagnostics, directory, bindings }
|
|
174
|
+
}
|
|
175
|
+
candidates.push({
|
|
176
|
+
kind: 'document',
|
|
177
|
+
businessId: business.id,
|
|
178
|
+
path: docPath,
|
|
179
|
+
title: docPath,
|
|
180
|
+
href,
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
// 图目录(E9c):全部登记图都进目录(含不可用者,"业务资料"里能看见并说明原因)。
|
|
184
|
+
// 带 descriptorError/idConflict 的图保留身份并附 unavailableReason,不静默过滤、
|
|
185
|
+
// 不输出 undefined 链接;hasWorkflow=false 仅表示文件不存在,仍生成登记引用——缺图
|
|
186
|
+
// 由既有阅读页/CLI 显示原因,不在这里猜。href 固定走 read 页面(不含 workspace)。
|
|
187
|
+
const entryByChartId = new Map()
|
|
188
|
+
for (const chart of chartList) {
|
|
189
|
+
const entry = {
|
|
190
|
+
kind: 'workflow',
|
|
191
|
+
businessId: business.id,
|
|
192
|
+
chartId: chart.id,
|
|
193
|
+
title: chart.name,
|
|
194
|
+
href: `/specdev-workbench/read/${encodeURIComponent(business.id)}/${encodeURIComponent(chart.id)}`,
|
|
195
|
+
}
|
|
196
|
+
const unavailableReason = chart.descriptorError ?? chart.idConflict
|
|
197
|
+
if (unavailableReason) entry.unavailableReason = unavailableReason
|
|
198
|
+
entryByChartId.set(chart.id, entry)
|
|
199
|
+
candidates.push(entry)
|
|
200
|
+
}
|
|
201
|
+
directory.push(...candidates)
|
|
202
|
+
const entryByPath = new Map(directory.filter((entry) => entry.kind === 'document').map((entry) => [entry.path, entry]))
|
|
203
|
+
|
|
204
|
+
// ③ 引用前提:实例与资产可解析(对象、ID 字段可用、不重复)。失败时目录仍返回,绑定为空。
|
|
205
|
+
if (!Array.isArray(layout.instances)) {
|
|
206
|
+
diagnostics.push(diag('BINDING_INPUT', 'layout.instances 必须是数组', { fieldPath: 'layout.instances' }))
|
|
207
|
+
return { ok: false, diagnostics, directory, bindings }
|
|
208
|
+
}
|
|
209
|
+
if (!isObject(catalog) || !Array.isArray(catalog.assets)) {
|
|
210
|
+
diagnostics.push(diag('BINDING_INPUT', 'catalog.assets 必须是数组', { fieldPath: 'catalog.assets' }))
|
|
211
|
+
return { ok: false, diagnostics, directory, bindings }
|
|
212
|
+
}
|
|
213
|
+
const instanceById = new Map()
|
|
214
|
+
let instancesUsable = true
|
|
215
|
+
for (const [index, instance] of layout.instances.entries()) {
|
|
216
|
+
if (!isObject(instance) || !isNonEmptyString(instance.instanceId) || !isNonEmptyString(instance.assetId)) {
|
|
217
|
+
diagnostics.push(diag(
|
|
218
|
+
'BINDING_INPUT',
|
|
219
|
+
`布局实例 layout.instances[${index}] 必须是带非空 instanceId/assetId 字符串的对象`,
|
|
220
|
+
{ fieldPath: `layout.instances[${index}]` },
|
|
221
|
+
))
|
|
222
|
+
instancesUsable = false
|
|
223
|
+
} else if (instanceById.has(instance.instanceId)) {
|
|
224
|
+
diagnostics.push(diag(
|
|
225
|
+
'BINDING_INPUT',
|
|
226
|
+
`布局里的实例 ID 重复:${instance.instanceId}`,
|
|
227
|
+
{ instanceIds: [instance.instanceId], fieldPath: `layout.instances[${index}].instanceId` },
|
|
228
|
+
))
|
|
229
|
+
instancesUsable = false
|
|
230
|
+
} else {
|
|
231
|
+
instanceById.set(instance.instanceId, instance)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const assetById = new Map()
|
|
235
|
+
let assetsUsable = true
|
|
236
|
+
for (const [index, asset] of catalog.assets.entries()) {
|
|
237
|
+
if (!isObject(asset) || !isNonEmptyString(asset.assetId)) {
|
|
238
|
+
diagnostics.push(diag(
|
|
239
|
+
'BINDING_INPUT',
|
|
240
|
+
`目录资产 catalog.assets[${index}] 必须是带非空 assetId 字符串的对象`,
|
|
241
|
+
{ fieldPath: `catalog.assets[${index}]` },
|
|
242
|
+
))
|
|
243
|
+
assetsUsable = false
|
|
244
|
+
} else if (assetById.has(asset.assetId)) {
|
|
245
|
+
diagnostics.push(diag(
|
|
246
|
+
'BINDING_INPUT',
|
|
247
|
+
`目录里的资产 ID 重复:${asset.assetId}`,
|
|
248
|
+
{ fieldPath: `catalog.assets[${index}].assetId` },
|
|
249
|
+
))
|
|
250
|
+
assetsUsable = false
|
|
251
|
+
} else {
|
|
252
|
+
assetById.set(asset.assetId, asset)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (!instancesUsable || !assetsUsable) {
|
|
256
|
+
return { ok: false, diagnostics, directory, bindings }
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ④ 逐条校验 bindings:字段白名单 → 重复目标 → 实例 → 资产 → 动作 → 能力 → 资料清单。
|
|
260
|
+
const bindingList = layout.bindings === undefined ? [] : layout.bindings
|
|
261
|
+
if (!Array.isArray(bindingList)) {
|
|
262
|
+
diagnostics.push(diag(
|
|
263
|
+
'BINDING_INPUT',
|
|
264
|
+
`layout.bindings 必须是数组(省略或空数组=没有家具资料绑定;实际是 ${bindingList === null ? 'null' : typeof bindingList})`,
|
|
265
|
+
{ fieldPath: 'layout.bindings' },
|
|
266
|
+
))
|
|
267
|
+
return { ok: false, diagnostics, directory, bindings }
|
|
268
|
+
}
|
|
269
|
+
const boundInstances = new Set()
|
|
270
|
+
const resolved = []
|
|
271
|
+
for (const [index, raw] of bindingList.entries()) {
|
|
272
|
+
const at = `bindings[${index}]`
|
|
273
|
+
if (!isObject(raw)) {
|
|
274
|
+
diagnostics.push(diag('BINDING_INPUT', `${at} 必须是对象(实际是 ${Array.isArray(raw) ? '数组' : typeof raw})`, { fieldPath: at }))
|
|
275
|
+
continue
|
|
276
|
+
}
|
|
277
|
+
// 字段白名单(E9c):instanceId、action、documents 必填,charts 唯一可选;
|
|
278
|
+
// 旧三字段配置照常通过;href、workspace、businessId 等仍然拒绝。
|
|
279
|
+
const keys = Object.keys(raw)
|
|
280
|
+
const extraFields = keys.filter((key) => !BINDING_ALLOWED_FIELDS.includes(key))
|
|
281
|
+
const missingFields = BINDING_REQUIRED_FIELDS.filter((field) => !(field in raw))
|
|
282
|
+
if (extraFields.length > 0 || missingFields.length > 0) {
|
|
283
|
+
const problems = []
|
|
284
|
+
if (missingFields.length) problems.push(`缺少:${missingFields.join('、')}`)
|
|
285
|
+
if (extraFields.length) problems.push(`多出:${extraFields.join('、')}`)
|
|
286
|
+
diagnostics.push(diag(
|
|
287
|
+
'BINDING_INPUT',
|
|
288
|
+
`${at} 只允许 instanceId、action、documents(必填)与 charts(可选),不收 href、workspace、businessId 等;实际字段(${keys.join('、') || '(无)'})${problems.join(';')}`,
|
|
289
|
+
{ fieldPath: at },
|
|
290
|
+
))
|
|
291
|
+
continue
|
|
292
|
+
}
|
|
293
|
+
const { instanceId, action, documents, charts } = raw
|
|
294
|
+
if (!isNonEmptyString(instanceId)) {
|
|
295
|
+
diagnostics.push(diag('BINDING_INPUT', `${at}.instanceId 必须是非空字符串(实际是 ${JSON.stringify(instanceId)})`, { fieldPath: `${at}.instanceId` }))
|
|
296
|
+
continue
|
|
297
|
+
}
|
|
298
|
+
if (boundInstances.has(instanceId)) {
|
|
299
|
+
diagnostics.push(diag(
|
|
300
|
+
'BINDING_DUPLICATE_TARGET',
|
|
301
|
+
`实例 ${instanceId} 已绑定过一次,首版每个实例最多绑定一个动作`,
|
|
302
|
+
{ instanceIds: [instanceId], fieldPath: `${at}.instanceId` },
|
|
303
|
+
))
|
|
304
|
+
continue
|
|
305
|
+
}
|
|
306
|
+
const instance = instanceById.get(instanceId)
|
|
307
|
+
if (!instance) {
|
|
308
|
+
diagnostics.push(diag(
|
|
309
|
+
'BINDING_UNKNOWN_INSTANCE',
|
|
310
|
+
`绑定指向的实例 ${instanceId} 不在本房间布局里`,
|
|
311
|
+
{ instanceIds: [instanceId], fieldPath: `${at}.instanceId` },
|
|
312
|
+
))
|
|
313
|
+
continue
|
|
314
|
+
}
|
|
315
|
+
const asset = assetById.get(instance.assetId)
|
|
316
|
+
if (!asset) {
|
|
317
|
+
diagnostics.push(diag(
|
|
318
|
+
'BINDING_UNKNOWN_ASSET',
|
|
319
|
+
`实例 ${instanceId} 引用的资产 ${instance.assetId} 不在目录里(绑定不能提供或覆盖资产)`,
|
|
320
|
+
{ instanceIds: [instanceId], fieldPath: `${at}.instanceId` },
|
|
321
|
+
))
|
|
322
|
+
continue
|
|
323
|
+
}
|
|
324
|
+
if (action !== BINDING_ACTION) {
|
|
325
|
+
diagnostics.push(diag(
|
|
326
|
+
'BINDING_ACTION_UNSUPPORTED',
|
|
327
|
+
`动作只支持 ${BINDING_ACTION}(实际是 ${JSON.stringify(action)})`,
|
|
328
|
+
{ instanceIds: [instanceId], fieldPath: `${at}.action` },
|
|
329
|
+
))
|
|
330
|
+
continue
|
|
331
|
+
}
|
|
332
|
+
if (!Array.isArray(asset.capabilities) || !asset.capabilities.includes(BINDING_ACTION)) {
|
|
333
|
+
diagnostics.push(diag(
|
|
334
|
+
'BINDING_CAPABILITY_MISSING',
|
|
335
|
+
`资产 ${instance.assetId} 未在目录登记 ${BINDING_ACTION} 能力,不能给它绑资料`,
|
|
336
|
+
{ instanceIds: [instanceId], fieldPath: `${at}.action` },
|
|
337
|
+
))
|
|
338
|
+
continue
|
|
339
|
+
}
|
|
340
|
+
if (!Array.isArray(documents)) {
|
|
341
|
+
diagnostics.push(diag(
|
|
342
|
+
'BINDING_INPUT',
|
|
343
|
+
`${at}.documents 必须是数组(可为空=明确的空集合;实际是 ${documents === null ? 'null' : typeof documents})`,
|
|
344
|
+
{ instanceIds: [instanceId], fieldPath: `${at}.documents` },
|
|
345
|
+
))
|
|
346
|
+
continue
|
|
347
|
+
}
|
|
348
|
+
// charts 省略视为 [];只收当前业务已登记的图 ID 字符串——不收路径、URL、workspace、
|
|
349
|
+
// 版本或对象。集合内重复图拒绝;不同家具可以引用同一张图。图与文档身份分开:
|
|
350
|
+
// 同一字符串在两份清单里各查各的表,不互相覆盖、不互相借用。
|
|
351
|
+
const chartIds = charts === undefined ? [] : charts
|
|
352
|
+
if (!Array.isArray(chartIds)) {
|
|
353
|
+
diagnostics.push(diag(
|
|
354
|
+
'BINDING_INPUT',
|
|
355
|
+
`${at}.charts 必须是图 ID 字符串数组(省略视为空;实际是 ${chartIds === null ? 'null' : typeof chartIds})`,
|
|
356
|
+
{ instanceIds: [instanceId], fieldPath: `${at}.charts` },
|
|
357
|
+
))
|
|
358
|
+
continue
|
|
359
|
+
}
|
|
360
|
+
const entries = []
|
|
361
|
+
const collectionPaths = new Set()
|
|
362
|
+
let documentsOk = true
|
|
363
|
+
for (const [docIndex, docPath] of documents.entries()) {
|
|
364
|
+
if (!isNonEmptyString(docPath)) {
|
|
365
|
+
diagnostics.push(diag(
|
|
366
|
+
'BINDING_INPUT',
|
|
367
|
+
`${at}.documents[${docIndex}] 必须是非空字符串(实际是 ${JSON.stringify(docPath)})`,
|
|
368
|
+
{ instanceIds: [instanceId], fieldPath: `${at}.documents[${docIndex}]` },
|
|
369
|
+
))
|
|
370
|
+
documentsOk = false
|
|
371
|
+
continue
|
|
372
|
+
}
|
|
373
|
+
if (collectionPaths.has(docPath)) {
|
|
374
|
+
diagnostics.push(diag(
|
|
375
|
+
'BINDING_DUPLICATE_DOCUMENT',
|
|
376
|
+
`实例 ${instanceId} 的集合里重复登记了文档:${docPath}`,
|
|
377
|
+
{ instanceIds: [instanceId], fieldPath: `${at}.documents[${docIndex}]` },
|
|
378
|
+
))
|
|
379
|
+
documentsOk = false
|
|
380
|
+
continue
|
|
381
|
+
}
|
|
382
|
+
collectionPaths.add(docPath)
|
|
383
|
+
const entry = entryByPath.get(docPath)
|
|
384
|
+
if (!entry) {
|
|
385
|
+
diagnostics.push(diag(
|
|
386
|
+
'BINDING_DOCUMENT_UNREGISTERED',
|
|
387
|
+
`文档 ${docPath} 未在业务 ${business.id} 的登记清单里(仅在其他业务登记也不行)`,
|
|
388
|
+
{ instanceIds: [instanceId], fieldPath: `${at}.documents[${docIndex}]` },
|
|
389
|
+
))
|
|
390
|
+
documentsOk = false
|
|
391
|
+
} else {
|
|
392
|
+
entries.push({ ...entry })
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
// 文档清单有错也继续检查 charts:一次报全(与文档循环收满再退同风格),
|
|
396
|
+
// 最后统一 documentsOk || chartsOk 才continue。
|
|
397
|
+
// 图清单:先按 documents 顺序放文档条目,再按 charts 顺序放图条目(E9c 契约)。
|
|
398
|
+
const collectionChartIds = new Set()
|
|
399
|
+
let chartsOk = true
|
|
400
|
+
for (const [chartIndex, chartId] of chartIds.entries()) {
|
|
401
|
+
if (!isNonEmptyString(chartId)) {
|
|
402
|
+
diagnostics.push(diag(
|
|
403
|
+
'BINDING_INPUT',
|
|
404
|
+
`${at}.charts[${chartIndex}] 必须是非空字符串的图 ID(不收路径、URL、workspace、版本或对象;实际是 ${JSON.stringify(chartId)})`,
|
|
405
|
+
{ instanceIds: [instanceId], fieldPath: `${at}.charts[${chartIndex}]` },
|
|
406
|
+
))
|
|
407
|
+
chartsOk = false
|
|
408
|
+
continue
|
|
409
|
+
}
|
|
410
|
+
if (collectionChartIds.has(chartId)) {
|
|
411
|
+
diagnostics.push(diag(
|
|
412
|
+
'BINDING_DUPLICATE_CHART',
|
|
413
|
+
`实例 ${instanceId} 的集合里重复登记了流程图:${chartId}`,
|
|
414
|
+
{ instanceIds: [instanceId], fieldPath: `${at}.charts[${chartIndex}]` },
|
|
415
|
+
))
|
|
416
|
+
chartsOk = false
|
|
417
|
+
continue
|
|
418
|
+
}
|
|
419
|
+
collectionChartIds.add(chartId)
|
|
420
|
+
const chart = chartById.get(chartId)
|
|
421
|
+
if (!chart) {
|
|
422
|
+
diagnostics.push(diag(
|
|
423
|
+
'BINDING_CHART_UNREGISTERED',
|
|
424
|
+
`流程图 ${chartId} 未在业务 ${business.id} 的图目录里(仅在其他业务登记也不行)`,
|
|
425
|
+
{ instanceIds: [instanceId], fieldPath: `${at}.charts[${chartIndex}]` },
|
|
426
|
+
))
|
|
427
|
+
chartsOk = false
|
|
428
|
+
continue
|
|
429
|
+
}
|
|
430
|
+
const unavailableReason = chart.descriptorError ?? chart.idConflict
|
|
431
|
+
if (unavailableReason) {
|
|
432
|
+
diagnostics.push(diag(
|
|
433
|
+
'BINDING_CHART_UNAVAILABLE',
|
|
434
|
+
`流程图 ${chartId}(${chart.name})当前不可用:${unavailableReason}`,
|
|
435
|
+
{ instanceIds: [instanceId], fieldPath: `${at}.charts[${chartIndex}]` },
|
|
436
|
+
))
|
|
437
|
+
chartsOk = false
|
|
438
|
+
continue
|
|
439
|
+
}
|
|
440
|
+
entries.push({ ...entryByChartId.get(chartId) })
|
|
441
|
+
}
|
|
442
|
+
if (!documentsOk || !chartsOk) continue
|
|
443
|
+
boundInstances.add(instanceId)
|
|
444
|
+
resolved.push({ instanceId, assetId: instance.assetId, action, entries })
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (diagnostics.length > 0) return { ok: false, diagnostics, directory, bindings }
|
|
448
|
+
return { ok: true, diagnostics: [], directory, bindings: resolved }
|
|
449
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
|
|
3
|
+
// 所有陶瓷房间共用递归锁;离屏渲染期间不再触发另一面反射。
|
|
4
|
+
const reflectingRenderers = new WeakSet();
|
|
5
|
+
|
|
6
|
+
/** 只为水平地面服务的实时平面反射,保留物理材质原有的釉面照明。 */
|
|
7
|
+
export function createCeramicReflection({ surface, material, glazeTexture, renderer, scene }) {
|
|
8
|
+
const target = new THREE.WebGLRenderTarget(1024, 1024, {
|
|
9
|
+
type: THREE.HalfFloatType,
|
|
10
|
+
minFilter: THREE.LinearFilter,
|
|
11
|
+
magFilter: THREE.LinearFilter,
|
|
12
|
+
depthBuffer: true,
|
|
13
|
+
generateMipmaps: false,
|
|
14
|
+
});
|
|
15
|
+
target.texture.name = '陶瓷地面 / 实时线性反射';
|
|
16
|
+
target.texture.colorSpace = THREE.LinearSRGBColorSpace;
|
|
17
|
+
const uniforms = {
|
|
18
|
+
roomReflection: { value: target.texture },
|
|
19
|
+
roomReflectionMatrix: { value: new THREE.Matrix4() },
|
|
20
|
+
roomGlaze: { value: glazeTexture },
|
|
21
|
+
roomReflectionReady: { value: 0 },
|
|
22
|
+
roomReflectionTexel: { value: new THREE.Vector2(1 / 1024, 1 / 1024) },
|
|
23
|
+
};
|
|
24
|
+
let mirrorCamera = new THREE.PerspectiveCamera();
|
|
25
|
+
let disposed = false;
|
|
26
|
+
let lastWidth = 1024;
|
|
27
|
+
let lastHeight = 1024;
|
|
28
|
+
const surfacePosition = new THREE.Vector3();
|
|
29
|
+
const cameraPosition = new THREE.Vector3();
|
|
30
|
+
const normal = new THREE.Vector3();
|
|
31
|
+
const view = new THREE.Vector3();
|
|
32
|
+
const lookAt = new THREE.Vector3();
|
|
33
|
+
const reflectedTarget = new THREE.Vector3();
|
|
34
|
+
const rotation = new THREE.Matrix4();
|
|
35
|
+
const plane = new THREE.Plane();
|
|
36
|
+
const clip = new THREE.Vector4();
|
|
37
|
+
const q = new THREE.Vector4();
|
|
38
|
+
const projectionInverse = new THREE.Matrix4();
|
|
39
|
+
const drawSize = new THREE.Vector2();
|
|
40
|
+
const viewport = new THREE.Vector4();
|
|
41
|
+
|
|
42
|
+
material.onBeforeCompile = (shader) => {
|
|
43
|
+
Object.assign(shader.uniforms, uniforms);
|
|
44
|
+
shader.vertexShader = shader.vertexShader.replace('#include <common>', `
|
|
45
|
+
#include <common>
|
|
46
|
+
uniform mat4 roomReflectionMatrix;
|
|
47
|
+
varying vec4 vRoomReflectionCoord;
|
|
48
|
+
`).replace('#include <project_vertex>', `
|
|
49
|
+
#include <project_vertex>
|
|
50
|
+
vRoomReflectionCoord = roomReflectionMatrix * modelMatrix * vec4(transformed, 1.0);
|
|
51
|
+
`);
|
|
52
|
+
shader.fragmentShader = shader.fragmentShader.replace('#include <common>', `
|
|
53
|
+
#include <common>
|
|
54
|
+
uniform sampler2D roomReflection;
|
|
55
|
+
uniform sampler2D roomGlaze;
|
|
56
|
+
uniform vec2 roomReflectionTexel;
|
|
57
|
+
uniform float roomReflectionReady;
|
|
58
|
+
varying vec4 vRoomReflectionCoord;
|
|
59
|
+
`).replace('#include <opaque_fragment>', `
|
|
60
|
+
// 真实镜像相机提供窗框、门与之后加入的家具;釉面起伏只扰动极少量坐标。
|
|
61
|
+
vec2 reflectUv = vRoomReflectionCoord.xy / vRoomReflectionCoord.w;
|
|
62
|
+
vec2 glazeSlope = texture2D(roomGlaze, vMapUv).rg * 2.0 - 1.0;
|
|
63
|
+
reflectUv += glazeSlope * 0.0022;
|
|
64
|
+
vec2 blurStep = roomReflectionTexel * 1.35;
|
|
65
|
+
vec3 reflected = texture2D(roomReflection, reflectUv).rgb * 0.28;
|
|
66
|
+
reflected += texture2D(roomReflection, reflectUv + vec2(blurStep.x, 0.0)).rgb * 0.12;
|
|
67
|
+
reflected += texture2D(roomReflection, reflectUv - vec2(blurStep.x, 0.0)).rgb * 0.12;
|
|
68
|
+
reflected += texture2D(roomReflection, reflectUv + vec2(0.0, blurStep.y)).rgb * 0.12;
|
|
69
|
+
reflected += texture2D(roomReflection, reflectUv - vec2(0.0, blurStep.y)).rgb * 0.12;
|
|
70
|
+
reflected += texture2D(roomReflection, reflectUv + blurStep).rgb * 0.055;
|
|
71
|
+
reflected += texture2D(roomReflection, reflectUv - blurStep).rgb * 0.055;
|
|
72
|
+
reflected += texture2D(roomReflection, reflectUv + vec2(blurStep.x, -blurStep.y)).rgb * 0.055;
|
|
73
|
+
reflected += texture2D(roomReflection, reflectUv + vec2(-blurStep.x, blurStep.y)).rgb * 0.055;
|
|
74
|
+
float viewGlance = 1.0 - clamp(dot(normal, geometryViewDir), 0.0, 1.0);
|
|
75
|
+
float reflectAmount = (0.19 + 0.46 * pow(viewGlance, 3.0)) * roomReflectionReady;
|
|
76
|
+
// 灰缝保持哑光;只有完整釉面承担倒影。
|
|
77
|
+
reflectAmount *= smoothstep(0.12, 0.4, texture2D(roomGlaze, vMapUv).b);
|
|
78
|
+
float edgeMask = step(0.001, reflectUv.x) * step(reflectUv.x, 0.999)
|
|
79
|
+
* step(0.001, reflectUv.y) * step(reflectUv.y, 0.999);
|
|
80
|
+
outgoingLight = mix(outgoingLight, reflected, reflectAmount * edgeMask);
|
|
81
|
+
#include <opaque_fragment>
|
|
82
|
+
`);
|
|
83
|
+
};
|
|
84
|
+
material.customProgramCacheKey = () => 'ceramic-room-planar-reflection-v1';
|
|
85
|
+
|
|
86
|
+
surface.onBeforeRender = (activeRenderer, activeScene, camera) => {
|
|
87
|
+
const render = renderer || activeRenderer;
|
|
88
|
+
const world = scene || activeScene;
|
|
89
|
+
if (disposed || !render || !world || !camera || reflectingRenderers.has(render)) return;
|
|
90
|
+
if (!camera.isPerspectiveCamera && !camera.isOrthographicCamera) return;
|
|
91
|
+
|
|
92
|
+
surface.updateWorldMatrix(true, false);
|
|
93
|
+
camera.updateWorldMatrix(true, false);
|
|
94
|
+
surfacePosition.setFromMatrixPosition(surface.matrixWorld);
|
|
95
|
+
cameraPosition.setFromMatrixPosition(camera.matrixWorld);
|
|
96
|
+
rotation.extractRotation(surface.matrixWorld);
|
|
97
|
+
normal.set(0, 0, 1).transformDirection(rotation);
|
|
98
|
+
view.subVectors(surfacePosition, cameraPosition);
|
|
99
|
+
if (view.dot(normal) >= 0) { uniforms.roomReflectionReady.value = 0; return; }
|
|
100
|
+
|
|
101
|
+
if (mirrorCamera.isOrthographicCamera !== camera.isOrthographicCamera) {
|
|
102
|
+
mirrorCamera = camera.isOrthographicCamera ? new THREE.OrthographicCamera() : new THREE.PerspectiveCamera();
|
|
103
|
+
}
|
|
104
|
+
view.reflect(normal).negate().add(surfacePosition);
|
|
105
|
+
rotation.extractRotation(camera.matrixWorld);
|
|
106
|
+
lookAt.set(0, 0, -1).transformDirection(rotation).add(cameraPosition);
|
|
107
|
+
reflectedTarget.subVectors(surfacePosition, lookAt).reflect(normal).negate().add(surfacePosition);
|
|
108
|
+
mirrorCamera.position.copy(view);
|
|
109
|
+
mirrorCamera.up.set(0, 1, 0).transformDirection(rotation).reflect(normal);
|
|
110
|
+
mirrorCamera.lookAt(reflectedTarget);
|
|
111
|
+
mirrorCamera.near = camera.near;
|
|
112
|
+
mirrorCamera.far = camera.far;
|
|
113
|
+
mirrorCamera.layers.mask = camera.layers.mask;
|
|
114
|
+
mirrorCamera.projectionMatrix.copy(camera.projectionMatrix);
|
|
115
|
+
mirrorCamera.updateMatrixWorld();
|
|
116
|
+
mirrorCamera.matrixWorldInverse.copy(mirrorCamera.matrixWorld).invert();
|
|
117
|
+
|
|
118
|
+
uniforms.roomReflectionMatrix.value.set(
|
|
119
|
+
0.5, 0, 0, 0.5,
|
|
120
|
+
0, 0.5, 0, 0.5,
|
|
121
|
+
0, 0, 0.5, 0.5,
|
|
122
|
+
0, 0, 0, 1,
|
|
123
|
+
).multiply(mirrorCamera.projectionMatrix).multiply(mirrorCamera.matrixWorldInverse);
|
|
124
|
+
|
|
125
|
+
// 将地面变成镜像相机的近裁面,避免地台底面和房间外部倒灌进反射。
|
|
126
|
+
plane.setFromNormalAndCoplanarPoint(normal, surfacePosition).applyMatrix4(mirrorCamera.matrixWorldInverse);
|
|
127
|
+
clip.set(plane.normal.x, plane.normal.y, plane.normal.z, plane.constant);
|
|
128
|
+
projectionInverse.copy(mirrorCamera.projectionMatrix).invert();
|
|
129
|
+
q.set(Math.sign(clip.x), Math.sign(clip.y), 1, 1).applyMatrix4(projectionInverse);
|
|
130
|
+
const denominator = clip.dot(q);
|
|
131
|
+
if (Math.abs(denominator) < 0.000001) return;
|
|
132
|
+
clip.multiplyScalar(2 / denominator);
|
|
133
|
+
const projection = mirrorCamera.projectionMatrix.elements;
|
|
134
|
+
projection[2] = clip.x - projection[3];
|
|
135
|
+
projection[6] = clip.y - projection[7];
|
|
136
|
+
projection[10] = clip.z - projection[11] - 0.0003;
|
|
137
|
+
projection[14] = clip.w - projection[15];
|
|
138
|
+
mirrorCamera.projectionMatrixInverse.copy(mirrorCamera.projectionMatrix).invert();
|
|
139
|
+
|
|
140
|
+
render.getDrawingBufferSize(drawSize);
|
|
141
|
+
const scale = Math.min(1, 1400 / Math.max(drawSize.x, drawSize.y));
|
|
142
|
+
const width = Math.max(256, Math.round(drawSize.x * scale));
|
|
143
|
+
const height = Math.max(256, Math.round(drawSize.y * scale));
|
|
144
|
+
if (width !== lastWidth || height !== lastHeight) {
|
|
145
|
+
target.setSize(width, height);
|
|
146
|
+
uniforms.roomReflectionTexel.value.set(1 / width, 1 / height);
|
|
147
|
+
lastWidth = width;
|
|
148
|
+
lastHeight = height;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const previousTarget = render.getRenderTarget();
|
|
152
|
+
const previousFace = render.getActiveCubeFace();
|
|
153
|
+
const previousMip = render.getActiveMipmapLevel();
|
|
154
|
+
const previousXR = render.xr.enabled;
|
|
155
|
+
const previousShadowUpdate = render.shadowMap.autoUpdate;
|
|
156
|
+
const previousAutoClear = render.autoClear;
|
|
157
|
+
const previousVisibility = surface.visible;
|
|
158
|
+
render.getCurrentViewport(viewport);
|
|
159
|
+
reflectingRenderers.add(render);
|
|
160
|
+
try {
|
|
161
|
+
surface.visible = false;
|
|
162
|
+
render.xr.enabled = false;
|
|
163
|
+
render.shadowMap.autoUpdate = false;
|
|
164
|
+
render.autoClear = true;
|
|
165
|
+
render.setRenderTarget(target);
|
|
166
|
+
// setRenderTarget 已用目标纹理的物理尺寸设置 viewport / scissor;
|
|
167
|
+
// 此处不能再 setViewport,否则高 DPI 屏幕会被额外乘一次像素比例。
|
|
168
|
+
render.state.buffers.depth.setMask(true);
|
|
169
|
+
render.clear();
|
|
170
|
+
render.render(world, mirrorCamera);
|
|
171
|
+
uniforms.roomReflectionReady.value = 1;
|
|
172
|
+
} finally {
|
|
173
|
+
surface.visible = previousVisibility;
|
|
174
|
+
render.xr.enabled = previousXR;
|
|
175
|
+
render.shadowMap.autoUpdate = previousShadowUpdate;
|
|
176
|
+
render.autoClear = previousAutoClear;
|
|
177
|
+
render.setRenderTarget(previousTarget, previousFace, previousMip);
|
|
178
|
+
render.state.viewport(viewport);
|
|
179
|
+
reflectingRenderers.delete(render);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
target,
|
|
185
|
+
dispose() {
|
|
186
|
+
if (disposed) return;
|
|
187
|
+
disposed = true;
|
|
188
|
+
surface.onBeforeRender = () => {};
|
|
189
|
+
target.dispose();
|
|
190
|
+
uniforms.roomReflectionReady.value = 0;
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
}
|