@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.
- package/README.md +71 -0
- package/dist/AnchorMarkers.d.ts +128 -0
- package/dist/AnchorMarkers.js +162 -0
- package/dist/CameraFeed.d.ts +45 -4
- package/dist/CameraFeed.js +163 -23
- package/dist/CommandBrackets.d.ts +61 -12
- package/dist/CommandBrackets.js +106 -53
- package/dist/FeedActionBar.d.ts +7 -0
- package/dist/FeedActionBar.js +4 -4
- package/dist/KerbalFaceFeed.d.ts +1 -1
- package/dist/KerbalFaceFeed.js +1 -1
- package/dist/RecordingsTray.d.ts +6 -0
- package/dist/RecordingsTray.js +17 -1
- package/dist/hooks/useCommandDisplayUt.d.ts +16 -3
- package/dist/hooks/useCommandDisplayUt.js +57 -16
- package/dist/hooks/useKerbcastWaitingForSlot.d.ts +5 -0
- package/dist/hooks/useKerbcastWaitingForSlot.js +20 -0
- package/dist/hooks/useTargetBracketsPreference.d.ts +3 -2
- package/dist/hooks/useTargetBracketsPreference.js +18 -5
- package/dist/index.d.ts +4 -1
- package/dist/index.js +5 -1
- package/dist/menuPositioning.d.ts +8 -2
- package/dist/menuPositioning.js +30 -5
- package/dist/sharedAnchors.d.ts +3 -0
- package/dist/sharedAnchors.js +61 -0
- package/dist/videoBox.d.ts +10 -0
- package/dist/videoBox.js +40 -0
- package/package.json +2 -2
- package/src/AnchorMarkers.test.tsx +173 -0
- package/src/AnchorMarkers.tsx +341 -0
- package/src/CameraFeed.test.tsx +687 -6
- package/src/CameraFeed.tsx +240 -27
- package/src/CommandBrackets.test.tsx +204 -4
- package/src/CommandBrackets.tsx +170 -53
- package/src/FeedActionBar.test.ts +22 -0
- package/src/FeedActionBar.tsx +11 -4
- package/src/KerbalFaceFeed.tsx +1 -1
- package/src/RecordingsTray.test.tsx +15 -0
- package/src/RecordingsTray.tsx +25 -1
- package/src/hooks/useCommandDisplayUt.test.tsx +74 -3
- package/src/hooks/useCommandDisplayUt.ts +61 -15
- package/src/hooks/useKerbcastWaitingForSlot.ts +25 -0
- package/src/hooks/useRecordings.sharedState.test.tsx +6 -3
- package/src/hooks/useRecordings.test.tsx +6 -3
- package/src/hooks/useTargetBracketsPreference.ts +15 -5
- package/src/index.ts +23 -1
- package/src/menuPositioning.ts +31 -5
- package/src/sharedAnchors.ts +67 -0
- 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
|
+
`;
|
package/dist/CameraFeed.d.ts
CHANGED
|
@@ -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
|
|
154
|
-
* demand
|
|
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;
|