@chatlychat/world 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/LICENSE +21 -0
  3. package/README.md +184 -2
  4. package/dist/agent.d.ts +63 -0
  5. package/dist/agent.d.ts.map +1 -0
  6. package/dist/agent.js +251 -0
  7. package/dist/agent.js.map +1 -0
  8. package/dist/bubble.d.ts +36 -0
  9. package/dist/bubble.d.ts.map +1 -0
  10. package/dist/bubble.js +108 -0
  11. package/dist/bubble.js.map +1 -0
  12. package/dist/capabilities.d.ts +77 -0
  13. package/dist/capabilities.d.ts.map +1 -0
  14. package/dist/capabilities.js +129 -0
  15. package/dist/capabilities.js.map +1 -0
  16. package/dist/cards.d.ts +51 -0
  17. package/dist/cards.d.ts.map +1 -0
  18. package/dist/cards.js +136 -0
  19. package/dist/cards.js.map +1 -0
  20. package/dist/composer.d.ts +25 -0
  21. package/dist/composer.d.ts.map +1 -0
  22. package/dist/composer.js +53 -0
  23. package/dist/composer.js.map +1 -0
  24. package/dist/handlers.d.ts +26 -0
  25. package/dist/handlers.d.ts.map +1 -0
  26. package/dist/handlers.js +41 -0
  27. package/dist/handlers.js.map +1 -0
  28. package/dist/index.d.ts +27 -0
  29. package/dist/index.d.ts.map +1 -0
  30. package/dist/index.js +25 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/input.d.ts +46 -0
  33. package/dist/input.d.ts.map +1 -0
  34. package/dist/input.js +89 -0
  35. package/dist/input.js.map +1 -0
  36. package/dist/layout.d.ts +24 -0
  37. package/dist/layout.d.ts.map +1 -0
  38. package/dist/layout.js +69 -0
  39. package/dist/layout.js.map +1 -0
  40. package/dist/model.d.ts +57 -0
  41. package/dist/model.d.ts.map +1 -0
  42. package/dist/model.js +86 -0
  43. package/dist/model.js.map +1 -0
  44. package/dist/motion.d.ts +27 -0
  45. package/dist/motion.d.ts.map +1 -0
  46. package/dist/motion.js +65 -0
  47. package/dist/motion.js.map +1 -0
  48. package/dist/panel.d.ts +77 -0
  49. package/dist/panel.d.ts.map +1 -0
  50. package/dist/panel.js +214 -0
  51. package/dist/panel.js.map +1 -0
  52. package/dist/picks.d.ts +45 -0
  53. package/dist/picks.d.ts.map +1 -0
  54. package/dist/picks.js +186 -0
  55. package/dist/picks.js.map +1 -0
  56. package/dist/selection.d.ts +14 -0
  57. package/dist/selection.d.ts.map +1 -0
  58. package/dist/selection.js +21 -0
  59. package/dist/selection.js.map +1 -0
  60. package/dist/text.d.ts +26 -0
  61. package/dist/text.d.ts.map +1 -0
  62. package/dist/text.js +100 -0
  63. package/dist/text.js.map +1 -0
  64. package/dist/theme-apply.d.ts +19 -0
  65. package/dist/theme-apply.d.ts.map +1 -0
  66. package/dist/theme-apply.js +88 -0
  67. package/dist/theme-apply.js.map +1 -0
  68. package/dist/types.d.ts +108 -0
  69. package/dist/types.d.ts.map +1 -0
  70. package/dist/types.js +41 -0
  71. package/dist/types.js.map +1 -0
  72. package/dist/visemes.d.ts +28 -0
  73. package/dist/visemes.d.ts.map +1 -0
  74. package/dist/visemes.js +42 -0
  75. package/dist/visemes.js.map +1 -0
  76. package/dist/vrm.d.ts +97 -0
  77. package/dist/vrm.d.ts.map +1 -0
  78. package/dist/vrm.js +212 -0
  79. package/dist/vrm.js.map +1 -0
  80. package/dist/world-api.d.ts +39 -0
  81. package/dist/world-api.d.ts.map +1 -0
  82. package/dist/world-api.js +2 -0
  83. package/dist/world-api.js.map +1 -0
  84. package/dist/world.d.ts +5 -0
  85. package/dist/world.d.ts.map +1 -0
  86. package/dist/world.js +414 -0
  87. package/dist/world.js.map +1 -0
  88. package/package.json +75 -3
package/CHANGELOG.md ADDED
@@ -0,0 +1,26 @@
1
+ # @chatlychat/world
2
+
3
+ ## 0.2.0 — 2026-10-26
4
+
5
+ Not yet published.
6
+
7
+ - VRM avatars. An avatar with `format: 'vrm'`, or a glTF carrying the `VRM` / `VRMC_vrm` extension, loads through `@pixiv/three-vrm`'s `VRMLoaderPlugin`. `@pixiv/three-vrm` (`^3.5.5`) is an OPTIONAL peer dependency, loaded by dynamic import; a glTF-only host does not install or bundle it. Missing, it is a `WorldError` with code `missing_peer` naming the package. Bundlers: pass `loadVrm: () => import('@pixiv/three-vrm')`.
8
+ - The mouth on a VRM avatar is driven through the `aa` / `ih` / `ou` / `ee` / `oh` expression bindings (`VISEME_TO_VRM`), not morph targets.
9
+ - `embodiment.expression` maps to the `happy`, `sad` and `surprised` presets (`EXPRESSION_TO_VRM`); gestures stay clips. Automatic blink (off under reduced motion).
10
+ - `lookAt` drives VRM's `lookAt.target` and the humanoid head bone. `vrm.update(delta)` runs every frame.
11
+ - `ModelLoader` takes an optional second argument `{ format }`; `LoadedModel` has an optional `vrm`; `WorldErrorCode` gains `missing_peer`; new `loadVrm` option and `gltfLoader({ loadVrm })`.
12
+ - The glTF morph-target path is unchanged.
13
+
14
+ ## 0.1.0 — 2026-10-25
15
+
16
+ Unpublished. The first version: a Chatly conversation rendered into your three.js scene.
17
+
18
+ - `createWorld({ conversation, camera, … })` → `world.root` (add it to your scene) and `world.update(dt)` (call it from your loop).
19
+ - Picks: a `card_grid` laid out as `arc`, `shelf`, `wall` or `table`; `card.model` loaded with three's `GLTFLoader` (scale and anchor honoured); pedestal, beam, price label and selection ring from `worldTheme`. Tapping a pick selects it in the conversation (a postback) and the selection is shared with the host.
20
+ - Speech bubbles anchored to a speaker (never head-locked), scaled by distance, with a typing indicator.
21
+ - A world-space panel and composer; `fixed`, `lazy` and `wrist` follow; a `locked` placement is honoured as `lazy`.
22
+ - Hand, controller (`attachController`), gaze-dwell (`attachGaze`) and pointer (`pickAtPointer`) picking. The host keeps input.
23
+ - An embodied agent body: avatar model, `idle`/`listening`/`thinking`/`speaking` clips, expression and `lookAt` cues, viseme-driven mouth when a message carries `metadata.visemes`.
24
+ - Host capabilities: `world.walk_to`, `avatar.try_on`, `scene.highlight`, `scene.spawn` and custom names, dispatched to handlers you register; bot-fired invocations are answered into the thread; every action keeps its `equivalent`.
25
+ - Replaceable `renderCard`, `renderPick`, `renderBubble`, `renderPanel`, `renderComposer`, `renderAgent`, with a named, versioned object contract (`WORLD_CONTRACT`).
26
+ - `worldTheme` applied to panel, type, cards, speech, agent, sound cues and motion; reduced motion always wins.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Chatly Engineering
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,185 @@
1
- # Temporary Holding Version
1
+ # @chatlychat/world
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A Chatly conversation, rendered into **your** three.js scene. You keep the scene,
4
+ the avatars, the input and the wayfinding; this draws what the conversation
5
+ contains and sends the visitor's taps back.
6
+
7
+ ```bash
8
+ pnpm add @chatlychat/world @chatlychat/core three
9
+ ```
10
+
11
+ `three` (>= 0.160) and `@chatlychat/core` are peer dependencies. The only other
12
+ is **optional**: `@pixiv/three-vrm`, for VRM avatars (see below). Nothing here
13
+ touches the web widget.
14
+
15
+ ```ts
16
+ import { createChatlyCore } from '@chatlychat/core';
17
+ import { createWorld } from '@chatlychat/world';
18
+ import { Clock } from 'three';
19
+
20
+ const core = createChatlyCore({ publicId: 'pub_xxx', socket: io });
21
+ const conversation = await core.startConversation('Do you have this in blue?');
22
+
23
+ const world = createWorld({
24
+ conversation,
25
+ camera, // the camera (or XR camera) the visitor sees through
26
+ theme: core.worldTheme, // the channel's worldTheme
27
+ scene: 'light', // what your scene's light is, for colorScheme "auto"
28
+ reducedMotion: () => false, // return your viewer's reduced-motion setting
29
+ presences: { '*': { name: 'Mia', avatar: { url: 'https://cdn.example/mia.glb' } } },
30
+ });
31
+ scene.add(world.root);
32
+
33
+ const clock = new Clock();
34
+ renderer.setAnimationLoop(() => {
35
+ world.update(clock.getDelta()); // seconds
36
+ renderer.render(scene, camera);
37
+ });
38
+ ```
39
+
40
+ ## What it draws
41
+
42
+ - **Picks.** A `card_grid` with `placement` `arc`, `shelf`, `wall` or `table`;
43
+ a card with a `model` becomes a glTF object (`scale` and `anchor` honoured)
44
+ with the theme's pedestal, beam, price label and selection ring. Tapping one
45
+ selects it: a postback to the conversation, a ring on the card, the object in
46
+ hand. `world.selection` is shared — selecting from your own UI is the same event
47
+ (`await world.select(key)`; keys from `world.cardKeys()`).
48
+ - **Speech bubbles** over a speaker, turned to the viewer and scaled by distance —
49
+ positioned from the speaker, never from the camera. Typing indicator per
50
+ `typingLook`. History is not replayed as speech.
51
+ - **A world-space panel and composer.** Never head-locked: `fixed`, `lazy` (an
52
+ eased follow with a dead zone) or `wrist` (carried on a controller you pass).
53
+ `follow: 'locked'` in a placement is honoured as `lazy`.
54
+ Typing is yours: fill `world.composer.setDraft(text)` from your keyboard, mic or
55
+ overlay (`onComposeRequest` fires when the field is pressed), then it sends.
56
+ - **An agent body**: avatar model (glTF or VRM; `usdz` is refused with a typed error),
57
+ `idle`/`listening`/`thinking`/`speaking` clips from `presence.clips`, one-shot
58
+ `expressions`, `lookAt` cues resolved through `anchors.resolve(ref)`, and a mouth
59
+ driven by `message.metadata.visemes` (`[{ t: ms, v: 'aa', w?: 0..1 }]`) when
60
+ present. `presence.idle` and `personalSpaceMetres` are yours to honour.
61
+
62
+ ## VRM avatars
63
+
64
+ An avatar whose `format` is `vrm` (or whose file carries the VRM extension) is
65
+ loaded through `@pixiv/three-vrm`'s `VRMLoaderPlugin`. The package is an
66
+ **optional** peer dependency: a glTF-only host does not install it, and nothing
67
+ of it is in your bundle.
68
+
69
+ ```bash
70
+ npm install @pixiv/three-vrm # ^3.5.5; VRM avatars only
71
+ ```
72
+
73
+ If you bundle, hand the import over so your bundler can see the package:
74
+
75
+ ```ts
76
+ createWorld({ conversation, camera, loadVrm: () => import('@pixiv/three-vrm') });
77
+ ```
78
+
79
+ Without it installed, a VRM avatar reports a `WorldError` with code `missing_peer`
80
+ to `onError`, names the package, and the placeholder body stays.
81
+
82
+ **Mouth.** Chatly visemes drive VRM's five expression bindings
83
+ (`aa`, `ih`, `ou`, `ee`, `oh`) instead of morph targets. Names are case-insensitive
84
+ and a `viseme_` prefix is ignored.
85
+
86
+ | Chatly viseme | VRM expression | Opening |
87
+ | ----------------------------- | -------------- | -------------------------- |
88
+ | `aa` | `aa` | full |
89
+ | `ih`, `I` | `ih` | full |
90
+ | `ou`, `U` | `ou` | full |
91
+ | `ee`, `E` | `ee` | full |
92
+ | `oh`, `O` | `oh` | full |
93
+ | `FF`, `TH`, `SS` | `ih` | 0.4, 0.5, 0.5 |
94
+ | `DD`, `kk` | `aa` | 0.4 |
95
+ | `CH`, `RR` | `ou` | 0.5 |
96
+ | `nn` | `ih` | 0.3 |
97
+ | `PP`, `sil`, anything unknown | none | closed (all five set to 0) |
98
+
99
+ The opening is multiplied by the track's own `w`. The five are always set
100
+ together, so a released mouth closes.
101
+
102
+ **Face.** `embodiment.expression` maps to a VRM preset where one exists:
103
+
104
+ | Expression | Preset | Weight |
105
+ | ----------- | ----------- | ------ |
106
+ | `smile` | `happy` | 0.7 |
107
+ | `laugh` | `happy` | 1 |
108
+ | `surprised` | `surprised` | 1 |
109
+ | `concerned` | `sad` | 0.6 |
110
+
111
+ The face eases in over 0.12 s, holds 2.5 s and fades over 0.5 s. `nod`,
112
+ `shake_head`, `point`, `wave`, `shrug` and `think` are gestures with no preset:
113
+ they play only as your own clip from `presence.expressions`, as on a glTF avatar.
114
+ A preset and a host clip both play if both exist. No Chatly expression is
115
+ `angry` or `relaxed`, so those presets are never driven. `blink` is automatic
116
+ (every 4 s) and off under reduced motion; there, a face is applied and removed
117
+ without easing.
118
+
119
+ **Gaze.** A `lookAt` cue sets VRM's `lookAt.target` (eyes) and turns the humanoid
120
+ `head` bone toward it, clamped to about 20 degrees up or down and 34 degrees sideways.
121
+ The body still turns on the spot as before. **Clips.** State and expression clips
122
+ in the file play on the model as they do for glTF; the SDK does not retarget
123
+ clips from other rigs (such as separate VRMA files) — load those yourself.
124
+ `vrm.update(delta)` runs every `world.update`.
125
+
126
+ The glTF morph-target path (`v`, `viseme_v`, `Viseme_v`) is unchanged.
127
+
128
+ ## Input
129
+
130
+ The host owns input; the world only knows which of its objects a ray hit.
131
+
132
+ ```ts
133
+ world.attachController(xrController); // your XR controller object: trigger or hand pinch
134
+ const gaze = world.attachGaze({ dwellMs: 1500 }); // call gaze.update(ms) per frame
135
+ const hit = world.pickAtPointer(ndcX, ndcY); // mouse / touch
136
+ if (hit) world.activate(hit);
137
+ ```
138
+
139
+ ## Host capabilities
140
+
141
+ ```ts
142
+ world.capabilities.register(
143
+ 'world.walk_to', // also: avatar.try_on, scene.highlight, scene.spawn, or your own
144
+ async () => true, // do your own movement here; return false or throw to refuse
145
+ { equivalent: { kind: 'text', text: 'The shop is on level 2.' }, unprompted: true },
146
+ );
147
+ await core.setSurface(world.capabilities.surface({ room: 'atrium' }));
148
+ ```
149
+
150
+ A bot-fired invocation reaches your handler and is answered into the thread
151
+ (`done`, or `failed` + `refused: true`). The `equivalent` is required and is
152
+ announced (`announce`) whenever the handler is missing, refuses or throws.
153
+
154
+ ## Replaceable renderers
155
+
156
+ `renderCard`, `renderPick`, `renderBubble`, `renderPanel`, `renderComposer`,
157
+ `renderAgent` — each receives the typed block (or message, or agent view), the
158
+ resolved `theme` and `placement`, `reducedMotion`, and for blocks the Chatly
159
+ handlers `select`, `press` and `react`. Return any `Object3D`.
160
+
161
+ ```ts
162
+ createWorld({ …, renderers: { contract: 1, renderCard: (p) => yourShelfMesh } });
163
+ ```
164
+
165
+ The world names an override's node `cl3-card` etc. if you did not, and routes
166
+ taps on it to selection (set `userData.cl3Handled = true` to take taps yourself).
167
+ Object names and the renderer set are versioned in `WORLD_CONTRACT`; pinning
168
+ `contract: N` fails loudly on a world that speaks another N.
169
+
170
+ ## Theme and reduced motion
171
+
172
+ Every `worldTheme` token is applied: panel material/colour/emissive, type
173
+ (`fontUrl`, weight, outline, `textScale`), cards, speech, agent, sound (as
174
+ `onSound` cues — you play them) and motion. `world.setTheme`, `setScene` and
175
+ `setReducedMotion` re-apply live. **Reduced motion always wins:** entrances and
176
+ trails off, lazy follow and gaze snap, expression clips skipped, clips held on
177
+ their first frame.
178
+
179
+ ## Tests without WebGL
180
+
181
+ three builds a scene graph with no GL context, so you can test your integration in
182
+ Node: see `src/world.spec.ts` for a fake conversation and the assertions.
183
+ `pnpm --filter @chatlychat/world example` runs a scripted demo.
184
+
185
+ MIT.
@@ -0,0 +1,63 @@
1
+ /**
2
+ * The embodied agent's body: a model, four state clips, one-shot expressions,
3
+ * a gaze and a mouth.
4
+ *
5
+ * What it does NOT do is walk. `presence.idle` (`stand`/`wander`/`follow`) and
6
+ * `personalSpaceMetres` are facts about where the agent may go, and wayfinding
7
+ * is the host's — they are readable on `presence` and honoured by nobody here.
8
+ *
9
+ * Reduced motion always wins: state clips are held on their first frame (a
10
+ * pose, not a loop), expression one-shots are not played, gaze snaps rather
11
+ * than turns, and there is no crossfade. The mouth still follows visemes —
12
+ * speech is not decorative motion, and a mouth that never moves on a speaking
13
+ * agent would read as a broken one.
14
+ */
15
+ import { Group, Vector3, type Camera, type Object3D } from 'three';
16
+ import { type ModelLoader } from './model.js';
17
+ import { type VisemeTrack } from './visemes.js';
18
+ import type { AgentExpression, AgentPresenceState, AgentWorldPresence } from '@chatlychat/core';
19
+ import type { ResolvedWorldTheme } from '@chatlychat/core';
20
+ export type GazeTarget = Object3D | Vector3 | 'visitor' | null;
21
+ export declare class AgentBody {
22
+ private theme;
23
+ private readonly loadModel;
24
+ private readonly onError;
25
+ readonly root: Group;
26
+ presence: AgentWorldPresence | null;
27
+ reduced: boolean;
28
+ private model;
29
+ private mixer;
30
+ private clips;
31
+ private current;
32
+ private avatarUrl;
33
+ private tag;
34
+ private gaze;
35
+ private track;
36
+ private trackStart;
37
+ private morphs;
38
+ private rig;
39
+ private lastState;
40
+ private refused;
41
+ constructor(theme: ResolvedWorldTheme, loadModel: ModelLoader, onError: (e: unknown) => void, reduced: boolean);
42
+ get hasModel(): boolean;
43
+ get activeClip(): string | null;
44
+ setTheme(theme: ResolvedWorldTheme): void;
45
+ private placeholder;
46
+ setPresence(presence: AgentWorldPresence | null): void;
47
+ private setTag;
48
+ private swapModel;
49
+ private action;
50
+ /** Move to the state's clip — the host's own name for it, from `presence.clips`. */
51
+ setState(state: AgentPresenceState): void;
52
+ /** Play a one-shot expression clip. Skipped entirely under reduced motion. */
53
+ playExpression(clipName: string | null): boolean;
54
+ /** A face for a cue, on a VRM avatar. False for a gesture or a glTF avatar: the host's clip is all there is. */
55
+ express(e: AgentExpression | null): boolean;
56
+ get isVrm(): boolean;
57
+ lookAt(target: GazeTarget): void;
58
+ setVisemes(track: VisemeTrack | null, nowMs: number): void;
59
+ private applyViseme;
60
+ update(dtMs: number, nowMs: number, camera: Camera): void;
61
+ dispose(): void;
62
+ }
63
+ //# sourceMappingURL=agent.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAIL,KAAK,EAIL,OAAO,EAEP,KAAK,MAAM,EACX,KAAK,QAAQ,EACd,MAAM,OAAO,CAAC;AAEf,OAAO,EAAY,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AAKxD,OAAO,EAA2B,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAGzE,OAAO,KAAK,EAAE,eAAe,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAChG,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAK3D,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,OAAO,GAAG,SAAS,GAAG,IAAI,CAAC;AAO/D,qBAAa,SAAS;IAmBlB,OAAO,CAAC,KAAK;IACb,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,OAAO;IApB1B,SAAgB,IAAI,QAAe;IAC5B,QAAQ,EAAE,kBAAkB,GAAG,IAAI,CAAQ;IAC3C,OAAO,EAAE,OAAO,CAAC;IACxB,OAAO,CAAC,KAAK,CAAyB;IACtC,OAAO,CAAC,KAAK,CAA+B;IAC5C,OAAO,CAAC,KAAK,CAAgC;IAC7C,OAAO,CAAC,OAAO,CAAgC;IAC/C,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,GAAG,CAAyB;IACpC,OAAO,CAAC,IAAI,CAAoB;IAChC,OAAO,CAAC,KAAK,CAA4B;IACzC,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,MAAM,CAAmB;IACjC,OAAO,CAAC,GAAG,CAAuB;IAClC,OAAO,CAAC,SAAS,CAAmC;IACpD,OAAO,CAAC,OAAO,CAAuB;gBAG5B,KAAK,EAAE,kBAAkB,EAChB,SAAS,EAAE,WAAW,EACtB,OAAO,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,IAAI,EAC9C,OAAO,EAAE,OAAO;IAOlB,IAAW,QAAQ,IAAI,OAAO,CAE7B;IAED,IAAW,UAAU,IAAI,MAAM,GAAG,IAAI,CAErC;IAEM,QAAQ,CAAC,KAAK,EAAE,kBAAkB,GAAG,IAAI;IAIhD,OAAO,CAAC,WAAW;IAWZ,WAAW,CAAC,QAAQ,EAAE,kBAAkB,GAAG,IAAI,GAAG,IAAI;IAiC7D,OAAO,CAAC,MAAM;IAad,OAAO,CAAC,SAAS;IAkCjB,OAAO,CAAC,MAAM;IAQd,oFAAoF;IAC7E,QAAQ,CAAC,KAAK,EAAE,kBAAkB,GAAG,IAAI;IAiBhD,8EAA8E;IACvE,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO;IAYvD,gHAAgH;IACzG,OAAO,CAAC,CAAC,EAAE,eAAe,GAAG,IAAI,GAAG,OAAO;IAIlD,IAAW,KAAK,IAAI,OAAO,CAE1B;IAEM,MAAM,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI;IAIhC,UAAU,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAQjE,OAAO,CAAC,WAAW;IA0BZ,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAiCzD,OAAO,IAAI,IAAI;CAIvB"}
package/dist/agent.js ADDED
@@ -0,0 +1,251 @@
1
+ /**
2
+ * The embodied agent's body: a model, four state clips, one-shot expressions,
3
+ * a gaze and a mouth.
4
+ *
5
+ * What it does NOT do is walk. `presence.idle` (`stand`/`wander`/`follow`) and
6
+ * `personalSpaceMetres` are facts about where the agent may go, and wayfinding
7
+ * is the host's — they are readable on `presence` and honoured by nobody here.
8
+ *
9
+ * Reduced motion always wins: state clips are held on their first frame (a
10
+ * pose, not a loop), expression one-shots are not played, gaze snaps rather
11
+ * than turns, and there is no crossfade. The mouth still follows visemes —
12
+ * speech is not decorative motion, and a mouth that never moves on a speaking
13
+ * agent would read as a broken one.
14
+ */
15
+ import { AnimationClip, AnimationMixer, CapsuleGeometry, Group, LoopOnce, Mesh, SphereGeometry, Vector3, } from 'three';
16
+ import { fitModel } from './model.js';
17
+ import { wrapAngle } from './panel.js';
18
+ import { createTextMesh } from './text.js';
19
+ import { accentMaterial } from './theme-apply.js';
20
+ import { WORLD_CONTRACT, WorldError } from './types.js';
21
+ import { morphNamesFor, visemeAt } from './visemes.js';
22
+ import { gazePoint, VrmRig } from './vrm.js';
23
+ const TURN_RATE = 6;
24
+ const FADE_S = 0.25;
25
+ export class AgentBody {
26
+ theme;
27
+ loadModel;
28
+ onError;
29
+ root = new Group();
30
+ presence = null;
31
+ reduced;
32
+ model = null;
33
+ mixer = null;
34
+ clips = [];
35
+ current = null;
36
+ avatarUrl = null;
37
+ tag = null;
38
+ gaze = null;
39
+ track = null;
40
+ trackStart = 0;
41
+ morphs = [];
42
+ rig = null;
43
+ lastState = null;
44
+ refused = null;
45
+ constructor(theme, loadModel, onError, reduced) {
46
+ this.theme = theme;
47
+ this.loadModel = loadModel;
48
+ this.onError = onError;
49
+ this.reduced = reduced;
50
+ this.root.name = WORLD_CONTRACT.objects.agent;
51
+ this.root.add(this.placeholder());
52
+ }
53
+ get hasModel() {
54
+ return this.model !== null;
55
+ }
56
+ get activeClip() {
57
+ return this.current?.getClip().name ?? null;
58
+ }
59
+ setTheme(theme) {
60
+ this.theme = theme;
61
+ }
62
+ placeholder() {
63
+ const g = new Group();
64
+ g.name = 'cl3-agent-placeholder';
65
+ const body = new Mesh(new CapsuleGeometry(0.2, 0.7, 4, 12), accentMaterial(this.theme));
66
+ body.position.y = 0.55;
67
+ const head = new Mesh(new SphereGeometry(0.17, 16, 12), accentMaterial(this.theme));
68
+ head.position.y = 1.3;
69
+ g.add(body, head);
70
+ return g;
71
+ }
72
+ setPresence(presence) {
73
+ this.presence = presence;
74
+ this.setTag(presence === null ? null : (presence.nameTag ?? presence.name));
75
+ const a = presence?.avatar;
76
+ if (a?.format === 'usdz' && this.refused !== a.url) {
77
+ this.refused = a.url;
78
+ this.onError(new WorldError('unsupported_format', 'usdz avatars are not loadable by three.js; showing the placeholder'));
79
+ }
80
+ const url = a !== undefined && a.format !== 'usdz' ? a.url : null;
81
+ if (url === this.avatarUrl) {
82
+ return;
83
+ }
84
+ this.avatarUrl = url;
85
+ if (url === null || a === undefined) {
86
+ this.swapModel(null, []);
87
+ return;
88
+ }
89
+ this.loadModel(url, a.format === undefined ? undefined : { format: a.format }).then((m) => {
90
+ if (this.avatarUrl === url) {
91
+ this.swapModel(fitModel(m.scene, a.scale ?? 1, a.anchor ?? 'base'), m.animations, m.vrm ?? null);
92
+ }
93
+ }, this.onError);
94
+ }
95
+ setTag(text) {
96
+ if (this.tag !== null) {
97
+ this.root.remove(this.tag);
98
+ this.tag = null;
99
+ }
100
+ if (text !== null) {
101
+ this.tag = createTextMesh(text, this.theme, { maxWidthMetres: 0.8, size: 0.8 });
102
+ this.tag.name = 'cl3-agent-nametag';
103
+ this.tag.position.y = 1.75;
104
+ this.root.add(this.tag);
105
+ }
106
+ }
107
+ swapModel(model, clips, vrm = null) {
108
+ this.mixer?.stopAllAction();
109
+ this.mixer = null;
110
+ this.current = null;
111
+ for (const c of [...this.root.children]) {
112
+ if (c !== this.tag) {
113
+ this.root.remove(c);
114
+ }
115
+ }
116
+ this.model = model;
117
+ this.clips = clips;
118
+ this.morphs = [];
119
+ this.rig = vrm === null || model === null ? null : new VrmRig(vrm, this.reduced);
120
+ if (model === null) {
121
+ this.root.add(this.placeholder());
122
+ return;
123
+ }
124
+ this.root.add(model);
125
+ model.traverse((o) => {
126
+ if (o.morphTargetDictionary !== undefined) {
127
+ this.morphs.push(o);
128
+ }
129
+ });
130
+ this.mixer = new AnimationMixer(model);
131
+ // The state was set before the model arrived; its clip could not play then.
132
+ if (this.lastState !== null) {
133
+ this.setState(this.lastState);
134
+ }
135
+ }
136
+ action(name) {
137
+ if (this.mixer === null || name === null || name === undefined) {
138
+ return null;
139
+ }
140
+ const clip = AnimationClip.findByName([...this.clips], name);
141
+ return clip === null ? null : this.mixer.clipAction(clip);
142
+ }
143
+ /** Move to the state's clip — the host's own name for it, from `presence.clips`. */
144
+ setState(state) {
145
+ this.lastState = state;
146
+ const next = this.action(this.presence?.clips?.[state]);
147
+ if (next === null || next === this.current) {
148
+ return;
149
+ }
150
+ next.reset();
151
+ next.play();
152
+ if (this.reduced) {
153
+ next.paused = true; // a held pose, not a loop
154
+ this.current?.stop();
155
+ }
156
+ else if (this.current !== null) {
157
+ next.crossFadeFrom(this.current, FADE_S, false);
158
+ }
159
+ this.current = next;
160
+ }
161
+ /** Play a one-shot expression clip. Skipped entirely under reduced motion. */
162
+ playExpression(clipName) {
163
+ const a = this.reduced ? null : this.action(clipName);
164
+ if (a === null) {
165
+ return false;
166
+ }
167
+ a.reset();
168
+ a.setLoop(LoopOnce, 1);
169
+ a.clampWhenFinished = true;
170
+ a.play();
171
+ return true;
172
+ }
173
+ /** A face for a cue, on a VRM avatar. False for a gesture or a glTF avatar: the host's clip is all there is. */
174
+ express(e) {
175
+ return this.rig?.express(e) ?? false;
176
+ }
177
+ get isVrm() {
178
+ return this.rig !== null;
179
+ }
180
+ lookAt(target) {
181
+ this.gaze = target;
182
+ }
183
+ setVisemes(track, nowMs) {
184
+ this.track = track;
185
+ this.trackStart = nowMs;
186
+ if (track === null) {
187
+ this.applyViseme(null, 0);
188
+ }
189
+ }
190
+ applyViseme(v, w) {
191
+ if (this.rig !== null) {
192
+ this.rig.setMouth(v, w);
193
+ return;
194
+ }
195
+ for (const m of this.morphs) {
196
+ const dict = m.morphTargetDictionary;
197
+ const inf = m.morphTargetInfluences;
198
+ if (dict === undefined || inf === undefined) {
199
+ continue;
200
+ }
201
+ // Zero every viseme-ish target first so a released mouth closes.
202
+ for (const [name, idx] of Object.entries(dict)) {
203
+ if (/^(viseme_|Viseme_)/.test(name) || ['aa', 'ih', 'ou', 'ee', 'oh'].includes(name)) {
204
+ inf[idx] = 0;
205
+ }
206
+ }
207
+ if (v !== null) {
208
+ const hit = morphNamesFor(v).find((n) => dict[n] !== undefined);
209
+ if (hit !== undefined) {
210
+ inf[dict[hit]] = w;
211
+ }
212
+ }
213
+ }
214
+ }
215
+ update(dtMs, nowMs, camera) {
216
+ this.mixer?.update(dtMs / 1000);
217
+ if (this.track !== null) {
218
+ const e = visemeAt(this.track, nowMs - this.trackStart);
219
+ this.applyViseme(e?.v ?? null, e?.w ?? 0);
220
+ }
221
+ if (this.gaze !== null) {
222
+ const here = this.root.getWorldPosition(new Vector3());
223
+ const at = this.gaze === 'visitor'
224
+ ? camera.getWorldPosition(new Vector3())
225
+ : this.gaze instanceof Vector3
226
+ ? this.gaze
227
+ : this.gaze.getWorldPosition(new Vector3());
228
+ const want = Math.atan2(at.x - here.x, at.z - here.z);
229
+ const err = wrapAngle(want - this.root.rotation.y);
230
+ const step = TURN_RATE * (dtMs / 1000);
231
+ this.root.rotation.y =
232
+ this.reduced || Math.abs(err) <= step ? want : this.root.rotation.y + Math.sign(err) * step;
233
+ }
234
+ if (this.rig !== null) {
235
+ this.rig.reduced = this.reduced;
236
+ this.rig.gazeAt(this.root, gazePoint(this.gaze, camera), dtMs / 1000);
237
+ this.rig.update(dtMs / 1000);
238
+ }
239
+ if (this.tag !== null) {
240
+ // The tag turns to the viewer regardless of where the body faces.
241
+ const cam = camera.getWorldPosition(new Vector3());
242
+ const t = this.tag.getWorldPosition(new Vector3());
243
+ this.tag.rotation.y = Math.atan2(cam.x - t.x, cam.z - t.z) - this.root.rotation.y;
244
+ }
245
+ }
246
+ dispose() {
247
+ this.mixer?.stopAllAction();
248
+ this.root.removeFromParent();
249
+ }
250
+ }
251
+ //# sourceMappingURL=agent.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent.js","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Speech bubbles, anchored to a speaker in space.
3
+ *
4
+ * A bubble is positioned from its SPEAKER'S world position — never from the
5
+ * camera, so it cannot be head-locked — and only turned to face the viewer.
6
+ * Its size grows with distance (clamped) so type stays legible across a room,
7
+ * which is why the text itself is built at one size and the whole bubble is
8
+ * scaled.
9
+ */
10
+ import { Group, Vector3, type Camera, type Object3D } from 'three';
11
+ import type { BubbleRenderProps, ConversationMessage, ResolvedWorldTheme } from '@chatlychat/core';
12
+ export declare const MAX_BUBBLES = 3;
13
+ /** Uniform scale for a bubble `distance` metres from the viewer. */
14
+ export declare const bubbleScale: (distance: number) => number;
15
+ export declare function defaultBubble(props: BubbleRenderProps): Object3D;
16
+ /** The typing indicator: three dots, one pulsing dot, or nothing (`avatar`/`none` live on the body). */
17
+ export declare function createTyping(theme: ResolvedWorldTheme): Group;
18
+ /** Bob or pulse the dots. With reduced motion they sit still: a static ellipsis still says "typing". */
19
+ export declare function animateTyping(typing: Object3D, nowMs: number, reduced: boolean): void;
20
+ /** World-space point above a speaker's head. */
21
+ export declare function headPoint(speaker: Object3D): Vector3;
22
+ export interface LiveBubble {
23
+ readonly messageId: string;
24
+ readonly speaker: Object3D;
25
+ node: Object3D;
26
+ body: string;
27
+ readonly bornAt: number;
28
+ }
29
+ /**
30
+ * Re-anchor `node` over `speaker`: stack slot `slot` above the head, turned to
31
+ * the camera, scaled by distance. Called every frame, so a walking speaker
32
+ * carries its bubble.
33
+ */
34
+ export declare function anchorBubble(node: Object3D, speaker: Object3D, camera: Camera, slot: number, parent: Object3D): void;
35
+ export declare const speakerKind: (m: ConversationMessage) => "visitor" | "agent" | null;
36
+ //# sourceMappingURL=bubble.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bubble.d.ts","sourceRoot":"","sources":["../src/bubble.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAGL,KAAK,EAGL,OAAO,EACP,KAAK,MAAM,EACX,KAAK,QAAQ,EACd,MAAM,OAAO,CAAC;AAMf,OAAO,KAAK,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAOnG,eAAO,MAAM,WAAW,IAAI,CAAC;AAE7B,oEAAoE;AACpE,eAAO,MAAM,WAAW,aAAc,MAAM,KAAG,MACwB,CAAC;AAExE,wBAAgB,aAAa,CAAC,KAAK,EAAE,iBAAiB,GAAG,QAAQ,CAwBhE;AAED,wGAAwG;AACxG,wBAAgB,YAAY,CAAC,KAAK,EAAE,kBAAkB,GAAG,KAAK,CAW7D;AAED,wGAAwG;AACxG,wBAAgB,aAAa,CAAC,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CAiBrF;AAED,gDAAgD;AAChD,wBAAgB,SAAS,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO,CAKpD;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC;IAC3B,IAAI,EAAE,QAAQ,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,QAAQ,EACd,OAAO,EAAE,QAAQ,EACjB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,QAAQ,GACf,IAAI,CAUN;AAED,eAAO,MAAM,WAAW,MAAO,mBAAmB,KAAG,SAAS,GAAG,OAAO,GAAG,IAK/D,CAAC"}