@ksp-gonogo/kerbcast-react 1.8.0 → 1.9.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/CHANGELOG.md +1 -1
- package/dist/CameraFeed.d.ts +19 -0
- package/dist/CameraFeed.js +6 -3
- package/package.json +2 -2
- package/src/CameraFeed.test.tsx +92 -0
- package/src/CameraFeed.tsx +26 -2
package/CHANGELOG.md
CHANGED
package/dist/CameraFeed.d.ts
CHANGED
|
@@ -116,6 +116,25 @@ export interface CameraFeedProps {
|
|
|
116
116
|
* the gimbal + FoV). Default false.
|
|
117
117
|
*/
|
|
118
118
|
enableTracking?: boolean;
|
|
119
|
+
/**
|
|
120
|
+
* Stand down the built-in MANUAL camera controls: the pan ball and the
|
|
121
|
+
* zoom/FoV controls disappear, their in-flight rates are zeroed, and the
|
|
122
|
+
* imperative handle's pan/zoom methods become no-ops (so a physical
|
|
123
|
+
* controller wired to the handle can't reach the camera either).
|
|
124
|
+
*
|
|
125
|
+
* "Manual" in the sense this file already uses it - a human directly driving
|
|
126
|
+
* the gimbal and optics - and named for that intent rather than for today's
|
|
127
|
+
* specific surfaces, so anything added later that hand-flies the camera
|
|
128
|
+
* stands down under this flag too. Auto-track is NOT manual and stays live:
|
|
129
|
+
* the aim loop runs game-side, so it keeps working when a host takes the
|
|
130
|
+
* hand-flown path away. Feed affordances are untouched as well - action bar,
|
|
131
|
+
* recording, fullscreen, PiP, quality.
|
|
132
|
+
*
|
|
133
|
+
* For a host that owns manual control itself, e.g. a signal-delay setpoint
|
|
134
|
+
* surface where live pan/zoom would be steering at where the craft is now
|
|
135
|
+
* while watching where it was a light-time ago. Default false.
|
|
136
|
+
*/
|
|
137
|
+
disableManualControls?: boolean;
|
|
119
138
|
/**
|
|
120
139
|
* Show a built-in REC control in the action bar: a STATEFUL toggle (never
|
|
121
140
|
* overflow-eligible, per the #6 spec) that starts/stops a client-side
|
package/dist/CameraFeed.js
CHANGED
|
@@ -59,7 +59,7 @@ function exitFullscreen() {
|
|
|
59
59
|
// ---------------------------------------------------------------------------
|
|
60
60
|
// Inner component (reads from context)
|
|
61
61
|
// ---------------------------------------------------------------------------
|
|
62
|
-
const CameraFeedInner = forwardRef(function CameraFeedInner({ flightId: requestedFlightId, onSelectCamera, cameraFilter, onDisplayedCameraChange, showDebugInfo = false, showStatic, inFlight: inFlightProp, showStandbyIcon = true, renderSize = "auto", emptyMessage = "No camera feeds - start a vessel with Hullcam parts installed", enableFullscreen = false, enablePictureInPicture = false, enableQualityControl = false, enableTracking = false, enableRecording = false, recordFullResolution = false, actions, trailingActions, showActions = true, useStream, }, ref) {
|
|
62
|
+
const CameraFeedInner = forwardRef(function CameraFeedInner({ flightId: requestedFlightId, onSelectCamera, cameraFilter, onDisplayedCameraChange, showDebugInfo = false, showStatic, inFlight: inFlightProp, showStandbyIcon = true, renderSize = "auto", emptyMessage = "No camera feeds - start a vessel with Hullcam parts installed", enableFullscreen = false, enablePictureInPicture = false, enableQualityControl = false, enableTracking = false, disableManualControls = false, enableRecording = false, recordFullResolution = false, actions, trailingActions, showActions = true, useStream, }, ref) {
|
|
63
63
|
const client = useKerbcastClient();
|
|
64
64
|
// Always called (rules of hooks); `enableRecording` only gates whether the
|
|
65
65
|
// REC entry/badge render, not whether the store is subscribed.
|
|
@@ -232,8 +232,11 @@ const CameraFeedInner = forwardRef(function CameraFeedInner({ flightId: requeste
|
|
|
232
232
|
}
|
|
233
233
|
}, []);
|
|
234
234
|
const isDestroyed = camera ? isCameraDestroyed(camera) : false;
|
|
235
|
-
|
|
236
|
-
|
|
235
|
+
/* Gating here rather than at the render sites covers the whole manual
|
|
236
|
+
path at once: the controls unmount, the effects below zero any in-flight
|
|
237
|
+
rate, and the imperative handle's pan/zoom methods no-op. */
|
|
238
|
+
const showPan = camera?.supportsPan && !isDestroyed && !disableManualControls;
|
|
239
|
+
const showZoom = camera?.supportsZoom && !isDestroyed && !disableManualControls;
|
|
237
240
|
// Auto-track state is server-authoritative: driven purely by the published
|
|
238
241
|
// trackMode (absent -> none), never local click. While tracking, the aim
|
|
239
242
|
// loop owns the gimbal + FoV, so the manual pan/zoom controls are disabled
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ksp-gonogo/kerbcast-react",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "React components and hooks for kerbcast camera feeds.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Jon Pepler",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"access": "public"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@ksp-gonogo/kerbcast": "^1.
|
|
48
|
+
"@ksp-gonogo/kerbcast": "^1.9.0"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
51
|
"react": "^18 || ^19",
|
package/src/CameraFeed.test.tsx
CHANGED
|
@@ -272,6 +272,7 @@ function renderFeed(
|
|
|
272
272
|
renderSize?: "auto" | "none";
|
|
273
273
|
enableQualityControl?: boolean;
|
|
274
274
|
enableTracking?: boolean;
|
|
275
|
+
disableManualControls?: boolean;
|
|
275
276
|
enableRecording?: boolean;
|
|
276
277
|
recordFullResolution?: boolean;
|
|
277
278
|
enableFullscreen?: boolean;
|
|
@@ -2014,6 +2015,97 @@ describe("CameraFeed - tracking (crosshair)", () => {
|
|
|
2014
2015
|
expect((getByLabelText("Zoom in") as HTMLButtonElement).disabled).toBe(false);
|
|
2015
2016
|
expect((getByLabelText("Pan left") as HTMLButtonElement).disabled).toBe(false);
|
|
2016
2017
|
});
|
|
2018
|
+
|
|
2019
|
+
// -------------------------------------------------------------------------
|
|
2020
|
+
// disableManualControls: a host taking over the hand-flown path
|
|
2021
|
+
// -------------------------------------------------------------------------
|
|
2022
|
+
|
|
2023
|
+
it("shows the manual pan + zoom controls by default", async () => {
|
|
2024
|
+
const { client } = await buildConnectedSource([panZoomCam()]);
|
|
2025
|
+
const { queryByLabelText } = renderFeed(client, { flightId: 42 });
|
|
2026
|
+
expect(queryByLabelText("Zoom in")).not.toBeNull();
|
|
2027
|
+
expect(queryByLabelText("Pan camera")).not.toBeNull();
|
|
2028
|
+
});
|
|
2029
|
+
|
|
2030
|
+
it("removes the manual pan + zoom controls when disableManualControls", async () => {
|
|
2031
|
+
const { client } = await buildConnectedSource([panZoomCam()]);
|
|
2032
|
+
const { queryByLabelText } = renderFeed(client, {
|
|
2033
|
+
flightId: 42,
|
|
2034
|
+
disableManualControls: true,
|
|
2035
|
+
});
|
|
2036
|
+
// Gone, not merely disabled: a host owning control shouldn't have to
|
|
2037
|
+
// occlude ours.
|
|
2038
|
+
expect(queryByLabelText("Zoom in")).toBeNull();
|
|
2039
|
+
expect(queryByLabelText("Zoom out")).toBeNull();
|
|
2040
|
+
expect(queryByLabelText("Pan camera")).toBeNull();
|
|
2041
|
+
expect(queryByLabelText("Pan left")).toBeNull();
|
|
2042
|
+
});
|
|
2043
|
+
|
|
2044
|
+
it("keeps auto-track live when disableManualControls: the aim loop is not manual", async () => {
|
|
2045
|
+
const { client } = await buildConnectedSource([panZoomCam()]);
|
|
2046
|
+
const { queryByLabelText } = renderFeed(client, {
|
|
2047
|
+
flightId: 42,
|
|
2048
|
+
enableTracking: true,
|
|
2049
|
+
disableManualControls: true,
|
|
2050
|
+
});
|
|
2051
|
+
expect(queryByLabelText(/track/i)).not.toBeNull();
|
|
2052
|
+
});
|
|
2053
|
+
|
|
2054
|
+
const panRates = (sidecar: MockSidecar) =>
|
|
2055
|
+
sidecar.commands.filter((c) => c.type === "set-pan-rate");
|
|
2056
|
+
|
|
2057
|
+
it("no-ops the imperative pan/zoom handle when disableManualControls", async () => {
|
|
2058
|
+
// Covers the serial-controller path: a host that hides the on-screen
|
|
2059
|
+
// controls must not be reachable through the handle either.
|
|
2060
|
+
const { client, sidecar } = await buildConnectedSource([panZoomCam()]);
|
|
2061
|
+
|
|
2062
|
+
const handleRef = createRef<CameraFeedHandle>();
|
|
2063
|
+
renderFeed(client, {
|
|
2064
|
+
flightId: 42,
|
|
2065
|
+
disableManualControls: true,
|
|
2066
|
+
ref: handleRef,
|
|
2067
|
+
});
|
|
2068
|
+
|
|
2069
|
+
await act(async () => {
|
|
2070
|
+
handleRef.current?.setPanAxis("yaw", 1);
|
|
2071
|
+
handleRef.current?.nudgePan(1, 0);
|
|
2072
|
+
handleRef.current?.setZoomRate(1);
|
|
2073
|
+
handleRef.current?.nudgeZoom(1);
|
|
2074
|
+
});
|
|
2075
|
+
|
|
2076
|
+
expect(panRates(sidecar)).toHaveLength(0);
|
|
2077
|
+
});
|
|
2078
|
+
|
|
2079
|
+
it("zeroes an in-flight pan rate when disableManualControls flips on", async () => {
|
|
2080
|
+
const { client, sidecar } = await buildConnectedSource([panZoomCam()]);
|
|
2081
|
+
|
|
2082
|
+
const handleRef = createRef<CameraFeedHandle>();
|
|
2083
|
+
const { rerender } = render(
|
|
2084
|
+
<KerbcastProvider client={client}>
|
|
2085
|
+
<CameraFeed ref={handleRef} flightId={42} />
|
|
2086
|
+
</KerbcastProvider>,
|
|
2087
|
+
);
|
|
2088
|
+
|
|
2089
|
+
await act(async () => {
|
|
2090
|
+
handleRef.current?.setPanAxis("yaw", 1);
|
|
2091
|
+
});
|
|
2092
|
+
expect(panRates(sidecar).length).toBeGreaterThan(0);
|
|
2093
|
+
|
|
2094
|
+
// Handing control over mid-pan must not leave the gimbal running.
|
|
2095
|
+
await act(async () => {
|
|
2096
|
+
rerender(
|
|
2097
|
+
<KerbcastProvider client={client}>
|
|
2098
|
+
<CameraFeed ref={handleRef} flightId={42} disableManualControls />
|
|
2099
|
+
</KerbcastProvider>,
|
|
2100
|
+
);
|
|
2101
|
+
});
|
|
2102
|
+
|
|
2103
|
+
expect(sidecar.lastCommand("set-pan-rate")?.content).toMatchObject({
|
|
2104
|
+
flightId: 42,
|
|
2105
|
+
yawRate: 0,
|
|
2106
|
+
pitchRate: 0,
|
|
2107
|
+
});
|
|
2108
|
+
});
|
|
2017
2109
|
});
|
|
2018
2110
|
|
|
2019
2111
|
// ---------------------------------------------------------------------------
|
package/src/CameraFeed.tsx
CHANGED
|
@@ -220,6 +220,25 @@ export interface CameraFeedProps {
|
|
|
220
220
|
* the gimbal + FoV). Default false.
|
|
221
221
|
*/
|
|
222
222
|
enableTracking?: boolean;
|
|
223
|
+
/**
|
|
224
|
+
* Stand down the built-in MANUAL camera controls: the pan ball and the
|
|
225
|
+
* zoom/FoV controls disappear, their in-flight rates are zeroed, and the
|
|
226
|
+
* imperative handle's pan/zoom methods become no-ops (so a physical
|
|
227
|
+
* controller wired to the handle can't reach the camera either).
|
|
228
|
+
*
|
|
229
|
+
* "Manual" in the sense this file already uses it - a human directly driving
|
|
230
|
+
* the gimbal and optics - and named for that intent rather than for today's
|
|
231
|
+
* specific surfaces, so anything added later that hand-flies the camera
|
|
232
|
+
* stands down under this flag too. Auto-track is NOT manual and stays live:
|
|
233
|
+
* the aim loop runs game-side, so it keeps working when a host takes the
|
|
234
|
+
* hand-flown path away. Feed affordances are untouched as well - action bar,
|
|
235
|
+
* recording, fullscreen, PiP, quality.
|
|
236
|
+
*
|
|
237
|
+
* For a host that owns manual control itself, e.g. a signal-delay setpoint
|
|
238
|
+
* surface where live pan/zoom would be steering at where the craft is now
|
|
239
|
+
* while watching where it was a light-time ago. Default false.
|
|
240
|
+
*/
|
|
241
|
+
disableManualControls?: boolean;
|
|
223
242
|
/**
|
|
224
243
|
* Show a built-in REC control in the action bar: a STATEFUL toggle (never
|
|
225
244
|
* overflow-eligible, per the #6 spec) that starts/stops a client-side
|
|
@@ -302,6 +321,7 @@ const CameraFeedInner = forwardRef<CameraFeedHandle, CameraFeedProps>(
|
|
|
302
321
|
enablePictureInPicture = false,
|
|
303
322
|
enableQualityControl = false,
|
|
304
323
|
enableTracking = false,
|
|
324
|
+
disableManualControls = false,
|
|
305
325
|
enableRecording = false,
|
|
306
326
|
recordFullResolution = false,
|
|
307
327
|
actions,
|
|
@@ -496,8 +516,12 @@ const CameraFeedInner = forwardRef<CameraFeedHandle, CameraFeedProps>(
|
|
|
496
516
|
}, []);
|
|
497
517
|
|
|
498
518
|
const isDestroyed = camera ? isCameraDestroyed(camera) : false;
|
|
499
|
-
|
|
500
|
-
|
|
519
|
+
/* Gating here rather than at the render sites covers the whole manual
|
|
520
|
+
path at once: the controls unmount, the effects below zero any in-flight
|
|
521
|
+
rate, and the imperative handle's pan/zoom methods no-op. */
|
|
522
|
+
const showPan = camera?.supportsPan && !isDestroyed && !disableManualControls;
|
|
523
|
+
const showZoom =
|
|
524
|
+
camera?.supportsZoom && !isDestroyed && !disableManualControls;
|
|
501
525
|
|
|
502
526
|
// Auto-track state is server-authoritative: driven purely by the published
|
|
503
527
|
// trackMode (absent -> none), never local click. While tracking, the aim
|