@ksp-gonogo/kerbcast-react 1.11.1 → 1.12.0-rc.2

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.
Files changed (49) hide show
  1. package/README.md +71 -0
  2. package/dist/AnchorMarkers.d.ts +128 -0
  3. package/dist/AnchorMarkers.js +162 -0
  4. package/dist/CameraFeed.d.ts +45 -4
  5. package/dist/CameraFeed.js +163 -23
  6. package/dist/CommandBrackets.d.ts +61 -12
  7. package/dist/CommandBrackets.js +106 -53
  8. package/dist/FeedActionBar.d.ts +7 -0
  9. package/dist/FeedActionBar.js +4 -4
  10. package/dist/KerbalFaceFeed.d.ts +1 -1
  11. package/dist/KerbalFaceFeed.js +1 -1
  12. package/dist/RecordingsTray.d.ts +6 -0
  13. package/dist/RecordingsTray.js +17 -1
  14. package/dist/hooks/useCommandDisplayUt.d.ts +16 -3
  15. package/dist/hooks/useCommandDisplayUt.js +57 -16
  16. package/dist/hooks/useKerbcastWaitingForSlot.d.ts +5 -0
  17. package/dist/hooks/useKerbcastWaitingForSlot.js +20 -0
  18. package/dist/hooks/useTargetBracketsPreference.d.ts +3 -2
  19. package/dist/hooks/useTargetBracketsPreference.js +18 -5
  20. package/dist/index.d.ts +4 -1
  21. package/dist/index.js +5 -1
  22. package/dist/menuPositioning.d.ts +8 -2
  23. package/dist/menuPositioning.js +30 -5
  24. package/dist/sharedAnchors.d.ts +3 -0
  25. package/dist/sharedAnchors.js +61 -0
  26. package/dist/videoBox.d.ts +10 -0
  27. package/dist/videoBox.js +40 -0
  28. package/package.json +2 -2
  29. package/src/AnchorMarkers.test.tsx +173 -0
  30. package/src/AnchorMarkers.tsx +341 -0
  31. package/src/CameraFeed.test.tsx +687 -6
  32. package/src/CameraFeed.tsx +240 -27
  33. package/src/CommandBrackets.test.tsx +204 -4
  34. package/src/CommandBrackets.tsx +170 -53
  35. package/src/FeedActionBar.test.ts +22 -0
  36. package/src/FeedActionBar.tsx +11 -4
  37. package/src/KerbalFaceFeed.tsx +1 -1
  38. package/src/RecordingsTray.test.tsx +15 -0
  39. package/src/RecordingsTray.tsx +25 -1
  40. package/src/hooks/useCommandDisplayUt.test.tsx +74 -3
  41. package/src/hooks/useCommandDisplayUt.ts +61 -15
  42. package/src/hooks/useKerbcastWaitingForSlot.ts +25 -0
  43. package/src/hooks/useRecordings.sharedState.test.tsx +6 -3
  44. package/src/hooks/useRecordings.test.tsx +6 -3
  45. package/src/hooks/useTargetBracketsPreference.ts +15 -5
  46. package/src/index.ts +23 -1
  47. package/src/menuPositioning.ts +31 -5
  48. package/src/sharedAnchors.ts +67 -0
  49. package/src/videoBox.ts +49 -0
package/README.md CHANGED
@@ -82,6 +82,18 @@ cannot loop with the request. A resize within about a second of the stream
82
82
  changing that matches its new shape counts as following it. Layout changes
83
83
  outside that window always report.
84
84
 
85
+ The feed frame (the element fullscreen targets) carries two attributes for
86
+ hosts and tests:
87
+
88
+ - `data-kc-signal`: `live`, `stalled` (no frame has left the decoder for
89
+ half a second, or none yet since the source attached) or `none` (no
90
+ source). It is measured after any encoded transform the host installs, so
91
+ a transform that holds frames back shows as `stalled` while it holds them.
92
+ The video keeps presenting frames while it shows static, so its frame
93
+ counts cannot tell a picture from static, and inbound-rtp `framesDecoded`
94
+ averaged over seconds misses gaps this short; this attribute can
95
+ - `data-fullscreen`: present while the feed is fullscreen
96
+
85
97
  An imperative `CameraFeedHandle` (via `ref`) exposes `stepCamera`,
86
98
  `setPanAxis`, `setZoomRate`, `nudgePan`, and `nudgeZoom` for wiring feeds
87
99
  to a gamepad or keyboard.
@@ -92,6 +104,65 @@ while a camera is tracking. `cameraFilter` narrows the selectable cameras
92
104
  (picker, stepper, auto-latch). `showActions={false}` hides the whole action
93
105
  bar without affecting resolution reporting.
94
106
 
107
+ ## Anchored markers
108
+
109
+ `anchors` marks things in the game on the feed's picture (see the core
110
+ SDK's "Anchored markers" for the kinds and fields). The feed sets them on
111
+ the camera it shows, clears them when it moves on or unmounts, and draws a
112
+ marker where each one lands in every displayed frame: four corner marks
113
+ sized from `apparentRadiusPx` (never below 14 px), green when clear, red and
114
+ dashed when occluded.
115
+
116
+ `anchorOverlay` has the same shape as `targetBracketOverlay`: `corners`
117
+ restyles the four marks (`top`, `right`, `bottom`, `left`, each
118
+ `{ colour, dashed, hidden, shape }`), `render` adds SVG in screen pixels,
119
+ `readout` puts text under the marker, and `shape` sets every mark to
120
+ `"corners"` or `"triangles"`. Each is called per anchor with the raw result
121
+ and a context: its screen geometry, the fitted content rect, the displayed
122
+ frame's UT and the screen position of every drawn anchor. `owners` also
123
+ draws anchors set in game by those owners. What a colour means is yours to
124
+ decide; Kerbcast only reports geometry.
125
+
126
+ A direction reticle on a docking port: four triangles, each side coloured
127
+ from the port's axes.
128
+
129
+ ```tsx
130
+ const anchors = useMemo(
131
+ () => [{ id: "port", kind: AnchorKind.Part, vessel: targetPid, part: portPid, orientation: true }],
132
+ [targetPid, portPid],
133
+ );
134
+
135
+ <CameraFeed
136
+ flightId={dockingCam}
137
+ anchors={anchors}
138
+ anchorOverlay={{
139
+ shape: "triangles",
140
+ corners: (a) => {
141
+ /* The port faces the lens along -z; tilt shows in its forward's x and y. */
142
+ const [fx = 0, fy = 0] = a.forward ?? [];
143
+ const side = (off: number) => ({ colour: Math.abs(off) < 0.02 ? "lime" : "orange" });
144
+ return { top: side(fy), bottom: side(fy), left: side(fx), right: side(fx) };
145
+ },
146
+ readout: (a) => (a.distanceM != null ? `${a.distanceM.toFixed(1)} m` : null),
147
+ }}
148
+ />
149
+ ```
150
+
151
+ For a video of your own, `AnchorMarkers` draws a frame's anchors over any
152
+ `<video>` and `useDisplayedFrame(enabled, flightId, video)` gives the
153
+ displayed frame with its results.
154
+
155
+ On command feeds the target brackets follow the subject's anchor while it
156
+ is visible and no command lock is held, so the box stays on the craft where
157
+ the picture shows it even while an aim points elsewhere. With the craft out
158
+ of frame or occluded, or under the lock, the box is the 1.11 one: centred on
159
+ the aim, sized from the aimed range, red and dashed when the aim line is
160
+ occluded. `targetBracketOverlay` is unchanged; its context gains `subject`,
161
+ the observed anchor, so you can draw it as a second mark.
162
+
163
+ An observed anchor out of view (see the core SDK) carries no position and
164
+ draws no marker; it is still in `ctx.frame.anchors`.
165
+
95
166
  ## `KerbalFaceFeed`
96
167
 
97
168
  A single-kerbal face-camera primitive: one crew face in a square frame, keyed
@@ -0,0 +1,128 @@
1
+ import { type DisplayedFrameUt, type FittedFrame, type FrameAnchor, type FrameFit, type FrameUtSource } from "@ksp-gonogo/kerbcast";
2
+ import { type ReactNode } from "react";
3
+ /** Default colours; each is overridable through the named CSS custom property. */
4
+ export declare const ANCHOR_MARKER_COLOURS: {
5
+ readonly clear: "var(--kerbcast-anchor-clear, #4caf50)";
6
+ readonly occluded: "var(--kerbcast-anchor-occluded, #f44336)";
7
+ };
8
+ /** The four marks around an anchor, clockwise from the top. */
9
+ export type AnchorCorner = "top" | "right" | "bottom" | "left";
10
+ /**
11
+ * How a mark is drawn. `corners`: a right-angle corner of a box around the
12
+ * anchor, like the command brackets; each key names the corner that starts
13
+ * its side going clockwise, so `top` is the top left, `right` the top right,
14
+ * `bottom` the bottom right and `left` the bottom left. `triangles`: a
15
+ * triangle outside the box on that side, pointing at the anchor.
16
+ */
17
+ export type AnchorMarkerShape = "corners" | "triangles";
18
+ export interface AnchorCornerStyle {
19
+ colour: string;
20
+ dashed: boolean;
21
+ shape: AnchorMarkerShape;
22
+ }
23
+ /** Changes to one default mark; unset fields keep the default. */
24
+ export interface AnchorCornerOverride {
25
+ colour?: string;
26
+ dashed?: boolean;
27
+ hidden?: boolean;
28
+ shape?: AnchorMarkerShape;
29
+ }
30
+ export type AnchorCornerOverrides = Partial<Record<AnchorCorner, AnchorCornerOverride>>;
31
+ /** The marker box for one anchor, in the overlay SVG's pixels (the video element's box). */
32
+ export interface AnchorMarkerGeometry {
33
+ /** Where the anchor is on screen. Off the box when it is out of frame. */
34
+ cx: number;
35
+ cy: number;
36
+ /** Half the box side: the anchor's apparent radius on screen, never below a minimum. */
37
+ half: number;
38
+ /** Length of each corner stroke, and the side of each triangle. */
39
+ arm: number;
40
+ stroke: number;
41
+ }
42
+ /** An anchor with its place on screen. */
43
+ export interface PlacedAnchor {
44
+ anchor: FrameAnchor;
45
+ x: number;
46
+ y: number;
47
+ }
48
+ /** Everything an anchor overlay needs to line up with the drawn markers. */
49
+ export interface AnchorContext {
50
+ geometry: AnchorMarkerGeometry;
51
+ /** Where the frame lands in the element, after `object-fit`. */
52
+ content: FittedFrame;
53
+ /** Size of the overlay SVG, the video element's box. */
54
+ width: number;
55
+ height: number;
56
+ /** Mission UT of the displayed frame the anchors describe. */
57
+ displayUt: number;
58
+ utSource: FrameUtSource;
59
+ /** Every drawn anchor in this frame with its screen position. */
60
+ anchors: readonly PlacedAnchor[];
61
+ /** The displayed frame, with its raw per-frame results and size. */
62
+ frame: DisplayedFrameUt;
63
+ }
64
+ /**
65
+ * Consumer changes to a feed's anchor markers. Each part is optional and is
66
+ * called for every drawn anchor on every displayed frame. What an anchor
67
+ * means, and which colour says so, is the consumer's: Kerbcast only reports
68
+ * where it is.
69
+ */
70
+ export interface AnchorOverlay {
71
+ /** Default shape of every mark. Default `"corners"`. */
72
+ shape?: AnchorMarkerShape;
73
+ /** Restyle or hide the four marks of one anchor. */
74
+ corners?: AnchorCornerOverrides | ((anchor: FrameAnchor, ctx: AnchorContext) => AnchorCornerOverrides | null | undefined);
75
+ /**
76
+ * SVG content drawn above the marks, in the same pixel space. Runs for an
77
+ * anchor out of frame or behind the lens too (its marks are left out then),
78
+ * so an overlay can point to it from the edge. An observed result out of
79
+ * view carries no position and is not drawn at all; it is still in
80
+ * `ctx.frame.anchors`.
81
+ */
82
+ render?: (anchor: FrameAnchor, ctx: AnchorContext) => ReactNode;
83
+ /** Text under the marker, or null for none. Default none. */
84
+ readout?: (anchor: FrameAnchor, ctx: AnchorContext) => string | null;
85
+ /**
86
+ * Game-side owners whose anchors are drawn as well as this client's own,
87
+ * e.g. `["my-mod"]`. Default: only this client's.
88
+ */
89
+ owners?: readonly string[];
90
+ /**
91
+ * Draw Kerbcast's own `aim` anchor too when `"kerbcast"` is among
92
+ * `owners`. Off by default: the brackets already mark the aim.
93
+ */
94
+ showAim?: boolean;
95
+ }
96
+ /** The marker box for an anchor at `centre` with `radiusPx` capture pixels on a `frameHeight`-tall frame. */
97
+ export declare function anchorMarkerGeometry(centre: {
98
+ x: number;
99
+ y: number;
100
+ }, radiusPx: number, frameHeight: number, content: FittedFrame): AnchorMarkerGeometry;
101
+ export interface AnchorMarkersProps {
102
+ /** The displayed frame whose anchors to draw. */
103
+ frame: DisplayedFrameUt | null;
104
+ video: HTMLVideoElement | null;
105
+ overlay?: AnchorOverlay | null;
106
+ /** How the video fits its box. Default: read from the element's CSS. */
107
+ fit?: FrameFit;
108
+ }
109
+ /**
110
+ * Markers for the anchors in one displayed frame: by default four corner
111
+ * marks around each anchor sized from its apparent radius, green when clear
112
+ * and red and dashed when occluded. Positions come from the frame's own
113
+ * stamp, so they match the picture under any delay.
114
+ */
115
+ export declare function AnchorMarkers({ frame, video, overlay, fit }: AnchorMarkersProps): import("react").JSX.Element | null;
116
+ /**
117
+ * The displayed frame of `flightId` on `video`, re-rendering on every
118
+ * presented frame while `enabled`. Kept to the component that draws from it,
119
+ * so a feed's other UI does not re-render at the frame rate.
120
+ */
121
+ export declare function useDisplayedFrame(enabled: boolean, flightId: number | null, video: HTMLVideoElement | null): DisplayedFrameUt | null;
122
+ /** Self-subscribing markers for a feed: watches its displayed frames and draws their anchors. */
123
+ export declare function LiveAnchorMarkers({ flightId, video, overlay, fit, }: {
124
+ flightId: number | null;
125
+ video: HTMLVideoElement | null;
126
+ overlay?: AnchorOverlay | null;
127
+ fit?: FrameFit;
128
+ }): import("react").JSX.Element;
@@ -0,0 +1,162 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { COMMAND_AIM_ANCHOR_ID, KERBCAST_ANCHOR_OWNER, anchorPoint, fitFrame, frameToScreen, } from "@ksp-gonogo/kerbcast";
3
+ import { useEffect, useState } from "react";
4
+ import styled from "styled-components";
5
+ import { useKerbcastClient } from "./context";
6
+ import { useVideoBox, videoFit } from "./videoBox";
7
+ const MARKER_STROKE_PX = 2;
8
+ const MARKER_MIN_HALF_PX = 14;
9
+ const MARKER_ARM_FRACTION = 0.3;
10
+ const MARKER_MIN_ARM_PX = 6;
11
+ const READOUT_GAP_PX = 11;
12
+ /** Default colours; each is overridable through the named CSS custom property. */
13
+ export const ANCHOR_MARKER_COLOURS = {
14
+ clear: "var(--kerbcast-anchor-clear, #4caf50)",
15
+ occluded: "var(--kerbcast-anchor-occluded, #f44336)",
16
+ };
17
+ /** The marker box for an anchor at `centre` with `radiusPx` capture pixels on a `frameHeight`-tall frame. */
18
+ export function anchorMarkerGeometry(centre, radiusPx, frameHeight, content) {
19
+ const scale = frameHeight > 0 ? content.height / frameHeight : 1;
20
+ const half = Math.max(MARKER_MIN_HALF_PX, radiusPx * scale);
21
+ const arm = Math.max(MARKER_MIN_ARM_PX, MARKER_ARM_FRACTION * half * 2);
22
+ return { cx: centre.x, cy: centre.y, half, arm, stroke: MARKER_STROKE_PX };
23
+ }
24
+ const CORNERS = ["top", "right", "bottom", "left"];
25
+ /** SVG path data for one mark. Corners are open strokes; triangles are closed. */
26
+ function markPath(corner, shape, g) {
27
+ const { cx, cy, half: h, arm: a } = g;
28
+ if (shape === "triangles") {
29
+ const t = a * 0.8;
30
+ const w = a / 2;
31
+ switch (corner) {
32
+ case "top":
33
+ return `M${cx} ${cy - h}L${cx - w} ${cy - h - t}L${cx + w} ${cy - h - t}Z`;
34
+ case "right":
35
+ return `M${cx + h} ${cy}L${cx + h + t} ${cy - w}L${cx + h + t} ${cy + w}Z`;
36
+ case "bottom":
37
+ return `M${cx} ${cy + h}L${cx + w} ${cy + h + t}L${cx - w} ${cy + h + t}Z`;
38
+ default:
39
+ return `M${cx - h} ${cy}L${cx - h - t} ${cy + w}L${cx - h - t} ${cy - w}Z`;
40
+ }
41
+ }
42
+ const l = cx - h, r = cx + h, tp = cy - h, b = cy + h;
43
+ switch (corner) {
44
+ case "top":
45
+ return `M${l} ${tp + a}L${l} ${tp}L${l + a} ${tp}`;
46
+ case "right":
47
+ return `M${r - a} ${tp}L${r} ${tp}L${r} ${tp + a}`;
48
+ case "bottom":
49
+ return `M${r} ${b - a}L${r} ${b}L${r - a} ${b}`;
50
+ default:
51
+ return `M${l + a} ${b}L${l} ${b}L${l} ${b - a}`;
52
+ }
53
+ }
54
+ /** Whether a feed shows this anchor: this client's own, or one from a listed owner, Kerbcast's aim only on request. */
55
+ function drawn(a, overlay) {
56
+ if (a.owner === undefined)
57
+ return true;
58
+ if (a.owner === KERBCAST_ANCHOR_OWNER && a.id === COMMAND_AIM_ANCHOR_ID && !overlay?.showAim)
59
+ return false;
60
+ return overlay?.owners?.includes(a.owner) ?? false;
61
+ }
62
+ /**
63
+ * Markers for the anchors in one displayed frame: by default four corner
64
+ * marks around each anchor sized from its apparent radius, green when clear
65
+ * and red and dashed when occluded. Positions come from the frame's own
66
+ * stamp, so they match the picture under any delay.
67
+ */
68
+ export function AnchorMarkers({ frame, video, overlay, fit }) {
69
+ const { boxW, boxH, mediaW, mediaH } = useVideoBox(video);
70
+ if (!frame?.anchors?.length)
71
+ return null;
72
+ const frameW = frame.frameWidth || mediaW;
73
+ const frameH = frame.frameHeight || mediaH;
74
+ if (!(frameW > 0 && frameH > 0 && boxW > 0 && boxH > 0))
75
+ return null;
76
+ const content = fitFrame(boxW, boxH, frameW, frameH, fit ?? videoFit(video));
77
+ /* An observed result out of view has no position, so it has no marker. */
78
+ const placed = [];
79
+ for (const a of frame.anchors) {
80
+ const point = drawn(a, overlay) ? anchorPoint(a) : null;
81
+ if (point)
82
+ placed.push({ anchor: a, ...frameToScreen(point, frameW, frameH, content) });
83
+ }
84
+ if (placed.length === 0)
85
+ return null;
86
+ const shape = overlay?.shape ?? "corners";
87
+ const items = placed.map(({ anchor, x, y }) => {
88
+ const geometry = anchorMarkerGeometry({ x, y }, anchor.apparentRadiusPx ?? 0, frameH, content);
89
+ const ctx = {
90
+ geometry,
91
+ content,
92
+ width: boxW,
93
+ height: boxH,
94
+ displayUt: frame.ut,
95
+ utSource: frame.source,
96
+ anchors: placed,
97
+ frame,
98
+ };
99
+ const overrides = typeof overlay?.corners === "function" ? overlay.corners(anchor, ctx) : overlay?.corners;
100
+ const base = anchor.occluded
101
+ ? { colour: ANCHOR_MARKER_COLOURS.occluded, dashed: true, shape }
102
+ : { colour: ANCHOR_MARKER_COLOURS.clear, dashed: false, shape };
103
+ const visible = anchor.inFrame && !anchor.behind;
104
+ const readout = overlay?.readout ? overlay.readout(anchor, ctx) : null;
105
+ return { anchor, geometry, ctx, overrides, base, visible, readout };
106
+ });
107
+ return (_jsxs(_Fragment, { children: [_jsx(MarkerSvg, { "aria-hidden": "true", "data-anchor-markers": "", width: boxW, height: boxH, children: items.map(({ anchor, geometry, ctx, overrides, base, visible }) => (_jsxs("g", { "data-anchor": anchor.id, "data-owner": anchor.owner, children: [visible &&
108
+ CORNERS.map((corner) => {
109
+ const o = overrides?.[corner];
110
+ if (o?.hidden)
111
+ return null;
112
+ const s = o?.shape ?? base.shape;
113
+ const colour = o?.colour ?? base.colour;
114
+ const dashed = o?.dashed ?? base.dashed;
115
+ const filled = s === "triangles";
116
+ return (_jsx("path", { "data-corner": corner, "data-shape": s, d: markPath(corner, s, geometry), strokeWidth: geometry.stroke, strokeDasharray: dashed ? "3 2" : undefined, style: filled ? { fill: dashed ? "none" : colour, stroke: colour } : { fill: "none", stroke: colour } }, corner));
117
+ }), overlay?.render && _jsx("g", { "data-anchor-overlay": "", children: overlay.render(anchor, ctx) })] }, `${anchor.owner ?? ""}/${anchor.id}`))) }), items.map(({ anchor, geometry, readout, visible }) => readout &&
118
+ visible && (_jsx(Readout, { "data-anchor-readout": anchor.id, style: { left: geometry.cx, top: geometry.cy + geometry.half + READOUT_GAP_PX }, children: readout }, `${anchor.owner ?? ""}/${anchor.id}`)))] }));
119
+ }
120
+ /**
121
+ * The displayed frame of `flightId` on `video`, re-rendering on every
122
+ * presented frame while `enabled`. Kept to the component that draws from it,
123
+ * so a feed's other UI does not re-render at the frame rate.
124
+ */
125
+ export function useDisplayedFrame(enabled, flightId, video) {
126
+ const client = useKerbcastClient();
127
+ const [frame, setFrame] = useState(null);
128
+ useEffect(() => {
129
+ setFrame(null);
130
+ if (!enabled || flightId === null || !video)
131
+ return;
132
+ return client.watchDisplayedFrames(flightId, video, (f) =>
133
+ /* Skip re-renders between frames that carry the same results. */
134
+ setFrame((prev) => prev && prev.anchors === f.anchors && prev.rtpTimestamp === f.rtpTimestamp ? prev : f));
135
+ }, [client, enabled, flightId, video]);
136
+ return frame;
137
+ }
138
+ /** Self-subscribing markers for a feed: watches its displayed frames and draws their anchors. */
139
+ export function LiveAnchorMarkers({ flightId, video, overlay, fit, }) {
140
+ const frame = useDisplayedFrame(true, flightId, video);
141
+ return _jsx(AnchorMarkers, { frame: frame, video: video, overlay: overlay, fit: fit });
142
+ }
143
+ const MarkerSvg = styled.svg `
144
+ position: absolute;
145
+ top: 0;
146
+ left: 0;
147
+ z-index: 1;
148
+ overflow: visible;
149
+ pointer-events: none;
150
+ `;
151
+ const Readout = styled.div `
152
+ position: absolute;
153
+ z-index: 1;
154
+ transform: translateX(-50%);
155
+ white-space: nowrap;
156
+ color: #fff;
157
+ font-size: 10px;
158
+ font-weight: 600;
159
+ letter-spacing: 0.12em;
160
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.9);
161
+ pointer-events: none;
162
+ `;
@@ -1,6 +1,7 @@
1
- import type { CameraState, KerbcastClient } from "@ksp-gonogo/kerbcast";
1
+ import type { Anchor, CameraState, DisplayedFrameUt, KerbcastClient } from "@ksp-gonogo/kerbcast";
2
2
  import { type FeedAction } from "./FeedActionBar";
3
3
  import { type CommandBracketOverlay } from "./CommandBrackets";
4
+ import { type AnchorOverlay } from "./AnchorMarkers";
4
5
  /**
5
6
  * The consumer-supplied action shape, and the shared top-right action bar
6
7
  * both `CameraFeed` and `KerbalFaceFeed` render, live in `FeedActionBar`.
@@ -150,8 +151,9 @@ export interface CameraFeedProps {
150
151
  enableRecording?: boolean;
151
152
  /**
152
153
  * Record this feed at the operator's full render size for the duration of
153
- * the built-in REC control's recording (overrides only the display-size
154
- * demand; still yields to the adaptive framerate shed). Off by default.
154
+ * the built-in REC control's recording (overrides the display-size
155
+ * demand and any viewer quality preset; still capped by an operator
156
+ * render-size cap and the adaptive shed). Off by default.
155
157
  * Threaded straight into `start(flightId, { forceFullResolution })`; has
156
158
  * no effect unless `enableRecording` is also set. While the feed is armed
157
159
  * (force sent, waiting for the resolution bump) the tile shows an
@@ -199,14 +201,53 @@ export interface CameraFeedProps {
199
201
  * brackets. See {@link CommandBracketOverlay}.
200
202
  */
201
203
  targetBracketOverlay?: CommandBracketOverlay;
204
+ /**
205
+ * What a command camera feed says while the camera has no subject to aim
206
+ * at yet and shows its placeholder framing (a launch site camera on its
207
+ * pad, a station looking at the horizon). Drawn over the picture, never
208
+ * into the video, so recordings stay clean. Default "Waiting for target";
209
+ * `null` or an empty string shows nothing.
210
+ */
211
+ waitingForTargetLabel?: string | null;
212
+ /**
213
+ * What the feed says while its camera has no video slot on this connection
214
+ * (`KerbcastCameraHandle.waitingForSlot`): the pool is at its ceiling or
215
+ * cannot grow. The feed takes the next slot another feed gives up. Drawn
216
+ * over the picture; `null` or an empty string shows nothing.
217
+ */
218
+ noSlotLabel?: string | null;
202
219
  /**
203
220
  * Mission UT the displayed video was captured at, for a host running its
204
221
  * own delayed playout. Picks which command-track transition is in force and
205
222
  * ages the readout. Defaults to the displayed frame's own capture UT where
206
223
  * the browser reports frame RTP timestamps, else the client's capture
207
- * clock interpolated to now.
224
+ * clock interpolated to now. While a command lock is held the brackets and
225
+ * readout come from the displayed frame's own state, which a UT alone does
226
+ * not carry: such a host passes `displayFrame` instead.
208
227
  */
209
228
  displayUt?: number;
229
+ /**
230
+ * The frame a host's own delayed playout is showing, as
231
+ * `watchDisplayedFrames` reported it when it arrived. Wins over
232
+ * `displayUt`: its `ut` picks the transition in force and its `command`
233
+ * draws the brackets and readout, so they never run ahead of the picture.
234
+ */
235
+ displayFrame?: DisplayedFrameUt | null;
236
+ /**
237
+ * Things to mark on this feed's picture: parts, craft, points on a body or
238
+ * directions. Set for the displayed camera as this client's anchors and
239
+ * cleared when the feed shows another camera or unmounts; the game
240
+ * projects each through the camera that rendered every frame, and a
241
+ * default marker is drawn where it lands. One set per camera per client:
242
+ * two feeds on the same camera share it, the last to set winning. At
243
+ * most 16. Pass a stable or memoised array; it is compared by value.
244
+ */
245
+ anchors?: Anchor[];
246
+ /**
247
+ * Restyle the anchor markers, add SVG of your own, put text under them,
248
+ * or draw anchors set in game too. See {@link AnchorOverlay}.
249
+ */
250
+ anchorOverlay?: AnchorOverlay;
210
251
  }
211
252
  export interface CameraFeedHandle {
212
253
  stepCamera(delta: number): void;