@oxy.so/bloom 7.1.2 → 7.1.3

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
@@ -1,19 +1,28 @@
1
1
  // Adapter for the recovered renderer's prepared vertex layout. Packaged meshes,
2
2
  // resource manifests, the WASM binary, and the appearance record stay untouched.
3
- const decoder = new TextDecoder(), encoder = new TextEncoder();
4
- const BODY = ':body:no-shadow:studio-fur-v6:';
3
+ const decoder = new TextDecoder(),
4
+ encoder = new TextEncoder();
5
+ const BODY = ":body:no-shadow:studio-fur-v6:";
5
6
  // Highlights are separate non-shadow-casting meshes in the original renderer.
6
- const SURFACES = [':surface:shadow', ':surface:no-shadow'];
7
+ const SURFACES = [":surface:shadow", ":surface:no-shadow"];
7
8
  const SURFACE_KEY = /^[a-f0-9]{64}:surface:(?:shadow|no-shadow)$/;
8
9
  const FUR_SURFACE_KEY = /^[a-f0-9]{64}:surface:shadow:studio-fur-v6:/;
9
- const STRIDE = 96, TAU = Math.PI * 2;
10
- function invalid(message) { throw new Error(`Unsupported prepared avatar geometry: ${message}`); }
11
- function finite(value) { if (!Number.isFinite(value)) invalid('non-finite coordinate'); return value; }
10
+ const STRIDE = 96,
11
+ TAU = Math.PI * 2;
12
+ function invalid(message) {
13
+ throw new Error(`Unsupported prepared avatar geometry: ${message}`);
14
+ }
15
+ function finite(value) {
16
+ if (!Number.isFinite(value)) invalid("non-finite coordinate");
17
+ return value;
18
+ }
12
19
  function findAll(bytes, text) {
13
- const needle = encoder.encode(text), result = [];
20
+ const needle = encoder.encode(text),
21
+ result = [];
14
22
  outer: for (let i = 0; i <= bytes.length - needle.length; i++) {
15
23
  if (bytes[i] !== needle[0]) continue;
16
- for (let j = 1; j < needle.length; j++) if (bytes[i + j] !== needle[j]) continue outer;
24
+ for (let j = 1; j < needle.length; j++)
25
+ if (bytes[i + j] !== needle[j]) continue outer;
17
26
  result.push(i);
18
27
  }
19
28
  return result;
@@ -21,82 +30,187 @@ function findAll(bytes, text) {
21
30
 
22
31
  /** Inspect the bounded, length-prefixed mesh records; reject unknown layouts. */
23
32
  export function inspectLegacyAssembly(bytes) {
24
- if (!(bytes instanceof Uint8Array) || bytes.length < 400) invalid('truncated assembly');
33
+ if (!(bytes instanceof Uint8Array) || bytes.length < 400)
34
+ invalid("truncated assembly");
25
35
  const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
26
- const uint = at => { if (at < 0 || at + 4 > bytes.length) invalid('truncated count'); return view.getUint32(at, true); };
36
+ const uint = (at) => {
37
+ if (at < 0 || at + 4 > bytes.length) invalid("truncated count");
38
+ return view.getUint32(at, true);
39
+ };
27
40
  const bodies = findAll(bytes, BODY);
28
- if (bodies.length !== 1) invalid('expected one furry body');
41
+ if (bodies.length !== 1) invalid("expected one furry body");
29
42
  const bodyStart = bodies[0] - 64;
30
- const starts = [bodyStart, ...SURFACES.flatMap(kind => findAll(bytes, kind)
31
- .map(i => i - 64)
32
- .filter(i => {
33
- if (i <= bodyStart || i < 4) return false;
34
- const length = uint(i - 4);
35
- if (length < 79 || length > 8192 || i + length > bytes.length) return false;
36
- const name = decoder.decode(bytes.subarray(i, i + length));
37
- return SURFACE_KEY.test(name) || FUR_SURFACE_KEY.test(name);
38
- }))
39
- .sort((a, b) => a - b)];
43
+ const starts = [
44
+ bodyStart,
45
+ ...SURFACES.flatMap((kind) =>
46
+ findAll(bytes, kind)
47
+ .map((i) => i - 64)
48
+ .filter((i) => {
49
+ if (i <= bodyStart || i < 4) return false;
50
+ const length = uint(i - 4);
51
+ if (length < 79 || length > 8192 || i + length > bytes.length)
52
+ return false;
53
+ const name = decoder.decode(bytes.subarray(i, i + length));
54
+ return SURFACE_KEY.test(name) || FUR_SURFACE_KEY.test(name);
55
+ }),
56
+ ).sort((a, b) => a - b),
57
+ ];
40
58
  const meshCount = uint(0);
41
- if (meshCount !== starts.length || meshCount > 40) invalid('mesh count/key mismatch');
59
+ if (meshCount !== starts.length || meshCount > 40)
60
+ invalid("mesh count/key mismatch");
42
61
  const parts = starts.map((nameOffset, part) => {
43
- const nameLength = uint(nameOffset - 4), name = decoder.decode(bytes.subarray(nameOffset, nameOffset + nameLength));
44
- if (nameLength < 79 || nameLength > 8192 || nameOffset + nameLength > bytes.length || !/^[a-f0-9]{64}:/.test(name)) invalid('mesh identity');
45
- if (part === 0 ? !name.includes(BODY) : !SURFACE_KEY.test(name) && !FUR_SURFACE_KEY.test(name)) invalid('mesh kind');
46
- const vertexCount = uint(nameOffset + nameLength), vertexOffset = nameOffset + nameLength + 4;
47
- if (vertexCount < 3 || vertexCount > 1000000) invalid('vertex count');
62
+ const nameLength = uint(nameOffset - 4),
63
+ name = decoder.decode(
64
+ bytes.subarray(nameOffset, nameOffset + nameLength),
65
+ );
66
+ if (
67
+ nameLength < 79 ||
68
+ nameLength > 8192 ||
69
+ nameOffset + nameLength > bytes.length ||
70
+ !/^[a-f0-9]{64}:/.test(name)
71
+ )
72
+ invalid("mesh identity");
73
+ if (
74
+ part === 0
75
+ ? !name.includes(BODY)
76
+ : !SURFACE_KEY.test(name) && !FUR_SURFACE_KEY.test(name)
77
+ )
78
+ invalid("mesh kind");
79
+ const vertexCount = uint(nameOffset + nameLength),
80
+ vertexOffset = nameOffset + nameLength + 4;
81
+ if (vertexCount < 3 || vertexCount > 1000000) invalid("vertex count");
48
82
  let at = vertexOffset + vertexCount * STRIDE;
49
- const count32 = uint(at); at += 4;
50
- if (count32 > 6000000 || at + count32 * 4 > bytes.length) invalid('32-bit indices');
51
- for (let i = 0; i < count32; i++) if (uint(at + i * 4) >= vertexCount) invalid('index outside mesh');
83
+ const count32 = uint(at);
84
+ at += 4;
85
+ if (count32 > 6000000 || at + count32 * 4 > bytes.length)
86
+ invalid("32-bit indices");
87
+ for (let i = 0; i < count32; i++)
88
+ if (uint(at + i * 4) >= vertexCount) invalid("index outside mesh");
52
89
  at += count32 * 4;
53
- const count16 = uint(at); at += 4;
54
- if (count16 > 6000000 || at + count16 * 2 > bytes.length) invalid('16-bit indices');
55
- for (let i = 0; i < count16; i++) if (view.getUint16(at + i * 2, true) >= vertexCount) invalid('index outside mesh');
90
+ const count16 = uint(at);
91
+ at += 4;
92
+ if (count16 > 6000000 || at + count16 * 2 > bytes.length)
93
+ invalid("16-bit indices");
94
+ for (let i = 0; i < count16; i++)
95
+ if (view.getUint16(at + i * 2, true) >= vertexCount)
96
+ invalid("index outside mesh");
56
97
  at += count16 * 2;
57
98
  // Empty supplemental vertex buffer followed by three boolean mesh flags.
58
- if (at + 7 > bytes.length || uint(at) !== 0 || bytes[at + 4] > 1 || bytes[at + 5] > 1 || bytes[at + 6] > 1) invalid('mesh flags');
59
- const hasFur = bytes[at + 6] === 1; at += 7;
60
- const furCount = hasFur ? uint(at) : 0, furOffset = hasFur ? at + 4 : 0;
61
- if (furCount > 1000000 || hasFur && furOffset + furCount * 64 > bytes.length) invalid('fur records');
99
+ if (
100
+ at + 7 > bytes.length ||
101
+ uint(at) !== 0 ||
102
+ bytes[at + 4] > 1 ||
103
+ bytes[at + 5] > 1 ||
104
+ bytes[at + 6] > 1
105
+ )
106
+ invalid("mesh flags");
107
+ const hasFur = bytes[at + 6] === 1;
108
+ at += 7;
109
+ const furCount = hasFur ? uint(at) : 0,
110
+ furOffset = hasFur ? at + 4 : 0;
111
+ if (
112
+ furCount > 1000000 ||
113
+ (hasFur && furOffset + furCount * 64 > bytes.length)
114
+ )
115
+ invalid("fur records");
62
116
  const geometryEnd = hasFur ? furOffset + furCount * 64 : at;
63
- if (geometryEnd > (starts[part + 1] === undefined ? bytes.length : starts[part + 1] - 4)) invalid('overlapping meshes');
64
- if (part === 0 && !hasFur) invalid('body lacks fur layout');
65
- if (FUR_SURFACE_KEY.test(name) && !hasFur) invalid('accessory lacks fur layout');
66
- const min = [Infinity, Infinity, Infinity], max = [-Infinity, -Infinity, -Infinity];
67
- for (let i = 0; i < vertexCount; i++) for (let k = 0; k < 24; k++) {
68
- const value = finite(view.getFloat32(vertexOffset + i * STRIDE + k * 4, true));
69
- if (k < 3) { min[k] = Math.min(min[k], value); max[k] = Math.max(max[k], value); }
70
- }
71
- return { part, name, nameOffset, nameLength, vertexCount, vertexOffset, furCount, furOffset, bounds: { min, max } };
117
+ if (
118
+ geometryEnd >
119
+ (starts[part + 1] === undefined ? bytes.length : starts[part + 1] - 4)
120
+ )
121
+ invalid("overlapping meshes");
122
+ if (part === 0 && !hasFur) invalid("body lacks fur layout");
123
+ if (FUR_SURFACE_KEY.test(name) && !hasFur)
124
+ invalid("accessory lacks fur layout");
125
+ const min = [Infinity, Infinity, Infinity],
126
+ max = [-Infinity, -Infinity, -Infinity];
127
+ for (let i = 0; i < vertexCount; i++)
128
+ for (let k = 0; k < 24; k++) {
129
+ const value = finite(
130
+ view.getFloat32(vertexOffset + i * STRIDE + k * 4, true),
131
+ );
132
+ if (k < 3) {
133
+ min[k] = Math.min(min[k], value);
134
+ max[k] = Math.max(max[k], value);
135
+ }
136
+ }
137
+ return {
138
+ part,
139
+ name,
140
+ nameOffset,
141
+ nameLength,
142
+ vertexCount,
143
+ vertexOffset,
144
+ furCount,
145
+ furOffset,
146
+ geometryEnd,
147
+ bounds: { min, max },
148
+ };
72
149
  });
73
150
  return { body: parts[0], parts };
74
151
  }
75
152
 
76
153
  function mapper({ points, radii, depthScale = 1 }) {
77
- if (!Number.isFinite(depthScale) || depthScale <= 0 || depthScale > 4) invalid('depth scale');
78
- if (points && radii || !points && !radii) invalid('provide points or radii');
154
+ if (!Number.isFinite(depthScale) || depthScale <= 0 || depthScale > 4)
155
+ invalid("depth scale");
156
+ if ((points && radii) || (!points && !radii))
157
+ invalid("provide points or radii");
79
158
  const count = (points || radii).length;
80
- if (count < 16 || count > 65536) invalid('contour resolution');
81
- if (points) for (const p of points) { if (p.length !== 2) invalid('contour point'); p.forEach(finite); }
82
- else for (const radius of radii) if (!(finite(radius) > 0)) invalid('contour radius');
159
+ if (count < 16 || count > 65536) invalid("contour resolution");
160
+ if (points)
161
+ for (const p of points) {
162
+ if (p.length !== 2) invalid("contour point");
163
+ p.forEach(finite);
164
+ }
165
+ else
166
+ for (const radius of radii)
167
+ if (!(finite(radius) > 0)) invalid("contour radius");
83
168
  return (x, y, z) => {
84
- const radius = Math.hypot(x, y), angle = (Math.atan2(y, x) + TAU) % TAU;
85
- const slot = angle / TAU * count, index = Math.floor(slot), t = slot - index, next = (index + 1) % count;
86
- if (points) return [radius * (points[index][0] * (1 - t) + points[next][0] * t), radius * (points[index][1] * (1 - t) + points[next][1] * t), z * depthScale];
169
+ const radius = Math.hypot(x, y),
170
+ angle = (Math.atan2(y, x) + TAU) % TAU;
171
+ const slot = (angle / TAU) * count,
172
+ index = Math.floor(slot),
173
+ t = slot - index,
174
+ next = (index + 1) % count;
175
+ if (points)
176
+ return [
177
+ radius * (points[index][0] * (1 - t) + points[next][0] * t),
178
+ radius * (points[index][1] * (1 - t) + points[next][1] * t),
179
+ z * depthScale,
180
+ ];
87
181
  const scale = radii[index] * (1 - t) + radii[next] * t;
88
182
  return [x * scale, y * scale, z * depthScale];
89
183
  };
90
184
  }
91
185
  function derivative(map, p) {
92
- const epsilon = .00005, base = map(...p);
93
- const columns = [0, 1, 2].map(k => { const q = [...p]; q[k] += epsilon; return map(...q).map((v, i) => (v - base[i]) / epsilon); });
186
+ const epsilon = 0.00005,
187
+ base = map(...p);
188
+ const columns = [0, 1, 2].map((k) => {
189
+ const q = [...p];
190
+ q[k] += epsilon;
191
+ return map(...q).map((v, i) => (v - base[i]) / epsilon);
192
+ });
94
193
  const [a, b, c] = columns;
95
- const cross = (u, v) => [u[1] * v[2] - u[2] * v[1], u[2] * v[0] - u[0] * v[2], u[0] * v[1] - u[1] * v[0]];
194
+ const cross = (u, v) => [
195
+ u[1] * v[2] - u[2] * v[1],
196
+ u[2] * v[0] - u[0] * v[2],
197
+ u[0] * v[1] - u[1] * v[0],
198
+ ];
96
199
  const inverse = [cross(b, c), cross(c, a), cross(a, b)];
97
200
  const determinant = a.reduce((sum, v, i) => sum + v * inverse[0][i], 0);
98
- if (Math.abs(determinant) < .000001) invalid('singular contour');
99
- return { base, vector: v => [0, 1, 2].map(i => columns.reduce((sum, col, j) => sum + col[i] * v[j], 0)), normal: v => [0, 1, 2].map(i => inverse.reduce((sum, col, j) => sum + col[i] * v[j], 0) / determinant) };
201
+ if (Math.abs(determinant) < 0.000001) invalid("singular contour");
202
+ return {
203
+ base,
204
+ vector: (v) =>
205
+ [0, 1, 2].map((i) =>
206
+ columns.reduce((sum, col, j) => sum + col[i] * v[j], 0),
207
+ ),
208
+ normal: (v) =>
209
+ [0, 1, 2].map(
210
+ (i) =>
211
+ inverse.reduce((sum, col, j) => sum + col[i] * v[j], 0) / determinant,
212
+ ),
213
+ };
100
214
  }
101
215
 
102
216
  /** Rigid attachment anchors on the source circle, in the engine's XY units.
@@ -105,9 +219,10 @@ function derivative(map, p) {
105
219
  */
106
220
  export function legacyAttachmentOffset(points, accessory) {
107
221
  let anchor;
108
- if (accessory === 'bow') anchor = [0, -1, 0];
109
- else if (accessory === 'three_lobe') anchor = [-.5, Math.sqrt(.75), 0];
110
- else if (['beanie', 'hat', 'beret', 'orb', 'crown'].includes(accessory)) anchor = [0, 1, 0];
222
+ if (accessory === "bow") anchor = [0, -1, 0];
223
+ else if (accessory === "three_lobe") anchor = [-0.5, Math.sqrt(0.75), 0];
224
+ else if (["beanie", "hat", "beret", "orb", "crown"].includes(accessory))
225
+ anchor = [0, 1, 0];
111
226
  else return undefined;
112
227
  return mapper({ points })(...anchor).map((value, i) => value - anchor[i]);
113
228
  }
@@ -120,47 +235,89 @@ export function legacyAttachmentOffset(points, accessory) {
120
235
  * Attachments may explicitly transform a numbered part around an origin.
121
236
  */
122
237
  export function deformLegacyAssembly(bytes, options) {
123
- const info = inspectLegacyAssembly(bytes), result = bytes.slice();
124
- if (!/^[a-f0-9]{64}$/.test(options.geometryKey || '')) invalid('missing geometry cache key');
125
- const view = new DataView(result.buffer), map = mapper(options);
126
- const read = at => [0, 1, 2].map(i => view.getFloat32(at + i * 4, true));
127
- const write = (at, values) => values.forEach((v, i) => view.setFloat32(at + i * 4, finite(v), true));
128
- const unit = v => { const n = Math.hypot(...v); return n > 1e-9 ? v.map(x => x / n) : v; };
238
+ const info = inspectLegacyAssembly(bytes),
239
+ result = bytes.slice();
240
+ if (!/^[a-f0-9]{64}$/.test(options.geometryKey || ""))
241
+ invalid("missing geometry cache key");
242
+ const view = new DataView(result.buffer),
243
+ map = mapper(options);
244
+ const read = (at) => [0, 1, 2].map((i) => view.getFloat32(at + i * 4, true));
245
+ const write = (at, values) =>
246
+ values.forEach((v, i) => view.setFloat32(at + i * 4, finite(v), true));
247
+ const unit = (v) => {
248
+ const n = Math.hypot(...v);
249
+ return n > 1e-9 ? v.map((x) => x / n) : v;
250
+ };
129
251
  function transform(part, mapping) {
130
252
  for (let i = 0; i < part.vertexCount; i++) {
131
- const at = part.vertexOffset + i * STRIDE, p = read(at), d = derivative(mapping, p);
253
+ const at = part.vertexOffset + i * STRIDE,
254
+ p = read(at),
255
+ d = derivative(mapping, p);
132
256
  // The two pose positions are DELTAS: preserve zero as zero and deform the
133
257
  // target position before subtracting the newly transformed base.
134
- for (const offset of [24, 48]) { const delta = read(at + offset), target = mapping(...p.map((v, k) => v + delta[k])); write(at + offset, target.map((v, k) => v - d.base[k])); }
135
- for (const offset of [12, 36, 60, 84]) write(at + offset, unit(d.normal(read(at + offset))));
258
+ for (const offset of [24, 48]) {
259
+ const delta = read(at + offset),
260
+ target = mapping(...p.map((v, k) => v + delta[k]));
261
+ write(
262
+ at + offset,
263
+ target.map((v, k) => v - d.base[k]),
264
+ );
265
+ }
266
+ for (const offset of [12, 36, 60, 84])
267
+ write(at + offset, unit(d.normal(read(at + offset))));
136
268
  write(at, d.base);
137
269
  }
138
270
  for (let i = 0; i < part.furCount; i++) {
139
- const at = part.furOffset + i * 64, d = derivative(mapping, read(at));
140
- write(at, d.base); write(at + 12, unit(d.normal(read(at + 12)))); write(at + 24, unit(d.vector(read(at + 24))));
271
+ const at = part.furOffset + i * 64,
272
+ d = derivative(mapping, read(at));
273
+ write(at, d.base);
274
+ write(at + 12, unit(d.normal(read(at + 12))));
275
+ write(at + 24, unit(d.vector(read(at + 24))));
141
276
  }
142
277
  result.set(encoder.encode(options.geometryKey), part.nameOffset);
143
278
  }
144
279
  transform(info.body, map);
145
280
  for (const attachment of options.attachments || []) {
146
281
  const part = info.parts[attachment.part];
147
- if (!part || part.part === 0) invalid('attachment index');
148
- const scale = attachment.scale || [1, 1, 1], origin = attachment.origin || [0, 0, 0], translate = attachment.translate || [0, 0, 0];
149
- if ([scale, origin, translate].some(v => v.length !== 3 || v.some(x => !Number.isFinite(x))) || scale.some(x => x <= 0)) invalid('attachment transform');
150
- transform(part, (...p) => p.map((v, k) => (v - origin[k]) * scale[k] + origin[k] + translate[k]));
282
+ if (!part || part.part === 0) invalid("attachment index");
283
+ const scale = attachment.scale || [1, 1, 1],
284
+ origin = attachment.origin || [0, 0, 0],
285
+ translate = attachment.translate || [0, 0, 0];
286
+ if (
287
+ [scale, origin, translate].some(
288
+ (v) => v.length !== 3 || v.some((x) => !Number.isFinite(x)),
289
+ ) ||
290
+ scale.some((x) => x <= 0)
291
+ )
292
+ invalid("attachment transform");
293
+ transform(part, (...p) =>
294
+ p.map((v, k) => (v - origin[k]) * scale[k] + origin[k] + translate[k]),
295
+ );
151
296
  // Independent transformed parts must not alias the body's cache identity.
152
297
  const key = options.attachmentKeys?.[attachment.part];
153
- if (!/^[a-f0-9]{64}$/.test(key || '')) invalid('attachment cache key');
298
+ if (!/^[a-f0-9]{64}$/.test(key || "")) invalid("attachment cache key");
154
299
  result.set(encoder.encode(key), part.nameOffset);
155
300
  }
156
301
  // Keep original extents while admitting moved accessories and the new body.
157
- const transformed = [info.body, ...(options.attachments || []).map(a => info.parts[a.part])];
158
- for (const part of transformed) for (let i = 0; i < part.vertexCount; i++) {
159
- const p = read(part.vertexOffset + i * STRIDE);
160
- for (let k = 0; k < 3; k++) {
161
- view.setFloat32(4 + k * 4, Math.min(view.getFloat32(4 + k * 4, true), p[k]), true);
162
- view.setFloat32(16 + k * 4, Math.max(view.getFloat32(16 + k * 4, true), p[k]), true);
302
+ const transformed = [
303
+ info.body,
304
+ ...(options.attachments || []).map((a) => info.parts[a.part]),
305
+ ];
306
+ for (const part of transformed)
307
+ for (let i = 0; i < part.vertexCount; i++) {
308
+ const p = read(part.vertexOffset + i * STRIDE);
309
+ for (let k = 0; k < 3; k++) {
310
+ view.setFloat32(
311
+ 4 + k * 4,
312
+ Math.min(view.getFloat32(4 + k * 4, true), p[k]),
313
+ true,
314
+ );
315
+ view.setFloat32(
316
+ 16 + k * 4,
317
+ Math.max(view.getFloat32(16 + k * 4, true), p[k]),
318
+ true,
319
+ );
320
+ }
163
321
  }
164
- }
165
322
  return result;
166
323
  }
@@ -2,6 +2,14 @@
2
2
  // the recovered engine, packaged meshes and native manifests remain intact.
3
3
  import createModule from './orbit-characters.mjs';
4
4
  import { decodeAppearance, encodeAppearance } from './appearance-codec.mjs';
5
+ import { composePresetBody, composeCatalogParts } from './catalog-parts.mjs';
6
+ import {
7
+ fitAuthoredEyeSpacing,
8
+ fitAuthoredHeadwear,
9
+ shapeFaceLayout,
10
+ authoredAssemblyRecords,
11
+ } from './authored-parts.mjs';
12
+ import { sha256 } from './sha256.mjs';
5
13
  import {
6
14
  deformLegacyAssembly,
7
15
  inspectLegacyAssembly,
@@ -10,13 +18,8 @@ import {
10
18
  const engine = createModule({
11
19
  locateFile: (file) => new URL(file, import.meta.url).href,
12
20
  });
13
- const digest = async (value) =>
14
- Array.from(
15
- new Uint8Array(
16
- await crypto.subtle.digest('SHA-256', typeof value === 'string' ? new TextEncoder().encode(value) : value),
17
- ),
18
- (byte) => byte.toString(16).padStart(2, '0'),
19
- ).join('');
21
+ const digest = (value) =>
22
+ sha256(typeof value === 'string' ? new TextEncoder().encode(value) : value);
20
23
  // Cache only identities, never the large prepared buffers. The same face with
21
24
  // no accessories tells us exactly which meshes belong to eyes and eyewear.
22
25
  const faceIdentities = new Map();
@@ -25,8 +28,12 @@ async function accessoryTransforms(module, data, source) {
25
28
  // The editor selects one accessory. Preserve any unknown multi-accessory
26
29
  // state rather than guessing which of its pieces should move together.
27
30
  if (appearance.accessories.length !== 1) return {};
28
- const translate = legacyAttachmentOffset(data.points, appearance.accessories[0]);
29
- if (!translate || translate.every(value => Math.abs(value) < 1e-6)) return {};
31
+ const translate = legacyAttachmentOffset(
32
+ data.points,
33
+ appearance.accessories[0],
34
+ );
35
+ if (!translate || translate.every((value) => Math.abs(value) < 1e-6))
36
+ return {};
30
37
  appearance.accessories = [];
31
38
  appearance.accessoryColors = {};
32
39
  if (appearance.model) appearance.model.accessories = {};
@@ -34,22 +41,35 @@ async function accessoryTransforms(module, data, source) {
34
41
  const key = `${await digest(bytes)}:${data.quality}:${data.activities}`;
35
42
  let names = faceIdentities.get(key);
36
43
  if (!names) {
37
- const base = module.orbitPrepareAssembly(bytes, data.quality, `${data.key}:attachment-base`, data.activities);
38
- if (!base.bytes) throw new Error(base.error || 'Accessory reference preparation failed');
39
- names = new Set(inspectLegacyAssembly(base.bytes).parts.slice(1).map(part => part.name));
44
+ const base = module.orbitPrepareAssembly(
45
+ bytes,
46
+ data.quality,
47
+ `${data.key}:attachment-base`,
48
+ data.activities,
49
+ );
50
+ if (!base.bytes)
51
+ throw new Error(base.error || 'Accessory reference preparation failed');
52
+ names = new Set(
53
+ inspectLegacyAssembly(base.bytes)
54
+ .parts.slice(1)
55
+ .map((part) => part.name),
56
+ );
40
57
  faceIdentities.set(key, names);
41
- if (faceIdentities.size > 32) faceIdentities.delete(faceIdentities.keys().next().value);
58
+ if (faceIdentities.size > 32)
59
+ faceIdentities.delete(faceIdentities.keys().next().value);
42
60
  }
43
61
  // Do not move anything unless every original face mesh survives unchanged.
44
- const current = new Set(source.parts.slice(1).map(part => part.name));
45
- if ([...names].some(name => !current.has(name))) return {};
46
- const attachments = source.parts.slice(1)
47
- .filter(part => !names.has(part.name))
48
- .map(part => ({ part: part.part, translate }));
62
+ const current = new Set(source.parts.slice(1).map((part) => part.name));
63
+ if ([...names].some((name) => !current.has(name))) return {};
64
+ const attachments = source.parts
65
+ .slice(1)
66
+ .filter((part) => !names.has(part.name))
67
+ .map((part) => ({ part: part.part, translate }));
49
68
  const attachmentKeys = {};
50
- for (const attachment of attachments) attachmentKeys[attachment.part] = await digest(
51
- JSON.stringify([source.parts[attachment.part].name, translate]),
52
- );
69
+ for (const attachment of attachments)
70
+ attachmentKeys[attachment.part] = await digest(
71
+ JSON.stringify([source.parts[attachment.part].name, translate]),
72
+ );
53
73
  return { attachments, attachmentKeys };
54
74
  }
55
75
  let queue = Promise.resolve();
@@ -63,7 +83,12 @@ self.onmessage = ({ data }) => {
63
83
  data.key,
64
84
  data.activities,
65
85
  );
66
- if (result.bytes) {
86
+ // Native preparation may return a view of temporary storage. Subsequent
87
+ // face/donor reference calls must never overwrite the target scene.
88
+ if (result.bytes) result.bytes = result.bytes.slice();
89
+ if (result.bytes && data.authoredParts?.bodyPreset)
90
+ result.bytes = await composePresetBody(module, result.bytes, data);
91
+ if (result.bytes && data.points) {
67
92
  const source = inspectLegacyAssembly(result.bytes);
68
93
  const geometryKey = await digest(
69
94
  JSON.stringify([source.body.name, data.points]),
@@ -71,9 +96,43 @@ self.onmessage = ({ data }) => {
71
96
  result.bytes = deformLegacyAssembly(result.bytes, {
72
97
  points: data.points,
73
98
  geometryKey,
74
- ...await accessoryTransforms(module, data, source),
99
+ ...(await accessoryTransforms(module, data, source)),
75
100
  });
76
101
  }
102
+ if (result.bytes && data.authoredParts)
103
+ data.faceLayout = shapeFaceLayout(
104
+ authoredAssemblyRecords(result.bytes),
105
+ data,
106
+ module,
107
+ );
108
+ if (
109
+ result.bytes &&
110
+ (data.authoredParts?.shape === 'clippo' ||
111
+ data.authoredParts?.eyes === 'clippo')
112
+ ) {
113
+ const { composeClippoAssembly } = await import('./clippo-geometry.mjs');
114
+ result.bytes = await composeClippoAssembly(module, result.bytes, data);
115
+ }
116
+ if (result.bytes && data.authoredParts) {
117
+ result.bytes = await composeCatalogParts(module, result.bytes, data, [
118
+ 'eyes',
119
+ 'accessory',
120
+ ]);
121
+ if (data.authoredParts.eyes === 'cyclops') {
122
+ const { composeCyclopsAssembly } =
123
+ await import('./cyclops-geometry.mjs');
124
+ result.bytes = await composeCyclopsAssembly(
125
+ module,
126
+ result.bytes,
127
+ data,
128
+ );
129
+ }
130
+ result.bytes = await composeCatalogParts(module, result.bytes, data, [
131
+ 'eyewear',
132
+ ]);
133
+ result.bytes = await fitAuthoredEyeSpacing(result.bytes, data);
134
+ result.bytes = await fitAuthoredHeadwear(result.bytes, data);
135
+ }
77
136
  self.postMessage(
78
137
  { id: data.id, ...result },
79
138
  result.bytes ? [result.bytes.buffer] : [],
@@ -1,21 +1,10 @@
1
1
  import { renderBudget } from './render-budget.mjs';
2
+ import { characterPixels } from './resolution.mjs';
2
3
 
3
4
  const snapshots = new Map();
4
5
  const waiting = new Map();
5
- const pixelsFor = (canvas) =>
6
- Math.max(
7
- 64,
8
- Math.min(
9
- 512,
10
- Math.round(
11
- Math.min(canvas.clientWidth, canvas.clientHeight) *
12
- Math.min(devicePixelRatio || 1, 2),
13
- ),
14
- ),
15
- );
16
-
17
- // Presentation lifetime is independent of the GPU lease. Overflow avatars keep
18
- // their real engine pixels and can reclaim a lease through pointer/command input.
6
+ // Presentation lifetime is independent of the rendering lease. Offscreen and
7
+ // paused avatars retain real engine pixels until they need rendering again.
19
8
  export function createManagedAvatar(canvas, initial, callbacks, factory) {
20
9
  let props = initial,
21
10
  controller,
@@ -42,7 +31,7 @@ export function createManagedAvatar(canvas, initial, callbacks, factory) {
42
31
  props.config.character,
43
32
  props.legacy,
44
33
  props.config.lookAt,
45
- pixelsFor(canvas),
34
+ characterPixels(canvas),
46
35
  ]);
47
36
  const priority = () =>
48
37
  pendingWork || pendingReaction || performance.now() < urgentUntil
@@ -67,7 +56,8 @@ export function createManagedAvatar(canvas, initial, callbacks, factory) {
67
56
  refresh();
68
57
  }
69
58
  function commands() {
70
- if (!controller?.diagnostics?.()?.ready) return;
59
+ const state = controller?.diagnostics?.();
60
+ if (disposed || still() || !ready || !state?.ready || state.pending) return;
71
61
  if (pendingWork)
72
62
  promote(
73
63
  2500 * Math.max(1, Math.min(10, props.workingCycles || 1)) + 1000,
@@ -118,9 +108,13 @@ export function createManagedAvatar(canvas, initial, callbacks, factory) {
118
108
  for (const receive of waiting.get(cacheKey) ?? []) receive();
119
109
  }
120
110
  callbacks.onPaint?.();
111
+ // Scope replacements and in-place edits may settle without another
112
+ // onReady event. Deliver retained commands only after their paint.
113
+ if (pendingWork || pendingReaction) queueMicrotask(commands);
121
114
  },
122
115
  });
123
116
  controller = created;
117
+ if (!disposed) callbacks.onPreparationStart?.();
124
118
  created.update(props, true);
125
119
  return created;
126
120
  } finally {
@@ -178,7 +172,8 @@ export function createManagedAvatar(canvas, initial, callbacks, factory) {
178
172
  pendingReaction = false;
179
173
  }
180
174
  appearance();
181
- const queueCommands = !controller?.diagnostics?.()?.ready;
175
+ const state = controller?.diagnostics?.();
176
+ const queueCommands = !ready || !state?.ready || !!state.pending;
182
177
  if (!still() && (work || reaction)) {
183
178
  if (queueCommands) {
184
179
  pendingWork ||= work;