@pascal-app/viewer 1.0.5 → 1.0.7

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 (199) hide show
  1. package/dist/components/viewer/daylight-atmosphere.d.ts +9 -0
  2. package/dist/components/viewer/daylight-atmosphere.d.ts.map +1 -0
  3. package/dist/components/viewer/daylight-atmosphere.js +315 -0
  4. package/dist/components/viewer/drone-controller.d.ts +15 -0
  5. package/dist/components/viewer/drone-controller.d.ts.map +1 -0
  6. package/dist/components/viewer/drone-controller.js +194 -0
  7. package/dist/components/viewer/glb-interactive.d.ts +4 -18
  8. package/dist/components/viewer/glb-interactive.d.ts.map +1 -1
  9. package/dist/components/viewer/glb-interactive.js +32 -268
  10. package/dist/components/viewer/glb-scene-motion.d.ts +8 -0
  11. package/dist/components/viewer/glb-scene-motion.d.ts.map +1 -0
  12. package/dist/components/viewer/glb-scene-motion.js +57 -0
  13. package/dist/components/viewer/glb-scene.d.ts +3 -1
  14. package/dist/components/viewer/glb-scene.d.ts.map +1 -1
  15. package/dist/components/viewer/glb-scene.js +289 -49
  16. package/dist/components/viewer/glb-selection-callout.d.ts +17 -0
  17. package/dist/components/viewer/glb-selection-callout.d.ts.map +1 -0
  18. package/dist/components/viewer/glb-selection-callout.js +54 -0
  19. package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -1
  20. package/dist/components/viewer/glb-walkthrough-controller.js +6 -3
  21. package/dist/components/viewer/index.d.ts +5 -0
  22. package/dist/components/viewer/index.d.ts.map +1 -1
  23. package/dist/components/viewer/index.js +30 -6
  24. package/dist/components/viewer/lights.d.ts.map +1 -1
  25. package/dist/components/viewer/lights.js +192 -5
  26. package/dist/components/viewer/post-processing.d.ts.map +1 -1
  27. package/dist/components/viewer/post-processing.js +150 -94
  28. package/dist/components/viewer/scene-environment.js +4 -5
  29. package/dist/components/viewer/selection-callout.d.ts +51 -0
  30. package/dist/components/viewer/selection-callout.d.ts.map +1 -0
  31. package/dist/components/viewer/selection-callout.js +197 -0
  32. package/dist/components/viewer/use-daylight-rooms.d.ts +13 -0
  33. package/dist/components/viewer/use-daylight-rooms.d.ts.map +1 -0
  34. package/dist/components/viewer/use-daylight-rooms.js +236 -0
  35. package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
  36. package/dist/components/viewer/viewer-camera.js +12 -1
  37. package/dist/index.d.ts +21 -7
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +23 -7
  40. package/dist/lib/daylight/ground.d.ts +21 -0
  41. package/dist/lib/daylight/ground.d.ts.map +1 -0
  42. package/dist/lib/daylight/ground.js +53 -0
  43. package/dist/lib/daylight/location.d.ts +32 -0
  44. package/dist/lib/daylight/location.d.ts.map +1 -0
  45. package/dist/lib/daylight/location.js +74 -0
  46. package/dist/lib/daylight/look.d.ts +33 -0
  47. package/dist/lib/daylight/look.d.ts.map +1 -0
  48. package/dist/lib/daylight/look.js +13 -0
  49. package/dist/lib/daylight/material-hooks.d.ts +2 -0
  50. package/dist/lib/daylight/material-hooks.d.ts.map +1 -0
  51. package/dist/lib/daylight/material-hooks.js +150 -0
  52. package/dist/lib/daylight/room-context.d.ts +12 -0
  53. package/dist/lib/daylight/room-context.d.ts.map +1 -0
  54. package/dist/lib/daylight/room-context.js +53 -0
  55. package/dist/lib/daylight/room-scene.d.ts +54 -0
  56. package/dist/lib/daylight/room-scene.d.ts.map +1 -0
  57. package/dist/lib/daylight/room-scene.js +356 -0
  58. package/dist/lib/daylight/rooms.d.ts +262 -0
  59. package/dist/lib/daylight/rooms.d.ts.map +1 -0
  60. package/dist/lib/daylight/rooms.js +1223 -0
  61. package/dist/lib/daylight/runtime.d.ts +33 -0
  62. package/dist/lib/daylight/runtime.d.ts.map +1 -0
  63. package/dist/lib/daylight/runtime.js +42 -0
  64. package/dist/lib/daylight/sky.d.ts +26 -0
  65. package/dist/lib/daylight/sky.d.ts.map +1 -0
  66. package/dist/lib/daylight/sky.js +217 -0
  67. package/dist/lib/daylight/solar.d.ts +46 -0
  68. package/dist/lib/daylight/solar.d.ts.map +1 -0
  69. package/dist/lib/daylight/solar.js +156 -0
  70. package/dist/lib/device-tier.d.ts +29 -0
  71. package/dist/lib/device-tier.d.ts.map +1 -0
  72. package/dist/lib/device-tier.js +71 -0
  73. package/dist/lib/dispose-object3d.d.ts.map +1 -1
  74. package/dist/lib/dispose-object3d.js +4 -2
  75. package/dist/lib/drone-flight.d.ts +44 -0
  76. package/dist/lib/drone-flight.d.ts.map +1 -0
  77. package/dist/lib/drone-flight.js +121 -0
  78. package/dist/lib/editor-highlight.d.ts +16 -0
  79. package/dist/lib/editor-highlight.d.ts.map +1 -0
  80. package/dist/lib/editor-highlight.js +33 -0
  81. package/dist/lib/foundation-materials.d.ts.map +1 -1
  82. package/dist/lib/foundation-materials.js +2 -4
  83. package/dist/lib/glb-merge.d.ts +23 -0
  84. package/dist/lib/glb-merge.d.ts.map +1 -0
  85. package/dist/lib/glb-merge.js +184 -0
  86. package/dist/lib/hardware-geometry.d.ts +21 -0
  87. package/dist/lib/hardware-geometry.d.ts.map +1 -0
  88. package/dist/lib/hardware-geometry.js +283 -0
  89. package/dist/lib/layer-pass.d.ts +2 -0
  90. package/dist/lib/layer-pass.d.ts.map +1 -1
  91. package/dist/lib/layer-pass.js +20 -0
  92. package/dist/lib/materials.d.ts.map +1 -1
  93. package/dist/lib/materials.js +50 -23
  94. package/dist/lib/merged-outline-node.d.ts.map +1 -1
  95. package/dist/lib/merged-outline-node.js +7 -1
  96. package/dist/lib/occlusion-culler.d.ts +34 -0
  97. package/dist/lib/occlusion-culler.d.ts.map +1 -0
  98. package/dist/lib/occlusion-culler.js +698 -0
  99. package/dist/lib/post-processing-resources.d.ts +4 -0
  100. package/dist/lib/post-processing-resources.d.ts.map +1 -1
  101. package/dist/lib/post-processing-resources.js +5 -0
  102. package/dist/lib/readback-rows.d.ts +14 -0
  103. package/dist/lib/readback-rows.d.ts.map +1 -0
  104. package/dist/lib/readback-rows.js +31 -0
  105. package/dist/lib/reveal-pose.d.ts.map +1 -1
  106. package/dist/lib/reveal-pose.js +3 -0
  107. package/dist/lib/scene-matrices.d.ts +12 -0
  108. package/dist/lib/scene-matrices.d.ts.map +1 -0
  109. package/dist/lib/scene-matrices.js +287 -0
  110. package/dist/lib/scene-pass-mrt.d.ts +5 -2
  111. package/dist/lib/scene-pass-mrt.d.ts.map +1 -1
  112. package/dist/lib/scene-pass-mrt.js +7 -2
  113. package/dist/lib/scene-themes.d.ts +0 -2
  114. package/dist/lib/scene-themes.d.ts.map +1 -1
  115. package/dist/lib/scene-themes.js +0 -2
  116. package/dist/lib/scene-visibility.d.ts +2 -0
  117. package/dist/lib/scene-visibility.d.ts.map +1 -1
  118. package/dist/lib/scene-visibility.js +12 -3
  119. package/dist/lib/site-surfaces.d.ts +1 -8
  120. package/dist/lib/site-surfaces.d.ts.map +1 -1
  121. package/dist/lib/site-surfaces.js +0 -19
  122. package/dist/lib/snapshot-pipeline.d.ts.map +1 -1
  123. package/dist/lib/snapshot-pipeline.js +24 -40
  124. package/dist/lib/texture-reference.d.ts.map +1 -1
  125. package/dist/lib/texture-reference.js +10 -8
  126. package/dist/lib/underground-ambiance.d.ts +0 -2
  127. package/dist/lib/underground-ambiance.d.ts.map +1 -1
  128. package/dist/lib/underground-ambiance.js +0 -2
  129. package/dist/store/use-item-light-pool.d.ts +3 -6
  130. package/dist/store/use-item-light-pool.d.ts.map +1 -1
  131. package/dist/store/use-item-light-pool.js +12 -0
  132. package/dist/store/use-viewer.d.ts +72 -15
  133. package/dist/store/use-viewer.d.ts.map +1 -1
  134. package/dist/store/use-viewer.js +103 -18
  135. package/dist/systems/ceiling/ceiling-system.d.ts +5 -0
  136. package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
  137. package/dist/systems/ceiling/ceiling-system.js +11 -1
  138. package/dist/systems/door/door-system.d.ts.map +1 -1
  139. package/dist/systems/door/door-system.js +501 -145
  140. package/dist/systems/door/leaf-profile.d.ts +11 -0
  141. package/dist/systems/door/leaf-profile.d.ts.map +1 -0
  142. package/dist/systems/door/leaf-profile.js +140 -0
  143. package/dist/systems/interactive/callout-actions.d.ts +72 -0
  144. package/dist/systems/interactive/callout-actions.d.ts.map +1 -0
  145. package/dist/systems/interactive/callout-actions.js +212 -0
  146. package/dist/systems/interactive/interactive-system.d.ts +5 -1
  147. package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
  148. package/dist/systems/interactive/interactive-system.js +44 -121
  149. package/dist/systems/interactive/opening-settled.d.ts +16 -0
  150. package/dist/systems/interactive/opening-settled.d.ts.map +1 -0
  151. package/dist/systems/interactive/opening-settled.js +53 -0
  152. package/dist/systems/item-light/item-light-system.d.ts.map +1 -1
  153. package/dist/systems/item-light/item-light-system.js +22 -214
  154. package/dist/systems/item-light/pooled-lights.d.ts +70 -0
  155. package/dist/systems/item-light/pooled-lights.d.ts.map +1 -0
  156. package/dist/systems/item-light/pooled-lights.js +449 -0
  157. package/dist/systems/level/level-system.d.ts.map +1 -1
  158. package/dist/systems/level/level-system.js +19 -1
  159. package/dist/systems/roof/roof-material-slots.d.ts +14 -0
  160. package/dist/systems/roof/roof-material-slots.d.ts.map +1 -0
  161. package/dist/systems/roof/roof-material-slots.js +13 -0
  162. package/dist/systems/roof/roof-materials.d.ts +25 -2
  163. package/dist/systems/roof/roof-materials.d.ts.map +1 -1
  164. package/dist/systems/roof/roof-materials.js +85 -26
  165. package/dist/systems/roof/roof-system.d.ts +34 -1
  166. package/dist/systems/roof/roof-system.d.ts.map +1 -1
  167. package/dist/systems/roof/roof-system.js +477 -105
  168. package/dist/systems/wall/opening-casing-geometry.d.ts +14 -0
  169. package/dist/systems/wall/opening-casing-geometry.d.ts.map +1 -0
  170. package/dist/systems/wall/opening-casing-geometry.js +102 -0
  171. package/dist/systems/wall/opening-cutout-geometry.d.ts +9 -0
  172. package/dist/systems/wall/opening-cutout-geometry.d.ts.map +1 -1
  173. package/dist/systems/wall/opening-cutout-geometry.js +7 -3
  174. package/dist/systems/wall/wall-materials.d.ts.map +1 -1
  175. package/dist/systems/wall/wall-materials.js +28 -35
  176. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  177. package/dist/systems/wall/wall-system.js +34 -19
  178. package/dist/systems/wall/wall-t-junctions.d.ts +15 -0
  179. package/dist/systems/wall/wall-t-junctions.d.ts.map +1 -0
  180. package/dist/systems/wall/wall-t-junctions.js +149 -0
  181. package/dist/systems/window/window-animation-system.d.ts +3 -12
  182. package/dist/systems/window/window-animation-system.d.ts.map +1 -1
  183. package/dist/systems/window/window-animation-system.js +37 -122
  184. package/dist/systems/window/window-pose.d.ts +31 -0
  185. package/dist/systems/window/window-pose.d.ts.map +1 -0
  186. package/dist/systems/window/window-pose.js +152 -0
  187. package/dist/systems/window/window-roller-shutter.d.ts +8 -0
  188. package/dist/systems/window/window-roller-shutter.d.ts.map +1 -0
  189. package/dist/systems/window/window-roller-shutter.js +64 -0
  190. package/dist/systems/window/window-system.d.ts +2 -11
  191. package/dist/systems/window/window-system.d.ts.map +1 -1
  192. package/dist/systems/window/window-system.js +470 -303
  193. package/package.json +6 -6
  194. package/dist/systems/interactive/control-widget.d.ts +0 -10
  195. package/dist/systems/interactive/control-widget.d.ts.map +0 -1
  196. package/dist/systems/interactive/control-widget.js +0 -35
  197. package/dist/systems/interactive/procedural-controls.d.ts +0 -13
  198. package/dist/systems/interactive/procedural-controls.d.ts.map +0 -1
  199. package/dist/systems/interactive/procedural-controls.js +0 -17
@@ -1,19 +1,17 @@
1
1
  'use client';
2
2
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { containsPoint, isScriptedNode, scriptedOrigin, scriptedSize, scriptInteractive, useInteractive, } from '@pascal-app/core';
3
+ import { isScriptedNode, scriptedInteractive, scriptedSize, useInteractive, } from '@pascal-app/core';
4
4
  import { evaluateRecipe, } from '@pascal-app/core/procedural-items';
5
- import { Html } from '@react-three/drei';
6
- import { createPortal, useFrame, useThree } from '@react-three/fiber';
7
- import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
8
- import { LoopRepeat, MathUtils, Vector3, } from 'three';
5
+ import { useThree } from '@react-three/fiber';
6
+ import { useEffect, useLayoutEffect, useMemo } from 'react';
7
+ import { LoopRepeat, MathUtils, Quaternion, Vector3, } from 'three';
9
8
  import { useShallow } from 'zustand/react/shallow';
10
9
  import { SCENE_LAYER } from '../../lib/layers';
11
10
  import { decorateProceduralEmission, proceduralSlotMeshes, setProceduralEmission, } from '../../lib/procedural-emission';
12
11
  import { useItemLightPool } from '../../store/use-item-light-pool';
13
12
  import useViewer from '../../store/use-viewer';
14
- import { ControlWidget } from '../../systems/interactive/control-widget';
15
- import { proceduralControlDescriptors } from '../../systems/interactive/procedural-controls';
16
13
  import { ScriptedClips } from '../../systems/interactive/scripted-clips';
14
+ import { PooledLights } from '../../systems/item-light/pooled-lights';
17
15
  /** Pull the interactive items out of a scene graph. Only items that actually
18
16
  * carry effects (light / animation) are returned — everything else baked
19
17
  * faithfully and needs no runtime help. */
@@ -46,7 +44,7 @@ export function buildGlbInteractiveItems(sceneGraph) {
46
44
  // emitted, as in the editor.
47
45
  if (isScriptedNode(raw)) {
48
46
  const scripted = raw;
49
- const interactive = scriptInteractive(scripted.source.manifest, scriptedOrigin(scripted));
47
+ const interactive = scriptedInteractive(scripted);
50
48
  if (!interactive)
51
49
  continue;
52
50
  const [, height] = scriptedSize(scripted.source.manifest);
@@ -113,6 +111,7 @@ export function buildGlbLightRegs(items, identity) {
113
111
  const anchor = motion ?? object;
114
112
  anchor.updateWorldMatrix(true, false);
115
113
  out.copy(local).applyMatrix4(anchor.matrixWorld);
114
+ return true;
116
115
  },
117
116
  getIntensity: () => light.intensity,
118
117
  isOn: () => useInteractive.getState().procedural[item.pascalId]?.lightsOn ??
@@ -134,6 +133,14 @@ export function buildGlbLightRegs(items, identity) {
134
133
  object,
135
134
  color: effect.color,
136
135
  distance: effect.distance ?? 0,
136
+ spot: effect.spot,
137
+ shadows: effect.shadows,
138
+ glow: effect.glow,
139
+ peak: effect.intensityRange[1],
140
+ getWorldDirection: (out) => {
141
+ out.set(...(effect.spot?.direction ?? [0, -1, 0])).normalize();
142
+ out.applyQuaternion(object.getWorldQuaternion(_quaternion));
143
+ },
137
144
  getWorldPosition: (out) => {
138
145
  object.updateWorldMatrix(true, false);
139
146
  if (item.scripted) {
@@ -141,10 +148,11 @@ export function buildGlbLightRegs(items, identity) {
141
148
  out
142
149
  .set(effect.offset[0] * scale[0], effect.offset[1] * scale[1], effect.offset[2] * scale[2])
143
150
  .applyMatrix4(object.matrixWorld);
144
- return;
151
+ return true;
145
152
  }
146
153
  object.getWorldPosition(out);
147
154
  out.set(out.x + effect.offset[0], out.y + effect.offset[1], out.z + effect.offset[2]);
155
+ return true;
148
156
  },
149
157
  getIntensity: () => {
150
158
  const values = useInteractive.getState().items[item.pascalId]?.controlValues;
@@ -162,7 +170,6 @@ export function buildGlbLightRegs(items, identity) {
162
170
  }
163
171
  return regs;
164
172
  }
165
- const _itemPos = new Vector3();
166
173
  /**
167
174
  * Re-creates the item-driven interactivity the parametric viewer has — pooled
168
175
  * lights, ambient animation, and the controls overlay — on top of a baked GLB.
@@ -170,7 +177,7 @@ const _itemPos = new Vector3();
170
177
  * baked Object3Ds (`identity`), joined on `pascalId`. Nothing is stamped into
171
178
  * the GLB itself, so the artifact stays integrator-clean.
172
179
  */
173
- export function GlbInteractive({ items, identity, zones, actions, levelOrder, }) {
180
+ export function GlbInteractive({ items, identity, actions, }) {
174
181
  const scene = useThree((state) => state.scene);
175
182
  useLayoutEffect(() => {
176
183
  useItemLightPool.getState().setBakedCanvas(scene, true);
@@ -201,34 +208,11 @@ export function GlbInteractive({ items, identity, zones, actions, levelOrder, })
201
208
  }, [items]);
202
209
  const animationItems = useMemo(() => items.filter((item) => item.interactive.effects.some((e) => e.kind === 'animation')), [items]);
203
210
  const lightRegs = useMemo(() => buildGlbLightRegs(items, identity), [items, identity]);
204
- const levelIndexById = useMemo(() => new Map(levelOrder.map((id, i) => [id, i])), [levelOrder]);
205
- // Project the zone's baked-local polygon into world space so focused zone
206
- // membership still works after level stacking moves its parent.
207
- const focusedZoneId = useViewer((s) => s.selection.zoneId);
208
- const selectedIds = useViewer((s) => s.selection.selectedIds);
209
- const worldPolygon = useMemo(() => {
210
- if (!focusedZoneId)
211
- return null;
212
- const zone = zones.find((z) => z.id === focusedZoneId);
213
- if (!zone)
214
- return null;
215
- zone.node.updateWorldMatrix(true, false);
216
- const project = (ring) => ring.map(([x, z]) => {
217
- const v = new Vector3(x, 0, z).applyMatrix4(zone.node.matrixWorld);
218
- return [v.x, v.z];
219
- });
220
- return { outer: project(zone.polygon), holes: (zone.holes ?? []).map(project) };
221
- }, [focusedZoneId, zones]);
222
- return (_jsxs(_Fragment, { children: [_jsx(GlbItemLights, { levelIndexById: levelIndexById, regs: lightRegs }), animationItems.map((item) => item.scripted ? (_jsx(GlbScriptedClips, { actions: actions, item: item }, item.pascalId)) : (_jsx(GlbItemAnimation, { actions: actions, item: item }, item.pascalId))), items
211
+ return (_jsxs(_Fragment, { children: [_jsx(GlbItemLights, { regs: lightRegs }), animationItems.map((item) => item.scripted ? (_jsx(GlbScriptedClips, { actions: actions, item: item }, item.pascalId)) : (_jsx(GlbItemAnimation, { actions: actions, item: item }, item.pascalId))), items
223
212
  .filter((item) => item.procedural?.lights.length)
224
213
  .map((item) => {
225
214
  const object = identity.get(item.pascalId);
226
215
  return object ? (_jsx(GlbProceduralEmission, { item: item, object: object }, item.pascalId)) : null;
227
- }), items
228
- .filter((item) => worldPolygon || selectedIds.includes(item.pascalId))
229
- .map((item) => {
230
- const object = identity.get(item.pascalId);
231
- return object ? (_jsx(GlbItemControls, { isSelected: selectedIds.includes(item.pascalId), item: item, object: object, worldPolygon: worldPolygon }, item.pascalId)) : null;
232
216
  })] }));
233
217
  }
234
218
  function GlbProceduralEmission({ item, object }) {
@@ -258,22 +242,7 @@ function GlbProceduralEmission({ item, object }) {
258
242
  }, [item, object]);
259
243
  return null;
260
244
  }
261
- // ── Pooled item lights ──────────────────────────────────────────────────────
262
- //
263
- // Mirrors the parametric `ItemLightSystem`: a fixed pool of point lights is
264
- // assigned to the nearest/most-visible lit items each tick (camera-proximity
265
- // scored, with hysteresis), snapped to the item's world position + offset, and
266
- // faded in/out on reassignment. Mounting a light per item instead would blow
267
- // the renderer's light budget on a large house.
268
- const POOL_SIZE = 12;
269
- const REASSIGN_INTERVAL = 0.2;
270
- const HYSTERESIS = 0.15;
271
- const CAM_MOVE_DIST = 0.5;
272
- const CAM_ROT_DOT = 0.995;
273
- const _camPos = new Vector3();
274
- const _camFwd = new Vector3();
275
- const _dir = new Vector3();
276
- const _lightWorld = new Vector3();
245
+ const _quaternion = new Quaternion();
277
246
  /** The nearest level-identity ancestor's pascalId, for the level factor. */
278
247
  function findLevelId(object) {
279
248
  let cur = object;
@@ -294,168 +263,21 @@ function isRendered(object) {
294
263
  }
295
264
  return true;
296
265
  }
297
- function scoreReg(reg, selectedLevelId, levelMode, levelIndexById) {
266
+ // A light on another storey than the focused one counts for this much of what it adds.
267
+ const OTHER_LEVEL_WEIGHT = 0.25;
268
+ /** 0 while the light cannot be seen lit (off, hidden, on a hidden level), else its level factor. */
269
+ function regWeight(reg, selectedLevelId, levelMode) {
298
270
  if (!reg.isOn() || !isRendered(reg.object))
299
- return Number.POSITIVE_INFINITY;
300
- if (selectedLevelId && reg.levelId !== selectedLevelId && levelMode === 'solo')
301
- return Number.POSITIVE_INFINITY;
302
- reg.getWorldPosition(_lightWorld);
303
- _dir.copy(_lightWorld).sub(_camPos).normalize();
304
- const angular = 1 - _camFwd.dot(_dir);
305
- const dist = _camPos.distanceTo(_lightWorld) / 200;
306
- let levelPenalty = 0;
307
- if (selectedLevelId) {
308
- if (reg.levelId !== selectedLevelId)
309
- levelPenalty = levelMode === 'solo' ? 100 : 0.8;
310
- }
311
- else if (reg.levelId && (levelIndexById.get(reg.levelId) ?? 0) !== 0) {
312
- levelPenalty = 0.3;
313
- }
314
- return angular * 0.7 + dist * 0.3 + levelPenalty;
271
+ return 0;
272
+ if (!selectedLevelId || reg.levelId === selectedLevelId)
273
+ return 1;
274
+ return levelMode === 'solo' ? 0 : OTHER_LEVEL_WEIGHT;
315
275
  }
316
- function GlbItemLights({ regs, levelIndexById, }) {
317
- const lightRefs = useRef(Array.from({ length: POOL_SIZE }, () => null));
318
- const slots = useRef(Array.from({ length: POOL_SIZE }, () => ({ key: null, pendingKey: null, isFadingOut: false })));
319
- const reassignTimer = useRef(0);
320
- const prevCamPos = useRef(new Vector3());
321
- const prevCamFwd = useRef(new Vector3(0, 0, -1));
322
- const regByKey = useMemo(() => new Map(regs.map((r) => [r.key, r])), [regs]);
323
- useFrame(({ camera }, delta) => {
324
- const dt = Math.min(delta, 0.1);
325
- camera.getWorldPosition(_camPos);
326
- camera.getWorldDirection(_camFwd);
327
- const camMoved = _camPos.distanceTo(prevCamPos.current) > CAM_MOVE_DIST ||
328
- _camFwd.dot(prevCamFwd.current) < CAM_ROT_DOT;
329
- reassignTimer.current -= delta;
330
- if (reassignTimer.current <= 0 || camMoved) {
331
- reassignTimer.current = REASSIGN_INTERVAL;
332
- prevCamPos.current.copy(_camPos);
333
- prevCamFwd.current.copy(_camFwd);
276
+ function GlbItemLights({ regs }) {
277
+ return (_jsx(PooledLights, { getLights: () => regs, intensity: (reg) => (reg.isOn() && isRendered(reg.object) ? reg.getIntensity() : 0), weight: (reg) => {
334
278
  const viewer = useViewer.getState();
335
- const selectedLevelId = viewer.selection.levelId;
336
- const levelMode = viewer.levelMode;
337
- const scored = regs.map((reg) => ({
338
- key: reg.key,
339
- score: scoreReg(reg, selectedLevelId, levelMode, levelIndexById),
340
- }));
341
- scored.sort((a, b) => a.score - b.score);
342
- const scoreByKey = new Map(scored.map((s) => [s.key, s.score]));
343
- const desired = scored
344
- .filter((s) => Number.isFinite(s.score))
345
- .slice(0, POOL_SIZE)
346
- .map((s) => s.key);
347
- const currentlyAssigned = new Map();
348
- for (let i = 0; i < POOL_SIZE; i++) {
349
- const s = slots.current[i];
350
- const k = s?.key ?? s?.pendingKey;
351
- if (k)
352
- currentlyAssigned.set(k, i);
353
- }
354
- const usedSlots = new Set();
355
- const assignedKeys = new Set();
356
- // Pass 1: keep existing slots whose key is still wanted.
357
- for (const key of desired) {
358
- const existingSlot = currentlyAssigned.get(key);
359
- if (existingSlot !== undefined && !usedSlots.has(existingSlot)) {
360
- usedSlots.add(existingSlot);
361
- assignedKeys.add(key);
362
- }
363
- }
364
- // Pass 2: assign the rest to free slots, evicting only on a clear win.
365
- let freeSlot = 0;
366
- for (const key of desired) {
367
- if (assignedKeys.has(key))
368
- continue;
369
- while (freeSlot < POOL_SIZE && usedSlots.has(freeSlot))
370
- freeSlot++;
371
- if (freeSlot >= POOL_SIZE)
372
- break;
373
- const freeSlotData = slots.current[freeSlot];
374
- const currentKey = freeSlotData ? (freeSlotData.key ?? freeSlotData.pendingKey) : null;
375
- if (currentKey && !desired.includes(currentKey)) {
376
- const currentScore = scoreByKey.get(currentKey) ?? Number.POSITIVE_INFINITY;
377
- const newScore = scoreByKey.get(key) ?? 0;
378
- if (currentScore - newScore < HYSTERESIS) {
379
- freeSlot++;
380
- continue;
381
- }
382
- }
383
- usedSlots.add(freeSlot);
384
- assignedKeys.add(key);
385
- const slot = slots.current[freeSlot];
386
- if (slot && slot.key !== key) {
387
- slot.pendingKey = key;
388
- slot.isFadingOut = slot.key !== null;
389
- if (!slot.isFadingOut) {
390
- slot.key = key;
391
- slot.pendingKey = null;
392
- const light = lightRefs.current[freeSlot];
393
- const reg = regByKey.get(key);
394
- if (light && reg) {
395
- light.color.set(reg.color);
396
- light.distance = reg.distance;
397
- }
398
- }
399
- }
400
- freeSlot++;
401
- }
402
- // Retire slots whose key is no longer wanted.
403
- for (let i = 0; i < POOL_SIZE; i++) {
404
- if (!usedSlots.has(i)) {
405
- const slot = slots.current[i];
406
- if (slot?.key && !desired.includes(slot.key)) {
407
- slot.pendingKey = null;
408
- slot.isFadingOut = true;
409
- }
410
- }
411
- }
412
- }
413
- // Per-frame: fade, snap position, and track intensity from control state.
414
- // The pool lights stay permanently `visible` — only `intensity` is animated
415
- // (an idle light just lerps to 0). Toggling `visible` would change the
416
- // active-light count, which forces the WebGPU renderer to recompile every
417
- // material's lighting node — a hard frame-time spike on every reassignment
418
- // (i.e. on every camera move). Keeping the count fixed avoids that entirely.
419
- for (let i = 0; i < POOL_SIZE; i++) {
420
- const light = lightRefs.current[i];
421
- const slot = slots.current[i];
422
- if (!(light && slot))
423
- continue;
424
- if (slot.isFadingOut) {
425
- light.intensity = MathUtils.lerp(light.intensity, 0, dt * 12);
426
- if (light.intensity < 0.01) {
427
- light.intensity = 0;
428
- slot.isFadingOut = false;
429
- slot.key = slot.pendingKey;
430
- slot.pendingKey = null;
431
- if (slot.key) {
432
- const reg = regByKey.get(slot.key);
433
- if (reg) {
434
- light.color.set(reg.color);
435
- light.distance = reg.distance;
436
- }
437
- }
438
- }
439
- continue;
440
- }
441
- if (!slot.key) {
442
- light.intensity = MathUtils.lerp(light.intensity, 0, dt * 12);
443
- continue;
444
- }
445
- const reg = regByKey.get(slot.key);
446
- if (!reg) {
447
- slot.key = null;
448
- continue;
449
- }
450
- reg.getWorldPosition(_lightWorld);
451
- light.position.copy(_lightWorld);
452
- const targetIntensity = reg.isOn() && isRendered(reg.object) ? reg.getIntensity() : 0;
453
- light.intensity = MathUtils.lerp(light.intensity, targetIntensity, dt * 12);
454
- }
455
- }, 6);
456
- return (_jsx(_Fragment, { children: Array.from({ length: POOL_SIZE }, (_, i) => (_jsx("pointLight", { castShadow: false, intensity: 0, ref: (el) => {
457
- lightRefs.current[i] = el;
458
- } }, i))) }));
279
+ return regWeight(reg, viewer.selection.levelId, viewer.levelMode);
280
+ } }));
459
281
  }
460
282
  /** Plays an item's baked ambient loop (a fan's spin), gated on its toggle.
461
283
  * The clip and its targets are already in the GLB; we only start/stop it. */
@@ -501,61 +323,3 @@ function GlbScriptedClips({ item, actions, }) {
501
323
  }, [actions, item.pascalId]);
502
324
  return (_jsx(ScriptedClips, { actions: scriptActions, interactive: item.interactive, nodeId: item.pascalId }));
503
325
  }
504
- const FADE_MS = 300;
505
- /** Controls overlay for a selected item or one inside the focused zone. */
506
- function GlbItemControls({ item, object, worldPolygon, isSelected, }) {
507
- const controlValues = useInteractive(useShallow((s) => s.items[item.pascalId]?.controlValues));
508
- const proceduralState = useInteractive((s) => s.procedural[item.pascalId]);
509
- const lampDefault = useInteractive((s) => s.lampDefault);
510
- const setControlValue = useInteractive((s) => s.setControlValue);
511
- const togglePart = useInteractive((s) => s.toggleProceduralPart);
512
- const toggleLights = useInteractive((s) => s.toggleProceduralLights);
513
- const descriptors = item.procedural
514
- ? proceduralControlDescriptors(item.procedural.recipe ?? { parts: item.procedural.parts }, proceduralState, (partId) => togglePart(item.pascalId, partId), () => toggleLights(item.pascalId), lampDefault)
515
- : item.interactive.controls.map((control, index) => ({
516
- key: String(index),
517
- control,
518
- value: controlValues?.[index] ?? false,
519
- onChange: (value) => setControlValue(item.pascalId, index, value),
520
- }));
521
- let visible = isSelected;
522
- if (worldPolygon) {
523
- object.getWorldPosition(_itemPos);
524
- visible = visible || containsPoint([worldPolygon], [_itemPos.x, _itemPos.z]);
525
- }
526
- // Fade in on mount and fade out before unmounting the <Html>.
527
- const [mounted, setMounted] = useState(false);
528
- const [shown, setShown] = useState(false);
529
- useEffect(() => {
530
- if (visible) {
531
- setMounted(true);
532
- let raf2 = 0;
533
- const raf1 = requestAnimationFrame(() => {
534
- raf2 = requestAnimationFrame(() => setShown(true));
535
- });
536
- return () => {
537
- cancelAnimationFrame(raf1);
538
- cancelAnimationFrame(raf2);
539
- };
540
- }
541
- setShown(false);
542
- const timeout = setTimeout(() => setMounted(false), FADE_MS);
543
- return () => clearTimeout(timeout);
544
- }, [visible]);
545
- if (!(mounted && descriptors.length))
546
- return null;
547
- return createPortal(_jsx(Html, { center: true, distanceFactor: 8, eps: -1, position: [0, item.height + 0.3, 0], zIndexRange: [20, 0], children: _jsx("div", { onClick: (e) => e.stopPropagation(), onPointerDown: (e) => e.stopPropagation(), onPointerUp: (e) => e.stopPropagation(), style: {
548
- display: 'flex',
549
- flexDirection: 'column',
550
- gap: 6,
551
- background: 'rgba(0,0,0,0.75)',
552
- backdropFilter: 'blur(8px)',
553
- borderRadius: 8,
554
- padding: '8px 12px',
555
- minWidth: 120,
556
- pointerEvents: visible ? 'auto' : 'none',
557
- userSelect: 'none',
558
- opacity: shown ? 1 : 0,
559
- transition: `opacity ${FADE_MS}ms ease`,
560
- }, children: descriptors.map((descriptor) => (_jsx(ControlWidget, { control: descriptor.control, onChange: descriptor.onChange, value: descriptor.value }, descriptor.key))) }) }), object);
561
- }
@@ -0,0 +1,8 @@
1
+ import { type AnimationAction, type Object3D } from 'three';
2
+ import type { ScriptedClipActions } from '../../systems/interactive/scripted-clips';
3
+ export declare function isGlbShutterHit(hit: Object3D, identity: Object3D): boolean;
4
+ export declare function playGlbShutters(action: AnimationAction, open: boolean): void;
5
+ /** Own only the shutters part; the sash and other mechanisms keep their own state. */
6
+ export declare function bindGlbShutters(identity: ReadonlyMap<string, Object3D>, actions: ScriptedClipActions): () => void;
7
+ export declare function toggleGlbShutters(node: Object3D): boolean;
8
+ //# sourceMappingURL=glb-scene-motion.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"glb-scene-motion.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/glb-scene-motion.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,eAAe,EAAY,KAAK,QAAQ,EAAE,MAAM,OAAO,CAAA;AACrE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,0CAA0C,CAAA;AAEnF,wBAAgB,eAAe,CAAC,GAAG,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAK1E;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,eAAe,EAAE,IAAI,EAAE,OAAO,GAAG,IAAI,CAO5E;AAED,sFAAsF;AACtF,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,QAAQ,CAAC,EACvC,OAAO,EAAE,mBAAmB,GAC3B,MAAM,IAAI,CAgCZ;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAMzD"}
@@ -0,0 +1,57 @@
1
+ import { useInteractive } from '@pascal-app/core';
2
+ import { LoopOnce } from 'three';
3
+ export function isGlbShutterHit(hit, identity) {
4
+ for (let part = hit; part && part !== identity; part = part.parent) {
5
+ if (part.name.startsWith('shutter-') || part.name.startsWith('window-roller-'))
6
+ return true;
7
+ }
8
+ return false;
9
+ }
10
+ export function playGlbShutters(action, open) {
11
+ action.enabled = true;
12
+ action.paused = false;
13
+ action.setLoop(LoopOnce, 1);
14
+ action.clampWhenFinished = true;
15
+ action.timeScale = open ? 1 : -1;
16
+ action.play();
17
+ }
18
+ /** Own only the shutters part; the sash and other mechanisms keep their own state. */
19
+ export function bindGlbShutters(identity, actions) {
20
+ const parts = new Map();
21
+ for (const [id, object] of identity) {
22
+ const extras = object.userData;
23
+ const name = extras.clips?.find((clip) => clip === `${id}: shutters`);
24
+ const action = name ? actions[name] : undefined;
25
+ if (!extras.shutters || !action)
26
+ continue;
27
+ const fraction = Math.max(0, Math.min(1, extras.shuttersOpen ?? 1));
28
+ action.time = fraction * action.getClip().duration;
29
+ // Keep the baked rest pose until the first toggle, including separately saved roller poses.
30
+ parts.set(id, action);
31
+ useInteractive.getState().setMechanismPart(id, 'shutters', fraction > 0.5);
32
+ }
33
+ const unsubscribe = useInteractive.subscribe((state, previous) => {
34
+ if (state.mechanismParts === previous.mechanismParts)
35
+ return;
36
+ for (const [id, action] of parts) {
37
+ const key = `${id}:shutters`;
38
+ if (state.mechanismParts[key] !== previous.mechanismParts[key])
39
+ playGlbShutters(action, Boolean(state.mechanismParts[key]));
40
+ }
41
+ });
42
+ return () => {
43
+ unsubscribe();
44
+ for (const [id, action] of parts) {
45
+ action.stop();
46
+ useInteractive.getState().removeMechanismPart(id, 'shutters');
47
+ }
48
+ };
49
+ }
50
+ export function toggleGlbShutters(node) {
51
+ const { shutters, pascalId } = node.userData;
52
+ if (!shutters || !pascalId)
53
+ return false;
54
+ const state = useInteractive.getState();
55
+ state.setMechanismPart(pascalId, 'shutters', !state.mechanismParts[`${pascalId}:shutters`]);
56
+ return true;
57
+ }
@@ -51,7 +51,7 @@ export declare function resolveGlbPick(hits: readonly HitCandidate[], identity:
51
51
  * when clicked. The host disables the parametric `SelectionManager`
52
52
  * (`selectionManager="custom"`).
53
53
  */
54
- export declare function GlbScene({ url, interactiveItems, referenceNodes, replaceNodes, onObjectsChange, onPick, onWalkthroughChange, }: {
54
+ export declare function GlbScene({ url, interactiveItems, referenceNodes, replaceNodes, onObjectsChange, onPick, onWalkthroughChange, sceneNodes, }: {
55
55
  url: string;
56
56
  /** Light / animation effects + controls recovered from the DB scene graph,
57
57
  * joined to the baked nodes by `pascalId` to re-light + re-animate the GLB. */
@@ -67,6 +67,8 @@ export declare function GlbScene({ url, interactiveItems, referenceNodes, replac
67
67
  /** Clicks and hovers on the building; without it the scene is look-only. */
68
68
  onPick?: (event: GlbPickEvent) => void;
69
69
  onWalkthroughChange?: (state: GlbWalkthrough) => void;
70
+ /** The scene graph's nodes, so the selection callout can say what each element is. */
71
+ sceneNodes?: Readonly<Record<string, AnyNode>>;
70
72
  }): import("react").JSX.Element;
71
73
  export {};
72
74
  //# sourceMappingURL=glb-scene.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"glb-scene.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/glb-scene.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,OAAO,EAYb,MAAM,kBAAkB,CAAA;AAQzB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAS9B,OAAO,EAAkB,KAAK,kBAAkB,EAAE,MAAM,mBAAmB,CAAA;AAmB3E;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,OAAO,GAAG,OAAO,CAAA;IACvB,IAAI,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI,CAAA;IAC/D,SAAS,EAAE;QAAE,GAAG,EAAE,OAAO,CAAC;QAAC,IAAI,EAAE,OAAO,CAAC;QAAC,IAAI,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,CAAA;CAC1E,CAAA;AAKD;0EAC0E;AAC1E,MAAM,MAAM,cAAc,GAAG;IAC3B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,OAAO,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;CAC/D,GAAG,IAAI,CAAA;AAoDR,KAAK,YAAY,GAAG;IAAE,MAAM,EAAE,KAAK,CAAC,QAAQ,CAAC;IAAC,KAAK,EAAE,KAAK,CAAC,OAAO,CAAA;CAAE,CAAA;AA0CpE;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,SAAS,YAAY,EAAE,EAC7B,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,KAAK,CAAC,QAAQ,CAAC,GAC5C,CAAC,WAAW,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,GAAG;IAAE,SAAS,EAAE,KAAK,CAAC,QAAQ,CAAA;CAAE,CAAC,GAAG,IAAI,CAkB5E;AASD;;;;;;;;GAQG;AACH,wBAAgB,QAAQ,CAAC,EACvB,GAAG,EACH,gBAAgB,EAChB,cAAc,EACd,YAAY,EACZ,eAAe,EACf,MAAM,EACN,mBAAmB,GACpB,EAAE;IACD,GAAG,EAAE,MAAM,CAAA;IACX;oFACgF;IAChF,gBAAgB,CAAC,EAAE,kBAAkB,EAAE,CAAA;IACvC;mFAC+E;IAC/E,cAAc,CAAC,EAAE,OAAO,EAAE,CAAA;IAC1B;kFAC8E;IAC9E,YAAY,CAAC,EAAE,OAAO,EAAE,CAAA;IACxB,iFAAiF;IACjF,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,KAAK,CAAC,QAAQ,CAAC,KAAK,IAAI,CAAA;IACxE,4EAA4E;IAC5E,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAA;IACtC,mBAAmB,CAAC,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAA;CACtD,+BA8xBA"}
1
+ {"version":3,"file":"glb-scene.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/glb-scene.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,OAAO,EAab,MAAM,kBAAkB,CAAA;AASzB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAoB9B,OAAO,EAAkB,KAAK,kBAAkB,EAAE,MAAM,mBAAmB,CAAA;AAuB3E;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,OAAO,GAAG,OAAO,CAAA;IACvB,IAAI,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI,CAAA;IAC/D,SAAS,EAAE;QAAE,GAAG,EAAE,OAAO,CAAC;QAAC,IAAI,EAAE,OAAO,CAAC;QAAC,IAAI,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,CAAA;CAC1E,CAAA;AAKD;0EAC0E;AAC1E,MAAM,MAAM,cAAc,GAAG;IAC3B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,OAAO,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;CAC/D,GAAG,IAAI,CAAA;AAqDR,KAAK,YAAY,GAAG;IAAE,MAAM,EAAE,KAAK,CAAC,QAAQ,CAAC;IAAC,KAAK,EAAE,KAAK,CAAC,OAAO,CAAA;CAAE,CAAA;AA4CpE;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,SAAS,YAAY,EAAE,EAC7B,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,KAAK,CAAC,QAAQ,CAAC,GAC5C,CAAC,WAAW,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,GAAG;IAAE,SAAS,EAAE,KAAK,CAAC,QAAQ,CAAA;CAAE,CAAC,GAAG,IAAI,CAkB5E;AASD;;;;;;;;GAQG;AACH,wBAAgB,QAAQ,CAAC,EACvB,GAAG,EACH,gBAAgB,EAChB,cAAc,EACd,YAAY,EACZ,eAAe,EACf,MAAM,EACN,mBAAmB,EACnB,UAAU,GACX,EAAE;IACD,GAAG,EAAE,MAAM,CAAA;IACX;oFACgF;IAChF,gBAAgB,CAAC,EAAE,kBAAkB,EAAE,CAAA;IACvC;mFAC+E;IAC/E,cAAc,CAAC,EAAE,OAAO,EAAE,CAAA;IAC1B;kFAC8E;IAC9E,YAAY,CAAC,EAAE,OAAO,EAAE,CAAA;IACxB,iFAAiF;IACjF,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,KAAK,CAAC,QAAQ,CAAC,KAAK,IAAI,CAAA;IACxE,4EAA4E;IAC5E,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAA;IACtC,mBAAmB,CAAC,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAA;IACrD,sFAAsF;IACtF,UAAU,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;CAC/C,+BAu7BA"}