@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,443 +1,473 @@
1
- import { ADDRESS_CLAMP_TO_EDGE, BLEND_NORMAL, CULLFACE_NONE, Color, FILTER_LINEAR, LAYERID_IMMEDIATE, Mesh, MeshInstance, Quat, Script, StandardMaterial, Texture, Vec3 } from "playcanvas";
2
- import { BODY_LINE_HEIGHT, PANEL_CANVAS_WIDTH, PANEL_FONTS, PANEL_MAX_HEIGHT, PANEL_PAD_X, TITLE_LINE_HEIGHT, layoutPanel } from "./vr-annotation-panel-layout.js";
3
- import { nextScrollY, readScrollAxis } from "./vr-annotation-scroll.js";
4
- import { rayQuadHit } from "./xr-ray-quad.js";
1
+ import { Vec3, Script, Quat, StandardMaterial, MeshInstance, Color, BLEND_NORMAL, CULLFACE_NONE, LAYERID_IMMEDIATE, Texture, FILTER_LINEAR, ADDRESS_CLAMP_TO_EDGE, Mesh } from 'playcanvas';
2
+ import { PANEL_MAX_HEIGHT, PANEL_CANVAS_WIDTH, layoutPanel, PANEL_PAD_X, PANEL_FONTS, TITLE_LINE_HEIGHT, BODY_LINE_HEIGHT } from './vr-annotation-panel-layout.js';
3
+ import { readScrollAxis, nextScrollY } from './vr-annotation-scroll.js';
4
+ import { rayQuadHit } from './xr-ray-quad.js';
5
+
5
6
  const PANEL_WIDTH = 7.5;
7
+
8
+ /** Gap (m) between the top of the hotspot and the bottom edge of the panel. */
6
9
  const PANEL_GAP = 0.75;
7
10
  const PX_TO_M = PANEL_WIDTH / PANEL_CANVAS_WIDTH;
11
+
12
+ /** Local +z faces the viewer, so the overlay quads sit just in front of the chrome quad. Their
13
+ * draw order would otherwise depend on how the transparent sort breaks a tie between coplanar
14
+ * quads, all of which have depth testing off. */
8
15
  const TEXT_Z = 0.001;
9
16
  const THUMB_Z = 0.002;
10
17
  const SCROLLBAR_WIDTH = 10;
11
18
  const SCROLLBAR_INSET = 18;
12
19
  const SCROLLBAR_MIN_HEIGHT = 48;
20
+
21
+ /** Local +z (the quad's front face) — used to derive the panel's yaw toward the viewer. */
13
22
  const FORWARD_Z = new Vec3(0, 0, 1);
23
+ /** Quad in-plane axes in local space, transformed to world for ray hit-testing. */
14
24
  const RIGHT_AXIS = new Vec3(1, 0, 0);
15
25
  const UP_AXIS = new Vec3(0, 1, 0);
16
26
  const RAD_TO_DEG = 180 / Math.PI;
27
+
28
+ /** Width (canvas px) of the left/right arrow hit + draw zones over the image band. */
17
29
  const ARROW_ZONE_W = 170;
18
30
  class VrAnnotationPanel extends Script {
19
- static scriptName = 'vrAnnotationPanel';
20
- title = '';
21
- label;
22
- bodyText;
23
- imageUrls;
24
- annotationIndex = -1;
25
- _chromeMaterial;
26
- _chromeTexture;
27
- _chromeMesh;
28
- _chromeCanvas;
29
- _textMaterial;
30
- _textTexture;
31
- _textMesh;
32
- _textCanvas;
33
- _textInstance;
34
- _thumbMaterial;
35
- _thumbMesh;
36
- _thumbInstance;
37
- _drawnSignature = null;
38
- _currentImage = 0;
39
- _loadToken = 0;
40
- _loadedImage;
41
- _layout;
42
- _scrollY = 0;
43
- _maxScroll = 0;
44
- _canvasHeight = PANEL_MAX_HEIGHT;
45
- _panelHeight = PANEL_MAX_HEIGHT * PX_TO_M;
46
- _headRotation = new Quat();
47
- _anchor = new Vec3();
48
- _scratchDir = new Vec3();
49
- _yawQuat = new Quat();
50
- _axisRight = new Vec3();
51
- _axisUp = new Vec3();
52
- _scratchScale = new Vec3();
53
- _onSelect = (inputSource)=>{
54
- if (this.app.xr?.active !== true) return;
55
- const images = this.imageUrls ?? [];
56
- const band = this._layout?.image;
57
- if (images.length <= 1 || !band) return;
58
- const hit = this._rayQuadHit(inputSource.getOrigin(), inputSource.getDirection());
59
- if (!hit) return;
60
- const cx = (hit.u + 1) / 2 * PANEL_CANVAS_WIDTH;
61
- const cy = (1 - hit.v) / 2 * this._canvasHeight;
62
- if (cy < band.y || cy > band.y + band.height) return;
63
- if (cx <= ARROW_ZONE_W) this._setImage((this._currentImage - 1 + images.length) % images.length);
64
- else if (cx >= PANEL_CANVAS_WIDTH - ARROW_ZONE_W) this._setImage((this._currentImage + 1) % images.length);
65
- };
66
- initialize() {
67
- this._chromeCanvas = document.createElement('canvas');
68
- this._chromeCanvas.width = PANEL_CANVAS_WIDTH;
69
- this._chromeCanvas.height = PANEL_MAX_HEIGHT;
70
- this._chromeTexture = this._createTexture('vr-annotation-panel', this._chromeCanvas);
71
- this._chromeMaterial = this._createTexturedMaterial(this._chromeTexture);
72
- this._chromeMesh = this._createQuadMesh();
73
- this._textMesh = this._createQuadMesh();
74
- this._textMaterial = new StandardMaterial();
75
- this._textInstance = new MeshInstance(this._textMesh, this._textMaterial);
76
- this._textInstance.visible = false;
77
- this._thumbMaterial = new StandardMaterial();
78
- this._thumbMaterial.useLighting = false;
79
- this._thumbMaterial.emissive = new Color(0, 0, 0);
80
- this._thumbMaterial.opacity = 0.3;
81
- this._thumbMaterial.blendType = BLEND_NORMAL;
82
- this._thumbMaterial.depthTest = false;
83
- this._thumbMaterial.depthWrite = false;
84
- this._thumbMaterial.cull = CULLFACE_NONE;
85
- this._thumbMaterial.update();
86
- this._thumbMesh = this._createQuadMesh();
87
- this._thumbInstance = new MeshInstance(this._thumbMesh, this._thumbMaterial);
88
- this._thumbInstance.visible = false;
89
- this.entity.addComponent('render', {
90
- meshInstances: [
91
- new MeshInstance(this._chromeMesh, this._chromeMaterial),
92
- this._textInstance,
93
- this._thumbInstance
94
- ],
95
- layers: [
96
- LAYERID_IMMEDIATE
97
- ]
98
- });
99
- this.app.xr?.input?.on('select', this._onSelect);
100
- this.once('destroy', ()=>this._teardown());
31
+ static scriptName = 'vrAnnotationPanel';
32
+
33
+ // Props assigned by the React wrapper.
34
+ title = '';
35
+ annotationIndex = -1;
36
+ _drawnSignature = null;
37
+ _currentImage = 0;
38
+ _loadToken = 0;
39
+ _scrollY = 0;
40
+ _maxScroll = 0;
41
+ /** Height (canvas px) the chrome occupies; the rest of its canvas is unused. */
42
+ _canvasHeight = PANEL_MAX_HEIGHT;
43
+ _panelHeight = PANEL_MAX_HEIGHT * PX_TO_M;
44
+ _headRotation = new Quat();
45
+ _anchor = new Vec3();
46
+ _scratchDir = new Vec3();
47
+ _yawQuat = new Quat();
48
+ _axisRight = new Vec3();
49
+ _axisUp = new Vec3();
50
+ _scratchScale = new Vec3();
51
+ _onSelect = inputSource => {
52
+ if (this.app.xr?.active !== true) {
53
+ return;
101
54
  }
102
- _createTexture(name, source) {
103
- const texture = new Texture(this.app.graphicsDevice, {
104
- name,
105
- width: source.width,
106
- height: source.height,
107
- addressU: ADDRESS_CLAMP_TO_EDGE,
108
- addressV: ADDRESS_CLAMP_TO_EDGE,
109
- minFilter: FILTER_LINEAR,
110
- magFilter: FILTER_LINEAR,
111
- mipmaps: true
112
- });
113
- texture.setSource(source);
114
- return texture;
55
+ const images = this.imageUrls ?? [];
56
+ const band = this._layout?.image;
57
+ if (images.length <= 1 || !band) {
58
+ return;
115
59
  }
116
- _createTexturedMaterial(texture) {
117
- const material = new StandardMaterial();
118
- material.useLighting = false;
119
- material.emissive = new Color(1, 1, 1);
120
- material.emissiveMap = texture;
121
- material.opacityMap = texture;
122
- material.opacityMapChannel = 'a';
123
- material.blendType = BLEND_NORMAL;
124
- material.depthTest = false;
125
- material.depthWrite = false;
126
- material.cull = CULLFACE_NONE;
127
- material.update();
128
- return material;
60
+ const hit = this._rayQuadHit(inputSource.getOrigin(), inputSource.getDirection());
61
+ if (!hit) {
62
+ return;
129
63
  }
130
- _createQuadMesh() {
131
- const mesh = new Mesh(this.app.graphicsDevice);
132
- mesh.setNormals([
133
- 0,
134
- 0,
135
- 1,
136
- 0,
137
- 0,
138
- 1,
139
- 0,
140
- 0,
141
- 1,
142
- 0,
143
- 0,
144
- 1
145
- ]);
146
- mesh.setIndices([
147
- 0,
148
- 1,
149
- 2,
150
- 0,
151
- 2,
152
- 3
153
- ]);
154
- this._writeQuad(mesh, -PANEL_WIDTH / 2, PANEL_WIDTH / 2, 0, 0, 0, 0, 1);
155
- return mesh;
64
+ const cx = (hit.u + 1) / 2 * PANEL_CANVAS_WIDTH;
65
+ // The image sits on the chrome quad, which doesn't scroll, so this needs no scroll offset.
66
+ const cy = (1 - hit.v) / 2 * this._canvasHeight;
67
+ if (cy < band.y || cy > band.y + band.height) {
68
+ return;
156
69
  }
157
- _writeQuad(mesh, left, right, top, bottom, z, vTop, vBottom) {
158
- mesh.setPositions([
159
- left,
160
- bottom,
161
- z,
162
- right,
163
- bottom,
164
- z,
165
- right,
166
- top,
167
- z,
168
- left,
169
- top,
170
- z
171
- ]);
172
- mesh.setUvs(0, [
173
- 0,
174
- vBottom,
175
- 1,
176
- vBottom,
177
- 1,
178
- vTop,
179
- 0,
180
- vTop
181
- ]);
182
- mesh.update();
70
+ if (cx <= ARROW_ZONE_W) {
71
+ this._setImage((this._currentImage - 1 + images.length) % images.length);
72
+ } else if (cx >= PANEL_CANVAS_WIDTH - ARROW_ZONE_W) {
73
+ this._setImage((this._currentImage + 1) % images.length);
183
74
  }
184
- _localY(canvasY) {
185
- return this._panelHeight / 2 - canvasY * PX_TO_M;
75
+ };
76
+ initialize() {
77
+ this._chromeCanvas = document.createElement('canvas');
78
+ this._chromeCanvas.width = PANEL_CANVAS_WIDTH;
79
+ this._chromeCanvas.height = PANEL_MAX_HEIGHT;
80
+ this._chromeTexture = this._createTexture('vr-annotation-panel', this._chromeCanvas);
81
+ this._chromeMaterial = this._createTexturedMaterial(this._chromeTexture);
82
+ this._chromeMesh = this._createQuadMesh();
83
+
84
+ // Sized once the first layout runs; until then it has nothing to show.
85
+ this._textMesh = this._createQuadMesh();
86
+ this._textMaterial = new StandardMaterial();
87
+ this._textInstance = new MeshInstance(this._textMesh, this._textMaterial);
88
+ this._textInstance.visible = false;
89
+ this._thumbMaterial = new StandardMaterial();
90
+ this._thumbMaterial.useLighting = false;
91
+ this._thumbMaterial.emissive = new Color(0, 0, 0);
92
+ this._thumbMaterial.opacity = 0.3;
93
+ this._thumbMaterial.blendType = BLEND_NORMAL;
94
+ this._thumbMaterial.depthTest = false;
95
+ this._thumbMaterial.depthWrite = false;
96
+ this._thumbMaterial.cull = CULLFACE_NONE;
97
+ this._thumbMaterial.update();
98
+ this._thumbMesh = this._createQuadMesh();
99
+ this._thumbInstance = new MeshInstance(this._thumbMesh, this._thumbMaterial);
100
+ this._thumbInstance.visible = false;
101
+ this.entity.addComponent('render', {
102
+ meshInstances: [new MeshInstance(this._chromeMesh, this._chromeMaterial), this._textInstance, this._thumbInstance],
103
+ layers: [LAYERID_IMMEDIATE]
104
+ });
105
+ this.app.xr?.input?.on('select', this._onSelect);
106
+ // Script removal fires a 'destroy' event rather than calling a destroy() method, so the
107
+ // teardown has to be registered as a listener or the handler and GPU resources outlive us.
108
+ this.once('destroy', () => this._teardown());
109
+ }
110
+ _createTexture(name, source) {
111
+ const texture = new Texture(this.app.graphicsDevice, {
112
+ name,
113
+ width: source.width,
114
+ height: source.height,
115
+ addressU: ADDRESS_CLAMP_TO_EDGE,
116
+ addressV: ADDRESS_CLAMP_TO_EDGE,
117
+ minFilter: FILTER_LINEAR,
118
+ magFilter: FILTER_LINEAR,
119
+ mipmaps: true
120
+ });
121
+ texture.setSource(source);
122
+ return texture;
123
+ }
124
+ _createTexturedMaterial(texture) {
125
+ const material = new StandardMaterial();
126
+ material.useLighting = false;
127
+ material.emissive = new Color(1, 1, 1);
128
+ material.emissiveMap = texture;
129
+ material.opacityMap = texture;
130
+ material.opacityMapChannel = 'a';
131
+ material.blendType = BLEND_NORMAL;
132
+ material.depthTest = false;
133
+ material.depthWrite = false;
134
+ material.cull = CULLFACE_NONE;
135
+ material.update();
136
+ return material;
137
+ }
138
+ _createQuadMesh() {
139
+ const mesh = new Mesh(this.app.graphicsDevice);
140
+ mesh.setNormals([0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1]);
141
+ mesh.setIndices([0, 1, 2, 0, 2, 3]);
142
+ this._writeQuad(mesh, -PANEL_WIDTH / 2, PANEL_WIDTH / 2, 0, 0, 0, 0, 1);
143
+ return mesh;
144
+ }
145
+
146
+ /** Rewrites the quad's four corners and the slice of its texture they map to. */
147
+ _writeQuad(mesh, left, right, top, bottom, z, vTop, vBottom) {
148
+ mesh.setPositions([left, bottom, z, right, bottom, z, right, top, z, left, top, z]);
149
+ mesh.setUvs(0, [0, vBottom, 1, vBottom, 1, vTop, 0, vTop]);
150
+ mesh.update();
151
+ }
152
+
153
+ /** Converts a y in panel canvas coordinates to the entity's local space. */
154
+ _localY(canvasY) {
155
+ return this._panelHeight / 2 - canvasY * PX_TO_M;
156
+ }
157
+ _writeChromeQuad() {
158
+ if (!this._chromeMesh) {
159
+ return;
186
160
  }
187
- _writeChromeQuad() {
188
- if (!this._chromeMesh) return;
189
- const hw = PANEL_WIDTH / 2;
190
- const hh = this._panelHeight / 2;
191
- this._writeQuad(this._chromeMesh, -hw, hw, hh, -hh, 0, 0, this._canvasHeight / PANEL_MAX_HEIGHT);
161
+ const hw = PANEL_WIDTH / 2;
162
+ const hh = this._panelHeight / 2;
163
+ this._writeQuad(this._chromeMesh, -hw, hw, hh, -hh, 0, 0, this._canvasHeight / PANEL_MAX_HEIGHT);
164
+ }
165
+
166
+ /** Maps the visible window of the (much taller) text texture onto the body quad. */
167
+ _writeTextQuad() {
168
+ const body = this._layout?.body;
169
+ if (!this._textMesh || !this._textInstance || !body || body.contentHeight === 0) {
170
+ return;
192
171
  }
193
- _writeTextQuad() {
194
- const body = this._layout?.body;
195
- if (!this._textMesh || !this._textInstance || !body || 0 === body.contentHeight) return;
196
- this._writeQuad(this._textMesh, -PANEL_WIDTH / 2, PANEL_WIDTH / 2, this._localY(body.y), this._localY(body.y + body.viewportHeight), TEXT_Z, this._scrollY / body.contentHeight, (this._scrollY + body.viewportHeight) / body.contentHeight);
172
+ this._writeQuad(this._textMesh, -PANEL_WIDTH / 2, PANEL_WIDTH / 2, this._localY(body.y), this._localY(body.y + body.viewportHeight), TEXT_Z, this._scrollY / body.contentHeight, (this._scrollY + body.viewportHeight) / body.contentHeight);
173
+ }
174
+ _writeThumbQuad() {
175
+ const body = this._layout?.body;
176
+ if (!this._thumbMesh || !body?.scrollable) {
177
+ return;
197
178
  }
198
- _writeThumbQuad() {
199
- const body = this._layout?.body;
200
- if (!this._thumbMesh || !body?.scrollable) return;
201
- const height = Math.min(body.viewportHeight, Math.max(SCROLLBAR_MIN_HEIGHT, body.viewportHeight * body.viewportHeight / body.contentHeight));
202
- const travel = body.viewportHeight - height;
203
- const top = body.y + (this._maxScroll > 0 ? this._scrollY / this._maxScroll * travel : 0);
204
- const right = PANEL_CANVAS_WIDTH - SCROLLBAR_INSET;
205
- this._writeQuad(this._thumbMesh, (right - SCROLLBAR_WIDTH) * PX_TO_M - PANEL_WIDTH / 2, right * PX_TO_M - PANEL_WIDTH / 2, this._localY(top), this._localY(top + height), THUMB_Z, 0, 1);
179
+ // The minimum keeps the thumb grabbable on very long text, but never past the track itself.
180
+ const height = Math.min(body.viewportHeight, Math.max(SCROLLBAR_MIN_HEIGHT, body.viewportHeight * body.viewportHeight / body.contentHeight));
181
+ const travel = body.viewportHeight - height;
182
+ const top = body.y + (this._maxScroll > 0 ? this._scrollY / this._maxScroll * travel : 0);
183
+ const right = PANEL_CANVAS_WIDTH - SCROLLBAR_INSET;
184
+ this._writeQuad(this._thumbMesh, (right - SCROLLBAR_WIDTH) * PX_TO_M - PANEL_WIDTH / 2, right * PX_TO_M - PANEL_WIDTH / 2, this._localY(top), this._localY(top + height), THUMB_Z, 0, 1);
185
+ }
186
+ _roundRect(ctx, x, y, w, h, r) {
187
+ ctx.beginPath();
188
+ ctx.moveTo(x + r, y);
189
+ ctx.arcTo(x + w, y, x + w, y + h, r);
190
+ ctx.arcTo(x + w, y + h, x, y + h, r);
191
+ ctx.arcTo(x, y + h, x, y, r);
192
+ ctx.arcTo(x, y, x + w, y, r);
193
+ ctx.closePath();
194
+ }
195
+ _drawArrow(ctx, cx, cy, pointsLeft) {
196
+ ctx.beginPath();
197
+ ctx.arc(cx, cy, 40, 0, Math.PI * 2);
198
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.45)';
199
+ ctx.fill();
200
+ const s = 16;
201
+ const tip = pointsLeft ? cx - s * 0.6 : cx + s * 0.6;
202
+ const base = pointsLeft ? cx + s * 0.6 : cx - s * 0.6;
203
+ ctx.beginPath();
204
+ ctx.moveTo(base, cy - s);
205
+ ctx.lineTo(tip, cy);
206
+ ctx.lineTo(base, cy + s);
207
+ ctx.closePath();
208
+ ctx.fillStyle = '#ffffff';
209
+ ctx.fill();
210
+ }
211
+ _drawDots(ctx, count, dotY) {
212
+ const gap = 26;
213
+ const startX = PANEL_CANVAS_WIDTH / 2 - (count - 1) * gap / 2;
214
+ for (let i = 0; i < count; i++) {
215
+ ctx.beginPath();
216
+ ctx.arc(startX + i * gap, dotY, 7, 0, Math.PI * 2);
217
+ ctx.fillStyle = i === this._currentImage ? 'rgba(0, 0, 0, 0.85)' : 'rgba(0, 0, 0, 0.25)';
218
+ ctx.fill();
206
219
  }
207
- _roundRect(ctx, x, y, w, h, r) {
208
- ctx.beginPath();
209
- ctx.moveTo(x + r, y);
210
- ctx.arcTo(x + w, y, x + w, y + h, r);
211
- ctx.arcTo(x + w, y + h, x, y + h, r);
212
- ctx.arcTo(x, y + h, x, y, r);
213
- ctx.arcTo(x, y, x + w, y, r);
214
- ctx.closePath();
220
+ }
221
+
222
+ /** Re-measures the content, resizes the panel, and repaints both canvases. */
223
+ _rebuild() {
224
+ const ctx = this._chromeCanvas?.getContext('2d');
225
+ if (!ctx) {
226
+ return;
215
227
  }
216
- _drawArrow(ctx, cx, cy, pointsLeft) {
217
- ctx.beginPath();
218
- ctx.arc(cx, cy, 40, 0, 2 * Math.PI);
219
- ctx.fillStyle = 'rgba(0, 0, 0, 0.45)';
220
- ctx.fill();
221
- const s = 16;
222
- const tip = pointsLeft ? cx - 0.6 * s : cx + 0.6 * s;
223
- const base = pointsLeft ? cx + 0.6 * s : cx - 0.6 * s;
224
- ctx.beginPath();
225
- ctx.moveTo(base, cy - s);
226
- ctx.lineTo(tip, cy);
227
- ctx.lineTo(base, cy + s);
228
- ctx.closePath();
229
- ctx.fillStyle = '#ffffff';
230
- ctx.fill();
228
+ const images = this.imageUrls ?? [];
229
+ this._layout = layoutPanel({
230
+ title: this.title,
231
+ label: this.label,
232
+ bodyText: this.bodyText,
233
+ imageCount: images.length,
234
+ contentWidth: PANEL_CANVAS_WIDTH - PANEL_PAD_X * 2,
235
+ measure: (text, style) => {
236
+ ctx.font = PANEL_FONTS[style];
237
+ return ctx.measureText(text).width;
238
+ }
239
+ });
240
+ const {
241
+ body
242
+ } = this._layout;
243
+ this._maxScroll = body.contentHeight - body.viewportHeight;
244
+ this._canvasHeight = this._layout.height;
245
+ this._panelHeight = this._layout.height * PX_TO_M;
246
+ this._drawChrome();
247
+ this._drawText();
248
+ this._writeChromeQuad();
249
+ this._writeTextQuad();
250
+ this._writeThumbQuad();
251
+ if (this._textInstance) {
252
+ this._textInstance.visible = body.contentHeight > 0;
231
253
  }
232
- _drawDots(ctx, count, dotY) {
233
- const gap = 26;
234
- const startX = PANEL_CANVAS_WIDTH / 2 - (count - 1) * gap / 2;
235
- for(let i = 0; i < count; i++){
236
- ctx.beginPath();
237
- ctx.arc(startX + i * gap, dotY, 7, 0, 2 * Math.PI);
238
- ctx.fillStyle = i === this._currentImage ? 'rgba(0, 0, 0, 0.85)' : 'rgba(0, 0, 0, 0.25)';
239
- ctx.fill();
240
- }
254
+ if (this._thumbInstance) {
255
+ this._thumbInstance.visible = body.scrollable;
241
256
  }
242
- _rebuild() {
243
- const ctx = this._chromeCanvas?.getContext('2d');
244
- if (!ctx) return;
245
- const images = this.imageUrls ?? [];
246
- this._layout = layoutPanel({
247
- title: this.title,
248
- label: this.label,
249
- bodyText: this.bodyText,
250
- imageCount: images.length,
251
- contentWidth: PANEL_CANVAS_WIDTH - 2 * PANEL_PAD_X,
252
- measure: (text, style)=>{
253
- ctx.font = PANEL_FONTS[style];
254
- return ctx.measureText(text).width;
255
- }
256
- });
257
- const { body } = this._layout;
258
- this._maxScroll = body.contentHeight - body.viewportHeight;
259
- this._canvasHeight = this._layout.height;
260
- this._panelHeight = this._layout.height * PX_TO_M;
261
- this._drawChrome();
262
- this._drawText();
263
- this._writeChromeQuad();
264
- this._writeTextQuad();
265
- this._writeThumbQuad();
266
- if (this._textInstance) this._textInstance.visible = body.contentHeight > 0;
267
- if (this._thumbInstance) this._thumbInstance.visible = body.scrollable;
257
+ }
258
+ _drawChrome() {
259
+ const ctx = this._chromeCanvas?.getContext('2d');
260
+ const layout = this._layout;
261
+ if (!ctx || !layout) {
262
+ return;
263
+ }
264
+ const contentWidth = PANEL_CANVAS_WIDTH - PANEL_PAD_X * 2;
265
+ const images = this.imageUrls ?? [];
266
+ ctx.clearRect(0, 0, PANEL_CANVAS_WIDTH, PANEL_MAX_HEIGHT);
267
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.96)';
268
+ this._roundRect(ctx, 0, 0, PANEL_CANVAS_WIDTH, layout.height, 32);
269
+ ctx.fill();
270
+ if (layout.image) {
271
+ const {
272
+ y: bandY,
273
+ height: bandH
274
+ } = layout.image;
275
+ ctx.save();
276
+ this._roundRect(ctx, PANEL_PAD_X, bandY, contentWidth, bandH, 16);
277
+ ctx.clip();
278
+ const image = this._loadedImage;
279
+ if (image) {
280
+ const scale = Math.max(contentWidth / image.width, bandH / image.height);
281
+ const dw = image.width * scale;
282
+ const dh = image.height * scale;
283
+ ctx.drawImage(image, PANEL_PAD_X + (contentWidth - dw) / 2, bandY + (bandH - dh) / 2, dw, dh);
284
+ } else {
285
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.06)';
286
+ ctx.fillRect(PANEL_PAD_X, bandY, contentWidth, bandH);
287
+ }
288
+ ctx.restore();
289
+ if (layout.dotsY !== null) {
290
+ const bandCenterY = bandY + bandH / 2;
291
+ this._drawArrow(ctx, ARROW_ZONE_W / 2, bandCenterY, true);
292
+ this._drawArrow(ctx, PANEL_CANVAS_WIDTH - ARROW_ZONE_W / 2, bandCenterY, false);
293
+ this._drawDots(ctx, images.length, layout.dotsY);
294
+ }
295
+ }
296
+ if (layout.chip && this.label) {
297
+ ctx.font = PANEL_FONTS.label;
298
+ ctx.textBaseline = 'middle';
299
+ ctx.fillStyle = 'rgba(44, 134, 88, 0.95)';
300
+ this._roundRect(ctx, PANEL_PAD_X, layout.chip.y, layout.chip.width, layout.chip.height, 12);
301
+ ctx.fill();
302
+ ctx.fillStyle = '#ffffff';
303
+ ctx.fillText(this.label, PANEL_PAD_X + 16, layout.chip.y + layout.chip.height / 2);
268
304
  }
269
- _drawChrome() {
270
- const ctx = this._chromeCanvas?.getContext('2d');
271
- const layout = this._layout;
272
- if (!ctx || !layout) return;
273
- const contentWidth = PANEL_CANVAS_WIDTH - 2 * PANEL_PAD_X;
274
- const images = this.imageUrls ?? [];
275
- ctx.clearRect(0, 0, PANEL_CANVAS_WIDTH, PANEL_MAX_HEIGHT);
276
- ctx.fillStyle = 'rgba(255, 255, 255, 0.96)';
277
- this._roundRect(ctx, 0, 0, PANEL_CANVAS_WIDTH, layout.height, 32);
278
- ctx.fill();
279
- if (layout.image) {
280
- const { y: bandY, height: bandH } = layout.image;
281
- ctx.save();
282
- this._roundRect(ctx, PANEL_PAD_X, bandY, contentWidth, bandH, 16);
283
- ctx.clip();
284
- const image = this._loadedImage;
285
- if (image) {
286
- const scale = Math.max(contentWidth / image.width, bandH / image.height);
287
- const dw = image.width * scale;
288
- const dh = image.height * scale;
289
- ctx.drawImage(image, PANEL_PAD_X + (contentWidth - dw) / 2, bandY + (bandH - dh) / 2, dw, dh);
290
- } else {
291
- ctx.fillStyle = 'rgba(0, 0, 0, 0.06)';
292
- ctx.fillRect(PANEL_PAD_X, bandY, contentWidth, bandH);
293
- }
294
- ctx.restore();
295
- if (null !== layout.dotsY) {
296
- const bandCenterY = bandY + bandH / 2;
297
- this._drawArrow(ctx, ARROW_ZONE_W / 2, bandCenterY, true);
298
- this._drawArrow(ctx, PANEL_CANVAS_WIDTH - ARROW_ZONE_W / 2, bandCenterY, false);
299
- this._drawDots(ctx, images.length, layout.dotsY);
300
- }
301
- }
302
- if (layout.chip && this.label) {
303
- ctx.font = PANEL_FONTS.label;
304
- ctx.textBaseline = 'middle';
305
- ctx.fillStyle = 'rgba(44, 134, 88, 0.95)';
306
- this._roundRect(ctx, PANEL_PAD_X, layout.chip.y, layout.chip.width, layout.chip.height, 12);
307
- ctx.fill();
308
- ctx.fillStyle = '#ffffff';
309
- ctx.fillText(this.label, PANEL_PAD_X + 16, layout.chip.y + layout.chip.height / 2);
310
- }
311
- ctx.textBaseline = 'top';
312
- ctx.fillStyle = '#000000';
313
- ctx.font = PANEL_FONTS.title;
314
- layout.title.lines.forEach((line, index)=>{
315
- ctx.fillText(line, PANEL_PAD_X, layout.title.y + index * TITLE_LINE_HEIGHT);
316
- });
317
- if (layout.body.scrollable) {
318
- ctx.fillStyle = 'rgba(0, 0, 0, 0.08)';
319
- const trackX = PANEL_CANVAS_WIDTH - SCROLLBAR_INSET - SCROLLBAR_WIDTH;
320
- this._roundRect(ctx, trackX, layout.body.y, SCROLLBAR_WIDTH, layout.body.viewportHeight, SCROLLBAR_WIDTH / 2);
321
- ctx.fill();
322
- }
323
- this._chromeTexture?.upload();
305
+ ctx.textBaseline = 'top';
306
+ ctx.fillStyle = '#000000';
307
+ ctx.font = PANEL_FONTS.title;
308
+ layout.title.lines.forEach((line, index) => {
309
+ ctx.fillText(line, PANEL_PAD_X, layout.title.y + index * TITLE_LINE_HEIGHT);
310
+ });
311
+ if (layout.body.scrollable) {
312
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.08)';
313
+ const trackX = PANEL_CANVAS_WIDTH - SCROLLBAR_INSET - SCROLLBAR_WIDTH;
314
+ this._roundRect(ctx, trackX, layout.body.y, SCROLLBAR_WIDTH, layout.body.viewportHeight, SCROLLBAR_WIDTH / 2);
315
+ ctx.fill();
324
316
  }
325
- _drawText() {
326
- const body = this._layout?.body;
327
- if (!body) return;
328
- const height = Math.max(1, body.contentHeight);
329
- if (!this._textCanvas || this._textCanvas.height !== height) {
330
- this._textCanvas = document.createElement('canvas');
331
- this._textCanvas.width = PANEL_CANVAS_WIDTH;
332
- this._textCanvas.height = height;
333
- this._textTexture?.destroy();
334
- this._textTexture = this._createTexture('vr-annotation-panel-text', this._textCanvas);
335
- this._textMaterial?.destroy();
336
- this._textMaterial = this._createTexturedMaterial(this._textTexture);
337
- if (this._textInstance) this._textInstance.material = this._textMaterial;
338
- }
339
- const ctx = this._textCanvas.getContext('2d');
340
- if (!ctx) return;
341
- ctx.clearRect(0, 0, PANEL_CANVAS_WIDTH, height);
342
- ctx.textBaseline = 'top';
343
- ctx.font = PANEL_FONTS.body;
344
- ctx.fillStyle = 'rgba(0, 0, 0, 0.85)';
345
- body.lines.forEach((line, index)=>{
346
- ctx.fillText(line, PANEL_PAD_X, index * BODY_LINE_HEIGHT);
347
- });
348
- this._textTexture?.upload();
317
+ this._chromeTexture?.upload();
318
+ }
319
+
320
+ /**
321
+ * Paints every body line onto its own canvas, tall enough to hold all of them. Scrolling then
322
+ * only moves the quad's UV window over this texture, so no repaint or upload happens per frame.
323
+ */
324
+ _drawText() {
325
+ const body = this._layout?.body;
326
+ if (!body) {
327
+ return;
349
328
  }
350
- _setImage(index) {
351
- this._currentImage = index;
352
- this._loadedImage = void 0;
329
+ const height = Math.max(1, body.contentHeight);
330
+ if (!this._textCanvas || this._textCanvas.height !== height) {
331
+ this._textCanvas = document.createElement('canvas');
332
+ this._textCanvas.width = PANEL_CANVAS_WIDTH;
333
+ this._textCanvas.height = height;
334
+ this._textTexture?.destroy();
335
+ this._textTexture = this._createTexture('vr-annotation-panel-text', this._textCanvas);
336
+ this._textMaterial?.destroy();
337
+ this._textMaterial = this._createTexturedMaterial(this._textTexture);
338
+ if (this._textInstance) {
339
+ this._textInstance.material = this._textMaterial;
340
+ }
341
+ }
342
+ const ctx = this._textCanvas.getContext('2d');
343
+ if (!ctx) {
344
+ return;
345
+ }
346
+ ctx.clearRect(0, 0, PANEL_CANVAS_WIDTH, height);
347
+ ctx.textBaseline = 'top';
348
+ ctx.font = PANEL_FONTS.body;
349
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.85)';
350
+ body.lines.forEach((line, index) => {
351
+ ctx.fillText(line, PANEL_PAD_X, index * BODY_LINE_HEIGHT);
352
+ });
353
+ this._textTexture?.upload();
354
+ }
355
+ _setImage(index) {
356
+ this._currentImage = index;
357
+ this._loadedImage = undefined;
358
+ this._drawChrome();
359
+ this._loadCurrentImage();
360
+ }
361
+ _loadCurrentImage() {
362
+ const url = this.imageUrls?.[this._currentImage];
363
+ if (!url) {
364
+ return;
365
+ }
366
+ const token = ++this._loadToken;
367
+ const img = new Image();
368
+ img.crossOrigin = 'anonymous';
369
+ img.onload = () => {
370
+ // Ignore a stale load that a newer navigation has superseded.
371
+ if (token === this._loadToken) {
372
+ this._loadedImage = img;
353
373
  this._drawChrome();
354
- this._loadCurrentImage();
374
+ }
375
+ };
376
+ // onerror (e.g. a host without CORS headers): keep the placeholder + text already drawn.
377
+ img.src = url;
378
+ }
379
+ _rayQuadHit(origin, direction) {
380
+ const rotation = this.entity.getRotation();
381
+ rotation.transformVector(RIGHT_AXIS, this._axisRight);
382
+ rotation.transformVector(UP_AXIS, this._axisUp);
383
+ this.entity.getWorldTransform().getScale(this._scratchScale);
384
+ const halfWidth = PANEL_WIDTH / 2 * this._scratchScale.x;
385
+ const halfHeight = this._panelHeight / 2 * this._scratchScale.y;
386
+ return rayQuadHit(origin, direction, this.entity.getPosition(), this._axisRight, this._axisUp, halfWidth, halfHeight);
387
+ }
388
+
389
+ /** True when the ray points at the panel quad (used to suppress the empty-select dismiss). */
390
+ rayHitsPanel(origin, direction) {
391
+ return this._rayQuadHit(origin, direction) !== null;
392
+ }
393
+ update(dt) {
394
+ if (this.app.xr?.active !== true) {
395
+ return;
355
396
  }
356
- _loadCurrentImage() {
357
- const url = this.imageUrls?.[this._currentImage];
358
- if (!url) return;
359
- const token = ++this._loadToken;
360
- const img = new Image();
361
- img.crossOrigin = 'anonymous';
362
- img.onload = ()=>{
363
- if (token === this._loadToken) {
364
- this._loadedImage = img;
365
- this._drawChrome();
366
- }
367
- };
368
- img.src = url;
397
+ const signature = `${this.title}|${this.label ?? ''}|${this.bodyText ?? ''}|${(this.imageUrls ?? []).join(',')}`;
398
+ if (signature !== this._drawnSignature) {
399
+ this._drawnSignature = signature;
400
+ this._currentImage = 0;
401
+ this._scrollY = 0;
402
+ this._loadedImage = undefined;
403
+ this._rebuild();
404
+ this._loadCurrentImage();
369
405
  }
370
- _rayQuadHit(origin, direction) {
371
- const rotation = this.entity.getRotation();
372
- rotation.transformVector(RIGHT_AXIS, this._axisRight);
373
- rotation.transformVector(UP_AXIS, this._axisUp);
374
- this.entity.getWorldTransform().getScale(this._scratchScale);
375
- const halfWidth = PANEL_WIDTH / 2 * this._scratchScale.x;
376
- const halfHeight = this._panelHeight / 2 * this._scratchScale.y;
377
- return rayQuadHit(origin, direction, this.entity.getPosition(), this._axisRight, this._axisUp, halfWidth, halfHeight);
406
+ this._scroll(dt);
407
+ this._trackHead();
408
+ const anchor = this.app.root.findByName(`annotation-${this.annotationIndex}`);
409
+ if (!anchor) {
410
+ return;
378
411
  }
379
- rayHitsPanel(origin, direction) {
380
- return null !== this._rayQuadHit(origin, direction);
412
+ this._anchor.copy(anchor.getPosition());
413
+ this.entity.setPosition(this._anchor.x, this._anchor.y + this._panelHeight / 2 + PANEL_GAP, this._anchor.z);
414
+
415
+ // Billboard on yaw only: face the viewer horizontally but stay upright and level, so
416
+ // tilting/rolling the headset does not tilt the panel. Derived from the head's forward
417
+ // heading, so it matches the level head-rotation orientation but with pitch/roll removed.
418
+ this._headRotation.transformVector(FORWARD_Z, this._scratchDir);
419
+ const yaw = Math.atan2(this._scratchDir.x, this._scratchDir.z) * RAD_TO_DEG;
420
+ this._yawQuat.setFromEulerAngles(0, yaw, 0);
421
+ this.entity.setRotation(this._yawQuat);
422
+ }
423
+ _scroll(dt) {
424
+ if (this._maxScroll <= 0) {
425
+ return;
381
426
  }
382
- update(dt) {
383
- if (this.app.xr?.active !== true) return;
384
- const signature = `${this.title}|${this.label ?? ''}|${this.bodyText ?? ''}|${(this.imageUrls ?? []).join(',')}`;
385
- if (signature !== this._drawnSignature) {
386
- this._drawnSignature = signature;
387
- this._currentImage = 0;
388
- this._scrollY = 0;
389
- this._loadedImage = void 0;
390
- this._rebuild();
391
- this._loadCurrentImage();
392
- }
393
- this._scroll(dt);
394
- this._trackHead();
395
- const anchor = this.app.root.findByName(`annotation-${this.annotationIndex}`);
396
- if (!anchor) return;
397
- this._anchor.copy(anchor.getPosition());
398
- this.entity.setPosition(this._anchor.x, this._anchor.y + this._panelHeight / 2 + PANEL_GAP, this._anchor.z);
399
- this._headRotation.transformVector(FORWARD_Z, this._scratchDir);
400
- const yaw = Math.atan2(this._scratchDir.x, this._scratchDir.z) * RAD_TO_DEG;
401
- this._yawQuat.setFromEulerAngles(0, yaw, 0);
402
- this.entity.setRotation(this._yawQuat);
427
+ let axis = 0;
428
+ for (const inputSource of this.app.xr?.input?.inputSources ?? []) {
429
+ const sourceAxis = readScrollAxis(inputSource);
430
+ if (Math.abs(sourceAxis) > Math.abs(axis)) {
431
+ axis = sourceAxis;
432
+ }
403
433
  }
404
- _scroll(dt) {
405
- if (this._maxScroll <= 0) return;
406
- let axis = 0;
407
- for (const inputSource of this.app.xr?.input?.inputSources ?? []){
408
- const sourceAxis = readScrollAxis(inputSource);
409
- if (Math.abs(sourceAxis) > Math.abs(axis)) axis = sourceAxis;
410
- }
411
- const scrollY = nextScrollY(this._scrollY, axis, dt, this._maxScroll);
412
- if (scrollY === this._scrollY) return;
413
- this._scrollY = scrollY;
414
- this._writeTextQuad();
415
- this._writeThumbQuad();
434
+ const scrollY = nextScrollY(this._scrollY, axis, dt, this._maxScroll);
435
+ if (scrollY === this._scrollY) {
436
+ return;
416
437
  }
417
- _trackHead() {
418
- const views = this.app.xr?.views?.list;
419
- if (!views || 0 === views.length) return;
420
- this._headRotation.setFromMat4(views[0].viewInvOffMat);
438
+ this._scrollY = scrollY;
439
+ this._writeTextQuad();
440
+ this._writeThumbQuad();
441
+ }
442
+ _trackHead() {
443
+ const views = this.app.xr?.views?.list;
444
+ if (!views || views.length === 0) {
445
+ return;
421
446
  }
422
- _teardown() {
423
- this.app.xr?.input?.off('select', this._onSelect);
424
- if (this.entity.render) this.entity.render.meshInstances = [];
425
- this._chromeMesh?.destroy();
426
- this._textMesh?.destroy();
427
- this._thumbMesh?.destroy();
428
- this._chromeMaterial?.destroy();
429
- this._textMaterial?.destroy();
430
- this._thumbMaterial?.destroy();
431
- this._chromeTexture?.destroy();
432
- this._textTexture?.destroy();
433
- this._chromeMesh = void 0;
434
- this._textMesh = void 0;
435
- this._thumbMesh = void 0;
436
- this._chromeMaterial = void 0;
437
- this._textMaterial = void 0;
438
- this._thumbMaterial = void 0;
439
- this._chromeTexture = void 0;
440
- this._textTexture = void 0;
447
+ this._headRotation.setFromMat4(views[0].viewInvOffMat);
448
+ }
449
+ _teardown() {
450
+ this.app.xr?.input?.off('select', this._onSelect);
451
+ if (this.entity.render) {
452
+ this.entity.render.meshInstances = [];
441
453
  }
454
+ this._chromeMesh?.destroy();
455
+ this._textMesh?.destroy();
456
+ this._thumbMesh?.destroy();
457
+ this._chromeMaterial?.destroy();
458
+ this._textMaterial?.destroy();
459
+ this._thumbMaterial?.destroy();
460
+ this._chromeTexture?.destroy();
461
+ this._textTexture?.destroy();
462
+ this._chromeMesh = undefined;
463
+ this._textMesh = undefined;
464
+ this._thumbMesh = undefined;
465
+ this._chromeMaterial = undefined;
466
+ this._textMaterial = undefined;
467
+ this._thumbMaterial = undefined;
468
+ this._chromeTexture = undefined;
469
+ this._textTexture = undefined;
470
+ }
442
471
  }
472
+
443
473
  export { VrAnnotationPanel };