entangled-worlds-orbiters-shared 2.7.0 → 2.8.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.
@@ -14,6 +14,16 @@ export interface SyncSettingsLike {
14
14
  sourceTimeSec?: unknown;
15
15
  } | null | undefined> | null;
16
16
  gridStartTimeSec?: unknown;
17
+ /** Whether this track time-stretches onto the grid. A BOOLEAN, so `false` is an answer rather than
18
+ * an absence — see `resolveWarpEnabledFromTrackData`. */
19
+ warpEnabled?: unknown;
20
+ /** Whether this track plays looped. Same boolean rule as `warpEnabled`, and the same three states:
21
+ * on, off, and nobody-said — see `resolveLoopEnabledFromTrackData`. */
22
+ loopEnabled?: unknown;
23
+ /** The loop region in SECONDS (storage's time unit; the audio engine works in ms and converts at
24
+ * its edge). Only ever meaningful as a pair — see `resolveLoopRangeSecFromTrackData`. */
25
+ loopStartSec?: unknown;
26
+ loopEndSec?: unknown;
17
27
  }
18
28
  export interface TrackMetadataLike {
19
29
  trackBpm?: unknown;
@@ -71,3 +81,33 @@ export declare function resolveTrackBpmFromTrackData(trackData?: TrackDataLike |
71
81
  * Returns null when nothing resolves (consumers default to 0).
72
82
  */
73
83
  export declare function resolvePrimaryGridMarkerTimeSecFromTrackData(trackData?: TrackDataLike | null): number | null;
84
+ /**
85
+ * Resolve whether this track should warp (time-stretch onto the grid) for the viewer.
86
+ *
87
+ * Returns null when nothing anywhere expressed a preference. Null means "no opinion", NOT "off" —
88
+ * the caller supplies its own default, because only the caller knows its runtime constraints (in
89
+ * orbiters, a deck with no tempo cannot warp at all, whatever was saved).
90
+ */
91
+ export declare function resolveWarpEnabledFromTrackData(trackData?: TrackDataLike | null): boolean | null;
92
+ /**
93
+ * Resolve whether this track should play looped for the viewer.
94
+ *
95
+ * Same three states as warp — on, off, nobody-said — but no smart default anywhere behind it: the
96
+ * server stops at the owner's setting, so null here is genuinely common and simply means the caller
97
+ * keeps whatever looping default it already had.
98
+ */
99
+ export declare function resolveLoopEnabledFromTrackData(trackData?: TrackDataLike | null): boolean | null;
100
+ /**
101
+ * Resolve the saved loop region, in seconds, or null when no layer holds a complete one.
102
+ *
103
+ * Resolved as a PAIR, never edge by edge: a start borrowed from one layer and an end from another
104
+ * describes a loop nobody set, and can invert. `start` of 0 is a real value, so the guard is a
105
+ * finiteness test rather than a truthiness one.
106
+ *
107
+ * The caller must still clamp this against the audio's real duration before using it — a region
108
+ * saved against an earlier version of the track can outlive the audio it was drawn on.
109
+ */
110
+ export declare function resolveLoopRangeSecFromTrackData(trackData?: TrackDataLike | null): {
111
+ startSec: number;
112
+ endSec: number;
113
+ } | null;
@@ -20,6 +20,22 @@ function firstResolvedValue(candidates, parser) {
20
20
  }
21
21
  return null;
22
22
  }
23
+ /**
24
+ * The first candidate that is an actual boolean, or null when none of them is.
25
+ *
26
+ * Deliberately separate from `firstResolvedValue`: that one folds "absent" and "invalid" together
27
+ * and returns a number, which is exactly the wrong shape here. A warp preference has THREE states —
28
+ * on, off, and nobody-said — and `false` is the middle one, not the last. Any `||`, any truthiness
29
+ * test, any `?? ` against a default collapses off into nobody-said and loses the user's choice.
30
+ */
31
+ function firstBoolean(candidates) {
32
+ for (const candidate of candidates) {
33
+ if (typeof candidate === 'boolean') {
34
+ return candidate;
35
+ }
36
+ }
37
+ return null;
38
+ }
23
39
  /**
24
40
  * Resolve the effective track BPM. Precedence (first finite, > 0 wins):
25
41
  * owner `trackBpm` → owner `effectiveSettings.sync.trackBpm` → per-user `trackUserSettings.sync.trackBpm`
@@ -64,3 +80,71 @@ export function resolvePrimaryGridMarkerTimeSecFromTrackData(trackData = null) {
64
80
  trackData?.metadata?.wrap?.gridStartTimeSec,
65
81
  ], toNonNegativeNumber);
66
82
  }
83
+ /**
84
+ * The shared walk behind the boolean preferences below.
85
+ *
86
+ * The server already applied the real precedence — the viewer's own saved setting, then the owner's,
87
+ * then (for warp) a smart default — and publishes the answer at `effectiveSettings.sync`. So that is
88
+ * read FIRST: it is the resolved value, not a competing opinion. The per-user path below it covers
89
+ * the shapes where the server has not resolved anything (a raw `trackUserSettings` fetch that has not
90
+ * been merged into a track payload yet).
91
+ *
92
+ * One walk, not one per field: warp and loop differ in what they MEAN, never in where they are found,
93
+ * and two hand-maintained copies of this list would drift the moment a payload shape changed.
94
+ */
95
+ function resolveSyncFlagFromTrackData(trackData, field) {
96
+ return firstBoolean([
97
+ trackData?.track?.metadata?.effectiveSettings?.sync?.[field],
98
+ trackData?.release?.metadata?.effectiveSettings?.sync?.[field],
99
+ trackData?.metadata?.effectiveSettings?.sync?.[field],
100
+ trackData?.effectiveSettings?.sync?.[field],
101
+ trackData?.trackUserSettings?.sync?.[field],
102
+ ]);
103
+ }
104
+ /**
105
+ * Resolve whether this track should warp (time-stretch onto the grid) for the viewer.
106
+ *
107
+ * Returns null when nothing anywhere expressed a preference. Null means "no opinion", NOT "off" —
108
+ * the caller supplies its own default, because only the caller knows its runtime constraints (in
109
+ * orbiters, a deck with no tempo cannot warp at all, whatever was saved).
110
+ */
111
+ export function resolveWarpEnabledFromTrackData(trackData = null) {
112
+ return resolveSyncFlagFromTrackData(trackData, 'warpEnabled');
113
+ }
114
+ /**
115
+ * Resolve whether this track should play looped for the viewer.
116
+ *
117
+ * Same three states as warp — on, off, nobody-said — but no smart default anywhere behind it: the
118
+ * server stops at the owner's setting, so null here is genuinely common and simply means the caller
119
+ * keeps whatever looping default it already had.
120
+ */
121
+ export function resolveLoopEnabledFromTrackData(trackData = null) {
122
+ return resolveSyncFlagFromTrackData(trackData, 'loopEnabled');
123
+ }
124
+ /**
125
+ * Resolve the saved loop region, in seconds, or null when no layer holds a complete one.
126
+ *
127
+ * Resolved as a PAIR, never edge by edge: a start borrowed from one layer and an end from another
128
+ * describes a loop nobody set, and can invert. `start` of 0 is a real value, so the guard is a
129
+ * finiteness test rather than a truthiness one.
130
+ *
131
+ * The caller must still clamp this against the audio's real duration before using it — a region
132
+ * saved against an earlier version of the track can outlive the audio it was drawn on.
133
+ */
134
+ export function resolveLoopRangeSecFromTrackData(trackData = null) {
135
+ const candidates = [
136
+ trackData?.track?.metadata?.effectiveSettings?.sync,
137
+ trackData?.release?.metadata?.effectiveSettings?.sync,
138
+ trackData?.metadata?.effectiveSettings?.sync,
139
+ trackData?.effectiveSettings?.sync,
140
+ trackData?.trackUserSettings?.sync,
141
+ ];
142
+ for (const candidate of candidates) {
143
+ const startSec = toNonNegativeNumber(candidate?.loopStartSec);
144
+ const endSec = toNonNegativeNumber(candidate?.loopEndSec);
145
+ if (startSec !== null && endSec !== null && endSec > startSec) {
146
+ return { startSec, endSec };
147
+ }
148
+ }
149
+ return null;
150
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "entangled-worlds-orbiters-shared",
3
- "version": "2.7.0",
3
+ "version": "2.8.0",
4
4
  "description": "Shared library for entangled worlds orbiters - contains core world rendering, moons system, and UI components",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -18,6 +18,16 @@ export interface SyncSettingsLike {
18
18
  primaryGridMarkerTimeSec?: unknown;
19
19
  gridMarkers?: ReadonlyArray<{ sourceTimeSec?: unknown } | null | undefined> | null;
20
20
  gridStartTimeSec?: unknown;
21
+ /** Whether this track time-stretches onto the grid. A BOOLEAN, so `false` is an answer rather than
22
+ * an absence — see `resolveWarpEnabledFromTrackData`. */
23
+ warpEnabled?: unknown;
24
+ /** Whether this track plays looped. Same boolean rule as `warpEnabled`, and the same three states:
25
+ * on, off, and nobody-said — see `resolveLoopEnabledFromTrackData`. */
26
+ loopEnabled?: unknown;
27
+ /** The loop region in SECONDS (storage's time unit; the audio engine works in ms and converts at
28
+ * its edge). Only ever meaningful as a pair — see `resolveLoopRangeSecFromTrackData`. */
29
+ loopStartSec?: unknown;
30
+ loopEndSec?: unknown;
21
31
  }
22
32
 
23
33
  export interface TrackMetadataLike {
@@ -72,6 +82,23 @@ function firstResolvedValue(
72
82
  return null;
73
83
  }
74
84
 
85
+ /**
86
+ * The first candidate that is an actual boolean, or null when none of them is.
87
+ *
88
+ * Deliberately separate from `firstResolvedValue`: that one folds "absent" and "invalid" together
89
+ * and returns a number, which is exactly the wrong shape here. A warp preference has THREE states —
90
+ * on, off, and nobody-said — and `false` is the middle one, not the last. Any `||`, any truthiness
91
+ * test, any `?? ` against a default collapses off into nobody-said and loses the user's choice.
92
+ */
93
+ function firstBoolean(candidates: ReadonlyArray<unknown>): boolean | null {
94
+ for (const candidate of candidates) {
95
+ if (typeof candidate === 'boolean') {
96
+ return candidate;
97
+ }
98
+ }
99
+ return null;
100
+ }
101
+
75
102
  /**
76
103
  * Resolve the effective track BPM. Precedence (first finite, > 0 wins):
77
104
  * owner `trackBpm` → owner `effectiveSettings.sync.trackBpm` → per-user `trackUserSettings.sync.trackBpm`
@@ -125,3 +152,85 @@ export function resolvePrimaryGridMarkerTimeSecFromTrackData(
125
152
  toNonNegativeNumber,
126
153
  );
127
154
  }
155
+
156
+ /**
157
+ * The shared walk behind the boolean preferences below.
158
+ *
159
+ * The server already applied the real precedence — the viewer's own saved setting, then the owner's,
160
+ * then (for warp) a smart default — and publishes the answer at `effectiveSettings.sync`. So that is
161
+ * read FIRST: it is the resolved value, not a competing opinion. The per-user path below it covers
162
+ * the shapes where the server has not resolved anything (a raw `trackUserSettings` fetch that has not
163
+ * been merged into a track payload yet).
164
+ *
165
+ * One walk, not one per field: warp and loop differ in what they MEAN, never in where they are found,
166
+ * and two hand-maintained copies of this list would drift the moment a payload shape changed.
167
+ */
168
+ function resolveSyncFlagFromTrackData(
169
+ trackData: TrackDataLike | null,
170
+ field: 'warpEnabled' | 'loopEnabled',
171
+ ): boolean | null {
172
+ return firstBoolean([
173
+ trackData?.track?.metadata?.effectiveSettings?.sync?.[field],
174
+ trackData?.release?.metadata?.effectiveSettings?.sync?.[field],
175
+ trackData?.metadata?.effectiveSettings?.sync?.[field],
176
+ trackData?.effectiveSettings?.sync?.[field],
177
+ trackData?.trackUserSettings?.sync?.[field],
178
+ ]);
179
+ }
180
+
181
+ /**
182
+ * Resolve whether this track should warp (time-stretch onto the grid) for the viewer.
183
+ *
184
+ * Returns null when nothing anywhere expressed a preference. Null means "no opinion", NOT "off" —
185
+ * the caller supplies its own default, because only the caller knows its runtime constraints (in
186
+ * orbiters, a deck with no tempo cannot warp at all, whatever was saved).
187
+ */
188
+ export function resolveWarpEnabledFromTrackData(
189
+ trackData: TrackDataLike | null = null,
190
+ ): boolean | null {
191
+ return resolveSyncFlagFromTrackData(trackData, 'warpEnabled');
192
+ }
193
+
194
+ /**
195
+ * Resolve whether this track should play looped for the viewer.
196
+ *
197
+ * Same three states as warp — on, off, nobody-said — but no smart default anywhere behind it: the
198
+ * server stops at the owner's setting, so null here is genuinely common and simply means the caller
199
+ * keeps whatever looping default it already had.
200
+ */
201
+ export function resolveLoopEnabledFromTrackData(
202
+ trackData: TrackDataLike | null = null,
203
+ ): boolean | null {
204
+ return resolveSyncFlagFromTrackData(trackData, 'loopEnabled');
205
+ }
206
+
207
+ /**
208
+ * Resolve the saved loop region, in seconds, or null when no layer holds a complete one.
209
+ *
210
+ * Resolved as a PAIR, never edge by edge: a start borrowed from one layer and an end from another
211
+ * describes a loop nobody set, and can invert. `start` of 0 is a real value, so the guard is a
212
+ * finiteness test rather than a truthiness one.
213
+ *
214
+ * The caller must still clamp this against the audio's real duration before using it — a region
215
+ * saved against an earlier version of the track can outlive the audio it was drawn on.
216
+ */
217
+ export function resolveLoopRangeSecFromTrackData(
218
+ trackData: TrackDataLike | null = null,
219
+ ): { startSec: number; endSec: number } | null {
220
+ const candidates: Array<SyncSettingsLike | null | undefined> = [
221
+ trackData?.track?.metadata?.effectiveSettings?.sync,
222
+ trackData?.release?.metadata?.effectiveSettings?.sync,
223
+ trackData?.metadata?.effectiveSettings?.sync,
224
+ trackData?.effectiveSettings?.sync,
225
+ trackData?.trackUserSettings?.sync,
226
+ ];
227
+
228
+ for (const candidate of candidates) {
229
+ const startSec = toNonNegativeNumber(candidate?.loopStartSec);
230
+ const endSec = toNonNegativeNumber(candidate?.loopEndSec);
231
+ if (startSec !== null && endSec !== null && endSec > startSec) {
232
+ return { startSec, endSec };
233
+ }
234
+ }
235
+ return null;
236
+ }