@terraware/web-components 5.0.27 → 5.1.1-rc.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 (311) 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/Stepper/index.d.ts +33 -0
  58. package/components/Stepper/index.d.ts.map +1 -0
  59. package/components/Stepper/index.js +66 -0
  60. package/components/SummaryBox.js +70 -72
  61. package/components/Tabs/index.js +133 -135
  62. package/components/TextTruncated/index.js +117 -130
  63. package/components/Textfield/Textfield.js +158 -190
  64. package/components/Textfield/TruncatedTextArea.js +70 -84
  65. package/components/TimelineSlider/TimelineMarker.js +25 -30
  66. package/components/TimelineSlider/TimelineRail.js +8 -11
  67. package/components/TimelineSlider/index.js +64 -66
  68. package/components/TimelineSliderV2/ClusterBadge.js +25 -29
  69. package/components/TimelineSliderV2/TimelineDot.js +31 -35
  70. package/components/TimelineSliderV2/TimelineRail.js +8 -11
  71. package/components/TimelineSliderV2/index.js +121 -103
  72. package/components/TimelineSliderV2/layout.js +124 -147
  73. package/components/TimelineSliderV2/types.js +0 -1
  74. package/components/Tooltip/Tooltip.js +47 -56
  75. package/components/ViewPhotosDialog/index.js +28 -35
  76. package/components/VirtualWalkthrough/Annotation.js +163 -207
  77. package/components/VirtualWalkthrough/AnnotationEditPane.js +198 -184
  78. package/components/VirtualWalkthrough/AnnotationPanel.js +187 -199
  79. package/components/VirtualWalkthrough/Application.js +31 -34
  80. package/components/VirtualWalkthrough/AutoRotator.js +85 -223
  81. package/components/VirtualWalkthrough/BlockingSpinner.js +26 -28
  82. package/components/VirtualWalkthrough/BoundaryRing.js +30 -34
  83. package/components/VirtualWalkthrough/BoundaryWall.js +32 -36
  84. package/components/VirtualWalkthrough/CameraInfo.js +100 -89
  85. package/components/VirtualWalkthrough/ControlsInfoPane.js +254 -195
  86. package/components/VirtualWalkthrough/GradientSky.js +72 -73
  87. package/components/VirtualWalkthrough/SplatControls.js +209 -221
  88. package/components/VirtualWalkthrough/SplatCrop.js +21 -20
  89. package/components/VirtualWalkthrough/SplatFadeCrop.js +21 -20
  90. package/components/VirtualWalkthrough/SplatModel.js +65 -85
  91. package/components/VirtualWalkthrough/SplatRevealRain.js +41 -46
  92. package/components/VirtualWalkthrough/TfAnnotationManager.js +197 -379
  93. package/components/VirtualWalkthrough/TfXrNavigation.js +73 -162
  94. package/components/VirtualWalkthrough/VirtualWalkthroughViewer.js +438 -431
  95. package/components/VirtualWalkthrough/VrAnnotationPanel.js +18 -22
  96. package/components/VirtualWalkthrough/XrAnnotationInteraction.js +14 -17
  97. package/components/VirtualWalkthrough/XrExitButton.js +14 -17
  98. package/components/VirtualWalkthrough/XrGazeDwell.js +14 -17
  99. package/components/VirtualWalkthrough/XrPointerRay.js +14 -17
  100. package/components/VirtualWalkthrough/XrStartPose.js +67 -121
  101. package/components/VirtualWalkthrough/boundary-ring.js +87 -117
  102. package/components/VirtualWalkthrough/boundary-wall.js +175 -258
  103. package/components/VirtualWalkthrough/camera-adoption.js +18 -64
  104. package/components/VirtualWalkthrough/groundPlane.js +32 -47
  105. package/components/VirtualWalkthrough/gsplat-fade-crop-effect.js +35 -35
  106. package/components/VirtualWalkthrough/splatFormat.js +13 -47
  107. package/components/VirtualWalkthrough/useAdoptedCamera.js +23 -44
  108. package/components/VirtualWalkthrough/useRestartOnVr.js +26 -37
  109. package/components/VirtualWalkthrough/useXrRenderTuning.js +33 -54
  110. package/components/VirtualWalkthrough/vr-annotation-panel-layout.js +67 -99
  111. package/components/VirtualWalkthrough/vr-annotation-panel.js +403 -433
  112. package/components/VirtualWalkthrough/vr-annotation-scroll.js +10 -28
  113. package/components/VirtualWalkthrough/walkthrough-camera.js +204 -329
  114. package/components/VirtualWalkthrough/xr-annotation-candidate-buffer.js +39 -80
  115. package/components/VirtualWalkthrough/xr-annotation-candidates.js +6 -18
  116. package/components/VirtualWalkthrough/xr-annotation-interaction.js +31 -56
  117. package/components/VirtualWalkthrough/xr-annotation-targeting.js +22 -38
  118. package/components/VirtualWalkthrough/xr-button-arming.js +7 -17
  119. package/components/VirtualWalkthrough/xr-button-hold.js +22 -36
  120. package/components/VirtualWalkthrough/xr-exit-button-mask.js +22 -36
  121. package/components/VirtualWalkthrough/xr-exit-button.js +148 -207
  122. package/components/VirtualWalkthrough/xr-face-buttons.js +19 -29
  123. package/components/VirtualWalkthrough/xr-gaze-dwell-state.js +23 -34
  124. package/components/VirtualWalkthrough/xr-gaze-dwell.js +107 -144
  125. package/components/VirtualWalkthrough/xr-interactive-ui.js +12 -24
  126. package/components/VirtualWalkthrough/xr-pointer-ray.js +13 -20
  127. package/components/VirtualWalkthrough/xr-progress-pie-material.js +117 -88
  128. package/components/VirtualWalkthrough/xr-progress-pie.js +1 -10
  129. package/components/VirtualWalkthrough/xr-ray-quad.js +16 -30
  130. package/components/VirtualWalkthrough/xr-render-tuning.js +17 -48
  131. package/components/VirtualWalkthrough/xr-scene-bounds.js +21 -37
  132. package/components/VirtualWalkthrough/xr-start-pose.js +27 -73
  133. package/components/VirtualWalkthrough/xr-teleport-gesture.js +16 -42
  134. package/components/svg/BlobbyGrayIconImage.js +41 -39
  135. package/components/svg/BlobbyGrayIconImport.js +41 -39
  136. package/components/svg/BlobbyGrayIconUploadToTheCloud.js +41 -39
  137. package/components/svg/BlobbyIconCloudChecked.js +202 -164
  138. package/components/svg/BlobbyIconExportOrUploadPhoto.js +202 -164
  139. package/components/svg/BlobbyIconFolder.js +202 -164
  140. package/components/svg/BlobbyIconGraphReport.js +202 -164
  141. package/components/svg/BlobbyIconHappy.js +202 -164
  142. package/components/svg/BlobbyIconHeartMonitor.js +202 -164
  143. package/components/svg/BlobbyIconLeaf.js +202 -164
  144. package/components/svg/BlobbyIconLibrary.js +202 -164
  145. package/components/svg/BlobbyIconNursery.js +204 -166
  146. package/components/svg/BlobbyIconOrganization.js +202 -164
  147. package/components/svg/BlobbyIconParchment.js +202 -164
  148. package/components/svg/BlobbyIconPeople.js +202 -164
  149. package/components/svg/BlobbyIconSeedBank.js +202 -164
  150. package/components/svg/BlobbyIconSeedling.js +202 -164
  151. package/components/svg/BlobbyIconSeeds.js +202 -164
  152. package/components/svg/BlobbyIconSite.js +206 -167
  153. package/components/svg/BlobbyIconSparkles.js +202 -164
  154. package/components/svg/BlobbyIconWrench.js +202 -164
  155. package/components/svg/Bug.js +19 -21
  156. package/components/svg/Calendar.js +19 -21
  157. package/components/svg/CaretDown.js +19 -21
  158. package/components/svg/CaretLeft.js +19 -21
  159. package/components/svg/CaretRight.js +19 -21
  160. package/components/svg/CaretUp.js +19 -21
  161. package/components/svg/ChevronDown.js +19 -21
  162. package/components/svg/ChevronUp.js +19 -21
  163. package/components/svg/Close.js +19 -21
  164. package/components/svg/Critical.js +19 -21
  165. package/components/svg/Edit.js +202 -164
  166. package/components/svg/Error.js +19 -21
  167. package/components/svg/Filter.js +19 -21
  168. package/components/svg/Folder.js +19 -21
  169. package/components/svg/Help.js +19 -21
  170. package/components/svg/Home.js +19 -21
  171. package/components/svg/IconAdd.js +19 -21
  172. package/components/svg/IconArrowRight.js +21 -23
  173. package/components/svg/IconBusinessNetwork.js +19 -21
  174. package/components/svg/IconCalendar.js +19 -21
  175. package/components/svg/IconCancel.js +19 -21
  176. package/components/svg/IconChargingBattery.js +19 -21
  177. package/components/svg/IconChecklist.js +21 -23
  178. package/components/svg/IconCheckmark.js +19 -21
  179. package/components/svg/IconCoinInHand.js +19 -21
  180. package/components/svg/IconColumns.js +19 -21
  181. package/components/svg/IconComment.js +20 -22
  182. package/components/svg/IconDashboard.js +19 -21
  183. package/components/svg/IconDataMigration.js +19 -21
  184. package/components/svg/IconDocument.js +21 -23
  185. package/components/svg/IconDownloadFromTheCloud.js +21 -23
  186. package/components/svg/IconEdit.js +21 -23
  187. package/components/svg/IconExpand.js +21 -23
  188. package/components/svg/IconExport.js +19 -21
  189. package/components/svg/IconExternalLink.js +19 -21
  190. package/components/svg/IconEye.js +19 -21
  191. package/components/svg/IconEyeOff.js +19 -21
  192. package/components/svg/IconFile.js +19 -21
  193. package/components/svg/IconFolder.js +19 -21
  194. package/components/svg/IconFullScreen.js +20 -22
  195. package/components/svg/IconFutures.js +19 -21
  196. package/components/svg/IconGraphReport.js +19 -21
  197. package/components/svg/IconGrid.js +19 -21
  198. package/components/svg/IconHeartMonitor.js +19 -21
  199. package/components/svg/IconHelp.js +21 -23
  200. package/components/svg/IconHistory.js +19 -21
  201. package/components/svg/IconImport.js +19 -21
  202. package/components/svg/IconIndex.js +19 -21
  203. package/components/svg/IconLayers.js +19 -21
  204. package/components/svg/IconLibrary.js +20 -22
  205. package/components/svg/IconList.js +19 -21
  206. package/components/svg/IconLivePlant.js +19 -21
  207. package/components/svg/IconLocations.js +19 -21
  208. package/components/svg/IconMail.js +19 -21
  209. package/components/svg/IconManager.js +19 -21
  210. package/components/svg/IconMap.js +19 -21
  211. package/components/svg/IconMarker.js +19 -21
  212. package/components/svg/IconMenu.js +19 -21
  213. package/components/svg/IconMenuHorizontal.js +19 -21
  214. package/components/svg/IconModule.js +19 -21
  215. package/components/svg/IconMyLocation.js +19 -21
  216. package/components/svg/IconNote.js +21 -23
  217. package/components/svg/IconNursery.js +21 -23
  218. package/components/svg/IconOrg.js +19 -21
  219. package/components/svg/IconParchment.js +19 -21
  220. package/components/svg/IconPhoto.js +19 -21
  221. package/components/svg/IconPlantsFilled.js +19 -21
  222. package/components/svg/IconRedo.js +19 -21
  223. package/components/svg/IconRestorationSite.js +19 -21
  224. package/components/svg/IconSeedBank.js +19 -21
  225. package/components/svg/IconSeedling.js +19 -21
  226. package/components/svg/IconSettings.js +19 -21
  227. package/components/svg/IconSlice.js +19 -21
  228. package/components/svg/IconSubmit.js +19 -21
  229. package/components/svg/IconSubtract.js +21 -23
  230. package/components/svg/IconSynced.js +19 -21
  231. package/components/svg/IconTrashCan.js +19 -21
  232. package/components/svg/IconTreasureMap.js +19 -21
  233. package/components/svg/IconUnavailable.js +19 -21
  234. package/components/svg/IconUndo.js +19 -21
  235. package/components/svg/IconVariable.js +19 -21
  236. package/components/svg/IconVideo.js +30 -31
  237. package/components/svg/IconWifi.js +19 -21
  238. package/components/svg/Info.js +19 -21
  239. package/components/svg/Key.js +19 -21
  240. package/components/svg/Leaf.js +19 -21
  241. package/components/svg/Lock.js +19 -21
  242. package/components/svg/Logo.js +48 -43
  243. package/components/svg/MenuVertical.js +19 -21
  244. package/components/svg/Notification.js +19 -21
  245. package/components/svg/Person.js +19 -21
  246. package/components/svg/Plus.js +19 -21
  247. package/components/svg/RestorationSite.js +19 -21
  248. package/components/svg/Search.js +19 -21
  249. package/components/svg/Seeds.js +19 -21
  250. package/components/svg/Site.js +19 -21
  251. package/components/svg/Sparkles.js +19 -21
  252. package/components/svg/Species.js +19 -21
  253. package/components/svg/Species2.js +202 -164
  254. package/components/svg/Spinner.js +54 -48
  255. package/components/svg/Star.js +19 -21
  256. package/components/svg/Success.js +19 -21
  257. package/components/svg/SuccessFilled.js +19 -21
  258. package/components/svg/TerrawareLogoDesktop.js +55 -49
  259. package/components/svg/TerrawareLogoMobile.js +25 -26
  260. package/components/svg/Touchscreen.js +19 -21
  261. package/components/svg/UploadCloud.js +202 -164
  262. package/components/svg/Warning.js +19 -21
  263. package/components/svg/WelcomeClipboard.js +156 -133
  264. package/components/svg/index.js +129 -129
  265. package/components/table/EnhancedTableToolbar.js +68 -74
  266. package/components/table/EnhancedTableToolbarV2.js +31 -41
  267. package/components/table/TableCellRenderer.js +196 -231
  268. package/components/table/TableHeader.js +71 -73
  269. package/components/table/TableHeaderItem.js +97 -102
  270. package/components/table/density.js +18 -19
  271. package/components/table/index.js +350 -369
  272. package/components/table/sort.js +27 -52
  273. package/components/table/types.js +0 -1
  274. package/components/types/index.js +0 -1
  275. package/hooks/cameraEntityContext.js +2 -13
  276. package/hooks/useBoolean.js +17 -13
  277. package/hooks/useCameraPosition.js +56 -56
  278. package/hooks/useDevicePerformance.js +17 -17
  279. package/hooks/useXr.js +85 -87
  280. package/index.d.ts +3 -1
  281. package/index.d.ts.map +1 -1
  282. package/index.js +59 -58
  283. package/package.json +1 -1
  284. package/setupTests.js +0 -4
  285. package/strings/csv/en.csv +1 -0
  286. package/strings/csv/es.csv +1 -0
  287. package/strings/csv/fr.csv +1 -0
  288. package/strings/export.js +111 -170
  289. package/strings/index.js +25 -54
  290. package/strings/locales.js +21 -41
  291. package/strings/strings-en.d.ts +1 -0
  292. package/strings/strings-en.d.ts.map +1 -1
  293. package/strings/strings-en.js +50 -51
  294. package/strings/strings-es.d.ts +1 -0
  295. package/strings/strings-es.d.ts.map +1 -1
  296. package/strings/strings-es.js +50 -51
  297. package/strings/strings-fr.d.ts +1 -0
  298. package/strings/strings-fr.d.ts.map +1 -1
  299. package/strings/strings-fr.js +50 -51
  300. package/strings/strings-gx.d.ts +1 -0
  301. package/strings/strings-gx.d.ts.map +1 -1
  302. package/strings/strings-gx.js +50 -51
  303. package/style-dictionary-dist/TerrawareTheme.js +186 -186
  304. package/theme.js +74 -76
  305. package/utils/color.js +8 -15
  306. package/utils/date.js +42 -108
  307. package/utils/index.js +4 -4
  308. package/utils/preventDefaultEvent.js +3 -3
  309. package/utils/text.js +11 -11
  310. package/utils/useDeviceInfo.js +42 -79
  311. 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 };