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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (295) hide show
  1. package/components/AntSwitch/index.js +22 -19
  2. package/components/Autocomplete/Autocomplete.js +130 -106
  3. package/components/Badge/index.js +29 -28
  4. package/components/BusySpinner/index.js +45 -40
  5. package/components/Button/Button.js +47 -41
  6. package/components/Checkbox.js +63 -64
  7. package/components/Confirm/index.js +83 -74
  8. package/components/DatePicker/DatePicker.js +141 -138
  9. package/components/DialogBox/DialogBox.js +118 -111
  10. package/components/Divisor.js +18 -18
  11. package/components/Dropdown.js +94 -66
  12. package/components/EditableTable/index.js +271 -211
  13. package/components/ErrorBox/ErrorBox.js +49 -47
  14. package/components/FileChooser/index.js +135 -117
  15. package/components/FormBottomBar/index.js +84 -75
  16. package/components/Icon/Icon.js +20 -12
  17. package/components/Icon/icons/index.js +256 -256
  18. package/components/IconTooltip/index.js +60 -49
  19. package/components/Map/MapBox.js +452 -545
  20. package/components/Map/MapContainer.js +29 -25
  21. package/components/Map/MapDateSliderControl.js +64 -64
  22. package/components/Map/MapDrawer.js +47 -41
  23. package/components/Map/MapLegend.js +172 -176
  24. package/components/Map/MapViewStyleControl.js +63 -59
  25. package/components/Map/ReactMapboxWrapper.js +2 -3
  26. package/components/Map/index.js +180 -161
  27. package/components/Map/types.js +17 -12
  28. package/components/Map/useMaintainLayerOrder.js +26 -16
  29. package/components/Markdown/index.js +26 -25
  30. package/components/Message/Message.js +76 -73
  31. package/components/MultiSelect/index.js +124 -112
  32. package/components/Navbar/NavFooter.js +13 -9
  33. package/components/Navbar/NavItem.js +76 -70
  34. package/components/Navbar/NavSection.js +19 -17
  35. package/components/Navbar/Navbar.js +46 -39
  36. package/components/Navbar/SubNavbar.js +13 -9
  37. package/components/Note.js +22 -18
  38. package/components/OverlayModal/OverlayModal.js +63 -56
  39. package/components/PageForm/index.js +41 -33
  40. package/components/PhotoChooser/index.js +173 -165
  41. package/components/PhotosCarousel/PhotoSlide.js +35 -32
  42. package/components/PhotosCarousel/index.js +85 -80
  43. package/components/PhotosCarousel/usePhotosCarousel.js +76 -69
  44. package/components/Pill/index.js +45 -41
  45. package/components/PillList/index.js +24 -17
  46. package/components/PlacementWrapper/index.js +59 -38
  47. package/components/PopoverMenu/Popover.js +85 -68
  48. package/components/PopoverMenu/index.js +56 -51
  49. package/components/PopoverMultiSelect/index.js +59 -52
  50. package/components/ProgressCircle/ProgressCircle.js +32 -28
  51. package/components/RadioButton.js +19 -17
  52. package/components/Select/Select.js +15 -13
  53. package/components/Select/SelectT.js +250 -235
  54. package/components/Separator/index.js +17 -14
  55. package/components/Size.js +1 -0
  56. package/components/Slider/index.js +56 -43
  57. package/components/SummaryBox.js +72 -70
  58. package/components/Tabs/index.js +135 -133
  59. package/components/TextTruncated/index.js +130 -117
  60. package/components/Textfield/Textfield.js +190 -158
  61. package/components/Textfield/TruncatedTextArea.js +84 -70
  62. package/components/TimelineSlider/TimelineMarker.js +30 -25
  63. package/components/TimelineSlider/TimelineRail.js +11 -8
  64. package/components/TimelineSlider/index.js +66 -64
  65. package/components/TimelineSliderV2/ClusterBadge.js +29 -25
  66. package/components/TimelineSliderV2/TimelineDot.js +35 -31
  67. package/components/TimelineSliderV2/TimelineRail.js +11 -8
  68. package/components/TimelineSliderV2/index.js +103 -121
  69. package/components/TimelineSliderV2/layout.js +147 -124
  70. package/components/TimelineSliderV2/types.js +1 -0
  71. package/components/Tooltip/Tooltip.js +56 -47
  72. package/components/ViewPhotosDialog/index.js +35 -28
  73. package/components/VirtualWalkthrough/Annotation.js +207 -163
  74. package/components/VirtualWalkthrough/AnnotationEditPane.js +184 -198
  75. package/components/VirtualWalkthrough/AnnotationPanel.js +199 -187
  76. package/components/VirtualWalkthrough/Application.js +34 -31
  77. package/components/VirtualWalkthrough/AutoRotator.js +223 -85
  78. package/components/VirtualWalkthrough/BlockingSpinner.js +28 -26
  79. package/components/VirtualWalkthrough/BoundaryRing.js +34 -30
  80. package/components/VirtualWalkthrough/BoundaryWall.js +36 -32
  81. package/components/VirtualWalkthrough/CameraInfo.js +89 -100
  82. package/components/VirtualWalkthrough/ControlsInfoPane.js +195 -254
  83. package/components/VirtualWalkthrough/GradientSky.js +73 -72
  84. package/components/VirtualWalkthrough/SplatControls.js +221 -209
  85. package/components/VirtualWalkthrough/SplatCrop.js +20 -21
  86. package/components/VirtualWalkthrough/SplatFadeCrop.js +20 -21
  87. package/components/VirtualWalkthrough/SplatModel.js +85 -65
  88. package/components/VirtualWalkthrough/SplatRevealRain.js +46 -41
  89. package/components/VirtualWalkthrough/TfAnnotationManager.js +379 -197
  90. package/components/VirtualWalkthrough/TfXrNavigation.js +162 -73
  91. package/components/VirtualWalkthrough/VirtualWalkthroughViewer.js +431 -438
  92. package/components/VirtualWalkthrough/VrAnnotationPanel.js +22 -18
  93. package/components/VirtualWalkthrough/XrAnnotationInteraction.js +17 -14
  94. package/components/VirtualWalkthrough/XrExitButton.js +17 -14
  95. package/components/VirtualWalkthrough/XrGazeDwell.js +17 -14
  96. package/components/VirtualWalkthrough/XrPointerRay.js +17 -14
  97. package/components/VirtualWalkthrough/XrStartPose.js +121 -67
  98. package/components/VirtualWalkthrough/boundary-ring.js +117 -87
  99. package/components/VirtualWalkthrough/boundary-wall.js +258 -175
  100. package/components/VirtualWalkthrough/camera-adoption.js +64 -18
  101. package/components/VirtualWalkthrough/groundPlane.js +47 -32
  102. package/components/VirtualWalkthrough/gsplat-fade-crop-effect.js +35 -35
  103. package/components/VirtualWalkthrough/splatFormat.js +47 -13
  104. package/components/VirtualWalkthrough/useAdoptedCamera.js +44 -23
  105. package/components/VirtualWalkthrough/useRestartOnVr.js +37 -26
  106. package/components/VirtualWalkthrough/useXrRenderTuning.js +54 -33
  107. package/components/VirtualWalkthrough/vr-annotation-panel-layout.js +99 -67
  108. package/components/VirtualWalkthrough/vr-annotation-panel.js +433 -403
  109. package/components/VirtualWalkthrough/vr-annotation-scroll.js +28 -10
  110. package/components/VirtualWalkthrough/walkthrough-camera.js +329 -204
  111. package/components/VirtualWalkthrough/xr-annotation-candidate-buffer.js +80 -39
  112. package/components/VirtualWalkthrough/xr-annotation-candidates.js +18 -6
  113. package/components/VirtualWalkthrough/xr-annotation-interaction.js +56 -31
  114. package/components/VirtualWalkthrough/xr-annotation-targeting.js +38 -22
  115. package/components/VirtualWalkthrough/xr-button-arming.js +17 -7
  116. package/components/VirtualWalkthrough/xr-button-hold.js +36 -22
  117. package/components/VirtualWalkthrough/xr-exit-button-mask.js +36 -22
  118. package/components/VirtualWalkthrough/xr-exit-button.js +207 -148
  119. package/components/VirtualWalkthrough/xr-face-buttons.js +29 -19
  120. package/components/VirtualWalkthrough/xr-gaze-dwell-state.js +34 -23
  121. package/components/VirtualWalkthrough/xr-gaze-dwell.js +144 -107
  122. package/components/VirtualWalkthrough/xr-interactive-ui.js +24 -12
  123. package/components/VirtualWalkthrough/xr-pointer-ray.js +20 -13
  124. package/components/VirtualWalkthrough/xr-progress-pie-material.js +88 -117
  125. package/components/VirtualWalkthrough/xr-progress-pie.js +10 -1
  126. package/components/VirtualWalkthrough/xr-ray-quad.js +30 -16
  127. package/components/VirtualWalkthrough/xr-render-tuning.js +48 -17
  128. package/components/VirtualWalkthrough/xr-scene-bounds.js +37 -21
  129. package/components/VirtualWalkthrough/xr-start-pose.js +73 -27
  130. package/components/VirtualWalkthrough/xr-teleport-gesture.js +42 -16
  131. package/components/svg/BlobbyGrayIconImage.js +39 -41
  132. package/components/svg/BlobbyGrayIconImport.js +39 -41
  133. package/components/svg/BlobbyGrayIconUploadToTheCloud.js +39 -41
  134. package/components/svg/BlobbyIconCloudChecked.js +164 -202
  135. package/components/svg/BlobbyIconExportOrUploadPhoto.js +164 -202
  136. package/components/svg/BlobbyIconFolder.js +164 -202
  137. package/components/svg/BlobbyIconGraphReport.js +164 -202
  138. package/components/svg/BlobbyIconHappy.js +164 -202
  139. package/components/svg/BlobbyIconHeartMonitor.js +164 -202
  140. package/components/svg/BlobbyIconLeaf.js +164 -202
  141. package/components/svg/BlobbyIconLibrary.js +164 -202
  142. package/components/svg/BlobbyIconNursery.js +166 -204
  143. package/components/svg/BlobbyIconOrganization.js +164 -202
  144. package/components/svg/BlobbyIconParchment.js +164 -202
  145. package/components/svg/BlobbyIconPeople.js +164 -202
  146. package/components/svg/BlobbyIconSeedBank.js +164 -202
  147. package/components/svg/BlobbyIconSeedling.js +164 -202
  148. package/components/svg/BlobbyIconSeeds.js +164 -202
  149. package/components/svg/BlobbyIconSite.js +167 -206
  150. package/components/svg/BlobbyIconSparkles.js +164 -202
  151. package/components/svg/BlobbyIconWrench.js +164 -202
  152. package/components/svg/Bug.js +21 -19
  153. package/components/svg/Calendar.js +21 -19
  154. package/components/svg/CaretDown.js +21 -19
  155. package/components/svg/CaretLeft.js +21 -19
  156. package/components/svg/CaretRight.js +21 -19
  157. package/components/svg/CaretUp.js +21 -19
  158. package/components/svg/ChevronDown.js +21 -19
  159. package/components/svg/ChevronUp.js +21 -19
  160. package/components/svg/Close.js +21 -19
  161. package/components/svg/Critical.js +21 -19
  162. package/components/svg/Edit.js +164 -202
  163. package/components/svg/Error.js +21 -19
  164. package/components/svg/Filter.js +21 -19
  165. package/components/svg/Folder.js +21 -19
  166. package/components/svg/Help.js +21 -19
  167. package/components/svg/Home.js +21 -19
  168. package/components/svg/IconAdd.js +21 -19
  169. package/components/svg/IconArrowRight.js +23 -21
  170. package/components/svg/IconBusinessNetwork.js +21 -19
  171. package/components/svg/IconCalendar.js +21 -19
  172. package/components/svg/IconCancel.js +21 -19
  173. package/components/svg/IconChargingBattery.js +21 -19
  174. package/components/svg/IconChecklist.js +23 -21
  175. package/components/svg/IconCheckmark.js +21 -19
  176. package/components/svg/IconCoinInHand.js +21 -19
  177. package/components/svg/IconColumns.js +21 -19
  178. package/components/svg/IconComment.js +22 -20
  179. package/components/svg/IconDashboard.js +21 -19
  180. package/components/svg/IconDataMigration.js +21 -19
  181. package/components/svg/IconDocument.js +23 -21
  182. package/components/svg/IconDownloadFromTheCloud.js +23 -21
  183. package/components/svg/IconEdit.js +23 -21
  184. package/components/svg/IconExpand.js +23 -21
  185. package/components/svg/IconExport.js +21 -19
  186. package/components/svg/IconExternalLink.js +21 -19
  187. package/components/svg/IconEye.js +21 -19
  188. package/components/svg/IconEyeOff.js +21 -19
  189. package/components/svg/IconFile.js +21 -19
  190. package/components/svg/IconFolder.js +21 -19
  191. package/components/svg/IconFullScreen.js +22 -20
  192. package/components/svg/IconFutures.js +21 -19
  193. package/components/svg/IconGraphReport.js +21 -19
  194. package/components/svg/IconGrid.js +21 -19
  195. package/components/svg/IconHeartMonitor.js +21 -19
  196. package/components/svg/IconHelp.js +23 -21
  197. package/components/svg/IconHistory.js +21 -19
  198. package/components/svg/IconImport.js +21 -19
  199. package/components/svg/IconIndex.js +21 -19
  200. package/components/svg/IconLayers.js +21 -19
  201. package/components/svg/IconLibrary.js +22 -20
  202. package/components/svg/IconList.js +21 -19
  203. package/components/svg/IconLivePlant.js +21 -19
  204. package/components/svg/IconLocations.js +21 -19
  205. package/components/svg/IconMail.js +21 -19
  206. package/components/svg/IconManager.js +21 -19
  207. package/components/svg/IconMap.js +21 -19
  208. package/components/svg/IconMarker.js +21 -19
  209. package/components/svg/IconMenu.js +21 -19
  210. package/components/svg/IconMenuHorizontal.js +21 -19
  211. package/components/svg/IconModule.js +21 -19
  212. package/components/svg/IconMyLocation.js +21 -19
  213. package/components/svg/IconNote.js +23 -21
  214. package/components/svg/IconNursery.js +23 -21
  215. package/components/svg/IconOrg.js +21 -19
  216. package/components/svg/IconParchment.js +21 -19
  217. package/components/svg/IconPhoto.js +21 -19
  218. package/components/svg/IconPlantsFilled.js +21 -19
  219. package/components/svg/IconRedo.js +21 -19
  220. package/components/svg/IconRestorationSite.js +21 -19
  221. package/components/svg/IconSeedBank.js +21 -19
  222. package/components/svg/IconSeedling.js +21 -19
  223. package/components/svg/IconSettings.js +21 -19
  224. package/components/svg/IconSlice.js +21 -19
  225. package/components/svg/IconSubmit.js +21 -19
  226. package/components/svg/IconSubtract.js +23 -21
  227. package/components/svg/IconSynced.js +21 -19
  228. package/components/svg/IconTrashCan.js +21 -19
  229. package/components/svg/IconTreasureMap.js +21 -19
  230. package/components/svg/IconUnavailable.js +21 -19
  231. package/components/svg/IconUndo.js +21 -19
  232. package/components/svg/IconVariable.js +21 -19
  233. package/components/svg/IconVideo.js +31 -30
  234. package/components/svg/IconWifi.js +21 -19
  235. package/components/svg/Info.js +21 -19
  236. package/components/svg/Key.js +21 -19
  237. package/components/svg/Leaf.js +21 -19
  238. package/components/svg/Lock.js +21 -19
  239. package/components/svg/Logo.js +43 -48
  240. package/components/svg/MenuVertical.js +21 -19
  241. package/components/svg/Notification.js +21 -19
  242. package/components/svg/Person.js +21 -19
  243. package/components/svg/Plus.js +21 -19
  244. package/components/svg/RestorationSite.js +21 -19
  245. package/components/svg/Search.js +21 -19
  246. package/components/svg/Seeds.js +21 -19
  247. package/components/svg/Site.js +21 -19
  248. package/components/svg/Sparkles.js +21 -19
  249. package/components/svg/Species.js +21 -19
  250. package/components/svg/Species2.js +164 -202
  251. package/components/svg/Spinner.js +48 -54
  252. package/components/svg/Star.js +21 -19
  253. package/components/svg/Success.js +21 -19
  254. package/components/svg/SuccessFilled.js +21 -19
  255. package/components/svg/TerrawareLogoDesktop.js +49 -55
  256. package/components/svg/TerrawareLogoMobile.js +26 -25
  257. package/components/svg/Touchscreen.js +21 -19
  258. package/components/svg/UploadCloud.js +164 -202
  259. package/components/svg/Warning.js +21 -19
  260. package/components/svg/WelcomeClipboard.js +133 -156
  261. package/components/svg/index.js +129 -129
  262. package/components/table/EnhancedTableToolbar.js +74 -68
  263. package/components/table/EnhancedTableToolbarV2.js +41 -31
  264. package/components/table/TableCellRenderer.js +231 -196
  265. package/components/table/TableHeader.js +73 -71
  266. package/components/table/TableHeaderItem.js +102 -97
  267. package/components/table/density.js +19 -18
  268. package/components/table/index.js +369 -350
  269. package/components/table/sort.js +52 -27
  270. package/components/table/types.js +1 -0
  271. package/components/types/index.js +1 -0
  272. package/hooks/cameraEntityContext.js +13 -2
  273. package/hooks/useBoolean.js +13 -17
  274. package/hooks/useCameraPosition.js +56 -56
  275. package/hooks/useDevicePerformance.js +17 -17
  276. package/hooks/useXr.js +87 -85
  277. package/index.js +58 -58
  278. package/package.json +1 -1
  279. package/setupTests.js +4 -0
  280. package/strings/export.js +170 -111
  281. package/strings/index.js +54 -25
  282. package/strings/locales.js +41 -21
  283. package/strings/strings-en.js +51 -49
  284. package/strings/strings-es.js +51 -49
  285. package/strings/strings-fr.js +51 -49
  286. package/strings/strings-gx.js +51 -49
  287. package/style-dictionary-dist/TerrawareTheme.js +186 -186
  288. package/theme.js +76 -74
  289. package/utils/color.js +15 -8
  290. package/utils/date.js +108 -42
  291. package/utils/index.js +4 -4
  292. package/utils/preventDefaultEvent.js +3 -3
  293. package/utils/text.js +11 -11
  294. package/utils/useDeviceInfo.js +79 -42
  295. package/virtualWalkthrough.js +23 -23
@@ -1,218 +1,400 @@
1
- import { createElement } from "react";
2
- import { renderToStaticMarkup } from "react-dom/server";
3
- import { FILTER_LINEAR, PIXELFORMAT_RGBA8, Texture } from "playcanvas";
4
- import { AnnotationManager } from "playcanvas/scripts/esm/annotations.mjs";
5
- import Icon from "../Icon/Icon.js";
1
+ import { createElement } from 'react';
2
+ import { renderToStaticMarkup } from 'react-dom/server';
3
+ import { Texture, FILTER_LINEAR, PIXELFORMAT_RGBA8 } from 'playcanvas';
4
+ import { AnnotationManager } from 'playcanvas/scripts/esm/annotations.mjs';
5
+ import Icon from '../Icon/Icon.js';
6
+
7
+ // Maps each annotation icon type to a @terraware/web-components icon.
6
8
  const ANNOTATION_ICONS = {
7
- text: 'iconComment',
8
- image: 'iconPhoto',
9
- video: 'iconVideo'
9
+ text: 'iconComment',
10
+ image: 'iconPhoto',
11
+ video: 'iconVideo'
10
12
  };
13
+
14
+ // Pixel dimensions of the (square) hotspot texture canvas at rest.
11
15
  const HOTSPOT_TEXTURE_SIZE = 64;
16
+
17
+ // How much larger the hotspot appears while hovered.
12
18
  const HOTSPOT_HOVER_SCALE = 1.3;
19
+
20
+ /**
21
+ * Extended AnnotationManager that adds max size clamping for annotations
22
+ * and fixes positioning when canvas is not at document origin.
23
+ */
13
24
  class TfAnnotationManager extends AnnotationManager {
14
- static scriptName = 'tfAnnotationManager';
15
- _maxWorldSize = 1.0;
16
- _customParentDom = null;
17
- _clickHandlersAttached = new Set();
18
- _hotspotBackgroundColor = '#2C8658';
19
- _iconImages = new Map();
20
- set maxWorldSize(value) {
21
- this._maxWorldSize = value;
25
+ static scriptName = 'tfAnnotationManager';
26
+ _maxWorldSize = 1.0;
27
+ _customParentDom = null;
28
+ _clickHandlersAttached = new Set();
29
+ _hotspotBackgroundColor = '#2C8658';
30
+ _iconImages = new Map();
31
+
32
+ /**
33
+ * Maximum world-space size for annotations.
34
+ * This prevents annotations from growing beyond this size regardless of camera distance.
35
+ *
36
+ * @attribute
37
+ * @title Max World Size
38
+ * @type {number}
39
+ * @default 1.0
40
+ */
41
+ set maxWorldSize(value) {
42
+ this._maxWorldSize = value;
43
+ }
44
+ get maxWorldSize() {
45
+ return this._maxWorldSize;
46
+ }
47
+
48
+ /**
49
+ * Background color for the hotspot texture canvas (the circle fill color).
50
+ * This is a hex color string (e.g., '#2C8658') that's drawn on the texture canvas.
51
+ * Note: This is different from hotspotColor and hoverColor, which are PlayCanvas Color objects
52
+ * used for the material's emissive property.
53
+ *
54
+ * @attribute
55
+ * @title Hotspot Background Color
56
+ * @type {string}
57
+ * @default '#2C8658'
58
+ */
59
+ set hotspotBackgroundColor(value) {
60
+ this._hotspotBackgroundColor = value;
61
+ }
62
+ get hotspotBackgroundColor() {
63
+ return this._hotspotBackgroundColor;
64
+ }
65
+
66
+ /**
67
+ * Initialize with custom parent DOM if needed.
68
+ */
69
+ initialize() {
70
+ // Find a container div with data-annotation-container attribute
71
+ const canvas = this.app.graphicsDevice.canvas;
72
+ const container = canvas.parentElement?.querySelector('[data-annotation-container]');
73
+ if (container) {
74
+ this._customParentDom = container;
75
+ this._parentDom = container;
22
76
  }
23
- get maxWorldSize() {
24
- return this._maxWorldSize;
77
+
78
+ // Call parent initialize
79
+ super.initialize();
80
+ this._loadIconImages();
81
+ }
82
+
83
+ /**
84
+ * Renders each web-components icon to an SVG image once. When an image finishes
85
+ * loading, any annotations already using that icon are redrawn.
86
+ * @private
87
+ */
88
+ _loadIconImages() {
89
+ const fillColor = this._hotspotColor?.toString() ?? '#ffffff';
90
+ Object.keys(ANNOTATION_ICONS).forEach(iconType => {
91
+ const name = ANNOTATION_ICONS[iconType];
92
+ const markup = renderToStaticMarkup(/*#__PURE__*/createElement(Icon, {
93
+ name,
94
+ fillColor
95
+ })).replace(/^<svg/, '<svg width="128" height="128"');
96
+ const image = new Image();
97
+ image.onload = () => this._refreshIcon(iconType);
98
+ image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(markup)}`;
99
+ this._iconImages.set(iconType, image);
100
+ });
101
+ }
102
+
103
+ /**
104
+ * Redraws the texture for all annotations using the given icon, once its image
105
+ * has loaded.
106
+ * @private
107
+ */
108
+ _refreshIcon(iconType) {
109
+ const annotationResources = this._annotationResources;
110
+ if (!annotationResources) {
111
+ return;
25
112
  }
26
- set hotspotBackgroundColor(value) {
27
- this._hotspotBackgroundColor = value;
28
- }
29
- get hotspotBackgroundColor() {
30
- return this._hotspotBackgroundColor;
113
+ annotationResources.forEach((_resources, annotation) => {
114
+ if ((annotation.icon ?? 'text') === iconType) {
115
+ this._applyAnnotationIcon(annotation);
116
+ }
117
+ });
118
+ }
119
+
120
+ /**
121
+ * Update loop to attach click handlers to annotations and manage visibility.
122
+ */
123
+ update() {
124
+ const annotationResources = this._annotationResources;
125
+ if (!annotationResources) {
126
+ return;
31
127
  }
32
- initialize() {
33
- const canvas = this.app.graphicsDevice.canvas;
34
- const container = canvas.parentElement?.querySelector('[data-annotation-container]');
35
- if (container) {
36
- this._customParentDom = container;
37
- this._parentDom = container;
128
+
129
+ // Attach click handlers and manage visibility for all annotations
130
+ annotationResources.forEach((resources, annotation) => {
131
+ if (resources && resources.hotspotDom) {
132
+ // Attach click handler if not already attached
133
+ if (!this._clickHandlersAttached.has(annotation)) {
134
+ const callback = annotation.onClickCallback;
135
+ if (callback) {
136
+ this._clickHandlersAttached.add(annotation);
137
+ resources.hotspotDom.addEventListener('pointerdown', e => {
138
+ callback(e.clientX, e.clientY);
139
+ });
140
+ }
38
141
  }
39
- super.initialize();
40
- this._loadIconImages();
41
- }
42
- _loadIconImages() {
43
- const fillColor = this._hotspotColor?.toString() ?? '#ffffff';
44
- Object.keys(ANNOTATION_ICONS).forEach((iconType)=>{
45
- const name = ANNOTATION_ICONS[iconType];
46
- const markup = renderToStaticMarkup(createElement(Icon, {
47
- name,
48
- fillColor
49
- })).replace(/^<svg/, '<svg width="128" height="128"');
50
- const image = new Image();
51
- image.onload = ()=>this._refreshIcon(iconType);
52
- image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(markup)}`;
53
- this._iconImages.set(iconType, image);
54
- });
55
- }
56
- _refreshIcon(iconType) {
57
- const annotationResources = this._annotationResources;
58
- if (!annotationResources) return;
59
- annotationResources.forEach((_resources, annotation)=>{
60
- if ((annotation.icon ?? 'text') === iconType) this._applyAnnotationIcon(annotation);
61
- });
62
- }
63
- update() {
64
- const annotationResources = this._annotationResources;
65
- if (!annotationResources) return;
66
- annotationResources.forEach((resources, annotation)=>{
67
- if (resources && resources.hotspotDom) {
68
- if (!this._clickHandlersAttached.has(annotation)) {
69
- const callback = annotation.onClickCallback;
70
- if (callback) {
71
- this._clickHandlersAttached.add(annotation);
72
- resources.hotspotDom.addEventListener('pointerdown', (e)=>{
73
- callback(e.clientX, e.clientY);
74
- });
75
- }
76
- }
77
- const isVisible = void 0 !== annotation.enabled ? annotation.enabled : true;
78
- if (this._activeAnnotation === annotation && !isVisible) {
79
- this._tooltipDom.style.visibility = 'hidden';
80
- this._tooltipDom.style.opacity = '0';
81
- this._activeAnnotation = null;
82
- }
83
- }
84
- });
85
- }
86
- _registerAnnotation(annotation) {
87
- super._registerAnnotation(annotation);
88
- this._applyAnnotationIcon(annotation);
89
- const resources = this._annotationResources.get(annotation);
90
- if (resources) {
91
- resources.baseEntity.enabled = false;
92
- resources.overlayEntity.enabled = false;
142
+
143
+ // Disable/enable the entity itself based on visibility
144
+ const isVisible = annotation.enabled !== undefined ? annotation.enabled : true;
145
+
146
+ // Hide tooltip if the active annotation is hidden
147
+ if (this._activeAnnotation === annotation && !isVisible) {
148
+ this._tooltipDom.style.visibility = 'hidden';
149
+ this._tooltipDom.style.opacity = '0';
150
+ this._activeAnnotation = null;
93
151
  }
152
+ }
153
+ });
154
+ }
155
+
156
+ /**
157
+ * Override annotation registration to draw the media icon onto the hotspot
158
+ * texture once the annotation (and its icon) is available.
159
+ * @private
160
+ */
161
+ _registerAnnotation(annotation) {
162
+ super._registerAnnotation(annotation);
163
+ this._applyAnnotationIcon(annotation);
164
+
165
+ // Keep the hotspot mesh hidden until the first scale pass sizes it. The annotation entity
166
+ // starts at unit scale, so the plane would render a metre across for the frames before
167
+ // _updateAnnotationRotationAndScale runs.
168
+ const resources = this._annotationResources.get(annotation);
169
+ if (resources) {
170
+ resources.baseEntity.enabled = false;
171
+ resources.overlayEntity.enabled = false;
94
172
  }
95
- _applyAnnotationIcon(annotation) {
96
- const resources = this._annotationResources.get(annotation);
97
- if (!resources) return;
98
- const icon = annotation.icon ?? 'text';
99
- resources.texture.destroy();
100
- resources.texture = this._createHotspotTexture(annotation.label, HOTSPOT_TEXTURE_SIZE, 6, icon);
101
- resources.materials.forEach((material)=>{
102
- material.emissiveMap = resources.texture;
103
- material.opacityMap = resources.texture;
104
- material.update();
105
- });
173
+ }
174
+
175
+ /**
176
+ * Recreates the hotspot texture with the appropriate media icon drawn on it.
177
+ * @private
178
+ */
179
+ _applyAnnotationIcon(annotation) {
180
+ const resources = this._annotationResources.get(annotation);
181
+ if (!resources) {
182
+ return;
106
183
  }
107
- _drawIcon(ctx, icon, size) {
108
- const image = this._iconImages.get(icon);
109
- if (!image || !image.complete || 0 === image.naturalWidth) return;
110
- const iconSize = 0.5 * size;
111
- const offset = (size - iconSize) / 2;
112
- ctx.drawImage(image, offset, offset, iconSize, iconSize);
184
+ const icon = annotation.icon ?? 'text';
185
+ resources.texture.destroy();
186
+ resources.texture = this._createHotspotTexture(annotation.label, HOTSPOT_TEXTURE_SIZE, 6, icon);
187
+ resources.materials.forEach(material => {
188
+ material.emissiveMap = resources.texture;
189
+ material.opacityMap = resources.texture;
190
+ material.update();
191
+ });
192
+ }
193
+
194
+ /**
195
+ * Draws the icon image onto the hotspot texture canvas, scaled to fit the
196
+ * circle. Does nothing if the image has not finished loading yet; the
197
+ * annotation is redrawn once it has (see `_refreshIcon`).
198
+ * @private
199
+ */
200
+ _drawIcon(ctx, icon, size) {
201
+ const image = this._iconImages.get(icon);
202
+ if (!image || !image.complete || image.naturalWidth === 0) {
203
+ return;
113
204
  }
114
- _onLabelChange(annotation) {
115
- this._applyAnnotationIcon(annotation);
205
+ const iconSize = size * 0.5;
206
+ const offset = (size - iconSize) / 2;
207
+ ctx.drawImage(image, offset, offset, iconSize, iconSize);
208
+ }
209
+
210
+ /**
211
+ * Keep the media icon drawn on the hotspot texture. The label text itself
212
+ * is not rendered on the hotspot, so we simply redraw with the icon.
213
+ * @private
214
+ */
215
+ _onLabelChange(annotation) {
216
+ this._applyAnnotationIcon(annotation);
217
+ }
218
+
219
+ /**
220
+ * The hotspot colors are baked into the texture in `_createHotspotTexture`,
221
+ * so the material emissive is kept white to let those colors render as drawn.
222
+ * @private
223
+ */
224
+ _createHotspotMaterial(texture, options) {
225
+ const material = super._createHotspotMaterial(texture, options);
226
+ material.emissive.set(1, 1, 1);
227
+ material.update();
228
+ return material;
229
+ }
230
+
231
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
232
+ _setAnnotationHover(_annotation, _hover) {
233
+ // No-op: the hover growth is handled in _updateAnnotationRotationAndScale
234
+ // (via _hoverAnnotation); the texture is identical in both states.
235
+ }
236
+ _updateAllAnnotationColors() {
237
+ // No-op: hotspot colors are baked into the texture (see _createHotspotMaterial).
238
+ }
239
+
240
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
241
+ _showTooltip(_annotation) {
242
+ // No-op: AnnotationPanel handles all annotation UI, this removes default behavior
243
+ }
244
+
245
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
246
+ _onTextChange(_annotation, _text) {
247
+ // No-op: AnnotationPanel handles all annotation UI, this removes default behavior
248
+ }
249
+
250
+ /**
251
+ * Override position update to adjust for canvas offset and respect visibility.
252
+ * @private
253
+ */
254
+ _updateAnnotationPositions(annotation, resources, screenPos) {
255
+ const canvas = this.app.graphicsDevice.canvas;
256
+ const rect = canvas.getBoundingClientRect();
257
+
258
+ // If using a custom parent (positioned container), use raw screen coordinates
259
+ // Otherwise, adjust for canvas position in document
260
+ const offsetX = this._customParentDom ? 0 : rect.left + window.scrollX;
261
+ const offsetY = this._customParentDom ? 0 : rect.top + window.scrollY;
262
+
263
+ // Check if annotation is visible (defaults to true if not specified)
264
+ const isVisible = annotation.enabled !== undefined ? annotation.enabled : true;
265
+ resources.hotspotDom.style.display = isVisible ? 'block' : 'none';
266
+ resources.hotspotDom.style.left = `${screenPos.x + offsetX}px`;
267
+ resources.hotspotDom.style.top = `${screenPos.y + offsetY}px`;
268
+ if (this._activeAnnotation === annotation) {
269
+ this._tooltipDom.style.display = isVisible ? 'block' : 'none';
270
+ this._tooltipDom.style.left = `${screenPos.x + offsetX}px`;
271
+ this._tooltipDom.style.top = `${screenPos.y + offsetY}px`;
116
272
  }
117
- _createHotspotMaterial(texture, options) {
118
- const material = super._createHotspotMaterial(texture, options);
119
- material.emissive.set(1, 1, 1);
120
- material.update();
121
- return material;
273
+ if (annotation.onScreenPositionUpdateCallback) {
274
+ // Rendered hotspot diameter (px), set by _updateAnnotationRotationAndScale.
275
+ // Parsed from the style rather than measured to avoid forcing a reflow.
276
+ const hotspotSize = parseFloat(resources.hotspotDom.style.width) || undefined;
277
+ annotation.onScreenPositionUpdateCallback(screenPos.x + offsetX, screenPos.y + offsetY, hotspotSize);
122
278
  }
123
- _setAnnotationHover(_annotation, _hover) {}
124
- _updateAllAnnotationColors() {}
125
- _showTooltip(_annotation) {}
126
- _onTextChange(_annotation, _text) {}
127
- _updateAnnotationPositions(annotation, resources, screenPos) {
128
- const canvas = this.app.graphicsDevice.canvas;
129
- const rect = canvas.getBoundingClientRect();
130
- const offsetX = this._customParentDom ? 0 : rect.left + window.scrollX;
131
- const offsetY = this._customParentDom ? 0 : rect.top + window.scrollY;
132
- const isVisible = void 0 !== annotation.enabled ? annotation.enabled : true;
133
- resources.hotspotDom.style.display = isVisible ? 'block' : 'none';
134
- resources.hotspotDom.style.left = `${screenPos.x + offsetX}px`;
135
- resources.hotspotDom.style.top = `${screenPos.y + offsetY}px`;
136
- if (this._activeAnnotation === annotation) {
137
- this._tooltipDom.style.display = isVisible ? 'block' : 'none';
138
- this._tooltipDom.style.left = `${screenPos.x + offsetX}px`;
139
- this._tooltipDom.style.top = `${screenPos.y + offsetY}px`;
140
- }
141
- if (annotation.onScreenPositionUpdateCallback) {
142
- const hotspotSize = parseFloat(resources.hotspotDom.style.width) || void 0;
143
- annotation.onScreenPositionUpdateCallback(screenPos.x + offsetX, screenPos.y + offsetY, hotspotSize);
144
- }
279
+ }
280
+
281
+ /**
282
+ * Override to also disable the hotspot mesh (not just the DOM) when the annotation is behind the
283
+ * camera. The base implementation leaves the mesh enabled, so a not-yet-scaled plane would stay
284
+ * in view at its full unit size until the camera moves the anchor in front.
285
+ * @private
286
+ */
287
+ _hideAnnotationElements(annotation, resources) {
288
+ super._hideAnnotationElements(annotation, resources);
289
+ resources.baseEntity.enabled = false;
290
+ resources.overlayEntity.enabled = false;
291
+ }
292
+
293
+ /**
294
+ * Override the scale update to clamp the world size to a maximum value.
295
+ * Also scales the hotspot DOM element to match.
296
+ * @private
297
+ */
298
+ _updateAnnotationRotationAndScale(annotation) {
299
+ const cameraRotation = this._camera.getRotation();
300
+ annotation.entity.setRotation(cameraRotation);
301
+ annotation.entity.rotateLocal(90, 0, 0);
302
+ const cameraPos = this._camera.getPosition();
303
+ const distance = annotation.entity.getPosition().distance(cameraPos);
304
+ const canvas = this.app.graphicsDevice.canvas;
305
+ const screenHeight = canvas.clientHeight;
306
+ const projMatrix = this._camera.camera.projectionMatrix;
307
+
308
+ // The world size that projects to _hotspotSize screen pixels at this distance.
309
+ const targetWorldSize = this._hotspotSize / screenHeight * (2 * distance / projMatrix.data[5]);
310
+
311
+ // Annotations hang off an unscaled parent, so local scale is world scale and maxWorldSize
312
+ // clamps the hotspot at a size in metres.
313
+ const clampedScale = Math.min(targetWorldSize, this._maxWorldSize);
314
+ const hovered = this._hoverAnnotation === annotation;
315
+ const hoverScale = hovered ? HOTSPOT_HOVER_SCALE : 1;
316
+ const entityScale = clampedScale * hoverScale;
317
+ annotation.entity.setLocalScale(entityScale, entityScale, entityScale);
318
+
319
+ // Scale the hotspot DOM element proportionally when clamped, including the
320
+ // hover growth so it tracks the visible circle. This is a pure screen-space
321
+ // size, so it uses the true _hotspotSize and the clamp ratio only.
322
+ const resources = this._annotationResources.get(annotation);
323
+ if (resources) {
324
+ // This branch only runs for annotations in front of the camera, and the scale above is now
325
+ // clamped, so it is safe to reveal the mesh (hidden on register / when behind the camera).
326
+ resources.baseEntity.enabled = true;
327
+ resources.overlayEntity.enabled = true;
145
328
  }
146
- _hideAnnotationElements(annotation, resources) {
147
- super._hideAnnotationElements(annotation, resources);
148
- resources.baseEntity.enabled = false;
149
- resources.overlayEntity.enabled = false;
329
+ if (resources && resources.hotspotDom) {
330
+ const clampRatio = clampedScale / targetWorldSize;
331
+ const baseSize = this._hotspotSize + 5; // Match the +5 from the stylesheet
332
+ const scaledSize = baseSize * clampRatio * hoverScale;
333
+ resources.hotspotDom.style.width = `${scaledSize}px`;
334
+ resources.hotspotDom.style.height = `${scaledSize}px`;
150
335
  }
151
- _updateAnnotationRotationAndScale(annotation) {
152
- const cameraRotation = this._camera.getRotation();
153
- annotation.entity.setRotation(cameraRotation);
154
- annotation.entity.rotateLocal(90, 0, 0);
155
- const cameraPos = this._camera.getPosition();
156
- const distance = annotation.entity.getPosition().distance(cameraPos);
157
- const canvas = this.app.graphicsDevice.canvas;
158
- const screenHeight = canvas.clientHeight;
159
- const projMatrix = this._camera.camera.projectionMatrix;
160
- const targetWorldSize = this._hotspotSize / screenHeight * (2 * distance / projMatrix.data[5]);
161
- const clampedScale = Math.min(targetWorldSize, this._maxWorldSize);
162
- const hovered = this._hoverAnnotation === annotation;
163
- const hoverScale = hovered ? HOTSPOT_HOVER_SCALE : 1;
164
- const entityScale = clampedScale * hoverScale;
165
- annotation.entity.setLocalScale(entityScale, entityScale, entityScale);
166
- const resources = this._annotationResources.get(annotation);
167
- if (resources) {
168
- resources.baseEntity.enabled = true;
169
- resources.overlayEntity.enabled = true;
170
- }
171
- if (resources && resources.hotspotDom) {
172
- const clampRatio = clampedScale / targetWorldSize;
173
- const baseSize = this._hotspotSize + 5;
174
- const scaledSize = baseSize * clampRatio * hoverScale;
175
- resources.hotspotDom.style.width = `${scaledSize}px`;
176
- resources.hotspotDom.style.height = `${scaledSize}px`;
177
- }
336
+ }
337
+
338
+ /**
339
+ * Override to create hotspot texture with custom background color.
340
+ * @private
341
+ */
342
+ _createHotspotTexture(label, size = HOTSPOT_TEXTURE_SIZE, borderWidth = 6, icon) {
343
+ const canvas = document.createElement('canvas');
344
+ canvas.width = size;
345
+ canvas.height = size;
346
+ const ctx = canvas.getContext('2d');
347
+ if (!ctx) {
348
+ throw new Error('Failed to get canvas 2d context');
178
349
  }
179
- _createHotspotTexture(label, size = HOTSPOT_TEXTURE_SIZE, borderWidth = 6, icon) {
180
- const canvas = document.createElement('canvas');
181
- canvas.width = size;
182
- canvas.height = size;
183
- const ctx = canvas.getContext('2d');
184
- if (!ctx) throw new Error('Failed to get canvas 2d context');
185
- ctx.fillStyle = 'white';
186
- ctx.globalAlpha = 0;
187
- ctx.fillRect(0, 0, size, size);
188
- ctx.globalAlpha = 1.0;
189
- const centerX = size / 2;
190
- const centerY = size / 2;
191
- const radius = size / 2 - 4;
192
- const foregroundColor = this._hotspotColor?.toString() ?? '#ffffff';
193
- ctx.beginPath();
194
- ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI);
195
- ctx.fillStyle = this._hotspotBackgroundColor;
196
- ctx.fill();
197
- ctx.beginPath();
198
- ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI);
199
- ctx.lineWidth = borderWidth;
200
- ctx.strokeStyle = foregroundColor;
201
- ctx.stroke();
202
- if (icon) this._drawIcon(ctx, icon, size);
203
- const imageData = ctx.getImageData(0, 0, size, size);
204
- const data = imageData.data;
205
- return new Texture(this.app.graphicsDevice, {
206
- width: size,
207
- height: size,
208
- format: PIXELFORMAT_RGBA8,
209
- magFilter: FILTER_LINEAR,
210
- minFilter: FILTER_LINEAR,
211
- mipmaps: false,
212
- levels: [
213
- new Uint8Array(data.buffer)
214
- ]
215
- });
350
+
351
+ // First clear with stroke color at zero alpha
352
+ ctx.fillStyle = 'white';
353
+ ctx.globalAlpha = 0;
354
+ ctx.fillRect(0, 0, size, size);
355
+ ctx.globalAlpha = 1.0;
356
+
357
+ // Draw circle with custom background color
358
+ const centerX = size / 2;
359
+ const centerY = size / 2;
360
+ const radius = size / 2 - 4;
361
+
362
+ // The icon fill and border share the hotspot color
363
+ const foregroundColor = this._hotspotColor?.toString() ?? '#ffffff';
364
+
365
+ // Draw main circle
366
+ ctx.beginPath();
367
+ ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
368
+ ctx.fillStyle = this._hotspotBackgroundColor;
369
+ ctx.fill();
370
+
371
+ // Draw border
372
+ ctx.beginPath();
373
+ ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
374
+ ctx.lineWidth = borderWidth;
375
+ ctx.strokeStyle = foregroundColor;
376
+ ctx.stroke();
377
+
378
+ // Draw the media icon inside the circle
379
+ if (icon) {
380
+ this._drawIcon(ctx, icon, size);
216
381
  }
382
+
383
+ // Get pixel data
384
+ const imageData = ctx.getImageData(0, 0, size, size);
385
+ const data = imageData.data;
386
+
387
+ // Create and return the texture
388
+ return new Texture(this.app.graphicsDevice, {
389
+ width: size,
390
+ height: size,
391
+ format: PIXELFORMAT_RGBA8,
392
+ magFilter: FILTER_LINEAR,
393
+ minFilter: FILTER_LINEAR,
394
+ mipmaps: false,
395
+ levels: [new Uint8Array(data.buffer)]
396
+ });
397
+ }
217
398
  }
399
+
218
400
  export { TfAnnotationManager };