@terraware/web-components 5.0.27-rc.0 → 5.0.27

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 (295) hide show
  1. package/components/AntSwitch/index.js +22 -19
  2. package/components/Autocomplete/Autocomplete.js +130 -106
  3. package/components/Badge/index.js +29 -28
  4. package/components/BusySpinner/index.js +45 -40
  5. package/components/Button/Button.js +47 -41
  6. package/components/Checkbox.js +63 -64
  7. package/components/Confirm/index.js +83 -74
  8. package/components/DatePicker/DatePicker.js +141 -138
  9. package/components/DialogBox/DialogBox.js +118 -111
  10. package/components/Divisor.js +18 -18
  11. package/components/Dropdown.js +94 -66
  12. package/components/EditableTable/index.js +271 -211
  13. package/components/ErrorBox/ErrorBox.js +49 -47
  14. package/components/FileChooser/index.js +135 -117
  15. package/components/FormBottomBar/index.js +84 -75
  16. package/components/Icon/Icon.js +20 -12
  17. package/components/Icon/icons/index.js +256 -256
  18. package/components/IconTooltip/index.js +60 -49
  19. package/components/Map/MapBox.js +452 -545
  20. package/components/Map/MapContainer.js +29 -25
  21. package/components/Map/MapDateSliderControl.js +64 -64
  22. package/components/Map/MapDrawer.js +47 -41
  23. package/components/Map/MapLegend.js +172 -176
  24. package/components/Map/MapViewStyleControl.js +63 -59
  25. package/components/Map/ReactMapboxWrapper.js +2 -3
  26. package/components/Map/index.js +180 -161
  27. package/components/Map/types.js +17 -12
  28. package/components/Map/useMaintainLayerOrder.js +26 -16
  29. package/components/Markdown/index.js +26 -25
  30. package/components/Message/Message.js +76 -73
  31. package/components/MultiSelect/index.js +124 -112
  32. package/components/Navbar/NavFooter.js +13 -9
  33. package/components/Navbar/NavItem.js +76 -70
  34. package/components/Navbar/NavSection.js +19 -17
  35. package/components/Navbar/Navbar.js +46 -39
  36. package/components/Navbar/SubNavbar.js +13 -9
  37. package/components/Note.js +22 -18
  38. package/components/OverlayModal/OverlayModal.js +63 -56
  39. package/components/PageForm/index.js +41 -33
  40. package/components/PhotoChooser/index.js +173 -165
  41. package/components/PhotosCarousel/PhotoSlide.js +35 -32
  42. package/components/PhotosCarousel/index.js +85 -80
  43. package/components/PhotosCarousel/usePhotosCarousel.js +76 -69
  44. package/components/Pill/index.js +45 -41
  45. package/components/PillList/index.js +24 -17
  46. package/components/PlacementWrapper/index.js +59 -38
  47. package/components/PopoverMenu/Popover.js +85 -68
  48. package/components/PopoverMenu/index.js +56 -51
  49. package/components/PopoverMultiSelect/index.js +59 -52
  50. package/components/ProgressCircle/ProgressCircle.js +32 -28
  51. package/components/RadioButton.js +19 -17
  52. package/components/Select/Select.js +15 -13
  53. package/components/Select/SelectT.js +250 -235
  54. package/components/Separator/index.js +17 -14
  55. package/components/Size.js +1 -0
  56. package/components/Slider/index.js +56 -43
  57. package/components/SummaryBox.js +72 -70
  58. package/components/Tabs/index.js +135 -133
  59. package/components/TextTruncated/index.js +130 -117
  60. package/components/Textfield/Textfield.js +190 -158
  61. package/components/Textfield/TruncatedTextArea.js +84 -70
  62. package/components/TimelineSlider/TimelineMarker.js +30 -25
  63. package/components/TimelineSlider/TimelineRail.js +11 -8
  64. package/components/TimelineSlider/index.js +66 -64
  65. package/components/TimelineSliderV2/ClusterBadge.js +29 -25
  66. package/components/TimelineSliderV2/TimelineDot.js +35 -31
  67. package/components/TimelineSliderV2/TimelineRail.js +11 -8
  68. package/components/TimelineSliderV2/index.js +103 -121
  69. package/components/TimelineSliderV2/layout.js +147 -124
  70. package/components/TimelineSliderV2/types.js +1 -0
  71. package/components/Tooltip/Tooltip.js +56 -47
  72. package/components/ViewPhotosDialog/index.js +35 -28
  73. package/components/VirtualWalkthrough/Annotation.js +207 -163
  74. package/components/VirtualWalkthrough/AnnotationEditPane.js +184 -198
  75. package/components/VirtualWalkthrough/AnnotationPanel.js +199 -187
  76. package/components/VirtualWalkthrough/Application.js +34 -31
  77. package/components/VirtualWalkthrough/AutoRotator.js +223 -85
  78. package/components/VirtualWalkthrough/BlockingSpinner.js +28 -26
  79. package/components/VirtualWalkthrough/BoundaryRing.js +34 -30
  80. package/components/VirtualWalkthrough/BoundaryWall.js +36 -32
  81. package/components/VirtualWalkthrough/CameraInfo.js +89 -100
  82. package/components/VirtualWalkthrough/ControlsInfoPane.js +195 -254
  83. package/components/VirtualWalkthrough/GradientSky.js +73 -72
  84. package/components/VirtualWalkthrough/SplatControls.js +221 -209
  85. package/components/VirtualWalkthrough/SplatCrop.js +20 -21
  86. package/components/VirtualWalkthrough/SplatFadeCrop.js +20 -21
  87. package/components/VirtualWalkthrough/SplatModel.js +85 -65
  88. package/components/VirtualWalkthrough/SplatRevealRain.js +46 -41
  89. package/components/VirtualWalkthrough/TfAnnotationManager.js +379 -197
  90. package/components/VirtualWalkthrough/TfXrNavigation.js +162 -73
  91. package/components/VirtualWalkthrough/VirtualWalkthroughViewer.js +431 -438
  92. package/components/VirtualWalkthrough/VrAnnotationPanel.js +22 -18
  93. package/components/VirtualWalkthrough/XrAnnotationInteraction.js +17 -14
  94. package/components/VirtualWalkthrough/XrExitButton.js +17 -14
  95. package/components/VirtualWalkthrough/XrGazeDwell.js +17 -14
  96. package/components/VirtualWalkthrough/XrPointerRay.js +17 -14
  97. package/components/VirtualWalkthrough/XrStartPose.js +121 -67
  98. package/components/VirtualWalkthrough/boundary-ring.js +117 -87
  99. package/components/VirtualWalkthrough/boundary-wall.js +258 -175
  100. package/components/VirtualWalkthrough/camera-adoption.js +64 -18
  101. package/components/VirtualWalkthrough/groundPlane.js +47 -32
  102. package/components/VirtualWalkthrough/gsplat-fade-crop-effect.js +35 -35
  103. package/components/VirtualWalkthrough/splatFormat.js +47 -13
  104. package/components/VirtualWalkthrough/useAdoptedCamera.js +44 -23
  105. package/components/VirtualWalkthrough/useRestartOnVr.js +37 -26
  106. package/components/VirtualWalkthrough/useXrRenderTuning.js +54 -33
  107. package/components/VirtualWalkthrough/vr-annotation-panel-layout.js +99 -67
  108. package/components/VirtualWalkthrough/vr-annotation-panel.js +433 -403
  109. package/components/VirtualWalkthrough/vr-annotation-scroll.js +28 -10
  110. package/components/VirtualWalkthrough/walkthrough-camera.js +329 -204
  111. package/components/VirtualWalkthrough/xr-annotation-candidate-buffer.js +80 -39
  112. package/components/VirtualWalkthrough/xr-annotation-candidates.js +18 -6
  113. package/components/VirtualWalkthrough/xr-annotation-interaction.js +56 -31
  114. package/components/VirtualWalkthrough/xr-annotation-targeting.js +38 -22
  115. package/components/VirtualWalkthrough/xr-button-arming.js +17 -7
  116. package/components/VirtualWalkthrough/xr-button-hold.js +36 -22
  117. package/components/VirtualWalkthrough/xr-exit-button-mask.js +36 -22
  118. package/components/VirtualWalkthrough/xr-exit-button.js +207 -148
  119. package/components/VirtualWalkthrough/xr-face-buttons.js +29 -19
  120. package/components/VirtualWalkthrough/xr-gaze-dwell-state.js +34 -23
  121. package/components/VirtualWalkthrough/xr-gaze-dwell.js +144 -107
  122. package/components/VirtualWalkthrough/xr-interactive-ui.js +24 -12
  123. package/components/VirtualWalkthrough/xr-pointer-ray.js +20 -13
  124. package/components/VirtualWalkthrough/xr-progress-pie-material.js +88 -117
  125. package/components/VirtualWalkthrough/xr-progress-pie.js +10 -1
  126. package/components/VirtualWalkthrough/xr-ray-quad.js +30 -16
  127. package/components/VirtualWalkthrough/xr-render-tuning.js +48 -17
  128. package/components/VirtualWalkthrough/xr-scene-bounds.js +37 -21
  129. package/components/VirtualWalkthrough/xr-start-pose.js +73 -27
  130. package/components/VirtualWalkthrough/xr-teleport-gesture.js +42 -16
  131. package/components/svg/BlobbyGrayIconImage.js +39 -41
  132. package/components/svg/BlobbyGrayIconImport.js +39 -41
  133. package/components/svg/BlobbyGrayIconUploadToTheCloud.js +39 -41
  134. package/components/svg/BlobbyIconCloudChecked.js +164 -202
  135. package/components/svg/BlobbyIconExportOrUploadPhoto.js +164 -202
  136. package/components/svg/BlobbyIconFolder.js +164 -202
  137. package/components/svg/BlobbyIconGraphReport.js +164 -202
  138. package/components/svg/BlobbyIconHappy.js +164 -202
  139. package/components/svg/BlobbyIconHeartMonitor.js +164 -202
  140. package/components/svg/BlobbyIconLeaf.js +164 -202
  141. package/components/svg/BlobbyIconLibrary.js +164 -202
  142. package/components/svg/BlobbyIconNursery.js +166 -204
  143. package/components/svg/BlobbyIconOrganization.js +164 -202
  144. package/components/svg/BlobbyIconParchment.js +164 -202
  145. package/components/svg/BlobbyIconPeople.js +164 -202
  146. package/components/svg/BlobbyIconSeedBank.js +164 -202
  147. package/components/svg/BlobbyIconSeedling.js +164 -202
  148. package/components/svg/BlobbyIconSeeds.js +164 -202
  149. package/components/svg/BlobbyIconSite.js +167 -206
  150. package/components/svg/BlobbyIconSparkles.js +164 -202
  151. package/components/svg/BlobbyIconWrench.js +164 -202
  152. package/components/svg/Bug.js +21 -19
  153. package/components/svg/Calendar.js +21 -19
  154. package/components/svg/CaretDown.js +21 -19
  155. package/components/svg/CaretLeft.js +21 -19
  156. package/components/svg/CaretRight.js +21 -19
  157. package/components/svg/CaretUp.js +21 -19
  158. package/components/svg/ChevronDown.js +21 -19
  159. package/components/svg/ChevronUp.js +21 -19
  160. package/components/svg/Close.js +21 -19
  161. package/components/svg/Critical.js +21 -19
  162. package/components/svg/Edit.js +164 -202
  163. package/components/svg/Error.js +21 -19
  164. package/components/svg/Filter.js +21 -19
  165. package/components/svg/Folder.js +21 -19
  166. package/components/svg/Help.js +21 -19
  167. package/components/svg/Home.js +21 -19
  168. package/components/svg/IconAdd.js +21 -19
  169. package/components/svg/IconArrowRight.js +23 -21
  170. package/components/svg/IconBusinessNetwork.js +21 -19
  171. package/components/svg/IconCalendar.js +21 -19
  172. package/components/svg/IconCancel.js +21 -19
  173. package/components/svg/IconChargingBattery.js +21 -19
  174. package/components/svg/IconChecklist.js +23 -21
  175. package/components/svg/IconCheckmark.js +21 -19
  176. package/components/svg/IconCoinInHand.js +21 -19
  177. package/components/svg/IconColumns.js +21 -19
  178. package/components/svg/IconComment.js +22 -20
  179. package/components/svg/IconDashboard.js +21 -19
  180. package/components/svg/IconDataMigration.js +21 -19
  181. package/components/svg/IconDocument.js +23 -21
  182. package/components/svg/IconDownloadFromTheCloud.js +23 -21
  183. package/components/svg/IconEdit.js +23 -21
  184. package/components/svg/IconExpand.js +23 -21
  185. package/components/svg/IconExport.js +21 -19
  186. package/components/svg/IconExternalLink.js +21 -19
  187. package/components/svg/IconEye.js +21 -19
  188. package/components/svg/IconEyeOff.js +21 -19
  189. package/components/svg/IconFile.js +21 -19
  190. package/components/svg/IconFolder.js +21 -19
  191. package/components/svg/IconFullScreen.js +22 -20
  192. package/components/svg/IconFutures.js +21 -19
  193. package/components/svg/IconGraphReport.js +21 -19
  194. package/components/svg/IconGrid.js +21 -19
  195. package/components/svg/IconHeartMonitor.js +21 -19
  196. package/components/svg/IconHelp.js +23 -21
  197. package/components/svg/IconHistory.js +21 -19
  198. package/components/svg/IconImport.js +21 -19
  199. package/components/svg/IconIndex.js +21 -19
  200. package/components/svg/IconLayers.js +21 -19
  201. package/components/svg/IconLibrary.js +22 -20
  202. package/components/svg/IconList.js +21 -19
  203. package/components/svg/IconLivePlant.js +21 -19
  204. package/components/svg/IconLocations.js +21 -19
  205. package/components/svg/IconMail.js +21 -19
  206. package/components/svg/IconManager.js +21 -19
  207. package/components/svg/IconMap.js +21 -19
  208. package/components/svg/IconMarker.js +21 -19
  209. package/components/svg/IconMenu.js +21 -19
  210. package/components/svg/IconMenuHorizontal.js +21 -19
  211. package/components/svg/IconModule.js +21 -19
  212. package/components/svg/IconMyLocation.js +21 -19
  213. package/components/svg/IconNote.js +23 -21
  214. package/components/svg/IconNursery.js +23 -21
  215. package/components/svg/IconOrg.js +21 -19
  216. package/components/svg/IconParchment.js +21 -19
  217. package/components/svg/IconPhoto.js +21 -19
  218. package/components/svg/IconPlantsFilled.js +21 -19
  219. package/components/svg/IconRedo.js +21 -19
  220. package/components/svg/IconRestorationSite.js +21 -19
  221. package/components/svg/IconSeedBank.js +21 -19
  222. package/components/svg/IconSeedling.js +21 -19
  223. package/components/svg/IconSettings.js +21 -19
  224. package/components/svg/IconSlice.js +21 -19
  225. package/components/svg/IconSubmit.js +21 -19
  226. package/components/svg/IconSubtract.js +23 -21
  227. package/components/svg/IconSynced.js +21 -19
  228. package/components/svg/IconTrashCan.js +21 -19
  229. package/components/svg/IconTreasureMap.js +21 -19
  230. package/components/svg/IconUnavailable.js +21 -19
  231. package/components/svg/IconUndo.js +21 -19
  232. package/components/svg/IconVariable.js +21 -19
  233. package/components/svg/IconVideo.js +31 -30
  234. package/components/svg/IconWifi.js +21 -19
  235. package/components/svg/Info.js +21 -19
  236. package/components/svg/Key.js +21 -19
  237. package/components/svg/Leaf.js +21 -19
  238. package/components/svg/Lock.js +21 -19
  239. package/components/svg/Logo.js +43 -48
  240. package/components/svg/MenuVertical.js +21 -19
  241. package/components/svg/Notification.js +21 -19
  242. package/components/svg/Person.js +21 -19
  243. package/components/svg/Plus.js +21 -19
  244. package/components/svg/RestorationSite.js +21 -19
  245. package/components/svg/Search.js +21 -19
  246. package/components/svg/Seeds.js +21 -19
  247. package/components/svg/Site.js +21 -19
  248. package/components/svg/Sparkles.js +21 -19
  249. package/components/svg/Species.js +21 -19
  250. package/components/svg/Species2.js +164 -202
  251. package/components/svg/Spinner.js +48 -54
  252. package/components/svg/Star.js +21 -19
  253. package/components/svg/Success.js +21 -19
  254. package/components/svg/SuccessFilled.js +21 -19
  255. package/components/svg/TerrawareLogoDesktop.js +49 -55
  256. package/components/svg/TerrawareLogoMobile.js +26 -25
  257. package/components/svg/Touchscreen.js +21 -19
  258. package/components/svg/UploadCloud.js +164 -202
  259. package/components/svg/Warning.js +21 -19
  260. package/components/svg/WelcomeClipboard.js +133 -156
  261. package/components/svg/index.js +129 -129
  262. package/components/table/EnhancedTableToolbar.js +74 -68
  263. package/components/table/EnhancedTableToolbarV2.js +41 -31
  264. package/components/table/TableCellRenderer.js +231 -196
  265. package/components/table/TableHeader.js +73 -71
  266. package/components/table/TableHeaderItem.js +102 -97
  267. package/components/table/density.js +19 -18
  268. package/components/table/index.js +369 -350
  269. package/components/table/sort.js +52 -27
  270. package/components/table/types.js +1 -0
  271. package/components/types/index.js +1 -0
  272. package/hooks/cameraEntityContext.js +13 -2
  273. package/hooks/useBoolean.js +13 -17
  274. package/hooks/useCameraPosition.js +56 -56
  275. package/hooks/useDevicePerformance.js +17 -17
  276. package/hooks/useXr.js +87 -85
  277. package/index.js +58 -58
  278. package/package.json +1 -1
  279. package/setupTests.js +4 -0
  280. package/strings/export.js +170 -111
  281. package/strings/index.js +54 -25
  282. package/strings/locales.js +41 -21
  283. package/strings/strings-en.js +51 -49
  284. package/strings/strings-es.js +51 -49
  285. package/strings/strings-fr.js +51 -49
  286. package/strings/strings-gx.js +51 -49
  287. package/style-dictionary-dist/TerrawareTheme.js +186 -186
  288. package/theme.js +76 -74
  289. package/utils/color.js +15 -8
  290. package/utils/date.js +108 -42
  291. package/utils/index.js +4 -4
  292. package/utils/preventDefaultEvent.js +3 -3
  293. package/utils/text.js +11 -11
  294. package/utils/useDeviceInfo.js +79 -42
  295. package/virtualWalkthrough.js +23 -23
@@ -1,15 +1,33 @@
1
+ /** Deflection the stick must pass before the text moves, so a resting thumb doesn't drift it. */
1
2
  const SCROLL_DEADZONE = 0.15;
3
+
4
+ /** Canvas px per second at full deflection. */
2
5
  const SCROLL_SPEED = 1200;
3
- const readScrollAxis = (inputSource)=>{
4
- const axes = inputSource.gamepad?.axes;
5
- if ('right' !== inputSource.handedness || !axes || axes.length < 4) return 0;
6
- return axes[3];
6
+ /**
7
+ * Thumbstick Y for a source that can scroll the panel. Only the right hand scrolls: the left
8
+ * stick drives locomotion, and right-stick Y is free because snap-vertical is turned off.
9
+ * Hand-tracked sources report a gamepad with no axes, which would read as NaN.
10
+ */
11
+ const readScrollAxis = inputSource => {
12
+ const axes = inputSource.gamepad?.axes;
13
+ if (inputSource.handedness !== 'right' || !axes || axes.length < 4) {
14
+ return 0;
15
+ }
16
+ return axes[3];
7
17
  };
8
- const nextScrollY = (scrollY, axisY, dt, maxScroll)=>{
9
- const magnitude = Math.abs(axisY);
10
- if (magnitude <= SCROLL_DEADZONE) return Math.min(Math.max(scrollY, 0), maxScroll);
11
- const ramped = (magnitude - SCROLL_DEADZONE) / (1 - SCROLL_DEADZONE);
12
- const delta = -Math.sign(axisY) * ramped * SCROLL_SPEED * dt;
13
- return Math.min(Math.max(scrollY + delta, 0), maxScroll);
18
+
19
+ /**
20
+ * Advances the scroll position for one frame of stick deflection, ramping from a standstill at
21
+ * the deadzone edge. A forward push reports negative Y and moves further into the text.
22
+ */
23
+ const nextScrollY = (scrollY, axisY, dt, maxScroll) => {
24
+ const magnitude = Math.abs(axisY);
25
+ if (magnitude <= SCROLL_DEADZONE) {
26
+ return Math.min(Math.max(scrollY, 0), maxScroll);
27
+ }
28
+ const ramped = (magnitude - SCROLL_DEADZONE) / (1 - SCROLL_DEADZONE);
29
+ const delta = -Math.sign(axisY) * ramped * SCROLL_SPEED * dt;
30
+ return Math.min(Math.max(scrollY + delta, 0), maxScroll);
14
31
  };
32
+
15
33
  export { SCROLL_DEADZONE, SCROLL_SPEED, nextScrollY, readScrollAxis };
@@ -1,223 +1,348 @@
1
- import { Quat, Script, Vec3, math } from "playcanvas";
2
- import { computeGroundPlane, yOnPlane } from "./groundPlane.js";
1
+ import { Quat, Vec3, Script, math } from 'playcanvas';
2
+ import { computeGroundPlane, yOnPlane } from './groundPlane.js';
3
+
4
+ /**
5
+ * First-person walkthrough camera for Gaussian Splat scenes.
6
+ *
7
+ * - Pointer drag (mouse or touch) rotates the camera (pitch + yaw) with damping
8
+ * matching PlayCanvas CameraControls' rotateDamping=0.98.
9
+ * - WASD / arrow keys move along the XZ plane using yaw-only direction, so
10
+ * movement is always horizontal regardless of where the camera is pointing.
11
+ * - Mouse wheel moves forward/backward along the XZ plane.
12
+ * - Y position is clamped between boundsMin.y and boundsMax.y; set them equal
13
+ * to lock vertical position entirely.
14
+ * - Exposes reset() and focusPoint to stay compatible with useCameraPosition.
15
+ *
16
+ * The pose is written to the camera under this script's entity rather than to the entity itself, so
17
+ * mounting it on the walkthrough's rig drives whichever camera the rig is holding — one the viewer
18
+ * brought, or one it adopted from the host scene. Mounted straight onto a camera entity it still
19
+ * drives that entity, which `findComponent` resolves to itself.
20
+ */
21
+
3
22
  const _quat = new Quat();
4
23
  const _flatForward = new Vec3();
5
24
  const _flatRight = new Vec3();
6
- const dampRate = (damping, dt)=>1 - Math.pow(damping, 1000 * dt);
25
+
26
+ /** Frame-rate-independent damping lerp rate — matches PlayCanvas CameraControls. */
27
+ const dampRate = (damping, dt) => 1 - Math.pow(damping, dt * 1000);
28
+
29
+ /** Below this angular delta (degrees) the damped lerp snaps to its target and stops writing. */
7
30
  const ANGLE_EPSILON = 1e-3;
31
+
32
+ /** Below this positional delta (world units) an idle frame skips the transform write. */
8
33
  const POSITION_EPSILON = 1e-5;
9
34
  class WalkthroughCamera extends Script {
10
- static scriptName = 'walkthroughCamera';
11
- moveSpeed = 4.5;
12
- moveFastSpeed = 7.5;
13
- moveSlowSpeed = 2.25;
14
- lookSensitivity = 0.1;
15
- rotateDamping = 0.98;
16
- pitchMin = -85;
17
- pitchMax = 85;
18
- scrollSpeed = 0.001;
19
- boundsCenter = new Vec3(0, 0, 0);
20
- boundsRadius = 10;
21
- freeFly = false;
22
- enableFly = true;
23
- averageCameraHeight = 0;
24
- groundPlane = [];
25
- _pitch = 0;
26
- _yaw = 0;
27
- _targetPitch = 0;
28
- _targetYaw = 0;
29
- _isDragging = false;
30
- _lastX = 0;
31
- _lastY = 0;
32
- _scrollDelta = 0;
33
- _keys = {};
34
- _removeListeners = [];
35
- _camera = null;
36
- _planeNormal = new Vec3(0, 1, 0);
37
- _planePoint = new Vec3(0, 0, 0);
38
- _hasGroundPlane = false;
39
- _posedCamera() {
40
- this._camera ??= this.entity.findComponent('camera')?.entity ?? null;
41
- return this._camera;
35
+ static scriptName = 'walkthroughCamera';
36
+ moveSpeed = 4.5;
37
+
38
+ /** @attribute */
39
+ moveFastSpeed = 7.5;
40
+
41
+ /** @attribute */
42
+ moveSlowSpeed = 2.25;
43
+
44
+ /** @attribute */
45
+ lookSensitivity = 0.1;
46
+
47
+ /** @attribute */
48
+ rotateDamping = 0.98;
49
+
50
+ /** @attribute */
51
+ pitchMin = -85;
52
+
53
+ /** @attribute */
54
+ pitchMax = 85;
55
+
56
+ /** @attribute */
57
+ scrollSpeed = 0.001;
58
+
59
+ /** @attribute */
60
+ boundsCenter = new Vec3(0, 0, 0);
61
+
62
+ /** @attribute */
63
+ boundsRadius = 10;
64
+
65
+ /** @attribute */
66
+ freeFly = false;
67
+
68
+ /** @attribute */
69
+ enableFly = true;
70
+
71
+ /** @attribute */
72
+ averageCameraHeight = 0;
73
+
74
+ /**
75
+ * Three world-space points defining the ground plane.
76
+ * When provided, the camera Y position is derived from this plane during non-freeFly movement.
77
+ *
78
+ * Not a React prop — must be set directly on the script instance via useEffect. The @playcanvas/react
79
+ * Script component is wrapped in memo() whose shallowEquals comparator returns early on the first prop
80
+ * with a .equals() method (Vec3), so any prop listed after boundsCenter is never compared and the
81
+ * component won't re-render when it changes. Set via:
82
+ * cameraRoot.script.walkthroughCamera.groundPlane = points
83
+ * Should be updated if shallowEquals is fixed (see https://github.com/playcanvas/react/pull/298)
84
+ */
85
+ groundPlane = [];
86
+
87
+ // Current (damped) angles applied to the entity each frame.
88
+ _pitch = 0;
89
+ _yaw = 0;
90
+
91
+ // Target angles — updated immediately on pointer move, entity lerps toward them.
92
+ _targetPitch = 0;
93
+ _targetYaw = 0;
94
+ _isDragging = false;
95
+ _lastX = 0;
96
+ _lastY = 0;
97
+ _scrollDelta = 0;
98
+ _keys = {};
99
+ _removeListeners = [];
100
+
101
+ // The camera being posed, resolved lazily. Held until it exists: a camera adopted from the host
102
+ // scene is reparented into the rig by an effect that runs after the scripts are mounted.
103
+ _camera = null;
104
+
105
+ // Cached plane derived from groundPlane attribute. Computed once when 3 points become available.
106
+ _planeNormal = new Vec3(0, 1, 0);
107
+ _planePoint = new Vec3(0, 0, 0);
108
+ _hasGroundPlane = false;
109
+
110
+ /**
111
+ * The camera to pose, or null while there is none to pose. The entity this script is mounted on is
112
+ * never posed in its place: on the walkthrough that is the rig, which is the frame an XR session
113
+ * is measured from and whose height the session's floor is taken from.
114
+ */
115
+ _posedCamera() {
116
+ this._camera ??= this.entity.findComponent('camera')?.entity ?? null;
117
+ return this._camera;
118
+ }
119
+ _syncGroundPlane() {
120
+ if (this._hasGroundPlane || this.groundPlane.length < 3) {
121
+ return;
42
122
  }
43
- _syncGroundPlane() {
44
- if (this._hasGroundPlane || this.groundPlane.length < 3) return;
45
- const plane = computeGroundPlane(this.groundPlane);
46
- if (!plane) return;
47
- this._planeNormal.copy(plane.normal);
48
- this._planePoint.copy(plane.point);
49
- this._hasGroundPlane = true;
123
+ const plane = computeGroundPlane(this.groundPlane);
124
+ if (!plane) {
125
+ return;
50
126
  }
51
- initialize() {
52
- this._pitch = 0;
53
- this._yaw = 0;
54
- this._targetPitch = 0;
55
- this._targetYaw = 0;
56
- const canvas = this.app.graphicsDevice.canvas;
57
- const onPointerDown = (e)=>{
58
- this._isDragging = true;
59
- this._lastX = e.clientX;
60
- this._lastY = e.clientY;
61
- try {
62
- canvas.setPointerCapture(e.pointerId);
63
- } catch {}
64
- };
65
- const onPointerMove = (e)=>{
66
- if (!this._isDragging) return;
67
- const dx = e.clientX - this._lastX;
68
- const dy = e.clientY - this._lastY;
69
- this._lastX = e.clientX;
70
- this._lastY = e.clientY;
71
- this._targetYaw -= dx * this.lookSensitivity;
72
- this._targetPitch = math.clamp(this._targetPitch - dy * this.lookSensitivity, this.pitchMin, this.pitchMax);
73
- };
74
- const onPointerUp = (e)=>{
75
- this._isDragging = false;
76
- try {
77
- canvas.releasePointerCapture(e.pointerId);
78
- } catch {}
79
- };
80
- const onWheel = (e)=>{
81
- e.preventDefault();
82
- this._scrollDelta += e.deltaY;
83
- };
84
- const onContextMenu = (e)=>e.preventDefault();
85
- const onKeyDown = (e)=>{
86
- this._keys[e.code] = true;
87
- };
88
- const onKeyUp = (e)=>{
89
- this._keys[e.code] = false;
90
- };
91
- const onBlur = ()=>{
92
- this._keys = {};
93
- this._isDragging = false;
94
- };
95
- canvas.addEventListener('pointerdown', onPointerDown);
96
- canvas.addEventListener('pointermove', onPointerMove);
97
- canvas.addEventListener('pointerup', onPointerUp);
98
- canvas.addEventListener('pointercancel', onPointerUp);
99
- canvas.addEventListener('wheel', onWheel, {
100
- passive: false
101
- });
102
- canvas.addEventListener('contextmenu', onContextMenu);
103
- window.addEventListener('keydown', onKeyDown);
104
- window.addEventListener('keyup', onKeyUp);
105
- window.addEventListener('blur', onBlur);
106
- this._removeListeners = [
107
- ()=>canvas.removeEventListener('pointerdown', onPointerDown),
108
- ()=>canvas.removeEventListener('pointermove', onPointerMove),
109
- ()=>canvas.removeEventListener('pointerup', onPointerUp),
110
- ()=>canvas.removeEventListener('pointercancel', onPointerUp),
111
- ()=>canvas.removeEventListener('wheel', onWheel),
112
- ()=>canvas.removeEventListener('contextmenu', onContextMenu),
113
- ()=>window.removeEventListener('keydown', onKeyDown),
114
- ()=>window.removeEventListener('keyup', onKeyUp),
115
- ()=>window.removeEventListener('blur', onBlur)
116
- ];
127
+ this._planeNormal.copy(plane.normal);
128
+ this._planePoint.copy(plane.point);
129
+ this._hasGroundPlane = true;
130
+ }
131
+ initialize() {
132
+ // Start horizontal; reset() will set the correct pose once splat data loads.
133
+ this._pitch = 0;
134
+ this._yaw = 0;
135
+ this._targetPitch = 0;
136
+ this._targetYaw = 0;
137
+ const canvas = this.app.graphicsDevice.canvas;
138
+ const onPointerDown = e => {
139
+ this._isDragging = true;
140
+ this._lastX = e.clientX;
141
+ this._lastY = e.clientY;
142
+ try {
143
+ canvas.setPointerCapture(e.pointerId);
144
+ } catch {
145
+ // ignore — canvas may not be focusable yet
146
+ }
147
+ };
148
+ const onPointerMove = e => {
149
+ if (!this._isDragging) {
150
+ return;
151
+ }
152
+ const dx = e.clientX - this._lastX;
153
+ const dy = e.clientY - this._lastY;
154
+ this._lastX = e.clientX;
155
+ this._lastY = e.clientY;
156
+ this._targetYaw -= dx * this.lookSensitivity;
157
+ this._targetPitch = math.clamp(this._targetPitch - dy * this.lookSensitivity, this.pitchMin, this.pitchMax);
158
+ };
159
+ const onPointerUp = e => {
160
+ this._isDragging = false;
161
+ try {
162
+ canvas.releasePointerCapture(e.pointerId);
163
+ } catch {
164
+ // ignore
165
+ }
166
+ };
167
+ const onWheel = e => {
168
+ e.preventDefault();
169
+ this._scrollDelta += e.deltaY;
170
+ };
171
+ const onContextMenu = e => e.preventDefault();
172
+ const onKeyDown = e => {
173
+ this._keys[e.code] = true;
174
+ };
175
+ const onKeyUp = e => {
176
+ this._keys[e.code] = false;
177
+ };
178
+ const onBlur = () => {
179
+ this._keys = {};
180
+ this._isDragging = false;
181
+ };
182
+ canvas.addEventListener('pointerdown', onPointerDown);
183
+ canvas.addEventListener('pointermove', onPointerMove);
184
+ canvas.addEventListener('pointerup', onPointerUp);
185
+ canvas.addEventListener('pointercancel', onPointerUp);
186
+ canvas.addEventListener('wheel', onWheel, {
187
+ passive: false
188
+ });
189
+ canvas.addEventListener('contextmenu', onContextMenu);
190
+ window.addEventListener('keydown', onKeyDown);
191
+ window.addEventListener('keyup', onKeyUp);
192
+ window.addEventListener('blur', onBlur);
193
+ this._removeListeners = [() => canvas.removeEventListener('pointerdown', onPointerDown), () => canvas.removeEventListener('pointermove', onPointerMove), () => canvas.removeEventListener('pointerup', onPointerUp), () => canvas.removeEventListener('pointercancel', onPointerUp), () => canvas.removeEventListener('wheel', onWheel), () => canvas.removeEventListener('contextmenu', onContextMenu), () => window.removeEventListener('keydown', onKeyDown), () => window.removeEventListener('keyup', onKeyUp), () => window.removeEventListener('blur', onBlur)];
194
+ }
195
+
196
+ /**
197
+ * Position the camera at `position` looking toward `focus`.
198
+ * Compatible with the useCameraPosition hook's reset() call.
199
+ */
200
+ reset(focus, position) {
201
+ const camera = this._posedCamera();
202
+ camera?.setPosition(position);
203
+ const dx = focus.x - position.x;
204
+ const dy = focus.y - position.y;
205
+ const dz = focus.z - position.z;
206
+ const len = Math.sqrt(dx * dx + dy * dy + dz * dz);
207
+ if (len > 0) {
208
+ // PlayCanvas uses right-handed CCW Y rotation: forward = (-sinθ, 0, -cosθ),
209
+ // so to look toward (dx, 0, dz): θ = atan2(-dx, -dz).
210
+ this._yaw = Math.atan2(-dx, -dz) * (180 / Math.PI);
211
+ this._pitch = Math.atan2(-dy, Math.sqrt(dx * dx + dz * dz)) * (180 / Math.PI);
212
+ this._pitch = math.clamp(this._pitch, this.pitchMin, this.pitchMax);
117
213
  }
118
- reset(focus, position) {
119
- const camera = this._posedCamera();
120
- camera?.setPosition(position);
121
- const dx = focus.x - position.x;
122
- const dy = focus.y - position.y;
123
- const dz = focus.z - position.z;
124
- const len = Math.sqrt(dx * dx + dy * dy + dz * dz);
125
- if (len > 0) {
126
- this._yaw = 180 / Math.PI * Math.atan2(-dx, -dz);
127
- this._pitch = 180 / Math.PI * Math.atan2(-dy, Math.sqrt(dx * dx + dz * dz));
128
- this._pitch = math.clamp(this._pitch, this.pitchMin, this.pitchMax);
129
- }
130
- this._targetPitch = this._pitch;
131
- this._targetYaw = this._yaw;
132
- camera?.setEulerAngles(this._pitch, this._yaw, 0);
214
+
215
+ // Snap targets so there's no damped drift from the previous pose.
216
+ this._targetPitch = this._pitch;
217
+ this._targetYaw = this._yaw;
218
+ camera?.setEulerAngles(this._pitch, this._yaw, 0);
219
+ }
220
+ get currentYaw() {
221
+ return this._targetYaw;
222
+ }
223
+ get currentPitch() {
224
+ return this._targetPitch;
225
+ }
226
+
227
+ /**
228
+ * Orbit the camera by `yawDelta` degrees around `boundsCenter`.
229
+ * Called by AutoRotator
230
+ */
231
+ orbitStep(yawDelta) {
232
+ this._syncGroundPlane();
233
+ const camera = this._posedCamera();
234
+ if (!camera) {
235
+ return;
133
236
  }
134
- get currentYaw() {
135
- return this._targetYaw;
237
+ const pos = camera.getPosition();
238
+ const dx = pos.x - this.boundsCenter.x;
239
+ const dz = pos.z - this.boundsCenter.z;
240
+ const radius = Math.sqrt(dx * dx + dz * dz) || this.boundsRadius * 0.5;
241
+ const currentAngle = Math.atan2(dz, dx);
242
+ const newAngle = currentAngle + yawDelta * Math.PI / 180;
243
+ const nx = this.boundsCenter.x + radius * Math.cos(newAngle);
244
+ const nz = this.boundsCenter.z + radius * Math.sin(newAngle);
245
+ const ny = this._hasGroundPlane ? yOnPlane(nx, nz, this._planeNormal, this._planePoint, this.boundsCenter.y) + this.averageCameraHeight : this.boundsCenter.y;
246
+ camera.setPosition(nx, ny, nz);
247
+ const faceDx = this.boundsCenter.x - nx;
248
+ const faceDz = this.boundsCenter.z - nz;
249
+ this._yaw = Math.atan2(-faceDx, -faceDz) * (180 / Math.PI);
250
+ this._targetYaw = this._yaw;
251
+ camera.setEulerAngles(this._pitch, this._yaw, 0);
252
+ }
253
+
254
+ /**
255
+ * Returns the point directly in front of the camera.
256
+ * Compatible with the useCameraPosition hook's focusPoint read.
257
+ */
258
+ get focusPoint() {
259
+ const pos = (this._posedCamera() ?? this.entity).getPosition();
260
+ _quat.setFromEulerAngles(this._pitch, this._yaw, 0);
261
+ _quat.transformVector(Vec3.FORWARD, _flatForward);
262
+ return new Vec3(pos.x + _flatForward.x, pos.y + _flatForward.y, pos.z + _flatForward.z);
263
+ }
264
+ update(dt) {
265
+ this._syncGroundPlane();
266
+ const camera = this._posedCamera();
267
+ if (!camera) {
268
+ return;
136
269
  }
137
- get currentPitch() {
138
- return this._targetPitch;
270
+
271
+ // Lerp current angles toward targets using the same damping as CameraControls.
272
+ // Snap to the target once within ANGLE_EPSILON so the asymptotic lerp actually
273
+ // settles instead of emitting ever-smaller micro-rotations for ~0.4s after each
274
+ // drag. The gsplat sorter guards its own re-sorts with an epsilon, so this mainly
275
+ // trims that sort tail; it does not affect per-frame rendering (autoRender does).
276
+ const r = dampRate(this.rotateDamping, dt);
277
+ let nextPitch = this._pitch + (this._targetPitch - this._pitch) * r;
278
+ let nextYaw = this._yaw + (this._targetYaw - this._yaw) * r;
279
+ if (Math.abs(this._targetPitch - nextPitch) < ANGLE_EPSILON) {
280
+ nextPitch = this._targetPitch;
139
281
  }
140
- orbitStep(yawDelta) {
141
- this._syncGroundPlane();
142
- const camera = this._posedCamera();
143
- if (!camera) return;
144
- const pos = camera.getPosition();
145
- const dx = pos.x - this.boundsCenter.x;
146
- const dz = pos.z - this.boundsCenter.z;
147
- const radius = Math.sqrt(dx * dx + dz * dz) || 0.5 * this.boundsRadius;
148
- const currentAngle = Math.atan2(dz, dx);
149
- const newAngle = currentAngle + yawDelta * Math.PI / 180;
150
- const nx = this.boundsCenter.x + radius * Math.cos(newAngle);
151
- const nz = this.boundsCenter.z + radius * Math.sin(newAngle);
152
- const ny = this._hasGroundPlane ? yOnPlane(nx, nz, this._planeNormal, this._planePoint, this.boundsCenter.y) + this.averageCameraHeight : this.boundsCenter.y;
153
- camera.setPosition(nx, ny, nz);
154
- const faceDx = this.boundsCenter.x - nx;
155
- const faceDz = this.boundsCenter.z - nz;
156
- this._yaw = 180 / Math.PI * Math.atan2(-faceDx, -faceDz);
157
- this._targetYaw = this._yaw;
158
- camera.setEulerAngles(this._pitch, this._yaw, 0);
282
+ if (Math.abs(this._targetYaw - nextYaw) < ANGLE_EPSILON) {
283
+ nextYaw = this._targetYaw;
159
284
  }
160
- get focusPoint() {
161
- const pos = (this._posedCamera() ?? this.entity).getPosition();
162
- _quat.setFromEulerAngles(this._pitch, this._yaw, 0);
163
- _quat.transformVector(Vec3.FORWARD, _flatForward);
164
- return new Vec3(pos.x + _flatForward.x, pos.y + _flatForward.y, pos.z + _flatForward.z);
285
+ nextPitch = math.clamp(nextPitch, this.pitchMin, this.pitchMax);
286
+
287
+ // Only write the rotation when it actually changed, to avoid needlessly dirtying
288
+ // the scene node once the angles have settled.
289
+ const anglesChanged = nextPitch !== this._pitch || nextYaw !== this._yaw;
290
+ this._pitch = nextPitch;
291
+ this._yaw = nextYaw;
292
+ if (anglesChanged) {
293
+ camera.setEulerAngles(this._pitch, this._yaw, 0);
165
294
  }
166
- update(dt) {
167
- this._syncGroundPlane();
168
- const camera = this._posedCamera();
169
- if (!camera) return;
170
- const r = dampRate(this.rotateDamping, dt);
171
- let nextPitch = this._pitch + (this._targetPitch - this._pitch) * r;
172
- let nextYaw = this._yaw + (this._targetYaw - this._yaw) * r;
173
- if (Math.abs(this._targetPitch - nextPitch) < ANGLE_EPSILON) nextPitch = this._targetPitch;
174
- if (Math.abs(this._targetYaw - nextYaw) < ANGLE_EPSILON) nextYaw = this._targetYaw;
175
- nextPitch = math.clamp(nextPitch, this.pitchMin, this.pitchMax);
176
- const anglesChanged = nextPitch !== this._pitch || nextYaw !== this._yaw;
177
- this._pitch = nextPitch;
178
- this._yaw = nextYaw;
179
- if (anglesChanged) camera.setEulerAngles(this._pitch, this._yaw, 0);
180
- const pos = camera.getPosition();
181
- let nx = pos.x;
182
- let ny = pos.y;
183
- let nz = pos.z;
184
- if (this.enableFly) {
185
- const fast = this._keys.ShiftLeft || this._keys.ShiftRight;
186
- const slow = this._keys.ControlLeft || this._keys.ControlRight;
187
- const speed = (fast ? this.moveFastSpeed : slow ? this.moveSlowSpeed : this.moveSpeed) * dt;
188
- const fwd = (this._keys.KeyW || this._keys.ArrowUp ? 1 : 0) - (this._keys.KeyS || this._keys.ArrowDown ? 1 : 0);
189
- const strafe = (this._keys.KeyD || this._keys.ArrowRight ? 1 : 0) - (this._keys.KeyA || this._keys.ArrowLeft ? 1 : 0);
190
- const scrollFwd = -this._scrollDelta * this.scrollSpeed;
191
- if (0 !== fwd || 0 !== strafe || 0 !== scrollFwd) {
192
- _quat.setFromEulerAngles(0, this._yaw, 0);
193
- _quat.transformVector(Vec3.FORWARD, _flatForward);
194
- _quat.transformVector(Vec3.RIGHT, _flatRight);
195
- nx += _flatForward.x * (fwd * speed + scrollFwd) + _flatRight.x * strafe * speed;
196
- nz += _flatForward.z * (fwd * speed + scrollFwd) + _flatRight.z * strafe * speed;
197
- }
198
- if (this.freeFly) {
199
- const up = (this._keys.KeyE ? 1 : 0) - (this._keys.KeyQ ? 1 : 0);
200
- if (0 !== up) ny += up * speed;
201
- }
202
- }
203
- this._scrollDelta = 0;
204
- if (!this.freeFly) {
205
- const cdx = nx - this.boundsCenter.x;
206
- const cdz = nz - this.boundsCenter.z;
207
- const dist = Math.sqrt(cdx * cdx + cdz * cdz);
208
- if (dist > this.boundsRadius) {
209
- const scale = this.boundsRadius / dist;
210
- nx = this.boundsCenter.x + cdx * scale;
211
- nz = this.boundsCenter.z + cdz * scale;
212
- }
295
+ const pos = camera.getPosition();
296
+ let nx = pos.x;
297
+ let ny = pos.y;
298
+ let nz = pos.z;
299
+ if (this.enableFly) {
300
+ const fast = this._keys.ShiftLeft || this._keys.ShiftRight;
301
+ const slow = this._keys.ControlLeft || this._keys.ControlRight;
302
+ const speed = (fast ? this.moveFastSpeed : slow ? this.moveSlowSpeed : this.moveSpeed) * dt;
303
+ const fwd = (this._keys.KeyW || this._keys.ArrowUp ? 1 : 0) - (this._keys.KeyS || this._keys.ArrowDown ? 1 : 0);
304
+ const strafe = (this._keys.KeyD || this._keys.ArrowRight ? 1 : 0) - (this._keys.KeyA || this._keys.ArrowLeft ? 1 : 0);
305
+ const scrollFwd = -this._scrollDelta * this.scrollSpeed;
306
+ if (fwd !== 0 || strafe !== 0 || scrollFwd !== 0) {
307
+ // Use yaw-only rotation so movement is always horizontal.
308
+ _quat.setFromEulerAngles(0, this._yaw, 0);
309
+ _quat.transformVector(Vec3.FORWARD, _flatForward);
310
+ _quat.transformVector(Vec3.RIGHT, _flatRight);
311
+ nx += _flatForward.x * (fwd * speed + scrollFwd) + _flatRight.x * strafe * speed;
312
+ nz += _flatForward.z * (fwd * speed + scrollFwd) + _flatRight.z * strafe * speed;
313
+ }
314
+ if (this.freeFly) {
315
+ const up = (this._keys.KeyE ? 1 : 0) - (this._keys.KeyQ ? 1 : 0);
316
+ if (up !== 0) {
317
+ ny += up * speed;
213
318
  }
214
- const groundY = this._hasGroundPlane ? yOnPlane(nx, nz, this._planeNormal, this._planePoint, this.boundsCenter.y) + this.averageCameraHeight : this.boundsCenter.y;
215
- const targetY = this.freeFly ? ny : groundY;
216
- if (Math.abs(nx - pos.x) > POSITION_EPSILON || Math.abs(targetY - pos.y) > POSITION_EPSILON || Math.abs(nz - pos.z) > POSITION_EPSILON) camera.setPosition(nx, targetY, nz);
319
+ }
320
+ }
321
+ this._scrollDelta = 0;
322
+ if (!this.freeFly) {
323
+ // Circular XZ clamp: project back onto the circle edge if outside.
324
+ const cdx = nx - this.boundsCenter.x;
325
+ const cdz = nz - this.boundsCenter.z;
326
+ const dist = Math.sqrt(cdx * cdx + cdz * cdz);
327
+ if (dist > this.boundsRadius) {
328
+ const scale = this.boundsRadius / dist;
329
+ nx = this.boundsCenter.x + cdx * scale;
330
+ nz = this.boundsCenter.z + cdz * scale;
331
+ }
217
332
  }
218
- destroy() {
219
- this._removeListeners.forEach((fn)=>fn());
220
- this._removeListeners = [];
333
+ const groundY = this._hasGroundPlane ? yOnPlane(nx, nz, this._planeNormal, this._planePoint, this.boundsCenter.y) + this.averageCameraHeight : this.boundsCenter.y;
334
+ const targetY = this.freeFly ? ny : groundY;
335
+ // Only write the position when it actually moved. This keeps the scene node clean on
336
+ // idle frames; note it does not stop per-frame rasterization — while autoRender is on
337
+ // the app renders every frame regardless of whether the transform changed.
338
+ if (Math.abs(nx - pos.x) > POSITION_EPSILON || Math.abs(targetY - pos.y) > POSITION_EPSILON || Math.abs(nz - pos.z) > POSITION_EPSILON) {
339
+ camera.setPosition(nx, targetY, nz);
221
340
  }
341
+ }
342
+ destroy() {
343
+ this._removeListeners.forEach(fn => fn());
344
+ this._removeListeners = [];
345
+ }
222
346
  }
347
+
223
348
  export { WalkthroughCamera };