@pioneer_zmc/dsh-workbench 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +98 -0
  3. package/README.zh.md +100 -0
  4. package/client.js +122 -0
  5. package/cordis.patch.yml +8 -0
  6. package/dist/check-building.js +1669 -0
  7. package/dist/check-room.js +980 -0
  8. package/dist/index.js +2873 -0
  9. package/package.json +60 -0
  10. package/skills/specdev-building/SKILL.md +85 -0
  11. package/skills/specdev-building/assets/layouts/court.json +53 -0
  12. package/skills/specdev-building/assets/layouts/row.json +48 -0
  13. package/skills/specdev-building/assets/layouts/single.json +27 -0
  14. package/skills/specdev-building/assets/layouts/tower-9.json +71 -0
  15. package/skills/specdev-building/assets/layouts/tower.json +59 -0
  16. package/skills/specdev-building/references/blueprint-contract.md +126 -0
  17. package/skills/specdev-building/references/diagnostics.md +101 -0
  18. package/skills/specdev-building/references/layouts.md +112 -0
  19. package/skills/specdev-business/SKILL.md +91 -0
  20. package/skills/specdev-business/assets/plan-template.md +66 -0
  21. package/skills/specdev-business/assets/spec-template.md +111 -0
  22. package/skills/specdev-business/assets/tasks-template.md +36 -0
  23. package/skills/specdev-business/references/change.md +117 -0
  24. package/skills/specdev-business/references/evidence-review.md +125 -0
  25. package/skills/specdev-business/references/implementation.md +67 -0
  26. package/skills/specdev-business/references/project-contract.md +211 -0
  27. package/skills/specdev-business/references/source-map.md +110 -0
  28. package/skills/specdev-business/references/specification.md +93 -0
  29. package/skills/specdev-business/references/workflow.md +126 -0
  30. package/skills/specdev-room/SKILL.md +77 -0
  31. package/skills/specdev-room/assets/intro-room.json +14 -0
  32. package/skills/specdev-room/references/diagnostics.md +21 -0
  33. package/skills/specdev-room/references/room-contract.md +59 -0
  34. package/vendor/archify-renderer/VERSION.md +52 -0
  35. package/vendor/archify-renderer/archify/LICENSE +22 -0
  36. package/vendor/archify-renderer/archify/THIRD_PARTY_NOTICES.md +56 -0
  37. package/vendor/archify-renderer/archify/assets/template.html +14787 -0
  38. package/vendor/archify-renderer/archify/bin/archify.mjs +1990 -0
  39. package/vendor/archify-renderer/archify/renderers/shared/brand-marks.mjs +563 -0
  40. package/vendor/archify-renderer/archify/renderers/shared/cli.mjs +218 -0
  41. package/vendor/archify-renderer/archify/renderers/shared/desktop-readability.mjs +26 -0
  42. package/vendor/archify-renderer/archify/renderers/shared/diagnostics.mjs +127 -0
  43. package/vendor/archify-renderer/archify/renderers/shared/engineering-profiles.mjs +157 -0
  44. package/vendor/archify-renderer/archify/renderers/shared/generated-brand-marks.mjs +2003 -0
  45. package/vendor/archify-renderer/archify/renderers/shared/generated-validators.mjs +13 -0
  46. package/vendor/archify-renderer/archify/renderers/shared/geometry.mjs +1423 -0
  47. package/vendor/archify-renderer/archify/renderers/shared/i18n.mjs +594 -0
  48. package/vendor/archify-renderer/archify/renderers/shared/legend.mjs +217 -0
  49. package/vendor/archify-renderer/archify/renderers/shared/output-path.mjs +321 -0
  50. package/vendor/archify-renderer/archify/renderers/shared/repository-evidence.mjs +235 -0
  51. package/vendor/archify-renderer/archify/renderers/shared/text-fit.mjs +49 -0
  52. package/vendor/archify-renderer/archify/renderers/shared/utils.mjs +232 -0
  53. package/vendor/archify-renderer/archify/renderers/shared/validator.mjs +86 -0
  54. package/vendor/archify-renderer/archify/renderers/workflow/render-workflow.mjs +35 -0
  55. package/vendor/archify-renderer/archify/renderers/workflow/workflow-compiler.mjs +4400 -0
  56. package/vendor/archify-renderer/archify/renderers/workflow/workflow-migration-geometry.mjs +144 -0
  57. package/vendor/archify-renderer/archify/schemas/common.schema.json +115 -0
  58. package/vendor/archify-renderer/archify/schemas/workflow.schema.json +428 -0
  59. package/vendor/archify-renderer/archify/scripts/check-render-output.mjs +835 -0
  60. package/vendor/archify-renderer/shims/process.mjs +18 -0
  61. package/vendor/archify-renderer/stubs/child_process.mjs +2 -0
  62. package/vendor/archify-renderer/stubs/crypto.mjs +3 -0
  63. package/vendor/archify-renderer/stubs/dns-promises.mjs +2 -0
  64. package/vendor/archify-renderer/stubs/fs.mjs +3 -0
  65. package/vendor/archify-renderer/stubs/http.mjs +3 -0
  66. package/vendor/archify-renderer/stubs/https.mjs +3 -0
  67. package/vendor/archify-renderer/stubs/net.mjs +3 -0
  68. package/vendor/archify-renderer/stubs/path.mjs +3 -0
  69. package/vendor/archify-renderer/stubs/url.mjs +3 -0
  70. package/web/assets/building-kit.js +1157 -0
  71. package/web/assets/building-look.js +185 -0
  72. package/web/assets/building-plan.js +495 -0
  73. package/web/assets/building-scene.js +529 -0
  74. package/web/assets/building-walk.js +248 -0
  75. package/web/assets/building.js +859 -0
  76. package/web/assets/business.js +124 -0
  77. package/web/assets/chart-link.js +165 -0
  78. package/web/assets/common.js +302 -0
  79. package/web/assets/details.js +197 -0
  80. package/web/assets/evidence.js +30 -0
  81. package/web/assets/furniture/SOURCES.md +24 -0
  82. package/web/assets/furniture/assets/ceramic-display-stand-01/asset.json +25 -0
  83. package/web/assets/furniture/assets/ceramic-display-stand-01/model.js +116 -0
  84. package/web/assets/furniture/assets/ceramic-display-stand-01/preview.webp +0 -0
  85. package/web/assets/furniture/assets/ceramic-floor-lamp-01/asset.json +61 -0
  86. package/web/assets/furniture/assets/ceramic-floor-lamp-01/model.js +56 -0
  87. package/web/assets/furniture/assets/ceramic-floor-lamp-01/preview.webp +0 -0
  88. package/web/assets/furniture/assets/ceramic-low-cabinet-01/asset.json +81 -0
  89. package/web/assets/furniture/assets/ceramic-low-cabinet-01/model.js +67 -0
  90. package/web/assets/furniture/assets/ceramic-low-cabinet-01/preview.webp +0 -0
  91. package/web/assets/furniture/assets/ceramic-vase-01/asset.json +21 -0
  92. package/web/assets/furniture/assets/ceramic-vase-01/model.js +119 -0
  93. package/web/assets/furniture/assets/ceramic-vase-01/preview.webp +0 -0
  94. package/web/assets/furniture/assets/fairy-bed-01/asset.json +61 -0
  95. package/web/assets/furniture/assets/fairy-bed-01/model.js +63 -0
  96. package/web/assets/furniture/assets/fairy-bed-01/preview.webp +0 -0
  97. package/web/assets/furniture/assets/fairy-book-stack-01/asset.json +21 -0
  98. package/web/assets/furniture/assets/fairy-book-stack-01/model.js +71 -0
  99. package/web/assets/furniture/assets/fairy-book-stack-01/preview.webp +0 -0
  100. package/web/assets/furniture/assets/fairy-bookshelf-01/asset.json +32 -0
  101. package/web/assets/furniture/assets/fairy-bookshelf-01/model.js +168 -0
  102. package/web/assets/furniture/assets/fairy-bookshelf-01/preview.webp +0 -0
  103. package/web/assets/furniture/assets/fairy-desk-01/asset.json +25 -0
  104. package/web/assets/furniture/assets/fairy-desk-01/model.js +132 -0
  105. package/web/assets/furniture/assets/fairy-desk-01/preview.webp +0 -0
  106. package/web/assets/furniture/assets/fairy-notice-board-01/asset.json +23 -0
  107. package/web/assets/furniture/assets/fairy-notice-board-01/model.js +125 -0
  108. package/web/assets/furniture/assets/fairy-notice-board-01/preview.webp +0 -0
  109. package/web/assets/furniture/assets/fairy-potted-plant-01/asset.json +21 -0
  110. package/web/assets/furniture/assets/fairy-potted-plant-01/model.js +128 -0
  111. package/web/assets/furniture/assets/fairy-potted-plant-01/preview.webp +0 -0
  112. package/web/assets/furniture/assets/fairy-reading-chair-01/asset.json +61 -0
  113. package/web/assets/furniture/assets/fairy-reading-chair-01/model.js +66 -0
  114. package/web/assets/furniture/assets/fairy-reading-chair-01/preview.webp +0 -0
  115. package/web/assets/furniture/assets/fairy-table-lamp-01/asset.json +21 -0
  116. package/web/assets/furniture/assets/fairy-table-lamp-01/model.js +182 -0
  117. package/web/assets/furniture/assets/fairy-table-lamp-01/preview.webp +0 -0
  118. package/web/assets/furniture/catalog.js +137 -0
  119. package/web/assets/furniture/index.json +110 -0
  120. package/web/assets/furniture/registry.js +72 -0
  121. package/web/assets/furniture-page.css +68 -0
  122. package/web/assets/furniture-page.js +302 -0
  123. package/web/assets/home.js +65 -0
  124. package/web/assets/node-detail.js +148 -0
  125. package/web/assets/panel-link.js +47 -0
  126. package/web/assets/read.js +746 -0
  127. package/web/assets/room-scene.js +386 -0
  128. package/web/assets/room.css +287 -0
  129. package/web/assets/room.js +437 -0
  130. package/web/assets/rooms/bindings.js +449 -0
  131. package/web/assets/rooms/ceramic-reflection.js +193 -0
  132. package/web/assets/rooms/ceramic-room.js +302 -0
  133. package/web/assets/rooms/door.js +35 -0
  134. package/web/assets/rooms/fairy-room.js +492 -0
  135. package/web/assets/rooms/furniture-assembly.js +113 -0
  136. package/web/assets/rooms/navigation.js +262 -0
  137. package/web/assets/rooms/placeholder-catalog.json +71 -0
  138. package/web/assets/rooms/placeholder-furniture.js +76 -0
  139. package/web/assets/rooms/placement.js +588 -0
  140. package/web/assets/rooms/reader.js +441 -0
  141. package/web/assets/rooms/reading-gate.js +157 -0
  142. package/web/assets/rooms/room-lighting.js +50 -0
  143. package/web/assets/rooms/templates.json +50 -0
  144. package/web/assets/rooms/walk-frame.js +64 -0
  145. package/web/assets/rooms/walk-session.js +138 -0
  146. package/web/assets/save-result.js +89 -0
  147. package/web/assets/screenshots/business.png +0 -0
  148. package/web/assets/screenshots/node-detail.png +0 -0
  149. package/web/assets/screenshots/project-home.png +0 -0
  150. package/web/assets/screenshots/workflow.png +0 -0
  151. package/web/assets/showcase.js +232 -0
  152. package/web/assets/style.css +836 -0
  153. package/web/assets/template.js +7 -0
  154. package/web/assets/theme.js +29 -0
  155. package/web/assets/three/OrbitControls.js +1972 -0
  156. package/web/assets/three/VERSION.md +12 -0
  157. package/web/assets/three/three.core.js +60586 -0
  158. package/web/assets/three/three.module.js +19719 -0
  159. package/web/assets/workspaces.css +21 -0
  160. package/web/assets/workspaces.js +81 -0
  161. package/web/building.html +131 -0
  162. package/web/business.html +43 -0
  163. package/web/furniture.html +47 -0
  164. package/web/index.html +57 -0
  165. package/web/read.html +107 -0
  166. package/web/room.html +146 -0
  167. package/web/showcase.html +50 -0
  168. package/web/workspaces.html +32 -0
@@ -0,0 +1,262 @@
1
+ // 独立房间 · 行走数据适配(P1c-1 室内版/P1c-2b 室外版):把已通过产品
2
+ // validatePlacement 的 template 与 assembly 翻译成原版建筑导航 building-walk.js
3
+ // 能消费的 model/presentation。本模块只做数据翻译,导航算法原样复用 createNavigator,
4
+ // 不复制、不改造,也不重新计算家具尺寸或承载关系;调用前置条件是产品校验成功,
5
+ // API 读取成功不能直接调用。
6
+ // 边界语义:zones 是摆放区、reservedVolumes 是禁放区,二者都不是人物唯一可走范围、
7
+ // 也不是碰撞障碍。室内版(buildRoomWalkData)整个室内空地都能走,房间边界由行走面
8
+ // 的支撑检查约束(人物保留约 0.26 米支撑半径,门洞处停在房内),不伪造墙厚或外部
9
+ // 碰撞盒,适用范围不随室外版收紧。室外版(buildRoomOutdoorWalkData)另加封闭墙壳、
10
+ // 真实门盒与外围隐形墙碰撞,人物可绕屋与经门进出。
11
+ // 人物尺寸必须与原导航一致(半径 0.26、躯干净高 1.9):模板 walkProfile 偏离时明确
12
+ // 报错,不静默换尺寸,也不改原导航去支持可变参数。
13
+
14
+ const NAVIGATOR_RADIUS = 0.26
15
+ const NAVIGATOR_EYE_CLEARANCE = 1.9
16
+ const EPS = 1e-9
17
+
18
+ /** 室内/室外两个入口共用的身份检查:装配结果必须与模板同源,错误消息保持 P1c-1 原文。 */
19
+ function assertRoomIdentity(template, assembly) {
20
+ if (assembly.templateId !== template.templateId) {
21
+ throw new Error(`房间行走适配失败:装配结果的 templateId 是「${assembly.templateId}」,与模板「${template.templateId}」不一致,不能跨房间误接导航数据。`)
22
+ }
23
+ if (assembly.styleId !== template.styleId) {
24
+ throw new Error(`房间行走适配失败:装配结果的 styleId 是「${assembly.styleId}」,与模板风格「${template.styleId}」不一致,导航数据必须来自同一模板的校验结果。`)
25
+ }
26
+ }
27
+
28
+ /** 家具 collider 转换(两个入口共用):id=instanceId,世界 solid 已含承载面高度(台灯 y0 即桌面高度)。 */
29
+ function toFurnitureColliders(assembly) {
30
+ return assembly.instances.map((instance) => ({
31
+ id: instance.instanceId,
32
+ x0: instance.solid.minX, x1: instance.solid.maxX,
33
+ y0: instance.solid.minY, y1: instance.solid.maxY,
34
+ z0: instance.solid.minZ, z1: instance.solid.maxZ,
35
+ }))
36
+ }
37
+
38
+ /**
39
+ * template+assembly → { model, presentation }(可直接交给 createNavigator/entryPoint)。
40
+ * 纯函数:不缓存上一次房间,不改写输入;输出范围对象全部重新构造,不与输入共用引用。
41
+ */
42
+ export function buildRoomWalkData(template, assembly) {
43
+ assertRoomIdentity(template, assembly)
44
+ const { radius, eyeClearance } = template.walkProfile ?? {}
45
+ if (Math.abs((radius ?? NaN) - NAVIGATOR_RADIUS) > EPS || Math.abs((eyeClearance ?? NaN) - NAVIGATOR_EYE_CLEARANCE) > EPS) {
46
+ throw new Error(`房间行走适配失败:模板 walkProfile(radius=${radius},eyeClearance=${eyeClearance})与现有导航支持的 radius=0.26/eyeClearance=1.9 不一致;不改导航支持可变人物尺寸,请修正模板。`)
47
+ }
48
+ const { minX, maxX, minY, maxY, minZ, maxZ } = template.interiorBounds
49
+ // 出生点 x/z 来自装配结果的 spawn(模板 entry.spawn 的同一份解析值),y 用室内地坪。
50
+ return {
51
+ model: {
52
+ spaces: [{ id: assembly.roomId, y: minY, height: maxY - minY, bounds: { x0: minX, x1: maxX, z0: minZ, z1: maxZ } }],
53
+ entry: { spaceId: assembly.roomId, x: assembly.spawn.x, y: minY, z: assembly.spawn.z },
54
+ },
55
+ presentation: {
56
+ surfaces: [{ kind: 'flat', y: minY, bounds: { x0: minX, x1: maxX, z0: minZ, z1: maxZ } }],
57
+ // 一一对应装配实例;室内输出的 collider 不带 kind,保持 P1c-1 契约不变。
58
+ colliders: toFurnitureColliders(assembly),
59
+ },
60
+ }
61
+ }
62
+
63
+ // ── 室外行走与隐形边界(P1c-2b)─────────────────────────────────────────────
64
+ // 本阶段已决定的场地尺寸(任务单定死,不交选型):隐形墙内侧 16×14 米,支撑面向外
65
+ // 多 0.5 米——把“没有地面支撑”与“隐形墙在挡人”区分开;后续可见地面可继续延伸,
66
+ // 不能用地板断口或围栏标记隐形墙。墙壳是通行用的简化结构碰撞,不宣称等于全部装饰
67
+ // 的逐顶点碰撞;窗户按墙体封闭,门洞空出、过梁从门高 2.55 起。
68
+
69
+ const ROOM_FLOOR_Y = 0
70
+ const ROOM_HEIGHT = 3.4
71
+ // 北门中心 (2.25,-3)、宽 1.4、高 2.55:门洞 X[1.55,2.95],与两套底座 meta.door 一致。
72
+ const DOOR_OPENING_X0 = 1.55
73
+ const DOOR_OPENING_X1 = 2.95
74
+ const DOOR_LINTEL_Y = 2.55
75
+ // 墙厚来自现有墙体(ceramic=0.18,fairy=0.26);未知风格没有登记墙厚,直接报错。
76
+ const OUTDOOR_WALL_THICKNESS = { ceramic: 0.18, fairy: 0.26 }
77
+ const OUTDOOR_SITE = {
78
+ floorY: ROOM_FLOOR_Y,
79
+ walkBounds: { minX: -8, maxX: 8, minZ: -7, maxZ: 7 },
80
+ supportBounds: { minX: -8.5, maxX: 8.5, minZ: -7.5, maxZ: 7.5 },
81
+ spawn: { x: 2.25, y: 0, z: -5.2 },
82
+ }
83
+ const OUTDOOR_INNER_X0 = -4
84
+ const OUTDOOR_INNER_X1 = 4
85
+ const OUTDOOR_INNER_Z0 = -3
86
+ const OUTDOOR_INNER_Z1 = 3
87
+
88
+ /**
89
+ * 风格墙厚查表(只认登记的自有键):`constructor`、`toString`、`__proto__` 等原型键
90
+ * 不是已登记风格,必须落进“尚未登记”报错,不能取到继承属性后参与运算。
91
+ */
92
+ function outdoorWallThickness(styleId) {
93
+ if (!Object.hasOwn(OUTDOOR_WALL_THICKNESS, styleId)) {
94
+ throw new Error(`房间室外行走适配失败:模板风格「${String(styleId)}」尚未登记室外墙厚与场地数据(当前仅 ceramic/fairy),不能套用本轮隐形墙参数,请为新底座单独登记后再开放室外行走。`)
95
+ }
96
+ const thickness = OUTDOOR_WALL_THICKNESS[styleId]
97
+ if (typeof thickness !== 'number' || !Number.isFinite(thickness) || thickness <= 0) {
98
+ throw new Error(`房间室外行走适配失败:风格「${String(styleId)}」登记的墙厚 ${String(thickness)} 不是有限正数,登记表本身有误,拒绝生成导航数据。`)
99
+ }
100
+ return thickness
101
+ }
102
+
103
+ /**
104
+ * 固定门洞前提校验:墙壳把北墙 X[1.55,2.95] 当门洞、Y2.55 当过梁底,因此模板的
105
+ * 门位置、宽高与朝向必须逐项等于本轮固定值。上游 placement 只保证门中心为有限数、
106
+ * 宽高为正数,不保证等于固定值——该校验不能由上游替代。
107
+ */
108
+ function assertOutdoorDoorGeometry(template) {
109
+ const entry = template?.entry
110
+ if (!entry || typeof entry !== 'object') {
111
+ throw new Error('房间室外行走适配失败:模板缺少 entry 声明,室外场地无从核对门洞(要求 north 墙、+Z 向内、向外开、中心 (2.25,-3)、宽 1.4、高 2.55),不能默认套用北门碰撞。')
112
+ }
113
+ const door = entry.door
114
+ if (!door || typeof door !== 'object') {
115
+ throw new Error('房间室外行走适配失败:模板 entry 缺少 door 声明(要求 north 墙、+Z 向内、向外开、中心 (2.25,-3)、宽 1.4、高 2.55),不能默认套用北门碰撞。')
116
+ }
117
+ const problems = []
118
+ const fixed = [
119
+ ['entry.wall', entry.wall, 'north'],
120
+ ['entry.inward', entry.inward, '+Z'],
121
+ ['entry.door.opens', door.opens, 'outward'],
122
+ ]
123
+ for (const [name, actual, expected] of fixed) {
124
+ if (actual !== expected) problems.push(`${name}=${JSON.stringify(actual)}(要求 ${JSON.stringify(expected)})`)
125
+ }
126
+ const numbers = [
127
+ ['entry.door.centerX', door.centerX, 2.25],
128
+ ['entry.door.centerZ', door.centerZ, -3],
129
+ ['entry.door.width', door.width, 1.4],
130
+ ['entry.door.height', door.height, 2.55],
131
+ ]
132
+ for (const [name, actual, expected] of numbers) {
133
+ if (typeof actual !== 'number' || !Number.isFinite(actual) || Math.abs(actual - expected) > EPS) {
134
+ problems.push(`${name}=${String(actual)}(要求有限数 ${expected})`)
135
+ }
136
+ }
137
+ if (problems.length > 0) {
138
+ throw new Error(`房间室外行走适配失败:${problems.join(';')}。本轮墙壳把北墙 X[1.55,2.95] 当门洞、Y2.55 当过梁底,门位置、宽高或朝向变更后必须重新登记场地数据,不能套用旧碰撞。`)
139
+ }
140
+ }
141
+
142
+ /** 室外场地的房间前提:室内范围与门洞参数只对当前两套未做根变换的房间成立。 */
143
+ function assertOutdoorRoomGeometry(template) {
144
+ const thickness = outdoorWallThickness(template.styleId)
145
+ const b = template.interiorBounds
146
+ const expected = { minX: OUTDOOR_INNER_X0, maxX: OUTDOOR_INNER_X1, minY: ROOM_FLOOR_Y, maxY: ROOM_HEIGHT, minZ: OUTDOOR_INNER_Z0, maxZ: OUTDOOR_INNER_Z1 }
147
+ for (const [key, value] of Object.entries(expected)) {
148
+ if (!Number.isFinite(b[key]) || Math.abs(b[key] - value) > EPS) {
149
+ throw new Error(`房间室外行走适配失败:模板 interiorBounds 的 ${key}=${b[key]} 与室外场地前提(X[-4,4]·Z[-3,3]·Y[0,3.4]、北门中心 (2.25,-3) 宽 1.4)不符;本轮墙壳与门洞参数只对当前两套房间成立,请先核对底座实际几何。`)
150
+ }
151
+ }
152
+ assertOutdoorDoorGeometry(template)
153
+ return thickness
154
+ }
155
+
156
+ /**
157
+ * 门盒校验与碰撞映射。doorBounds 必须是真实 room.door.getBounds() 的当前值
158
+ * (开门、关门时各取一次):六个字段有限数且各轴 min<max;不收 Three.js 对象或
159
+ * 门句柄——它们没有这六个数字字段,会在这里被明确拒绝。
160
+ */
161
+ function toDoorCollider(options) {
162
+ const source = options?.doorBounds
163
+ if (!source || typeof source !== 'object') {
164
+ throw new Error(`房间室外行走适配失败:缺少 doorBounds——门碰撞必须来自真实 room.door.getBounds() 的当前值(开门与关门各取一次),不收门句柄或 Three.js 对象。`)
165
+ }
166
+ const bounds = {}
167
+ for (const axis of ['X', 'Y', 'Z']) {
168
+ for (const edge of ['min', 'max']) {
169
+ const key = `${edge}${axis}`
170
+ const value = source[key]
171
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
172
+ throw new Error(`房间室外行走适配失败:doorBounds 的 ${key} 必须是有限数,收到「${String(value)}」(${typeof value});请传 room.door.getBounds() 返回的普通对象。`)
173
+ }
174
+ bounds[key] = value
175
+ }
176
+ if (!(bounds[`min${axis}`] < bounds[`max${axis}`])) {
177
+ throw new Error(`房间室外行走适配失败:doorBounds 的 ${axis} 轴 min(${bounds[`min${axis}`]}) 必须小于 max(${bounds[`max${axis}`]}),倒置或零厚度的门盒不是有效碰撞。`)
178
+ }
179
+ }
180
+ // 门盒直接映射为碰撞:不重新猜门板宽度、铰链位置或角度;打开的门板仍是实体。
181
+ return { id: 'structure:entry-door', kind: 'door', x0: bounds.minX, x1: bounds.maxX, y0: bounds.minY, y1: bounds.maxY, z0: bounds.minZ, z1: bounds.maxZ }
182
+ }
183
+
184
+ /** 封闭墙壳(转角重叠封缝):西/东/南/北左/北右/北门过梁,厚 t 来自风格。 */
185
+ function toWallColliders(thickness) {
186
+ const t = thickness
187
+ const wall = (id, x0, x1, z0, z1, y0 = ROOM_FLOOR_Y, y1 = ROOM_HEIGHT) =>
188
+ ({ id: `structure:${id}`, kind: 'wall', x0, x1, y0, y1, z0, z1 })
189
+ return [
190
+ wall('wall-west', OUTDOOR_INNER_X0 - t, OUTDOOR_INNER_X0, OUTDOOR_INNER_Z0 - t, OUTDOOR_INNER_Z1 + t),
191
+ wall('wall-east', OUTDOOR_INNER_X1, OUTDOOR_INNER_X1 + t, OUTDOOR_INNER_Z0 - t, OUTDOOR_INNER_Z1 + t),
192
+ wall('wall-south', OUTDOOR_INNER_X0 - t, OUTDOOR_INNER_X1 + t, OUTDOOR_INNER_Z1, OUTDOOR_INNER_Z1 + t),
193
+ wall('wall-north-left', OUTDOOR_INNER_X0 - t, DOOR_OPENING_X0, OUTDOOR_INNER_Z0 - t, OUTDOOR_INNER_Z0),
194
+ wall('wall-north-right', DOOR_OPENING_X1, OUTDOOR_INNER_X1 + t, OUTDOOR_INNER_Z0 - t, OUTDOOR_INNER_Z0),
195
+ wall('wall-lintel-north', DOOR_OPENING_X0, DOOR_OPENING_X1, OUTDOOR_INNER_Z0 - t, OUTDOOR_INNER_Z0, DOOR_LINTEL_Y, ROOM_HEIGHT),
196
+ ]
197
+ }
198
+
199
+ /** 外围隐形墙四盒:厚 0.25、Y[0,3.4],内侧即 walkBounds;只是碰撞数据,不是可见物。 */
200
+ function toBoundaryColliders() {
201
+ const { minX, maxX, minZ, maxZ } = OUTDOOR_SITE.walkBounds
202
+ const g = 0.25
203
+ const box = (id, x0, x1, z0, z1) =>
204
+ ({ id: `structure:${id}`, kind: 'boundary', x0, x1, y0: ROOM_FLOOR_Y, y1: ROOM_HEIGHT, z0, z1 })
205
+ return [
206
+ box('boundary-west', minX - g, minX, minZ - g, maxZ + g),
207
+ box('boundary-east', maxX, maxX + g, minZ - g, maxZ + g),
208
+ box('boundary-north', minX - g, maxX + g, minZ - g, minZ),
209
+ box('boundary-south', minX - g, maxX + g, maxZ, maxZ + g),
210
+ ]
211
+ }
212
+
213
+ /**
214
+ * template+assembly+{ doorBounds } → { model, presentation, site }(P1c-2b 室外版)。
215
+ * model/presentation 直接交给 createNavigator/entryPoint:门外出生、绕屋行走、
216
+ * 开门双向进出、关门挡人、隐形墙限距。纯数据翻译,不建 Mesh/事件/动画循环,
217
+ * 不调用 setOpen——门切态后由调用方取新的 getBounds 重建导航并自行保留人物位置。
218
+ */
219
+ export function buildRoomOutdoorWalkData(template, assembly, options) {
220
+ assertRoomIdentity(template, assembly)
221
+ // walkProfile 用严格有限数检查(缺失/NaN/Infinity/偏离都拒绝),不依赖 NaN 比较。
222
+ const { radius, eyeClearance } = template.walkProfile ?? {}
223
+ if (!Number.isFinite(radius) || !Number.isFinite(eyeClearance)
224
+ || Math.abs(radius - NAVIGATOR_RADIUS) > EPS || Math.abs(eyeClearance - NAVIGATOR_EYE_CLEARANCE) > EPS) {
225
+ throw new Error(`房间室外行走适配失败:模板 walkProfile(radius=${radius},eyeClearance=${eyeClearance})必须是有限数且等于现有导航的 0.26/1.9;不改导航支持可变人物尺寸,请修正模板。`)
226
+ }
227
+ const wallThickness = assertOutdoorRoomGeometry(template)
228
+ const doorCollider = toDoorCollider(options)
229
+
230
+ const { minX: wx0, maxX: wx1, minZ: wz0, maxZ: wz1 } = OUTDOOR_SITE.walkBounds
231
+ const { minX: sx0, maxX: sx1, minZ: sz0, maxZ: sz1 } = OUTDOOR_SITE.supportBounds
232
+ const outdoorSpaceId = `outdoor:${assembly.roomId}`
233
+ // spaces 室内在前:locate 按顺序优先返回室内,室外项是有意覆盖室内的回退范围,
234
+ // 不是第二间业务房间。entry 指室外项=运行时门外开局;assembly.spawn(室内摆放
235
+ // 校验的出生点)原样保留,不受本函数影响。
236
+ return {
237
+ model: {
238
+ spaces: [
239
+ { id: assembly.roomId, y: ROOM_FLOOR_Y, height: ROOM_HEIGHT, bounds: { x0: OUTDOOR_INNER_X0, x1: OUTDOOR_INNER_X1, z0: OUTDOOR_INNER_Z0, z1: OUTDOOR_INNER_Z1 } },
240
+ { id: outdoorSpaceId, y: ROOM_FLOOR_Y, height: ROOM_HEIGHT, bounds: { x0: wx0, x1: wx1, z0: wz0, z1: wz1 } },
241
+ ],
242
+ entry: { spaceId: outdoorSpaceId, x: OUTDOOR_SITE.spawn.x, y: ROOM_FLOOR_Y, z: OUTDOOR_SITE.spawn.z },
243
+ },
244
+ presentation: {
245
+ // 一张 flat 行走面盖住支撑面(含室内投影):室内外地坪同高,没有台阶;室内
246
+ // 外的分界只由墙壳碰撞表达,不由行走面断口表达。
247
+ surfaces: [{ kind: 'flat', y: ROOM_FLOOR_Y, bounds: { x0: sx0, x1: sx1, z0: sz0, z1: sz1 } }],
248
+ colliders: [
249
+ ...toFurnitureColliders(assembly).map((collider) => ({ ...collider, kind: 'furniture' })),
250
+ ...toWallColliders(wallThickness),
251
+ doorCollider,
252
+ ...toBoundaryColliders(),
253
+ ],
254
+ },
255
+ site: {
256
+ floorY: OUTDOOR_SITE.floorY,
257
+ walkBounds: { minX: wx0, maxX: wx1, minZ: wz0, maxZ: wz1 },
258
+ supportBounds: { minX: sx0, maxX: sx1, minZ: sz0, maxZ: sz1 },
259
+ spawn: { x: OUTDOOR_SITE.spawn.x, y: ROOM_FLOOR_Y, z: OUTDOOR_SITE.spawn.z },
260
+ },
261
+ }
262
+ }
@@ -0,0 +1,71 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "assets": [
4
+ {
5
+ "assetId": "placeholder-desk",
6
+ "name": "占位书桌",
7
+ "category": "desk",
8
+ "description": "按真实尺寸 1.6 × 0.8 × 0.75 米生成的占位盒,程序生成、非正式美术;正面为本地 +Z(前挡板颜色不同)。E2b 起在桌顶登记承载面 desktop,只接受台灯类物品。",
9
+ "styleIds": ["ceramic", "fairy"],
10
+ "modelRef": "placeholder-box-desk",
11
+ "previewRef": null,
12
+ "placeholder": true,
13
+ "bounds": { "minX": -0.8, "maxX": 0.8, "minY": 0, "maxY": 0.75, "minZ": -0.4, "maxZ": 0.4 },
14
+ "placement": { "allowedKinds": ["floor"], "allowedYawDeg": [0, 90, 180, 270] },
15
+ "clearanceBoxes": [
16
+ { "name": "桌前操作留空", "bounds": { "minX": -0.8, "maxX": 0.8, "minY": 0, "maxY": 1.9, "minZ": 0.4, "maxZ": 1.3 } }
17
+ ],
18
+ "approachPoint": { "x": 0, "z": 1.0 },
19
+ "surfaces": [
20
+ {
21
+ "surfaceId": "desktop",
22
+ "name": "桌面可摆放区",
23
+ "rect": { "minX": -0.7, "maxX": 0.7, "minZ": -0.3, "maxZ": 0.3 },
24
+ "y": 0.75,
25
+ "allowedCategories": ["lamp"]
26
+ }
27
+ ],
28
+ "capabilities": [],
29
+ "recommendations": { "note": "软建议:靠墙或靠展示通路南侧摆放,桌前朝向通路;不参与硬性校验。" },
30
+ "source": { "origin": "自制", "license": "本项目占位资产", "note": "程序生成占位盒,正式美术两步验证通过后另做。" }
31
+ },
32
+ {
33
+ "assetId": "placeholder-shelf",
34
+ "name": "占位书架",
35
+ "category": "shelf",
36
+ "description": "按真实尺寸 1.2 × 0.5 × 1.8 米生成的占位盒,程序生成、非正式美术;正面为本地 +Z(前挡板颜色不同),用于 E2a 回归;两件实例共用本资产。",
37
+ "styleIds": ["ceramic", "fairy"],
38
+ "modelRef": "placeholder-box-shelf",
39
+ "previewRef": null,
40
+ "placeholder": true,
41
+ "bounds": { "minX": -0.6, "maxX": 0.6, "minY": 0, "maxY": 1.8, "minZ": -0.25, "maxZ": 0.25 },
42
+ "placement": { "allowedKinds": ["floor"], "allowedYawDeg": [0, 90, 180, 270] },
43
+ "clearanceBoxes": [
44
+ { "name": "书架前阅读取书区", "bounds": { "minX": -0.65, "maxX": 0.65, "minY": 0, "maxY": 1.9, "minZ": 0.25, "maxZ": 1.3 } }
45
+ ],
46
+ "approachPoint": { "x": 0, "z": 0.9 },
47
+ "surfaces": [],
48
+ "capabilities": ["open-document-collection"],
49
+ "recommendations": { "note": "软建议:沿北墙排布、正面朝南;不参与硬性校验。" },
50
+ "source": { "origin": "自制", "license": "本项目占位资产", "note": "程序生成占位盒,正式美术两步验证通过后另做。" }
51
+ },
52
+ {
53
+ "assetId": "placeholder-table-lamp",
54
+ "name": "占位台灯",
55
+ "category": "lamp",
56
+ "description": "按真实尺寸 0.24 × 0.30 × 0.42 米生成的占位台灯,程序生成、非正式美术;只能放在承载面上(不提供 floor 放置),没有独立地面站位,操作站位沿用父家具;正面为本地 +Z(标记板颜色不同)。",
57
+ "styleIds": ["ceramic", "fairy"],
58
+ "modelRef": "placeholder-box-lamp",
59
+ "previewRef": null,
60
+ "placeholder": true,
61
+ "bounds": { "minX": -0.12, "maxX": 0.12, "minY": 0, "maxY": 0.42, "minZ": -0.15, "maxZ": 0.15 },
62
+ "placement": { "allowedKinds": ["surface"], "allowedYawDeg": [0, 90, 180, 270] },
63
+ "clearanceBoxes": [],
64
+ "approachPoint": null,
65
+ "surfaces": [],
66
+ "capabilities": [],
67
+ "recommendations": { "note": "软建议:靠桌子两侧摆放,不挡桌前操作区;不参与硬性校验。" },
68
+ "source": { "origin": "自制", "license": "本项目占位资产", "note": "程序生成占位模型,正式美术两步验证通过后另做。" }
69
+ }
70
+ ]
71
+ }
@@ -0,0 +1,76 @@
1
+ // 占位家具模型工厂:程序生成、非正式美术(placeholder: true)。产品模块;P1a 自 E2b 实验
2
+ // 迁入(来源提交 d80bea1),只承诺目录里已登记的占位资产尺寸,不当作任意尺寸模型生成器。
3
+ // 生成约定:合并几何严格落在声明的本地包围盒 X[-w/2,w/2] × Y[0,h] × Z[-d/2,d/2] 内——
4
+ // 本体深 d-0.03 且回缩 0.015,正面标记板厚 0.03 填满最前段,可见模型不超出校验盒;
5
+ // 正面(本地 +Z)用深色标记板区分。tests/room-placement.test.mjs 在 Node 中用真实 Three.js 复核这一约定。
6
+ // 台灯工厂:底座 + 灯柱 + 灯罩盒 + 正面标记,原点同为底面中心(Y 从 0 起),
7
+ // 桌面上摆放时 group.position.y 直接用装配结果的承载面高度。three 用页面同一份随包副本(importmap 裸名)。
8
+ import * as THREE from 'three';
9
+
10
+ export function createPlaceholderFurniture({ width, height, depth, bodyColor = '#c99f6b', frontColor = '#6f4c2e' } = {}) {
11
+ const panel = 0.03;
12
+ if (![width, height, depth].every((value) => Number.isFinite(value) && value > panel)) {
13
+ throw new Error(`占位家具尺寸必须为正且大于面板厚度 ${panel} 米`);
14
+ }
15
+ const group = new THREE.Group();
16
+ const body = new THREE.Mesh(
17
+ new THREE.BoxGeometry(width, height, depth - panel),
18
+ new THREE.MeshStandardMaterial({ color: bodyColor, roughness: 0.82, metalness: 0.02 }),
19
+ );
20
+ body.position.set(0, height / 2, -panel / 2);
21
+ body.castShadow = true;
22
+ body.receiveShadow = true;
23
+ const front = new THREE.Mesh(
24
+ new THREE.BoxGeometry(width, height * 0.42, panel),
25
+ new THREE.MeshStandardMaterial({ color: frontColor, roughness: 0.7 }),
26
+ );
27
+ front.position.set(0, height * 0.58, depth / 2 - panel / 2);
28
+ front.castShadow = true;
29
+ const edges = new THREE.LineSegments(
30
+ new THREE.EdgesGeometry(new THREE.BoxGeometry(width, height, depth)),
31
+ new THREE.LineBasicMaterial({ color: '#3a3f35' }),
32
+ );
33
+ edges.position.set(0, height / 2, 0);
34
+ group.add(body, front, edges);
35
+ return group;
36
+ }
37
+
38
+ /** 占位台灯:底座圆柱 + 灯柱 + 灯罩盒 + 正面标记板(本地 +Z),全部落在声明盒内、不外凸。 */
39
+ export function createPlaceholderLamp({ width, height, depth, bodyColor = '#e8c98f', frontColor = '#8a5a2e' } = {}) {
40
+ if (![width, height, depth].every((value) => Number.isFinite(value) && value > 0.08)) {
41
+ throw new Error('占位台灯尺寸必须为正且大于最小构件 0.08 米');
42
+ }
43
+ const group = new THREE.Group();
44
+ const baseR = Math.min(width, depth) * 0.42;
45
+ const base = new THREE.Mesh(
46
+ new THREE.CylinderGeometry(baseR * 0.88, baseR, 0.04, 20),
47
+ new THREE.MeshStandardMaterial({ color: frontColor, roughness: 0.62 }),
48
+ );
49
+ base.position.set(0, 0.02, 0); // Y[0, 0.04]
50
+ base.castShadow = true;
51
+ const poleHeight = height - 0.34;
52
+ const pole = new THREE.Mesh(
53
+ new THREE.CylinderGeometry(0.016, 0.016, poleHeight, 12),
54
+ new THREE.MeshStandardMaterial({ color: '#7c7f76', roughness: 0.5, metalness: 0.3 }),
55
+ );
56
+ pole.position.set(0, 0.04 + poleHeight / 2, 0); // Y[0.04, height-0.30]
57
+ pole.castShadow = true;
58
+ const shade = new THREE.Mesh(
59
+ new THREE.BoxGeometry(width, 0.3, depth),
60
+ new THREE.MeshStandardMaterial({ color: bodyColor, roughness: 0.85 }),
61
+ );
62
+ shade.position.set(0, height - 0.15, 0); // Y[height-0.3, height]
63
+ shade.castShadow = true;
64
+ const mark = new THREE.Mesh(
65
+ new THREE.BoxGeometry(width * 0.5, 0.1, 0.012),
66
+ new THREE.MeshStandardMaterial({ color: frontColor, roughness: 0.7 }),
67
+ );
68
+ mark.position.set(0, height - 0.15, depth / 2 - 0.006); // 嵌入灯罩正面,最外端恰在 depth/2
69
+ const edges = new THREE.LineSegments(
70
+ new THREE.EdgesGeometry(new THREE.BoxGeometry(width, height, depth)),
71
+ new THREE.LineBasicMaterial({ color: '#3a3f35' }),
72
+ );
73
+ edges.position.set(0, height / 2, 0);
74
+ group.add(base, pole, shade, mark, edges);
75
+ return group;
76
+ }