@ksp-gonogo/kerbcast 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 +229 -19
- package/dist/__generated__/types.d.ts +580 -14
- package/dist/__generated__/types.js +82 -1
- package/dist/anchors.d.ts +61 -0
- package/dist/anchors.js +71 -0
- package/dist/captureClip.d.ts +149 -0
- package/dist/captureClip.js +317 -0
- package/dist/client.d.ts +420 -27
- package/dist/client.js +1287 -58
- package/dist/commandTrack.d.ts +66 -6
- package/dist/commandTrack.js +137 -13
- package/dist/frameUt.d.ts +44 -10
- package/dist/frameUt.js +34 -4
- package/dist/index.d.ts +5 -3
- package/dist/index.js +3 -2
- package/dist/mp4.d.ts +125 -0
- package/dist/mp4.js +531 -0
- package/dist/panZoom.d.ts +1 -2
- package/dist/panZoom.js +1 -2
- package/dist/recording.d.ts +216 -16
- package/dist/recording.js +565 -50
- package/dist/testing/index.d.ts +41 -3
- package/dist/testing/index.js +97 -6
- package/package.json +1 -1
- package/src/__generated__/types.ts +579 -13
- package/src/anchors.test.ts +68 -0
- package/src/anchors.ts +110 -0
- package/src/captureClip.test.ts +631 -0
- package/src/captureClip.ts +379 -0
- package/src/client.test.ts +2681 -86
- package/src/client.ts +1499 -90
- package/src/commandTrack.test.ts +206 -10
- package/src/commandTrack.ts +160 -14
- package/src/framePipe.test.ts +79 -0
- package/src/frameUt.test.ts +15 -0
- package/src/frameUt.ts +72 -9
- package/src/index.ts +26 -0
- package/src/mp4.ts +678 -0
- package/src/panZoom.ts +1 -2
- package/src/recording.capture.test.ts +390 -0
- package/src/recording.test.ts +427 -11
- package/src/recording.ts +744 -66
- package/src/testing/index.ts +103 -8
- package/src/testing.test.ts +1 -1
package/README.md
CHANGED
|
@@ -102,13 +102,59 @@ static and frame lock, so anything it does (holding frames back, dropping
|
|
|
102
102
|
them) shows up in `cam.mediaStream` with the static still drawn on top.
|
|
103
103
|
|
|
104
104
|
Kerbcast installs the transform on whichever receiver carries the camera. A
|
|
105
|
-
transform instance works on one receiver only, so you pass a factory:
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
the
|
|
105
|
+
transform instance works on one receiver only, so you pass a factory: Kerbcast
|
|
106
|
+
calls it for a fresh instance when the camera is bound, again when a slot
|
|
107
|
+
rebind moves the camera to another receiver, and again after a reconnect. It
|
|
108
|
+
also calls it again when the camera's stream starts over on the receiver it
|
|
109
|
+
kept: back in the list after a scene change (a relaunch, say), back from
|
|
110
|
+
destroyed, or reattached in place without leaving the list (a vessel switch
|
|
111
|
+
that rebuilds its camera; needs a sidecar that sends `attach`). The new frames
|
|
112
|
+
come from a new encoder on a new capture timeline, so an instance holding
|
|
113
|
+
state from the old one is replaced, in one step with no frames passing
|
|
114
|
+
untransformed. A receiver the camera leaves goes back to passthrough, and
|
|
115
|
+
frames the old instance still holds are dropped. `setEncodedTransform(null)`
|
|
110
116
|
detaches. It works in both legacy and dynamic slot modes.
|
|
111
117
|
|
|
118
|
+
On the `"script"` path Chromium wires a script transform to its receiver
|
|
119
|
+
only as frames reach it, so on a receiver with no packets coming in (a
|
|
120
|
+
relaunch gap, a slot about to be fed) an instance built before it was handed
|
|
121
|
+
over can lose to the previous transform and receive nothing, for good.
|
|
122
|
+
The passthrough a receiver goes back to, or a transform given `holdMs` (see
|
|
123
|
+
below), is watched once packets arrive. One whose frames decode is left
|
|
124
|
+
alone. One that sees packets arrive with no frame decoded for 2 s past its
|
|
125
|
+
hold is renewed from the factory, which Chromium wires at once since packets
|
|
126
|
+
are flowing, and Kerbcast asks the sidecar for a keyframe after the swap. It
|
|
127
|
+
is renewed again, up to three more times, while packets keep arriving with no
|
|
128
|
+
frame decoded for 2 s, then 4 s, then 8 s past the hold. A transform with no
|
|
129
|
+
`holdMs` is renewed once when packets first arrive, and not checked after.
|
|
130
|
+
Build the transform inside the factory call
|
|
131
|
+
rather than ahead of it, and be ready for the factory to run again for the
|
|
132
|
+
same install. A factory that throws on a renewal leaves the previous instance in
|
|
133
|
+
place.
|
|
134
|
+
|
|
135
|
+
A transform that holds frames back, such as a delay, decodes nothing until
|
|
136
|
+
its hold has passed, so Kerbcast cannot tell it from one left without
|
|
137
|
+
frames. Give the hold, the longest a frame can sit inside the transform
|
|
138
|
+
including any pacing buffer, to have your transform checked the same way:
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
cam.setEncodedTransform(factory, { holdMs: () => currentDelayMs() });
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`holdMs` is a number or a function, read at each check, so it can follow a
|
|
145
|
+
changing delay. With it, the transform is renewed only when packets keep
|
|
146
|
+
arriving undecoded for the hold plus 2 s (then plus 4 s, then plus 8 s, at
|
|
147
|
+
most three more times), so a delay that is working keeps the frames it
|
|
148
|
+
holds. The longest hold read (a function is read every 2 s while the
|
|
149
|
+
transform is installed) also tells Kerbcast how long a replaced instance may
|
|
150
|
+
go on decoding what it held, so the check of whatever replaces it on that
|
|
151
|
+
receiver, after a relaunch too, waits that out before it trusts decoded
|
|
152
|
+
frames or renews. Without it, every install is renewed once when
|
|
153
|
+
packets first arrive on its receiver, and that renewal drops whatever the
|
|
154
|
+
first instance was holding and waits for a keyframe. Whatever replaces it on
|
|
155
|
+
that receiver, the passthrough included, is also renewed once when packets
|
|
156
|
+
first arrive.
|
|
157
|
+
|
|
112
158
|
It needs `encodedTransforms: true` when the client is constructed, and
|
|
113
159
|
`setEncodedTransform` throws without it. The option gives every receiver a
|
|
114
160
|
passthrough transform from the start, because Chrome only honours a receiver
|
|
@@ -199,7 +245,11 @@ and watches a craft from there, the way a tracking station would. The
|
|
|
199
245
|
game's settings give each scene one, the Launchpad camera, and a client can
|
|
200
246
|
create more at any vantage. Each appears in `client.cameras` with
|
|
201
247
|
`kind: "command"` and a `commandCentre` describing where it stands and what
|
|
202
|
-
it watches.
|
|
248
|
+
it watches. `commandCentre.siteKey` names the place the lens stands and
|
|
249
|
+
`siteKind` says what it is (`station`, `launchSite`, `surface` or `vessel`);
|
|
250
|
+
the key stays the same when the craft a `vessel` site rides docks, while
|
|
251
|
+
`vantagePersistentId` follows the craft it rides now. `stationId` is
|
|
252
|
+
deprecated.
|
|
203
253
|
|
|
204
254
|
A created camera lasts for the rest of the game scene (a flight, or the
|
|
205
255
|
Space Center while the game's command camera setting is on); leaving the
|
|
@@ -247,13 +297,26 @@ at the Space Center. `createCommandCamera` takes the same `aim` as an option.
|
|
|
247
297
|
The aim is one of:
|
|
248
298
|
|
|
249
299
|
- `vessel`: follows the craft with that `persistentId` (any entry of
|
|
250
|
-
`commandCentre.targets`, whatever its eligibility)
|
|
300
|
+
`commandCentre.targets`, whatever its eligibility). Where a world model
|
|
301
|
+
governs the feed, a craft whose position it cannot narrow enough for the
|
|
302
|
+
view to frame is refused as `tooUncertain`; add `allowUncertain: true` to
|
|
303
|
+
aim at it anyway, as a deliberate search of the region it may be in. Under
|
|
304
|
+
the game's own knowledge every known craft is accepted, and while a command
|
|
305
|
+
lock is held browser aims are refused whatever they allow
|
|
251
306
|
- `orbit`: follows a conic (`body`, `inclination`, `eccentricity`,
|
|
252
307
|
`semiMajorAxis`, `lan`, `argumentOfPeriapsis` in degrees and metres,
|
|
253
308
|
`meanAnomalyAtEpoch` in radians, `epoch`)
|
|
254
|
-
- `positionAt`: follows a
|
|
255
|
-
`velocity` as `[x, y, z]` relative to `body`
|
|
256
|
-
|
|
309
|
+
- `positionAt`: follows a craft coasting from a state you give: `position`
|
|
310
|
+
and `velocity` as `[x, y, z]` relative to `body` at `ut`, z up, in KSP's
|
|
311
|
+
fixed inertial frame, carried on ballistically under the body's gravity
|
|
312
|
+
with no thrust. From a KSP `Orbit` that frame is always
|
|
313
|
+
`Planetarium.Zup.LocalToWorld(orbit.getRelativePositionAtUT(ut))` (and the
|
|
314
|
+
same for `getOrbitalVelocityAtUT`, `orbit.pos`, `orbit.vel`); the raw
|
|
315
|
+
values are not, since KSP leaves Zup turned after a low flight. A browser
|
|
316
|
+
client has no `Orbit`, so it takes these vectors from whatever mod or model
|
|
317
|
+
hands it craft states in that frame. A velocity of zero no longer holds a
|
|
318
|
+
point still (it falls); use `surface` for a fixed point. Before 1.12 this
|
|
319
|
+
was Zup's own frame, carried on linearly
|
|
257
320
|
- `surface`: holds a point on a `body` (`latitude`, `longitude`, `altitude`
|
|
258
321
|
in metres above sea level), turning with the planet
|
|
259
322
|
- `direction`: holds a direction from the camera's local horizon,
|
|
@@ -377,9 +440,9 @@ before or during the lock are not restored.
|
|
|
377
440
|
|
|
378
441
|
The lock holds until its holder releases it (or the scene changes); it never
|
|
379
442
|
times out. A holder that stops calling for 5 s blanks every command feed
|
|
380
|
-
("
|
|
381
|
-
still refused. This is deliberate: a holder that delays commands (
|
|
382
|
-
applies signal delay) can not be bypassed while it is unresponsive.
|
|
443
|
+
("Authority not responding") but keeps the lock, so aims from anyone else are
|
|
444
|
+
still refused. This is deliberate: a holder that delays commands (one that
|
|
445
|
+
applies signal delay, say) can not be bypassed while it is unresponsive.
|
|
383
446
|
|
|
384
447
|
### In game (C#)
|
|
385
448
|
|
|
@@ -406,15 +469,15 @@ Holding the lock:
|
|
|
406
469
|
|
|
407
470
|
```csharp
|
|
408
471
|
// Re-take it on every scene load; a different owner is refused.
|
|
409
|
-
KerbcastControl.SetCommandAuthority("
|
|
472
|
+
KerbcastControl.SetCommandAuthority("mission-control");
|
|
410
473
|
|
|
411
474
|
// Only calls passing the owner are accepted now, and each is a heartbeat.
|
|
412
475
|
KerbcastControl.SetCommandAim(pad, CommandAim.PositionAt("Kerbin", ut, pos, vel,
|
|
413
|
-
subject: craft.persistentId, framingMarginM: uncertaintyM), "
|
|
476
|
+
subject: craft.persistentId, framingMarginM: uncertaintyM), "mission-control");
|
|
414
477
|
KerbcastControl.SetCommandAim(pad, CommandAim.Vessel(other)); // false: locked
|
|
415
478
|
|
|
416
479
|
// Every command camera returns to its default.
|
|
417
|
-
KerbcastControl.ClearCommandAuthority("
|
|
480
|
+
KerbcastControl.ClearCommandAuthority("mission-control");
|
|
418
481
|
```
|
|
419
482
|
|
|
420
483
|
- `CommandVantage.LaunchSite(site, distance, bearing, height)`,
|
|
@@ -438,16 +501,163 @@ KerbcastControl.ClearCommandAuthority("gonogo");
|
|
|
438
501
|
- The lock holds until `ClearCommandAuthority` or a scene change, and fails
|
|
439
502
|
closed: a holder that makes no call (`SetCommandAuthority`, `SetCommandAim`
|
|
440
503
|
with its owner, `SetCommandBlank`) for 5 s of real time blanks every
|
|
441
|
-
command feed "
|
|
504
|
+
command feed "Authority not responding" and keeps the lock, so everyone else
|
|
442
505
|
stays refused. `SetCommandBlank(flightId, reason)` blanks one feed
|
|
443
506
|
- `DestroyCommandCamera(flightId)` returns false for the settings camera or
|
|
444
507
|
an unknown id
|
|
445
|
-
- `CommandFeeds()` lists every built command camera, with its `
|
|
446
|
-
`
|
|
508
|
+
- `CommandFeeds()` lists every built command camera, with its `SiteKey`,
|
|
509
|
+
`SiteKind`, `VantagePersistentId`, `Aim`, `SubjectPersistentId`, and
|
|
510
|
+
`Created` set on the ones made at runtime (`StationId` is obsolete);
|
|
447
511
|
`CommandTargets(flightId)` lists the craft it could follow (a craft docked
|
|
448
512
|
into another drops out; the combined craft keeps the survivor's
|
|
449
513
|
`persistentId`)
|
|
450
514
|
|
|
515
|
+
## Anchored markers
|
|
516
|
+
|
|
517
|
+
Anchors mark things in the game on a camera's picture. The game projects
|
|
518
|
+
each one through the camera that rendered every frame (its pose and field
|
|
519
|
+
of view at that moment, so zoom and pan are exact), and the result rides
|
|
520
|
+
that frame's stamp. Markers drawn from it line up with the picture under
|
|
521
|
+
any stream delay. There is no live position: everything is tied to frames.
|
|
522
|
+
|
|
523
|
+
```ts
|
|
524
|
+
import { AnchorDirection, AnchorKind } from "@ksp-gonogo/kerbcast";
|
|
525
|
+
|
|
526
|
+
await client.setAnchors(flightId, [
|
|
527
|
+
{ id: "port", kind: AnchorKind.Part, vessel: targetPid, part: portPid, orientation: true },
|
|
528
|
+
{ id: "ship", kind: AnchorKind.Vessel, vessel: targetPid, occlusion: true },
|
|
529
|
+
{ id: "site", kind: AnchorKind.Surface, body: "Mun", latitude: 2.4, longitude: 81.5 },
|
|
530
|
+
{ id: "pro", kind: AnchorKind.Direction, direction: AnchorDirection.TargetRelativeVelocity },
|
|
531
|
+
]);
|
|
532
|
+
await client.setAnchors(flightId, null); // clear
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
- Kinds: `part` (craft and part persistentId, optional `offset` in the
|
|
536
|
+
part's axes; a docking port is marked at its docking face; follows the
|
|
537
|
+
part through docking), `vessel` (centre of mass; follows a craft merged
|
|
538
|
+
into another), `surface` (body, latitude, longitude, altitude) and
|
|
539
|
+
`direction` (a named direction of a craft, the active one by default, or
|
|
540
|
+
a fixed inertial `vector`), drawn at infinity, and `kinematic` (a point
|
|
541
|
+
you describe with `kinematics`: a `conic` from KSP Orbit elements, a
|
|
542
|
+
`stateVector` coasting ballistically in the same fixed frame as a
|
|
543
|
+
`positionAt` aim, or a `surface` point, each about a named `body`), for a
|
|
544
|
+
last-known ghost, a hypothesis or a predicted point
|
|
545
|
+
- `orientation: true` on a part or vessel adds its `forward` and `up` axes
|
|
546
|
+
in camera space (x right, y up, z forward) to every result.
|
|
547
|
+
`occlusion: true` adds `occluded`, at the cost of a ray cast per frame.
|
|
548
|
+
`marginM` pads the radius `apparentRadiusPx` is worked out from
|
|
549
|
+
- Anchors belong to the client that set them: other clients never see them,
|
|
550
|
+
the command lock does not apply, and the sidecar drops them when the
|
|
551
|
+
client disconnects. The client keeps them until cleared and sends them
|
|
552
|
+
again on reconnect; anchors on a camera the sidecar does not list wait
|
|
553
|
+
until it appears. At most `client.maxAnchorsPerCamera` per camera, which
|
|
554
|
+
the sidecar announces in `hello` (16 when it does not say,
|
|
555
|
+
`MAX_ANCHORS_PER_CAMERA`)
|
|
556
|
+
- Needs a 1.12 sidecar and plugin; against an older sidecar they are kept
|
|
557
|
+
but never sent (`client.anchorsSupported`)
|
|
558
|
+
|
|
559
|
+
Each result lands on the displayed frame through `watchDisplayedFrames`:
|
|
560
|
+
|
|
561
|
+
```ts
|
|
562
|
+
import { anchorPoint, fitFrame, frameToScreen, frameLengthToScreen, findAnchor } from "@ksp-gonogo/kerbcast";
|
|
563
|
+
|
|
564
|
+
client.watchDisplayedFrames(flightId, video, (frame) => {
|
|
565
|
+
const port = findAnchor(frame.anchors, "port");
|
|
566
|
+
const point = port && anchorPoint(port); // null when the result carries no position
|
|
567
|
+
if (!point || !frame.frameWidth || !frame.frameHeight) return;
|
|
568
|
+
const fitted = fitFrame(video.clientWidth, video.clientHeight,
|
|
569
|
+
frame.frameWidth, frame.frameHeight, "contain"); // or "cover"
|
|
570
|
+
const { x, y } = frameToScreen(point, frame.frameWidth, frame.frameHeight, fitted);
|
|
571
|
+
const r = frameLengthToScreen(port.apparentRadiusPx ?? 0, frame.frameHeight, fitted);
|
|
572
|
+
draw(x, y, r, port.inFrame, port.behind);
|
|
573
|
+
});
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
Each result has `x`, `y` (capture pixels from the top left), `inFrame`,
|
|
577
|
+
`behind` (then `x`, `y` sit off the frame on the side it is nearest),
|
|
578
|
+
`distanceM` (absent for directions), `apparentRadiusPx`, `angleOffAxis`
|
|
579
|
+
(degrees), and `forward`, `up`, `occluded` when asked for. Anchors set in
|
|
580
|
+
game carry their `owner`; this client's own have none. Every stamp also
|
|
581
|
+
gives the frame's `fov` and size, for overlays that do their own maths.
|
|
582
|
+
|
|
583
|
+
An `observed: true` result carries only what the picture shows: `x`, `y`,
|
|
584
|
+
`apparentRadiusPx` (and `forward`, `up`) only while the anchor is in frame and
|
|
585
|
+
not occluded, and never `distanceM`, `angleOffAxis` or `behind`. Out of view it
|
|
586
|
+
is just `inFrame` and `occluded`. Use `anchorPoint(result)`, which is `null`
|
|
587
|
+
when there is no position.
|
|
588
|
+
|
|
589
|
+
A command camera adds its own anchor: `id: "subject"`, owner `"kerbcast"`
|
|
590
|
+
(`COMMAND_SUBJECT_ANCHOR_ID`, `KERBCAST_ANCHOR_OWNER`), a `vessel` anchor on
|
|
591
|
+
the craft it watches with occlusion on. It is always observed, so it never
|
|
592
|
+
gives away a craft the picture does not show. The React brackets follow it.
|
|
593
|
+
A second, `id: "aim"` (`COMMAND_AIM_ANCHOR_ID`), is where the aim point lands
|
|
594
|
+
in the frame: the centre, except while the camera turns onto a moved aim.
|
|
595
|
+
|
|
596
|
+
While a command authority holds the lock, every browser client's anchors
|
|
597
|
+
are observed-only by default. The holder can relax that
|
|
598
|
+
(`KerbcastControl.SetAnchorPolicy(AnchorPolicy.Full, owner)`, for every
|
|
599
|
+
camera or one) and can withhold the subject anchor
|
|
600
|
+
(`KerbcastControl.SetSubjectAnchor(false, owner)`). Both return to the
|
|
601
|
+
unrestricted behaviour when the lock clears. A `kinematic` anchor is never
|
|
602
|
+
observed: it marks what you supplied, not what the game shows. Where it would
|
|
603
|
+
be observed, though, its result still withholds the lens's pose: `x`, `y` and
|
|
604
|
+
`apparentRadiusPx` only while `inFrame`, and never `distanceM`, `behind` or
|
|
605
|
+
`angleOffAxis`. Under the lock
|
|
606
|
+
`commandTrack.trueOffsetPx` is likewise sent only while the true craft is in
|
|
607
|
+
frame and not occluded.
|
|
608
|
+
|
|
609
|
+
### State that rides the frame
|
|
610
|
+
|
|
611
|
+
From 1.12 every frame's stamp carries what that frame shows, so a view that
|
|
612
|
+
delays the picture never learns it sooner.
|
|
613
|
+
|
|
614
|
+
A light speed comes only from the game: the mod holding the command lock
|
|
615
|
+
sets it through Kerbcast.API. Without one the picture is live, so a command
|
|
616
|
+
camera's `subjectUt` equals the frame's `ut`.
|
|
617
|
+
|
|
618
|
+
- `liveLightTimeS` on every frame of every camera: the light time covering
|
|
619
|
+
what of the lens's own craft and site the frame draws at its `ut`. It is
|
|
620
|
+
0 with no light speed. A delayed view releases the frame no sooner than
|
|
621
|
+
its `ut` plus this plus its own delay from the lens. Absent means unknown
|
|
622
|
+
(the game could not read it, or it is older than 1.12): under a light
|
|
623
|
+
speed, hold the frame. Other loaded craft near the lens are still drawn at
|
|
624
|
+
`ut` and are not covered yet; once craft beyond the boundary are hidden,
|
|
625
|
+
the bound covers everything drawn at `ut`
|
|
626
|
+
- `boundaryM`: how far light travels in one 60 fps frame at the light
|
|
627
|
+
speed, the picture's boundary distance. Absent with no light speed
|
|
628
|
+
- `command` on a command camera's frames: its fix, range, bracket,
|
|
629
|
+
occlusion, sunlight, blank reason, lock owner, subject, the subject's
|
|
630
|
+
refusal (`unknown`, `notPositioned`, `refused`, `stale`) and which world
|
|
631
|
+
model snapshot it read
|
|
632
|
+
|
|
633
|
+
All three arrive on `DisplayedFrameUt` through `watchDisplayedFrames`.
|
|
634
|
+
`effectiveCommandTrack(track, displayUt, frame.command)` uses a frame's own
|
|
635
|
+
state whole; the React feed does this for its brackets and readout.
|
|
636
|
+
|
|
637
|
+
A host with its own delayed playout keeps the `DisplayedFrameUt` it got for
|
|
638
|
+
each frame and hands the one it is showing to the React feed as
|
|
639
|
+
`displayFrame` (beside, and winning over, `displayUt`): its UT picks the
|
|
640
|
+
transition in force and its `command` draws the brackets and readout. A UT
|
|
641
|
+
alone carries no frame state, so under a lock `displayUt` by itself shows no
|
|
642
|
+
fix. Without either, the feed uses the frame the browser presents, or the
|
|
643
|
+
newest stamped frame before it, which lags the picture and so is never early.
|
|
644
|
+
|
|
645
|
+
While a command lock is held, the camera list's `commandTrack` keeps only its
|
|
646
|
+
`ut`, `authority` and `events` (each replayed at its own UT); the other fields
|
|
647
|
+
read as their defaults, which a 1.11 client shows as no fix. The targets list
|
|
648
|
+
in `commandCentre` carries the `ut` it describes:
|
|
649
|
+
`client.commandCentreAt(flightId, displayUt)` gives the list as it stood at
|
|
650
|
+
the displayed frame, and `null` while every list sent is newer. With no
|
|
651
|
+
displayed UT it gives the newest, except under a lock, where it gives `null`.
|
|
652
|
+
Lists are kept back to two hours of UT before the newest, at most 600 per
|
|
653
|
+
camera (a UT older than every list kept finds none), and are forgotten when
|
|
654
|
+
the camera leaves the list or the client reconnects.
|
|
655
|
+
|
|
656
|
+
In game, `KerbcastControl.SetAnchors(flightId, anchors, owner)` does the same
|
|
657
|
+
with `AnchorSpec.ForPart`, `ForVessel`, `ForSurface`, `ForDirection` and
|
|
658
|
+
`ForVector`. Those results go to every client that asked for anchors, tagged
|
|
659
|
+
with `owner`, until replaced, `ClearAnchors(owner)` or a scene change.
|
|
660
|
+
|
|
451
661
|
## Pan/zoom input helper
|
|
452
662
|
|
|
453
663
|
Driving pan and zoom from a gamepad stick, nudge buttons, or an FoV
|