@navaramap/three-plugins 0.0.8 → 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.
@@ -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: headingRad });
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 radians — 0 = north, increasing clockwise. */
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 radians (0 = north, increasing clockwise). If omitted, the
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 (radians) {@link alignToSlope} may apply, so ground far
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 `Math.PI / 4` (45°)
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 radians for **TPV**. `0` keeps the camera
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 radians for **FPV**. `0` looks straight ahead
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
- /** radians */
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 radians, as the geodetic helpers return it. */
329
- type LatLngRadians = { lat: number; lng: number };
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: Math.PI / 4,
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: Math.PI * 1.3,
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
- this.modelHeading = this.config.startHeading;
532
- this.cameraHeading = this.config.startHeading;
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: degreeToRadian(startLat),
583
- lng: degreeToRadian(startLng),
647
+ lat: startLat,
648
+ lng: startLng,
584
649
  height: startHeight,
585
650
  });
586
651
 
587
- this.modelHeading = startHeading;
588
- this.cameraHeading = startHeading;
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, startHeading),
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 headingRad =
693
- options.heading != null ? options.heading : this.cameraHeading;
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: degreeToRadian(lat), lng: degreeToRadian(lng) };
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: headingRad,
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 radians (0 = north,
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(radians: number): void {
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: radians };
885
+ this.state = { ...this.state, heading: degrees };
819
886
  this.notify();
820
887
  }
821
888
 
822
- /** Current character heading in radians. */
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 radians (0 = behind at eye level,
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(radians: number): void {
833
- this.config.cameraPitch = radians;
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 radians. */
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 radians (0 = horizontal, positive
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(radians: number): void {
855
- this.config.fpvPitch = radians;
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 radians. */
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(startLat, startLng, startHeight, startHeading);
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(degreeToRadian(at.lat), degreeToRadian(at.lng));
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 radians, or `undefined` if not loaded. */
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
- * radians. `deltaTime` drives the absorption of terrain data changes — pass
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: LatLngRadians,
1237
- from: LatLngRadians,
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: LatLngRadians,
1277
- from: LatLngRadians,
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 radians, `alt` in meters).
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
- * radians, fitted to a ring of probes, or `undefined` while any of them is
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: radianToDegree(lle.lat), lng: radianToDegree(lle.lng) };
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: degreeToRadian(this.state.lat),
1530
- lng: degreeToRadian(this.state.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
- const height = this.resolveGroundHeight(
1566
- advancedLLE,
1567
- currentLLE,
1568
- flownHeight,
1569
- deltaTime,
1570
- );
1571
- const nextLat = radianToDegree(advancedLLE.lat);
1572
- const nextLng = radianToDegree(advancedLLE.lng);
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,