@oxy.so/bloom 7.1.2 → 7.1.4

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 (236) hide show
  1. package/assets/character-runtime/README.md +114 -19
  2. package/assets/character-runtime/authored-parts.mjs +754 -0
  3. package/assets/character-runtime/catalog-parts.mjs +362 -0
  4. package/assets/character-runtime/character-recipe.mjs +247 -0
  5. package/assets/character-runtime/clippo-geometry.mjs +679 -0
  6. package/assets/character-runtime/controller-mode.mjs +208 -0
  7. package/assets/character-runtime/cyclops-geometry.mjs +180 -0
  8. package/assets/character-runtime/legacy-engine.mjs +406 -54
  9. package/assets/character-runtime/legacy-geometry.mjs +242 -85
  10. package/assets/character-runtime/legacy-worker.mjs +82 -23
  11. package/assets/character-runtime/managed-avatar.mjs +12 -17
  12. package/assets/character-runtime/migrated-contours.mjs +2067 -0
  13. package/assets/character-runtime/migrated-faces.mjs +59 -0
  14. package/assets/character-runtime/pose-transition.mjs +523 -0
  15. package/assets/character-runtime/preparation-cache.mjs +60 -0
  16. package/assets/character-runtime/render-budget.mjs +5 -3
  17. package/assets/character-runtime/render-errors.mjs +180 -0
  18. package/assets/character-runtime/resolution.mjs +10 -0
  19. package/assets/character-runtime/runtime.mjs +744 -295
  20. package/assets/character-runtime/sha256.mjs +44 -0
  21. package/assets/character-runtime/shared-programs.mjs +534 -0
  22. package/assets/character-runtime/shared-surface.mjs +490 -0
  23. package/assets/character-runtime/shared-textures.mjs +420 -0
  24. package/assets/character-runtime/thumbnails/eyes/todd.png +0 -0
  25. package/docs/agent-avatar.mdx +45 -8
  26. package/docs/agent-creator.mdx +13 -3
  27. package/lib/commonjs/agent-avatar/AgentAvatar.js +1 -1
  28. package/lib/commonjs/agent-avatar/AgentAvatar.js.map +1 -1
  29. package/lib/commonjs/agent-avatar/CharacterAvatar.js +2 -1
  30. package/lib/commonjs/agent-avatar/CharacterAvatar.js.map +1 -1
  31. package/lib/commonjs/agent-avatar/character-colors.js +19 -0
  32. package/lib/commonjs/agent-avatar/character-colors.js.map +1 -0
  33. package/lib/commonjs/agent-avatar/character-shapes.js +106 -0
  34. package/lib/commonjs/agent-avatar/character-shapes.js.map +1 -0
  35. package/lib/commonjs/agent-avatar/config-character.js +4 -0
  36. package/lib/commonjs/agent-avatar/config-character.js.map +1 -1
  37. package/lib/commonjs/agent-avatar/legacy-recipe.js +19 -6
  38. package/lib/commonjs/agent-avatar/legacy-recipe.js.map +1 -1
  39. package/lib/commonjs/agent-creator/AgentCreatorBase.js +67 -107
  40. package/lib/commonjs/agent-creator/AgentCreatorBase.js.map +1 -1
  41. package/lib/commonjs/agent-creator/CharacterControls.js +23 -5
  42. package/lib/commonjs/agent-creator/CharacterControls.js.map +1 -1
  43. package/lib/commonjs/agent-creator/ClippoArtwork.js +63 -0
  44. package/lib/commonjs/agent-creator/ClippoArtwork.js.map +1 -0
  45. package/lib/commonjs/agent-creator/CyclopsArtwork.js +45 -0
  46. package/lib/commonjs/agent-creator/CyclopsArtwork.js.map +1 -0
  47. package/lib/commonjs/agent-creator/EmotionPicker.js +8 -1
  48. package/lib/commonjs/agent-creator/EmotionPicker.js.map +1 -1
  49. package/lib/commonjs/agent-creator/ShapeSilhouette.js +8 -1
  50. package/lib/commonjs/agent-creator/ShapeSilhouette.js.map +1 -1
  51. package/lib/commonjs/agent-creator/ToddArtwork.js +29 -0
  52. package/lib/commonjs/agent-creator/ToddArtwork.js.map +1 -0
  53. package/lib/commonjs/agent-creator/constants.js +16 -17
  54. package/lib/commonjs/agent-creator/constants.js.map +1 -1
  55. package/lib/commonjs/agent-creator/messages.js +1 -1
  56. package/lib/commonjs/agent-creator/messages.js.map +1 -1
  57. package/lib/commonjs/locale/translations/ar.js +1 -1
  58. package/lib/commonjs/locale/translations/ar.js.map +1 -1
  59. package/lib/commonjs/locale/translations/bn.js +1 -1
  60. package/lib/commonjs/locale/translations/bn.js.map +1 -1
  61. package/lib/commonjs/locale/translations/ca.js +1 -1
  62. package/lib/commonjs/locale/translations/ca.js.map +1 -1
  63. package/lib/commonjs/locale/translations/de.js +1 -1
  64. package/lib/commonjs/locale/translations/de.js.map +1 -1
  65. package/lib/commonjs/locale/translations/es.js +1 -1
  66. package/lib/commonjs/locale/translations/es.js.map +1 -1
  67. package/lib/commonjs/locale/translations/fr.js +1 -1
  68. package/lib/commonjs/locale/translations/fr.js.map +1 -1
  69. package/lib/commonjs/locale/translations/hi.js +1 -1
  70. package/lib/commonjs/locale/translations/hi.js.map +1 -1
  71. package/lib/commonjs/locale/translations/id.js +1 -1
  72. package/lib/commonjs/locale/translations/id.js.map +1 -1
  73. package/lib/commonjs/locale/translations/it.js +1 -1
  74. package/lib/commonjs/locale/translations/it.js.map +1 -1
  75. package/lib/commonjs/locale/translations/ja.js +1 -1
  76. package/lib/commonjs/locale/translations/ja.js.map +1 -1
  77. package/lib/commonjs/locale/translations/pt.js +1 -1
  78. package/lib/commonjs/locale/translations/pt.js.map +1 -1
  79. package/lib/commonjs/locale/translations/ru.js +1 -1
  80. package/lib/commonjs/locale/translations/ru.js.map +1 -1
  81. package/lib/commonjs/locale/translations/tr.js +1 -1
  82. package/lib/commonjs/locale/translations/tr.js.map +1 -1
  83. package/lib/commonjs/locale/translations/zh.js +1 -1
  84. package/lib/commonjs/locale/translations/zh.js.map +1 -1
  85. package/lib/module/agent-avatar/AgentAvatar.js +1 -1
  86. package/lib/module/agent-avatar/AgentAvatar.js.map +1 -1
  87. package/lib/module/agent-avatar/CharacterAvatar.js +2 -1
  88. package/lib/module/agent-avatar/CharacterAvatar.js.map +1 -1
  89. package/lib/module/agent-avatar/character-colors.js +15 -0
  90. package/lib/module/agent-avatar/character-colors.js.map +1 -0
  91. package/lib/module/agent-avatar/character-shapes.js +97 -0
  92. package/lib/module/agent-avatar/character-shapes.js.map +1 -0
  93. package/lib/module/agent-avatar/config-character.js +4 -0
  94. package/lib/module/agent-avatar/config-character.js.map +1 -1
  95. package/lib/module/agent-avatar/legacy-recipe.js +19 -6
  96. package/lib/module/agent-avatar/legacy-recipe.js.map +1 -1
  97. package/lib/module/agent-creator/AgentCreatorBase.js +69 -109
  98. package/lib/module/agent-creator/AgentCreatorBase.js.map +1 -1
  99. package/lib/module/agent-creator/CharacterControls.js +24 -6
  100. package/lib/module/agent-creator/CharacterControls.js.map +1 -1
  101. package/lib/module/agent-creator/ClippoArtwork.js +59 -0
  102. package/lib/module/agent-creator/ClippoArtwork.js.map +1 -0
  103. package/lib/module/agent-creator/CyclopsArtwork.js +41 -0
  104. package/lib/module/agent-creator/CyclopsArtwork.js.map +1 -0
  105. package/lib/module/agent-creator/EmotionPicker.js +8 -1
  106. package/lib/module/agent-creator/EmotionPicker.js.map +1 -1
  107. package/lib/module/agent-creator/ShapeSilhouette.js +8 -1
  108. package/lib/module/agent-creator/ShapeSilhouette.js.map +1 -1
  109. package/lib/module/agent-creator/ToddArtwork.js +25 -0
  110. package/lib/module/agent-creator/ToddArtwork.js.map +1 -0
  111. package/lib/module/agent-creator/constants.js +8 -16
  112. package/lib/module/agent-creator/constants.js.map +1 -1
  113. package/lib/module/agent-creator/messages.js +1 -1
  114. package/lib/module/agent-creator/messages.js.map +1 -1
  115. package/lib/module/locale/translations/ar.js +1 -1
  116. package/lib/module/locale/translations/ar.js.map +1 -1
  117. package/lib/module/locale/translations/bn.js +1 -1
  118. package/lib/module/locale/translations/bn.js.map +1 -1
  119. package/lib/module/locale/translations/ca.js +1 -1
  120. package/lib/module/locale/translations/ca.js.map +1 -1
  121. package/lib/module/locale/translations/de.js +1 -1
  122. package/lib/module/locale/translations/de.js.map +1 -1
  123. package/lib/module/locale/translations/es.js +1 -1
  124. package/lib/module/locale/translations/es.js.map +1 -1
  125. package/lib/module/locale/translations/fr.js +1 -1
  126. package/lib/module/locale/translations/fr.js.map +1 -1
  127. package/lib/module/locale/translations/hi.js +1 -1
  128. package/lib/module/locale/translations/hi.js.map +1 -1
  129. package/lib/module/locale/translations/id.js +1 -1
  130. package/lib/module/locale/translations/id.js.map +1 -1
  131. package/lib/module/locale/translations/it.js +1 -1
  132. package/lib/module/locale/translations/it.js.map +1 -1
  133. package/lib/module/locale/translations/ja.js +1 -1
  134. package/lib/module/locale/translations/ja.js.map +1 -1
  135. package/lib/module/locale/translations/pt.js +1 -1
  136. package/lib/module/locale/translations/pt.js.map +1 -1
  137. package/lib/module/locale/translations/ru.js +1 -1
  138. package/lib/module/locale/translations/ru.js.map +1 -1
  139. package/lib/module/locale/translations/tr.js +1 -1
  140. package/lib/module/locale/translations/tr.js.map +1 -1
  141. package/lib/module/locale/translations/zh.js +1 -1
  142. package/lib/module/locale/translations/zh.js.map +1 -1
  143. package/lib/typescript/commonjs/agent-avatar/CharacterAvatar.d.ts.map +1 -1
  144. package/lib/typescript/commonjs/agent-avatar/character-colors.d.ts +13 -0
  145. package/lib/typescript/commonjs/agent-avatar/character-colors.d.ts.map +1 -0
  146. package/lib/typescript/commonjs/agent-avatar/character-shapes.d.ts +19 -0
  147. package/lib/typescript/commonjs/agent-avatar/character-shapes.d.ts.map +1 -0
  148. package/lib/typescript/commonjs/agent-avatar/config-character.d.ts +4 -0
  149. package/lib/typescript/commonjs/agent-avatar/config-character.d.ts.map +1 -1
  150. package/lib/typescript/commonjs/agent-avatar/legacy-recipe.d.ts +2 -10
  151. package/lib/typescript/commonjs/agent-avatar/legacy-recipe.d.ts.map +1 -1
  152. package/lib/typescript/commonjs/agent-creator/AgentCreatorBase.d.ts.map +1 -1
  153. package/lib/typescript/commonjs/agent-creator/CharacterControls.d.ts +2 -1
  154. package/lib/typescript/commonjs/agent-creator/CharacterControls.d.ts.map +1 -1
  155. package/lib/typescript/commonjs/agent-creator/ClippoArtwork.d.ts +6 -0
  156. package/lib/typescript/commonjs/agent-creator/ClippoArtwork.d.ts.map +1 -0
  157. package/lib/typescript/commonjs/agent-creator/CyclopsArtwork.d.ts +5 -0
  158. package/lib/typescript/commonjs/agent-creator/CyclopsArtwork.d.ts.map +1 -0
  159. package/lib/typescript/commonjs/agent-creator/EmotionPicker.d.ts +1 -0
  160. package/lib/typescript/commonjs/agent-creator/EmotionPicker.d.ts.map +1 -1
  161. package/lib/typescript/commonjs/agent-creator/ShapeSilhouette.d.ts.map +1 -1
  162. package/lib/typescript/commonjs/agent-creator/ToddArtwork.d.ts +6 -0
  163. package/lib/typescript/commonjs/agent-creator/ToddArtwork.d.ts.map +1 -0
  164. package/lib/typescript/commonjs/agent-creator/constants.d.ts +7 -15
  165. package/lib/typescript/commonjs/agent-creator/constants.d.ts.map +1 -1
  166. package/lib/typescript/commonjs/agent-creator/messages.d.ts.map +1 -1
  167. package/lib/typescript/module/agent-avatar/CharacterAvatar.d.ts.map +1 -1
  168. package/lib/typescript/module/agent-avatar/character-colors.d.ts +13 -0
  169. package/lib/typescript/module/agent-avatar/character-colors.d.ts.map +1 -0
  170. package/lib/typescript/module/agent-avatar/character-shapes.d.ts +19 -0
  171. package/lib/typescript/module/agent-avatar/character-shapes.d.ts.map +1 -0
  172. package/lib/typescript/module/agent-avatar/config-character.d.ts +4 -0
  173. package/lib/typescript/module/agent-avatar/config-character.d.ts.map +1 -1
  174. package/lib/typescript/module/agent-avatar/legacy-recipe.d.ts +2 -10
  175. package/lib/typescript/module/agent-avatar/legacy-recipe.d.ts.map +1 -1
  176. package/lib/typescript/module/agent-creator/AgentCreatorBase.d.ts.map +1 -1
  177. package/lib/typescript/module/agent-creator/CharacterControls.d.ts +2 -1
  178. package/lib/typescript/module/agent-creator/CharacterControls.d.ts.map +1 -1
  179. package/lib/typescript/module/agent-creator/ClippoArtwork.d.ts +6 -0
  180. package/lib/typescript/module/agent-creator/ClippoArtwork.d.ts.map +1 -0
  181. package/lib/typescript/module/agent-creator/CyclopsArtwork.d.ts +5 -0
  182. package/lib/typescript/module/agent-creator/CyclopsArtwork.d.ts.map +1 -0
  183. package/lib/typescript/module/agent-creator/EmotionPicker.d.ts +1 -0
  184. package/lib/typescript/module/agent-creator/EmotionPicker.d.ts.map +1 -1
  185. package/lib/typescript/module/agent-creator/ShapeSilhouette.d.ts.map +1 -1
  186. package/lib/typescript/module/agent-creator/ToddArtwork.d.ts +6 -0
  187. package/lib/typescript/module/agent-creator/ToddArtwork.d.ts.map +1 -0
  188. package/lib/typescript/module/agent-creator/constants.d.ts +7 -15
  189. package/lib/typescript/module/agent-creator/constants.d.ts.map +1 -1
  190. package/lib/typescript/module/agent-creator/messages.d.ts.map +1 -1
  191. package/package.json +1 -1
  192. package/src/agent-avatar/AgentAvatar.tsx +1 -1
  193. package/src/agent-avatar/CharacterAvatar.tsx +4 -1
  194. package/src/agent-avatar/character-colors.ts +12 -0
  195. package/src/agent-avatar/character-shapes.ts +122 -0
  196. package/src/agent-avatar/config-character.ts +39 -6
  197. package/src/agent-avatar/legacy-recipe.ts +34 -6
  198. package/src/agent-creator/AgentCreatorBase.tsx +121 -152
  199. package/src/agent-creator/CharacterControls.tsx +39 -14
  200. package/src/agent-creator/ClippoArtwork.tsx +49 -0
  201. package/src/agent-creator/CyclopsArtwork.tsx +27 -0
  202. package/src/agent-creator/EmotionPicker.tsx +55 -16
  203. package/src/agent-creator/ShapeSilhouette.tsx +16 -1
  204. package/src/agent-creator/ToddArtwork.tsx +24 -0
  205. package/src/agent-creator/constants.ts +47 -14
  206. package/src/agent-creator/messages.ts +2 -1
  207. package/src/locale/translations/ar.ts +1 -1
  208. package/src/locale/translations/bn.ts +1 -1
  209. package/src/locale/translations/ca.ts +1 -1
  210. package/src/locale/translations/de.ts +1 -1
  211. package/src/locale/translations/es.ts +1 -1
  212. package/src/locale/translations/fr.ts +1 -1
  213. package/src/locale/translations/hi.ts +1 -1
  214. package/src/locale/translations/id.ts +1 -1
  215. package/src/locale/translations/it.ts +1 -1
  216. package/src/locale/translations/ja.ts +1 -1
  217. package/src/locale/translations/pt.ts +1 -1
  218. package/src/locale/translations/ru.ts +1 -1
  219. package/src/locale/translations/tr.ts +1 -1
  220. package/src/locale/translations/zh.ts +1 -1
  221. package/src/__tests__/support/adoption-matrix.ts +0 -683
  222. package/src/__tests__/support/card-surface.ts +0 -25
  223. package/src/__tests__/support/collision-fixture-barrel.ts +0 -20
  224. package/src/__tests__/support/commerce-harness.tsx +0 -97
  225. package/src/__tests__/support/composite.ts +0 -68
  226. package/src/__tests__/support/constructed-style-sheets.ts +0 -68
  227. package/src/__tests__/support/messages-in.ts +0 -10
  228. package/src/__tests__/support/press-host.ts +0 -30
  229. package/src/__tests__/support/rendered-style.ts +0 -99
  230. package/src/__tests__/support/unread-hook-fixture.ts +0 -33
  231. package/src/__tests__/support/worklet-capture-fixture.tsx +0 -28
  232. package/src/agent-avatar/__tests__/artwork-golden.json +0 -774
  233. package/src/agent-creator/__tests__/support/character-runtime-fixture.tsx +0 -26
  234. package/src/theme/__tests__/__fixtures__/golden-resolved-tokens.json +0 -9346
  235. package/src/theme/__tests__/__fixtures__/tonal-glass-primary-failures.json +0 -69
  236. package/src/theme/__tests__/fixtures/color-engine-golden.json +0 -1
@@ -0,0 +1,122 @@
1
+ import { avatarHex } from './avatar-color';
2
+ import type { AvatarCharacterConfig } from './config-character';
3
+ import type { AvatarConfig } from './model';
4
+
5
+ export const NATIVE_CHARACTER_SHAPES = [
6
+ ['clippo', 'Clippo'],
7
+ ['todd', 'Todd'],
8
+ ['circle', 'Circle'],
9
+ ['rounded_triangle', 'Rounded triangle'],
10
+ ['capsule', 'Capsule'],
11
+ ['rounded_head_two_ears', 'Rounded head with two ears'],
12
+ ['six_lobed_flower', 'Six lobed flower'],
13
+ ['heart', 'Heart'],
14
+ ['four_lobed_butterfly', 'Four lobed butterfly'],
15
+ ['circle_two_ears', 'Circle with two ears'],
16
+ ['twelve_scalloped_rosette', 'Twelve scalloped rosette'],
17
+ ['rounded_cube', 'Hexagon'],
18
+ ['rounded_diamond', 'Rounded diamond'],
19
+ ] as const;
20
+ export const MIGRATED_CHARACTER_SHAPES = [
21
+ ['slender', 'Slender'],
22
+ ['pocket', 'Pocket'],
23
+ ['petal', 'Petal'],
24
+ ['star', 'Star'],
25
+ ['cloud', 'Cloud'],
26
+ ['shield', 'Shield'],
27
+ ['pebble', 'Pebble'],
28
+ ['squircle', 'Squircle'],
29
+ ] as const;
30
+ export const CHARACTER_SHAPES = [
31
+ ...NATIVE_CHARACTER_SHAPES,
32
+ ...MIGRATED_CHARACTER_SHAPES,
33
+ ] as const;
34
+
35
+ export function isMigratedCharacterShape(id: string | undefined): boolean {
36
+ return MIGRATED_CHARACTER_SHAPES.some(([shape]) => shape === id);
37
+ }
38
+
39
+ /** The saved paper/blob fields remain readable; canonical shape IDs own new edits. */
40
+ export function currentCharacterShape(config: AvatarConfig): string {
41
+ if (config.character?.selections?.shape)
42
+ return config.character.selections.shape;
43
+ const shape = config.family === 'fold' ? config.foldShape : config.shape;
44
+ const equivalent: Partial<Record<string, string>> = {
45
+ triangle: 'rounded_triangle',
46
+ flower: 'six_lobed_flower',
47
+ diamond: 'rounded_diamond',
48
+ };
49
+ return equivalent[shape] ?? shape;
50
+ }
51
+
52
+ /** Geometry-only view of one of the eight migrated contours. */
53
+ export function characterShapeGeometry(config: AvatarConfig): AvatarConfig {
54
+ const shape = config.character?.selections?.shape;
55
+ if (!isMigratedCharacterShape(shape)) return config;
56
+ return shape === 'pebble' || shape === 'squircle'
57
+ ? { ...config, family: 'blob', shape }
58
+ : {
59
+ ...config,
60
+ family: 'fold',
61
+ foldShape: shape as AvatarConfig['foldShape'],
62
+ };
63
+ }
64
+
65
+ /** Retain effective authored parts while preparing the next body. An old recipe
66
+ * is promoted once, with its actual paint and contour rather than Felipe defaults.
67
+ */
68
+ export function materializeCharacter(
69
+ config: AvatarConfig,
70
+ selected: Readonly<Record<string, string>> = {},
71
+ ): AvatarCharacterConfig {
72
+ const saved = config.character;
73
+ if (!saved || saved.preset === 'bloom')
74
+ return {
75
+ ...saved,
76
+ preset: 'legacy',
77
+ ...(saved?.bodyColor
78
+ ? { bodyColor: saved.bodyColor }
79
+ : !saved?.selections?.color
80
+ ? { bodyColor: avatarHex(config) }
81
+ : {}),
82
+ selections: {
83
+ shape: currentCharacterShape(config),
84
+ eyes: 'oval',
85
+ eyewear: 'none',
86
+ accessory: 'none',
87
+ ...saved?.selections,
88
+ },
89
+ };
90
+ const defaults: NonNullable<AvatarCharacterConfig['selections']> = {};
91
+ for (const part of ['eyes', 'eyewear', 'accessory'] as const)
92
+ if (selected[part]) defaults[part] = selected[part];
93
+ // These virtual parts are absent from the backing native catalog.
94
+ if (!defaults.eyes && saved.preset === 'clippo') defaults.eyes = 'clippo';
95
+ if (!defaults.eyes && saved.preset === 'lime_frog') defaults.eyes = 'todd';
96
+ if (!defaults.accessory && saved.preset === 'blue_beret')
97
+ defaults.accessory = 'felipe_beret';
98
+ return { ...saved, selections: { ...defaults, ...saved.selections } };
99
+ }
100
+
101
+ /** All 21 bodies change the same character recipe. Named paint and explicit RGB
102
+ * keep their precedence; a body never supplies a replacement color or face.
103
+ */
104
+ export function createConfigForShape(
105
+ config: AvatarConfig,
106
+ shape: string,
107
+ selected: Readonly<Record<string, string>> = {},
108
+ ): AvatarConfig {
109
+ if (!CHARACTER_SHAPES.some(([id]) => id === shape))
110
+ throw new Error('Unknown character shape.');
111
+ const character = materializeCharacter(config, selected);
112
+ if (!character.bodyColor && !character.selections?.color) {
113
+ if (character.preset === 'clippo') character.bodyColor = '#999b9d';
114
+ else if (selected.color)
115
+ character.selections = { ...character.selections, color: selected.color };
116
+ else if (selected.bodyColor) character.bodyColor = selected.bodyColor;
117
+ }
118
+ return characterShapeGeometry({
119
+ ...config,
120
+ character: { ...character, selections: { ...character.selections, shape } },
121
+ });
122
+ }
@@ -1,14 +1,25 @@
1
1
  /** Catalog parts extend the existing avatar recipe without discarding its shape or expression fields. */
2
- export type AvatarCharacterCategory = 'shape' | 'color' | 'eyes' | 'eyewear' | 'accessory';
2
+ export type AvatarCharacterCategory =
3
+ 'shape' | 'color' | 'eyes' | 'eyewear' | 'accessory';
3
4
 
4
5
  export type AvatarCharacterConfig = {
6
+ /** Named catalog preset, including Clippo (`clippo`), or a saved renderer ID. */
5
7
  preset: string;
8
+ /** Clippo's open wire body and ring eyes are independently selectable with ID `clippo`. */
6
9
  selections?: Partial<Record<AvatarCharacterCategory, string>>;
7
10
  /** Custom body RGB accepted by the original renderer, stored as lowercase #RRGGBB. */
8
11
  bodyColor?: string;
12
+ /** @deprecated Retained when reading saved recipes; ignored by the renderer. Each shape owns its eye layout. */
13
+ eyeSpacing?: number;
9
14
  };
10
15
 
11
- const categories: readonly AvatarCharacterCategory[] = ['shape', 'color', 'eyes', 'eyewear', 'accessory'];
16
+ const categories: readonly AvatarCharacterCategory[] = [
17
+ 'shape',
18
+ 'color',
19
+ 'eyes',
20
+ 'eyewear',
21
+ 'accessory',
22
+ ];
12
23
  const validId = (value: unknown): value is string =>
13
24
  typeof value === 'string' && value.length > 0 && value.length <= 63;
14
25
 
@@ -19,22 +30,44 @@ export function parseCharacterConfig(value: unknown): AvatarCharacterConfig {
19
30
  const candidate = value as Record<string, unknown>;
20
31
  if (!validId(candidate.preset)) throw new Error('Invalid character preset.');
21
32
  const result: AvatarCharacterConfig = { preset: candidate.preset };
33
+ if (candidate.eyeSpacing !== undefined) {
34
+ if (
35
+ typeof candidate.eyeSpacing !== 'number' ||
36
+ !Number.isFinite(candidate.eyeSpacing) ||
37
+ candidate.eyeSpacing < 0.5 ||
38
+ candidate.eyeSpacing > 1.5
39
+ )
40
+ throw new Error('Invalid character eye spacing.');
41
+ result.eyeSpacing = candidate.eyeSpacing;
42
+ }
22
43
  if (candidate.bodyColor !== undefined) {
23
- if (typeof candidate.bodyColor !== 'string' || candidate.bodyColor.length !== 7 || !/^#[0-9a-f]{6}$/i.test(candidate.bodyColor))
44
+ if (
45
+ typeof candidate.bodyColor !== 'string' ||
46
+ candidate.bodyColor.length !== 7 ||
47
+ !/^#[0-9a-f]{6}$/i.test(candidate.bodyColor)
48
+ )
24
49
  throw new Error('Invalid character body color.');
25
50
  result.bodyColor = candidate.bodyColor.toLowerCase();
26
51
  }
27
52
  if (candidate.selections !== undefined) {
28
- if (!candidate.selections || typeof candidate.selections !== 'object' || Array.isArray(candidate.selections))
53
+ if (
54
+ !candidate.selections ||
55
+ typeof candidate.selections !== 'object' ||
56
+ Array.isArray(candidate.selections)
57
+ )
29
58
  throw new Error('Invalid character selections.');
30
59
  const selections: NonNullable<AvatarCharacterConfig['selections']> = {};
31
60
  for (const [category, id] of Object.entries(candidate.selections)) {
32
- if (!categories.includes(category as AvatarCharacterCategory) || !validId(id))
61
+ if (
62
+ !categories.includes(category as AvatarCharacterCategory) ||
63
+ !validId(id)
64
+ )
33
65
  throw new Error('Invalid character selection.');
34
66
  selections[category as AvatarCharacterCategory] = id;
35
67
  }
36
68
  result.selections = selections;
37
69
  }
38
- if ('state' in candidate) throw new Error('Opaque character state is not supported.');
70
+ if ('state' in candidate)
71
+ throw new Error('Opaque character state is not supported.');
39
72
  return result;
40
73
  }
@@ -1,3 +1,8 @@
1
+ import { CHARACTER_COLORS } from './character-colors';
2
+ import {
3
+ characterShapeGeometry,
4
+ isMigratedCharacterShape,
5
+ } from './character-shapes';
1
6
  import { avatarHex } from './avatar-color';
2
7
  import { legacyContours } from './legacy-contours';
3
8
  import type { AvatarConfig } from './model';
@@ -9,6 +14,7 @@ export type LegacyRecipe = {
9
14
  points?: Point[];
10
15
  shape?: string;
11
16
  eyes: string;
17
+ eyeSpacing?: number;
12
18
  selections?: Pick<
13
19
  NonNullable<AvatarCharacterConfig['selections']>,
14
20
  'eyewear' | 'accessory'
@@ -18,6 +24,8 @@ export type LegacyRecipe = {
18
24
 
19
25
  /** Saved unsupported artwork stays readable without pretending it was migrated. */
20
26
  export function legacyRecipeUnsupported(config: AvatarConfig): boolean {
27
+ if (isMigratedCharacterShape(config.character?.selections?.shape))
28
+ return false;
21
29
  return (
22
30
  config.family === 'alien' || config.family === 'mascot' || !config.face
23
31
  );
@@ -74,6 +82,8 @@ export function radialContour(polygons: Point[][], samples = 256): Point[] {
74
82
 
75
83
  /** Shared capability identity for the editor and the legacy engine adapter. */
76
84
  export function legacyCharacterRecipe(config: AvatarConfig) {
85
+ if (config.character && config.character.preset !== 'bloom')
86
+ return config.character;
77
87
  const customization =
78
88
  config.character?.preset === 'bloom' ? config.character : undefined;
79
89
  return {
@@ -88,7 +98,15 @@ export function legacyCharacterRecipe(config: AvatarConfig) {
88
98
  ? { accessory: customization.selections.accessory }
89
99
  : {}),
90
100
  },
91
- bodyColor: customization?.bodyColor ?? avatarHex(config),
101
+ bodyColor:
102
+ customization?.bodyColor ??
103
+ CHARACTER_COLORS[
104
+ customization?.selections?.color as keyof typeof CHARACTER_COLORS
105
+ ] ??
106
+ avatarHex(config),
107
+ ...(customization?.eyeSpacing !== undefined
108
+ ? { eyeSpacing: customization.eyeSpacing }
109
+ : {}),
92
110
  };
93
111
  }
94
112
 
@@ -102,14 +120,24 @@ export function legacyRecipe(
102
120
  ): LegacyRecipe {
103
121
  if (legacyRecipeUnsupported(config))
104
122
  throw new Error('Unsupported legacy 3D recipe');
105
- const shape = legacyNativeShape(config);
123
+ const geometry = characterShapeGeometry(config);
124
+ const canonical = config.character && config.character.preset !== 'bloom';
125
+ const selectedShape = canonical
126
+ ? config.character?.selections?.shape
127
+ : undefined;
128
+ const shape = isMigratedCharacterShape(selectedShape)
129
+ ? undefined
130
+ : (selectedShape ?? legacyNativeShape(geometry));
106
131
  const recipe = legacyCharacterRecipe(config);
107
- const { eyewear, accessory } = recipe.selections;
132
+ const { eyewear, accessory } = recipe.selections ?? {};
108
133
  return {
109
134
  ...(shape
110
135
  ? { shape }
111
- : { points: radialContour(legacyContours(config), samples) }),
112
- eyes: recipe.selections.eyes,
136
+ : { points: radialContour(legacyContours(geometry), samples) }),
137
+ eyes: recipe.selections?.eyes ?? 'oval',
138
+ ...(recipe.eyeSpacing !== undefined
139
+ ? { eyeSpacing: recipe.eyeSpacing }
140
+ : {}),
113
141
  ...(eyewear || accessory
114
142
  ? {
115
143
  selections: {
@@ -118,6 +146,6 @@ export function legacyRecipe(
118
146
  },
119
147
  }
120
148
  : {}),
121
- patch: { bodyColor: recipe.bodyColor },
149
+ patch: { bodyColor: recipe.bodyColor ?? avatarHex(config) },
122
150
  };
123
151
  }
@@ -1,15 +1,19 @@
1
- import { useContext, useMemo, useState } from 'react';
1
+ import { useContext, useMemo, useRef, useState } from 'react';
2
2
  import { Platform } from 'react-native';
3
3
  import { AgentAvatar, type AvatarConfig } from '../agent-avatar';
4
+ import {
5
+ CHARACTER_SHAPES,
6
+ NATIVE_CHARACTER_SHAPES,
7
+ createConfigForShape,
8
+ currentCharacterShape,
9
+ materializeCharacter,
10
+ } from '../agent-avatar/character-shapes';
4
11
  import type { AvatarCharacterCategory } from '../agent-avatar/config-character';
5
12
  import {
6
13
  CharacterRuntimeContext,
7
14
  CharacterCapabilitiesContext,
8
15
  } from '../agent-avatar/context';
9
- import {
10
- legacyCharacterRecipe,
11
- legacyNativeShape,
12
- } from '../agent-avatar/legacy-recipe';
16
+ import { legacyCharacterRecipe } from '../agent-avatar/legacy-recipe';
13
17
  import { Button } from '../button/Button';
14
18
  import { CloseButton } from '../button/CloseButton';
15
19
  import { useDirectionProps } from '../hooks/use-is-rtl';
@@ -56,58 +60,86 @@ export function AgentCreatorBase(props: AgentCreatorProps) {
56
60
  );
57
61
  const { runtimeUrl } = runtime;
58
62
  const beta = Boolean(c.character && c.character.preset !== 'bloom');
59
- const nativeShape = legacyNativeShape(c);
60
63
  const key = JSON.stringify(beta ? c.character : legacyCharacterRecipe(c));
61
- const capabilities =
64
+ const currentCapabilities =
62
65
  catalog.capabilitiesByKey?.get(key) ??
63
66
  (catalog.capabilities?.key === key ? catalog.capabilities : undefined);
67
+ const retained = useRef<{ owner: string; selected: Record<string, string> }>({
68
+ owner: '',
69
+ selected: {},
70
+ });
71
+ const owner = JSON.stringify([agent.id, c.character?.preset ?? 'bloom']);
72
+ if (retained.current.owner !== owner)
73
+ retained.current = { owner, selected: {} };
74
+ if (currentCapabilities)
75
+ retained.current.selected = currentCapabilities.selected;
76
+ const capabilities = currentCapabilities ?? {
77
+ selected: retained.current.selected,
78
+ available: {},
79
+ };
64
80
  const betaColor =
65
- capabilities?.selected.color ?? c.character?.selections?.color;
81
+ c.character?.selections?.color ?? capabilities?.selected.color;
82
+ const betaBodyColor =
83
+ c.character?.bodyColor ??
84
+ (c.character?.preset === 'clippo' && !c.character.selections?.color
85
+ ? '#999b9d'
86
+ : undefined) ??
87
+ (!betaColor ? capabilities?.selected.bodyColor : undefined);
66
88
  const customColorValue = beta
67
- ? (c.character?.bodyColor ??
89
+ ? (betaBodyColor ??
90
+ (c.character?.selections?.color
91
+ ? CHARACTER_COLORS[
92
+ c.character.selections.color as keyof typeof CHARACTER_COLORS
93
+ ]
94
+ : undefined) ??
95
+ capabilities?.selected.bodyColor ??
68
96
  CHARACTER_COLORS[betaColor as keyof typeof CHARACTER_COLORS] ??
69
97
  avatarHex(c))
70
- : (c.character?.bodyColor ?? avatarHex(c));
98
+ : (c.character?.bodyColor ??
99
+ CHARACTER_COLORS[
100
+ c.character?.selections?.color as keyof typeof CHARACTER_COLORS
101
+ ] ??
102
+ avatarHex(c));
71
103
  const selectedEyes =
72
- capabilities?.selected.eyes ?? c.character?.selections?.eyes ?? 'oval';
104
+ c.character?.selections?.eyes ??
105
+ capabilities?.selected.eyes ??
106
+ (c.character?.preset === 'clippo'
107
+ ? 'clippo'
108
+ : c.character?.preset === 'lime_frog'
109
+ ? 'todd'
110
+ : 'oval');
111
+ const selectedEyewear =
112
+ c.character?.selections?.eyewear ?? capabilities?.selected.eyewear;
113
+ const selectedAccessory =
114
+ c.character?.selections?.accessory ?? capabilities?.selected.accessory;
115
+ const selectedParts = {
116
+ ...(c.character?.selections?.eyes ||
117
+ capabilities.selected.eyes ||
118
+ c.character?.preset === 'clippo' ||
119
+ c.character?.preset === 'lime_frog'
120
+ ? { eyes: selectedEyes }
121
+ : {}),
122
+ ...(selectedEyewear ? { eyewear: selectedEyewear } : {}),
123
+ ...(selectedAccessory ? { accessory: selectedAccessory } : {}),
124
+ };
73
125
  const characterConfig = (
74
126
  category: AvatarCharacterCategory,
75
127
  id: string,
76
128
  ): AvatarConfig => {
77
- if (
78
- !beta &&
79
- (category === 'eyes' ||
80
- category === 'eyewear' ||
81
- category === 'accessory')
82
- )
83
- return {
84
- ...c,
85
- character: {
86
- ...c.character,
87
- preset: 'bloom',
88
- selections: { ...c.character?.selections, [category]: id },
89
- },
90
- };
91
- const character = beta
92
- ? c.character!
93
- : {
94
- preset: 'blue_beret',
95
- bodyColor: customColorValue,
96
- selections: {
97
- eyewear:
98
- c.character?.selections?.eyewear ??
99
- capabilities?.selected.eyewear ??
100
- 'none',
101
- accessory:
102
- c.character?.selections?.accessory ??
103
- capabilities?.selected.accessory ??
104
- 'none',
105
- eyes: selectedEyes,
106
- },
107
- };
108
- const selections = { ...character.selections, [category]: id };
109
- if (category === 'shape') delete selections.eyes;
110
- const next = { ...character, selections };
129
+ if (category === 'shape')
130
+ return createConfigForShape(c, id, capabilities.selected);
131
+ const character = materializeCharacter(c, capabilities.selected);
132
+ // Choosing a body retains the edited face and the preset's effective parts.
133
+ // Presets supply defaults; their catalog restrictions do not own edits.
134
+ const selections = {
135
+ ...selectedParts,
136
+ ...character.selections,
137
+ [category]: id,
138
+ };
139
+ const next = {
140
+ ...character,
141
+ selections,
142
+ };
111
143
  if (category === 'color') delete next.bodyColor;
112
144
  return { ...c, character: next };
113
145
  };
@@ -117,103 +149,37 @@ export function AgentCreatorBase(props: AgentCreatorProps) {
117
149
  ? runtimeUrl.slice(0, runtimeUrl.lastIndexOf('/') + 1)
118
150
  : '';
119
151
  const eyeBackground = customColorValue;
120
- const migratedShapes = [
121
- ...(['slender', 'pocket', 'petal', 'star', 'cloud', 'shield'] as const).map(
122
- (shape) => ({
123
- family: 'fold' as const,
124
- shape,
125
- title: messages.shapes[shape],
126
- }),
127
- ),
128
- ...(
129
- [
130
- ['pebble', 'Pebble'],
131
- ['squircle', 'Squircle'],
132
- ] as const
133
- ).map(([shape, title]) => ({
134
- family: 'blob' as const,
135
- shape,
136
- title: messages.characterOption('shape', shape, title),
137
- })),
138
- ];
139
- const migratedConfig = (
140
- family: 'fold' | 'blob',
141
- shape: string,
142
- ): AvatarConfig => {
143
- const { character: _character, ...existing } = c;
144
- const color = hexAppearance(customColorValue);
145
- return {
146
- ...existing,
147
- ...color,
148
- lightEyes: beta ? false : c.lightEyes,
149
- family,
150
- ...(family === 'fold'
151
- ? { foldShape: shape as AvatarConfig['foldShape'] }
152
- : { shape: shape as AvatarConfig['shape'] }),
153
- ...(runtimeUrl
154
- ? {
155
- character: {
156
- preset: 'bloom',
157
- selections: {
158
- eyes: selectedEyes,
159
- ...((capabilities?.selected.eyewear ??
160
- c.character?.selections?.eyewear)
161
- ? {
162
- eyewear:
163
- capabilities?.selected.eyewear ??
164
- c.character?.selections?.eyewear,
165
- }
166
- : {}),
167
- ...((capabilities?.selected.accessory ??
168
- c.character?.selections?.accessory)
169
- ? {
170
- accessory:
171
- capabilities?.selected.accessory ??
172
- c.character?.selections?.accessory,
173
- }
174
- : {}),
175
- },
176
- },
177
- }
178
- : {}),
179
- };
180
- };
181
152
  const shapeChoices = runtimeUrl
182
- ? [
183
- ...CHARACTER_OPTIONS.shape.map(([id, title]) => ({
184
- id,
185
- label: messages.characterOption('shape', id, title),
186
- config: characterConfig('shape', id),
187
- thumbnail: `${thumbnailBase}thumbnails/shapes/${id}.png`,
188
- })),
189
- ...migratedShapes.map(({ family, shape, title }) => ({
190
- id: `legacy:${family}:${shape}`,
191
- label: title,
192
- config: migratedConfig(family, shape),
193
- })),
194
- ]
153
+ ? CHARACTER_SHAPES.map(([id, title]) => ({
154
+ id,
155
+ label:
156
+ id in messages.shapes
157
+ ? messages.shapes[id as keyof typeof messages.shapes]
158
+ : messages.characterOption('shape', id, title),
159
+ config: createConfigForShape(c, id, capabilities.selected),
160
+ ...(NATIVE_CHARACTER_SHAPES.some(([shape]) => shape === id) &&
161
+ id !== 'clippo' &&
162
+ id !== 'todd'
163
+ ? { thumbnail: `${thumbnailBase}thumbnails/shapes/${id}.png` }
164
+ : {}),
165
+ }))
195
166
  : undefined;
196
- const shapeValue = beta
197
- ? (capabilities?.selected.shape ?? c.character?.selections?.shape)
198
- : (nativeShape ??
199
- `legacy:${c.family}:${c.family === 'fold' ? c.foldShape : c.shape}`);
200
- const selectShape = (id: string) => {
201
- if (id.startsWith('legacy:')) {
202
- const [, family, shape] = id.split(':');
203
- onChange({
204
- ...agent,
205
- avatar: migratedConfig(family as 'fold' | 'blob', shape!),
206
- });
207
- } else selectCharacter('shape', id);
208
- };
167
+ const shapeValue =
168
+ c.character?.selections?.shape ??
169
+ (beta
170
+ ? (capabilities.selected.shape ??
171
+ (c.character?.preset === 'clippo' ? 'clippo' : undefined))
172
+ : currentCharacterShape(c));
173
+ const selectShape = (id: string) => selectCharacter('shape', id);
209
174
  const eyeChoices = runtimeUrl
210
175
  ? CHARACTER_OPTIONS.eyes.map(([id, title]) => ({
211
176
  id,
212
177
  label: messages.characterOption('eyes', id, title),
213
178
  config: characterConfig('eyes', id),
214
- thumbnail: `${thumbnailBase}thumbnails/eyes/${id}.png`,
215
- disabled:
216
- !capabilities || capabilities.available[`eyes:${id}`] === false,
179
+ ...(id === 'clippo' || id === 'cyclops'
180
+ ? { artwork: id }
181
+ : { thumbnail: `${thumbnailBase}thumbnails/eyes/${id}.png` }),
182
+ disabled: false,
217
183
  }))
218
184
  : undefined;
219
185
  const appearance = (patch: Partial<AvatarConfig>) => {
@@ -367,8 +333,15 @@ export function AgentCreatorBase(props: AgentCreatorProps) {
367
333
  </StyledView>
368
334
  )}
369
335
  <CharacterControls
370
- character={c.character ?? { preset: 'bloom' }}
336
+ character={
337
+ runtimeUrl
338
+ ? beta
339
+ ? c.character
340
+ : materializeCharacter(c, capabilities.selected)
341
+ : c.character
342
+ }
371
343
  capabilitiesKey={key}
344
+ selected={capabilities.selected}
372
345
  onChange={(character) => {
373
346
  if (character)
374
347
  onChange({ ...agent, avatar: { ...c, character } });
@@ -386,7 +359,7 @@ export function AgentCreatorBase(props: AgentCreatorProps) {
386
359
  boxShadow: '0 1px 2px rgba(0,0,0,.05)',
387
360
  }}
388
361
  >
389
- {!beta &&
362
+ {!runtimeUrl &&
390
363
  AVATAR_COLORS.map(([hue, saturation, name, center, edge]) => {
391
364
  const active =
392
365
  c.hue === hue &&
@@ -415,15 +388,11 @@ export function AgentCreatorBase(props: AgentCreatorProps) {
415
388
  </StyledPressable>
416
389
  );
417
390
  })}
418
- {beta &&
391
+ {runtimeUrl &&
419
392
  CHARACTER_OPTIONS.color.map(([id, title]) => {
420
- const active =
421
- !c.character?.bodyColor &&
422
- capabilities?.selected.color === id;
393
+ const active = !betaBodyColor && betaColor === id;
423
394
  const hex = CHARACTER_COLORS[id];
424
- const disabled =
425
- !capabilities ||
426
- capabilities.available[`color:${id}`] === false;
395
+ const disabled = false;
427
396
  return (
428
397
  <StyledPressable
429
398
  key={id}
@@ -446,18 +415,18 @@ export function AgentCreatorBase(props: AgentCreatorProps) {
446
415
  <PopoverTrigger asChild label={messages.customColor}>
447
416
  <StyledPressable
448
417
  aria-pressed={
449
- beta ? Boolean(c.character?.bodyColor) : undefined
418
+ runtimeUrl ? Boolean(betaBodyColor) : undefined
450
419
  }
451
420
  accessibilityState={{
452
- selected: beta && Boolean(c.character?.bodyColor),
421
+ selected: Boolean(runtimeUrl) && Boolean(betaBodyColor),
453
422
  }}
454
423
  className="relative size-[26px] shrink-0 overflow-hidden rounded-full"
455
424
  >
456
425
  <GlossArt
457
- rainbow={!beta || !c.character?.bodyColor}
458
- center={beta ? c.character?.bodyColor : undefined}
459
- edge={beta ? c.character?.bodyColor : undefined}
460
- active={beta && Boolean(c.character?.bodyColor)}
426
+ rainbow={!runtimeUrl || !betaBodyColor}
427
+ center={runtimeUrl ? betaBodyColor : undefined}
428
+ edge={runtimeUrl ? betaBodyColor : undefined}
429
+ active={Boolean(runtimeUrl) && Boolean(betaBodyColor)}
461
430
  />
462
431
  </StyledPressable>
463
432
  </PopoverTrigger>
@@ -473,14 +442,14 @@ export function AgentCreatorBase(props: AgentCreatorProps) {
473
442
  onChange={(hex) => {
474
443
  const next = hexAppearance(hex);
475
444
  if (!next) return;
476
- if (beta)
445
+ if (runtimeUrl)
477
446
  onChange({
478
447
  ...agent,
479
448
  avatar: {
480
449
  ...c,
481
450
  ...next,
482
451
  character: {
483
- ...c.character!,
452
+ ...materializeCharacter(c, capabilities.selected),
484
453
  bodyColor: hex.toLowerCase(),
485
454
  },
486
455
  },