@sudobility/music_spatial_core 0.0.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 ADDED
@@ -0,0 +1,9 @@
1
+ # @sudobility/music_spatial_core
2
+
3
+ Platform-agnostic engine behind the Moosiac "Spatial" 3D view: instrument
4
+ geometry, scene composition, camera/listener math, movement stepping, and
5
+ map-overlay math — shared, byte-for-byte, by `@sudobility/music_spatial`
6
+ (web) and `@sudobility/music_spatial_rn` (React Native). Depends on `three`
7
+ and `react` core only — never `react-dom` or `react-native` — so the same
8
+ build runs unmodified on both. Not yet implemented — see
9
+ [`docs/SPEC.md`](docs/SPEC.md).
@@ -0,0 +1,39 @@
1
+ /**
2
+ * One colour per track, from its name — the same colour on the map marker
3
+ * and on the instrument's lines in the 3D view, so "which one is the
4
+ * cello" is answered the same way in both.
5
+ *
6
+ * The hue is a hash of the track's *name* (stable across sessions and
7
+ * devices, no state to store), then pushed apart from any other track's
8
+ * hue within the score so two "Violin" tracks — or two names that happen
9
+ * to hash close — still come out distinguishable. Saturation/lightness are
10
+ * fixed for the dark stage: a full-strength but not-quite-white "lit"
11
+ * colour while the part sounds, and a dimmer "base" while it rests.
12
+ */
13
+ export interface TrackColor {
14
+ /** 0–360. */
15
+ readonly hue: number;
16
+ /** CSS colour for a resting instrument. */
17
+ readonly base: string;
18
+ /** CSS colour for a sounding one. */
19
+ readonly lit: string;
20
+ }
21
+ /** FNV-1a over the UTF-16 code units, then spread over the hue circle. */
22
+ export declare function hueFromName(name: string): number;
23
+ /**
24
+ * Resting: mostly grey with a tint of the track's hue — enough to tell
25
+ * which instrument is which, not enough to compete with the ones sounding.
26
+ * Sounding: the hue at full strength.
27
+ */
28
+ export declare function colorFromHue(hue: number): TrackColor;
29
+ /**
30
+ * Colours for every track in a score, keyed by id. Hues start from each
31
+ * name's hash and are then separated: going round the circle, any hue
32
+ * closer than `MIN_GAP` to the previous one is pushed forward. With more
33
+ * than 360/MIN_GAP tracks the gap shrinks to what fits.
34
+ */
35
+ export declare function trackColors(tracks: readonly {
36
+ readonly id: string;
37
+ readonly name: string;
38
+ }[]): Readonly<Record<string, TrackColor>>;
39
+ //# sourceMappingURL=colors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"colors.d.ts","sourceRoot":"","sources":["../src/colors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,MAAM,WAAW,UAAU;IACzB,aAAa;IACb,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,2CAA2C;IAC3C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,qCAAqC;IACrC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED,0EAA0E;AAC1E,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAQhD;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAOpD;AAKD;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,MAAM,EAAE,SAAS;IAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAAE,GAChE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CAmBtC"}
package/dist/colors.js ADDED
@@ -0,0 +1,67 @@
1
+ /**
2
+ * One colour per track, from its name — the same colour on the map marker
3
+ * and on the instrument's lines in the 3D view, so "which one is the
4
+ * cello" is answered the same way in both.
5
+ *
6
+ * The hue is a hash of the track's *name* (stable across sessions and
7
+ * devices, no state to store), then pushed apart from any other track's
8
+ * hue within the score so two "Violin" tracks — or two names that happen
9
+ * to hash close — still come out distinguishable. Saturation/lightness are
10
+ * fixed for the dark stage: a full-strength but not-quite-white "lit"
11
+ * colour while the part sounds, and a dimmer "base" while it rests.
12
+ */
13
+ /** FNV-1a over the UTF-16 code units, then spread over the hue circle. */
14
+ export function hueFromName(name) {
15
+ let h = 0x811c9dc5;
16
+ for (let i = 0; i < name.length; i++) {
17
+ h ^= name.charCodeAt(i);
18
+ h = Math.imul(h, 0x01000193) >>> 0;
19
+ }
20
+ // The golden-angle stretch turns similar hashes into well-separated hues.
21
+ return (h * 137.508) % 360;
22
+ }
23
+ /**
24
+ * Resting: mostly grey with a tint of the track's hue — enough to tell
25
+ * which instrument is which, not enough to compete with the ones sounding.
26
+ * Sounding: the hue at full strength.
27
+ */
28
+ export function colorFromHue(hue) {
29
+ const h = ((hue % 360) + 360) % 360;
30
+ return {
31
+ hue: h,
32
+ base: `hsl(${h.toFixed(0)}, 10%, 50%)`,
33
+ lit: `hsl(${h.toFixed(0)}, 100%, 70%)`,
34
+ };
35
+ }
36
+ /** The smallest hue gap two tracks are allowed, in degrees. */
37
+ const MIN_GAP = 28;
38
+ /**
39
+ * Colours for every track in a score, keyed by id. Hues start from each
40
+ * name's hash and are then separated: going round the circle, any hue
41
+ * closer than `MIN_GAP` to the previous one is pushed forward. With more
42
+ * than 360/MIN_GAP tracks the gap shrinks to what fits.
43
+ */
44
+ export function trackColors(tracks) {
45
+ const out = {};
46
+ if (tracks.length === 0)
47
+ return out;
48
+ const gap = Math.min(MIN_GAP, 360 / tracks.length);
49
+ const order = tracks
50
+ .map((track, index) => ({ track, index, hue: hueFromName(track.name) }))
51
+ .sort((a, b) => a.hue - b.hue || a.index - b.index);
52
+ // Two passes round the circle: the second catches the wrap-around
53
+ // between the last and the first.
54
+ for (let pass = 0; pass < 2; pass++) {
55
+ for (let i = 0; i < order.length; i++) {
56
+ const prev = order[(i - 1 + order.length) % order.length];
57
+ const cur = order[i];
58
+ const prevHue = i === 0 ? prev.hue - 360 : prev.hue;
59
+ if (cur.hue - prevHue < gap)
60
+ cur.hue = prevHue + gap;
61
+ }
62
+ }
63
+ for (const { track, hue } of order)
64
+ out[track.id] = colorFromHue(hue);
65
+ return out;
66
+ }
67
+ //# sourceMappingURL=colors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"colors.js","sourceRoot":"","sources":["../src/colors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAWH,0EAA0E;AAC1E,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,IAAI,CAAC,GAAG,UAAU,CAAC;IACnB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACrC,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;QACxB,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,UAAU,CAAC,KAAK,CAAC,CAAC;IACrC,CAAC;IACD,0EAA0E;IAC1E,OAAO,CAAC,CAAC,GAAG,OAAO,CAAC,GAAG,GAAG,CAAC;AAC7B,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,GAAW;IACtC,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC;IACpC,OAAO;QACL,GAAG,EAAE,CAAC;QACN,IAAI,EAAE,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,aAAa;QACtC,GAAG,EAAE,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,cAAc;KACvC,CAAC;AACJ,CAAC;AAED,+DAA+D;AAC/D,MAAM,OAAO,GAAG,EAAE,CAAC;AAEnB;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CACzB,MAAiE;IAEjE,MAAM,GAAG,GAA+B,EAAE,CAAC;IAC3C,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,GAAG,CAAC;IACpC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;IACnD,MAAM,KAAK,GAAG,MAAM;SACjB,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,WAAW,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;SACvE,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC;IACtD,kEAAkE;IAClE,kCAAkC;IAClC,KAAK,IAAI,IAAI,GAAG,CAAC,EAAE,IAAI,GAAG,CAAC,EAAE,IAAI,EAAE,EAAE,CAAC;QACpC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACtC,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC;YAC1D,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;YACrB,MAAM,OAAO,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;YACpD,IAAI,GAAG,CAAC,GAAG,GAAG,OAAO,GAAG,GAAG;gBAAE,GAAG,CAAC,GAAG,GAAG,OAAO,GAAG,GAAG,CAAC;QACvD,CAAC;IACH,CAAC;IACD,KAAK,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,KAAK;QAAE,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;IACtE,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Which instrument the listener is looking at — the nearest one inside the
3
+ * field of view — so a host can follow it with its active track while the
4
+ * Spatial view is on.
5
+ *
6
+ * Sticky on purpose: the current pick is kept until it leaves the field of
7
+ * view or another instrument is clearly nearer (by `SWITCH_MARGIN`).
8
+ * Without that, standing between two instruments at equal distance would
9
+ * flip the selection every frame as the listener drifted.
10
+ */
11
+ import type { UnpluggedListener, UnpluggedPoint } from '@sudobility/music_types';
12
+ export interface FacingCandidate {
13
+ readonly id: string;
14
+ readonly point: UnpluggedPoint;
15
+ }
16
+ /** Half-angle of the field of view an instrument must be within, degrees. */
17
+ export declare const FACING_HALF_ANGLE = 60;
18
+ /**
19
+ * The id of the instrument to follow, or `currentId` when nothing has
20
+ * changed, or null when nothing is in view.
21
+ */
22
+ export declare function facingTrackId(listener: UnpluggedListener, candidates: readonly FacingCandidate[], currentId: string | null, halfAngleDeg?: number): string | null;
23
+ //# sourceMappingURL=facing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"facing.d.ts","sourceRoot":"","sources":["../src/facing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EACV,iBAAiB,EACjB,cAAc,EACf,MAAM,yBAAyB,CAAC;AAEjC,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC;CAChC;AAED,6EAA6E;AAC7E,eAAO,MAAM,iBAAiB,KAAK,CAAC;AAQpC;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,iBAAiB,EAC3B,UAAU,EAAE,SAAS,eAAe,EAAE,EACtC,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,YAAY,SAAoB,GAC/B,MAAM,GAAG,IAAI,CAsBf"}
package/dist/facing.js ADDED
@@ -0,0 +1,38 @@
1
+ /** Half-angle of the field of view an instrument must be within, degrees. */
2
+ export const FACING_HALF_ANGLE = 60;
3
+ /** A challenger must be this much nearer than the current pick to take over. */
4
+ const SWITCH_MARGIN = 0.85;
5
+ function normalizeDeg(deg) {
6
+ return ((((deg + 180) % 360) + 360) % 360) - 180;
7
+ }
8
+ /**
9
+ * The id of the instrument to follow, or `currentId` when nothing has
10
+ * changed, or null when nothing is in view.
11
+ */
12
+ export function facingTrackId(listener, candidates, currentId, halfAngleDeg = FACING_HALF_ANGLE) {
13
+ let best = null;
14
+ let current = null;
15
+ for (const c of candidates) {
16
+ const dx = c.point.x - listener.x;
17
+ const dz = c.point.z - listener.z;
18
+ const bearing = (Math.atan2(dx, dz) * 180) / Math.PI;
19
+ const off = Math.abs(normalizeDeg(bearing - listener.facingDeg));
20
+ if (off > halfAngleDeg)
21
+ continue;
22
+ // "Facing" weighs the angle as well as the range: an instrument dead
23
+ // ahead beats a slightly nearer one at the edge of view. The effective
24
+ // distance grows as the instrument moves off-axis — doubled at the
25
+ // edge of a 60° half-angle — rather than a hard nearest-wins.
26
+ const distance = Math.hypot(dx, dz) / Math.max(0.5, Math.cos((off * Math.PI) / 180));
27
+ if (c.id === currentId)
28
+ current = { id: c.id, distance };
29
+ if (!best || distance < best.distance)
30
+ best = { id: c.id, distance };
31
+ }
32
+ if (!best)
33
+ return null;
34
+ if (current && best.distance >= current.distance * SWITCH_MARGIN)
35
+ return current.id;
36
+ return best.id;
37
+ }
38
+ //# sourceMappingURL=facing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"facing.js","sourceRoot":"","sources":["../src/facing.ts"],"names":[],"mappings":"AAoBA,6EAA6E;AAC7E,MAAM,CAAC,MAAM,iBAAiB,GAAG,EAAE,CAAC;AACpC,gFAAgF;AAChF,MAAM,aAAa,GAAG,IAAI,CAAC;AAE3B,SAAS,YAAY,CAAC,GAAW;IAC/B,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC;AACnD,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAC3B,QAA2B,EAC3B,UAAsC,EACtC,SAAwB,EACxB,YAAY,GAAG,iBAAiB;IAEhC,IAAI,IAAI,GAA4C,IAAI,CAAC;IACzD,IAAI,OAAO,GAA4C,IAAI,CAAC;IAC5D,KAAK,MAAM,CAAC,IAAI,UAAU,EAAE,CAAC;QAC3B,MAAM,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC;QAClC,MAAM,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC;QAClC,MAAM,OAAO,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,GAAG,IAAI,CAAC,EAAE,CAAC;QACrD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC,OAAO,GAAG,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC;QACjE,IAAI,GAAG,GAAG,YAAY;YAAE,SAAS;QACjC,qEAAqE;QACrE,uEAAuE;QACvE,mEAAmE;QACnE,8DAA8D;QAC9D,MAAM,QAAQ,GACZ,IAAI,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,GAAG,IAAI,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC;QACtE,IAAI,CAAC,CAAC,EAAE,KAAK,SAAS;YAAE,OAAO,GAAG,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,QAAQ,EAAE,CAAC;QACzD,IAAI,CAAC,IAAI,IAAI,QAAQ,GAAG,IAAI,CAAC,QAAQ;YAAE,IAAI,GAAG,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,QAAQ,EAAE,CAAC;IACvE,CAAC;IACD,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IACvB,IAAI,OAAO,IAAI,IAAI,CAAC,QAAQ,IAAI,OAAO,CAAC,QAAQ,GAAG,aAAa;QAC9D,OAAO,OAAO,CAAC,EAAE,CAAC;IACpB,OAAO,IAAI,CAAC,EAAE,CAAC;AACjB,CAAC"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Turns an authored `InstrumentSpatialModel`'s strokes into drawable
3
+ * `three` geometry — the one mechanical, non-authored step in the pipeline.
4
+ *
5
+ * Deliberately does NOT take a pose. A model's pose (offset/rotation/scale
6
+ * from `spatialModelForTrack`) becomes an *object* transform — applied by
7
+ * `buildScene` as the `position`/`rotation`/`scale` a platform package
8
+ * hands to `<group>`/`<lineSegments>` — rather than baked into vertex
9
+ * coordinates here. `three`'s own transform pipeline already does this
10
+ * correctly; re-deriving rotation matrices by hand in this file would only
11
+ * be a second, riskier way to compute the same thing.
12
+ */
13
+ import { BufferGeometry } from 'three';
14
+ import type { InstrumentSpatialModel } from '@sudobility/music_types';
15
+ /** One `THREE.BufferGeometry` per stroke, set up for `THREE.LineSegments`
16
+ * (consecutive point pairs, not a single continuous line strip) — both
17
+ * platform packages wrap each in `<lineSegments geometry={g} />`. */
18
+ export declare function buildInstrumentGeometry(model: InstrumentSpatialModel): readonly BufferGeometry[];
19
+ //# sourceMappingURL=geometry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"geometry.d.ts","sourceRoot":"","sources":["../src/geometry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,cAAc,EAA0B,MAAM,OAAO,CAAC;AAC/D,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AAEtE;;sEAEsE;AACtE,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,sBAAsB,GAC5B,SAAS,cAAc,EAAE,CAM3B"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Turns an authored `InstrumentSpatialModel`'s strokes into drawable
3
+ * `three` geometry — the one mechanical, non-authored step in the pipeline.
4
+ *
5
+ * Deliberately does NOT take a pose. A model's pose (offset/rotation/scale
6
+ * from `spatialModelForTrack`) becomes an *object* transform — applied by
7
+ * `buildScene` as the `position`/`rotation`/`scale` a platform package
8
+ * hands to `<group>`/`<lineSegments>` — rather than baked into vertex
9
+ * coordinates here. `three`'s own transform pipeline already does this
10
+ * correctly; re-deriving rotation matrices by hand in this file would only
11
+ * be a second, riskier way to compute the same thing.
12
+ */
13
+ import { BufferGeometry, Float32BufferAttribute } from 'three';
14
+ /** One `THREE.BufferGeometry` per stroke, set up for `THREE.LineSegments`
15
+ * (consecutive point pairs, not a single continuous line strip) — both
16
+ * platform packages wrap each in `<lineSegments geometry={g} />`. */
17
+ export function buildInstrumentGeometry(model) {
18
+ const cached = GEOMETRY_CACHE.get(model);
19
+ if (cached)
20
+ return cached;
21
+ const built = buildUncached(model);
22
+ GEOMETRY_CACHE.set(model, built);
23
+ return built;
24
+ }
25
+ /**
26
+ * Keyed by model identity: the models are module-level singletons in
27
+ * music_types (a violin is one object for the life of the process), and
28
+ * `buildScene` runs on every sounding-state change — many times a second
29
+ * while playing — so without this every note-on rebuilt a few hundred
30
+ * segments per instrument. Geometries are never mutated after creation,
31
+ * so sharing one across instances and frames is safe.
32
+ */
33
+ const GEOMETRY_CACHE = new WeakMap();
34
+ function buildUncached(model) {
35
+ return model.strokes.map(stroke => {
36
+ const positions = [];
37
+ for (let i = 0; i < stroke.length - 1; i++) {
38
+ const [ax, ay, az] = stroke[i];
39
+ const [bx, by, bz] = stroke[i + 1];
40
+ // x mirrored: the models are authored in the left-handed arrangement
41
+ // frame (+x = the player's right facing +z); three.js is
42
+ // right-handed. See scene.ts, which mirrors positions and rotations
43
+ // to match.
44
+ positions.push(-ax, ay, az, -bx, by, bz);
45
+ }
46
+ const geometry = new BufferGeometry();
47
+ geometry.setAttribute('position', new Float32BufferAttribute(positions, 3));
48
+ return geometry;
49
+ });
50
+ }
51
+ //# sourceMappingURL=geometry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"geometry.js","sourceRoot":"","sources":["../src/geometry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,cAAc,EAAE,sBAAsB,EAAE,MAAM,OAAO,CAAC;AAG/D;;sEAEsE;AACtE,MAAM,UAAU,uBAAuB,CACrC,KAA6B;IAE7B,MAAM,MAAM,GAAG,cAAc,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IACzC,IAAI,MAAM;QAAE,OAAO,MAAM,CAAC;IAC1B,MAAM,KAAK,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC;IACnC,cAAc,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IACjC,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,cAAc,GAAG,IAAI,OAAO,EAG/B,CAAC;AAEJ,SAAS,aAAa,CACpB,KAA6B;IAE7B,OAAO,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE;QAChC,MAAM,SAAS,GAAa,EAAE,CAAC;QAC/B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;YAC3C,MAAM,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;YAC/B,MAAM,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,GAAG,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YACnC,qEAAqE;YACrE,yDAAyD;YACzD,oEAAoE;YACpE,YAAY;YACZ,SAAS,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;QAC3C,CAAC;QACD,MAAM,QAAQ,GAAG,IAAI,cAAc,EAAE,CAAC;QACtC,QAAQ,CAAC,YAAY,CAAC,UAAU,EAAE,IAAI,sBAAsB,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,CAAC;QAC5E,OAAO,QAAQ,CAAC;IAClB,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,11 @@
1
+ export * from './geometry';
2
+ export * from './map-math';
3
+ export * from './movement';
4
+ export * from './scene';
5
+ export * from './use-throttled-commit';
6
+ export { effectiveUnpluggedArrangement, UNPLUGGED_RADIUS, } from '@sudobility/music_types';
7
+ export type { EffectiveUnpluggedArrangement } from '@sudobility/music_types';
8
+ export * from './colors';
9
+ export * from './wander';
10
+ export * from './facing';
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAC3B,cAAc,SAAS,CAAC;AACxB,cAAc,wBAAwB,CAAC;AAMvC,OAAO,EACL,6BAA6B,EAC7B,gBAAgB,GACjB,MAAM,yBAAyB,CAAC;AACjC,YAAY,EAAE,6BAA6B,EAAE,MAAM,yBAAyB,CAAC;AAC7E,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
1
+ export * from './geometry';
2
+ export * from './map-math';
3
+ export * from './movement';
4
+ export * from './scene';
5
+ export * from './use-throttled-commit';
6
+ // Re-exported for the platform packages, which already depend on
7
+ // music_types themselves but read these through here alongside everything
8
+ // above. They live in music_types (domain/unplugged) — this package has no
9
+ // music_lib dependency.
10
+ export { effectiveUnpluggedArrangement, UNPLUGGED_RADIUS, } from '@sudobility/music_types';
11
+ export * from './colors';
12
+ export * from './wander';
13
+ export * from './facing';
14
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAC3B,cAAc,SAAS,CAAC;AACxB,cAAc,wBAAwB,CAAC;AAEvC,iEAAiE;AACjE,0EAA0E;AAC1E,2EAA2E;AAC3E,wBAAwB;AACxB,OAAO,EACL,6BAA6B,EAC7B,gBAAgB,GACjB,MAAM,yBAAyB,CAAC;AAEjC,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Pure geometry for the 2D map overlay: pixel position <-> arrangement
3
+ * point, and the listener's rotation-handle bearing — extracted from the
4
+ * existing `UnpluggedTab.tsx` (both platforms) so it's written once. Each
5
+ * platform still owns *capturing* a pixel position (DOM pointer events vs.
6
+ * RN `measureInWindow`/gesture-handler); everything downstream of "here is
7
+ * a pixel position" lives here.
8
+ */
9
+ import type { UnpluggedPoint } from '@sudobility/music_types';
10
+ export interface OverlayGeometry {
11
+ readonly centerX: number;
12
+ readonly centerY: number;
13
+ /** The rendered circle's radius, in the overlay's own pixel space. */
14
+ readonly radiusPx: number;
15
+ /** The arrangement-space radius (e.g. `UNPLUGGED_RADIUS`) that `radiusPx`
16
+ * represents — together they give the pixels-per-unit scale. */
17
+ readonly radiusUnits: number;
18
+ }
19
+ /**
20
+ * Pixel position -> a point in arrangement space, clamped to the
21
+ * arrangement's radius so a drag can never place an instrument or the
22
+ * listener further out than the overlay can show.
23
+ *
24
+ * Screen Y increases downward; arrangement `z` increases into the
25
+ * arrangement (away from the overlay's bottom, where the listener's default
26
+ * position sits) — so screen-down maps to arrangement-backward, not forward.
27
+ */
28
+ export declare function pointFromPixel(geometry: OverlayGeometry, pixelX: number, pixelY: number): UnpluggedPoint;
29
+ /** The reverse of `pointFromPixel`, for drawing a marker at a point's pixel
30
+ * position. */
31
+ export declare function pixelFromPoint(geometry: OverlayGeometry, point: UnpluggedPoint): {
32
+ readonly x: number;
33
+ readonly y: number;
34
+ };
35
+ /**
36
+ * A pixel *delta* converted to a stage-space delta — for dragging a marker
37
+ * by accumulating movement onto its own last position, not by snapping the
38
+ * marker to the pointer's absolute position. That distinction matters:
39
+ * grabbing a marker somewhere other than its exact center and dragging it
40
+ * would otherwise make the marker jump to re-center itself under the
41
+ * pointer on the very first event. Rotation (`bearingFromCenter`) is the
42
+ * opposite case on purpose — always absolute, never delta, so it cannot
43
+ * drift from compounding rounding the way this function's own accumulation
44
+ * would if used for an angle.
45
+ */
46
+ export declare function stageDeltaFromPixelDelta(geometry: OverlayGeometry, dxPx: number, dyPx: number): {
47
+ readonly dx: number;
48
+ readonly dz: number;
49
+ };
50
+ /**
51
+ * Bearing from the arrangement's center to a pixel position, in degrees,
52
+ * using the exact same convention as `unpluggedMixFor` (music_lib):
53
+ * `atan2(dx, dz)`, clockwise-positive from `+z`.
54
+ *
55
+ * Computed fresh from the CURRENT absolute pixel position every call, never
56
+ * from an accumulated delta — that's the exact-rotation fix the 2D build
57
+ * already needed (drift from compounding rounding when a bearing was
58
+ * derived from its own previous answer) and it applies identically here.
59
+ */
60
+ export declare function bearingFromCenter(geometry: OverlayGeometry, pixelX: number, pixelY: number): number;
61
+ //# sourceMappingURL=map-math.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"map-math.d.ts","sourceRoot":"","sources":["../src/map-math.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAE9D,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,sEAAsE;IACtE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;qEACiE;IACjE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,eAAe,EACzB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,GACb,cAAc,CAYhB;AAED;gBACgB;AAChB,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,eAAe,EACzB,KAAK,EAAE,cAAc,GACpB;IAAE,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAM5C;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,eAAe,EACzB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,GACX;IAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAG9C;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,eAAe,EACzB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,GACb,MAAM,CAIR"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Pixel position -> a point in arrangement space, clamped to the
3
+ * arrangement's radius so a drag can never place an instrument or the
4
+ * listener further out than the overlay can show.
5
+ *
6
+ * Screen Y increases downward; arrangement `z` increases into the
7
+ * arrangement (away from the overlay's bottom, where the listener's default
8
+ * position sits) — so screen-down maps to arrangement-backward, not forward.
9
+ */
10
+ export function pointFromPixel(geometry, pixelX, pixelY) {
11
+ const unitsPerPixel = geometry.radiusUnits / geometry.radiusPx;
12
+ let x = (pixelX - geometry.centerX) * unitsPerPixel;
13
+ let z = -(pixelY - geometry.centerY) * unitsPerPixel;
14
+ const distance = Math.hypot(x, z);
15
+ if (distance > geometry.radiusUnits) {
16
+ const factor = geometry.radiusUnits / distance;
17
+ x *= factor;
18
+ z *= factor;
19
+ }
20
+ return { x, z };
21
+ }
22
+ /** The reverse of `pointFromPixel`, for drawing a marker at a point's pixel
23
+ * position. */
24
+ export function pixelFromPoint(geometry, point) {
25
+ const pixelsPerUnit = geometry.radiusPx / geometry.radiusUnits;
26
+ return {
27
+ x: geometry.centerX + point.x * pixelsPerUnit,
28
+ y: geometry.centerY - point.z * pixelsPerUnit,
29
+ };
30
+ }
31
+ /**
32
+ * A pixel *delta* converted to a stage-space delta — for dragging a marker
33
+ * by accumulating movement onto its own last position, not by snapping the
34
+ * marker to the pointer's absolute position. That distinction matters:
35
+ * grabbing a marker somewhere other than its exact center and dragging it
36
+ * would otherwise make the marker jump to re-center itself under the
37
+ * pointer on the very first event. Rotation (`bearingFromCenter`) is the
38
+ * opposite case on purpose — always absolute, never delta, so it cannot
39
+ * drift from compounding rounding the way this function's own accumulation
40
+ * would if used for an angle.
41
+ */
42
+ export function stageDeltaFromPixelDelta(geometry, dxPx, dyPx) {
43
+ const unitsPerPixel = geometry.radiusUnits / geometry.radiusPx;
44
+ return { dx: dxPx * unitsPerPixel, dz: -dyPx * unitsPerPixel };
45
+ }
46
+ /**
47
+ * Bearing from the arrangement's center to a pixel position, in degrees,
48
+ * using the exact same convention as `unpluggedMixFor` (music_lib):
49
+ * `atan2(dx, dz)`, clockwise-positive from `+z`.
50
+ *
51
+ * Computed fresh from the CURRENT absolute pixel position every call, never
52
+ * from an accumulated delta — that's the exact-rotation fix the 2D build
53
+ * already needed (drift from compounding rounding when a bearing was
54
+ * derived from its own previous answer) and it applies identically here.
55
+ */
56
+ export function bearingFromCenter(geometry, pixelX, pixelY) {
57
+ const dx = pixelX - geometry.centerX;
58
+ const dz = -(pixelY - geometry.centerY);
59
+ return (Math.atan2(dx, dz) * 180) / Math.PI;
60
+ }
61
+ //# sourceMappingURL=map-math.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"map-math.js","sourceRoot":"","sources":["../src/map-math.ts"],"names":[],"mappings":"AAoBA;;;;;;;;GAQG;AACH,MAAM,UAAU,cAAc,CAC5B,QAAyB,EACzB,MAAc,EACd,MAAc;IAEd,MAAM,aAAa,GAAG,QAAQ,CAAC,WAAW,GAAG,QAAQ,CAAC,QAAQ,CAAC;IAC/D,IAAI,CAAC,GAAG,CAAC,MAAM,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,aAAa,CAAC;IACpD,IAAI,CAAC,GAAG,CAAC,CAAC,MAAM,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,aAAa,CAAC;IAErD,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAClC,IAAI,QAAQ,GAAG,QAAQ,CAAC,WAAW,EAAE,CAAC;QACpC,MAAM,MAAM,GAAG,QAAQ,CAAC,WAAW,GAAG,QAAQ,CAAC;QAC/C,CAAC,IAAI,MAAM,CAAC;QACZ,CAAC,IAAI,MAAM,CAAC;IACd,CAAC;IACD,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC;AAClB,CAAC;AAED;gBACgB;AAChB,MAAM,UAAU,cAAc,CAC5B,QAAyB,EACzB,KAAqB;IAErB,MAAM,aAAa,GAAG,QAAQ,CAAC,QAAQ,GAAG,QAAQ,CAAC,WAAW,CAAC;IAC/D,OAAO;QACL,CAAC,EAAE,QAAQ,CAAC,OAAO,GAAG,KAAK,CAAC,CAAC,GAAG,aAAa;QAC7C,CAAC,EAAE,QAAQ,CAAC,OAAO,GAAG,KAAK,CAAC,CAAC,GAAG,aAAa;KAC9C,CAAC;AACJ,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,wBAAwB,CACtC,QAAyB,EACzB,IAAY,EACZ,IAAY;IAEZ,MAAM,aAAa,GAAG,QAAQ,CAAC,WAAW,GAAG,QAAQ,CAAC,QAAQ,CAAC;IAC/D,OAAO,EAAE,EAAE,EAAE,IAAI,GAAG,aAAa,EAAE,EAAE,EAAE,CAAC,IAAI,GAAG,aAAa,EAAE,CAAC;AACjE,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,iBAAiB,CAC/B,QAAyB,EACzB,MAAc,EACd,MAAc;IAEd,MAAM,EAAE,GAAG,MAAM,GAAG,QAAQ,CAAC,OAAO,CAAC;IACrC,MAAM,EAAE,GAAG,CAAC,CAAC,MAAM,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC;IACxC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,GAAG,IAAI,CAAC,EAAE,CAAC;AAC9C,CAAC"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Continuous, frame-rate-independent listener movement for the Spatial 3D
3
+ * view — walking while a key is held or a joystick is pushed, as distinct
4
+ * from the 2D map's discrete one-step-per-keypress nudge (which stays a
5
+ * direct, simpler caller of the Unplugged commands, unchanged by this).
6
+ *
7
+ * Lives here, not in `@sudobility/music_lib`, because it's Spatial-view-only
8
+ * behavior — nothing else in either app ever calls it — while `music_lib`
9
+ * keeps only what's needed independent of whether the Spatial view exists
10
+ * (`effectiveUnpluggedArrangement`, the audio mix formula).
11
+ */
12
+ import type { UnpluggedListener } from '@sudobility/music_types';
13
+ export interface MovementInput {
14
+ /** -1 (backward) .. 1 (forward), relative to the listener's current facing. */
15
+ readonly forward: number;
16
+ /** -1 (left) .. 1 (right), relative to the listener's current facing. */
17
+ readonly strafe: number;
18
+ /** -1 (turn left) .. 1 (turn right). */
19
+ readonly turn: number;
20
+ }
21
+ export interface MovementSpeed {
22
+ readonly metersPerSecond: number;
23
+ readonly degreesPerSecond: number;
24
+ }
25
+ export declare const DEFAULT_MOVEMENT_SPEED: MovementSpeed;
26
+ /**
27
+ * One frame's movement: turn first, then move along the *new* facing — the
28
+ * way a person actually walks while turning, and the only order that makes
29
+ * "hold forward and turn" curve rather than move in a straight line then
30
+ * snap direction.
31
+ *
32
+ * Uses the exact bearing convention `unplugged.ts` (music_lib) already
33
+ * established: `dx = distance * sin(bearingRad)`, `dz = distance *
34
+ * cos(bearingRad)`, `facingDeg` clockwise-positive from `+z`. Strafe moves
35
+ * perpendicular to facing, 90° clockwise (right).
36
+ */
37
+ export declare function stepListener(listener: UnpluggedListener, input: MovementInput, deltaSeconds: number, speed?: MovementSpeed): UnpluggedListener;
38
+ //# sourceMappingURL=movement.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"movement.d.ts","sourceRoot":"","sources":["../src/movement.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAEjE,MAAM,WAAW,aAAa;IAC5B,+EAA+E;IAC/E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,yEAAyE;IACzE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,wCAAwC;IACxC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;CACnC;AAED,eAAO,MAAM,sBAAsB,EAAE,aAGpC,CAAC;AAYF;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,iBAAiB,EAC3B,KAAK,EAAE,aAAa,EACpB,YAAY,EAAE,MAAM,EACpB,KAAK,GAAE,aAAsC,GAC5C,iBAAiB,CAyBnB"}
@@ -0,0 +1,43 @@
1
+ export const DEFAULT_MOVEMENT_SPEED = {
2
+ metersPerSecond: 3,
3
+ degreesPerSecond: 120,
4
+ };
5
+ function clampAxis(v) {
6
+ return Math.max(-1, Math.min(1, v));
7
+ }
8
+ /** Normalizes to (-180, 180], the same range a long play session's
9
+ * accumulated turning would otherwise drift outside of. */
10
+ function normalizeDeg(deg) {
11
+ return ((((deg + 180) % 360) + 360) % 360) - 180;
12
+ }
13
+ /**
14
+ * One frame's movement: turn first, then move along the *new* facing — the
15
+ * way a person actually walks while turning, and the only order that makes
16
+ * "hold forward and turn" curve rather than move in a straight line then
17
+ * snap direction.
18
+ *
19
+ * Uses the exact bearing convention `unplugged.ts` (music_lib) already
20
+ * established: `dx = distance * sin(bearingRad)`, `dz = distance *
21
+ * cos(bearingRad)`, `facingDeg` clockwise-positive from `+z`. Strafe moves
22
+ * perpendicular to facing, 90° clockwise (right).
23
+ */
24
+ export function stepListener(listener, input, deltaSeconds, speed = DEFAULT_MOVEMENT_SPEED) {
25
+ const forward = clampAxis(input.forward);
26
+ const strafe = clampAxis(input.strafe);
27
+ const turn = clampAxis(input.turn);
28
+ const facingDeg = normalizeDeg(listener.facingDeg + turn * speed.degreesPerSecond * deltaSeconds);
29
+ const facingRad = (facingDeg * Math.PI) / 180;
30
+ const strafeRad = facingRad + Math.PI / 2;
31
+ const forwardDistance = forward * speed.metersPerSecond * deltaSeconds;
32
+ const strafeDistance = strafe * speed.metersPerSecond * deltaSeconds;
33
+ return {
34
+ x: listener.x +
35
+ forwardDistance * Math.sin(facingRad) +
36
+ strafeDistance * Math.sin(strafeRad),
37
+ z: listener.z +
38
+ forwardDistance * Math.cos(facingRad) +
39
+ strafeDistance * Math.cos(strafeRad),
40
+ facingDeg,
41
+ };
42
+ }
43
+ //# sourceMappingURL=movement.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"movement.js","sourceRoot":"","sources":["../src/movement.ts"],"names":[],"mappings":"AA2BA,MAAM,CAAC,MAAM,sBAAsB,GAAkB;IACnD,eAAe,EAAE,CAAC;IAClB,gBAAgB,EAAE,GAAG;CACtB,CAAC;AAEF,SAAS,SAAS,CAAC,CAAS;IAC1B,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;AACtC,CAAC;AAED;4DAC4D;AAC5D,SAAS,YAAY,CAAC,GAAW;IAC/B,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC;AACnD,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,YAAY,CAC1B,QAA2B,EAC3B,KAAoB,EACpB,YAAoB,EACpB,QAAuB,sBAAsB;IAE7C,MAAM,OAAO,GAAG,SAAS,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACzC,MAAM,MAAM,GAAG,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IACvC,MAAM,IAAI,GAAG,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAEnC,MAAM,SAAS,GAAG,YAAY,CAC5B,QAAQ,CAAC,SAAS,GAAG,IAAI,GAAG,KAAK,CAAC,gBAAgB,GAAG,YAAY,CAClE,CAAC;IACF,MAAM,SAAS,GAAG,CAAC,SAAS,GAAG,IAAI,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC;IAC9C,MAAM,SAAS,GAAG,SAAS,GAAG,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC;IAE1C,MAAM,eAAe,GAAG,OAAO,GAAG,KAAK,CAAC,eAAe,GAAG,YAAY,CAAC;IACvE,MAAM,cAAc,GAAG,MAAM,GAAG,KAAK,CAAC,eAAe,GAAG,YAAY,CAAC;IAErE,OAAO;QACL,CAAC,EACC,QAAQ,CAAC,CAAC;YACV,eAAe,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC;YACrC,cAAc,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC;QACtC,CAAC,EACC,QAAQ,CAAC,CAAC;YACV,eAAe,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC;YACrC,cAAc,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC;QACtC,SAAS;KACV,CAAC;AACJ,CAAC"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Composes a `Score` and its Unplugged arrangement into everything a
3
+ * platform package needs to render one frame: each instrument's geometry
4
+ * and world transform, and the camera transform for the listener. Called
5
+ * once per relevant render/tick — a platform's `SpatialCanvas` becomes
6
+ * close to a plain `.map()` over `instruments` plus one camera assignment.
7
+ *
8
+ * ## Handedness
9
+ *
10
+ * The arrangement convention (`unplugged.ts`, the map overlay, the audio
11
+ * pan) is: +z is straight ahead of the listener's default facing and **+x
12
+ * is the listener's right**. That is a left-handed frame. three.js is
13
+ * right-handed — facing +z with +y up puts +x on the *left* — so rendering
14
+ * arrangement coordinates as-is produced a mirror image: an instrument
15
+ * panned right appeared on the left, turning left turned the camera right,
16
+ * and a violin held on the left shoulder rendered on the performer's
17
+ * right. This module is the one place the two meet, so it mirrors x here:
18
+ * positions, geometry (see `buildInstrumentGeometry`), and the sign of
19
+ * every rotation about y and z. Nothing upstream has to know.
20
+ */
21
+ import type { BufferGeometry } from 'three';
22
+ import type { Score } from '@sudobility/music_types';
23
+ export type Vec3Tuple = readonly [number, number, number];
24
+ export interface SpatialInstrument {
25
+ readonly trackId: string;
26
+ readonly geometry: readonly BufferGeometry[];
27
+ readonly position: Vec3Tuple;
28
+ readonly rotation: Vec3Tuple;
29
+ readonly scale: number;
30
+ readonly lit: boolean;
31
+ /** The track's own colour (see `colors.ts`), dimmed while it rests. */
32
+ readonly color: string;
33
+ /** The same colour at full strength — what to draw while it sounds. */
34
+ readonly litColor: string;
35
+ }
36
+ export interface SpatialCamera {
37
+ readonly position: Vec3Tuple;
38
+ readonly rotation: Vec3Tuple;
39
+ }
40
+ export interface SpatialScene {
41
+ readonly instruments: readonly SpatialInstrument[];
42
+ readonly camera: SpatialCamera;
43
+ }
44
+ export declare function buildScene(score: Pick<Score, 'tracks' | 'unplugged'>, soundingTrackIds: ReadonlySet<string>): SpatialScene;
45
+ //# sourceMappingURL=scene.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scene.d.ts","sourceRoot":"","sources":["../src/scene.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,OAAO,CAAC;AAK5C,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,yBAAyB,CAAC;AAIrD,MAAM,MAAM,SAAS,GAAG,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;AAE1D,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,SAAS,cAAc,EAAE,CAAC;IAC7C,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,uEAAuE;IACvE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,uEAAuE;IACvE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC;CAC9B;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,WAAW,EAAE,SAAS,iBAAiB,EAAE,CAAC;IACnD,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;CAChC;AAqBD,wBAAgB,UAAU,CACxB,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,QAAQ,GAAG,WAAW,CAAC,EAC1C,gBAAgB,EAAE,WAAW,CAAC,MAAM,CAAC,GACpC,YAAY,CAiDd"}
package/dist/scene.js ADDED
@@ -0,0 +1,66 @@
1
+ import { effectiveUnpluggedArrangement, trackSpatialModel, } from '@sudobility/music_types';
2
+ import { buildInstrumentGeometry } from './geometry';
3
+ import { trackColors } from './colors';
4
+ /** Stage floor height every instrument's own pose offset is measured from. */
5
+ const GROUND_Y = 0;
6
+ /** A first-person listener's ear height above the floor. */
7
+ const EAR_HEIGHT = 1.6;
8
+ /**
9
+ * Every instrument on stage is yawed 180° from its authored rest
10
+ * orientation, so a model authored facing +z (toward its own audience)
11
+ * faces back toward the listener's default position — a stage convention
12
+ * (performers face the audience), applied uniformly rather than aimed
13
+ * per-instrument at wherever the listener currently stands.
14
+ */
15
+ const AUDIENCE_YAW = Math.PI;
16
+ function degToRad(deg) {
17
+ return (deg * Math.PI) / 180;
18
+ }
19
+ export function buildScene(score, soundingTrackIds) {
20
+ const arrangement = effectiveUnpluggedArrangement(score);
21
+ const colors = trackColors(score.tracks);
22
+ const instruments = score.tracks.map(track => {
23
+ const point = arrangement.tracks[track.id] ?? { x: 0, z: 0 };
24
+ const { model, pose } = trackSpatialModel(track);
25
+ const lit = soundingTrackIds.has(track.id);
26
+ const color = colors[track.id];
27
+ return {
28
+ trackId: track.id,
29
+ geometry: buildInstrumentGeometry(model),
30
+ // x mirrored — see the module doc.
31
+ position: [
32
+ -(point.x + pose.position[0]),
33
+ GROUND_Y + pose.position[1],
34
+ point.z + pose.position[2],
35
+ ],
36
+ // Mirroring x negates rotations about y and z and keeps those about
37
+ // x. Composing the instrument's own tilt with the stage yaw by adding
38
+ // the y components is an approximation, exact for the y-only stage
39
+ // yaw and the zero poses every authored model uses today (their
40
+ // poses are baked into the geometry).
41
+ rotation: [
42
+ degToRad(pose.rotationDeg[0]),
43
+ -(degToRad(pose.rotationDeg[1]) + AUDIENCE_YAW),
44
+ -degToRad(pose.rotationDeg[2]),
45
+ ],
46
+ scale: pose.scale,
47
+ lit,
48
+ color: color.base,
49
+ litColor: color.lit,
50
+ };
51
+ });
52
+ return {
53
+ instruments,
54
+ camera: {
55
+ position: [-arrangement.listener.x, EAR_HEIGHT, arrangement.listener.z],
56
+ // Three.js cameras look down their local -Z. With x mirrored, a
57
+ // facing of f degrees (clockwise-positive from +z in arrangement
58
+ // space) is a camera yaw of PI - f: PI looks toward +z, and turning
59
+ // left (f decreasing) turns the camera toward its own left. Pinned
60
+ // by scene.test.ts rather than left to visual inspection — the sign
61
+ // is exactly what was wrong before.
62
+ rotation: [0, Math.PI - degToRad(arrangement.listener.facingDeg), 0],
63
+ },
64
+ };
65
+ }
66
+ //# sourceMappingURL=scene.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scene.js","sourceRoot":"","sources":["../src/scene.ts"],"names":[],"mappings":"AAqBA,OAAO,EACL,6BAA6B,EAC7B,iBAAiB,GAClB,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AA2BvC,8EAA8E;AAC9E,MAAM,QAAQ,GAAG,CAAC,CAAC;AAEnB,4DAA4D;AAC5D,MAAM,UAAU,GAAG,GAAG,CAAC;AAEvB;;;;;;GAMG;AACH,MAAM,YAAY,GAAG,IAAI,CAAC,EAAE,CAAC;AAE7B,SAAS,QAAQ,CAAC,GAAW;IAC3B,OAAO,CAAC,GAAG,GAAG,IAAI,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC;AAC/B,CAAC;AAED,MAAM,UAAU,UAAU,CACxB,KAA0C,EAC1C,gBAAqC;IAErC,MAAM,WAAW,GAAG,6BAA6B,CAAC,KAAK,CAAC,CAAC;IACzD,MAAM,MAAM,GAAG,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAEzC,MAAM,WAAW,GAAwB,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE;QAChE,MAAM,KAAK,GAAG,WAAW,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC;QAC7D,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,iBAAiB,CAAC,KAAK,CAAC,CAAC;QACjD,MAAM,GAAG,GAAG,gBAAgB,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAC3C,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAE/B,OAAO;YACL,OAAO,EAAE,KAAK,CAAC,EAAE;YACjB,QAAQ,EAAE,uBAAuB,CAAC,KAAK,CAAC;YACxC,mCAAmC;YACnC,QAAQ,EAAE;gBACR,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;gBAC7B,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;gBAC3B,KAAK,CAAC,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;aAC3B;YACD,oEAAoE;YACpE,sEAAsE;YACtE,mEAAmE;YACnE,gEAAgE;YAChE,sCAAsC;YACtC,QAAQ,EAAE;gBACR,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;gBAC7B,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC;gBAC/C,CAAC,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;aAC/B;YACD,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,GAAG;YACH,KAAK,EAAE,KAAK,CAAC,IAAI;YACjB,QAAQ,EAAE,KAAK,CAAC,GAAG;SACpB,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,OAAO;QACL,WAAW;QACX,MAAM,EAAE;YACN,QAAQ,EAAE,CAAC,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC,EAAE,UAAU,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAC,CAAC;YACvE,gEAAgE;YAChE,iEAAiE;YACjE,oEAAoE;YACpE,mEAAmE;YACnE,oEAAoE;YACpE,oCAAoC;YACpC,QAAQ,EAAE,CAAC,CAAC,EAAE,IAAI,CAAC,EAAE,GAAG,QAAQ,CAAC,WAAW,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;SACrE;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,11 @@
1
+ export interface ThrottledCommit<T> {
2
+ /** The in-progress value — updates instantly on every `setValue`, back to `initial` after `commitNow`. */
3
+ readonly value: T;
4
+ /** Instant local update; the store commit is scheduled for the next frame. */
5
+ readonly setValue: (next: T) => void;
6
+ /** Forces a pending commit through synchronously — call on drag end, so
7
+ * the final position is never lost to a frame that never arrives. */
8
+ readonly commitNow: () => void;
9
+ }
10
+ export declare function useThrottledCommit<T>(initial: T, commit: (value: T) => void): ThrottledCommit<T>;
11
+ //# sourceMappingURL=use-throttled-commit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-throttled-commit.d.ts","sourceRoot":"","sources":["../src/use-throttled-commit.ts"],"names":[],"mappings":"AAkBA,MAAM,WAAW,eAAe,CAAC,CAAC;IAChC,0GAA0G;IAC1G,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC;IAClB,8EAA8E;IAC9E,QAAQ,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,CAAC;IACrC;0EACsE;IACtE,QAAQ,CAAC,SAAS,EAAE,MAAM,IAAI,CAAC;CAChC;AAED,wBAAgB,kBAAkB,CAAC,CAAC,EAClC,OAAO,EAAE,CAAC,EACV,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,GACzB,eAAe,CAAC,CAAC,CAAC,CAqCpB"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Instant local feedback + rAF-throttled store commits, with a guaranteed
3
+ * final synchronous commit — the drag-performance fix already built and
4
+ * verified for the 2D map. Turns out to have no DOM or RN dependency at
5
+ * all: `useState`/`useRef`/`useCallback` are core React, and
6
+ * `requestAnimationFrame` is standard on both web and RN (RN polyfills it)
7
+ * — so the hook itself, not just the pattern, is shared rather than
8
+ * reimplemented per platform.
9
+ *
10
+ * `value` is only ever the *in-progress* drag: it returns to `initial`
11
+ * once `commitNow` has handed the final value to the store, so a caller
12
+ * that renders `value ?? committed` follows the store again afterwards.
13
+ * Without that, a marker dragged once stayed pinned to its last dragged
14
+ * position for good and ignored every later store change — which is how
15
+ * the map overlay stopped following the keyboard after a single drag.
16
+ */
17
+ import { useCallback, useRef, useState } from 'react';
18
+ export function useThrottledCommit(initial, commit) {
19
+ const [value, setValueState] = useState(initial);
20
+ const pendingRef = useRef(null);
21
+ const frameRef = useRef(null);
22
+ const flush = useCallback(() => {
23
+ frameRef.current = null;
24
+ if (pendingRef.current) {
25
+ commit(pendingRef.current.value);
26
+ pendingRef.current = null;
27
+ }
28
+ }, [commit]);
29
+ const setValue = useCallback((next) => {
30
+ setValueState(next);
31
+ pendingRef.current = { value: next };
32
+ if (frameRef.current === null) {
33
+ frameRef.current = requestAnimationFrame(flush);
34
+ }
35
+ }, [flush]);
36
+ const commitNow = useCallback(() => {
37
+ if (frameRef.current !== null) {
38
+ cancelAnimationFrame(frameRef.current);
39
+ frameRef.current = null;
40
+ }
41
+ if (pendingRef.current) {
42
+ commit(pendingRef.current.value);
43
+ pendingRef.current = null;
44
+ }
45
+ setValueState(initial);
46
+ }, [commit, initial]);
47
+ return { value, setValue, commitNow };
48
+ }
49
+ //# sourceMappingURL=use-throttled-commit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-throttled-commit.js","sourceRoot":"","sources":["../src/use-throttled-commit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAYtD,MAAM,UAAU,kBAAkB,CAChC,OAAU,EACV,MAA0B;IAE1B,MAAM,CAAC,KAAK,EAAE,aAAa,CAAC,GAAG,QAAQ,CAAI,OAAO,CAAC,CAAC;IACpD,MAAM,UAAU,GAAG,MAAM,CAA+B,IAAI,CAAC,CAAC;IAC9D,MAAM,QAAQ,GAAG,MAAM,CAAgB,IAAI,CAAC,CAAC;IAE7C,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE;QAC7B,QAAQ,CAAC,OAAO,GAAG,IAAI,CAAC;QACxB,IAAI,UAAU,CAAC,OAAO,EAAE,CAAC;YACvB,MAAM,CAAC,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;YACjC,UAAU,CAAC,OAAO,GAAG,IAAI,CAAC;QAC5B,CAAC;IACH,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;IAEb,MAAM,QAAQ,GAAG,WAAW,CAC1B,CAAC,IAAO,EAAE,EAAE;QACV,aAAa,CAAC,IAAI,CAAC,CAAC;QACpB,UAAU,CAAC,OAAO,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;QACrC,IAAI,QAAQ,CAAC,OAAO,KAAK,IAAI,EAAE,CAAC;YAC9B,QAAQ,CAAC,OAAO,GAAG,qBAAqB,CAAC,KAAK,CAAC,CAAC;QAClD,CAAC;IACH,CAAC,EACD,CAAC,KAAK,CAAC,CACR,CAAC;IAEF,MAAM,SAAS,GAAG,WAAW,CAAC,GAAG,EAAE;QACjC,IAAI,QAAQ,CAAC,OAAO,KAAK,IAAI,EAAE,CAAC;YAC9B,oBAAoB,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;YACvC,QAAQ,CAAC,OAAO,GAAG,IAAI,CAAC;QAC1B,CAAC;QACD,IAAI,UAAU,CAAC,OAAO,EAAE,CAAC;YACvB,MAAM,CAAC,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;YACjC,UAAU,CAAC,OAAO,GAAG,IAAI,CAAC;QAC5B,CAAC;QACD,aAAa,CAAC,OAAO,CAAC,CAAC;IACzB,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAEtB,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC;AACxC,CAAC"}
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Random walking: the listener strolls from one *playing* instrument to
3
+ * another on its own — picks one that is sounding (not the one it has just
4
+ * visited, when there is a choice), walks up to it and stops a pace or two
5
+ * in front, stands there facing it for a while, then picks another. If the
6
+ * instrument it is heading for or standing at falls silent, it turns to
7
+ * another that is playing; if nothing is playing, it stands where it is.
8
+ *
9
+ * "Playing" carries a short memory: a part rests for a beat between notes,
10
+ * and a wanderer that gave up on every rest would never arrive anywhere.
11
+ * An instrument counts as playing for `RECENT` seconds after it last
12
+ * sounded.
13
+ *
14
+ * Never far from the group: every stop is within the instruments' own
15
+ * spread plus a margin, and never inside one.
16
+ *
17
+ * Pure and frame-rate independent like `stepListener`, which it drives:
18
+ * the platform package owns the frame loop and the random source, this
19
+ * owns the behaviour, so it can be tested with a fixed `rng`.
20
+ */
21
+ import type { UnpluggedListener, UnpluggedPoint } from '@sudobility/music_types';
22
+ import { type MovementSpeed } from './movement';
23
+ export interface WanderInstrument {
24
+ readonly id: string;
25
+ readonly point: UnpluggedPoint;
26
+ /** Has an active note right now. */
27
+ readonly sounding: boolean;
28
+ }
29
+ export interface WanderState {
30
+ /** Where the listener is walking to, or null while standing. */
31
+ readonly target: UnpluggedPoint | null;
32
+ /** The instrument being visited — faced while standing, not chosen twice running. */
33
+ readonly anchorId: string | null;
34
+ /** Elapsed-time value at which standing ends and a new target is picked. */
35
+ readonly dwellUntil: number;
36
+ /** Seconds of wandering so far — the clock `dwellUntil` is measured on. */
37
+ readonly elapsed: number;
38
+ /** When each instrument last sounded, on the same clock. */
39
+ readonly heard: Readonly<Record<string, number>>;
40
+ }
41
+ export declare const IDLE_WANDER: WanderState;
42
+ export interface WanderOptions {
43
+ /** Walking pace. Default: a slow stroll. */
44
+ readonly speed?: MovementSpeed;
45
+ /** How long to stand at each instrument, seconds, [min, max]. */
46
+ readonly dwell?: readonly [number, number];
47
+ /** Closest the listener will stand to an instrument, meters. */
48
+ readonly clearance?: number;
49
+ /** How far beyond the instruments' own spread the listener may go, meters. */
50
+ readonly margin?: number;
51
+ /** Uniform random in [0, 1). Default `Math.random`; tests pass their own. */
52
+ readonly rng?: () => number;
53
+ }
54
+ export declare const WANDER_SPEED: MovementSpeed;
55
+ /** How long after its last note an instrument still counts as playing. */
56
+ export declare const RECENT = 2.5;
57
+ /**
58
+ * The next instrument to visit, from `candidates`, and where to stand for
59
+ * it: a spot a pace or two (`clearance` + 0.3–0.9 m) in front of it on the
60
+ * side the listener is coming from, so the walk heads *toward* it. Never
61
+ * the instrument just visited when there is a choice; never inside any
62
+ * other instrument's clearance (`all`) or outside the group's reach —
63
+ * after ten tries the stand-off point on the direct line is taken as is.
64
+ */
65
+ export declare function pickWanderTarget(candidates: readonly WanderInstrument[], all: readonly WanderInstrument[], from: UnpluggedPoint, previousId: string | null, rng: () => number, clearance?: number, margin?: number): {
66
+ readonly target: UnpluggedPoint;
67
+ readonly anchorId: string;
68
+ };
69
+ /**
70
+ * One frame of wandering. Standing: turns to face the instrument it is
71
+ * visiting and waits out the dwell, then picks the next playing one.
72
+ * Walking: turns toward the target at a rate proportional to how far off
73
+ * it is (so the turn eases out), and moves forward only once roughly
74
+ * facing it, easing in as it lines up and easing out as it arrives.
75
+ * Arrival starts the next dwell. Whichever it is doing, an anchor that
76
+ * has fallen silent is dropped for a playing one at once; with nothing
77
+ * playing it stands still.
78
+ */
79
+ export declare function stepWander(listener: UnpluggedListener, state: WanderState, instruments: readonly WanderInstrument[], deltaSeconds: number, options?: WanderOptions): {
80
+ readonly listener: UnpluggedListener;
81
+ readonly state: WanderState;
82
+ };
83
+ //# sourceMappingURL=wander.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wander.d.ts","sourceRoot":"","sources":["../src/wander.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,KAAK,EACV,iBAAiB,EACjB,cAAc,EACf,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAgB,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AAE9D,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC;IAC/B,oCAAoC;IACpC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,WAAW,WAAW;IAC1B,gEAAgE;IAChE,QAAQ,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI,CAAC;IACvC,qFAAqF;IACrF,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,4EAA4E;IAC5E,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,2EAA2E;IAC3E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,4DAA4D;IAC5D,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CAClD;AAED,eAAO,MAAM,WAAW,EAAE,WAMzB,CAAC;AAEF,MAAM,WAAW,aAAa;IAC5B,4CAA4C;IAC5C,QAAQ,CAAC,KAAK,CAAC,EAAE,aAAa,CAAC;IAC/B,iEAAiE;IACjE,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC3C,gEAAgE;IAChE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,8EAA8E;IAC9E,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,6EAA6E;IAC7E,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CAC7B;AAED,eAAO,MAAM,YAAY,EAAE,aAG1B,CAAC;AACF,0EAA0E;AAC1E,eAAO,MAAM,MAAM,MAAM,CAAC;AA4C1B;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,SAAS,gBAAgB,EAAE,EACvC,GAAG,EAAE,SAAS,gBAAgB,EAAE,EAChC,IAAI,EAAE,cAAc,EACpB,UAAU,EAAE,MAAM,GAAG,IAAI,EACzB,GAAG,EAAE,MAAM,MAAM,EACjB,SAAS,SAAoB,EAC7B,MAAM,SAAiB,GACtB;IAAE,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAiChE;AAED;;;;;;;;;GASG;AACH,wBAAgB,UAAU,CACxB,QAAQ,EAAE,iBAAiB,EAC3B,KAAK,EAAE,WAAW,EAClB,WAAW,EAAE,SAAS,gBAAgB,EAAE,EACxC,YAAY,EAAE,MAAM,EACpB,OAAO,GAAE,aAAkB,GAC1B;IAAE,QAAQ,CAAC,QAAQ,EAAE,iBAAiB,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAA;CAAE,CAyFvE"}
package/dist/wander.js ADDED
@@ -0,0 +1,153 @@
1
+ import { stepListener } from './movement';
2
+ export const IDLE_WANDER = {
3
+ target: null,
4
+ anchorId: null,
5
+ dwellUntil: 0,
6
+ elapsed: 0,
7
+ heard: {},
8
+ };
9
+ export const WANDER_SPEED = {
10
+ metersPerSecond: 0.6,
11
+ degreesPerSecond: 45,
12
+ };
13
+ /** How long after its last note an instrument still counts as playing. */
14
+ export const RECENT = 2.5;
15
+ const DEFAULT_DWELL = [4, 9];
16
+ const DEFAULT_CLEARANCE = 0.9;
17
+ const DEFAULT_MARGIN = 1.5;
18
+ /** Considered arrived within this distance of the target, meters. */
19
+ const ARRIVE = 0.2;
20
+ /** Close enough to facing: stop turning, in degrees. */
21
+ const FACING_DEADBAND = 0.25;
22
+ function normalizeDeg(deg) {
23
+ return ((((deg + 180) % 360) + 360) % 360) - 180;
24
+ }
25
+ function centroid(points) {
26
+ let x = 0;
27
+ let z = 0;
28
+ for (const p of points) {
29
+ x += p.x;
30
+ z += p.z;
31
+ }
32
+ return { x: x / points.length, z: z / points.length };
33
+ }
34
+ function bearingDeg(from, to) {
35
+ return (Math.atan2(to.x - from.x, to.z - from.z) * 180) / Math.PI;
36
+ }
37
+ function turnToward(listener, point, deltaSeconds, speed) {
38
+ const off = normalizeDeg(bearingDeg(listener, point) - listener.facingDeg);
39
+ if (Math.abs(off) < FACING_DEADBAND)
40
+ return { listener, off };
41
+ const turned = stepListener(listener, { forward: 0, strafe: 0, turn: Math.max(-1, Math.min(1, off / 25)) }, deltaSeconds, speed);
42
+ return { listener: turned, off };
43
+ }
44
+ /**
45
+ * The next instrument to visit, from `candidates`, and where to stand for
46
+ * it: a spot a pace or two (`clearance` + 0.3–0.9 m) in front of it on the
47
+ * side the listener is coming from, so the walk heads *toward* it. Never
48
+ * the instrument just visited when there is a choice; never inside any
49
+ * other instrument's clearance (`all`) or outside the group's reach —
50
+ * after ten tries the stand-off point on the direct line is taken as is.
51
+ */
52
+ export function pickWanderTarget(candidates, all, from, previousId, rng, clearance = DEFAULT_CLEARANCE, margin = DEFAULT_MARGIN) {
53
+ const points = all.map(i => i.point);
54
+ const center = centroid(points);
55
+ const reach = Math.max(...points.map(p => Math.hypot(p.x - center.x, p.z - center.z))) +
56
+ margin;
57
+ const choices = candidates.length > 1 && previousId
58
+ ? candidates.filter(i => i.id !== previousId)
59
+ : candidates;
60
+ const anchor = choices[Math.floor(rng() * choices.length)];
61
+ const standOff = clearance + 0.3 + rng() * 0.6;
62
+ const direct = bearingDeg(anchor.point, from);
63
+ let fallback = null;
64
+ for (let attempt = 0; attempt < 10; attempt++) {
65
+ // Straight toward it first; if that spot is taken, fan out around it.
66
+ const spreadDeg = attempt === 0 ? 0 : (rng() - 0.5) * 120;
67
+ const a = ((direct + spreadDeg) * Math.PI) / 180;
68
+ const candidate = {
69
+ x: anchor.point.x + Math.sin(a) * standOff,
70
+ z: anchor.point.z + Math.cos(a) * standOff,
71
+ };
72
+ fallback ?? (fallback = candidate);
73
+ const tooClose = all.some(i => i.id !== anchor.id &&
74
+ Math.hypot(i.point.x - candidate.x, i.point.z - candidate.z) < clearance);
75
+ const tooFar = Math.hypot(candidate.x - center.x, candidate.z - center.z) > reach;
76
+ if (!tooClose && !tooFar)
77
+ return { target: candidate, anchorId: anchor.id };
78
+ }
79
+ return { target: fallback, anchorId: anchor.id };
80
+ }
81
+ /**
82
+ * One frame of wandering. Standing: turns to face the instrument it is
83
+ * visiting and waits out the dwell, then picks the next playing one.
84
+ * Walking: turns toward the target at a rate proportional to how far off
85
+ * it is (so the turn eases out), and moves forward only once roughly
86
+ * facing it, easing in as it lines up and easing out as it arrives.
87
+ * Arrival starts the next dwell. Whichever it is doing, an anchor that
88
+ * has fallen silent is dropped for a playing one at once; with nothing
89
+ * playing it stands still.
90
+ */
91
+ export function stepWander(listener, state, instruments, deltaSeconds, options = {}) {
92
+ const rng = options.rng ?? Math.random;
93
+ const speed = options.speed ?? WANDER_SPEED;
94
+ const dwell = options.dwell ?? DEFAULT_DWELL;
95
+ const elapsed = state.elapsed + deltaSeconds;
96
+ // Remember what sounded, and decide what counts as playing.
97
+ let heard = state.heard;
98
+ for (const i of instruments) {
99
+ if (i.sounding && heard[i.id] !== elapsed)
100
+ heard = { ...heard, [i.id]: elapsed };
101
+ }
102
+ const playing = instruments.filter(i => i.sounding ||
103
+ (heard[i.id] !== undefined && elapsed - heard[i.id] <= RECENT));
104
+ const idle = (extra = {}) => ({
105
+ ...state,
106
+ ...extra,
107
+ elapsed,
108
+ heard,
109
+ });
110
+ if (instruments.length === 0 || playing.length === 0) {
111
+ // Nothing to walk toward. Forget any visit so the next note starts one.
112
+ return {
113
+ listener,
114
+ state: idle({ target: null, anchorId: null, dwellUntil: 0 }),
115
+ };
116
+ }
117
+ const anchorPlaying = state.anchorId !== null && playing.some(i => i.id === state.anchorId);
118
+ let target = anchorPlaying ? state.target : null;
119
+ let anchorId = anchorPlaying ? state.anchorId : null;
120
+ // A silent anchor is dropped: no dwell to wait out, pick afresh now.
121
+ const dwellUntil = anchorPlaying ? state.dwellUntil : 0;
122
+ if (!target) {
123
+ if (elapsed < dwellUntil && anchorId) {
124
+ // Standing: settle on facing the instrument being visited.
125
+ const anchor = instruments.find(i => i.id === anchorId);
126
+ const { listener: turned } = turnToward(listener, anchor.point, deltaSeconds, speed);
127
+ return {
128
+ listener: turned,
129
+ state: idle({ target: null, anchorId, dwellUntil }),
130
+ };
131
+ }
132
+ const pick = pickWanderTarget(playing, instruments, listener, state.anchorId, rng, options.clearance, options.margin);
133
+ target = pick.target;
134
+ anchorId = pick.anchorId;
135
+ }
136
+ const dx = target.x - listener.x;
137
+ const dz = target.z - listener.z;
138
+ const distance = Math.hypot(dx, dz);
139
+ if (distance < ARRIVE) {
140
+ const wait = dwell[0] + rng() * (dwell[1] - dwell[0]);
141
+ return {
142
+ listener,
143
+ state: idle({ target: null, anchorId, dwellUntil: elapsed + wait }),
144
+ };
145
+ }
146
+ const off = normalizeDeg(bearingDeg(listener, target) - listener.facingDeg);
147
+ const turn = Math.max(-1, Math.min(1, off / 25));
148
+ const facingFactor = Math.max(0, 1 - Math.abs(off) / 60);
149
+ const forward = Math.min(1, distance / 1.2) * facingFactor;
150
+ const next = stepListener(listener, { forward, strafe: 0, turn }, deltaSeconds, speed);
151
+ return { listener: next, state: idle({ target, anchorId, dwellUntil }) };
152
+ }
153
+ //# sourceMappingURL=wander.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wander.js","sourceRoot":"","sources":["../src/wander.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "@sudobility/music_spatial_core",
3
+ "version": "0.0.2",
4
+ "description": "Platform-agnostic engine behind the Moosiac Spatial view: instrument geometry, scene composition, camera/listener math, movement, and map-overlay math shared by music_spatial and music_spatial_rn.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "react-native": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js",
14
+ "default": "./dist/index.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "scripts": {
21
+ "build": "tsc -p tsconfig.esm.json",
22
+ "clean": "rimraf dist",
23
+ "test:unit": "vitest run",
24
+ "test:watch": "vitest",
25
+ "lint": "eslint src",
26
+ "format": "prettier --write \"src/**/*.ts\"",
27
+ "format:check": "prettier --check \"src/**/*.ts\"",
28
+ "typecheck": "tsc --noEmit",
29
+ "verify": "bun run format:check && bun run typecheck && bun run lint && bun run test:unit && bun run build",
30
+ "prepublishOnly": "bun run clean && bun run verify"
31
+ },
32
+ "dependencies": {},
33
+ "peerDependencies": {
34
+ "@sudobility/music_types": "^0.13.39",
35
+ "react": ">=18",
36
+ "three": "^0.160.0"
37
+ },
38
+ "devDependencies": {
39
+ "@eslint/js": "^9.38.0",
40
+ "@sudobility/music_types": "^0.13.39",
41
+ "@testing-library/react": "^16.0.0",
42
+ "@types/react": "^19.2.18",
43
+ "@types/react-dom": "^19.2.0",
44
+ "@types/three": "^0.186.0",
45
+ "@typescript-eslint/eslint-plugin": "^8.46.2",
46
+ "@typescript-eslint/parser": "^8.46.2",
47
+ "eslint": "^9.38.0",
48
+ "eslint-config-prettier": "^10.1.8",
49
+ "eslint-plugin-prettier": "^5.5.4",
50
+ "globals": "^16.4.0",
51
+ "jsdom": "^25.0.0",
52
+ "prettier": "^3.6.2",
53
+ "react": "^19.2.8",
54
+ "react-dom": "^19.2.8",
55
+ "rimraf": "^6.0.1",
56
+ "three": "^0.186.0",
57
+ "typescript": "^5.9.3",
58
+ "vitest": "^4.0.15"
59
+ },
60
+ "publishConfig": {
61
+ "access": "public"
62
+ },
63
+ "repository": {
64
+ "type": "git",
65
+ "url": "git+ssh://git@github.com/johnqh/music_spatial_core.git"
66
+ },
67
+ "license": "BUSL-1.1"
68
+ }