@pascal-app/viewer 0.9.1 → 1.0.0-beta.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 (206) hide show
  1. package/README.md +20 -5
  2. package/dist/components/error-boundary.d.ts +6 -0
  3. package/dist/components/error-boundary.d.ts.map +1 -1
  4. package/dist/components/error-boundary.js +6 -0
  5. package/dist/components/renderers/node-renderer.d.ts +5 -1
  6. package/dist/components/renderers/node-renderer.d.ts.map +1 -1
  7. package/dist/components/renderers/node-renderer.js +5 -2
  8. package/dist/components/renderers/parametric-node-renderer.d.ts.map +1 -1
  9. package/dist/components/renderers/parametric-node-renderer.js +7 -4
  10. package/dist/components/viewer/bvh-ecctrl.d.ts +77 -0
  11. package/dist/components/viewer/bvh-ecctrl.d.ts.map +1 -0
  12. package/dist/components/viewer/bvh-ecctrl.js +552 -0
  13. package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
  14. package/dist/components/viewer/frame-limiter.js +59 -4
  15. package/dist/components/viewer/glb-interactive.d.ts +41 -0
  16. package/dist/components/viewer/glb-interactive.d.ts.map +1 -0
  17. package/dist/components/viewer/glb-interactive.js +394 -0
  18. package/dist/components/viewer/glb-reference-nodes.d.ts +29 -0
  19. package/dist/components/viewer/glb-reference-nodes.d.ts.map +1 -0
  20. package/dist/components/viewer/glb-reference-nodes.js +80 -0
  21. package/dist/components/viewer/glb-replace-instances.d.ts +18 -0
  22. package/dist/components/viewer/glb-replace-instances.d.ts.map +1 -0
  23. package/dist/components/viewer/glb-replace-instances.js +61 -0
  24. package/dist/components/viewer/glb-scene.d.ts +54 -0
  25. package/dist/components/viewer/glb-scene.d.ts.map +1 -0
  26. package/dist/components/viewer/glb-scene.js +920 -0
  27. package/dist/components/viewer/glb-walkthrough-controller.d.ts +22 -0
  28. package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -0
  29. package/dist/components/viewer/glb-walkthrough-controller.js +445 -0
  30. package/dist/components/viewer/index.d.ts +19 -2
  31. package/dist/components/viewer/index.d.ts.map +1 -1
  32. package/dist/components/viewer/index.js +113 -12
  33. package/dist/components/viewer/lights.d.ts.map +1 -1
  34. package/dist/components/viewer/lights.js +36 -5
  35. package/dist/components/viewer/perf-monitor.d.ts.map +1 -1
  36. package/dist/components/viewer/perf-monitor.js +17 -2
  37. package/dist/components/viewer/post-processing.d.ts +7 -1
  38. package/dist/components/viewer/post-processing.d.ts.map +1 -1
  39. package/dist/components/viewer/post-processing.js +180 -24
  40. package/dist/components/viewer/registered-systems.d.ts.map +1 -1
  41. package/dist/components/viewer/registered-systems.js +6 -4
  42. package/dist/components/viewer/scene-bvh.d.ts +2 -1
  43. package/dist/components/viewer/scene-bvh.d.ts.map +1 -1
  44. package/dist/components/viewer/scene-bvh.js +3 -0
  45. package/dist/components/viewer/scene-bvh.test.d.ts +2 -0
  46. package/dist/components/viewer/scene-bvh.test.d.ts.map +1 -0
  47. package/dist/components/viewer/scene-bvh.test.js +13 -0
  48. package/dist/components/viewer/scene-environment.d.ts +2 -0
  49. package/dist/components/viewer/scene-environment.d.ts.map +1 -0
  50. package/dist/components/viewer/scene-environment.js +79 -0
  51. package/dist/components/viewer/selection-manager.d.ts.map +1 -1
  52. package/dist/components/viewer/selection-manager.js +23 -12
  53. package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
  54. package/dist/hooks/use-gltf-ktx2.js +2 -24
  55. package/dist/hooks/use-node-events.d.ts +1 -1
  56. package/dist/hooks/use-node-events.d.ts.map +1 -1
  57. package/dist/hooks/use-node-events.js +22 -14
  58. package/dist/index.d.ts +24 -10
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +29 -13
  61. package/dist/lib/backdrop.d.ts +33 -0
  62. package/dist/lib/backdrop.d.ts.map +1 -0
  63. package/dist/lib/backdrop.js +92 -0
  64. package/dist/lib/box-uv.d.ts +13 -0
  65. package/dist/lib/box-uv.d.ts.map +1 -0
  66. package/dist/lib/box-uv.js +32 -0
  67. package/dist/lib/csg-utils.d.ts +3 -9
  68. package/dist/lib/csg-utils.d.ts.map +1 -1
  69. package/dist/lib/csg-utils.js +65 -5
  70. package/dist/lib/csg-utils.test.d.ts +2 -0
  71. package/dist/lib/csg-utils.test.d.ts.map +1 -0
  72. package/dist/lib/csg-utils.test.js +45 -0
  73. package/dist/lib/edge-style.d.ts +1 -0
  74. package/dist/lib/edge-style.d.ts.map +1 -1
  75. package/dist/lib/edge-style.js +26 -6
  76. package/dist/lib/hero-pose.d.ts +47 -0
  77. package/dist/lib/hero-pose.d.ts.map +1 -0
  78. package/dist/lib/hero-pose.js +227 -0
  79. package/dist/lib/ink-edges.d.ts +1 -1
  80. package/dist/lib/ink-edges.d.ts.map +1 -1
  81. package/dist/lib/ink-edges.js +13 -7
  82. package/dist/lib/ktx2-loader.d.ts +46 -0
  83. package/dist/lib/ktx2-loader.d.ts.map +1 -0
  84. package/dist/lib/ktx2-loader.js +142 -0
  85. package/dist/lib/layers.d.ts +10 -0
  86. package/dist/lib/layers.d.ts.map +1 -1
  87. package/dist/lib/layers.js +10 -0
  88. package/dist/lib/materials.d.ts +16 -1
  89. package/dist/lib/materials.d.ts.map +1 -1
  90. package/dist/lib/materials.js +158 -34
  91. package/dist/lib/materials.test.d.ts +2 -0
  92. package/dist/lib/materials.test.d.ts.map +1 -0
  93. package/dist/lib/materials.test.js +25 -0
  94. package/dist/lib/merged-outline-node.test.d.ts +2 -0
  95. package/dist/lib/merged-outline-node.test.d.ts.map +1 -0
  96. package/dist/lib/merged-outline-node.test.js +22 -0
  97. package/dist/lib/scene-themes.d.ts +1 -0
  98. package/dist/lib/scene-themes.d.ts.map +1 -1
  99. package/dist/lib/scene-themes.js +25 -16
  100. package/dist/lib/shadow-only.d.ts +4 -0
  101. package/dist/lib/shadow-only.d.ts.map +1 -0
  102. package/dist/lib/shadow-only.js +36 -0
  103. package/dist/lib/snapshot-pipeline.d.ts +42 -0
  104. package/dist/lib/snapshot-pipeline.d.ts.map +1 -0
  105. package/dist/lib/snapshot-pipeline.js +254 -0
  106. package/dist/lib/terrain-perimeter-fill.d.ts +7 -0
  107. package/dist/lib/terrain-perimeter-fill.d.ts.map +1 -0
  108. package/dist/lib/terrain-perimeter-fill.js +75 -0
  109. package/dist/lib/texture-node-guard.d.ts +13 -0
  110. package/dist/lib/texture-node-guard.d.ts.map +1 -0
  111. package/dist/lib/texture-node-guard.js +39 -0
  112. package/dist/lib/texture-reference.d.ts +31 -0
  113. package/dist/lib/texture-reference.d.ts.map +1 -0
  114. package/dist/lib/texture-reference.js +139 -0
  115. package/dist/lib/texture-reference.test.d.ts +2 -0
  116. package/dist/lib/texture-reference.test.d.ts.map +1 -0
  117. package/dist/lib/texture-reference.test.js +86 -0
  118. package/dist/lib/tsl-compat.d.ts +12 -0
  119. package/dist/lib/tsl-compat.d.ts.map +1 -0
  120. package/dist/lib/tsl-compat.js +13 -0
  121. package/dist/store/use-viewer.d.ts +73 -18
  122. package/dist/store/use-viewer.d.ts.map +1 -1
  123. package/dist/store/use-viewer.js +191 -4
  124. package/dist/store/use-viewer.test.d.ts +2 -0
  125. package/dist/store/use-viewer.test.d.ts.map +1 -0
  126. package/dist/store/use-viewer.test.js +61 -0
  127. package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
  128. package/dist/systems/ceiling/ceiling-system.js +12 -5
  129. package/dist/systems/door/door-animation.test.d.ts +2 -0
  130. package/dist/systems/door/door-animation.test.d.ts.map +1 -0
  131. package/dist/systems/door/door-animation.test.js +51 -0
  132. package/dist/systems/door/door-floor-alignment.test.d.ts +2 -0
  133. package/dist/systems/door/door-floor-alignment.test.d.ts.map +1 -0
  134. package/dist/systems/door/door-floor-alignment.test.js +50 -0
  135. package/dist/systems/door/door-system.d.ts +24 -0
  136. package/dist/systems/door/door-system.d.ts.map +1 -1
  137. package/dist/systems/door/door-system.js +494 -161
  138. package/dist/systems/fence/fence-system.d.ts +17 -0
  139. package/dist/systems/fence/fence-system.d.ts.map +1 -1
  140. package/dist/systems/fence/fence-system.js +221 -53
  141. package/dist/systems/floor-elevation/floor-elevation-system.d.ts.map +1 -1
  142. package/dist/systems/floor-elevation/floor-elevation-system.js +30 -7
  143. package/dist/systems/geometry/geometry-system.d.ts +7 -0
  144. package/dist/systems/geometry/geometry-system.d.ts.map +1 -1
  145. package/dist/systems/geometry/geometry-system.js +95 -26
  146. package/dist/systems/geometry/geometry-system.test.d.ts +2 -0
  147. package/dist/systems/geometry/geometry-system.test.d.ts.map +1 -0
  148. package/dist/systems/geometry/geometry-system.test.js +30 -0
  149. package/dist/systems/interactive/control-widget.d.ts +10 -0
  150. package/dist/systems/interactive/control-widget.d.ts.map +1 -0
  151. package/dist/systems/interactive/control-widget.js +35 -0
  152. package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
  153. package/dist/systems/interactive/interactive-system.js +55 -56
  154. package/dist/systems/item/item-system.d.ts.map +1 -1
  155. package/dist/systems/item/item-system.js +20 -6
  156. package/dist/systems/level/level-system.d.ts.map +1 -1
  157. package/dist/systems/level/level-system.js +34 -9
  158. package/dist/systems/level/level-utils.d.ts +0 -3
  159. package/dist/systems/level/level-utils.d.ts.map +1 -1
  160. package/dist/systems/level/level-utils.js +7 -45
  161. package/dist/systems/roof/roof-materials.d.ts.map +1 -1
  162. package/dist/systems/roof/roof-materials.js +29 -8
  163. package/dist/systems/roof/roof-system.d.ts +21 -8
  164. package/dist/systems/roof/roof-system.d.ts.map +1 -1
  165. package/dist/systems/roof/roof-system.js +1079 -221
  166. package/dist/systems/slab/slab-system.d.ts +7 -4
  167. package/dist/systems/slab/slab-system.d.ts.map +1 -1
  168. package/dist/systems/slab/slab-system.js +31 -83
  169. package/dist/systems/slab/slab-system.test.js +54 -14
  170. package/dist/systems/stair/stair-system.js +7 -4
  171. package/dist/systems/wall/opening-cutout-geometry.d.ts +31 -0
  172. package/dist/systems/wall/opening-cutout-geometry.d.ts.map +1 -0
  173. package/dist/systems/wall/opening-cutout-geometry.js +167 -0
  174. package/dist/systems/wall/opening-cutout-geometry.test.d.ts +2 -0
  175. package/dist/systems/wall/opening-cutout-geometry.test.d.ts.map +1 -0
  176. package/dist/systems/wall/opening-cutout-geometry.test.js +172 -0
  177. package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
  178. package/dist/systems/wall/wall-cutout.js +49 -17
  179. package/dist/systems/wall/wall-materials.d.ts +11 -7
  180. package/dist/systems/wall/wall-materials.d.ts.map +1 -1
  181. package/dist/systems/wall/wall-materials.js +244 -35
  182. package/dist/systems/wall/wall-opening-cutout.test.d.ts +2 -0
  183. package/dist/systems/wall/wall-opening-cutout.test.d.ts.map +1 -0
  184. package/dist/systems/wall/wall-opening-cutout.test.js +59 -0
  185. package/dist/systems/wall/wall-support-extension.test.d.ts +2 -0
  186. package/dist/systems/wall/wall-support-extension.test.d.ts.map +1 -0
  187. package/dist/systems/wall/wall-support-extension.test.js +116 -0
  188. package/dist/systems/wall/wall-system.d.ts +4 -8
  189. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  190. package/dist/systems/wall/wall-system.js +397 -177
  191. package/dist/systems/wall/wall-terrain-bottom.test.d.ts +2 -0
  192. package/dist/systems/wall/wall-terrain-bottom.test.d.ts.map +1 -0
  193. package/dist/systems/wall/wall-terrain-bottom.test.js +34 -0
  194. package/dist/systems/window/window-animation-system.d.ts +12 -0
  195. package/dist/systems/window/window-animation-system.d.ts.map +1 -1
  196. package/dist/systems/window/window-animation-system.js +16 -5
  197. package/dist/systems/window/window-system.d.ts +7 -0
  198. package/dist/systems/window/window-system.d.ts.map +1 -1
  199. package/dist/systems/window/window-system.js +240 -14
  200. package/package.json +4 -4
  201. package/dist/components/viewer/ground-occluder.d.ts +0 -2
  202. package/dist/components/viewer/ground-occluder.d.ts.map +0 -1
  203. package/dist/components/viewer/ground-occluder.js +0 -67
  204. package/dist/components/viewer/walkthrough-controls.d.ts +0 -2
  205. package/dist/components/viewer/walkthrough-controls.d.ts.map +0 -1
  206. package/dist/components/viewer/walkthrough-controls.js +0 -131
@@ -0,0 +1,22 @@
1
+ export declare const WALKTHROUGH_FOV = 60;
2
+ export declare const STAND_CAPSULE: [number, number, number, number];
3
+ export declare const CROUCH_CAPSULE: [number, number, number, number];
4
+ export declare const STAND_FLOAT_HEIGHT = 0.5;
5
+ export declare const CROUCH_FLOAT_HEIGHT = 0.25;
6
+ export declare const CROUCH_EYE_OFFSET = 0.1;
7
+ export declare const CROUCH_WALK_SPEED = 1;
8
+ export declare const CROUCH_RUN_SPEED = 1.4;
9
+ export declare const STAND_CLEARANCE = 1.25;
10
+ export declare const EYE_LERP_SPEED = 12;
11
+ /**
12
+ * First-person walkthrough controller for the baked GLB. Reuses the editor's
13
+ * `BVHEcctrl` capsule character controller (gravity, jump, sprint, ground-float,
14
+ * mesh collision) fed a collider built from the artifact's own geometry — so the
15
+ * baked viewer walks the building with the same physics as the editor, without
16
+ * the parametric scene. Pointer-lock drives look; WASD moves; Space jumps; Shift
17
+ * sprints. Door/window interaction stays in `GlbScene` (its centre-ray HUD).
18
+ */
19
+ export declare function GlbWalkthroughController({ url }: {
20
+ url: string;
21
+ }): import("react").JSX.Element | null;
22
+ //# sourceMappingURL=glb-walkthrough-controller.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"glb-walkthrough-controller.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/glb-walkthrough-controller.tsx"],"names":[],"mappings":"AAiCA,eAAO,MAAM,eAAe,KAAK,CAAA;AAkBjC,eAAO,MAAM,aAAa,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAqB,CAAA;AAChF,eAAO,MAAM,cAAc,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAqB,CAAA;AACjF,eAAO,MAAM,kBAAkB,MAAM,CAAA;AACrC,eAAO,MAAM,mBAAmB,OAAO,CAAA;AACvC,eAAO,MAAM,iBAAiB,MAAM,CAAA;AACpC,eAAO,MAAM,iBAAiB,IAAI,CAAA;AAClC,eAAO,MAAM,gBAAgB,MAAM,CAAA;AAInC,eAAO,MAAM,eAAe,OAAO,CAAA;AACnC,eAAO,MAAM,cAAc,KAAK,CAAA;AA8LhC;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CAAC,EAAE,GAAG,EAAE,EAAE;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,sCAqPhE"}
@@ -0,0 +1,445 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { KeyboardControls } from '@react-three/drei';
4
+ import { useFrame, useThree } from '@react-three/fiber';
5
+ import { useCallback, useEffect, useRef, useState } from 'react';
6
+ import { Box3, BoxGeometry, BufferGeometry, Euler, Float32BufferAttribute, Matrix4, Mesh, MeshBasicMaterial, Quaternion, Raycaster, Vector3, } from 'three';
7
+ import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
8
+ import { acceleratedRaycast, computeBoundsTree, disposeBoundsTree } from 'three-mesh-bvh';
9
+ import { useGLTFKTX2 } from '../../hooks/use-gltf-ktx2';
10
+ import { SCENE_LAYER } from '../../lib/layers';
11
+ import useViewer from '../../store/use-viewer';
12
+ import BVHEcctrl, {} from './bvh-ecctrl';
13
+ // First-person FOV. The orbit camera is 50° (set on the Canvas), which feels
14
+ // cramped on foot; ~60° vertical (~90° horizontal at 16:9) restores peripheral
15
+ // awareness without wide-angle distortion. Applied only while walking — both
16
+ // walkthrough controllers read this and restore the orbit FOV on exit.
17
+ export const WALKTHROUGH_FOV = 60;
18
+ // Eye/capsule geometry mirrors the editor's first-person controller so the
19
+ // baked walkthrough feels identical. The capsule centre sits below the eye; the
20
+ // camera rides the capsule with a small offset and the controller floats it to
21
+ // the ground.
22
+ const CAMERA_EYE_OFFSET = 0.45;
23
+ const CONTROLLER_CENTER_FROM_EYE = 0.85;
24
+ const SPAWN_EYE_HEIGHT = 1.65;
25
+ const LOOK_SENSITIVITY = 0.002;
26
+ const VOID_FALL_RESPAWN_DEPTH = 12;
27
+ // Crouch (hold Ctrl): swap to a short capsule — it shrinks around the centre,
28
+ // so a crouch mid-jump also lowers the head AND raises the feet, letting the
29
+ // player thread window openings. Standing back up is gated on headroom.
30
+ // The float gap counts toward the effective obstacle height (the capsule rides
31
+ // floatHeight above the ground), so crouching also lowers it: crouched span is
32
+ // CROUCH_FLOAT_HEIGHT + capsule = 0.25 + 0.7 = 0.95 m — fits a 1 m opening.
33
+ export const STAND_CAPSULE = [0.25, 0.8, 4, 8];
34
+ export const CROUCH_CAPSULE = [0.25, 0.2, 4, 8];
35
+ export const STAND_FLOAT_HEIGHT = 0.5;
36
+ export const CROUCH_FLOAT_HEIGHT = 0.25;
37
+ export const CROUCH_EYE_OFFSET = 0.1;
38
+ export const CROUCH_WALK_SPEED = 1;
39
+ export const CROUCH_RUN_SPEED = 1.4;
40
+ // Headroom (from the capsule centre, upward) required before uncrouching:
41
+ // standing raises the centre by half the length delta plus the float delta,
42
+ // and the standing capsule top sits standLength/2 + radius above the centre.
43
+ export const STAND_CLEARANCE = 1.25;
44
+ export const EYE_LERP_SPEED = 12;
45
+ const standClearanceRaycaster = new Raycaster();
46
+ const UP = new Vector3(0, 1, 0);
47
+ // Kinds that must not block the player: room helpers, the spawn marker, the
48
+ // ceiling/roof shell (you walk under them), and door/window leaves — excluding
49
+ // the latter lets you pass any doorway whether the leaf is open or shut (the
50
+ // wall already has the opening cut into its baked geometry).
51
+ const COLLIDER_EXCLUDED_KINDS = new Set(['zone', 'spawn', 'ceiling', 'roof', 'door', 'window']);
52
+ const colliderMaterial = new MeshBasicMaterial({ visible: false });
53
+ const keyboardMap = [
54
+ { name: 'forward', keys: ['ArrowUp', 'KeyW'] },
55
+ { name: 'backward', keys: ['ArrowDown', 'KeyS'] },
56
+ { name: 'leftward', keys: ['ArrowLeft', 'KeyA'] },
57
+ { name: 'rightward', keys: ['ArrowRight', 'KeyD'] },
58
+ { name: 'jump', keys: ['Space'] },
59
+ { name: 'run', keys: ['ShiftLeft', 'ShiftRight'] },
60
+ ];
61
+ const cameraOffset = new Vector3();
62
+ const cameraEuler = new Euler(0, 0, 0, 'YXZ');
63
+ const spawnQuat = new Quaternion();
64
+ const spawnEuler = new Euler(0, 0, 0, 'YXZ');
65
+ const spawnPos = new Vector3();
66
+ /** Effective visibility — an invisible ancestor hides the whole subtree. */
67
+ function isEffectivelyVisible(object) {
68
+ let current = object;
69
+ while (current) {
70
+ if (!current.visible)
71
+ return false;
72
+ current = current.parent;
73
+ }
74
+ return true;
75
+ }
76
+ function kindOf(object) {
77
+ let current = object;
78
+ while (current) {
79
+ const kind = current.userData.kind;
80
+ if (kind)
81
+ return kind;
82
+ current = current.parent;
83
+ }
84
+ return undefined;
85
+ }
86
+ // Coerce any position attribute (quantized/interleaved) to a plain Float32 one
87
+ // so mergeGeometries can combine geometries that don't share an array type.
88
+ function toFloat32Position(source) {
89
+ const array = new Float32Array(source.count * 3);
90
+ for (let i = 0; i < source.count; i++) {
91
+ array[i * 3] = source.getX(i);
92
+ array[i * 3 + 1] = source.getY(i);
93
+ array[i * 3 + 2] = source.getZ(i);
94
+ }
95
+ return new Float32BufferAttribute(array, 3);
96
+ }
97
+ const FALLBACK_THICKNESS = 0.08;
98
+ const GROUND_MIN = 2000;
99
+ /** A thin position-only floor box whose top face sits at `topY`. */
100
+ function boxFloorGeometry(cx, topY, cz, width, depth) {
101
+ const box = new BoxGeometry(width, FALLBACK_THICKNESS, depth).toNonIndexed();
102
+ const geometry = new BufferGeometry();
103
+ geometry.setAttribute('position', box.getAttribute('position').clone());
104
+ box.dispose();
105
+ geometry.applyMatrix4(new Matrix4().makeTranslation(cx, topY - FALLBACK_THICKNESS / 2, cz));
106
+ return geometry;
107
+ }
108
+ // Fallback ground so the player never falls into the void: a single large
109
+ // ground plane at the lowest level (level 0). Upper levels rely on their own
110
+ // baked slabs — a slab-less upper floor lets you fall down to the ground, which
111
+ // the ground plane catches. (Mirrors the editor walkthrough's site ground.)
112
+ function addFallbackFloors(scene, geometries) {
113
+ const sceneBounds = new Box3();
114
+ for (const geometry of geometries) {
115
+ geometry.computeBoundingBox();
116
+ if (geometry.boundingBox)
117
+ sceneBounds.union(geometry.boundingBox);
118
+ }
119
+ const center = new Vector3();
120
+ const levelPos = new Vector3();
121
+ let lowestLevelY = Number.POSITIVE_INFINITY;
122
+ scene.traverse((object) => {
123
+ if (object.userData.kind !== 'level')
124
+ return;
125
+ object.updateWorldMatrix(true, false);
126
+ object.getWorldPosition(levelPos);
127
+ lowestLevelY = Math.min(lowestLevelY, levelPos.y);
128
+ });
129
+ if (sceneBounds.isEmpty())
130
+ return;
131
+ sceneBounds.getCenter(center);
132
+ const groundY = Number.isFinite(lowestLevelY) ? lowestLevelY : sceneBounds.min.y;
133
+ geometries.push(boxFloorGeometry(center.x, groundY, center.z, GROUND_MIN, GROUND_MIN));
134
+ }
135
+ /** Merge the baked GLB's walkable/blocking meshes into one BVH collider. */
136
+ function buildGlbColliderWorld(scene) {
137
+ scene.updateWorldMatrix(true, true);
138
+ const geometries = [];
139
+ scene.traverse((object) => {
140
+ const mesh = object;
141
+ if (!mesh.isMesh)
142
+ return;
143
+ // Zone fills live on a separate layer (and never collide).
144
+ if (!mesh.layers.isEnabled(SCENE_LAYER))
145
+ return;
146
+ if (!isEffectivelyVisible(mesh))
147
+ return;
148
+ const kind = kindOf(mesh);
149
+ if (kind && COLLIDER_EXCLUDED_KINDS.has(kind))
150
+ return;
151
+ const position = mesh.geometry?.getAttribute('position');
152
+ if (!position || position.count < 3)
153
+ return;
154
+ const geometry = new BufferGeometry();
155
+ const source = mesh.geometry.index ? mesh.geometry.toNonIndexed() : mesh.geometry;
156
+ geometry.setAttribute('position', toFloat32Position(source.getAttribute('position')));
157
+ if (mesh.geometry.index)
158
+ source.dispose();
159
+ geometry.applyMatrix4(mesh.matrixWorld);
160
+ geometries.push(geometry);
161
+ });
162
+ if (geometries.length === 0)
163
+ return null;
164
+ addFallbackFloors(scene, geometries);
165
+ const merged = mergeGeometries(geometries, false);
166
+ for (const geometry of geometries)
167
+ geometry.dispose();
168
+ if (!merged || merged.getAttribute('position') == null) {
169
+ merged?.dispose();
170
+ return null;
171
+ }
172
+ ;
173
+ merged.computeBoundsTree = computeBoundsTree;
174
+ merged.disposeBoundsTree = disposeBoundsTree;
175
+ merged.computeBoundsTree({ maxLeafSize: 12, strategy: 0 });
176
+ merged.computeBoundingBox();
177
+ const mesh = new Mesh(merged, colliderMaterial);
178
+ mesh.raycast = acceleratedRaycast;
179
+ mesh.visible = true;
180
+ mesh.userData = {
181
+ type: 'STATIC',
182
+ friction: 0.8,
183
+ restitution: 0.05,
184
+ excludeFloatHit: false,
185
+ excludeCollisionCheck: false,
186
+ };
187
+ mesh.updateMatrixWorld(true);
188
+ return {
189
+ mesh,
190
+ minY: merged.boundingBox?.min.y ?? 0,
191
+ dispose: () => {
192
+ ;
193
+ merged.disposeBoundsTree?.();
194
+ merged.dispose();
195
+ },
196
+ };
197
+ }
198
+ /** The baked spawn marker's eye position + yaw, if the artifact carries one. */
199
+ function resolveGlbSpawn(scene) {
200
+ let spawn = null;
201
+ scene.traverse((object) => {
202
+ if (object.userData.kind === 'spawn')
203
+ spawn = object;
204
+ });
205
+ if (!spawn)
206
+ return null;
207
+ const node = spawn;
208
+ node.updateWorldMatrix(true, false);
209
+ node.getWorldPosition(spawnPos);
210
+ node.getWorldQuaternion(spawnQuat);
211
+ spawnEuler.setFromQuaternion(spawnQuat, 'YXZ');
212
+ return {
213
+ position: [spawnPos.x, spawnPos.y + SPAWN_EYE_HEIGHT, spawnPos.z],
214
+ yaw: spawnEuler.y,
215
+ };
216
+ }
217
+ /**
218
+ * First-person walkthrough controller for the baked GLB. Reuses the editor's
219
+ * `BVHEcctrl` capsule character controller (gravity, jump, sprint, ground-float,
220
+ * mesh collision) fed a collider built from the artifact's own geometry — so the
221
+ * baked viewer walks the building with the same physics as the editor, without
222
+ * the parametric scene. Pointer-lock drives look; WASD moves; Space jumps; Shift
223
+ * sprints. Door/window interaction stays in `GlbScene` (its centre-ray HUD).
224
+ */
225
+ export function GlbWalkthroughController({ url }) {
226
+ const { camera, gl } = useThree();
227
+ const gltf = useGLTFKTX2(url);
228
+ const worldRef = useRef(null);
229
+ const controllerRef = useRef(null);
230
+ const yawRef = useRef(0);
231
+ const pitchRef = useRef(0);
232
+ const crouchKeyRef = useRef(false);
233
+ const suspendRef = useRef(false);
234
+ const eyeOffsetRef = useRef(CAMERA_EYE_OFFSET);
235
+ const [crouched, setCrouched] = useState(false);
236
+ const [start, setStart] = useState(null);
237
+ const [world, setWorld] = useState(null);
238
+ // Build the collider on the first frame (priority after GlbScene's level loop,
239
+ // which snaps the floors to their stacked world positions in walkthrough) so it
240
+ // matches the rendered building rather than a mid-lerp / exploded layout.
241
+ const builtRef = useRef(false);
242
+ useFrame(() => {
243
+ if (builtRef.current)
244
+ return;
245
+ builtRef.current = true;
246
+ setWorld(buildGlbColliderWorld(gltf.scene));
247
+ }, 6);
248
+ // First-person needs a perspective camera — an orthographic projection has no
249
+ // foreshortening and makes the walkthrough unusable. Force perspective while
250
+ // walking and restore the prior projection on exit.
251
+ useEffect(() => {
252
+ const prevMode = useViewer.getState().cameraMode;
253
+ if (prevMode === 'orthographic')
254
+ useViewer.getState().setCameraMode('perspective');
255
+ return () => {
256
+ if (prevMode === 'orthographic')
257
+ useViewer.getState().setCameraMode('orthographic');
258
+ };
259
+ }, []);
260
+ // Widen FOV while walking; the baked walkthrough rides the default 50° orbit
261
+ // camera, which feels cramped on foot. Keyed on `camera` so it re-applies if
262
+ // the instance swaps (e.g. the ortho→perspective switch above), restoring the
263
+ // prior FOV on exit.
264
+ useEffect(() => {
265
+ const cam = camera;
266
+ if (!cam.isPerspectiveCamera)
267
+ return;
268
+ const prevFov = cam.fov;
269
+ cam.fov = WALKTHROUGH_FOV;
270
+ cam.updateProjectionMatrix();
271
+ return () => {
272
+ cam.fov = prevFov;
273
+ cam.updateProjectionMatrix();
274
+ };
275
+ }, [camera]);
276
+ useEffect(() => {
277
+ worldRef.current = world;
278
+ if (world) {
279
+ const triangles = world.mesh.geometry.getAttribute('position').count / 3;
280
+ console.warn('[glb-walkthrough] collider built', {
281
+ triangles,
282
+ minY: world.minY,
283
+ hasBoundsTree: !!world.mesh.geometry.boundsTree,
284
+ spawn: resolveGlbSpawn(gltf.scene),
285
+ });
286
+ }
287
+ else {
288
+ console.warn('[glb-walkthrough] NO collider world (no eligible meshes)');
289
+ }
290
+ return () => {
291
+ world?.dispose();
292
+ worldRef.current = null;
293
+ };
294
+ }, [world, gltf.scene]);
295
+ // Resolve the spawn once the collider exists; the capsule centre sits below
296
+ // the eye, then the controller floats it onto the ground.
297
+ useEffect(() => {
298
+ if (!world || start)
299
+ return;
300
+ const spawn = resolveGlbSpawn(gltf.scene);
301
+ const eye = spawn?.position ?? [0, SPAWN_EYE_HEIGHT, 0];
302
+ yawRef.current = spawn?.yaw ?? 0;
303
+ pitchRef.current = 0;
304
+ setStart({ position: [eye[0], eye[1] - CONTROLLER_CENTER_FROM_EYE, eye[2]] });
305
+ }, [world, start, gltf.scene]);
306
+ // Pointer-lock look + click-to-lock fallback + Esc/unlock to exit. Once the
307
+ // pointer has been locked, releasing it (Esc — the browser swallows that
308
+ // keydown, so we can't rely on it; or any other unlock) leaves the walkthrough
309
+ // in a single press rather than just freeing the cursor.
310
+ useEffect(() => {
311
+ const canvas = gl.domElement;
312
+ let wasLocked = false;
313
+ const onMouseMove = (event) => {
314
+ if (document.pointerLockElement !== canvas)
315
+ return;
316
+ yawRef.current -= event.movementX * LOOK_SENSITIVITY;
317
+ pitchRef.current = Math.max(-(Math.PI / 2 - 0.05), Math.min(Math.PI / 2 - 0.05, pitchRef.current - event.movementY * LOOK_SENSITIVITY));
318
+ };
319
+ const onClick = () => {
320
+ if (document.pointerLockElement !== canvas)
321
+ canvas.requestPointerLock?.();
322
+ };
323
+ const onKeyDown = (event) => {
324
+ // When locked, the browser intercepts Esc to release the pointer and the
325
+ // pointerlockchange handler below exits; this only covers Esc while the
326
+ // pointer is already free (e.g. lock never engaged).
327
+ if (event.code === 'Escape' && document.pointerLockElement !== canvas) {
328
+ useViewer.getState().setWalkthroughMode(false);
329
+ }
330
+ // P toggles a cursor pause (advertised in the HUD): frees the pointer
331
+ // without leaving the walkthrough — e.g. for an OS screenshot, which
332
+ // needs a movable cursor — and click or P resumes.
333
+ if (event.code === 'KeyP') {
334
+ if (document.pointerLockElement === canvas) {
335
+ suspendRef.current = true;
336
+ useViewer.getState().setWalkthroughSuspended(true);
337
+ document.exitPointerLock();
338
+ }
339
+ else if (suspendRef.current) {
340
+ const result = canvas.requestPointerLock?.();
341
+ if (result && typeof result.catch === 'function')
342
+ result.catch(() => { });
343
+ }
344
+ }
345
+ // While paused (P), crouch is frozen as-is — ⌃⇧⌘4 (clipboard screenshot)
346
+ // must not toggle it under the user.
347
+ if ((event.code === 'ControlLeft' || event.code === 'ControlRight') && !suspendRef.current) {
348
+ crouchKeyRef.current = true;
349
+ }
350
+ };
351
+ const onKeyUp = (event) => {
352
+ if ((event.code === 'ControlLeft' || event.code === 'ControlRight') && !suspendRef.current) {
353
+ crouchKeyRef.current = false;
354
+ }
355
+ };
356
+ const onBlur = () => {
357
+ if (!suspendRef.current)
358
+ crouchKeyRef.current = false;
359
+ };
360
+ const onPointerLockChange = () => {
361
+ if (document.pointerLockElement === canvas) {
362
+ wasLocked = true;
363
+ suspendRef.current = false;
364
+ useViewer.getState().setWalkthroughSuspended(false);
365
+ }
366
+ else if (suspendRef.current) {
367
+ // Deliberately released (screenshot pause) — stay in walkthrough.
368
+ }
369
+ else if (wasLocked) {
370
+ useViewer.getState().setWalkthroughMode(false);
371
+ }
372
+ };
373
+ document.addEventListener('mousemove', onMouseMove);
374
+ canvas.addEventListener('click', onClick);
375
+ document.addEventListener('keydown', onKeyDown);
376
+ document.addEventListener('keyup', onKeyUp);
377
+ window.addEventListener('blur', onBlur);
378
+ document.addEventListener('pointerlockchange', onPointerLockChange);
379
+ return () => {
380
+ document.removeEventListener('mousemove', onMouseMove);
381
+ canvas.removeEventListener('click', onClick);
382
+ document.removeEventListener('keydown', onKeyDown);
383
+ document.removeEventListener('keyup', onKeyUp);
384
+ window.removeEventListener('blur', onBlur);
385
+ document.removeEventListener('pointerlockchange', onPointerLockChange);
386
+ useViewer.getState().setWalkthroughSuspended(false);
387
+ if (document.pointerLockElement === canvas)
388
+ document.exitPointerLock();
389
+ };
390
+ }, [gl]);
391
+ // Lock the pointer the moment the walkthrough is ready, so the user doesn't
392
+ // have to click the canvas first. The walkthrough toggle is itself a user
393
+ // gesture; if the browser still rejects the request (no transient activation
394
+ // left), the click-to-lock fallback above covers it.
395
+ useEffect(() => {
396
+ if (!(world && start))
397
+ return;
398
+ const canvas = gl.domElement;
399
+ if (document.pointerLockElement === canvas)
400
+ return;
401
+ const result = canvas.requestPointerLock?.();
402
+ if (result && typeof result.catch === 'function')
403
+ result.catch(() => { });
404
+ }, [gl, world, start]);
405
+ const setControllerApi = useCallback((api) => {
406
+ controllerRef.current = api;
407
+ }, []);
408
+ const hasStandingClearance = useCallback((position) => {
409
+ const mesh = worldRef.current?.mesh;
410
+ if (!mesh)
411
+ return true;
412
+ standClearanceRaycaster.set(position, UP);
413
+ standClearanceRaycaster.far = STAND_CLEARANCE;
414
+ return standClearanceRaycaster.intersectObject(mesh, false).length === 0;
415
+ }, []);
416
+ // Drive the camera from the capsule each frame + respawn if it falls into void.
417
+ useFrame((_, delta) => {
418
+ const group = controllerRef.current?.group;
419
+ if (!group)
420
+ return;
421
+ if (start && world && group.position.y < world.minY - VOID_FALL_RESPAWN_DEPTH) {
422
+ group.position.set(start.position[0], start.position[1], start.position[2]);
423
+ controllerRef.current?.resetLinVel();
424
+ }
425
+ // Crouch follows the held key; standing back up waits for headroom.
426
+ // Frozen while the cursor pause is active.
427
+ if (!suspendRef.current && crouchKeyRef.current !== crouched) {
428
+ if (crouchKeyRef.current)
429
+ setCrouched(true);
430
+ else if (hasStandingClearance(group.position))
431
+ setCrouched(false);
432
+ }
433
+ const targetEyeOffset = crouched ? CROUCH_EYE_OFFSET : CAMERA_EYE_OFFSET;
434
+ eyeOffsetRef.current +=
435
+ (targetEyeOffset - eyeOffsetRef.current) * Math.min(1, delta * EYE_LERP_SPEED);
436
+ group.rotation.y = 0;
437
+ camera.position.copy(group.position).add(cameraOffset.set(0, eyeOffsetRef.current, 0));
438
+ cameraEuler.set(pitchRef.current, yawRef.current, 0, 'YXZ');
439
+ camera.quaternion.setFromEuler(cameraEuler);
440
+ camera.updateMatrixWorld(true);
441
+ }, 2.5);
442
+ if (!(world && start))
443
+ return null;
444
+ return (_jsx(KeyboardControls, { map: keyboardMap, children: _jsx(BVHEcctrl, { acceleration: 26, airDragFactor: 0.3, colliderCapsuleArgs: crouched ? CROUCH_CAPSULE : STAND_CAPSULE, colliderMeshes: [world.mesh], collisionCheckIteration: 3, collisionPushBackDamping: 0.1, collisionPushBackThreshold: 0.001, deceleration: 30, delay: 0, fallGravityFactor: 4, floatCheckType: "BOTH", floatDampingC: 36, floatHeight: crouched ? CROUCH_FLOAT_HEIGHT : STAND_FLOAT_HEIGHT, floatPullBackHeight: 0.35, floatSensorRadius: 0.15, floatSpringK: 1200, gravity: 9.81, jumpVel: 5, maxRunSpeed: crouched ? CROUCH_RUN_SPEED : 5, maxSlope: 1.2, maxWalkSpeed: crouched ? CROUCH_WALK_SPEED : 2, position: start.position, ref: setControllerApi }) }));
445
+ }
@@ -1,11 +1,12 @@
1
1
  import { type AnyNodeId } from '@pascal-app/core';
2
- import { type ThreeToJSXElements } from '@react-three/fiber';
2
+ import { type ThreeElement } from '@react-three/fiber';
3
3
  import * as THREE from 'three/webgpu';
4
4
  import type { ColorPreset, RenderShading } from '../../lib/materials';
5
5
  import { type RenderContext } from '../../store/use-viewer';
6
6
  import { type HoverStyles } from './post-processing';
7
7
  declare module '@react-three/fiber' {
8
- interface ThreeElements extends ThreeToJSXElements<typeof THREE> {
8
+ interface ThreeElements {
9
+ lineBasicNodeMaterial: ThreeElement<typeof THREE.LineBasicNodeMaterial>;
9
10
  }
10
11
  }
11
12
  interface ViewerProps {
@@ -15,6 +16,7 @@ interface ViewerProps {
15
16
  perf?: boolean;
16
17
  useBvh?: boolean;
17
18
  renderContext?: RenderContext;
19
+ transparent?: boolean;
18
20
  defaultRender?: {
19
21
  shading?: RenderShading;
20
22
  textures?: boolean;
@@ -37,6 +39,21 @@ interface ViewerProps {
37
39
  */
38
40
  sceneReadyKey?: string | number | null;
39
41
  onSceneReadyChange?: (ready: boolean) => void;
42
+ /**
43
+ * Wall-clock give-up cap for scene readiness, replacing the default
44
+ * frame-count cap. Set it on hosts whose frame cadence is decoupled from
45
+ * real time (the headless bake page's timer-driven loop runs the default
46
+ * 180-frame cap in ~3.6s — shorter than a cold item-model download).
47
+ */
48
+ sceneReadyMaxWaitMs?: number;
49
+ /**
50
+ * Skip the TSL post-processing pipeline (SSGI/denoise/ink/outline) and render
51
+ * the scene directly. For headless/capture surfaces (the bake page) where
52
+ * frame quality is irrelevant: on a software-rasterised worker the pipeline
53
+ * consumes the whole CPU budget and bakes time out. Equivalent to the
54
+ * `?disable=postFx` diagnostic URL flag, but host-controlled.
55
+ */
56
+ disablePostFx?: boolean;
40
57
  }
41
58
  /** Imperative handle exposed via `ref` on `<Viewer>`. */
42
59
  export type ViewerHandle = {
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/index.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,SAAS,EAKf,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAkB,KAAK,kBAAkB,EAAsB,MAAM,oBAAoB,CAAA;AAEhG,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AAIrC,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAErE,OAAkB,EAAE,KAAK,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAQtE,OAAuB,EAAwB,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAM1F,OAAO,QAAQ,oBAAoB,CAAC;IAClC,UAAU,aAAc,SAAQ,kBAAkB,CAAC,OAAO,KAAK,CAAC;KAAG;CACpE;AAuOD,UAAU,WAAW;IACnB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC1B,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB,gBAAgB,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAA;IACvC,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,aAAa,CAAC,EAAE,aAAa,CAAA;IAC7B,aAAa,CAAC,EAAE;QACd,OAAO,CAAC,EAAE,aAAa,CAAA;QACvB,QAAQ,CAAC,EAAE,OAAO,CAAA;QAClB,WAAW,CAAC,EAAE,WAAW,CAAA;KAC1B,CAAA;IACD;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,SAAS,EAAE,GAAG,IAAI,CAAA;IAC5B;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;IACtC,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;CAC9C;AAED,yDAAyD;AACzD,MAAM,MAAM,YAAY,GAAG;IACzB;;;;;OAKG;IACH,WAAW,CAAC,GAAG,EAAE,SAAS,EAAE,GAAG,IAAI,GAAG,IAAI,CAAA;CAC3C,CAAA;AAED,QAAA,MAAM,MAAM,sGAmKV,CAAA;AAkBF,eAAe,MAAM,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/index.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,SAAS,EAKf,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAkB,KAAK,YAAY,EAAsB,MAAM,oBAAoB,CAAA;AAS1F,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AAKrC,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAGrE,OAAkB,EAAE,KAAK,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAQtE,OAAuB,EAAwB,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAU1F,OAAO,QAAQ,oBAAoB,CAAC;IAOlC,UAAU,aAAa;QACrB,qBAAqB,EAAE,YAAY,CAAC,OAAO,KAAK,CAAC,qBAAqB,CAAC,CAAA;KACxE;CACF;AAuSD,UAAU,WAAW;IACnB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC1B,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB,gBAAgB,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAA;IACvC,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,aAAa,CAAC,EAAE,aAAa,CAAA;IAC7B,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,aAAa,CAAC,EAAE;QACd,OAAO,CAAC,EAAE,aAAa,CAAA;QACvB,QAAQ,CAAC,EAAE,OAAO,CAAA;QAClB,WAAW,CAAC,EAAE,WAAW,CAAA;KAC1B,CAAA;IACD;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,SAAS,EAAE,GAAG,IAAI,CAAA;IAC5B;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;IACtC,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;IAC7C;;;;;OAKG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,OAAO,CAAA;CACxB;AAED,yDAAyD;AACzD,MAAM,MAAM,YAAY,GAAG;IACzB;;;;;OAKG;IACH,WAAW,CAAC,GAAG,EAAE,SAAS,EAAE,GAAG,IAAI,GAAG,IAAI,CAAA;CAC3C,CAAA;AAED,QAAA,MAAM,MAAM,sGA4NV,CAAA;AAkBF,eAAe,MAAM,CAAA"}