@terraware/web-components 5.0.27 → 5.1.0

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 +19 -22
  2. package/components/Autocomplete/Autocomplete.js +106 -130
  3. package/components/Badge/index.js +28 -29
  4. package/components/BusySpinner/index.js +40 -45
  5. package/components/Button/Button.js +41 -47
  6. package/components/Checkbox.js +64 -63
  7. package/components/Confirm/index.js +74 -83
  8. package/components/DatePicker/DatePicker.js +138 -141
  9. package/components/DialogBox/DialogBox.js +111 -118
  10. package/components/Divisor.js +18 -18
  11. package/components/Dropdown.js +66 -94
  12. package/components/EditableTable/index.js +211 -271
  13. package/components/ErrorBox/ErrorBox.js +47 -49
  14. package/components/FileChooser/index.js +117 -135
  15. package/components/FormBottomBar/index.js +75 -84
  16. package/components/Icon/Icon.js +12 -20
  17. package/components/Icon/icons/index.js +256 -256
  18. package/components/IconTooltip/index.js +49 -60
  19. package/components/Map/MapBox.js +545 -452
  20. package/components/Map/MapContainer.js +25 -29
  21. package/components/Map/MapDateSliderControl.js +64 -64
  22. package/components/Map/MapDrawer.js +41 -47
  23. package/components/Map/MapLegend.js +176 -172
  24. package/components/Map/MapViewStyleControl.js +59 -63
  25. package/components/Map/ReactMapboxWrapper.js +3 -2
  26. package/components/Map/index.js +161 -180
  27. package/components/Map/types.js +12 -17
  28. package/components/Map/useMaintainLayerOrder.js +16 -26
  29. package/components/Markdown/index.js +25 -26
  30. package/components/Message/Message.js +73 -76
  31. package/components/MultiSelect/index.js +112 -124
  32. package/components/Navbar/NavFooter.js +9 -13
  33. package/components/Navbar/NavItem.js +70 -76
  34. package/components/Navbar/NavSection.js +17 -19
  35. package/components/Navbar/Navbar.js +39 -46
  36. package/components/Navbar/SubNavbar.js +9 -13
  37. package/components/Note.js +18 -22
  38. package/components/OverlayModal/OverlayModal.js +56 -63
  39. package/components/PageForm/index.js +33 -41
  40. package/components/PhotoChooser/index.js +165 -173
  41. package/components/PhotosCarousel/PhotoSlide.js +32 -35
  42. package/components/PhotosCarousel/index.js +80 -85
  43. package/components/PhotosCarousel/usePhotosCarousel.js +69 -76
  44. package/components/Pill/index.js +41 -45
  45. package/components/PillList/index.js +17 -24
  46. package/components/PlacementWrapper/index.js +38 -59
  47. package/components/PopoverMenu/Popover.js +68 -85
  48. package/components/PopoverMenu/index.js +51 -56
  49. package/components/PopoverMultiSelect/index.js +52 -59
  50. package/components/ProgressCircle/ProgressCircle.js +28 -32
  51. package/components/RadioButton.js +17 -19
  52. package/components/Select/Select.js +13 -15
  53. package/components/Select/SelectT.js +235 -250
  54. package/components/Separator/index.js +14 -17
  55. package/components/Size.js +0 -1
  56. package/components/Slider/index.js +43 -56
  57. package/components/SummaryBox.js +70 -72
  58. package/components/Tabs/index.js +133 -135
  59. package/components/TextTruncated/index.js +117 -130
  60. package/components/Textfield/Textfield.js +158 -190
  61. package/components/Textfield/TruncatedTextArea.js +70 -84
  62. package/components/TimelineSlider/TimelineMarker.js +25 -30
  63. package/components/TimelineSlider/TimelineRail.js +8 -11
  64. package/components/TimelineSlider/index.js +64 -66
  65. package/components/TimelineSliderV2/ClusterBadge.js +25 -29
  66. package/components/TimelineSliderV2/TimelineDot.js +31 -35
  67. package/components/TimelineSliderV2/TimelineRail.js +8 -11
  68. package/components/TimelineSliderV2/index.js +121 -103
  69. package/components/TimelineSliderV2/layout.js +124 -147
  70. package/components/TimelineSliderV2/types.js +0 -1
  71. package/components/Tooltip/Tooltip.js +47 -56
  72. package/components/ViewPhotosDialog/index.js +28 -35
  73. package/components/VirtualWalkthrough/Annotation.js +163 -207
  74. package/components/VirtualWalkthrough/AnnotationEditPane.js +198 -184
  75. package/components/VirtualWalkthrough/AnnotationPanel.js +187 -199
  76. package/components/VirtualWalkthrough/Application.js +31 -34
  77. package/components/VirtualWalkthrough/AutoRotator.js +85 -223
  78. package/components/VirtualWalkthrough/BlockingSpinner.js +26 -28
  79. package/components/VirtualWalkthrough/BoundaryRing.js +30 -34
  80. package/components/VirtualWalkthrough/BoundaryWall.js +32 -36
  81. package/components/VirtualWalkthrough/CameraInfo.js +100 -89
  82. package/components/VirtualWalkthrough/ControlsInfoPane.js +254 -195
  83. package/components/VirtualWalkthrough/GradientSky.js +72 -73
  84. package/components/VirtualWalkthrough/SplatControls.js +209 -221
  85. package/components/VirtualWalkthrough/SplatCrop.js +21 -20
  86. package/components/VirtualWalkthrough/SplatFadeCrop.js +21 -20
  87. package/components/VirtualWalkthrough/SplatModel.js +65 -85
  88. package/components/VirtualWalkthrough/SplatRevealRain.js +41 -46
  89. package/components/VirtualWalkthrough/TfAnnotationManager.js +197 -379
  90. package/components/VirtualWalkthrough/TfXrNavigation.js +73 -162
  91. package/components/VirtualWalkthrough/VirtualWalkthroughViewer.js +438 -431
  92. package/components/VirtualWalkthrough/VrAnnotationPanel.js +18 -22
  93. package/components/VirtualWalkthrough/XrAnnotationInteraction.js +14 -17
  94. package/components/VirtualWalkthrough/XrExitButton.js +14 -17
  95. package/components/VirtualWalkthrough/XrGazeDwell.js +14 -17
  96. package/components/VirtualWalkthrough/XrPointerRay.js +14 -17
  97. package/components/VirtualWalkthrough/XrStartPose.js +67 -121
  98. package/components/VirtualWalkthrough/boundary-ring.js +87 -117
  99. package/components/VirtualWalkthrough/boundary-wall.js +175 -258
  100. package/components/VirtualWalkthrough/camera-adoption.js +18 -64
  101. package/components/VirtualWalkthrough/groundPlane.js +32 -47
  102. package/components/VirtualWalkthrough/gsplat-fade-crop-effect.js +35 -35
  103. package/components/VirtualWalkthrough/splatFormat.js +13 -47
  104. package/components/VirtualWalkthrough/useAdoptedCamera.js +23 -44
  105. package/components/VirtualWalkthrough/useRestartOnVr.js +26 -37
  106. package/components/VirtualWalkthrough/useXrRenderTuning.js +33 -54
  107. package/components/VirtualWalkthrough/vr-annotation-panel-layout.js +67 -99
  108. package/components/VirtualWalkthrough/vr-annotation-panel.js +403 -433
  109. package/components/VirtualWalkthrough/vr-annotation-scroll.js +10 -28
  110. package/components/VirtualWalkthrough/walkthrough-camera.js +204 -329
  111. package/components/VirtualWalkthrough/xr-annotation-candidate-buffer.js +39 -80
  112. package/components/VirtualWalkthrough/xr-annotation-candidates.js +6 -18
  113. package/components/VirtualWalkthrough/xr-annotation-interaction.js +31 -56
  114. package/components/VirtualWalkthrough/xr-annotation-targeting.js +22 -38
  115. package/components/VirtualWalkthrough/xr-button-arming.js +7 -17
  116. package/components/VirtualWalkthrough/xr-button-hold.js +22 -36
  117. package/components/VirtualWalkthrough/xr-exit-button-mask.js +22 -36
  118. package/components/VirtualWalkthrough/xr-exit-button.js +148 -207
  119. package/components/VirtualWalkthrough/xr-face-buttons.js +19 -29
  120. package/components/VirtualWalkthrough/xr-gaze-dwell-state.js +23 -34
  121. package/components/VirtualWalkthrough/xr-gaze-dwell.js +107 -144
  122. package/components/VirtualWalkthrough/xr-interactive-ui.js +12 -24
  123. package/components/VirtualWalkthrough/xr-pointer-ray.js +13 -20
  124. package/components/VirtualWalkthrough/xr-progress-pie-material.js +117 -88
  125. package/components/VirtualWalkthrough/xr-progress-pie.js +1 -10
  126. package/components/VirtualWalkthrough/xr-ray-quad.js +16 -30
  127. package/components/VirtualWalkthrough/xr-render-tuning.js +17 -48
  128. package/components/VirtualWalkthrough/xr-scene-bounds.js +21 -37
  129. package/components/VirtualWalkthrough/xr-start-pose.js +27 -73
  130. package/components/VirtualWalkthrough/xr-teleport-gesture.js +16 -42
  131. package/components/svg/BlobbyGrayIconImage.js +41 -39
  132. package/components/svg/BlobbyGrayIconImport.js +41 -39
  133. package/components/svg/BlobbyGrayIconUploadToTheCloud.js +41 -39
  134. package/components/svg/BlobbyIconCloudChecked.js +202 -164
  135. package/components/svg/BlobbyIconExportOrUploadPhoto.js +202 -164
  136. package/components/svg/BlobbyIconFolder.js +202 -164
  137. package/components/svg/BlobbyIconGraphReport.js +202 -164
  138. package/components/svg/BlobbyIconHappy.js +202 -164
  139. package/components/svg/BlobbyIconHeartMonitor.js +202 -164
  140. package/components/svg/BlobbyIconLeaf.js +202 -164
  141. package/components/svg/BlobbyIconLibrary.js +202 -164
  142. package/components/svg/BlobbyIconNursery.js +204 -166
  143. package/components/svg/BlobbyIconOrganization.js +202 -164
  144. package/components/svg/BlobbyIconParchment.js +202 -164
  145. package/components/svg/BlobbyIconPeople.js +202 -164
  146. package/components/svg/BlobbyIconSeedBank.js +202 -164
  147. package/components/svg/BlobbyIconSeedling.js +202 -164
  148. package/components/svg/BlobbyIconSeeds.js +202 -164
  149. package/components/svg/BlobbyIconSite.js +206 -167
  150. package/components/svg/BlobbyIconSparkles.js +202 -164
  151. package/components/svg/BlobbyIconWrench.js +202 -164
  152. package/components/svg/Bug.js +19 -21
  153. package/components/svg/Calendar.js +19 -21
  154. package/components/svg/CaretDown.js +19 -21
  155. package/components/svg/CaretLeft.js +19 -21
  156. package/components/svg/CaretRight.js +19 -21
  157. package/components/svg/CaretUp.js +19 -21
  158. package/components/svg/ChevronDown.js +19 -21
  159. package/components/svg/ChevronUp.js +19 -21
  160. package/components/svg/Close.js +19 -21
  161. package/components/svg/Critical.js +19 -21
  162. package/components/svg/Edit.js +202 -164
  163. package/components/svg/Error.js +19 -21
  164. package/components/svg/Filter.js +19 -21
  165. package/components/svg/Folder.js +19 -21
  166. package/components/svg/Help.js +19 -21
  167. package/components/svg/Home.js +19 -21
  168. package/components/svg/IconAdd.js +19 -21
  169. package/components/svg/IconArrowRight.js +21 -23
  170. package/components/svg/IconBusinessNetwork.js +19 -21
  171. package/components/svg/IconCalendar.js +19 -21
  172. package/components/svg/IconCancel.js +19 -21
  173. package/components/svg/IconChargingBattery.js +19 -21
  174. package/components/svg/IconChecklist.js +21 -23
  175. package/components/svg/IconCheckmark.js +19 -21
  176. package/components/svg/IconCoinInHand.js +19 -21
  177. package/components/svg/IconColumns.js +19 -21
  178. package/components/svg/IconComment.js +20 -22
  179. package/components/svg/IconDashboard.js +19 -21
  180. package/components/svg/IconDataMigration.js +19 -21
  181. package/components/svg/IconDocument.js +21 -23
  182. package/components/svg/IconDownloadFromTheCloud.js +21 -23
  183. package/components/svg/IconEdit.js +21 -23
  184. package/components/svg/IconExpand.js +21 -23
  185. package/components/svg/IconExport.js +19 -21
  186. package/components/svg/IconExternalLink.js +19 -21
  187. package/components/svg/IconEye.js +19 -21
  188. package/components/svg/IconEyeOff.js +19 -21
  189. package/components/svg/IconFile.js +19 -21
  190. package/components/svg/IconFolder.js +19 -21
  191. package/components/svg/IconFullScreen.js +20 -22
  192. package/components/svg/IconFutures.js +19 -21
  193. package/components/svg/IconGraphReport.js +19 -21
  194. package/components/svg/IconGrid.js +19 -21
  195. package/components/svg/IconHeartMonitor.js +19 -21
  196. package/components/svg/IconHelp.js +21 -23
  197. package/components/svg/IconHistory.js +19 -21
  198. package/components/svg/IconImport.js +19 -21
  199. package/components/svg/IconIndex.js +19 -21
  200. package/components/svg/IconLayers.js +19 -21
  201. package/components/svg/IconLibrary.js +20 -22
  202. package/components/svg/IconList.js +19 -21
  203. package/components/svg/IconLivePlant.js +19 -21
  204. package/components/svg/IconLocations.js +19 -21
  205. package/components/svg/IconMail.js +19 -21
  206. package/components/svg/IconManager.js +19 -21
  207. package/components/svg/IconMap.js +19 -21
  208. package/components/svg/IconMarker.js +19 -21
  209. package/components/svg/IconMenu.js +19 -21
  210. package/components/svg/IconMenuHorizontal.js +19 -21
  211. package/components/svg/IconModule.js +19 -21
  212. package/components/svg/IconMyLocation.js +19 -21
  213. package/components/svg/IconNote.js +21 -23
  214. package/components/svg/IconNursery.js +21 -23
  215. package/components/svg/IconOrg.js +19 -21
  216. package/components/svg/IconParchment.js +19 -21
  217. package/components/svg/IconPhoto.js +19 -21
  218. package/components/svg/IconPlantsFilled.js +19 -21
  219. package/components/svg/IconRedo.js +19 -21
  220. package/components/svg/IconRestorationSite.js +19 -21
  221. package/components/svg/IconSeedBank.js +19 -21
  222. package/components/svg/IconSeedling.js +19 -21
  223. package/components/svg/IconSettings.js +19 -21
  224. package/components/svg/IconSlice.js +19 -21
  225. package/components/svg/IconSubmit.js +19 -21
  226. package/components/svg/IconSubtract.js +21 -23
  227. package/components/svg/IconSynced.js +19 -21
  228. package/components/svg/IconTrashCan.js +19 -21
  229. package/components/svg/IconTreasureMap.js +19 -21
  230. package/components/svg/IconUnavailable.js +19 -21
  231. package/components/svg/IconUndo.js +19 -21
  232. package/components/svg/IconVariable.js +19 -21
  233. package/components/svg/IconVideo.js +30 -31
  234. package/components/svg/IconWifi.js +19 -21
  235. package/components/svg/Info.js +19 -21
  236. package/components/svg/Key.js +19 -21
  237. package/components/svg/Leaf.js +19 -21
  238. package/components/svg/Lock.js +19 -21
  239. package/components/svg/Logo.js +48 -43
  240. package/components/svg/MenuVertical.js +19 -21
  241. package/components/svg/Notification.js +19 -21
  242. package/components/svg/Person.js +19 -21
  243. package/components/svg/Plus.js +19 -21
  244. package/components/svg/RestorationSite.js +19 -21
  245. package/components/svg/Search.js +19 -21
  246. package/components/svg/Seeds.js +19 -21
  247. package/components/svg/Site.js +19 -21
  248. package/components/svg/Sparkles.js +19 -21
  249. package/components/svg/Species.js +19 -21
  250. package/components/svg/Species2.js +202 -164
  251. package/components/svg/Spinner.js +54 -48
  252. package/components/svg/Star.js +19 -21
  253. package/components/svg/Success.js +19 -21
  254. package/components/svg/SuccessFilled.js +19 -21
  255. package/components/svg/TerrawareLogoDesktop.js +55 -49
  256. package/components/svg/TerrawareLogoMobile.js +25 -26
  257. package/components/svg/Touchscreen.js +19 -21
  258. package/components/svg/UploadCloud.js +202 -164
  259. package/components/svg/Warning.js +19 -21
  260. package/components/svg/WelcomeClipboard.js +156 -133
  261. package/components/svg/index.js +129 -129
  262. package/components/table/EnhancedTableToolbar.js +68 -74
  263. package/components/table/EnhancedTableToolbarV2.js +31 -41
  264. package/components/table/TableCellRenderer.js +196 -231
  265. package/components/table/TableHeader.js +71 -73
  266. package/components/table/TableHeaderItem.js +97 -102
  267. package/components/table/density.js +18 -19
  268. package/components/table/index.js +350 -369
  269. package/components/table/sort.js +27 -52
  270. package/components/table/types.js +0 -1
  271. package/components/types/index.js +0 -1
  272. package/hooks/cameraEntityContext.js +2 -13
  273. package/hooks/useBoolean.js +17 -13
  274. package/hooks/useCameraPosition.js +56 -56
  275. package/hooks/useDevicePerformance.js +17 -17
  276. package/hooks/useXr.js +85 -87
  277. package/index.js +58 -58
  278. package/package.json +1 -1
  279. package/setupTests.js +0 -4
  280. package/strings/export.js +111 -170
  281. package/strings/index.js +25 -54
  282. package/strings/locales.js +21 -41
  283. package/strings/strings-en.js +49 -51
  284. package/strings/strings-es.js +49 -51
  285. package/strings/strings-fr.js +49 -51
  286. package/strings/strings-gx.js +49 -51
  287. package/style-dictionary-dist/TerrawareTheme.js +186 -186
  288. package/theme.js +74 -76
  289. package/utils/color.js +8 -15
  290. package/utils/date.js +42 -108
  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 +42 -79
  295. package/virtualWalkthrough.js +23 -23
@@ -1,473 +1,443 @@
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
-
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";
6
5
  const PANEL_WIDTH = 7.5;
7
-
8
- /** Gap (m) between the top of the hotspot and the bottom edge of the panel. */
9
6
  const PANEL_GAP = 0.75;
10
7
  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. */
15
8
  const TEXT_Z = 0.001;
16
9
  const THUMB_Z = 0.002;
17
10
  const SCROLLBAR_WIDTH = 10;
18
11
  const SCROLLBAR_INSET = 18;
19
12
  const SCROLLBAR_MIN_HEIGHT = 48;
20
-
21
- /** Local +z (the quad's front face) — used to derive the panel's yaw toward the viewer. */
22
13
  const FORWARD_Z = new Vec3(0, 0, 1);
23
- /** Quad in-plane axes in local space, transformed to world for ray hit-testing. */
24
14
  const RIGHT_AXIS = new Vec3(1, 0, 0);
25
15
  const UP_AXIS = new Vec3(0, 1, 0);
26
16
  const RAD_TO_DEG = 180 / Math.PI;
27
-
28
- /** Width (canvas px) of the left/right arrow hit + draw zones over the image band. */
29
17
  const ARROW_ZONE_W = 170;
30
18
  class VrAnnotationPanel extends Script {
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;
54
- }
55
- const images = this.imageUrls ?? [];
56
- const band = this._layout?.image;
57
- if (images.length <= 1 || !band) {
58
- return;
59
- }
60
- const hit = this._rayQuadHit(inputSource.getOrigin(), inputSource.getDirection());
61
- if (!hit) {
62
- return;
63
- }
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;
69
- }
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);
74
- }
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;
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());
160
101
  }
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;
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;
171
115
  }
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;
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;
178
129
  }
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();
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;
219
156
  }
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;
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();
227
183
  }
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;
184
+ _localY(canvasY) {
185
+ return this._panelHeight / 2 - canvasY * PX_TO_M;
253
186
  }
254
- if (this._thumbInstance) {
255
- this._thumbInstance.visible = body.scrollable;
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);
256
192
  }
257
- }
258
- _drawChrome() {
259
- const ctx = this._chromeCanvas?.getContext('2d');
260
- const layout = this._layout;
261
- if (!ctx || !layout) {
262
- return;
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);
263
197
  }
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
- }
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);
295
206
  }
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);
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();
304
215
  }
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();
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();
316
231
  }
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;
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
+ }
328
241
  }
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
- }
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;
341
268
  }
342
- const ctx = this._textCanvas.getContext('2d');
343
- if (!ctx) {
344
- return;
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();
345
324
  }
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;
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();
365
349
  }
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;
350
+ _setImage(index) {
351
+ this._currentImage = index;
352
+ this._loadedImage = void 0;
373
353
  this._drawChrome();
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;
354
+ this._loadCurrentImage();
396
355
  }
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();
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;
405
369
  }
406
- this._scroll(dt);
407
- this._trackHead();
408
- const anchor = this.app.root.findByName(`annotation-${this.annotationIndex}`);
409
- if (!anchor) {
410
- return;
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);
411
378
  }
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;
379
+ rayHitsPanel(origin, direction) {
380
+ return null !== this._rayQuadHit(origin, direction);
426
381
  }
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
- }
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);
433
403
  }
434
- const scrollY = nextScrollY(this._scrollY, axis, dt, this._maxScroll);
435
- if (scrollY === this._scrollY) {
436
- return;
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();
437
416
  }
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;
417
+ _trackHead() {
418
+ const views = this.app.xr?.views?.list;
419
+ if (!views || 0 === views.length) return;
420
+ this._headRotation.setFromMat4(views[0].viewInvOffMat);
446
421
  }
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 = [];
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;
453
441
  }
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
- }
471
442
  }
472
-
473
443
  export { VrAnnotationPanel };