feat(06-06): the D-10 locked-follow guidance camera + the context-aware BackButton resolver (GREEN)

- guidance/camera.ts: GUIDANCE_CAMERA_PADDING {320,64,64,64} / ZOOM 16 /
  EASE_MS 400 / PITCH 35; easeToSnapped rides the follow controller (ONE
  easing discipline) and never passes bearing — north-up, recorded call;
  onRerouteApplied is the documented camera no-op (setData lives in the
  route layer)
- map/follow.ts: follow(point, opts?) pose overrides spread over the
  Phase 3/5 defaults — no-opts calls stay byte-identical (RS13); the
  re-lock announces the mode (the recenter notch's return signal, RS12);
  the originalEvent unlock law untouched
- telegram/backbutton.ts: BackAction resolver ({hidden|close|callback})
  consulted per click + wire.sync() visibility re-apply (visible iff
  actionable); the legacy (hasOverlay, walkBack) shape composes internally
  so Phase 5 call sites and RS7 stay valid; close() only at the true root
This commit is contained in:
araharah-executor
2026-10-24 14:47:50 +08:00
parent 77b62da111
commit 2f12c41060
3 changed files with 169 additions and 17 deletions
+26 -3
View File
@@ -7,6 +7,12 @@
* Micro-Behaviors). A USER gesture (movestart carrying originalEvent) flips
* to free — a programmatic move never does (T-03-16: the fix update must
* never read as the user having panned).
*
* Phase 6 (D-10): follow() takes optional pose overrides so the guidance
* camera rides the SAME easing discipline and the SAME unlock law with the
* guidance pose (padding {320,64,64,64}, pitch 35, zoom 16, 400ms — the
* constants live in ../guidance/camera.ts). Absent fields fall back to the
* Phase 3/5 defaults — existing callers stay byte-identical (RS13).
*/
import type { Map as MapLibreMap } from "maplibre-gl";
import type { Fix } from "../location/types.ts";
@@ -16,6 +22,16 @@ export const RECENTER_DURATION_MS = 400;
export type FollowMode = "following" | "free";
/** Optional camera-pose overrides (D-10's guidance pose rides these).
* Padding accepts MapLibre's number | per-edge form; pitch/zoom are only
* passed when provided (the no-opts call keeps its exact key set). */
export interface FollowOptions {
padding?: number | { top: number; bottom: number; left: number; right: number };
pitch?: number;
zoom?: number;
durationMs?: number;
}
export function createFollowController(map: MapLibreMap, onModeChange: (m: FollowMode) => void) {
let mode: FollowMode = "following";
let lock = false;
@@ -33,10 +49,17 @@ export function createFollowController(map: MapLibreMap, onModeChange: (m: Follo
get mode(): FollowMode {
return mode;
},
follow(fix: Fix): void {
follow(point: Fix | { lat: number; lng: number }, opts: FollowOptions = {}): void {
lock = true;
map.flyTo({ center: [fix.lng, fix.lat], padding: CAMERA_PADDING_PX, duration: RECENTER_DURATION_MS });
set("following");
map.flyTo({
center: [point.lng, point.lat],
padding: opts.padding ?? CAMERA_PADDING_PX,
duration: opts.durationMs ?? RECENTER_DURATION_MS,
...(opts.pitch !== undefined ? { pitch: opts.pitch } : {}),
...(opts.zoom !== undefined ? { zoom: opts.zoom } : {}),
});
mode = "following";
onModeChange(mode); // the re-lock is announced (the recenter notch's return signal)
lock = false;
},
};