@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 +9 -0
- package/dist/colors.d.ts +39 -0
- package/dist/colors.d.ts.map +1 -0
- package/dist/colors.js +67 -0
- package/dist/colors.js.map +1 -0
- package/dist/facing.d.ts +23 -0
- package/dist/facing.d.ts.map +1 -0
- package/dist/facing.js +38 -0
- package/dist/facing.js.map +1 -0
- package/dist/geometry.d.ts +19 -0
- package/dist/geometry.d.ts.map +1 -0
- package/dist/geometry.js +51 -0
- package/dist/geometry.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/map-math.d.ts +61 -0
- package/dist/map-math.d.ts.map +1 -0
- package/dist/map-math.js +61 -0
- package/dist/map-math.js.map +1 -0
- package/dist/movement.d.ts +38 -0
- package/dist/movement.d.ts.map +1 -0
- package/dist/movement.js +43 -0
- package/dist/movement.js.map +1 -0
- package/dist/scene.d.ts +45 -0
- package/dist/scene.d.ts.map +1 -0
- package/dist/scene.js +66 -0
- package/dist/scene.js.map +1 -0
- package/dist/use-throttled-commit.d.ts +11 -0
- package/dist/use-throttled-commit.d.ts.map +1 -0
- package/dist/use-throttled-commit.js +49 -0
- package/dist/use-throttled-commit.js.map +1 -0
- package/dist/wander.d.ts +83 -0
- package/dist/wander.d.ts.map +1 -0
- package/dist/wander.js +153 -0
- package/dist/wander.js.map +1 -0
- package/package.json +68 -0
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).
|
package/dist/colors.d.ts
ADDED
|
@@ -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"}
|
package/dist/facing.d.ts
ADDED
|
@@ -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"}
|
package/dist/geometry.js
ADDED
|
@@ -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"}
|
package/dist/index.d.ts
ADDED
|
@@ -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"}
|
package/dist/map-math.js
ADDED
|
@@ -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"}
|
package/dist/movement.js
ADDED
|
@@ -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"}
|
package/dist/scene.d.ts
ADDED
|
@@ -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"}
|
package/dist/wander.d.ts
ADDED
|
@@ -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":"AAwBA,OAAO,EAAE,YAAY,EAAsB,MAAM,YAAY,CAAC;AAsB9D,MAAM,CAAC,MAAM,WAAW,GAAgB;IACtC,MAAM,EAAE,IAAI;IACZ,QAAQ,EAAE,IAAI;IACd,UAAU,EAAE,CAAC;IACb,OAAO,EAAE,CAAC;IACV,KAAK,EAAE,EAAE;CACV,CAAC;AAeF,MAAM,CAAC,MAAM,YAAY,GAAkB;IACzC,eAAe,EAAE,GAAG;IACpB,gBAAgB,EAAE,EAAE;CACrB,CAAC;AACF,0EAA0E;AAC1E,MAAM,CAAC,MAAM,MAAM,GAAG,GAAG,CAAC;AAC1B,MAAM,aAAa,GAA8B,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AACxD,MAAM,iBAAiB,GAAG,GAAG,CAAC;AAC9B,MAAM,cAAc,GAAG,GAAG,CAAC;AAC3B,qEAAqE;AACrE,MAAM,MAAM,GAAG,GAAG,CAAC;AACnB,wDAAwD;AACxD,MAAM,eAAe,GAAG,IAAI,CAAC;AAE7B,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,SAAS,QAAQ,CAAC,MAAiC;IACjD,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;QACvB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACT,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACX,CAAC;IACD,OAAO,EAAE,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC;AACxD,CAAC;AAED,SAAS,UAAU,CAAC,IAAoB,EAAE,EAAkB;IAC1D,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,GAAG,IAAI,CAAC,EAAE,CAAC;AACpE,CAAC;AAED,SAAS,UAAU,CACjB,QAA2B,EAC3B,KAAqB,EACrB,YAAoB,EACpB,KAAoB;IAEpB,MAAM,GAAG,GAAG,YAAY,CAAC,UAAU,CAAC,QAAQ,EAAE,KAAK,CAAC,GAAG,QAAQ,CAAC,SAAS,CAAC,CAAC;IAC3E,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,eAAe;QAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC;IAC9D,MAAM,MAAM,GAAG,YAAY,CACzB,QAAQ,EACR,EAAE,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,GAAG,EAAE,CAAC,CAAC,EAAE,EACpE,YAAY,EACZ,KAAK,CACN,CAAC;IACF,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;AACnC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,gBAAgB,CAC9B,UAAuC,EACvC,GAAgC,EAChC,IAAoB,EACpB,UAAyB,EACzB,GAAiB,EACjB,SAAS,GAAG,iBAAiB,EAC7B,MAAM,GAAG,cAAc;IAEvB,MAAM,MAAM,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;IACrC,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC;IAChC,MAAM,KAAK,GACT,IAAI,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QACxE,MAAM,CAAC;IACT,MAAM,OAAO,GACX,UAAU,CAAC,MAAM,GAAG,CAAC,IAAI,UAAU;QACjC,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,UAAU,CAAC;QAC7C,CAAC,CAAC,UAAU,CAAC;IACjB,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;IAC3D,MAAM,QAAQ,GAAG,SAAS,GAAG,GAAG,GAAG,GAAG,EAAE,GAAG,GAAG,CAAC;IAC/C,MAAM,MAAM,GAAG,UAAU,CAAC,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IAC9C,IAAI,QAAQ,GAA0B,IAAI,CAAC;IAC3C,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,GAAG,EAAE,EAAE,OAAO,EAAE,EAAE,CAAC;QAC9C,sEAAsE;QACtE,MAAM,SAAS,GAAG,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC;QAC1D,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,GAAG,SAAS,CAAC,GAAG,IAAI,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC;QACjD,MAAM,SAAS,GAAG;YAChB,CAAC,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,QAAQ;YAC1C,CAAC,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,QAAQ;SAC3C,CAAC;QACF,QAAQ,KAAR,QAAQ,GAAK,SAAS,EAAC;QACvB,MAAM,QAAQ,GAAG,GAAG,CAAC,IAAI,CACvB,CAAC,CAAC,EAAE,CACF,CAAC,CAAC,EAAE,KAAK,MAAM,CAAC,EAAE;YAClB,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC,GAAG,SAAS,CAC3E,CAAC;QACF,MAAM,MAAM,GACV,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC;QACrE,IAAI,CAAC,QAAQ,IAAI,CAAC,MAAM;YAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC;IAC9E,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,QAAS,EAAE,QAAQ,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC;AACpD,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,UAAU,CACxB,QAA2B,EAC3B,KAAkB,EAClB,WAAwC,EACxC,YAAoB,EACpB,UAAyB,EAAE;IAE3B,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,MAAM,CAAC;IACvC,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,YAAY,CAAC;IAC5C,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,aAAa,CAAC;IAC7C,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,GAAG,YAAY,CAAC;IAE7C,4DAA4D;IAC5D,IAAI,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;IACxB,KAAK,MAAM,CAAC,IAAI,WAAW,EAAE,CAAC;QAC5B,IAAI,CAAC,CAAC,QAAQ,IAAI,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,OAAO;YACvC,KAAK,GAAG,EAAE,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC;IAC1C,CAAC;IACD,MAAM,OAAO,GAAG,WAAW,CAAC,MAAM,CAChC,CAAC,CAAC,EAAE,CACF,CAAC,CAAC,QAAQ;QACV,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,SAAS,IAAI,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,MAAM,CAAC,CACjE,CAAC;IACF,MAAM,IAAI,GAAG,CAAC,QAA8B,EAAE,EAAe,EAAE,CAAC,CAAC;QAC/D,GAAG,KAAK;QACR,GAAG,KAAK;QACR,OAAO;QACP,KAAK;KACN,CAAC,CAAC;IAEH,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACrD,wEAAwE;QACxE,OAAO;YACL,QAAQ;YACR,KAAK,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,EAAE,CAAC;SAC7D,CAAC;IACJ,CAAC;IAED,MAAM,aAAa,GACjB,KAAK,CAAC,QAAQ,KAAK,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,KAAK,CAAC,QAAQ,CAAC,CAAC;IACxE,IAAI,MAAM,GAAG,aAAa,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;IACjD,IAAI,QAAQ,GAAG,aAAa,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC;IACrD,qEAAqE;IACrE,MAAM,UAAU,GAAG,aAAa,CAAC,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;IAExD,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,IAAI,OAAO,GAAG,UAAU,IAAI,QAAQ,EAAE,CAAC;YACrC,2DAA2D;YAC3D,MAAM,MAAM,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,QAAQ,CAAE,CAAC;YACzD,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,UAAU,CACrC,QAAQ,EACR,MAAM,CAAC,KAAK,EACZ,YAAY,EACZ,KAAK,CACN,CAAC;YACF,OAAO;gBACL,QAAQ,EAAE,MAAM;gBAChB,KAAK,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAC;aACpD,CAAC;QACJ,CAAC;QACD,MAAM,IAAI,GAAG,gBAAgB,CAC3B,OAAO,EACP,WAAW,EACX,QAAQ,EACR,KAAK,CAAC,QAAQ,EACd,GAAG,EACH,OAAO,CAAC,SAAS,EACjB,OAAO,CAAC,MAAM,CACf,CAAC;QACF,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;QACrB,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC;IAC3B,CAAC;IAED,MAAM,EAAE,GAAG,MAAM,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC;IACjC,MAAM,EAAE,GAAG,MAAM,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC;IACjC,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;IACpC,IAAI,QAAQ,GAAG,MAAM,EAAE,CAAC;QACtB,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,GAAG,GAAG,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;QACtD,OAAO;YACL,QAAQ;YACR,KAAK,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,OAAO,GAAG,IAAI,EAAE,CAAC;SACpE,CAAC;IACJ,CAAC;IAED,MAAM,GAAG,GAAG,YAAY,CAAC,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC,GAAG,QAAQ,CAAC,SAAS,CAAC,CAAC;IAC5E,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,GAAG,EAAE,CAAC,CAAC,CAAC;IACjD,MAAM,YAAY,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC;IACzD,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,GAAG,GAAG,CAAC,GAAG,YAAY,CAAC;IAC3D,MAAM,IAAI,GAAG,YAAY,CACvB,QAAQ,EACR,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,EAC5B,YAAY,EACZ,KAAK,CACN,CAAC;IACF,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAC,EAAE,CAAC;AAC3E,CAAC"}
|
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
|
+
}
|