@oxy.so/bloom 7.1.1 → 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.
- package/assets/character-runtime/README.md +114 -19
- package/assets/character-runtime/authored-parts.mjs +754 -0
- package/assets/character-runtime/catalog-parts.mjs +362 -0
- package/assets/character-runtime/character-recipe.mjs +247 -0
- package/assets/character-runtime/clippo-geometry.mjs +679 -0
- package/assets/character-runtime/controller-mode.mjs +208 -0
- package/assets/character-runtime/cyclops-geometry.mjs +180 -0
- package/assets/character-runtime/legacy-engine.mjs +406 -54
- package/assets/character-runtime/legacy-geometry.mjs +242 -85
- package/assets/character-runtime/legacy-worker.mjs +82 -23
- package/assets/character-runtime/managed-avatar.mjs +12 -17
- package/assets/character-runtime/migrated-contours.mjs +2067 -0
- package/assets/character-runtime/migrated-faces.mjs +59 -0
- package/assets/character-runtime/pose-transition.mjs +523 -0
- package/assets/character-runtime/preparation-cache.mjs +60 -0
- package/assets/character-runtime/render-budget.mjs +5 -3
- package/assets/character-runtime/render-errors.mjs +180 -0
- package/assets/character-runtime/resolution.mjs +10 -0
- package/assets/character-runtime/runtime.mjs +744 -295
- package/assets/character-runtime/sha256.mjs +44 -0
- package/assets/character-runtime/shared-programs.mjs +534 -0
- package/assets/character-runtime/shared-surface.mjs +490 -0
- package/assets/character-runtime/shared-textures.mjs +420 -0
- package/assets/character-runtime/thumbnails/eyes/todd.png +0 -0
- package/docs/adoption-matrix.mdx +2 -2
- package/docs/agent-avatar.mdx +45 -8
- package/docs/agent-creator.mdx +13 -3
- package/docs/bottom-sheet.mdx +1 -1
- package/lib/commonjs/agent-avatar/AgentAvatar.js +1 -1
- package/lib/commonjs/agent-avatar/AgentAvatar.js.map +1 -1
- package/lib/commonjs/agent-avatar/CharacterAvatar.js +2 -1
- package/lib/commonjs/agent-avatar/CharacterAvatar.js.map +1 -1
- package/lib/commonjs/agent-avatar/character-colors.js +19 -0
- package/lib/commonjs/agent-avatar/character-colors.js.map +1 -0
- package/lib/commonjs/agent-avatar/character-shapes.js +106 -0
- package/lib/commonjs/agent-avatar/character-shapes.js.map +1 -0
- package/lib/commonjs/agent-avatar/config-character.js +4 -0
- package/lib/commonjs/agent-avatar/config-character.js.map +1 -1
- package/lib/commonjs/agent-avatar/legacy-recipe.js +19 -6
- package/lib/commonjs/agent-avatar/legacy-recipe.js.map +1 -1
- package/lib/commonjs/agent-creator/AgentCreatorBase.js +67 -107
- package/lib/commonjs/agent-creator/AgentCreatorBase.js.map +1 -1
- package/lib/commonjs/agent-creator/CharacterControls.js +23 -5
- package/lib/commonjs/agent-creator/CharacterControls.js.map +1 -1
- package/lib/commonjs/agent-creator/ClippoArtwork.js +63 -0
- package/lib/commonjs/agent-creator/ClippoArtwork.js.map +1 -0
- package/lib/commonjs/agent-creator/CyclopsArtwork.js +45 -0
- package/lib/commonjs/agent-creator/CyclopsArtwork.js.map +1 -0
- package/lib/commonjs/agent-creator/EmotionPicker.js +8 -1
- package/lib/commonjs/agent-creator/EmotionPicker.js.map +1 -1
- package/lib/commonjs/agent-creator/ShapeSilhouette.js +8 -1
- package/lib/commonjs/agent-creator/ShapeSilhouette.js.map +1 -1
- package/lib/commonjs/agent-creator/ToddArtwork.js +29 -0
- package/lib/commonjs/agent-creator/ToddArtwork.js.map +1 -0
- package/lib/commonjs/agent-creator/constants.js +16 -17
- package/lib/commonjs/agent-creator/constants.js.map +1 -1
- package/lib/commonjs/agent-creator/messages.js +1 -1
- package/lib/commonjs/agent-creator/messages.js.map +1 -1
- package/lib/commonjs/bottom-sheet/BottomSheetBase.js +33 -49
- package/lib/commonjs/bottom-sheet/BottomSheetBase.js.map +1 -1
- package/lib/commonjs/dialog/DialogBottomSheet.js +1 -1
- package/lib/commonjs/dialog/DialogBottomSheet.js.map +1 -1
- package/lib/commonjs/locale/translations/ar.js +1 -1
- package/lib/commonjs/locale/translations/ar.js.map +1 -1
- package/lib/commonjs/locale/translations/bn.js +1 -1
- package/lib/commonjs/locale/translations/bn.js.map +1 -1
- package/lib/commonjs/locale/translations/ca.js +1 -1
- package/lib/commonjs/locale/translations/ca.js.map +1 -1
- package/lib/commonjs/locale/translations/de.js +1 -1
- package/lib/commonjs/locale/translations/de.js.map +1 -1
- package/lib/commonjs/locale/translations/es.js +1 -1
- package/lib/commonjs/locale/translations/es.js.map +1 -1
- package/lib/commonjs/locale/translations/fr.js +1 -1
- package/lib/commonjs/locale/translations/fr.js.map +1 -1
- package/lib/commonjs/locale/translations/hi.js +1 -1
- package/lib/commonjs/locale/translations/hi.js.map +1 -1
- package/lib/commonjs/locale/translations/id.js +1 -1
- package/lib/commonjs/locale/translations/id.js.map +1 -1
- package/lib/commonjs/locale/translations/it.js +1 -1
- package/lib/commonjs/locale/translations/it.js.map +1 -1
- package/lib/commonjs/locale/translations/ja.js +1 -1
- package/lib/commonjs/locale/translations/ja.js.map +1 -1
- package/lib/commonjs/locale/translations/pt.js +1 -1
- package/lib/commonjs/locale/translations/pt.js.map +1 -1
- package/lib/commonjs/locale/translations/ru.js +1 -1
- package/lib/commonjs/locale/translations/ru.js.map +1 -1
- package/lib/commonjs/locale/translations/tr.js +1 -1
- package/lib/commonjs/locale/translations/tr.js.map +1 -1
- package/lib/commonjs/locale/translations/zh.js +1 -1
- package/lib/commonjs/locale/translations/zh.js.map +1 -1
- package/lib/commonjs/surface/SurfacePaint.js +17 -4
- package/lib/commonjs/surface/SurfacePaint.js.map +1 -1
- package/lib/module/agent-avatar/AgentAvatar.js +1 -1
- package/lib/module/agent-avatar/AgentAvatar.js.map +1 -1
- package/lib/module/agent-avatar/CharacterAvatar.js +2 -1
- package/lib/module/agent-avatar/CharacterAvatar.js.map +1 -1
- package/lib/module/agent-avatar/character-colors.js +15 -0
- package/lib/module/agent-avatar/character-colors.js.map +1 -0
- package/lib/module/agent-avatar/character-shapes.js +97 -0
- package/lib/module/agent-avatar/character-shapes.js.map +1 -0
- package/lib/module/agent-avatar/config-character.js +4 -0
- package/lib/module/agent-avatar/config-character.js.map +1 -1
- package/lib/module/agent-avatar/legacy-recipe.js +19 -6
- package/lib/module/agent-avatar/legacy-recipe.js.map +1 -1
- package/lib/module/agent-creator/AgentCreatorBase.js +69 -109
- package/lib/module/agent-creator/AgentCreatorBase.js.map +1 -1
- package/lib/module/agent-creator/CharacterControls.js +24 -6
- package/lib/module/agent-creator/CharacterControls.js.map +1 -1
- package/lib/module/agent-creator/ClippoArtwork.js +59 -0
- package/lib/module/agent-creator/ClippoArtwork.js.map +1 -0
- package/lib/module/agent-creator/CyclopsArtwork.js +41 -0
- package/lib/module/agent-creator/CyclopsArtwork.js.map +1 -0
- package/lib/module/agent-creator/EmotionPicker.js +8 -1
- package/lib/module/agent-creator/EmotionPicker.js.map +1 -1
- package/lib/module/agent-creator/ShapeSilhouette.js +8 -1
- package/lib/module/agent-creator/ShapeSilhouette.js.map +1 -1
- package/lib/module/agent-creator/ToddArtwork.js +25 -0
- package/lib/module/agent-creator/ToddArtwork.js.map +1 -0
- package/lib/module/agent-creator/constants.js +8 -16
- package/lib/module/agent-creator/constants.js.map +1 -1
- package/lib/module/agent-creator/messages.js +1 -1
- package/lib/module/agent-creator/messages.js.map +1 -1
- package/lib/module/bottom-sheet/BottomSheetBase.js +33 -49
- package/lib/module/bottom-sheet/BottomSheetBase.js.map +1 -1
- package/lib/module/dialog/DialogBottomSheet.js +1 -1
- package/lib/module/dialog/DialogBottomSheet.js.map +1 -1
- package/lib/module/locale/translations/ar.js +1 -1
- package/lib/module/locale/translations/ar.js.map +1 -1
- package/lib/module/locale/translations/bn.js +1 -1
- package/lib/module/locale/translations/bn.js.map +1 -1
- package/lib/module/locale/translations/ca.js +1 -1
- package/lib/module/locale/translations/ca.js.map +1 -1
- package/lib/module/locale/translations/de.js +1 -1
- package/lib/module/locale/translations/de.js.map +1 -1
- package/lib/module/locale/translations/es.js +1 -1
- package/lib/module/locale/translations/es.js.map +1 -1
- package/lib/module/locale/translations/fr.js +1 -1
- package/lib/module/locale/translations/fr.js.map +1 -1
- package/lib/module/locale/translations/hi.js +1 -1
- package/lib/module/locale/translations/hi.js.map +1 -1
- package/lib/module/locale/translations/id.js +1 -1
- package/lib/module/locale/translations/id.js.map +1 -1
- package/lib/module/locale/translations/it.js +1 -1
- package/lib/module/locale/translations/it.js.map +1 -1
- package/lib/module/locale/translations/ja.js +1 -1
- package/lib/module/locale/translations/ja.js.map +1 -1
- package/lib/module/locale/translations/pt.js +1 -1
- package/lib/module/locale/translations/pt.js.map +1 -1
- package/lib/module/locale/translations/ru.js +1 -1
- package/lib/module/locale/translations/ru.js.map +1 -1
- package/lib/module/locale/translations/tr.js +1 -1
- package/lib/module/locale/translations/tr.js.map +1 -1
- package/lib/module/locale/translations/zh.js +1 -1
- package/lib/module/locale/translations/zh.js.map +1 -1
- package/lib/module/surface/SurfacePaint.js +18 -5
- package/lib/module/surface/SurfacePaint.js.map +1 -1
- package/lib/typescript/commonjs/agent-avatar/CharacterAvatar.d.ts.map +1 -1
- package/lib/typescript/commonjs/agent-avatar/character-colors.d.ts +13 -0
- package/lib/typescript/commonjs/agent-avatar/character-colors.d.ts.map +1 -0
- package/lib/typescript/commonjs/agent-avatar/character-shapes.d.ts +19 -0
- package/lib/typescript/commonjs/agent-avatar/character-shapes.d.ts.map +1 -0
- package/lib/typescript/commonjs/agent-avatar/config-character.d.ts +4 -0
- package/lib/typescript/commonjs/agent-avatar/config-character.d.ts.map +1 -1
- package/lib/typescript/commonjs/agent-avatar/legacy-recipe.d.ts +2 -10
- package/lib/typescript/commonjs/agent-avatar/legacy-recipe.d.ts.map +1 -1
- package/lib/typescript/commonjs/agent-creator/AgentCreatorBase.d.ts.map +1 -1
- package/lib/typescript/commonjs/agent-creator/CharacterControls.d.ts +2 -1
- package/lib/typescript/commonjs/agent-creator/CharacterControls.d.ts.map +1 -1
- package/lib/typescript/commonjs/agent-creator/ClippoArtwork.d.ts +6 -0
- package/lib/typescript/commonjs/agent-creator/ClippoArtwork.d.ts.map +1 -0
- package/lib/typescript/commonjs/agent-creator/CyclopsArtwork.d.ts +5 -0
- package/lib/typescript/commonjs/agent-creator/CyclopsArtwork.d.ts.map +1 -0
- package/lib/typescript/commonjs/agent-creator/EmotionPicker.d.ts +1 -0
- package/lib/typescript/commonjs/agent-creator/EmotionPicker.d.ts.map +1 -1
- package/lib/typescript/commonjs/agent-creator/ShapeSilhouette.d.ts.map +1 -1
- package/lib/typescript/commonjs/agent-creator/ToddArtwork.d.ts +6 -0
- package/lib/typescript/commonjs/agent-creator/ToddArtwork.d.ts.map +1 -0
- package/lib/typescript/commonjs/agent-creator/constants.d.ts +7 -15
- package/lib/typescript/commonjs/agent-creator/constants.d.ts.map +1 -1
- package/lib/typescript/commonjs/agent-creator/messages.d.ts.map +1 -1
- package/lib/typescript/commonjs/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
- package/lib/typescript/commonjs/bottom-sheet/types.d.ts +1 -0
- package/lib/typescript/commonjs/bottom-sheet/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/surface/SurfacePaint.d.ts.map +1 -1
- package/lib/typescript/module/agent-avatar/CharacterAvatar.d.ts.map +1 -1
- package/lib/typescript/module/agent-avatar/character-colors.d.ts +13 -0
- package/lib/typescript/module/agent-avatar/character-colors.d.ts.map +1 -0
- package/lib/typescript/module/agent-avatar/character-shapes.d.ts +19 -0
- package/lib/typescript/module/agent-avatar/character-shapes.d.ts.map +1 -0
- package/lib/typescript/module/agent-avatar/config-character.d.ts +4 -0
- package/lib/typescript/module/agent-avatar/config-character.d.ts.map +1 -1
- package/lib/typescript/module/agent-avatar/legacy-recipe.d.ts +2 -10
- package/lib/typescript/module/agent-avatar/legacy-recipe.d.ts.map +1 -1
- package/lib/typescript/module/agent-creator/AgentCreatorBase.d.ts.map +1 -1
- package/lib/typescript/module/agent-creator/CharacterControls.d.ts +2 -1
- package/lib/typescript/module/agent-creator/CharacterControls.d.ts.map +1 -1
- package/lib/typescript/module/agent-creator/ClippoArtwork.d.ts +6 -0
- package/lib/typescript/module/agent-creator/ClippoArtwork.d.ts.map +1 -0
- package/lib/typescript/module/agent-creator/CyclopsArtwork.d.ts +5 -0
- package/lib/typescript/module/agent-creator/CyclopsArtwork.d.ts.map +1 -0
- package/lib/typescript/module/agent-creator/EmotionPicker.d.ts +1 -0
- package/lib/typescript/module/agent-creator/EmotionPicker.d.ts.map +1 -1
- package/lib/typescript/module/agent-creator/ShapeSilhouette.d.ts.map +1 -1
- package/lib/typescript/module/agent-creator/ToddArtwork.d.ts +6 -0
- package/lib/typescript/module/agent-creator/ToddArtwork.d.ts.map +1 -0
- package/lib/typescript/module/agent-creator/constants.d.ts +7 -15
- package/lib/typescript/module/agent-creator/constants.d.ts.map +1 -1
- package/lib/typescript/module/agent-creator/messages.d.ts.map +1 -1
- package/lib/typescript/module/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
- package/lib/typescript/module/bottom-sheet/types.d.ts +1 -0
- package/lib/typescript/module/bottom-sheet/types.d.ts.map +1 -1
- package/lib/typescript/module/surface/SurfacePaint.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/agent-avatar/AgentAvatar.tsx +1 -1
- package/src/agent-avatar/CharacterAvatar.tsx +4 -1
- package/src/agent-avatar/character-colors.ts +12 -0
- package/src/agent-avatar/character-shapes.ts +122 -0
- package/src/agent-avatar/config-character.ts +39 -6
- package/src/agent-avatar/legacy-recipe.ts +34 -6
- package/src/agent-creator/AgentCreatorBase.tsx +121 -152
- package/src/agent-creator/CharacterControls.tsx +39 -14
- package/src/agent-creator/ClippoArtwork.tsx +49 -0
- package/src/agent-creator/CyclopsArtwork.tsx +27 -0
- package/src/agent-creator/EmotionPicker.tsx +55 -16
- package/src/agent-creator/ShapeSilhouette.tsx +16 -1
- package/src/agent-creator/ToddArtwork.tsx +24 -0
- package/src/agent-creator/constants.ts +47 -14
- package/src/agent-creator/messages.ts +2 -1
- package/src/bottom-sheet/BottomSheetBase.tsx +24 -34
- package/src/bottom-sheet/types.ts +1 -0
- package/src/dialog/DialogBottomSheet.tsx +1 -1
- package/src/locale/translations/ar.ts +1 -1
- package/src/locale/translations/bn.ts +1 -1
- package/src/locale/translations/ca.ts +1 -1
- package/src/locale/translations/de.ts +1 -1
- package/src/locale/translations/es.ts +1 -1
- package/src/locale/translations/fr.ts +1 -1
- package/src/locale/translations/hi.ts +1 -1
- package/src/locale/translations/id.ts +1 -1
- package/src/locale/translations/it.ts +1 -1
- package/src/locale/translations/ja.ts +1 -1
- package/src/locale/translations/pt.ts +1 -1
- package/src/locale/translations/ru.ts +1 -1
- package/src/locale/translations/tr.ts +1 -1
- package/src/locale/translations/zh.ts +1 -1
- package/src/surface/SurfacePaint.tsx +14 -4
|
@@ -0,0 +1,754 @@
|
|
|
1
|
+
import { encodeAppearance } from './appearance-codec.mjs';
|
|
2
|
+
import { inspectLegacyAssembly } from './legacy-geometry.mjs';
|
|
3
|
+
import { sha256 } from './sha256.mjs';
|
|
4
|
+
import { MIGRATED_FACES } from './migrated-faces.mjs';
|
|
5
|
+
import { MIGRATED_CONTOURS } from './migrated-contours.mjs';
|
|
6
|
+
|
|
7
|
+
const encoder = new TextEncoder();
|
|
8
|
+
const BODY = ':body:no-shadow:studio-fur-v6:';
|
|
9
|
+
const fail = (message) => {
|
|
10
|
+
throw new Error(`Authored avatar parts: ${message}`);
|
|
11
|
+
};
|
|
12
|
+
const digest = (value) => sha256(encoder.encode(JSON.stringify(value)));
|
|
13
|
+
|
|
14
|
+
/** Complete native renderable records, including their material/pose prefix.
|
|
15
|
+
* After the vertex/fur payload the native format stores vertex/index counts,
|
|
16
|
+
* three flags and four scalars (27 bytes). The next record begins with bounds;
|
|
17
|
+
* slicing at its mesh name instead would attach the next part's material.
|
|
18
|
+
*/
|
|
19
|
+
export function authoredAssemblyRecords(bytes) {
|
|
20
|
+
const info = inspectLegacyAssembly(bytes);
|
|
21
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
22
|
+
let start = 4;
|
|
23
|
+
const records = info.parts.map((part) => {
|
|
24
|
+
const end = part.geometryEnd + 27;
|
|
25
|
+
if (part.nameOffset - 4 < start + 24 || end > bytes.length)
|
|
26
|
+
fail('truncated renderable');
|
|
27
|
+
if (view.getUint32(part.geometryEnd, true) !== part.vertexCount)
|
|
28
|
+
fail('vertex descriptor mismatch');
|
|
29
|
+
let at = part.vertexOffset + part.vertexCount * 96;
|
|
30
|
+
const indices32 = view.getUint32(at, true);
|
|
31
|
+
at += 4 + indices32 * 4;
|
|
32
|
+
const indices16 = view.getUint32(at, true);
|
|
33
|
+
if (view.getUint32(part.geometryEnd + 4, true) !== indices32 + indices16)
|
|
34
|
+
fail('index descriptor mismatch');
|
|
35
|
+
for (let k = 0; k < 3; k++)
|
|
36
|
+
if (bytes[part.geometryEnd + 8 + k] > 1) fail('descriptor flags');
|
|
37
|
+
for (let k = 0; k < 6; k++)
|
|
38
|
+
if (!Number.isFinite(view.getFloat32(start + k * 4, true)))
|
|
39
|
+
fail('bounds');
|
|
40
|
+
for (let k = 0; k < 3; k++)
|
|
41
|
+
if (
|
|
42
|
+
view.getFloat32(start + k * 4, true) >
|
|
43
|
+
view.getFloat32(start + 12 + k * 4, true)
|
|
44
|
+
)
|
|
45
|
+
fail('inverted bounds');
|
|
46
|
+
for (let k = 0; k < 4; k++)
|
|
47
|
+
if (
|
|
48
|
+
!Number.isFinite(view.getFloat32(part.geometryEnd + 11 + k * 4, true))
|
|
49
|
+
)
|
|
50
|
+
fail('descriptor scalar');
|
|
51
|
+
for (let i = 0; i < part.furCount * 16; i++)
|
|
52
|
+
if (!Number.isFinite(view.getFloat32(part.furOffset + i * 4, true)))
|
|
53
|
+
fail('fur scalar');
|
|
54
|
+
const record = { ...part, start, end };
|
|
55
|
+
start = end;
|
|
56
|
+
return record;
|
|
57
|
+
});
|
|
58
|
+
// Activity-enabled scenes append the original hands/panel assemblies here.
|
|
59
|
+
// Keep that opaque payload intact, rather than imposing a static-scene size.
|
|
60
|
+
if (bytes.length - start < 100 || bytes.length > 64 * 1024 * 1024)
|
|
61
|
+
fail(`scene footer (${bytes.length - start} bytes)`);
|
|
62
|
+
return {
|
|
63
|
+
...info,
|
|
64
|
+
bytes,
|
|
65
|
+
body: records[0],
|
|
66
|
+
records,
|
|
67
|
+
footer: bytes.slice(start),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function midpoint(bounds, axis) {
|
|
72
|
+
return (bounds.min[axis] + bounds.max[axis]) / 2;
|
|
73
|
+
}
|
|
74
|
+
function union(parts) {
|
|
75
|
+
return {
|
|
76
|
+
min: [0, 1, 2].map((k) =>
|
|
77
|
+
Math.min(...parts.map((part) => part.bounds.min[k])),
|
|
78
|
+
),
|
|
79
|
+
max: [0, 1, 2].map((k) =>
|
|
80
|
+
Math.max(...parts.map((part) => part.bounds.max[k])),
|
|
81
|
+
),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
export function hasAuthoredLabel(bytes, part, label) {
|
|
85
|
+
const needle = encoder.encode(label);
|
|
86
|
+
for (
|
|
87
|
+
let at = part.start + 24;
|
|
88
|
+
at + 4 + needle.length <= part.nameOffset - 4;
|
|
89
|
+
at++
|
|
90
|
+
) {
|
|
91
|
+
if (
|
|
92
|
+
new DataView(bytes.buffer, bytes.byteOffset + at, 4).getUint32(
|
|
93
|
+
0,
|
|
94
|
+
true,
|
|
95
|
+
) !== needle.length
|
|
96
|
+
)
|
|
97
|
+
continue;
|
|
98
|
+
if (needle.every((value, i) => bytes[at + 4 + i] === value)) return true;
|
|
99
|
+
}
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
export async function translateAuthoredRecord(bytes, part, translate) {
|
|
103
|
+
return transformAuthoredRecord(bytes, part, { translate });
|
|
104
|
+
}
|
|
105
|
+
/** Uniform native attachment fitting preserves authored proportions and all
|
|
106
|
+
* material/normal records. Pose deltas and known native pivots follow the mesh. */
|
|
107
|
+
export async function transformAuthoredRecord(
|
|
108
|
+
bytes,
|
|
109
|
+
part,
|
|
110
|
+
{ scale = 1, origin = [0, 0, 0], translate = [0, 0, 0] } = {},
|
|
111
|
+
) {
|
|
112
|
+
if (
|
|
113
|
+
!Number.isFinite(scale) ||
|
|
114
|
+
scale <= 0 ||
|
|
115
|
+
scale > 8 ||
|
|
116
|
+
[origin, translate].some(
|
|
117
|
+
(v) =>
|
|
118
|
+
!Array.isArray(v) ||
|
|
119
|
+
v.length !== 3 ||
|
|
120
|
+
v.some((x) => !Number.isFinite(x)),
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
fail('non-finite translated coordinate or invalid attachment scale');
|
|
124
|
+
const result = bytes.slice(part.start, part.end);
|
|
125
|
+
const view = new DataView(result.buffer);
|
|
126
|
+
const move = (at) => {
|
|
127
|
+
for (let k = 0; k < 3; k++) {
|
|
128
|
+
const next = Math.fround(
|
|
129
|
+
(view.getFloat32(at + k * 4, true) - origin[k]) * scale +
|
|
130
|
+
origin[k] +
|
|
131
|
+
translate[k],
|
|
132
|
+
);
|
|
133
|
+
if (!Number.isFinite(next)) fail('non-finite translated coordinate');
|
|
134
|
+
view.setFloat32(at + k * 4, next, true);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
move(0);
|
|
138
|
+
move(12);
|
|
139
|
+
// Authored eye/headwear records store their deformation anchor after the
|
|
140
|
+
// first material albedo. Move that proven XYZ center with their geometry so
|
|
141
|
+
// native blink/reaction deformation still pivots around the fitted part.
|
|
142
|
+
if (view.getUint32(24, true) !== 1) fail('authored material prefix');
|
|
143
|
+
// Original beret and eyewear records also carry an attachment/frame center
|
|
144
|
+
// at96. Measured native labels+flags identify those layouts; eye records use
|
|
145
|
+
// these bytes for different optional fields. Generic hat/frame anchors can
|
|
146
|
+
// differ from the per-mesh material center at40.
|
|
147
|
+
const attachmentKinds = {
|
|
148
|
+
beret: [7],
|
|
149
|
+
beanie: [7],
|
|
150
|
+
hat: [7],
|
|
151
|
+
crown: [7],
|
|
152
|
+
headphones: [2, 3],
|
|
153
|
+
bow: [9],
|
|
154
|
+
orb: [8],
|
|
155
|
+
three_lobe: [8],
|
|
156
|
+
};
|
|
157
|
+
const attachmentAnchor =
|
|
158
|
+
part.nameOffset - 4 - part.start >= 108 &&
|
|
159
|
+
result[93] === 1 &&
|
|
160
|
+
((result[95] === 0 &&
|
|
161
|
+
Object.entries(attachmentKinds).some(
|
|
162
|
+
([label, kinds]) =>
|
|
163
|
+
kinds.includes(result[94]) && hasAuthoredLabel(bytes, part, label),
|
|
164
|
+
)) ||
|
|
165
|
+
(result[92] === 1 &&
|
|
166
|
+
result[94] === 1 &&
|
|
167
|
+
result[95] === 1 &&
|
|
168
|
+
[
|
|
169
|
+
'monocle',
|
|
170
|
+
'tall_oval_frames',
|
|
171
|
+
'separate_trapezoid_lenses',
|
|
172
|
+
'classic_sunglasses',
|
|
173
|
+
'round_sunglasses',
|
|
174
|
+
].some((label) => hasAuthoredLabel(bytes, part, label))));
|
|
175
|
+
move(40);
|
|
176
|
+
if (attachmentAnchor) move(96);
|
|
177
|
+
for (let i = 0; i < part.vertexCount; i++) {
|
|
178
|
+
const at = part.vertexOffset - part.start + i * 96;
|
|
179
|
+
move(at);
|
|
180
|
+
if (scale !== 1)
|
|
181
|
+
for (const offset of [24, 48])
|
|
182
|
+
for (let k = 0; k < 3; k++) {
|
|
183
|
+
const next = Math.fround(
|
|
184
|
+
view.getFloat32(at + offset + k * 4, true) * scale,
|
|
185
|
+
);
|
|
186
|
+
if (!Number.isFinite(next)) fail('non-finite scaled morph');
|
|
187
|
+
view.setFloat32(at + offset + k * 4, next, true);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (
|
|
191
|
+
scale !== 1 &&
|
|
192
|
+
part.nameOffset - 4 - part.start > 160 &&
|
|
193
|
+
view.getUint32(108, true) === 0
|
|
194
|
+
) {
|
|
195
|
+
const effectAt = 116 + view.getUint32(112, true) + 36;
|
|
196
|
+
if (
|
|
197
|
+
effectAt + 10 <= part.nameOffset - 4 - part.start &&
|
|
198
|
+
result[effectAt] === 5
|
|
199
|
+
)
|
|
200
|
+
for (const offset of [2, 6])
|
|
201
|
+
view.setFloat32(
|
|
202
|
+
effectAt + offset,
|
|
203
|
+
view.getFloat32(effectAt + offset, true) * scale,
|
|
204
|
+
true,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
for (let i = 0; i < part.furCount; i++)
|
|
208
|
+
move(part.furOffset - part.start + i * 64);
|
|
209
|
+
// Normals, UVs, material parameters and original blink/reaction deltas stay
|
|
210
|
+
// byte-exact. Translated geometry receives its own native cache identity.
|
|
211
|
+
const key = await digest([part.name, scale, origin, translate]);
|
|
212
|
+
result.set(encoder.encode(key), part.nameOffset - part.start);
|
|
213
|
+
return {
|
|
214
|
+
bytes: result,
|
|
215
|
+
name: key + part.name.slice(64),
|
|
216
|
+
bounds: {
|
|
217
|
+
min: part.bounds.min.map(
|
|
218
|
+
(v, k) => (v - origin[k]) * scale + origin[k] + translate[k],
|
|
219
|
+
),
|
|
220
|
+
max: part.bounds.max.map(
|
|
221
|
+
(v, k) => (v - origin[k]) * scale + origin[k] + translate[k],
|
|
222
|
+
),
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Rebuild the native cache identity and scene extents after attachment fitting. */
|
|
228
|
+
export async function assembleAuthoredRecords(target, records) {
|
|
229
|
+
const names = records.slice(1).map((record) => record.name);
|
|
230
|
+
const bodyName =
|
|
231
|
+
(await digest([target.body.name, names])) +
|
|
232
|
+
BODY +
|
|
233
|
+
names.map((name) => `${name.length}:${name}`).join(':');
|
|
234
|
+
const body = records[0].bytes,
|
|
235
|
+
encoded = encoder.encode(bodyName);
|
|
236
|
+
const nameAt = target.body.nameOffset - target.records[0].start;
|
|
237
|
+
const renamedBody = new Uint8Array(
|
|
238
|
+
body.length + encoded.length - target.body.nameLength,
|
|
239
|
+
);
|
|
240
|
+
renamedBody.set(body.subarray(0, nameAt - 4));
|
|
241
|
+
new DataView(renamedBody.buffer).setUint32(nameAt - 4, encoded.length, true);
|
|
242
|
+
renamedBody.set(encoded, nameAt);
|
|
243
|
+
renamedBody.set(
|
|
244
|
+
body.subarray(nameAt + target.body.nameLength),
|
|
245
|
+
nameAt + encoded.length,
|
|
246
|
+
);
|
|
247
|
+
records[0].bytes = renamedBody;
|
|
248
|
+
const result = new Uint8Array(
|
|
249
|
+
4 +
|
|
250
|
+
records.reduce((sum, record) => sum + record.bytes.length, 0) +
|
|
251
|
+
target.footer.length,
|
|
252
|
+
);
|
|
253
|
+
new DataView(result.buffer).setUint32(0, records.length, true);
|
|
254
|
+
let at = 4;
|
|
255
|
+
for (const record of records) {
|
|
256
|
+
result.set(record.bytes, at);
|
|
257
|
+
at += record.bytes.length;
|
|
258
|
+
}
|
|
259
|
+
result.set(target.footer, at);
|
|
260
|
+
const bounds = union(records),
|
|
261
|
+
footer = new DataView(result.buffer, at, target.footer.length);
|
|
262
|
+
// The footer retains the original animation/panel payload. Its first and
|
|
263
|
+
// third bounds sets are aggregate scene extents; the middle set belongs to
|
|
264
|
+
// the original body and must not move when a hat is added.
|
|
265
|
+
for (const offset of [0, 56])
|
|
266
|
+
for (let k = 0; k < 3; k++) {
|
|
267
|
+
footer.setFloat32(
|
|
268
|
+
offset + k * 4,
|
|
269
|
+
Math.min(footer.getFloat32(offset + k * 4, true), bounds.min[k]),
|
|
270
|
+
true,
|
|
271
|
+
);
|
|
272
|
+
footer.setFloat32(
|
|
273
|
+
offset + 12 + k * 4,
|
|
274
|
+
Math.max(footer.getFloat32(offset + 12 + k * 4, true), bounds.max[k]),
|
|
275
|
+
true,
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
// Reinspect the assembled native layout before allowing it into the renderer.
|
|
279
|
+
authoredAssemblyRecords(result);
|
|
280
|
+
return result;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Historical spacing fields no longer override shape-owned geometry. */
|
|
284
|
+
export async function fitAuthoredEyeSpacing(bytes) {
|
|
285
|
+
return bytes;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const EYE_LABELS = [
|
|
289
|
+
'source_integrated_eye',
|
|
290
|
+
'oval',
|
|
291
|
+
'dots',
|
|
292
|
+
'swept_lids',
|
|
293
|
+
'sparkle_capsules',
|
|
294
|
+
'highlight_capsules',
|
|
295
|
+
'double_highlights',
|
|
296
|
+
'round_inset',
|
|
297
|
+
'crescent_inset',
|
|
298
|
+
'sleepy_lids',
|
|
299
|
+
];
|
|
300
|
+
const nativeFaces = new Map();
|
|
301
|
+
export function authoredEyeRecords(info) {
|
|
302
|
+
return info.records
|
|
303
|
+
.slice(1)
|
|
304
|
+
.filter((p) =>
|
|
305
|
+
EYE_LABELS.some((id) => hasAuthoredLabel(info.bytes, p, id)),
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
function eyeCenters(info) {
|
|
309
|
+
const eyes = authoredEyeRecords(info);
|
|
310
|
+
const sides = [-1, 1].map((sign) =>
|
|
311
|
+
eyes.filter((p) => Math.sign(midpoint(p.bounds, 0)) === sign),
|
|
312
|
+
);
|
|
313
|
+
if (sides.some((side) => !side.length)) fail('catalog face anchors missing');
|
|
314
|
+
return sides.map((side) => union(side));
|
|
315
|
+
}
|
|
316
|
+
/** Capture the body's face once, before selecting any replacement eye style. */
|
|
317
|
+
export function shapeFaceLayout(info, request, module) {
|
|
318
|
+
if (request.authoredParts?.shape === 'clippo')
|
|
319
|
+
return {
|
|
320
|
+
centers: [
|
|
321
|
+
[-102 / 240, 135 / 240, 0.03],
|
|
322
|
+
[46 / 240, 93 / 240, 0.03],
|
|
323
|
+
],
|
|
324
|
+
radius: 0.265,
|
|
325
|
+
};
|
|
326
|
+
const shape =
|
|
327
|
+
request.authoredParts?.faceShape ??
|
|
328
|
+
Object.keys(MIGRATED_CONTOURS).find(
|
|
329
|
+
(id) =>
|
|
330
|
+
request.points?.length === MIGRATED_CONTOURS[id].length &&
|
|
331
|
+
request.points.every((p, i) =>
|
|
332
|
+
p.every((v, k) => Math.abs(v - MIGRATED_CONTOURS[id][i][k]) < 1e-9),
|
|
333
|
+
),
|
|
334
|
+
);
|
|
335
|
+
const migrated = MIGRATED_FACES[shape];
|
|
336
|
+
if (migrated)
|
|
337
|
+
return {
|
|
338
|
+
radius: migrated.radius,
|
|
339
|
+
centers: migrated.centers.map(([x, y]) => {
|
|
340
|
+
const front = authoredBodyFront(info, x, y);
|
|
341
|
+
return [
|
|
342
|
+
x,
|
|
343
|
+
y,
|
|
344
|
+
Number.isFinite(front)
|
|
345
|
+
? front - migrated.radius * 0.3
|
|
346
|
+
: info.body.bounds.max[2] - migrated.radius * 0.3,
|
|
347
|
+
];
|
|
348
|
+
}),
|
|
349
|
+
};
|
|
350
|
+
if (shape && module) {
|
|
351
|
+
let face = nativeFaces.get(shape);
|
|
352
|
+
if (!face) {
|
|
353
|
+
for (const eyes of [
|
|
354
|
+
'oval',
|
|
355
|
+
'round_inset',
|
|
356
|
+
'dots',
|
|
357
|
+
'swept_lids',
|
|
358
|
+
'sparkle_capsules',
|
|
359
|
+
'highlight_capsules',
|
|
360
|
+
'double_highlights',
|
|
361
|
+
'crescent_inset',
|
|
362
|
+
'sleepy_lids',
|
|
363
|
+
]) {
|
|
364
|
+
const result = module.orbitPrepareAssembly(
|
|
365
|
+
encodeAppearance({
|
|
366
|
+
version: 1,
|
|
367
|
+
shape,
|
|
368
|
+
color: 'blue',
|
|
369
|
+
eyes,
|
|
370
|
+
eyewear: 'none',
|
|
371
|
+
accessories: [],
|
|
372
|
+
accessoryColors: {},
|
|
373
|
+
constrained: 0,
|
|
374
|
+
depth: 0.5,
|
|
375
|
+
model: null,
|
|
376
|
+
rig: null,
|
|
377
|
+
hereCharacter: null,
|
|
378
|
+
}),
|
|
379
|
+
request.quality,
|
|
380
|
+
'intrinsic-face:' + shape,
|
|
381
|
+
false,
|
|
382
|
+
);
|
|
383
|
+
if (!result.bytes || result.error) continue;
|
|
384
|
+
const reference = authoredAssemblyRecords(result.bytes.slice());
|
|
385
|
+
if (!authoredEyeRecords(reference).length) continue;
|
|
386
|
+
face = shapeFaceLayout(reference, {});
|
|
387
|
+
nativeFaces.set(shape, face);
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
if (!face) fail('native shape face reference missing');
|
|
391
|
+
}
|
|
392
|
+
const sourceGap = Math.hypot(
|
|
393
|
+
face.centers[1][0] - face.centers[0][0],
|
|
394
|
+
face.centers[1][1] - face.centers[0][1],
|
|
395
|
+
);
|
|
396
|
+
const sixLobedFace =
|
|
397
|
+
request.authoredParts?.faceShape === 'six_lobed_flower';
|
|
398
|
+
const radius = Math.max(face.radius, sixLobedFace ? 0.24 : 0);
|
|
399
|
+
const eyeGap = Math.max(sourceGap, radius * 2.1);
|
|
400
|
+
const centerX = (face.centers[0][0] + face.centers[1][0]) / 2;
|
|
401
|
+
return {
|
|
402
|
+
// Small native marks can understate the space their replacement styles
|
|
403
|
+
// need. Enlarge the shape-owned face anchors with their eye size.
|
|
404
|
+
radius,
|
|
405
|
+
centers: face.centers.map(([, y, z], index) => {
|
|
406
|
+
const x = centerX + (index === 0 ? -eyeGap / 2 : eyeGap / 2);
|
|
407
|
+
const front = authoredBodyFront(info, x, y);
|
|
408
|
+
return [x, y, Number.isFinite(front) ? front - radius * 0.3 : z];
|
|
409
|
+
}),
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
if (request.points?.length) {
|
|
413
|
+
const xs = request.points.map((p) => p[0]),
|
|
414
|
+
ys = request.points.map((p) => p[1]);
|
|
415
|
+
const width = Math.max(...xs) - Math.min(...xs),
|
|
416
|
+
height = Math.max(...ys) - Math.min(...ys);
|
|
417
|
+
const x = (Math.max(...xs) + Math.min(...xs)) / 2,
|
|
418
|
+
y = (Math.max(...ys) + Math.min(...ys)) / 2 + height * 0.05;
|
|
419
|
+
const halfGap = width * 0.13,
|
|
420
|
+
radius = Math.min(width * 0.1, height * 0.12, halfGap * 0.8);
|
|
421
|
+
return {
|
|
422
|
+
radius,
|
|
423
|
+
centers: [-1, 1].map((sign) => {
|
|
424
|
+
const at = x + sign * halfGap,
|
|
425
|
+
front = authoredBodyFront(info, at, y);
|
|
426
|
+
return [
|
|
427
|
+
at,
|
|
428
|
+
y,
|
|
429
|
+
Number.isFinite(front)
|
|
430
|
+
? front - radius * 0.3
|
|
431
|
+
: info.body.bounds.max[2] - radius * 0.3,
|
|
432
|
+
];
|
|
433
|
+
}),
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
const groups = eyeCenters(info);
|
|
437
|
+
return {
|
|
438
|
+
centers: groups.map((bounds) => [
|
|
439
|
+
midpoint(bounds, 0),
|
|
440
|
+
midpoint(bounds, 1),
|
|
441
|
+
bounds.min[2],
|
|
442
|
+
]),
|
|
443
|
+
radius:
|
|
444
|
+
groups.reduce(
|
|
445
|
+
(sum, b) =>
|
|
446
|
+
sum + Math.max(b.max[0] - b.min[0], b.max[1] - b.min[1]) / 2,
|
|
447
|
+
0,
|
|
448
|
+
) / 2,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
/** Front surface at a face anchor, sampled from the actual indexed body.
|
|
452
|
+
* A thick source eyeball's rear bound is not a safe anchor for shallow eyes.
|
|
453
|
+
*/
|
|
454
|
+
export function authoredBodyFront(info, x, y) {
|
|
455
|
+
const part = info.body;
|
|
456
|
+
if (!Number.isInteger(part.vertexOffset)) return -Infinity;
|
|
457
|
+
const view = new DataView(
|
|
458
|
+
info.bytes.buffer,
|
|
459
|
+
info.bytes.byteOffset,
|
|
460
|
+
info.bytes.byteLength,
|
|
461
|
+
);
|
|
462
|
+
let at = part.vertexOffset + part.vertexCount * 96;
|
|
463
|
+
const count32 = view.getUint32(at, true);
|
|
464
|
+
const start32 = at + 4;
|
|
465
|
+
at = start32 + count32 * 4;
|
|
466
|
+
const count16 = view.getUint32(at, true),
|
|
467
|
+
start16 = at + 4;
|
|
468
|
+
let front = -Infinity;
|
|
469
|
+
const vertex = (index) => {
|
|
470
|
+
if (index >= part.vertexCount) fail('body surface index');
|
|
471
|
+
return [0, 1, 2].map((k) =>
|
|
472
|
+
view.getFloat32(part.vertexOffset + index * 96 + k * 4, true),
|
|
473
|
+
);
|
|
474
|
+
};
|
|
475
|
+
for (const [start, count, stride] of [
|
|
476
|
+
[start32, count32, 4],
|
|
477
|
+
[start16, count16, 2],
|
|
478
|
+
]) {
|
|
479
|
+
if (count % 3) fail('body triangle index count');
|
|
480
|
+
for (let i = 0; i < count; i += 3) {
|
|
481
|
+
const points = [0, 1, 2].map((k) =>
|
|
482
|
+
vertex(
|
|
483
|
+
stride === 4
|
|
484
|
+
? view.getUint32(start + (i + k) * stride, true)
|
|
485
|
+
: view.getUint16(start + (i + k) * stride, true),
|
|
486
|
+
),
|
|
487
|
+
);
|
|
488
|
+
const [a, b, c] = points;
|
|
489
|
+
const det = (b[1] - c[1]) * (a[0] - c[0]) + (c[0] - b[0]) * (a[1] - c[1]);
|
|
490
|
+
if (Math.abs(det) < 1e-12) continue;
|
|
491
|
+
const u = ((b[1] - c[1]) * (x - c[0]) + (c[0] - b[0]) * (y - c[1])) / det;
|
|
492
|
+
const v = ((c[1] - a[1]) * (x - c[0]) + (a[0] - c[0]) * (y - c[1])) / det;
|
|
493
|
+
const w = 1 - u - v;
|
|
494
|
+
if (Math.min(u, v, w) >= -1e-6)
|
|
495
|
+
front = Math.max(front, u * a[2] + v * b[2] + w * c[2]);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
return front;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/** Fit independently selectable native parts against the target's real body
|
|
502
|
+
* and face records. Donor proportions remain uniform, never stretched flat. */
|
|
503
|
+
export function fitCatalogPartRecords(
|
|
504
|
+
target,
|
|
505
|
+
donor,
|
|
506
|
+
category,
|
|
507
|
+
id,
|
|
508
|
+
{ singleEye = false, faceLayout } = {},
|
|
509
|
+
) {
|
|
510
|
+
const selected =
|
|
511
|
+
category === 'eyes'
|
|
512
|
+
? authoredEyeRecords(donor)
|
|
513
|
+
: donor.records
|
|
514
|
+
.slice(1)
|
|
515
|
+
.filter((p) => hasAuthoredLabel(donor.bytes, p, id));
|
|
516
|
+
if (!selected.length) fail(`missing catalog ${category}:${id}`);
|
|
517
|
+
const bodyWidth = (info) => info.body.bounds.max[0] - info.body.bounds.min[0];
|
|
518
|
+
if (category === 'eyes' || category === 'eyewear') {
|
|
519
|
+
// A Cyclops face has one actual eye, not a collapsed left/right pair.
|
|
520
|
+
// Monocles surround its full eye. Two-lens styles retain both native lenses,
|
|
521
|
+
// fitted to the two halves of that eye rather than the discarded old face.
|
|
522
|
+
const singleBounds = singleEye ? union(authoredEyeRecords(target)) : null;
|
|
523
|
+
const dst = singleBounds
|
|
524
|
+
? id === 'monocle'
|
|
525
|
+
? [singleBounds, singleBounds]
|
|
526
|
+
: [0, 1].map((side) => ({
|
|
527
|
+
min: singleBounds.min.map((value, k) =>
|
|
528
|
+
k === 0 && side === 1 ? midpoint(singleBounds, 0) : value,
|
|
529
|
+
),
|
|
530
|
+
max: singleBounds.max.map((value, k) =>
|
|
531
|
+
k === 0 && side === 0 ? midpoint(singleBounds, 0) : value,
|
|
532
|
+
),
|
|
533
|
+
}))
|
|
534
|
+
: category === 'eyes' && faceLayout
|
|
535
|
+
? faceLayout.centers.map(([x, y, z]) => ({
|
|
536
|
+
min: [x - faceLayout.radius, y - faceLayout.radius, z],
|
|
537
|
+
max: [
|
|
538
|
+
x + faceLayout.radius,
|
|
539
|
+
y + faceLayout.radius,
|
|
540
|
+
z + faceLayout.radius,
|
|
541
|
+
],
|
|
542
|
+
}))
|
|
543
|
+
: eyeCenters(target),
|
|
544
|
+
src = eyeCenters(donor);
|
|
545
|
+
const gap = (a) => Math.abs(midpoint(a[1], 0) - midpoint(a[0], 0));
|
|
546
|
+
// Eye diameter follows the body, not the spacing of the old eye pair.
|
|
547
|
+
// Todd's stalks have over twice the neutral face gap on the same size body.
|
|
548
|
+
const clippo =
|
|
549
|
+
target.body.furCount === 0 &&
|
|
550
|
+
hasAuthoredLabel(target.bytes, target.body, 'metal');
|
|
551
|
+
const scale =
|
|
552
|
+
category === 'eyes'
|
|
553
|
+
? faceLayout
|
|
554
|
+
? faceLayout.radius /
|
|
555
|
+
(src.reduce(
|
|
556
|
+
(sum, b) =>
|
|
557
|
+
sum + Math.max(b.max[0] - b.min[0], b.max[1] - b.min[1]) / 2,
|
|
558
|
+
0,
|
|
559
|
+
) /
|
|
560
|
+
2)
|
|
561
|
+
: clippo
|
|
562
|
+
? 1
|
|
563
|
+
: Math.max(
|
|
564
|
+
0.65,
|
|
565
|
+
Math.min(1.2, bodyWidth(target) / bodyWidth(donor)),
|
|
566
|
+
)
|
|
567
|
+
: Math.max(0.25, Math.min(2, gap(dst) / gap(src)));
|
|
568
|
+
if (category === 'eyes') {
|
|
569
|
+
const fronts = dst.map((bounds) =>
|
|
570
|
+
authoredBodyFront(target, midpoint(bounds, 0), midpoint(bounds, 1)),
|
|
571
|
+
);
|
|
572
|
+
return selected.map((part) => {
|
|
573
|
+
const side = midpoint(part.bounds, 0) < 0 ? 0 : 1;
|
|
574
|
+
const origin = [0, 1, 2].map((k) => midpoint(src[side], k));
|
|
575
|
+
const translate = [0, 1].map((k) => midpoint(dst[side], k) - origin[k]);
|
|
576
|
+
const depth = (src[side].max[2] - src[side].min[2]) * scale;
|
|
577
|
+
const front = fronts[side];
|
|
578
|
+
// Keep some embedding, while the majority of a shallow native eye
|
|
579
|
+
// remains outside the real face. Open Clippo holes return no surface.
|
|
580
|
+
const back = Math.max(dst[side].min[2], front - depth * 0.35);
|
|
581
|
+
translate.push(
|
|
582
|
+
back - (src[side].min[2] - origin[2]) * scale - origin[2],
|
|
583
|
+
);
|
|
584
|
+
return { part, scale, origin, translate };
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
const middle = [0, 1, 2].map(
|
|
588
|
+
(k) => (midpoint(src[0], k) + midpoint(src[1], k)) / 2,
|
|
589
|
+
);
|
|
590
|
+
const destination = [0, 1, 2].map(
|
|
591
|
+
(k) => (midpoint(dst[0], k) + midpoint(dst[1], k)) / 2,
|
|
592
|
+
);
|
|
593
|
+
const sideParts = [0, 1].map((side) =>
|
|
594
|
+
selected.filter(
|
|
595
|
+
(p) =>
|
|
596
|
+
Math.abs(midpoint(p.bounds, 0) - middle[0]) > gap(src) * 0.15 &&
|
|
597
|
+
(midpoint(p.bounds, 0) < middle[0] ? 0 : 1) === side,
|
|
598
|
+
),
|
|
599
|
+
);
|
|
600
|
+
const frontBounds = sideParts.map((parts) => {
|
|
601
|
+
if (!parts.length) return null;
|
|
602
|
+
const z = Math.max(...parts.map((p) => p.bounds.max[2]));
|
|
603
|
+
return union(parts.filter((p) => p.bounds.max[2] > z - 0.05));
|
|
604
|
+
});
|
|
605
|
+
// Sunglasses can be a single centered mesh, including both temples.
|
|
606
|
+
// Its full minimum Z is the rear of the temples, not the front lens plane.
|
|
607
|
+
// Use the same frontmost 0.05-unit band as the separate-lens classifier.
|
|
608
|
+
const separateFronts = frontBounds.filter(Boolean);
|
|
609
|
+
const frontZ = separateFronts.length
|
|
610
|
+
? Math.min(...separateFronts.map((bounds) => bounds.min[2]))
|
|
611
|
+
: Math.max(...selected.map((part) => part.bounds.max[2])) - 0.05;
|
|
612
|
+
// Whole-frame sunglasses cannot move individual lenses independently.
|
|
613
|
+
// Fit their outside span to the final face instead of amplifying a donor's
|
|
614
|
+
// small interocular gap (Todd has widely spaced stalks on a normal body).
|
|
615
|
+
const centralScale = separateFronts.length
|
|
616
|
+
? scale
|
|
617
|
+
: Math.max(
|
|
618
|
+
0.25,
|
|
619
|
+
Math.min(
|
|
620
|
+
2,
|
|
621
|
+
((Math.max(...dst.map((b) => b.max[0])) -
|
|
622
|
+
Math.min(...dst.map((b) => b.min[0]))) *
|
|
623
|
+
1.18) /
|
|
624
|
+
(Math.max(...selected.map((p) => p.bounds.max[0])) -
|
|
625
|
+
Math.min(...selected.map((p) => p.bounds.min[0]))),
|
|
626
|
+
),
|
|
627
|
+
);
|
|
628
|
+
return selected.map((part) => {
|
|
629
|
+
const x = midpoint(part.bounds, 0) - middle[0];
|
|
630
|
+
if (Math.abs(x) <= gap(src) * 0.15)
|
|
631
|
+
return {
|
|
632
|
+
part,
|
|
633
|
+
scale: centralScale,
|
|
634
|
+
origin: middle,
|
|
635
|
+
translate: destination.map((v, k) =>
|
|
636
|
+
k === 2
|
|
637
|
+
? Math.max(...dst.map((bounds) => bounds.max[2])) +
|
|
638
|
+
0.012 -
|
|
639
|
+
((frontZ - middle[2]) * centralScale + middle[2])
|
|
640
|
+
: v - middle[k],
|
|
641
|
+
),
|
|
642
|
+
};
|
|
643
|
+
const side = x < 0 ? 0 : 1,
|
|
644
|
+
front = frontBounds[side];
|
|
645
|
+
const lensScale = Math.min(
|
|
646
|
+
singleEye && id === 'monocle' ? 4 : 2,
|
|
647
|
+
Math.max(
|
|
648
|
+
Math.min(scale, 1.2),
|
|
649
|
+
((dst[side].max[0] - dst[side].min[0]) * 1.08) /
|
|
650
|
+
(front.max[0] - front.min[0]),
|
|
651
|
+
),
|
|
652
|
+
);
|
|
653
|
+
const origin = [0, 1, 2].map((k) => midpoint(src[side], k));
|
|
654
|
+
const translate = [0, 1].map((k) => midpoint(dst[side], k) - origin[k]);
|
|
655
|
+
translate.push(
|
|
656
|
+
dst[side].max[2] +
|
|
657
|
+
0.012 -
|
|
658
|
+
((front.min[2] - origin[2]) * lensScale + origin[2]),
|
|
659
|
+
);
|
|
660
|
+
return { part, scale: lensScale, origin, translate };
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
if (category !== 'accessory') fail('unknown catalog fitting category');
|
|
664
|
+
// Clippo's crown is narrower than its full curved-wire silhouette.
|
|
665
|
+
const clippo =
|
|
666
|
+
target.body.furCount === 0 &&
|
|
667
|
+
hasAuthoredLabel(target.bytes, target.body, 'metal');
|
|
668
|
+
const scale = clippo
|
|
669
|
+
? 0.56
|
|
670
|
+
: Math.max(0.35, Math.min(1.5, bodyWidth(target) / bodyWidth(donor)));
|
|
671
|
+
const origin = [
|
|
672
|
+
midpoint(donor.body.bounds, 0),
|
|
673
|
+
donor.body.bounds.max[1],
|
|
674
|
+
midpoint(donor.body.bounds, 2),
|
|
675
|
+
];
|
|
676
|
+
const crown = [
|
|
677
|
+
clippo ? 0.02 : midpoint(target.body.bounds, 0),
|
|
678
|
+
target.body.bounds.max[1],
|
|
679
|
+
clippo ? 0 : midpoint(target.body.bounds, 2),
|
|
680
|
+
];
|
|
681
|
+
// A bow belongs near the lower face. Crown-mounted accessories retain the
|
|
682
|
+
// source offset above the actual target crown.
|
|
683
|
+
if (id === 'bow') {
|
|
684
|
+
const src = eyeCenters(donor),
|
|
685
|
+
dst = eyeCenters(target);
|
|
686
|
+
for (let k = 0; k < 2; k++) {
|
|
687
|
+
origin[k] = (midpoint(src[0], k) + midpoint(src[1], k)) / 2;
|
|
688
|
+
crown[k] = (midpoint(dst[0], k) + midpoint(dst[1], k)) / 2;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
const translate = crown.map((v, k) => v - origin[k]);
|
|
692
|
+
return selected.map((part) => ({ part, scale, origin, translate }));
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/** Clear the final face after all independently selected eyes and frames fit.
|
|
696
|
+
* Only rigidly raise crown accessories; headphones/bows retain their native
|
|
697
|
+
* side/lower-face attachment. Unedited native presets never invoke this pass.
|
|
698
|
+
*/
|
|
699
|
+
export async function fitAuthoredHeadwear(bytes, request) {
|
|
700
|
+
const id = request.authoredParts?.accessory;
|
|
701
|
+
if (
|
|
702
|
+
![
|
|
703
|
+
'felipe_beret',
|
|
704
|
+
'beret',
|
|
705
|
+
'beanie',
|
|
706
|
+
'hat',
|
|
707
|
+
'crown',
|
|
708
|
+
'orb',
|
|
709
|
+
'three_lobe',
|
|
710
|
+
].includes(id)
|
|
711
|
+
)
|
|
712
|
+
return bytes;
|
|
713
|
+
const info = authoredAssemblyRecords(bytes),
|
|
714
|
+
label = id === 'felipe_beret' ? 'beret' : id;
|
|
715
|
+
const head = info.records
|
|
716
|
+
.slice(1)
|
|
717
|
+
.filter((p) => hasAuthoredLabel(bytes, p, label));
|
|
718
|
+
const frames = request.authoredParts?.eyewear;
|
|
719
|
+
const eyes = new Set(authoredEyeRecords(info));
|
|
720
|
+
const face = info.records
|
|
721
|
+
.slice(1)
|
|
722
|
+
.filter(
|
|
723
|
+
(p) =>
|
|
724
|
+
eyes.has(p) ||
|
|
725
|
+
hasAuthoredLabel(bytes, p, 'brows') ||
|
|
726
|
+
(frames && frames !== 'none' && hasAuthoredLabel(bytes, p, frames)),
|
|
727
|
+
);
|
|
728
|
+
if (!head.length || !face.length) return bytes;
|
|
729
|
+
let raise = 0;
|
|
730
|
+
for (const h of head)
|
|
731
|
+
for (const eye of face) {
|
|
732
|
+
const a = h.bounds,
|
|
733
|
+
b = eye.bounds;
|
|
734
|
+
if (a.max[0] <= b.min[0] || a.min[0] >= b.max[0] || a.max[1] <= b.min[1])
|
|
735
|
+
continue;
|
|
736
|
+
raise = Math.max(raise, b.max[1] + 0.035 - a.min[1]);
|
|
737
|
+
}
|
|
738
|
+
if (raise <= 0) return bytes;
|
|
739
|
+
const selected = new Set(head);
|
|
740
|
+
return assembleAuthoredRecords(
|
|
741
|
+
info,
|
|
742
|
+
await Promise.all(
|
|
743
|
+
info.records.map((p) =>
|
|
744
|
+
selected.has(p)
|
|
745
|
+
? translateAuthoredRecord(bytes, p, [0, raise, 0])
|
|
746
|
+
: {
|
|
747
|
+
bytes: bytes.slice(p.start, p.end),
|
|
748
|
+
name: p.name,
|
|
749
|
+
bounds: p.bounds,
|
|
750
|
+
},
|
|
751
|
+
),
|
|
752
|
+
),
|
|
753
|
+
);
|
|
754
|
+
}
|