@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.
- package/components/AntSwitch/index.js +22 -19
- package/components/Autocomplete/Autocomplete.js +130 -106
- package/components/Badge/index.js +29 -28
- package/components/BusySpinner/index.js +45 -40
- package/components/Button/Button.js +47 -41
- package/components/Checkbox.js +63 -64
- package/components/Confirm/index.js +83 -74
- package/components/DatePicker/DatePicker.js +141 -138
- package/components/DialogBox/DialogBox.js +118 -111
- package/components/Divisor.js +18 -18
- package/components/Dropdown.js +94 -66
- package/components/EditableTable/index.js +271 -211
- package/components/ErrorBox/ErrorBox.js +49 -47
- package/components/FileChooser/index.js +135 -117
- package/components/FormBottomBar/index.js +84 -75
- package/components/Icon/Icon.js +20 -12
- package/components/Icon/icons/index.js +256 -256
- package/components/IconTooltip/index.js +60 -49
- package/components/Map/MapBox.js +452 -545
- package/components/Map/MapContainer.js +29 -25
- package/components/Map/MapDateSliderControl.js +64 -64
- package/components/Map/MapDrawer.js +47 -41
- package/components/Map/MapLegend.js +172 -176
- package/components/Map/MapViewStyleControl.js +63 -59
- package/components/Map/ReactMapboxWrapper.js +2 -3
- package/components/Map/index.js +180 -161
- package/components/Map/types.js +17 -12
- package/components/Map/useMaintainLayerOrder.js +26 -16
- package/components/Markdown/index.js +26 -25
- package/components/Message/Message.js +76 -73
- package/components/MultiSelect/index.js +124 -112
- package/components/Navbar/NavFooter.js +13 -9
- package/components/Navbar/NavItem.js +76 -70
- package/components/Navbar/NavSection.js +19 -17
- package/components/Navbar/Navbar.js +46 -39
- package/components/Navbar/SubNavbar.js +13 -9
- package/components/Note.js +22 -18
- package/components/OverlayModal/OverlayModal.js +63 -56
- package/components/PageForm/index.js +41 -33
- package/components/PhotoChooser/index.js +173 -165
- package/components/PhotosCarousel/PhotoSlide.js +35 -32
- package/components/PhotosCarousel/index.js +85 -80
- package/components/PhotosCarousel/usePhotosCarousel.js +76 -69
- package/components/Pill/index.js +45 -41
- package/components/PillList/index.js +24 -17
- package/components/PlacementWrapper/index.js +59 -38
- package/components/PopoverMenu/Popover.js +85 -68
- package/components/PopoverMenu/index.js +56 -51
- package/components/PopoverMultiSelect/index.js +59 -52
- package/components/ProgressCircle/ProgressCircle.js +32 -28
- package/components/RadioButton.js +19 -17
- package/components/Select/Select.js +15 -13
- package/components/Select/SelectT.js +250 -235
- package/components/Separator/index.js +17 -14
- package/components/Size.js +1 -0
- package/components/Slider/index.js +56 -43
- package/components/SummaryBox.js +72 -70
- package/components/Tabs/index.js +135 -133
- package/components/TextTruncated/index.js +130 -117
- package/components/Textfield/Textfield.js +190 -158
- package/components/Textfield/TruncatedTextArea.js +84 -70
- package/components/TimelineSlider/TimelineMarker.js +30 -25
- package/components/TimelineSlider/TimelineRail.js +11 -8
- package/components/TimelineSlider/index.js +66 -64
- package/components/TimelineSliderV2/ClusterBadge.js +29 -25
- package/components/TimelineSliderV2/TimelineDot.js +35 -31
- package/components/TimelineSliderV2/TimelineRail.js +11 -8
- package/components/TimelineSliderV2/index.js +103 -121
- package/components/TimelineSliderV2/layout.js +147 -124
- package/components/TimelineSliderV2/types.js +1 -0
- package/components/Tooltip/Tooltip.js +56 -47
- package/components/ViewPhotosDialog/index.js +35 -28
- package/components/VirtualWalkthrough/Annotation.js +207 -163
- package/components/VirtualWalkthrough/AnnotationEditPane.js +184 -198
- package/components/VirtualWalkthrough/AnnotationPanel.js +199 -187
- package/components/VirtualWalkthrough/Application.js +34 -31
- package/components/VirtualWalkthrough/AutoRotator.js +223 -85
- package/components/VirtualWalkthrough/BlockingSpinner.js +28 -26
- package/components/VirtualWalkthrough/BoundaryRing.js +34 -30
- package/components/VirtualWalkthrough/BoundaryWall.js +36 -32
- package/components/VirtualWalkthrough/CameraInfo.js +89 -100
- package/components/VirtualWalkthrough/ControlsInfoPane.js +195 -254
- package/components/VirtualWalkthrough/GradientSky.js +73 -72
- package/components/VirtualWalkthrough/SplatControls.js +221 -209
- package/components/VirtualWalkthrough/SplatCrop.js +20 -21
- package/components/VirtualWalkthrough/SplatFadeCrop.js +20 -21
- package/components/VirtualWalkthrough/SplatModel.js +85 -65
- package/components/VirtualWalkthrough/SplatRevealRain.js +46 -41
- package/components/VirtualWalkthrough/TfAnnotationManager.js +379 -197
- package/components/VirtualWalkthrough/TfXrNavigation.js +162 -73
- package/components/VirtualWalkthrough/VirtualWalkthroughViewer.js +431 -438
- package/components/VirtualWalkthrough/VrAnnotationPanel.js +22 -18
- package/components/VirtualWalkthrough/XrAnnotationInteraction.js +17 -14
- package/components/VirtualWalkthrough/XrExitButton.js +17 -14
- package/components/VirtualWalkthrough/XrGazeDwell.js +17 -14
- package/components/VirtualWalkthrough/XrPointerRay.js +17 -14
- package/components/VirtualWalkthrough/XrStartPose.js +121 -67
- package/components/VirtualWalkthrough/boundary-ring.js +117 -87
- package/components/VirtualWalkthrough/boundary-wall.js +258 -175
- package/components/VirtualWalkthrough/camera-adoption.js +64 -18
- package/components/VirtualWalkthrough/groundPlane.js +47 -32
- package/components/VirtualWalkthrough/gsplat-fade-crop-effect.js +35 -35
- package/components/VirtualWalkthrough/splatFormat.js +47 -13
- package/components/VirtualWalkthrough/useAdoptedCamera.js +44 -23
- package/components/VirtualWalkthrough/useRestartOnVr.js +37 -26
- package/components/VirtualWalkthrough/useXrRenderTuning.js +54 -33
- package/components/VirtualWalkthrough/vr-annotation-panel-layout.js +99 -67
- package/components/VirtualWalkthrough/vr-annotation-panel.js +433 -403
- package/components/VirtualWalkthrough/vr-annotation-scroll.js +28 -10
- package/components/VirtualWalkthrough/walkthrough-camera.js +329 -204
- package/components/VirtualWalkthrough/xr-annotation-candidate-buffer.js +80 -39
- package/components/VirtualWalkthrough/xr-annotation-candidates.js +18 -6
- package/components/VirtualWalkthrough/xr-annotation-interaction.js +56 -31
- package/components/VirtualWalkthrough/xr-annotation-targeting.js +38 -22
- package/components/VirtualWalkthrough/xr-button-arming.js +17 -7
- package/components/VirtualWalkthrough/xr-button-hold.js +36 -22
- package/components/VirtualWalkthrough/xr-exit-button-mask.js +36 -22
- package/components/VirtualWalkthrough/xr-exit-button.js +207 -148
- package/components/VirtualWalkthrough/xr-face-buttons.js +29 -19
- package/components/VirtualWalkthrough/xr-gaze-dwell-state.js +34 -23
- package/components/VirtualWalkthrough/xr-gaze-dwell.js +144 -107
- package/components/VirtualWalkthrough/xr-interactive-ui.js +24 -12
- package/components/VirtualWalkthrough/xr-pointer-ray.js +20 -13
- package/components/VirtualWalkthrough/xr-progress-pie-material.js +88 -117
- package/components/VirtualWalkthrough/xr-progress-pie.js +10 -1
- package/components/VirtualWalkthrough/xr-ray-quad.js +30 -16
- package/components/VirtualWalkthrough/xr-render-tuning.js +48 -17
- package/components/VirtualWalkthrough/xr-scene-bounds.js +37 -21
- package/components/VirtualWalkthrough/xr-start-pose.js +73 -27
- package/components/VirtualWalkthrough/xr-teleport-gesture.js +42 -16
- package/components/svg/BlobbyGrayIconImage.js +39 -41
- package/components/svg/BlobbyGrayIconImport.js +39 -41
- package/components/svg/BlobbyGrayIconUploadToTheCloud.js +39 -41
- package/components/svg/BlobbyIconCloudChecked.js +164 -202
- package/components/svg/BlobbyIconExportOrUploadPhoto.js +164 -202
- package/components/svg/BlobbyIconFolder.js +164 -202
- package/components/svg/BlobbyIconGraphReport.js +164 -202
- package/components/svg/BlobbyIconHappy.js +164 -202
- package/components/svg/BlobbyIconHeartMonitor.js +164 -202
- package/components/svg/BlobbyIconLeaf.js +164 -202
- package/components/svg/BlobbyIconLibrary.js +164 -202
- package/components/svg/BlobbyIconNursery.js +166 -204
- package/components/svg/BlobbyIconOrganization.js +164 -202
- package/components/svg/BlobbyIconParchment.js +164 -202
- package/components/svg/BlobbyIconPeople.js +164 -202
- package/components/svg/BlobbyIconSeedBank.js +164 -202
- package/components/svg/BlobbyIconSeedling.js +164 -202
- package/components/svg/BlobbyIconSeeds.js +164 -202
- package/components/svg/BlobbyIconSite.js +167 -206
- package/components/svg/BlobbyIconSparkles.js +164 -202
- package/components/svg/BlobbyIconWrench.js +164 -202
- package/components/svg/Bug.js +21 -19
- package/components/svg/Calendar.js +21 -19
- package/components/svg/CaretDown.js +21 -19
- package/components/svg/CaretLeft.js +21 -19
- package/components/svg/CaretRight.js +21 -19
- package/components/svg/CaretUp.js +21 -19
- package/components/svg/ChevronDown.js +21 -19
- package/components/svg/ChevronUp.js +21 -19
- package/components/svg/Close.js +21 -19
- package/components/svg/Critical.js +21 -19
- package/components/svg/Edit.js +164 -202
- package/components/svg/Error.js +21 -19
- package/components/svg/Filter.js +21 -19
- package/components/svg/Folder.js +21 -19
- package/components/svg/Help.js +21 -19
- package/components/svg/Home.js +21 -19
- package/components/svg/IconAdd.js +21 -19
- package/components/svg/IconArrowRight.js +23 -21
- package/components/svg/IconBusinessNetwork.js +21 -19
- package/components/svg/IconCalendar.js +21 -19
- package/components/svg/IconCancel.js +21 -19
- package/components/svg/IconChargingBattery.js +21 -19
- package/components/svg/IconChecklist.js +23 -21
- package/components/svg/IconCheckmark.js +21 -19
- package/components/svg/IconCoinInHand.js +21 -19
- package/components/svg/IconColumns.js +21 -19
- package/components/svg/IconComment.js +22 -20
- package/components/svg/IconDashboard.js +21 -19
- package/components/svg/IconDataMigration.js +21 -19
- package/components/svg/IconDocument.js +23 -21
- package/components/svg/IconDownloadFromTheCloud.js +23 -21
- package/components/svg/IconEdit.js +23 -21
- package/components/svg/IconExpand.js +23 -21
- package/components/svg/IconExport.js +21 -19
- package/components/svg/IconExternalLink.js +21 -19
- package/components/svg/IconEye.js +21 -19
- package/components/svg/IconEyeOff.js +21 -19
- package/components/svg/IconFile.js +21 -19
- package/components/svg/IconFolder.js +21 -19
- package/components/svg/IconFullScreen.js +22 -20
- package/components/svg/IconFutures.js +21 -19
- package/components/svg/IconGraphReport.js +21 -19
- package/components/svg/IconGrid.js +21 -19
- package/components/svg/IconHeartMonitor.js +21 -19
- package/components/svg/IconHelp.js +23 -21
- package/components/svg/IconHistory.js +21 -19
- package/components/svg/IconImport.js +21 -19
- package/components/svg/IconIndex.js +21 -19
- package/components/svg/IconLayers.js +21 -19
- package/components/svg/IconLibrary.js +22 -20
- package/components/svg/IconList.js +21 -19
- package/components/svg/IconLivePlant.js +21 -19
- package/components/svg/IconLocations.js +21 -19
- package/components/svg/IconMail.js +21 -19
- package/components/svg/IconManager.js +21 -19
- package/components/svg/IconMap.js +21 -19
- package/components/svg/IconMarker.js +21 -19
- package/components/svg/IconMenu.js +21 -19
- package/components/svg/IconMenuHorizontal.js +21 -19
- package/components/svg/IconModule.js +21 -19
- package/components/svg/IconMyLocation.js +21 -19
- package/components/svg/IconNote.js +23 -21
- package/components/svg/IconNursery.js +23 -21
- package/components/svg/IconOrg.js +21 -19
- package/components/svg/IconParchment.js +21 -19
- package/components/svg/IconPhoto.js +21 -19
- package/components/svg/IconPlantsFilled.js +21 -19
- package/components/svg/IconRedo.js +21 -19
- package/components/svg/IconRestorationSite.js +21 -19
- package/components/svg/IconSeedBank.js +21 -19
- package/components/svg/IconSeedling.js +21 -19
- package/components/svg/IconSettings.js +21 -19
- package/components/svg/IconSlice.js +21 -19
- package/components/svg/IconSubmit.js +21 -19
- package/components/svg/IconSubtract.js +23 -21
- package/components/svg/IconSynced.js +21 -19
- package/components/svg/IconTrashCan.js +21 -19
- package/components/svg/IconTreasureMap.js +21 -19
- package/components/svg/IconUnavailable.js +21 -19
- package/components/svg/IconUndo.js +21 -19
- package/components/svg/IconVariable.js +21 -19
- package/components/svg/IconVideo.js +31 -30
- package/components/svg/IconWifi.js +21 -19
- package/components/svg/Info.js +21 -19
- package/components/svg/Key.js +21 -19
- package/components/svg/Leaf.js +21 -19
- package/components/svg/Lock.js +21 -19
- package/components/svg/Logo.js +43 -48
- package/components/svg/MenuVertical.js +21 -19
- package/components/svg/Notification.js +21 -19
- package/components/svg/Person.js +21 -19
- package/components/svg/Plus.js +21 -19
- package/components/svg/RestorationSite.js +21 -19
- package/components/svg/Search.js +21 -19
- package/components/svg/Seeds.js +21 -19
- package/components/svg/Site.js +21 -19
- package/components/svg/Sparkles.js +21 -19
- package/components/svg/Species.js +21 -19
- package/components/svg/Species2.js +164 -202
- package/components/svg/Spinner.js +48 -54
- package/components/svg/Star.js +21 -19
- package/components/svg/Success.js +21 -19
- package/components/svg/SuccessFilled.js +21 -19
- package/components/svg/TerrawareLogoDesktop.js +49 -55
- package/components/svg/TerrawareLogoMobile.js +26 -25
- package/components/svg/Touchscreen.js +21 -19
- package/components/svg/UploadCloud.js +164 -202
- package/components/svg/Warning.js +21 -19
- package/components/svg/WelcomeClipboard.js +133 -156
- package/components/svg/index.js +129 -129
- package/components/table/EnhancedTableToolbar.js +74 -68
- package/components/table/EnhancedTableToolbarV2.js +41 -31
- package/components/table/TableCellRenderer.js +231 -196
- package/components/table/TableHeader.js +73 -71
- package/components/table/TableHeaderItem.js +102 -97
- package/components/table/density.js +19 -18
- package/components/table/index.js +369 -350
- package/components/table/sort.js +52 -27
- package/components/table/types.js +1 -0
- package/components/types/index.js +1 -0
- package/hooks/cameraEntityContext.js +13 -2
- package/hooks/useBoolean.js +13 -17
- package/hooks/useCameraPosition.js +56 -56
- package/hooks/useDevicePerformance.js +17 -17
- package/hooks/useXr.js +87 -85
- package/index.js +58 -58
- package/package.json +1 -1
- package/setupTests.js +4 -0
- package/strings/export.js +170 -111
- package/strings/index.js +54 -25
- package/strings/locales.js +41 -21
- package/strings/strings-en.js +51 -49
- package/strings/strings-es.js +51 -49
- package/strings/strings-fr.js +51 -49
- package/strings/strings-gx.js +51 -49
- package/style-dictionary-dist/TerrawareTheme.js +186 -186
- package/theme.js +76 -74
- package/utils/color.js +15 -8
- package/utils/date.js +108 -42
- package/utils/index.js +4 -4
- package/utils/preventDefaultEvent.js +3 -3
- package/utils/text.js +11 -11
- package/utils/useDeviceInfo.js +79 -42
- package/virtualWalkthrough.js +23 -23
|
@@ -1,218 +1,400 @@
|
|
|
1
|
-
import { createElement } from
|
|
2
|
-
import { renderToStaticMarkup } from
|
|
3
|
-
import { FILTER_LINEAR, PIXELFORMAT_RGBA8
|
|
4
|
-
import { AnnotationManager } from
|
|
5
|
-
import Icon from
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
115
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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 };
|