@chatlychat/world 0.0.0-stage → 0.1.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 (81) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/LICENSE +21 -0
  3. package/README.md +120 -2
  4. package/dist/agent.d.ts +59 -0
  5. package/dist/agent.d.ts.map +1 -0
  6. package/dist/agent.js +232 -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 +118 -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/examples/tsconfig.tsbuildinfo +1 -0
  25. package/dist/handlers.d.ts +26 -0
  26. package/dist/handlers.d.ts.map +1 -0
  27. package/dist/handlers.js +41 -0
  28. package/dist/handlers.js.map +1 -0
  29. package/dist/index.d.ts +26 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +24 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/input.d.ts +46 -0
  34. package/dist/input.d.ts.map +1 -0
  35. package/dist/input.js +89 -0
  36. package/dist/input.js.map +1 -0
  37. package/dist/layout.d.ts +24 -0
  38. package/dist/layout.d.ts.map +1 -0
  39. package/dist/layout.js +69 -0
  40. package/dist/layout.js.map +1 -0
  41. package/dist/model.d.ts +25 -0
  42. package/dist/model.d.ts.map +1 -0
  43. package/dist/model.js +60 -0
  44. package/dist/model.js.map +1 -0
  45. package/dist/motion.d.ts +27 -0
  46. package/dist/motion.d.ts.map +1 -0
  47. package/dist/motion.js +65 -0
  48. package/dist/motion.js.map +1 -0
  49. package/dist/panel.d.ts +77 -0
  50. package/dist/panel.d.ts.map +1 -0
  51. package/dist/panel.js +214 -0
  52. package/dist/panel.js.map +1 -0
  53. package/dist/selection.d.ts +14 -0
  54. package/dist/selection.d.ts.map +1 -0
  55. package/dist/selection.js +21 -0
  56. package/dist/selection.js.map +1 -0
  57. package/dist/text.d.ts +26 -0
  58. package/dist/text.d.ts.map +1 -0
  59. package/dist/text.js +100 -0
  60. package/dist/text.js.map +1 -0
  61. package/dist/theme-apply.d.ts +19 -0
  62. package/dist/theme-apply.d.ts.map +1 -0
  63. package/dist/theme-apply.js +88 -0
  64. package/dist/theme-apply.js.map +1 -0
  65. package/dist/types.d.ts +102 -0
  66. package/dist/types.d.ts.map +1 -0
  67. package/dist/types.js +41 -0
  68. package/dist/types.js.map +1 -0
  69. package/dist/visemes.d.ts +28 -0
  70. package/dist/visemes.d.ts.map +1 -0
  71. package/dist/visemes.js +42 -0
  72. package/dist/visemes.js.map +1 -0
  73. package/dist/world-api.d.ts +39 -0
  74. package/dist/world-api.d.ts.map +1 -0
  75. package/dist/world-api.js +2 -0
  76. package/dist/world-api.js.map +1 -0
  77. package/dist/world.d.ts +5 -0
  78. package/dist/world.d.ts.map +1 -0
  79. package/dist/world.js +573 -0
  80. package/dist/world.js.map +1 -0
  81. package/package.json +67 -3
package/CHANGELOG.md ADDED
@@ -0,0 +1,15 @@
1
+ # @chatlychat/world
2
+
3
+ ## 0.1.0 — 2026-10-25
4
+
5
+ Unpublished. The first version: a Chatly conversation rendered into your three.js scene.
6
+
7
+ - `createWorld({ conversation, camera, … })` → `world.root` (add it to your scene) and `world.update(dt)` (call it from your loop).
8
+ - 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.
9
+ - Speech bubbles anchored to a speaker (never head-locked), scaled by distance, with a typing indicator.
10
+ - A world-space panel and composer; `fixed`, `lazy` and `wrist` follow; a `locked` placement is honoured as `lazy`.
11
+ - Hand, controller (`attachController`), gaze-dwell (`attachGaze`) and pointer (`pickAtPointer`) picking. The host keeps input.
12
+ - An embodied agent body: avatar model, `idle`/`listening`/`thinking`/`speaking` clips, expression and `lookAt` cues, viseme-driven mouth when a message carries `metadata.visemes`.
13
+ - 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`.
14
+ - Replaceable `renderCard`, `renderPick`, `renderBubble`, `renderPanel`, `renderComposer`, `renderAgent`, with a named, versioned object contract (`WORLD_CONTRACT`).
15
+ - `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,121 @@
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. There are no
12
+ others, and nothing here touches the web widget.
13
+
14
+ > `@chatlychat/world` is **not yet published to npm**; the install above will 404
15
+ > until it is.
16
+
17
+ ```ts
18
+ import { createChatlyCore } from '@chatlychat/core';
19
+ import { createWorld } from '@chatlychat/world';
20
+ import { Clock } from 'three';
21
+
22
+ const core = createChatlyCore({ publicId: 'pub_xxx', socket: io });
23
+ const conversation = await core.startConversation('Do you have this in blue?');
24
+
25
+ const world = createWorld({
26
+ conversation,
27
+ camera, // the camera (or XR camera) the visitor sees through
28
+ theme: core.worldTheme, // the channel's worldTheme
29
+ scene: 'light', // what your scene's light is, for colorScheme "auto"
30
+ reducedMotion: () => false, // return your viewer's reduced-motion setting
31
+ presences: { '*': { name: 'Mia', avatar: { url: 'https://cdn.example/mia.glb' } } },
32
+ });
33
+ scene.add(world.root);
34
+
35
+ const clock = new Clock();
36
+ renderer.setAnimationLoop(() => {
37
+ world.update(clock.getDelta()); // seconds
38
+ renderer.render(scene, camera);
39
+ });
40
+ ```
41
+
42
+ ## What it draws
43
+
44
+ - **Picks.** A `card_grid` with `placement` `arc`, `shelf`, `wall` or `table`;
45
+ a card with a `model` becomes a glTF object (`scale` and `anchor` honoured)
46
+ with the theme's pedestal, beam, price label and selection ring. Tapping one
47
+ selects it: a postback to the conversation, a ring on the card, the object in
48
+ hand. `world.selection` is shared — selecting from your own UI is the same event
49
+ (`await world.select(key)`; keys from `world.cardKeys()`).
50
+ - **Speech bubbles** over a speaker, turned to the viewer and scaled by distance —
51
+ positioned from the speaker, never from the camera. Typing indicator per
52
+ `typingLook`. History is not replayed as speech.
53
+ - **A world-space panel and composer.** Never head-locked: `fixed`, `lazy` (an
54
+ eased follow with a dead zone) or `wrist` (carried on a controller you pass).
55
+ `follow: 'locked'` in a placement is honoured as `lazy`.
56
+ Typing is yours: fill `world.composer.setDraft(text)` from your keyboard, mic or
57
+ overlay (`onComposeRequest` fires when the field is pressed), then it sends.
58
+ - **An agent body**: avatar model (glTF; `usdz` is refused with a typed error),
59
+ `idle`/`listening`/`thinking`/`speaking` clips from `presence.clips`, one-shot
60
+ `expressions`, `lookAt` cues resolved through `anchors.resolve(ref)`, and a mouth
61
+ driven by `message.metadata.visemes` (`[{ t: ms, v: 'aa', w?: 0..1 }]`) when
62
+ present. `presence.idle` and `personalSpaceMetres` are yours to honour.
63
+
64
+ ## Input
65
+
66
+ The host owns input; the world only knows which of its objects a ray hit.
67
+
68
+ ```ts
69
+ world.attachController(xrController); // your XR controller object: trigger or hand pinch
70
+ const gaze = world.attachGaze({ dwellMs: 1500 }); // call gaze.update(ms) per frame
71
+ const hit = world.pickAtPointer(ndcX, ndcY); // mouse / touch
72
+ if (hit) world.activate(hit);
73
+ ```
74
+
75
+ ## Host capabilities
76
+
77
+ ```ts
78
+ world.capabilities.register(
79
+ 'world.walk_to', // also: avatar.try_on, scene.highlight, scene.spawn, or your own
80
+ async () => true, // do your own movement here; return false or throw to refuse
81
+ { equivalent: { kind: 'text', text: 'The shop is on level 2.' }, unprompted: true },
82
+ );
83
+ await core.setSurface(world.capabilities.surface({ room: 'atrium' }));
84
+ ```
85
+
86
+ A bot-fired invocation reaches your handler and is answered into the thread
87
+ (`done`, or `failed` + `refused: true`). The `equivalent` is required and is
88
+ announced (`announce`) whenever the handler is missing, refuses or throws.
89
+
90
+ ## Replaceable renderers
91
+
92
+ `renderCard`, `renderPick`, `renderBubble`, `renderPanel`, `renderComposer`,
93
+ `renderAgent` — each receives the typed block (or message, or agent view), the
94
+ resolved `theme` and `placement`, `reducedMotion`, and for blocks the Chatly
95
+ handlers `select`, `press` and `react`. Return any `Object3D`.
96
+
97
+ ```ts
98
+ createWorld({ …, renderers: { contract: 1, renderCard: (p) => yourShelfMesh } });
99
+ ```
100
+
101
+ The world names an override's node `cl3-card` etc. if you did not, and routes
102
+ taps on it to selection (set `userData.cl3Handled = true` to take taps yourself).
103
+ Object names and the renderer set are versioned in `WORLD_CONTRACT`; pinning
104
+ `contract: N` fails loudly on a world that speaks another N.
105
+
106
+ ## Theme and reduced motion
107
+
108
+ Every `worldTheme` token is applied: panel material/colour/emissive, type
109
+ (`fontUrl`, weight, outline, `textScale`), cards, speech, agent, sound (as
110
+ `onSound` cues — you play them) and motion. `world.setTheme`, `setScene` and
111
+ `setReducedMotion` re-apply live. **Reduced motion always wins:** entrances and
112
+ trails off, lazy follow and gaze snap, expression clips skipped, clips held on
113
+ their first frame.
114
+
115
+ ## Tests without WebGL
116
+
117
+ three builds a scene graph with no GL context, so you can test your integration in
118
+ Node: see `src/world.spec.ts` for a fake conversation and the assertions.
119
+ `pnpm --filter @chatlychat/world example` runs a scripted demo.
120
+
121
+ MIT.
@@ -0,0 +1,59 @@
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 { 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 lastState;
39
+ private refused;
40
+ constructor(theme: ResolvedWorldTheme, loadModel: ModelLoader, onError: (e: unknown) => void, reduced: boolean);
41
+ get hasModel(): boolean;
42
+ get activeClip(): string | null;
43
+ setTheme(theme: ResolvedWorldTheme): void;
44
+ private placeholder;
45
+ setPresence(presence: AgentWorldPresence | null): void;
46
+ private setTag;
47
+ private swapModel;
48
+ private action;
49
+ /** Move to the state's clip — the host's own name for it, from `presence.clips`. */
50
+ setState(state: AgentPresenceState): void;
51
+ /** Play a one-shot expression clip. Skipped entirely under reduced motion. */
52
+ playExpression(clipName: string | null): boolean;
53
+ lookAt(target: GazeTarget): void;
54
+ setVisemes(track: VisemeTrack | null, nowMs: number): void;
55
+ private applyViseme;
56
+ update(dtMs: number, nowMs: number, camera: Camera): void;
57
+ dispose(): void;
58
+ }
59
+ //# 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;AAEzE,OAAO,KAAK,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAC/E,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;IAkBlB,OAAO,CAAC,KAAK;IACb,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,OAAO;IAnB1B,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,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;IA6B7D,OAAO,CAAC,MAAM;IAad,OAAO,CAAC,SAAS;IA6BjB,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;IAYhD,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;IAsBZ,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IA4BzD,OAAO,IAAI,IAAI;CAIvB"}
package/dist/agent.js ADDED
@@ -0,0 +1,232 @@
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
+ const TURN_RATE = 6;
23
+ const FADE_S = 0.25;
24
+ export class AgentBody {
25
+ theme;
26
+ loadModel;
27
+ onError;
28
+ root = new Group();
29
+ presence = null;
30
+ reduced;
31
+ model = null;
32
+ mixer = null;
33
+ clips = [];
34
+ current = null;
35
+ avatarUrl = null;
36
+ tag = null;
37
+ gaze = null;
38
+ track = null;
39
+ trackStart = 0;
40
+ morphs = [];
41
+ lastState = null;
42
+ refused = null;
43
+ constructor(theme, loadModel, onError, reduced) {
44
+ this.theme = theme;
45
+ this.loadModel = loadModel;
46
+ this.onError = onError;
47
+ this.reduced = reduced;
48
+ this.root.name = WORLD_CONTRACT.objects.agent;
49
+ this.root.add(this.placeholder());
50
+ }
51
+ get hasModel() {
52
+ return this.model !== null;
53
+ }
54
+ get activeClip() {
55
+ return this.current?.getClip().name ?? null;
56
+ }
57
+ setTheme(theme) {
58
+ this.theme = theme;
59
+ }
60
+ placeholder() {
61
+ const g = new Group();
62
+ g.name = 'cl3-agent-placeholder';
63
+ const body = new Mesh(new CapsuleGeometry(0.2, 0.7, 4, 12), accentMaterial(this.theme));
64
+ body.position.y = 0.55;
65
+ const head = new Mesh(new SphereGeometry(0.17, 16, 12), accentMaterial(this.theme));
66
+ head.position.y = 1.3;
67
+ g.add(body, head);
68
+ return g;
69
+ }
70
+ setPresence(presence) {
71
+ this.presence = presence;
72
+ this.setTag(presence === null ? null : (presence.nameTag ?? presence.name));
73
+ const a = presence?.avatar;
74
+ if (a?.format === 'usdz' && this.refused !== a.url) {
75
+ this.refused = a.url;
76
+ this.onError(new WorldError('unsupported_format', 'usdz avatars are not loadable by three.js; showing the placeholder'));
77
+ }
78
+ const url = a !== undefined && a.format !== 'usdz' ? a.url : null;
79
+ if (url === this.avatarUrl) {
80
+ return;
81
+ }
82
+ this.avatarUrl = url;
83
+ if (url === null || a === undefined) {
84
+ this.swapModel(null, []);
85
+ return;
86
+ }
87
+ this.loadModel(url).then((m) => {
88
+ if (this.avatarUrl === url) {
89
+ this.swapModel(fitModel(m.scene, a.scale ?? 1, a.anchor ?? 'base'), m.animations);
90
+ }
91
+ }, this.onError);
92
+ }
93
+ setTag(text) {
94
+ if (this.tag !== null) {
95
+ this.root.remove(this.tag);
96
+ this.tag = null;
97
+ }
98
+ if (text !== null) {
99
+ this.tag = createTextMesh(text, this.theme, { maxWidthMetres: 0.8, size: 0.8 });
100
+ this.tag.name = 'cl3-agent-nametag';
101
+ this.tag.position.y = 1.75;
102
+ this.root.add(this.tag);
103
+ }
104
+ }
105
+ swapModel(model, clips) {
106
+ this.mixer?.stopAllAction();
107
+ this.mixer = null;
108
+ this.current = null;
109
+ for (const c of [...this.root.children]) {
110
+ if (c !== this.tag) {
111
+ this.root.remove(c);
112
+ }
113
+ }
114
+ this.model = model;
115
+ this.clips = clips;
116
+ this.morphs = [];
117
+ if (model === null) {
118
+ this.root.add(this.placeholder());
119
+ return;
120
+ }
121
+ this.root.add(model);
122
+ model.traverse((o) => {
123
+ if (o.morphTargetDictionary !== undefined) {
124
+ this.morphs.push(o);
125
+ }
126
+ });
127
+ this.mixer = new AnimationMixer(model);
128
+ // The state was set before the model arrived; its clip could not play then.
129
+ if (this.lastState !== null) {
130
+ this.setState(this.lastState);
131
+ }
132
+ }
133
+ action(name) {
134
+ if (this.mixer === null || name === null || name === undefined) {
135
+ return null;
136
+ }
137
+ const clip = AnimationClip.findByName([...this.clips], name);
138
+ return clip === null ? null : this.mixer.clipAction(clip);
139
+ }
140
+ /** Move to the state's clip — the host's own name for it, from `presence.clips`. */
141
+ setState(state) {
142
+ this.lastState = state;
143
+ const next = this.action(this.presence?.clips?.[state]);
144
+ if (next === null || next === this.current) {
145
+ return;
146
+ }
147
+ next.reset();
148
+ next.play();
149
+ if (this.reduced) {
150
+ next.paused = true; // a held pose, not a loop
151
+ this.current?.stop();
152
+ }
153
+ else if (this.current !== null) {
154
+ next.crossFadeFrom(this.current, FADE_S, false);
155
+ }
156
+ this.current = next;
157
+ }
158
+ /** Play a one-shot expression clip. Skipped entirely under reduced motion. */
159
+ playExpression(clipName) {
160
+ const a = this.reduced ? null : this.action(clipName);
161
+ if (a === null) {
162
+ return false;
163
+ }
164
+ a.reset();
165
+ a.setLoop(LoopOnce, 1);
166
+ a.clampWhenFinished = true;
167
+ a.play();
168
+ return true;
169
+ }
170
+ lookAt(target) {
171
+ this.gaze = target;
172
+ }
173
+ setVisemes(track, nowMs) {
174
+ this.track = track;
175
+ this.trackStart = nowMs;
176
+ if (track === null) {
177
+ this.applyViseme(null, 0);
178
+ }
179
+ }
180
+ applyViseme(v, w) {
181
+ for (const m of this.morphs) {
182
+ const dict = m.morphTargetDictionary;
183
+ const inf = m.morphTargetInfluences;
184
+ if (dict === undefined || inf === undefined) {
185
+ continue;
186
+ }
187
+ // Zero every viseme-ish target first so a released mouth closes.
188
+ for (const [name, idx] of Object.entries(dict)) {
189
+ if (/^(viseme_|Viseme_)/.test(name) || ['aa', 'ih', 'ou', 'ee', 'oh'].includes(name)) {
190
+ inf[idx] = 0;
191
+ }
192
+ }
193
+ if (v !== null) {
194
+ const hit = morphNamesFor(v).find((n) => dict[n] !== undefined);
195
+ if (hit !== undefined) {
196
+ inf[dict[hit]] = w;
197
+ }
198
+ }
199
+ }
200
+ }
201
+ update(dtMs, nowMs, camera) {
202
+ this.mixer?.update(dtMs / 1000);
203
+ if (this.track !== null) {
204
+ const e = visemeAt(this.track, nowMs - this.trackStart);
205
+ this.applyViseme(e?.v ?? null, e?.w ?? 0);
206
+ }
207
+ if (this.gaze !== null) {
208
+ const here = this.root.getWorldPosition(new Vector3());
209
+ const at = this.gaze === 'visitor'
210
+ ? camera.getWorldPosition(new Vector3())
211
+ : this.gaze instanceof Vector3
212
+ ? this.gaze
213
+ : this.gaze.getWorldPosition(new Vector3());
214
+ const want = Math.atan2(at.x - here.x, at.z - here.z);
215
+ const err = wrapAngle(want - this.root.rotation.y);
216
+ const step = TURN_RATE * (dtMs / 1000);
217
+ this.root.rotation.y =
218
+ this.reduced || Math.abs(err) <= step ? want : this.root.rotation.y + Math.sign(err) * step;
219
+ }
220
+ if (this.tag !== null) {
221
+ // The tag turns to the viewer regardless of where the body faces.
222
+ const cam = camera.getWorldPosition(new Vector3());
223
+ const t = this.tag.getWorldPosition(new Vector3());
224
+ this.tag.rotation.y = Math.atan2(cam.x - t.x, cam.z - t.z) - this.root.rotation.y;
225
+ }
226
+ }
227
+ dispose() {
228
+ this.mixer?.stopAllAction();
229
+ this.root.removeFromParent();
230
+ }
231
+ }
232
+ //# 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"}
package/dist/bubble.js ADDED
@@ -0,0 +1,108 @@
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 { Box3, ConeGeometry, Group, Mesh, SphereGeometry, Vector3, } from 'three';
11
+ import { createTextMesh } from './text.js';
12
+ import { accentMaterial, bubbleBacking } from './theme-apply.js';
13
+ import { WORLD_CONTRACT } from './types.js';
14
+ const REFERENCE_METRES = 2;
15
+ const SCALE_MIN = 0.6;
16
+ const SCALE_MAX = 2.5;
17
+ const HEAD_FALLBACK = 1.9;
18
+ const STACK_STEP = 0.22;
19
+ export const MAX_BUBBLES = 3;
20
+ /** Uniform scale for a bubble `distance` metres from the viewer. */
21
+ export const bubbleScale = (distance) => Math.min(SCALE_MAX, Math.max(SCALE_MIN, distance / REFERENCE_METRES));
22
+ export function defaultBubble(props) {
23
+ const { theme } = props;
24
+ const g = new Group();
25
+ g.name = WORLD_CONTRACT.objects.bubble;
26
+ const text = createTextMesh(props.message.body, theme, {
27
+ maxWidthMetres: theme.bubbleMaxWidthMetres,
28
+ });
29
+ const [w, h] = text.userData['size'];
30
+ const pad = 0.03;
31
+ const plate = bubbleBacking(theme, w + pad * 2, h + pad * 2);
32
+ if (plate !== null) {
33
+ g.add(plate);
34
+ if (theme.bubbleTail && theme.bubbleShape !== 'cloud') {
35
+ const tail = new Mesh(new ConeGeometry(0.025, 0.05, 3), plate.material);
36
+ tail.name = 'cl3-bubble-tail';
37
+ tail.position.set(0, -(h / 2 + pad + 0.02), 0);
38
+ tail.rotation.set(0, 0, Math.PI);
39
+ g.add(tail);
40
+ }
41
+ }
42
+ text.position.z = 0.002;
43
+ g.add(text);
44
+ g.userData['size'] = [w + pad * 2, h + pad * 2];
45
+ return g;
46
+ }
47
+ /** The typing indicator: three dots, one pulsing dot, or nothing (`avatar`/`none` live on the body). */
48
+ export function createTyping(theme) {
49
+ const g = new Group();
50
+ g.name = WORLD_CONTRACT.objects.typing;
51
+ g.userData['look'] = theme.typingLook;
52
+ const dots = theme.typingLook === 'dots' ? 3 : theme.typingLook === 'pulse' ? 1 : 0;
53
+ for (let i = 0; i < dots; i += 1) {
54
+ const d = new Mesh(new SphereGeometry(0.02, 12, 8), accentMaterial(theme));
55
+ d.position.x = (i - (dots - 1) / 2) * 0.06;
56
+ g.add(d);
57
+ }
58
+ return g;
59
+ }
60
+ /** Bob or pulse the dots. With reduced motion they sit still: a static ellipsis still says "typing". */
61
+ export function animateTyping(typing, nowMs, reduced) {
62
+ if (reduced) {
63
+ typing.children.forEach((c) => {
64
+ c.position.y = 0;
65
+ c.scale.setScalar(1);
66
+ });
67
+ return;
68
+ }
69
+ const pulse = typing.userData['look'] === 'pulse';
70
+ typing.children.forEach((c, i) => {
71
+ const phase = nowMs / 300 - i * 0.6;
72
+ if (pulse) {
73
+ c.scale.setScalar(1 + 0.4 * Math.sin(phase));
74
+ }
75
+ else {
76
+ c.position.y = 0.015 * Math.sin(phase);
77
+ }
78
+ });
79
+ }
80
+ /** World-space point above a speaker's head. */
81
+ export function headPoint(speaker) {
82
+ speaker.updateWorldMatrix(true, true);
83
+ const box = new Box3().setFromObject(speaker);
84
+ const p = speaker.getWorldPosition(new Vector3());
85
+ return box.isEmpty() ? p.setY(p.y + HEAD_FALLBACK) : new Vector3(p.x, box.max.y + 0.2, p.z);
86
+ }
87
+ /**
88
+ * Re-anchor `node` over `speaker`: stack slot `slot` above the head, turned to
89
+ * the camera, scaled by distance. Called every frame, so a walking speaker
90
+ * carries its bubble.
91
+ */
92
+ export function anchorBubble(node, speaker, camera, slot, parent) {
93
+ const head = headPoint(speaker);
94
+ const cam = camera.getWorldPosition(new Vector3());
95
+ const scale = bubbleScale(head.distanceTo(cam));
96
+ head.y += (slot * STACK_STEP + 0.1) * scale;
97
+ parent.worldToLocal(head);
98
+ node.position.copy(head);
99
+ const camLocal = parent.worldToLocal(cam.clone());
100
+ node.rotation.set(0, Math.atan2(camLocal.x - head.x, camLocal.z - head.z), 0);
101
+ node.scale.setScalar(scale);
102
+ }
103
+ export const speakerKind = (m) => m.authorType === 'contact'
104
+ ? 'visitor'
105
+ : m.authorType === 'agent' || m.authorType === 'bot'
106
+ ? 'agent'
107
+ : null;
108
+ //# sourceMappingURL=bubble.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bubble.js","sourceRoot":"","sources":["../src/bubble.ts"],"names":[],"mappings":"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"}