@navaramap/three-plugins 0.0.7 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +5 -5
- package/dist/index.d.ts +57 -20
- package/dist/index.js +94 -77
- package/dist/index.umd.cjs +1 -1
- package/package.json +5 -5
- package/src/OverlayPlugin.ts +5 -10
- package/src/PersonViewPlugin.test.ts +144 -17
- package/src/PersonViewPlugin.ts +142 -71
package/src/PersonViewPlugin.ts
CHANGED
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
* console.log(state.lat, state.lng, state.alt, state.mode);
|
|
44
44
|
* });
|
|
45
45
|
*
|
|
46
|
-
* personView.teleport({ lng, lat, alt, heading:
|
|
46
|
+
* personView.teleport({ lng, lat, alt, heading: 120 });
|
|
47
47
|
* personView.toggleViewMode();
|
|
48
48
|
*
|
|
49
49
|
* unsub();
|
|
@@ -58,6 +58,7 @@ import ThreeView, {
|
|
|
58
58
|
eastNorthUpToFixedFrame,
|
|
59
59
|
degreeToRadian,
|
|
60
60
|
radianToDegree,
|
|
61
|
+
type SourceRef,
|
|
61
62
|
type ViewContext,
|
|
62
63
|
} from "@navaramap/three";
|
|
63
64
|
import type { GLTFModelDesc } from "@navaramap/three-default-descs";
|
|
@@ -72,7 +73,7 @@ export type PersonViewState = {
|
|
|
72
73
|
lng: number;
|
|
73
74
|
lat: number;
|
|
74
75
|
alt: number;
|
|
75
|
-
/** Heading in
|
|
76
|
+
/** Heading in degrees — 0 = north, increasing clockwise. */
|
|
76
77
|
heading: number;
|
|
77
78
|
speed: number;
|
|
78
79
|
/** Current animation clip name, or null when no character is configured. */
|
|
@@ -89,7 +90,7 @@ export type TeleportOptions = {
|
|
|
89
90
|
/** Altitude in meters. */
|
|
90
91
|
alt: number;
|
|
91
92
|
/**
|
|
92
|
-
* Heading in
|
|
93
|
+
* Heading in degrees (0 = north, increasing clockwise). If omitted, the
|
|
93
94
|
* current camera heading is kept. (Use `setHeading` to rotate in place
|
|
94
95
|
* without moving, and `setCameraPitch` / `setFpvPitch` for camera pitch.)
|
|
95
96
|
*/
|
|
@@ -170,9 +171,9 @@ export type CollisionConfig = {
|
|
|
170
171
|
*/
|
|
171
172
|
slopeSampleDistance?: number;
|
|
172
173
|
/**
|
|
173
|
-
* Largest tilt (
|
|
174
|
+
* Largest tilt (degrees) {@link alignToSlope} may apply, so ground far
|
|
174
175
|
* steeper than anything walkable does not lay the character flat against it.
|
|
175
|
-
* @defaultValue `
|
|
176
|
+
* @defaultValue `45`
|
|
176
177
|
*/
|
|
177
178
|
maxSlopeTilt?: number;
|
|
178
179
|
/**
|
|
@@ -266,7 +267,7 @@ export type PersonViewConfig = {
|
|
|
266
267
|
maxAlt?: number;
|
|
267
268
|
cameraDistance?: number;
|
|
268
269
|
/**
|
|
269
|
-
* Downward camera pitch in
|
|
270
|
+
* Downward camera pitch in degrees for **TPV**. `0` keeps the camera
|
|
270
271
|
* behind the model at eye level; positive values orbit the camera up and
|
|
271
272
|
* over so it looks down at the model while keeping it centered. Has no
|
|
272
273
|
* effect while the free camera is active (Alt-hold or `allowCameraControl`),
|
|
@@ -283,7 +284,7 @@ export type PersonViewConfig = {
|
|
|
283
284
|
*/
|
|
284
285
|
fpvHeightOffset?: number;
|
|
285
286
|
/**
|
|
286
|
-
* Downward camera pitch in
|
|
287
|
+
* Downward camera pitch in degrees for **FPV**. `0` looks straight ahead
|
|
287
288
|
* (horizontal); positive values tilt the view down in place without moving
|
|
288
289
|
* the eye. Has no effect while the free camera is active.
|
|
289
290
|
*/
|
|
@@ -292,7 +293,7 @@ export type PersonViewConfig = {
|
|
|
292
293
|
startLat?: number;
|
|
293
294
|
startLng?: number;
|
|
294
295
|
startHeight?: number;
|
|
295
|
-
/**
|
|
296
|
+
/** Heading in degrees (0 = north, increasing clockwise). */
|
|
296
297
|
startHeading?: number;
|
|
297
298
|
|
|
298
299
|
keys?: KeyBindings;
|
|
@@ -325,8 +326,8 @@ type PersonViewDefaults = Required<
|
|
|
325
326
|
|
|
326
327
|
type ResolvedCollision = Required<CollisionConfig>;
|
|
327
328
|
|
|
328
|
-
/** A geodetic position in
|
|
329
|
-
type
|
|
329
|
+
/** A geodetic position in degrees, as the geodetic helpers return it. */
|
|
330
|
+
type LatLngDegrees = { lat: number; lng: number };
|
|
330
331
|
|
|
331
332
|
// Tuned so that `collision: { mode: "ground" }` alone walks terrain well: the
|
|
332
333
|
// slope footprint is wide enough for the triangle spacing real terrain meshes
|
|
@@ -336,7 +337,7 @@ const DEFAULT_COLLISION: ResolvedCollision = {
|
|
|
336
337
|
groundOffset: 0,
|
|
337
338
|
alignToSlope: true,
|
|
338
339
|
slopeSampleDistance: 4,
|
|
339
|
-
maxSlopeTilt:
|
|
340
|
+
maxSlopeTilt: 45,
|
|
340
341
|
cameraSlopeFollow: 1,
|
|
341
342
|
};
|
|
342
343
|
|
|
@@ -405,7 +406,7 @@ const DEFAULTS: PersonViewDefaults = {
|
|
|
405
406
|
startLat: 35.6812,
|
|
406
407
|
startLng: 139.7671,
|
|
407
408
|
startHeight: 500,
|
|
408
|
-
startHeading:
|
|
409
|
+
startHeading: 234,
|
|
409
410
|
};
|
|
410
411
|
|
|
411
412
|
const DEFAULT_MODEL_SCALE = 3;
|
|
@@ -456,6 +457,13 @@ export class PersonViewPlugin extends Plugin<View, ViewContext> {
|
|
|
456
457
|
private animId: number | null = null;
|
|
457
458
|
/** Whether the character has been placed, so `start()` resumes from here. */
|
|
458
459
|
private placed = false;
|
|
460
|
+
/**
|
|
461
|
+
* Height resolved by {@link resolveStartHeight}, held against the terrain
|
|
462
|
+
* collision until the first movement input. The coarse tiles that stream in
|
|
463
|
+
* right after load would otherwise pull the character off the resolved
|
|
464
|
+
* height and onto their own, tens of meters away.
|
|
465
|
+
*/
|
|
466
|
+
private pinnedStartHeight?: number;
|
|
459
467
|
private lastTime = 0;
|
|
460
468
|
|
|
461
469
|
private state!: PersonViewState;
|
|
@@ -472,7 +480,9 @@ export class PersonViewPlugin extends Plugin<View, ViewContext> {
|
|
|
472
480
|
// angle instead of snapping back the moment Alt is released.
|
|
473
481
|
private orbitLatched = false;
|
|
474
482
|
private currentAnimState: string | null;
|
|
483
|
+
/** Character heading in radians — public heading APIs convert at the boundary. */
|
|
475
484
|
private modelHeading: number;
|
|
485
|
+
/** Camera heading in radians. */
|
|
476
486
|
private cameraHeading: number;
|
|
477
487
|
private viewMode: ViewMode;
|
|
478
488
|
|
|
@@ -528,8 +538,10 @@ export class PersonViewPlugin extends Plugin<View, ViewContext> {
|
|
|
528
538
|
this.keys = { ...DEFAULT_KEYS, ...keys };
|
|
529
539
|
this.keyToAction = this.buildKeyMap(this.keys);
|
|
530
540
|
|
|
531
|
-
|
|
532
|
-
|
|
541
|
+
// MathUtils, not the WASM-backed degreeToRadian: the constructor runs
|
|
542
|
+
// before `view.init()` initializes the API WASM module.
|
|
543
|
+
this.modelHeading = MathUtils.degToRad(this.config.startHeading);
|
|
544
|
+
this.cameraHeading = this.modelHeading;
|
|
533
545
|
this.viewMode = this.config.initialView;
|
|
534
546
|
this.currentAnimState = this.character
|
|
535
547
|
? this.character.animation.idleClip
|
|
@@ -551,6 +563,59 @@ export class PersonViewPlugin extends Plugin<View, ViewContext> {
|
|
|
551
563
|
};
|
|
552
564
|
}
|
|
553
565
|
|
|
566
|
+
/**
|
|
567
|
+
* Pin `startHeight` to the terrain surface at the start position, sampled
|
|
568
|
+
* from the source's **most detailed** data — so `start()` places the
|
|
569
|
+
* character on the ground without a hand-measured `startHeight`, and
|
|
570
|
+
* without the initial settle once finer terrain tiles stream in. A one-shot
|
|
571
|
+
* sample fits here because the start position is fixed; the per-frame
|
|
572
|
+
* collision keeps following the terrain afterwards.
|
|
573
|
+
*
|
|
574
|
+
* The resolved height is **held until the first movement input** (or
|
|
575
|
+
* {@link teleport}): the render-resident tiles right after load are coarse,
|
|
576
|
+
* and letting the collision follow them would pull the character off the
|
|
577
|
+
* resolved height and onto theirs. Movement hands the altitude over to the
|
|
578
|
+
* normal terrain following.
|
|
579
|
+
*
|
|
580
|
+
* Call between `view.init()` (the sampler needs the initialized view) and
|
|
581
|
+
* {@link start}. Once the character has been placed this no longer moves
|
|
582
|
+
* it — use {@link teleport} with the returned height instead.
|
|
583
|
+
*
|
|
584
|
+
* @param source - A registered `quantized-mesh` or `raster-dem` source
|
|
585
|
+
* (handle or id) to sample, always explicit — the same contract as
|
|
586
|
+
* `ThreeView.sampleTerrainMostDetailed`.
|
|
587
|
+
* @returns The resolved start height in meters, including the collision's
|
|
588
|
+
* `groundOffset` (matching what the collision itself enforces), or
|
|
589
|
+
* `undefined` when the source has no height at the start position (the
|
|
590
|
+
* configured `startHeight` is then kept).
|
|
591
|
+
*/
|
|
592
|
+
async resolveStartHeight(source: SourceRef): Promise<number | undefined> {
|
|
593
|
+
const view = this.view;
|
|
594
|
+
if (!view) return undefined;
|
|
595
|
+
|
|
596
|
+
const { startLat, startLng } = this.config;
|
|
597
|
+
const [ground] = await view.sampleTerrainMostDetailed(source, [
|
|
598
|
+
{ lat: startLat, lng: startLng },
|
|
599
|
+
]);
|
|
600
|
+
if (ground?.height === undefined) return undefined;
|
|
601
|
+
|
|
602
|
+
const { mode, groundOffset } = this.collision;
|
|
603
|
+
// The collision keeps `groundOffset` above the surface, so the resolved
|
|
604
|
+
// height must include it or the character starts below its own floor.
|
|
605
|
+
const height = ground.height + (mode === "off" ? 0 : groundOffset);
|
|
606
|
+
this.config.startHeight = height;
|
|
607
|
+
if (!this.placed) {
|
|
608
|
+
// The state was seeded from the config in `init()`, and the camera-only
|
|
609
|
+
// mode keeps reading its altitude from there.
|
|
610
|
+
this.state = { ...this.state, alt: height };
|
|
611
|
+
this.pinnedStartHeight = height;
|
|
612
|
+
// What `resolveGroundHeight` would have reported, so the grounding
|
|
613
|
+
// logic (slope tilt, camera slope follow) keeps working while pinned.
|
|
614
|
+
if (mode !== "off") this.groundSurfaceHeight = height;
|
|
615
|
+
}
|
|
616
|
+
return height;
|
|
617
|
+
}
|
|
618
|
+
|
|
554
619
|
/**
|
|
555
620
|
* Take over the camera and start reading the movement keys. Calling it again
|
|
556
621
|
* after {@link stop} resumes from wherever the character was left, rather
|
|
@@ -579,13 +644,13 @@ export class PersonViewPlugin extends Plugin<View, ViewContext> {
|
|
|
579
644
|
|
|
580
645
|
const { startLat, startLng, startHeight, startHeading } = this.config;
|
|
581
646
|
const startPos = geodeticToVector3({
|
|
582
|
-
lat:
|
|
583
|
-
lng:
|
|
647
|
+
lat: startLat,
|
|
648
|
+
lng: startLng,
|
|
584
649
|
height: startHeight,
|
|
585
650
|
});
|
|
586
651
|
|
|
587
|
-
this.modelHeading = startHeading;
|
|
588
|
-
this.cameraHeading =
|
|
652
|
+
this.modelHeading = degreeToRadian(startHeading);
|
|
653
|
+
this.cameraHeading = this.modelHeading;
|
|
589
654
|
|
|
590
655
|
if (this.character) {
|
|
591
656
|
const {
|
|
@@ -608,7 +673,7 @@ export class PersonViewPlugin extends Plugin<View, ViewContext> {
|
|
|
608
673
|
animationLoop: true,
|
|
609
674
|
animationCrossfadeDuration: animation.crossfadeDuration,
|
|
610
675
|
},
|
|
611
|
-
matrixWorld: this.composeCharacterFrame(startPos,
|
|
676
|
+
matrixWorld: this.composeCharacterFrame(startPos, this.modelHeading),
|
|
612
677
|
rotation: {
|
|
613
678
|
x: modelRotationOffset.x,
|
|
614
679
|
y: modelRotationOffset.y,
|
|
@@ -687,12 +752,17 @@ export class PersonViewPlugin extends Plugin<View, ViewContext> {
|
|
|
687
752
|
/** Instantly move to a new geographic position. */
|
|
688
753
|
teleport(options: TeleportOptions): void {
|
|
689
754
|
if (!this.view) return;
|
|
755
|
+
// The pinned start height belongs to the start position being left.
|
|
756
|
+
this.pinnedStartHeight = undefined;
|
|
690
757
|
|
|
691
758
|
const { lng, lat } = options;
|
|
692
|
-
const
|
|
693
|
-
options.heading != null
|
|
759
|
+
const heading =
|
|
760
|
+
options.heading != null
|
|
761
|
+
? options.heading
|
|
762
|
+
: radianToDegree(this.cameraHeading);
|
|
763
|
+
const headingRad = degreeToRadian(heading);
|
|
694
764
|
// A teleport lands on the terrain outright rather than settling onto it.
|
|
695
|
-
const destination = { lat
|
|
765
|
+
const destination = { lat, lng };
|
|
696
766
|
const alt = this.resolveGroundHeight(
|
|
697
767
|
destination,
|
|
698
768
|
destination,
|
|
@@ -720,7 +790,7 @@ export class PersonViewPlugin extends Plugin<View, ViewContext> {
|
|
|
720
790
|
lng,
|
|
721
791
|
lat,
|
|
722
792
|
alt,
|
|
723
|
-
heading
|
|
793
|
+
heading,
|
|
724
794
|
speed: 0,
|
|
725
795
|
animationState: this.currentAnimState,
|
|
726
796
|
mode: this.viewMode,
|
|
@@ -789,23 +859,20 @@ export class PersonViewPlugin extends Plugin<View, ViewContext> {
|
|
|
789
859
|
}
|
|
790
860
|
|
|
791
861
|
/**
|
|
792
|
-
* Rotate the character to the given heading in
|
|
862
|
+
* Rotate the character to the given heading in degrees (0 = north,
|
|
793
863
|
* increasing clockwise) without changing position. Snaps the chase camera to
|
|
794
864
|
* match; in free-camera mode only the model rotates.
|
|
795
865
|
*/
|
|
796
|
-
setHeading(
|
|
866
|
+
setHeading(degrees: number): void {
|
|
797
867
|
if (!this.view) return;
|
|
798
868
|
|
|
799
869
|
const { lat, lng, alt } = this.state;
|
|
870
|
+
const radians = degreeToRadian(degrees);
|
|
800
871
|
this.modelHeading = radians;
|
|
801
872
|
this.cameraHeading = radians;
|
|
802
873
|
|
|
803
874
|
if (this.handle && this.character) {
|
|
804
|
-
const pos = geodeticToVector3({
|
|
805
|
-
lat: degreeToRadian(lat),
|
|
806
|
-
lng: degreeToRadian(lng),
|
|
807
|
-
height: alt,
|
|
808
|
-
});
|
|
875
|
+
const pos = geodeticToVector3({ lat, lng, height: alt });
|
|
809
876
|
this.handle.update({
|
|
810
877
|
matrixWorld: this.composeCharacterFrame(pos, radians),
|
|
811
878
|
});
|
|
@@ -815,22 +882,22 @@ export class PersonViewPlugin extends Plugin<View, ViewContext> {
|
|
|
815
882
|
this.placeChaseCamera(lat, lng, alt, radians);
|
|
816
883
|
}
|
|
817
884
|
|
|
818
|
-
this.state = { ...this.state, heading:
|
|
885
|
+
this.state = { ...this.state, heading: degrees };
|
|
819
886
|
this.notify();
|
|
820
887
|
}
|
|
821
888
|
|
|
822
|
-
/** Current character heading in
|
|
889
|
+
/** Current character heading in degrees. */
|
|
823
890
|
getHeading(): number {
|
|
824
891
|
return this.state.heading;
|
|
825
892
|
}
|
|
826
893
|
|
|
827
894
|
/**
|
|
828
|
-
* Set the downward TPV camera pitch in
|
|
895
|
+
* Set the downward TPV camera pitch in degrees (0 = behind at eye level,
|
|
829
896
|
* positive orbits up and over the model). Takes effect immediately for the
|
|
830
897
|
* chase / locked camera.
|
|
831
898
|
*/
|
|
832
|
-
setCameraPitch(
|
|
833
|
-
this.config.cameraPitch =
|
|
899
|
+
setCameraPitch(degrees: number): void {
|
|
900
|
+
this.config.cameraPitch = degrees;
|
|
834
901
|
if (!this.isFreeCamera()) {
|
|
835
902
|
this.placeChaseCamera(
|
|
836
903
|
this.state.lat,
|
|
@@ -841,18 +908,18 @@ export class PersonViewPlugin extends Plugin<View, ViewContext> {
|
|
|
841
908
|
}
|
|
842
909
|
}
|
|
843
910
|
|
|
844
|
-
/** Current downward TPV camera pitch in
|
|
911
|
+
/** Current downward TPV camera pitch in degrees. */
|
|
845
912
|
getCameraPitch(): number {
|
|
846
913
|
return this.config.cameraPitch;
|
|
847
914
|
}
|
|
848
915
|
|
|
849
916
|
/**
|
|
850
|
-
* Set the downward FPV camera pitch in
|
|
917
|
+
* Set the downward FPV camera pitch in degrees (0 = horizontal, positive
|
|
851
918
|
* tilts the view down in place). Takes effect immediately for the chase /
|
|
852
919
|
* locked camera.
|
|
853
920
|
*/
|
|
854
|
-
setFpvPitch(
|
|
855
|
-
this.config.fpvPitch =
|
|
921
|
+
setFpvPitch(degrees: number): void {
|
|
922
|
+
this.config.fpvPitch = degrees;
|
|
856
923
|
if (!this.isFreeCamera()) {
|
|
857
924
|
this.placeChaseCamera(
|
|
858
925
|
this.state.lat,
|
|
@@ -863,7 +930,7 @@ export class PersonViewPlugin extends Plugin<View, ViewContext> {
|
|
|
863
930
|
}
|
|
864
931
|
}
|
|
865
932
|
|
|
866
|
-
/** Current downward FPV camera pitch in
|
|
933
|
+
/** Current downward FPV camera pitch in degrees. */
|
|
867
934
|
getFpvPitch(): number {
|
|
868
935
|
return this.config.fpvPitch;
|
|
869
936
|
}
|
|
@@ -937,6 +1004,7 @@ export class PersonViewPlugin extends Plugin<View, ViewContext> {
|
|
|
937
1004
|
}
|
|
938
1005
|
this.modelRef = null;
|
|
939
1006
|
this.placed = false;
|
|
1007
|
+
this.pinnedStartHeight = undefined;
|
|
940
1008
|
this.listeners.clear();
|
|
941
1009
|
this.actionListeners.clear();
|
|
942
1010
|
this.view = undefined;
|
|
@@ -1011,7 +1079,12 @@ export class PersonViewPlugin extends Plugin<View, ViewContext> {
|
|
|
1011
1079
|
|
|
1012
1080
|
private applyInitialCamera(): void {
|
|
1013
1081
|
const { startLat, startLng, startHeight, startHeading } = this.config;
|
|
1014
|
-
this.placeChaseCamera(
|
|
1082
|
+
this.placeChaseCamera(
|
|
1083
|
+
startLat,
|
|
1084
|
+
startLng,
|
|
1085
|
+
startHeight,
|
|
1086
|
+
degreeToRadian(startHeading),
|
|
1087
|
+
);
|
|
1015
1088
|
}
|
|
1016
1089
|
|
|
1017
1090
|
/**
|
|
@@ -1046,7 +1119,7 @@ export class PersonViewPlugin extends Plugin<View, ViewContext> {
|
|
|
1046
1119
|
const backDistance = FPV_LOOK_AHEAD_DISTANCE;
|
|
1047
1120
|
const lookAheadDistance = fpvForwardOffset + FPV_LOOK_AHEAD_DISTANCE;
|
|
1048
1121
|
const pitch = this.followSlopeWithPitch(
|
|
1049
|
-
fpvPitch,
|
|
1122
|
+
degreeToRadian(fpvPitch),
|
|
1050
1123
|
lat,
|
|
1051
1124
|
lng,
|
|
1052
1125
|
alt,
|
|
@@ -1074,7 +1147,7 @@ export class PersonViewPlugin extends Plugin<View, ViewContext> {
|
|
|
1074
1147
|
// its place in frame. The eye sits at `cameraDistance`, pulled back by
|
|
1075
1148
|
// `cos(pitch)` horizontally and up by `sin(pitch)`.
|
|
1076
1149
|
const pitch = this.followSlopeWithPitch(
|
|
1077
|
-
this.config.cameraPitch,
|
|
1150
|
+
degreeToRadian(this.config.cameraPitch),
|
|
1078
1151
|
lat,
|
|
1079
1152
|
lng,
|
|
1080
1153
|
alt,
|
|
@@ -1162,7 +1235,7 @@ export class PersonViewPlugin extends Plugin<View, ViewContext> {
|
|
|
1162
1235
|
meters: number,
|
|
1163
1236
|
): number | undefined {
|
|
1164
1237
|
const at = this.advanceLatLng(lat, lng, heading, meters);
|
|
1165
|
-
return this.sampleTerrain(
|
|
1238
|
+
return this.sampleTerrain(at.lat, at.lng);
|
|
1166
1239
|
}
|
|
1167
1240
|
|
|
1168
1241
|
/**
|
|
@@ -1194,7 +1267,7 @@ export class PersonViewPlugin extends Plugin<View, ViewContext> {
|
|
|
1194
1267
|
if (gradEast !== 0 || gradNorth !== 0) {
|
|
1195
1268
|
const sin = Math.sin(heading);
|
|
1196
1269
|
const cos = Math.cos(heading);
|
|
1197
|
-
const limit = this.collision.maxSlopeTilt;
|
|
1270
|
+
const limit = degreeToRadian(this.collision.maxSlopeTilt);
|
|
1198
1271
|
// Rise per meter along the character's own axes: forward is
|
|
1199
1272
|
// (sin, cos) in ENU and right is (cos, -sin).
|
|
1200
1273
|
const pitch = MathUtils.clamp(
|
|
@@ -1222,19 +1295,19 @@ export class PersonViewPlugin extends Plugin<View, ViewContext> {
|
|
|
1222
1295
|
return alt + this.config.fpvHeightOffset;
|
|
1223
1296
|
}
|
|
1224
1297
|
|
|
1225
|
-
/** Terrain height at a position in
|
|
1298
|
+
/** Terrain height at a position in degrees, or `undefined` if not loaded. */
|
|
1226
1299
|
private sampleTerrain(lat: number, lng: number): number | undefined {
|
|
1227
1300
|
return this.view?.sampleTerrainHeight({ lat, lng });
|
|
1228
1301
|
}
|
|
1229
1302
|
|
|
1230
1303
|
/**
|
|
1231
1304
|
* Apply terrain collision to a candidate altitude (meters). Positions are in
|
|
1232
|
-
*
|
|
1305
|
+
* degrees. `deltaTime` drives the absorption of terrain data changes — pass
|
|
1233
1306
|
* `0` to land on the terrain the sampler reports right now.
|
|
1234
1307
|
*/
|
|
1235
1308
|
private resolveGroundHeight(
|
|
1236
|
-
at:
|
|
1237
|
-
from:
|
|
1309
|
+
at: LatLngDegrees,
|
|
1310
|
+
from: LatLngDegrees,
|
|
1238
1311
|
height: number,
|
|
1239
1312
|
deltaTime: number,
|
|
1240
1313
|
): number {
|
|
@@ -1273,8 +1346,8 @@ export class PersonViewPlugin extends Plugin<View, ViewContext> {
|
|
|
1273
1346
|
* changed by is data, and the rest is the character having moved.
|
|
1274
1347
|
*/
|
|
1275
1348
|
private absorbGroundDataShift(
|
|
1276
|
-
at:
|
|
1277
|
-
from:
|
|
1349
|
+
at: LatLngDegrees,
|
|
1350
|
+
from: LatLngDegrees,
|
|
1278
1351
|
terrain: number,
|
|
1279
1352
|
height: number,
|
|
1280
1353
|
deltaTime: number,
|
|
@@ -1324,7 +1397,7 @@ export class PersonViewPlugin extends Plugin<View, ViewContext> {
|
|
|
1324
1397
|
|
|
1325
1398
|
/**
|
|
1326
1399
|
* Refresh the terrain gradient the slope tilt is built from, by probing a
|
|
1327
|
-
* ring of points around the character (position in
|
|
1400
|
+
* ring of points around the character (position in degrees, `alt` in meters).
|
|
1328
1401
|
* `deltaTime` drives the smoothing — pass `0` to snap.
|
|
1329
1402
|
*/
|
|
1330
1403
|
private updateSlopeTilt(
|
|
@@ -1358,7 +1431,7 @@ export class PersonViewPlugin extends Plugin<View, ViewContext> {
|
|
|
1358
1431
|
|
|
1359
1432
|
/**
|
|
1360
1433
|
* Terrain gradient (rise per meter, east and north) under a point given in
|
|
1361
|
-
*
|
|
1434
|
+
* degrees, fitted to a ring of probes, or `undefined` while any of them is
|
|
1362
1435
|
* over terrain that has not loaded. The probes sit a few meters apart, so
|
|
1363
1436
|
* they share one ENU frame rather than each re-deriving its own.
|
|
1364
1437
|
*/
|
|
@@ -1399,11 +1472,7 @@ export class PersonViewPlugin extends Plugin<View, ViewContext> {
|
|
|
1399
1472
|
heading: number,
|
|
1400
1473
|
meters: number,
|
|
1401
1474
|
): { lat: number; lng: number } {
|
|
1402
|
-
const pos = geodeticToVector3({
|
|
1403
|
-
lat: degreeToRadian(lat),
|
|
1404
|
-
lng: degreeToRadian(lng),
|
|
1405
|
-
height: 0,
|
|
1406
|
-
});
|
|
1475
|
+
const pos = geodeticToVector3({ lat, lng, height: 0 });
|
|
1407
1476
|
const enu: Matrix4 = eastNorthUpToFixedFrame(pos);
|
|
1408
1477
|
this._east.setFromMatrixColumn(enu, 0).normalize();
|
|
1409
1478
|
this._north.setFromMatrixColumn(enu, 1).normalize();
|
|
@@ -1413,7 +1482,7 @@ export class PersonViewPlugin extends Plugin<View, ViewContext> {
|
|
|
1413
1482
|
.addScaledVector(this._north, Math.cos(heading));
|
|
1414
1483
|
pos.addScaledVector(this._worldForward, meters);
|
|
1415
1484
|
const lle = vector3ToGeodetic(pos);
|
|
1416
|
-
return { lat:
|
|
1485
|
+
return { lat: lle.lat, lng: lle.lng };
|
|
1417
1486
|
}
|
|
1418
1487
|
|
|
1419
1488
|
private notify(): void {
|
|
@@ -1459,8 +1528,10 @@ export class PersonViewPlugin extends Plugin<View, ViewContext> {
|
|
|
1459
1528
|
}
|
|
1460
1529
|
if (this.isMovementAction(action)) {
|
|
1461
1530
|
// A new movement keypress releases the latched orbit so the camera
|
|
1462
|
-
// snaps back to chase on the next tick
|
|
1531
|
+
// snaps back to chase on the next tick, and hands a pinned start
|
|
1532
|
+
// height over to the terrain collision.
|
|
1463
1533
|
this.orbitLatched = false;
|
|
1534
|
+
this.pinnedStartHeight = undefined;
|
|
1464
1535
|
}
|
|
1465
1536
|
this.heldActions.add(action);
|
|
1466
1537
|
}
|
|
@@ -1526,8 +1597,8 @@ export class PersonViewPlugin extends Plugin<View, ViewContext> {
|
|
|
1526
1597
|
} else {
|
|
1527
1598
|
curPos.copy(
|
|
1528
1599
|
geodeticToVector3({
|
|
1529
|
-
lat:
|
|
1530
|
-
lng:
|
|
1600
|
+
lat: this.state.lat,
|
|
1601
|
+
lng: this.state.lng,
|
|
1531
1602
|
height: this.state.alt,
|
|
1532
1603
|
}),
|
|
1533
1604
|
);
|
|
@@ -1562,14 +1633,14 @@ export class PersonViewPlugin extends Plugin<View, ViewContext> {
|
|
|
1562
1633
|
minAlt,
|
|
1563
1634
|
maxAlt,
|
|
1564
1635
|
);
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
const nextLat =
|
|
1572
|
-
const nextLng =
|
|
1636
|
+
// While the resolved start height is pinned, the collision does not get
|
|
1637
|
+
// to re-sample: the coarse tiles streaming in right after load would drag
|
|
1638
|
+
// the character onto their own height.
|
|
1639
|
+
const height =
|
|
1640
|
+
this.pinnedStartHeight ??
|
|
1641
|
+
this.resolveGroundHeight(advancedLLE, currentLLE, flownHeight, deltaTime);
|
|
1642
|
+
const nextLat = advancedLLE.lat;
|
|
1643
|
+
const nextLng = advancedLLE.lng;
|
|
1573
1644
|
const nextAlt = height;
|
|
1574
1645
|
|
|
1575
1646
|
this.updateSlopeTilt(advancedLLE.lat, advancedLLE.lng, height, deltaTime);
|
|
@@ -1652,7 +1723,7 @@ export class PersonViewPlugin extends Plugin<View, ViewContext> {
|
|
|
1652
1723
|
lng: nextLng,
|
|
1653
1724
|
lat: nextLat,
|
|
1654
1725
|
alt: nextAlt,
|
|
1655
|
-
heading: this.modelHeading,
|
|
1726
|
+
heading: radianToDegree(this.modelHeading),
|
|
1656
1727
|
speed: moveSpeed * dashMultiplier,
|
|
1657
1728
|
animationState: nextAnimState,
|
|
1658
1729
|
mode: this.viewMode,
|