@terraware/web-components 4.2.13 → 4.2.14-rc.1
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 +8 -14
- package/components/Autocomplete/Autocomplete.js +67 -70
- package/components/Badge/index.js +16 -20
- package/components/BusySpinner/index.js +30 -35
- package/components/Button/Button.js +23 -27
- package/components/Button/button.test.js +8 -10
- package/components/Checkbox.js +10 -19
- package/components/Confirm/index.js +44 -51
- package/components/DatePicker/DatePicker.js +116 -117
- package/components/DialogBox/DialogBox.js +76 -66
- package/components/Divisor.js +15 -20
- package/components/Dropdown.js +54 -61
- package/components/EditableTable/index.js +129 -135
- package/components/ErrorBox/ErrorBox.js +37 -36
- package/components/FileChooser/index.js +73 -77
- package/components/FormBottomBar/index.js +63 -69
- package/components/Icon/Icon.js +14 -21
- package/components/Icon/icons/index.js +251 -257
- package/components/IconTooltip/index.js +29 -34
- package/components/Map/MapBox.js +199 -220
- package/components/Map/MapContainer.js +14 -18
- package/components/Map/MapDateSliderControl.js +44 -46
- package/components/Map/MapDrawer.js +32 -33
- package/components/Map/MapLegend.js +96 -95
- package/components/Map/MapViewStyleControl.js +38 -48
- package/components/Map/ReactMapboxWrapper.js +2 -9
- package/components/Map/index.js +47 -55
- package/components/Map/types.js +2 -8
- package/components/Map/useMaintainLayerOrder.js +4 -11
- package/components/Markdown/index.js +14 -21
- package/components/Message/Message.js +59 -51
- package/components/MultiSelect/MultiSelect.test.js +22 -24
- package/components/MultiSelect/index.js +73 -69
- package/components/Navbar/NavFooter.js +8 -13
- package/components/Navbar/NavItem.js +43 -37
- package/components/Navbar/NavSection.js +13 -17
- package/components/Navbar/Navbar.js +35 -38
- package/components/Navbar/SubNavbar.js +8 -13
- package/components/Note.js +14 -19
- package/components/OverlayModal/OverlayModal.js +36 -37
- package/components/PageForm/index.js +30 -35
- package/components/PhotoChooser/index.js +93 -98
- package/components/Pill/index.js +29 -31
- package/components/PillList/index.js +16 -22
- package/components/PlacementWrapper/index.js +20 -25
- package/components/PopoverMenu/Popover.js +33 -37
- package/components/PopoverMenu/index.js +41 -46
- package/components/PopoverMultiSelect/index.js +18 -21
- package/components/ProgressCircle/ProgressCircle.js +23 -26
- package/components/RadioButton.js +7 -14
- package/components/Select/Select.js +8 -13
- package/components/Select/SelectT.js +110 -108
- package/components/Separator/index.js +8 -14
- package/components/Size.js +1 -5
- package/components/Slider/index.js +7 -13
- package/components/SummaryBox.js +36 -39
- package/components/Tabs/index.js +75 -77
- package/components/TextTruncated/index.js +64 -65
- package/components/Textfield/Textfield.js +100 -95
- package/components/Textfield/TruncatedTextArea.js +44 -46
- package/components/TimelineSlider/TimelineMarker.js +24 -28
- package/components/TimelineSlider/TimelineRail.js +5 -11
- package/components/TimelineSlider/index.js +47 -51
- package/components/Tooltip/Tooltip.js +21 -26
- package/components/ViewPhotosDialog/index.js +58 -62
- package/components/VirtualWalkthrough/Annotation.d.ts +41 -0
- package/components/VirtualWalkthrough/Annotation.d.ts.map +1 -0
- package/components/VirtualWalkthrough/Annotation.js +197 -0
- package/components/VirtualWalkthrough/AnnotationEditPane.d.ts +20 -0
- package/components/VirtualWalkthrough/AnnotationEditPane.d.ts.map +1 -0
- package/components/VirtualWalkthrough/AnnotationEditPane.js +136 -0
- package/components/VirtualWalkthrough/AnnotationPanel.d.ts +13 -0
- package/components/VirtualWalkthrough/AnnotationPanel.d.ts.map +1 -0
- package/components/VirtualWalkthrough/AnnotationPanel.js +196 -0
- package/components/VirtualWalkthrough/Application.d.ts +9 -0
- package/components/VirtualWalkthrough/Application.d.ts.map +1 -0
- package/components/VirtualWalkthrough/Application.js +32 -0
- package/components/VirtualWalkthrough/AutoRotator.d.ts +109 -0
- package/components/VirtualWalkthrough/AutoRotator.d.ts.map +1 -0
- package/components/VirtualWalkthrough/AutoRotator.js +217 -0
- package/components/VirtualWalkthrough/BlockingSpinner.d.ts +3 -0
- package/components/VirtualWalkthrough/BlockingSpinner.d.ts.map +1 -0
- package/components/VirtualWalkthrough/BlockingSpinner.js +27 -0
- package/components/VirtualWalkthrough/BoundaryRing.d.ts +9 -0
- package/components/VirtualWalkthrough/BoundaryRing.d.ts.map +1 -0
- package/components/VirtualWalkthrough/BoundaryRing.js +33 -0
- package/components/VirtualWalkthrough/CameraInfo.d.ts +16 -0
- package/components/VirtualWalkthrough/CameraInfo.d.ts.map +1 -0
- package/components/VirtualWalkthrough/CameraInfo.js +88 -0
- package/components/VirtualWalkthrough/ControlsInfoPane.d.ts +37 -0
- package/components/VirtualWalkthrough/ControlsInfoPane.d.ts.map +1 -0
- package/components/VirtualWalkthrough/ControlsInfoPane.js +196 -0
- package/components/VirtualWalkthrough/GradientSky.d.ts +9 -0
- package/components/VirtualWalkthrough/GradientSky.d.ts.map +1 -0
- package/components/VirtualWalkthrough/GradientSky.js +73 -0
- package/components/VirtualWalkthrough/SplatControls.d.ts +50 -0
- package/components/VirtualWalkthrough/SplatControls.d.ts.map +1 -0
- package/components/VirtualWalkthrough/SplatControls.js +252 -0
- package/components/VirtualWalkthrough/SplatCrop.d.ts +8 -0
- package/components/VirtualWalkthrough/SplatCrop.d.ts.map +1 -0
- package/components/VirtualWalkthrough/SplatCrop.js +18 -0
- package/components/VirtualWalkthrough/SplatFadeCrop.d.ts +8 -0
- package/components/VirtualWalkthrough/SplatFadeCrop.d.ts.map +1 -0
- package/components/VirtualWalkthrough/SplatFadeCrop.js +18 -0
- package/components/VirtualWalkthrough/SplatModel.d.ts +15 -0
- package/components/VirtualWalkthrough/SplatModel.d.ts.map +1 -0
- package/components/VirtualWalkthrough/SplatModel.js +60 -0
- package/components/VirtualWalkthrough/SplatRevealRain.d.ts +18 -0
- package/components/VirtualWalkthrough/SplatRevealRain.d.ts.map +1 -0
- package/components/VirtualWalkthrough/SplatRevealRain.js +38 -0
- package/components/VirtualWalkthrough/TfAnnotationManager.d.ts +110 -0
- package/components/VirtualWalkthrough/TfAnnotationManager.d.ts.map +1 -0
- package/components/VirtualWalkthrough/TfAnnotationManager.js +368 -0
- package/components/VirtualWalkthrough/TfXrNavigation.d.ts +15 -0
- package/components/VirtualWalkthrough/TfXrNavigation.d.ts.map +1 -0
- package/components/VirtualWalkthrough/TfXrNavigation.js +24 -0
- package/components/VirtualWalkthrough/VirtualWalkthroughViewer.d.ts +27 -0
- package/components/VirtualWalkthrough/VirtualWalkthroughViewer.d.ts.map +1 -0
- package/components/VirtualWalkthrough/VirtualWalkthroughViewer.js +282 -0
- package/components/VirtualWalkthrough/annotation-styles.css +5 -0
- package/components/VirtualWalkthrough/application-styles.css +4 -0
- package/components/VirtualWalkthrough/boundary-ring.d.ts +47 -0
- package/components/VirtualWalkthrough/boundary-ring.d.ts.map +1 -0
- package/components/VirtualWalkthrough/boundary-ring.js +125 -0
- package/components/VirtualWalkthrough/boundary-ring.test.js +109 -0
- package/components/VirtualWalkthrough/groundPlane.d.ts +14 -0
- package/components/VirtualWalkthrough/groundPlane.d.ts.map +1 -0
- package/components/VirtualWalkthrough/groundPlane.js +47 -0
- package/components/VirtualWalkthrough/groundPlane.test.js +45 -0
- package/components/VirtualWalkthrough/gsplat-fade-crop-effect.d.ts +19 -0
- package/components/VirtualWalkthrough/gsplat-fade-crop-effect.d.ts.map +1 -0
- package/components/VirtualWalkthrough/gsplat-fade-crop-effect.js +119 -0
- package/components/VirtualWalkthrough/walkthrough-camera.d.ts +77 -0
- package/components/VirtualWalkthrough/walkthrough-camera.d.ts.map +1 -0
- package/components/VirtualWalkthrough/walkthrough-camera.js +320 -0
- package/components/svg/BlobbyGrayIconImage.js +36 -41
- package/components/svg/BlobbyGrayIconImport.js +36 -41
- package/components/svg/BlobbyGrayIconUploadToTheCloud.js +36 -41
- package/components/svg/BlobbyIconCloudChecked.js +161 -158
- package/components/svg/BlobbyIconExportOrUploadPhoto.js +161 -158
- package/components/svg/BlobbyIconFolder.js +161 -158
- package/components/svg/BlobbyIconGraphReport.js +161 -158
- package/components/svg/BlobbyIconHappy.js +161 -158
- package/components/svg/BlobbyIconHeartMonitor.js +161 -158
- package/components/svg/BlobbyIconLeaf.js +161 -158
- package/components/svg/BlobbyIconLibrary.js +161 -158
- package/components/svg/BlobbyIconNursery.js +163 -160
- package/components/svg/BlobbyIconOrganization.js +161 -158
- package/components/svg/BlobbyIconParchment.js +161 -158
- package/components/svg/BlobbyIconPeople.js +161 -158
- package/components/svg/BlobbyIconSeedBank.js +161 -158
- package/components/svg/BlobbyIconSeedling.js +161 -158
- package/components/svg/BlobbyIconSeeds.js +161 -158
- package/components/svg/BlobbyIconSite.js +164 -161
- package/components/svg/BlobbyIconSparkles.js +161 -158
- package/components/svg/BlobbyIconWrench.js +161 -158
- package/components/svg/Bug.js +18 -27
- package/components/svg/Calendar.js +18 -27
- package/components/svg/CaretDown.js +18 -27
- package/components/svg/CaretLeft.js +18 -27
- package/components/svg/CaretRight.js +18 -27
- package/components/svg/CaretUp.js +18 -27
- package/components/svg/ChevronDown.js +18 -27
- package/components/svg/ChevronUp.js +18 -27
- package/components/svg/Close.js +18 -27
- package/components/svg/Critical.js +18 -27
- package/components/svg/Edit.js +161 -158
- package/components/svg/Error.js +18 -27
- package/components/svg/Filter.js +18 -27
- package/components/svg/Folder.js +18 -27
- package/components/svg/Help.js +18 -27
- package/components/svg/Home.js +18 -27
- package/components/svg/IconAdd.js +18 -27
- package/components/svg/IconArrowRight.js +19 -28
- package/components/svg/IconBusinessNetwork.js +18 -27
- package/components/svg/IconCalendar.js +18 -27
- package/components/svg/IconCancel.js +18 -27
- package/components/svg/IconChargingBattery.js +18 -27
- package/components/svg/IconChecklist.js +19 -28
- package/components/svg/IconCheckmark.js +18 -27
- package/components/svg/IconCoinInHand.js +18 -27
- package/components/svg/IconColumns.js +18 -27
- package/components/svg/IconComment.js +18 -27
- package/components/svg/IconDashboard.js +18 -27
- package/components/svg/IconDataMigration.js +18 -27
- package/components/svg/IconDocument.js +19 -28
- package/components/svg/IconDownloadFromTheCloud.js +19 -28
- package/components/svg/IconEdit.js +19 -28
- package/components/svg/IconExpand.js +19 -28
- package/components/svg/IconExport.js +18 -27
- package/components/svg/IconExternalLink.js +18 -27
- package/components/svg/IconEye.js +18 -27
- package/components/svg/IconEyeOff.js +18 -27
- package/components/svg/IconFile.js +18 -27
- package/components/svg/IconFolder.js +18 -27
- package/components/svg/IconFullScreen.js +18 -27
- package/components/svg/IconFutures.js +18 -27
- package/components/svg/IconGraphReport.js +18 -27
- package/components/svg/IconHeartMonitor.js +18 -27
- package/components/svg/IconHelp.js +19 -28
- package/components/svg/IconHistory.js +18 -27
- package/components/svg/IconImport.js +18 -27
- package/components/svg/IconIndex.js +18 -27
- package/components/svg/IconLayers.js +18 -27
- package/components/svg/IconLibrary.js +18 -27
- package/components/svg/IconList.js +18 -27
- package/components/svg/IconLivePlant.js +18 -27
- package/components/svg/IconLocations.js +18 -27
- package/components/svg/IconMail.js +18 -27
- package/components/svg/IconManager.js +18 -27
- package/components/svg/IconMarker.js +18 -27
- package/components/svg/IconMenu.js +18 -27
- package/components/svg/IconMenuHorizontal.js +18 -27
- package/components/svg/IconModule.js +18 -27
- package/components/svg/IconMyLocation.js +18 -27
- package/components/svg/IconNote.js +19 -28
- package/components/svg/IconNursery.js +18 -27
- package/components/svg/IconOrg.js +18 -27
- package/components/svg/IconParchment.js +18 -27
- package/components/svg/IconPhoto.js +18 -27
- package/components/svg/IconPlantsFilled.js +18 -27
- package/components/svg/IconRedo.js +18 -27
- package/components/svg/IconRestorationSite.js +18 -27
- package/components/svg/IconSeedBank.js +18 -27
- package/components/svg/IconSeedling.js +18 -27
- package/components/svg/IconSettings.js +18 -27
- package/components/svg/IconSlice.js +18 -27
- package/components/svg/IconSubmit.js +18 -27
- package/components/svg/IconSubtract.js +19 -28
- package/components/svg/IconSynced.js +18 -27
- package/components/svg/IconTrashCan.js +18 -27
- package/components/svg/IconTreasureMap.js +18 -27
- package/components/svg/IconUnavailable.js +18 -27
- package/components/svg/IconUndo.js +18 -27
- package/components/svg/IconVariable.js +18 -27
- package/components/svg/IconVideo.js +19 -28
- package/components/svg/IconWifi.js +18 -27
- package/components/svg/Info.js +18 -27
- package/components/svg/Key.js +18 -27
- package/components/svg/Leaf.js +18 -27
- package/components/svg/Lock.js +18 -27
- package/components/svg/Logo.js +40 -45
- package/components/svg/MenuVertical.js +18 -27
- package/components/svg/Notification.js +18 -27
- package/components/svg/Person.js +18 -27
- package/components/svg/Plus.js +18 -27
- package/components/svg/RestorationSite.js +18 -27
- package/components/svg/Search.js +18 -27
- package/components/svg/Seeds.js +18 -27
- package/components/svg/Site.js +18 -27
- package/components/svg/Sparkles.js +18 -27
- package/components/svg/Species.js +18 -27
- package/components/svg/Species2.js +161 -158
- package/components/svg/Spinner.js +27 -36
- package/components/svg/Star.js +18 -27
- package/components/svg/Success.js +18 -27
- package/components/svg/SuccessFilled.js +18 -27
- package/components/svg/TerrawareLogoDesktop.js +44 -49
- package/components/svg/TerrawareLogoMobile.js +20 -29
- package/components/svg/Touchscreen.js +18 -27
- package/components/svg/UploadCloud.js +161 -158
- package/components/svg/Warning.js +18 -27
- package/components/svg/WelcomeClipboard.js +130 -135
- package/components/svg/index.js +127 -895
- package/components/table/EnhancedTableToolbar.js +45 -47
- package/components/table/EnhancedTableToolbarV2.js +18 -26
- package/components/table/TableCellRenderer.js +137 -143
- package/components/table/TableHeader.js +45 -47
- package/components/table/TableHeaderItem.js +65 -64
- package/components/table/density.js +3 -11
- package/components/table/index.js +225 -218
- package/components/table/sort.js +8 -18
- package/components/table/sort.test.js +6 -8
- package/components/table/types.js +1 -5
- package/components/types/index.js +1 -5
- package/hooks/useBoolean.d.ts +4 -0
- package/hooks/useBoolean.d.ts.map +1 -0
- package/hooks/useBoolean.js +12 -0
- package/hooks/useCameraPosition.d.ts +8 -0
- package/hooks/useCameraPosition.d.ts.map +1 -0
- package/hooks/useCameraPosition.js +33 -0
- package/hooks/useDevicePerformance.d.ts +9 -0
- package/hooks/useDevicePerformance.d.ts.map +1 -0
- package/hooks/useDevicePerformance.js +16 -0
- package/index.js +57 -398
- package/license-report.html +1 -1
- package/package.json +10 -3
- package/style-dictionary-dist/TerrawareTheme.js +1 -7
- package/theme.js +7 -14
- package/utils/color.d.ts +2 -0
- package/utils/color.d.ts.map +1 -0
- package/utils/color.js +14 -0
- package/utils/date.js +13 -24
- package/utils/date.test.js +40 -43
- package/utils/index.js +5 -53
- package/utils/preventDefaultEvent.js +1 -7
- package/utils/text.js +2 -9
- package/utils/text.test.js +9 -11
- package/utils/useDeviceInfo.js +10 -16
- package/virtualWalkthrough.d.ts +34 -0
- package/virtualWalkthrough.d.ts.map +1 -0
- package/virtualWalkthrough.js +30 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import { useTheme } from '@mui/material';
|
|
3
|
+
import { Entity } from '@playcanvas/react';
|
|
4
|
+
import { Camera, Script } from '@playcanvas/react/components';
|
|
5
|
+
import { useApp } from '@playcanvas/react/hooks';
|
|
6
|
+
import { Color, Vec3 } from 'playcanvas';
|
|
7
|
+
import { XrControllers } from 'playcanvas/scripts/esm/xr-controllers.mjs';
|
|
8
|
+
import { useCameraPosition } from '../../hooks/useCameraPosition';
|
|
9
|
+
import { useDevicePerformance } from '../../hooks/useDevicePerformance';
|
|
10
|
+
import Annotation from './Annotation';
|
|
11
|
+
import AnnotationPanel from './AnnotationPanel';
|
|
12
|
+
import { AutoRotator } from './AutoRotator';
|
|
13
|
+
import BoundaryRing from './BoundaryRing';
|
|
14
|
+
import GradientSky from './GradientSky';
|
|
15
|
+
import SplatControls from './SplatControls';
|
|
16
|
+
import SplatModel from './SplatModel';
|
|
17
|
+
import { TfAnnotationManager } from './TfAnnotationManager';
|
|
18
|
+
import { TfXrNavigation } from './TfXrNavigation';
|
|
19
|
+
import { WalkthroughCamera } from './walkthrough-camera';
|
|
20
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
21
|
+
const DEFAULT_FOCUS_POINT = [0, 0.1, 0];
|
|
22
|
+
const DEFAULT_POSITION = [1, 0.1, 0];
|
|
23
|
+
const VirtualWalkthroughViewer = ({
|
|
24
|
+
splatSrc,
|
|
25
|
+
origin = DEFAULT_FOCUS_POINT,
|
|
26
|
+
cameraPosition = DEFAULT_POSITION,
|
|
27
|
+
sceneBounds,
|
|
28
|
+
groundPlane: groundPlaneProp,
|
|
29
|
+
skyColor,
|
|
30
|
+
groundColor,
|
|
31
|
+
averageCameraHeight = 0,
|
|
32
|
+
annotations,
|
|
33
|
+
onSaveAnnotations,
|
|
34
|
+
strings,
|
|
35
|
+
editable = false,
|
|
36
|
+
showFreeFly = false,
|
|
37
|
+
isFullScreen = false,
|
|
38
|
+
onToggleFullScreen
|
|
39
|
+
}) => {
|
|
40
|
+
const theme = useTheme();
|
|
41
|
+
const {
|
|
42
|
+
setCamera
|
|
43
|
+
} = useCameraPosition();
|
|
44
|
+
const {
|
|
45
|
+
isHighPerformance
|
|
46
|
+
} = useDevicePerformance();
|
|
47
|
+
const app = useApp();
|
|
48
|
+
const [showAnnotations, setShowAnnotations] = useState(true);
|
|
49
|
+
const [autoRotate, setAutoRotate] = useState(true);
|
|
50
|
+
const [isEdit, setIsEdit] = useState(false);
|
|
51
|
+
const [isFreeFly, setIsFreeFly] = useState(false);
|
|
52
|
+
const [selectedAnnotationIndex, setSelectedAnnotationIndex] = useState(-1);
|
|
53
|
+
const [localAnnotations, setLocalAnnotations] = useState(annotations);
|
|
54
|
+
const [isTextFieldFocused, setIsTextFieldFocused] = useState(false);
|
|
55
|
+
const [viewingAnnotation, setViewingAnnotation] = useState(null);
|
|
56
|
+
const [viewingAnnotationIndex, setViewingAnnotationIndex] = useState(-1);
|
|
57
|
+
const [viewedScreenPos, setViewedScreenPos] = useState(null);
|
|
58
|
+
const sceneBoundsRadius = useMemo(() => {
|
|
59
|
+
if (sceneBounds?.m !== undefined) {
|
|
60
|
+
return sceneBounds.m;
|
|
61
|
+
}
|
|
62
|
+
const dx = cameraPosition[0] - origin[0];
|
|
63
|
+
const dy = cameraPosition[1] - origin[1];
|
|
64
|
+
const dz = cameraPosition[2] - origin[2];
|
|
65
|
+
return Math.sqrt(dx * dx + dy * dy + dz * dz) * 0.5;
|
|
66
|
+
}, [cameraPosition, sceneBounds, origin]);
|
|
67
|
+
const sceneBoundsCenter = useMemo(() => sceneBounds ? new Vec3(sceneBounds.x, sceneBounds.y, sceneBounds.z) : new Vec3(origin[0], cameraPosition[1], origin[2]), [sceneBounds, origin, cameraPosition]);
|
|
68
|
+
const groundPlane = useMemo(() => groundPlaneProp?.length === 3 ? groundPlaneProp.map(p => new Vec3(p[0], p[1], p[2])) : [], [groundPlaneProp]);
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
setCamera(origin, cameraPosition);
|
|
71
|
+
}, [origin, cameraPosition, setCamera]);
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (!groundPlane.length) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
// @ts-expect-error - scripts are added dynamically to the camera entity
|
|
77
|
+
const walkthroughCam = app.root.findByName('camera')?.script?.walkthroughCamera;
|
|
78
|
+
if (walkthroughCam) {
|
|
79
|
+
// Should be changed to a react prop if shallowEquals in playcanvas/react is fixed (see https://github.com/playcanvas/react/pull/298)
|
|
80
|
+
walkthroughCam.groundPlane = groundPlane;
|
|
81
|
+
}
|
|
82
|
+
}, [groundPlane, app]);
|
|
83
|
+
const handleToggleFreeFly = useCallback(() => {
|
|
84
|
+
const newFreeFly = !isFreeFly;
|
|
85
|
+
// @ts-expect-error - scripts are added dynamically to the camera entity
|
|
86
|
+
const walkthroughCam = app.root.findByName('camera')?.script?.walkthroughCamera;
|
|
87
|
+
if (walkthroughCam) {
|
|
88
|
+
walkthroughCam.freeFly = newFreeFly;
|
|
89
|
+
}
|
|
90
|
+
if (!newFreeFly) {
|
|
91
|
+
setCamera(origin, cameraPosition);
|
|
92
|
+
}
|
|
93
|
+
setIsFreeFly(newFreeFly);
|
|
94
|
+
}, [isFreeFly, app, setCamera, origin, cameraPosition]);
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
setLocalAnnotations(annotations);
|
|
97
|
+
}, [annotations]);
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
if (!isEdit) {
|
|
100
|
+
setSelectedAnnotationIndex(-1);
|
|
101
|
+
}
|
|
102
|
+
}, [isEdit]);
|
|
103
|
+
const handleAnnotationPositionChange = useCallback(position => {
|
|
104
|
+
setLocalAnnotations(prev => {
|
|
105
|
+
if (selectedAnnotationIndex === -1) {
|
|
106
|
+
return prev;
|
|
107
|
+
}
|
|
108
|
+
const updated = [...prev];
|
|
109
|
+
updated[selectedAnnotationIndex] = {
|
|
110
|
+
...updated[selectedAnnotationIndex],
|
|
111
|
+
position
|
|
112
|
+
};
|
|
113
|
+
return updated;
|
|
114
|
+
});
|
|
115
|
+
}, [selectedAnnotationIndex]);
|
|
116
|
+
const handleSave = useCallback(() => {
|
|
117
|
+
const saveAndClose = async () => {
|
|
118
|
+
await onSaveAnnotations(localAnnotations);
|
|
119
|
+
setIsEdit(false);
|
|
120
|
+
setSelectedAnnotationIndex(-1);
|
|
121
|
+
};
|
|
122
|
+
void saveAndClose();
|
|
123
|
+
}, [onSaveAnnotations, localAnnotations]);
|
|
124
|
+
const handleCancel = useCallback(() => {
|
|
125
|
+
setLocalAnnotations(annotations);
|
|
126
|
+
setIsEdit(false);
|
|
127
|
+
setSelectedAnnotationIndex(-1);
|
|
128
|
+
}, [annotations]);
|
|
129
|
+
const handleAddAnnotation = useCallback(() => {
|
|
130
|
+
const newAnnotation = {
|
|
131
|
+
position: origin,
|
|
132
|
+
title: ''
|
|
133
|
+
};
|
|
134
|
+
setLocalAnnotations(prev => [...prev, newAnnotation]);
|
|
135
|
+
setSelectedAnnotationIndex(localAnnotations.length);
|
|
136
|
+
}, [origin, localAnnotations]);
|
|
137
|
+
const handleDeleteAnnotation = useCallback(() => {
|
|
138
|
+
if (selectedAnnotationIndex === -1) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
setLocalAnnotations(prev => prev.filter((_, index) => index !== selectedAnnotationIndex));
|
|
142
|
+
setSelectedAnnotationIndex(-1);
|
|
143
|
+
}, [selectedAnnotationIndex]);
|
|
144
|
+
const handleDeselectAnnotation = useCallback(() => {
|
|
145
|
+
setSelectedAnnotationIndex(-1);
|
|
146
|
+
}, []);
|
|
147
|
+
const handleAnnotationView = useCallback(
|
|
148
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
149
|
+
(annotation, annotationIndex, screenX, screenY) => {
|
|
150
|
+
setViewingAnnotation(annotation);
|
|
151
|
+
setViewingAnnotationIndex(annotationIndex);
|
|
152
|
+
// Cleared so the connector line waits for the hotspot's post-aim position
|
|
153
|
+
// rather than briefly pointing at its old spot.
|
|
154
|
+
setViewedScreenPos(null);
|
|
155
|
+
}, []);
|
|
156
|
+
const handleAnnotationScreenPositionUpdate = useCallback((_index, screenX, screenY, size) => {
|
|
157
|
+
// The scene is frozen while the panel is open, so guard against redundant
|
|
158
|
+
// updates from the per-frame callback to avoid needless re-renders.
|
|
159
|
+
setViewedScreenPos(prev => prev && prev.x === screenX && prev.y === screenY && prev.size === size ? prev : {
|
|
160
|
+
x: screenX,
|
|
161
|
+
y: screenY,
|
|
162
|
+
size
|
|
163
|
+
});
|
|
164
|
+
}, []);
|
|
165
|
+
const handleCloseAnnotation = useCallback(() => {
|
|
166
|
+
setViewingAnnotation(null);
|
|
167
|
+
setViewingAnnotationIndex(-1);
|
|
168
|
+
setViewedScreenPos(null);
|
|
169
|
+
}, []);
|
|
170
|
+
const handleAnnotationUpdate = useCallback(updates => {
|
|
171
|
+
if (selectedAnnotationIndex === -1) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
setLocalAnnotations(prev => {
|
|
175
|
+
const updated = [...prev];
|
|
176
|
+
updated[selectedAnnotationIndex] = {
|
|
177
|
+
...updated[selectedAnnotationIndex],
|
|
178
|
+
...updates
|
|
179
|
+
};
|
|
180
|
+
return updated;
|
|
181
|
+
});
|
|
182
|
+
}, [selectedAnnotationIndex]);
|
|
183
|
+
const canSave = useMemo(() => localAnnotations.every(annotation => annotation.title && annotation.title.trim() !== ''), [localAnnotations]);
|
|
184
|
+
const splatModel = useMemo(() => /*#__PURE__*/_jsx(SplatModel, {
|
|
185
|
+
splatSrc: splatSrc,
|
|
186
|
+
rotation: [-180, 0, 0],
|
|
187
|
+
revealRain: isHighPerformance
|
|
188
|
+
}, 'splat'), [isHighPerformance, splatSrc]);
|
|
189
|
+
return /*#__PURE__*/_jsxs(_Fragment, {
|
|
190
|
+
children: [/*#__PURE__*/_jsx(GradientSky, {
|
|
191
|
+
topColor: skyColor || '#FFFFFF',
|
|
192
|
+
horizonColor: skyColor || '#EAF8FF',
|
|
193
|
+
groundColor: groundColor || '#C3BDB7'
|
|
194
|
+
}), /*#__PURE__*/_jsxs(Entity, {
|
|
195
|
+
name: "camera-root",
|
|
196
|
+
children: [/*#__PURE__*/_jsxs(Entity, {
|
|
197
|
+
name: "camera",
|
|
198
|
+
children: [/*#__PURE__*/_jsx(Camera, {
|
|
199
|
+
clearColor: "#EAF8FF",
|
|
200
|
+
fov: 60
|
|
201
|
+
}), /*#__PURE__*/_jsx(Script, {
|
|
202
|
+
script: WalkthroughCamera,
|
|
203
|
+
boundsCenter: sceneBoundsCenter,
|
|
204
|
+
boundsRadius: sceneBoundsRadius,
|
|
205
|
+
enableFly: !isTextFieldFocused,
|
|
206
|
+
averageCameraHeight: averageCameraHeight
|
|
207
|
+
})]
|
|
208
|
+
}), /*#__PURE__*/_jsx(Script, {
|
|
209
|
+
script: XrControllers,
|
|
210
|
+
enabled: !isEdit
|
|
211
|
+
}), /*#__PURE__*/_jsx(Script, {
|
|
212
|
+
script: TfXrNavigation,
|
|
213
|
+
enabled: !isEdit,
|
|
214
|
+
enableTeleport: false
|
|
215
|
+
}), /*#__PURE__*/_jsx(Script, {
|
|
216
|
+
script: AutoRotator,
|
|
217
|
+
enabled: !isEdit && autoRotate && !viewingAnnotation,
|
|
218
|
+
startDelay: 0.5,
|
|
219
|
+
restartDelay: 3,
|
|
220
|
+
startFadeInTime: 0.5
|
|
221
|
+
})]
|
|
222
|
+
}), splatModel, sceneBounds?.m !== undefined && groundPlane.length === 3 && /*#__PURE__*/_jsx(BoundaryRing, {
|
|
223
|
+
center: sceneBoundsCenter,
|
|
224
|
+
radius: sceneBoundsRadius,
|
|
225
|
+
groundPlane: groundPlane
|
|
226
|
+
}), localAnnotations.length > 0 && /*#__PURE__*/_jsxs(Entity, {
|
|
227
|
+
name: "annotations-root",
|
|
228
|
+
children: [/*#__PURE__*/_jsx(Script, {
|
|
229
|
+
script: TfAnnotationManager,
|
|
230
|
+
enabled: true,
|
|
231
|
+
hotspotSize: 30,
|
|
232
|
+
maxWorldSize: 0.05,
|
|
233
|
+
opacity: 1,
|
|
234
|
+
hotspotColor: new Color().fromString(theme.palette.TwClrIcnBrand),
|
|
235
|
+
hoverColor: new Color().fromString('#ffffff'),
|
|
236
|
+
hotspotBackgroundColor: theme.palette.TwClrBaseWhite
|
|
237
|
+
}), localAnnotations.map((annotation, index) => /*#__PURE__*/_jsx(Annotation, {
|
|
238
|
+
...annotation,
|
|
239
|
+
index: index,
|
|
240
|
+
visible: showAnnotations,
|
|
241
|
+
isEdit: isEdit,
|
|
242
|
+
isSelected: selectedAnnotationIndex === index,
|
|
243
|
+
isViewed: viewingAnnotationIndex === index,
|
|
244
|
+
onSelect: () => setSelectedAnnotationIndex(index),
|
|
245
|
+
onPositionChange: handleAnnotationPositionChange,
|
|
246
|
+
onView: (anno, screenX, screenY) => handleAnnotationView(anno, index, screenX, screenY),
|
|
247
|
+
onScreenPositionUpdate: handleAnnotationScreenPositionUpdate
|
|
248
|
+
}, `annotation-${index}`))]
|
|
249
|
+
}), /*#__PURE__*/_jsx(SplatControls, {
|
|
250
|
+
defaultCameraFocus: origin,
|
|
251
|
+
defaultCameraPosition: cameraPosition,
|
|
252
|
+
showAnnotations: showAnnotations,
|
|
253
|
+
onToggleAnnotations: setShowAnnotations,
|
|
254
|
+
autoRotate: autoRotate,
|
|
255
|
+
onToggleAutoRotate: setAutoRotate,
|
|
256
|
+
isEdit: isEdit,
|
|
257
|
+
onToggleEdit: setIsEdit,
|
|
258
|
+
onSave: handleSave,
|
|
259
|
+
onCancel: handleCancel,
|
|
260
|
+
onAddAnnotation: handleAddAnnotation,
|
|
261
|
+
onDeleteAnnotation: handleDeleteAnnotation,
|
|
262
|
+
onDeselectAnnotation: handleDeselectAnnotation,
|
|
263
|
+
hasSelectedAnnotation: selectedAnnotationIndex >= 0,
|
|
264
|
+
selectedAnnotation: selectedAnnotationIndex >= 0 ? localAnnotations[selectedAnnotationIndex] : null,
|
|
265
|
+
onAnnotationUpdate: handleAnnotationUpdate,
|
|
266
|
+
onTextFieldFocus: setIsTextFieldFocused,
|
|
267
|
+
canSave: canSave,
|
|
268
|
+
editable: editable,
|
|
269
|
+
isFullScreen: isFullScreen,
|
|
270
|
+
onToggleFullScreen: onToggleFullScreen,
|
|
271
|
+
isFreeFly: isFreeFly,
|
|
272
|
+
onToggleFreeFly: handleToggleFreeFly,
|
|
273
|
+
showFreeFly: showFreeFly,
|
|
274
|
+
strings: strings
|
|
275
|
+
}), /*#__PURE__*/_jsx(AnnotationPanel, {
|
|
276
|
+
annotation: viewingAnnotation,
|
|
277
|
+
hotspotPosition: viewedScreenPos,
|
|
278
|
+
onClose: handleCloseAnnotation
|
|
279
|
+
})]
|
|
280
|
+
});
|
|
281
|
+
};
|
|
282
|
+
export default VirtualWalkthroughViewer;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Color, Script, Vec3 } from 'playcanvas';
|
|
2
|
+
export type BoundaryRingGeometryParams = {
|
|
3
|
+
center: Vec3;
|
|
4
|
+
radius: number;
|
|
5
|
+
groundPlane: Vec3[];
|
|
6
|
+
/** Band width in world units (the ring spans radius ± width / 2). */
|
|
7
|
+
width: number;
|
|
8
|
+
dashCount?: number;
|
|
9
|
+
dashRatio?: number;
|
|
10
|
+
};
|
|
11
|
+
/** Flat-array triangle geometry ready for `Mesh.setPositions` / `Mesh.setIndices`. */
|
|
12
|
+
export type BoundaryRingGeometry = {
|
|
13
|
+
positions: number[];
|
|
14
|
+
indices: number[];
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Triangle geometry for a dashed boundary ring laid flat on the ground plane. Each dash is a
|
|
18
|
+
* short band (a quad = two triangles) sweeping from `radius - width / 2` to `radius + width / 2`;
|
|
19
|
+
* every vertex is sampled on the circle in XZ and lifted onto the plane via `yOnPlane`.
|
|
20
|
+
* Returns empty arrays when radius, width, or dashCount is non-positive, or the plane is invalid.
|
|
21
|
+
*/
|
|
22
|
+
export declare const boundaryRingMesh: ({ center, radius, groundPlane, width, dashCount, dashRatio, }: BoundaryRingGeometryParams) => BoundaryRingGeometry;
|
|
23
|
+
/**
|
|
24
|
+
* Draws a dashed boundary ring as a flat band mesh on the ground plane.
|
|
25
|
+
*
|
|
26
|
+
* Rendered as a thin triangle band whose width is a fraction of the radius (`widthRatio`).
|
|
27
|
+
* center / radius / groundPlane are set imperatively by the BoundaryRing component (see BoundaryRing.tsx for why
|
|
28
|
+
* they are not reactive Script props), which calls `rebuild()` after updating them.
|
|
29
|
+
*/
|
|
30
|
+
export declare class BoundaryRingScript extends Script {
|
|
31
|
+
static scriptName: string;
|
|
32
|
+
center: Vec3;
|
|
33
|
+
radius: number;
|
|
34
|
+
groundPlane: Vec3[];
|
|
35
|
+
color: Color;
|
|
36
|
+
dashCount: number;
|
|
37
|
+
dashRatio: number;
|
|
38
|
+
/** Band width as a fraction of the radius. */
|
|
39
|
+
widthRatio: number;
|
|
40
|
+
private _material?;
|
|
41
|
+
private _mesh?;
|
|
42
|
+
initialize(): void;
|
|
43
|
+
rebuild(): void;
|
|
44
|
+
private _clearMesh;
|
|
45
|
+
destroy(): void;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=boundary-ring.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"boundary-ring.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/boundary-ring.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,EAIL,MAAM,EAEN,IAAI,EACL,MAAM,YAAY,CAAC;AAIpB,MAAM,MAAM,0BAA0B,GAAG;IACvC,MAAM,EAAE,IAAI,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,IAAI,EAAE,CAAC;IACpB,qEAAqE;IACrE,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,sFAAsF;AACtF,MAAM,MAAM,oBAAoB,GAAG;IAAE,SAAS,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC;AAE9E;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,GAAI,+DAO9B,0BAA0B,KAAG,oBAgC/B,CAAC;AAEF;;;;;;GAMG;AACH,qBAAa,kBAAmB,SAAQ,MAAM;IAC5C,MAAM,CAAC,UAAU,SAAkB;IAEnC,MAAM,OAAc;IACpB,MAAM,SAAK;IACX,WAAW,EAAE,IAAI,EAAE,CAAM;IACzB,KAAK,QAAsB;IAC3B,SAAS,SAAM;IACf,SAAS,SAAO;IAChB,8CAA8C;IAC9C,UAAU,SAAQ;IAElB,OAAO,CAAC,SAAS,CAAC,CAAmB;IACrC,OAAO,CAAC,KAAK,CAAC,CAAO;IAErB,UAAU;IAUV,OAAO;IA+BP,OAAO,CAAC,UAAU;IAUlB,OAAO;CAKR"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { CULLFACE_NONE, Color, LAYERID_IMMEDIATE, Mesh, MeshInstance, Script, StandardMaterial, Vec3 } from 'playcanvas';
|
|
2
|
+
import { computeGroundPlane, yOnPlane } from './groundPlane';
|
|
3
|
+
|
|
4
|
+
/** Flat-array triangle geometry ready for `Mesh.setPositions` / `Mesh.setIndices`. */
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Triangle geometry for a dashed boundary ring laid flat on the ground plane. Each dash is a
|
|
8
|
+
* short band (a quad = two triangles) sweeping from `radius - width / 2` to `radius + width / 2`;
|
|
9
|
+
* every vertex is sampled on the circle in XZ and lifted onto the plane via `yOnPlane`.
|
|
10
|
+
* Returns empty arrays when radius, width, or dashCount is non-positive, or the plane is invalid.
|
|
11
|
+
*/
|
|
12
|
+
export const boundaryRingMesh = ({
|
|
13
|
+
center,
|
|
14
|
+
radius,
|
|
15
|
+
groundPlane,
|
|
16
|
+
width,
|
|
17
|
+
dashCount = 64,
|
|
18
|
+
dashRatio = 0.5
|
|
19
|
+
}) => {
|
|
20
|
+
const plane = computeGroundPlane(groundPlane);
|
|
21
|
+
if (radius <= 0 || width <= 0 || dashCount <= 0 || !plane) {
|
|
22
|
+
return {
|
|
23
|
+
positions: [],
|
|
24
|
+
indices: []
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const innerR = radius - width / 2;
|
|
28
|
+
const outerR = radius + width / 2;
|
|
29
|
+
const step = Math.PI * 2 / dashCount;
|
|
30
|
+
const on = step * dashRatio;
|
|
31
|
+
const positions = [];
|
|
32
|
+
const indices = [];
|
|
33
|
+
const pushVertex = (r, angle) => {
|
|
34
|
+
const x = center.x + r * Math.cos(angle);
|
|
35
|
+
const z = center.z + r * Math.sin(angle);
|
|
36
|
+
positions.push(x, yOnPlane(x, z, plane.normal, plane.point, center.y), z);
|
|
37
|
+
};
|
|
38
|
+
for (let i = 0; i < dashCount; i++) {
|
|
39
|
+
const a0 = i * step;
|
|
40
|
+
const a1 = a0 + on;
|
|
41
|
+
const base = i * 4;
|
|
42
|
+
// Per dash: v0 inner@a0, v1 outer@a0, v2 inner@a1, v3 outer@a1.
|
|
43
|
+
pushVertex(innerR, a0);
|
|
44
|
+
pushVertex(outerR, a0);
|
|
45
|
+
pushVertex(innerR, a1);
|
|
46
|
+
pushVertex(outerR, a1);
|
|
47
|
+
indices.push(base, base + 1, base + 3, base, base + 3, base + 2);
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
positions,
|
|
51
|
+
indices
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Draws a dashed boundary ring as a flat band mesh on the ground plane.
|
|
57
|
+
*
|
|
58
|
+
* Rendered as a thin triangle band whose width is a fraction of the radius (`widthRatio`).
|
|
59
|
+
* center / radius / groundPlane are set imperatively by the BoundaryRing component (see BoundaryRing.tsx for why
|
|
60
|
+
* they are not reactive Script props), which calls `rebuild()` after updating them.
|
|
61
|
+
*/
|
|
62
|
+
export class BoundaryRingScript extends Script {
|
|
63
|
+
static scriptName = 'boundaryRing';
|
|
64
|
+
center = new Vec3();
|
|
65
|
+
radius = 0;
|
|
66
|
+
groundPlane = [];
|
|
67
|
+
color = new Color(1, 1, 1);
|
|
68
|
+
dashCount = 64;
|
|
69
|
+
dashRatio = 0.5;
|
|
70
|
+
/** Band width as a fraction of the radius. */
|
|
71
|
+
widthRatio = 0.05;
|
|
72
|
+
initialize() {
|
|
73
|
+
const material = new StandardMaterial();
|
|
74
|
+
material.useLighting = false;
|
|
75
|
+
material.emissive = this.color;
|
|
76
|
+
material.cull = CULLFACE_NONE;
|
|
77
|
+
material.update();
|
|
78
|
+
this._material = material;
|
|
79
|
+
this.rebuild();
|
|
80
|
+
}
|
|
81
|
+
rebuild() {
|
|
82
|
+
this._clearMesh();
|
|
83
|
+
const geometry = boundaryRingMesh({
|
|
84
|
+
center: this.center,
|
|
85
|
+
radius: this.radius,
|
|
86
|
+
groundPlane: this.groundPlane,
|
|
87
|
+
width: this.radius * this.widthRatio,
|
|
88
|
+
dashCount: this.dashCount,
|
|
89
|
+
dashRatio: this.dashRatio
|
|
90
|
+
});
|
|
91
|
+
if (geometry.positions.length === 0 || !this._material) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const mesh = new Mesh(this.app.graphicsDevice);
|
|
95
|
+
mesh.setPositions(geometry.positions);
|
|
96
|
+
mesh.setIndices(geometry.indices);
|
|
97
|
+
mesh.update();
|
|
98
|
+
this._mesh = mesh;
|
|
99
|
+
const meshInstance = new MeshInstance(mesh, this._material);
|
|
100
|
+
if (this.entity.render) {
|
|
101
|
+
this.entity.render.meshInstances = [meshInstance];
|
|
102
|
+
} else {
|
|
103
|
+
// Render on the Immediate layer (drawn after the World layer where the splats render) so
|
|
104
|
+
// the ring is not composited behind them.
|
|
105
|
+
this.entity.addComponent('render', {
|
|
106
|
+
meshInstances: [meshInstance],
|
|
107
|
+
layers: [LAYERID_IMMEDIATE]
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
_clearMesh() {
|
|
112
|
+
if (this.entity.render) {
|
|
113
|
+
this.entity.render.meshInstances = [];
|
|
114
|
+
}
|
|
115
|
+
if (this._mesh) {
|
|
116
|
+
this._mesh.destroy();
|
|
117
|
+
this._mesh = undefined;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
destroy() {
|
|
121
|
+
this._clearMesh();
|
|
122
|
+
this._material?.destroy();
|
|
123
|
+
this._material = undefined;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { Vec3 } from 'playcanvas';
|
|
2
|
+
import { boundaryRingMesh } from './boundary-ring';
|
|
3
|
+
const FLAT = [new Vec3(0, 0, 0), new Vec3(1, 0, 0), new Vec3(0, 0, 1)];
|
|
4
|
+
const toVertices = positions => {
|
|
5
|
+
const out = [];
|
|
6
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
7
|
+
out.push([positions[i], positions[i + 1], positions[i + 2]]);
|
|
8
|
+
}
|
|
9
|
+
return out;
|
|
10
|
+
};
|
|
11
|
+
describe('boundaryRingMesh', () => {
|
|
12
|
+
it('produces four vertices and six indices per dash', () => {
|
|
13
|
+
const geom = boundaryRingMesh({
|
|
14
|
+
center: new Vec3(0, 0, 0),
|
|
15
|
+
radius: 5,
|
|
16
|
+
width: 0.1,
|
|
17
|
+
groundPlane: FLAT,
|
|
18
|
+
dashCount: 16
|
|
19
|
+
});
|
|
20
|
+
expect(geom.positions).toHaveLength(16 * 4 * 3);
|
|
21
|
+
expect(geom.indices).toHaveLength(16 * 6);
|
|
22
|
+
});
|
|
23
|
+
it('places every vertex at the inner or outer radius from center in XZ', () => {
|
|
24
|
+
const center = new Vec3(2, 0, -3);
|
|
25
|
+
const radius = 4;
|
|
26
|
+
const width = 0.2;
|
|
27
|
+
const inner = radius - width / 2;
|
|
28
|
+
const outer = radius + width / 2;
|
|
29
|
+
const geom = boundaryRingMesh({
|
|
30
|
+
center,
|
|
31
|
+
radius,
|
|
32
|
+
width,
|
|
33
|
+
groundPlane: FLAT,
|
|
34
|
+
dashCount: 32
|
|
35
|
+
});
|
|
36
|
+
for (const [x,, z] of toVertices(geom.positions)) {
|
|
37
|
+
const d = Math.sqrt((x - center.x) ** 2 + (z - center.z) ** 2);
|
|
38
|
+
const atInner = Math.abs(d - inner) < 1e-6;
|
|
39
|
+
const atOuter = Math.abs(d - outer) < 1e-6;
|
|
40
|
+
expect(atInner || atOuter).toBe(true);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
it('lifts every vertex onto a tilted ground plane (y = z)', () => {
|
|
44
|
+
const tilted = [new Vec3(0, 0, 0), new Vec3(1, 0, 0), new Vec3(0, 1, 1)];
|
|
45
|
+
const geom = boundaryRingMesh({
|
|
46
|
+
center: new Vec3(0, 0, 0),
|
|
47
|
+
radius: 3,
|
|
48
|
+
width: 0.1,
|
|
49
|
+
groundPlane: tilted,
|
|
50
|
+
dashCount: 8
|
|
51
|
+
});
|
|
52
|
+
for (const [, y, z] of toVertices(geom.positions)) {
|
|
53
|
+
expect(y).toBeCloseTo(z);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
it('references only in-range vertices from the index buffer', () => {
|
|
57
|
+
const geom = boundaryRingMesh({
|
|
58
|
+
center: new Vec3(0, 0, 0),
|
|
59
|
+
radius: 5,
|
|
60
|
+
width: 0.1,
|
|
61
|
+
groundPlane: FLAT,
|
|
62
|
+
dashCount: 8
|
|
63
|
+
});
|
|
64
|
+
const vertexCount = geom.positions.length / 3;
|
|
65
|
+
for (const idx of geom.indices) {
|
|
66
|
+
expect(idx).toBeGreaterThanOrEqual(0);
|
|
67
|
+
expect(idx).toBeLessThan(vertexCount);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
it('returns empty geometry for a non-positive radius', () => {
|
|
71
|
+
const geom = boundaryRingMesh({
|
|
72
|
+
center: new Vec3(0, 0, 0),
|
|
73
|
+
radius: 0,
|
|
74
|
+
width: 0.1,
|
|
75
|
+
groundPlane: FLAT
|
|
76
|
+
});
|
|
77
|
+
expect(geom.positions).toHaveLength(0);
|
|
78
|
+
expect(geom.indices).toHaveLength(0);
|
|
79
|
+
});
|
|
80
|
+
it('returns empty geometry for a non-positive width', () => {
|
|
81
|
+
const geom = boundaryRingMesh({
|
|
82
|
+
center: new Vec3(0, 0, 0),
|
|
83
|
+
radius: 5,
|
|
84
|
+
width: 0,
|
|
85
|
+
groundPlane: FLAT
|
|
86
|
+
});
|
|
87
|
+
expect(geom.positions).toHaveLength(0);
|
|
88
|
+
});
|
|
89
|
+
it('returns empty geometry for a non-positive dashCount', () => {
|
|
90
|
+
const geom = boundaryRingMesh({
|
|
91
|
+
center: new Vec3(0, 0, 0),
|
|
92
|
+
radius: 5,
|
|
93
|
+
width: 0.1,
|
|
94
|
+
groundPlane: FLAT,
|
|
95
|
+
dashCount: 0
|
|
96
|
+
});
|
|
97
|
+
expect(geom.positions).toHaveLength(0);
|
|
98
|
+
});
|
|
99
|
+
it('returns empty geometry for an invalid ground plane', () => {
|
|
100
|
+
const collinear = [new Vec3(0, 0, 0), new Vec3(1, 0, 0), new Vec3(2, 0, 0)];
|
|
101
|
+
const geom = boundaryRingMesh({
|
|
102
|
+
center: new Vec3(0, 0, 0),
|
|
103
|
+
radius: 5,
|
|
104
|
+
width: 0.1,
|
|
105
|
+
groundPlane: collinear
|
|
106
|
+
});
|
|
107
|
+
expect(geom.positions).toHaveLength(0);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Vec3 } from 'playcanvas';
|
|
2
|
+
export type GroundPlane = {
|
|
3
|
+
normal: Vec3;
|
|
4
|
+
point: Vec3;
|
|
5
|
+
};
|
|
6
|
+
/** Compute Y on the plane defined by `normal` passing through `point` at world XZ coords (x, z). */
|
|
7
|
+
export declare const yOnPlane: (x: number, z: number, normal: Vec3, point: Vec3, fallback: number) => number;
|
|
8
|
+
/**
|
|
9
|
+
* Derive a ground plane from 3 world-space points.
|
|
10
|
+
* Returns a normalized normal (flipped so normal.y >= 0) and a point on the plane,
|
|
11
|
+
* or null when fewer than 3 points are supplied or they are degenerate/collinear.
|
|
12
|
+
*/
|
|
13
|
+
export declare const computeGroundPlane: (points: Vec3[]) => GroundPlane | null;
|
|
14
|
+
//# sourceMappingURL=groundPlane.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"groundPlane.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/groundPlane.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAElC,MAAM,MAAM,WAAW,GAAG;IAAE,MAAM,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,IAAI,CAAA;CAAE,CAAC;AAExD,oGAAoG;AACpG,eAAO,MAAM,QAAQ,GAAI,GAAG,MAAM,EAAE,GAAG,MAAM,EAAE,QAAQ,IAAI,EAAE,OAAO,IAAI,EAAE,UAAU,MAAM,KAAG,MAM5F,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAAI,QAAQ,IAAI,EAAE,KAAG,WAAW,GAAG,IA8BjE,CAAC"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Vec3 } from 'playcanvas';
|
|
2
|
+
/** Compute Y on the plane defined by `normal` passing through `point` at world XZ coords (x, z). */
|
|
3
|
+
export const yOnPlane = (x, z, normal, point, fallback) => {
|
|
4
|
+
if (Math.abs(normal.y) < 1e-6) {
|
|
5
|
+
return fallback;
|
|
6
|
+
}
|
|
7
|
+
return point.y - (normal.x * (x - point.x) + normal.z * (z - point.z)) / normal.y;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Derive a ground plane from 3 world-space points.
|
|
12
|
+
* Returns a normalized normal (flipped so normal.y >= 0) and a point on the plane,
|
|
13
|
+
* or null when fewer than 3 points are supplied or they are degenerate/collinear.
|
|
14
|
+
*/
|
|
15
|
+
export const computeGroundPlane = points => {
|
|
16
|
+
if (points.length < 3) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
const p0 = points[0];
|
|
20
|
+
const p1 = points[1];
|
|
21
|
+
const p2 = points[2];
|
|
22
|
+
const v1x = p1.x - p0.x;
|
|
23
|
+
const v1y = p1.y - p0.y;
|
|
24
|
+
const v1z = p1.z - p0.z;
|
|
25
|
+
const v2x = p2.x - p0.x;
|
|
26
|
+
const v2y = p2.y - p0.y;
|
|
27
|
+
const v2z = p2.z - p0.z;
|
|
28
|
+
let nx = v1y * v2z - v1z * v2y;
|
|
29
|
+
let ny = v1z * v2x - v1x * v2z;
|
|
30
|
+
let nz = v1x * v2y - v1y * v2x;
|
|
31
|
+
const len = Math.sqrt(nx * nx + ny * ny + nz * nz);
|
|
32
|
+
if (len < 1e-10) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
nx /= len;
|
|
36
|
+
ny /= len;
|
|
37
|
+
nz /= len;
|
|
38
|
+
if (ny < 0) {
|
|
39
|
+
nx = -nx;
|
|
40
|
+
ny = -ny;
|
|
41
|
+
nz = -nz;
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
normal: new Vec3(nx, ny, nz),
|
|
45
|
+
point: new Vec3(p0.x, p0.y, p0.z)
|
|
46
|
+
};
|
|
47
|
+
};
|