aegis-desktop 1.0.13 → 1.0.15
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/SOURCE-OFFER.txt +9 -0
- package/bin/aegis.js +1 -32
- package/lib/account-key.js +1 -56
- package/lib/auto.js +1 -430
- package/lib/avatar/companion.js +1 -698
- package/lib/avatar/cosmetics.js +1 -532
- package/lib/avatar/events.js +1 -344
- package/lib/avatar/holder-turn.js +1 -350
- package/lib/avatar/holders.js +1 -612
- package/lib/avatar/identity.js +1 -641
- package/lib/avatar/level.js +1 -368
- package/lib/avatar/mirror.js +1 -524
- package/lib/avatar/persona.js +1 -388
- package/lib/avatar/profile.js +1 -977
- package/lib/avatar/register.js +1 -307
- package/lib/avatar/store.js +1 -472
- package/lib/avatar/tiers.js +1 -38
- package/lib/avatar/transfer.js +1 -415
- package/lib/avatar/turn.js +1 -216
- package/lib/avatar/voice.js +1 -328
- package/lib/avatar/xp.js +1 -404
- package/lib/deep-link.js +1 -120
- package/lib/fingerprint.js +2 -347
- package/lib/google-connect.js +1 -169
- package/lib/local/agents.js +1 -105
- package/lib/local/autonomous.js +1 -1045
- package/lib/local/claude-pro.js +1 -993
- package/lib/local/context.js +1 -95
- package/lib/local/engine.js +1 -3228
- package/lib/local/entitlement.js +1 -654
- package/lib/local/git-scope.js +1 -228
- package/lib/local/local.js +1 -2135
- package/lib/local/prompt.js +1 -275
- package/lib/local/queue.js +1 -560
- package/lib/local/session-rounds.js +1 -208
- package/lib/local/shell.js +1 -680
- package/lib/local/telemetry.js +1 -470
- package/lib/local/tools.js +1 -1238
- package/lib/local/turn-guard.js +1 -300
- package/lib/local/worktree-lock.js +1 -235
- package/lib/provider-key-mirror.js +1 -252
- package/lib/quick-launcher.js +1 -84
- package/lib/settings.js +1 -890
- package/lib/sync/memory-queue.js +1 -60
- package/lib/sync/persist-gate.js +1 -148
- package/lib/sync/sessions.js +1 -106
- package/lib/window-state.js +1 -66
- package/main.js +3 -4390
- package/package.json +3 -5
- package/preload.js +3 -628
- package/renderer/app.js +1 -6560
- package/renderer/avatar/assemble.js +1 -335
- package/renderer/avatar/avatar.js +1 -380
- package/renderer/avatar/cards.js +1 -141
- package/renderer/avatar/hud.js +1 -206
- package/renderer/avatar/machine.js +1 -213
- package/renderer/avatar/pane.js +1 -439
- package/renderer/avatar/parts.js +1 -437
- package/renderer/budget.js +1 -168
- package/renderer/companion.js +1 -223
- package/renderer/diff.js +1 -359
- package/renderer/diffview.js +1 -336
- package/renderer/fingerprint.js +3 -720
- package/renderer/markdown.js +1 -168
- package/renderer/provider-toolbar.js +1 -134
- package/renderer/providers.js +1 -259
- package/renderer/quick.js +1 -191
- package/renderer/stream-policy.js +1 -306
- package/renderer/transcript-view.js +1 -323
- package/renderer/usage.js +1 -1239
- package/vendor/aegis.js +1 -2054
- package/vendor/attest.js +1 -442
- package/vendor/authorship-key.js +1 -410
- package/vendor/auto-provision.js +1 -134
- package/vendor/brain-catalog.js +1 -268
- package/vendor/byok-catalog.js +1 -230
- package/vendor/credentials.js +1 -494
- package/vendor/device-fingerprint.js +1 -141
- package/vendor/drive-connect.js +1 -85
- package/vendor/drive-upload.js +1 -174
- package/vendor/env-file.js +1 -1273
- package/vendor/fingerprint.js +1 -1064
- package/vendor/foreign-memory.js +1 -669
- package/vendor/google-signin.js +1 -125
- package/vendor/hooks.js +1 -430
- package/vendor/model-discovery.js +1 -330
- package/vendor/platform.js +1 -937
- package/vendor/prefs.js +1 -351
- package/vendor/presence-constants.js +1 -107
- package/vendor/presence.js +1 -438
- package/vendor/record-vault.js +3 -1055
- package/vendor/session-store.js +1 -548
- package/vendor/topup.js +1 -297
- package/vendor/touch.js +1 -458
- package/vendor/update.js +1 -704
- package/vendor/user-models.js +1 -381
|
@@ -1,337 +1,3 @@
|
|
|
1
1
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
2
|
// Copyright (C) 2026 Niklas Borneklint — AEGIS Code (aegiscode) / Aegiscloud.
|
|
3
|
-
|
|
4
|
-
// Conveying a modified copy, or running one as a network service, owes the
|
|
5
|
-
// corresponding source under this licence (§5/§13) — see LICENSING.md.
|
|
6
|
-
'use strict';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* assemble.js — persona JSON in, layered SVG out.
|
|
10
|
-
*
|
|
11
|
-
* A pure string builder. It takes a validated persona record plus one
|
|
12
|
-
* expression id and returns the markup, the layer list it walked, and — the
|
|
13
|
-
* part that matters — a `warnings`/`missing` pair. Three rules:
|
|
14
|
-
*
|
|
15
|
-
* 1. **Never blank, never throw.** An id the manifest does not know (a persona
|
|
16
|
-
* hand-edited, or written by a newer release) renders the default part and
|
|
17
|
-
* is REPORTED. `persona.js` made the same choice at the storage boundary
|
|
18
|
-
* for the same reason: the user's own file must not be able to take the
|
|
19
|
-
* companion's face away.
|
|
20
|
-
*
|
|
21
|
-
* 2. **No interpolation of user text.** Nothing the user typed is placed in
|
|
22
|
-
* the markup, so there is no escaping question and no injection surface:
|
|
23
|
-
* the only persona-supplied value that reaches the SVG is `hairColor`, and
|
|
24
|
-
* it is re-checked against `#rrggbb` here even though `persona.js` already
|
|
25
|
-
* checked it. Geometry comes from the manifest, which is code.
|
|
26
|
-
*
|
|
27
|
-
* 3. **No styles, no scripts, no references.** The output carries no `<style>`,
|
|
28
|
-
* no `on*` attribute and no `href`/`url(...)`: motion lives in avatar.css,
|
|
29
|
-
* keyed off the `data-motion` attribute this file sets. That keeps the
|
|
30
|
-
* renderer's CSP (`script-src 'self'; style-src 'self'`) satisfied by
|
|
31
|
-
* construction, and `desktop/test/avatar-render.test.mjs` asserts all four
|
|
32
|
-
* absences rather than trusting this paragraph.
|
|
33
|
-
*
|
|
34
|
-
* Pure: no DOM, no Electron. It runs identically under `node --test` and in the
|
|
35
|
-
* renderer, which is what makes the whole phase testable.
|
|
36
|
-
*/
|
|
37
|
-
|
|
38
|
-
'use strict';
|
|
39
|
-
|
|
40
|
-
const pkg = typeof window !== 'undefined' && window.AegisAvatarParts
|
|
41
|
-
? window.AegisAvatarParts
|
|
42
|
-
: typeof module !== 'undefined' && module.exports
|
|
43
|
-
? require('./parts.js')
|
|
44
|
-
: null;
|
|
45
|
-
|
|
46
|
-
if (!pkg) throw new Error('avatar/assemble.js: avatar/parts.js must be loaded first');
|
|
47
|
-
|
|
48
|
-
const HEX = /^#[0-9a-f]{6}$/i;
|
|
49
|
-
|
|
50
|
-
/** Lighten (`amount > 0`) or darken (`amount < 0`) a `#rrggbb` colour. */
|
|
51
|
-
function shade(hex, amount) {
|
|
52
|
-
const m = HEX.exec(String(hex || ''));
|
|
53
|
-
if (!m) return hex;
|
|
54
|
-
const n = parseInt(m[0].slice(1), 16);
|
|
55
|
-
const target = amount < 0 ? 0 : 255;
|
|
56
|
-
const k = Math.min(1, Math.abs(amount));
|
|
57
|
-
const mix = (c) => Math.round(c * (1 - k) + target * k);
|
|
58
|
-
const r = mix((n >> 16) & 255);
|
|
59
|
-
const g = mix((n >> 8) & 255);
|
|
60
|
-
const b = mix(n & 255);
|
|
61
|
-
return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')}`;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** Walk a flat list of shape specs, keeping only the ones with geometry. */
|
|
65
|
-
function shapesOf(list) {
|
|
66
|
-
return Array.isArray(list) ? list.filter((s) => s && typeof s === 'object') : [];
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** A path string from an expression is geometry — normalise it to a shape. */
|
|
70
|
-
function asStrokes(value) {
|
|
71
|
-
if (typeof value === 'string' && value.trim()) return [{ kind: 'path', d: value, stroke: true }];
|
|
72
|
-
if (!Array.isArray(value)) return [];
|
|
73
|
-
return value
|
|
74
|
-
.map((v) => (typeof v === 'string' ? { kind: 'path', d: v, stroke: true } : v))
|
|
75
|
-
.filter((s) => s && typeof s === 'object');
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function call(fn, ...args) {
|
|
79
|
-
return typeof fn === 'function' ? fn(...args) : undefined;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Resolve a custom expression from `persona.expressions.custom`.
|
|
84
|
-
*
|
|
85
|
-
* A custom expression is DATA too: `{ mouth, brows, eyes }` where `mouth` is an
|
|
86
|
-
* SVG path, `brows` is a path or a list of paths, and `eyes` names one of the
|
|
87
|
-
* manifest's eye overrides. Anything unrecognised is dropped with a warning, so
|
|
88
|
-
* a half-written custom expression renders the core face rather than nothing.
|
|
89
|
-
*/
|
|
90
|
-
function customExpression(persona, id, warnings) {
|
|
91
|
-
const custom = persona && persona.expressions && persona.expressions.custom;
|
|
92
|
-
const record = custom && typeof custom === 'object' ? custom[id] : null;
|
|
93
|
-
if (!record || typeof record !== 'object') return null;
|
|
94
|
-
const out = { label: id, custom: true };
|
|
95
|
-
if (typeof record.mouth === 'string' && record.mouth.trim()) out.mouth = record.mouth.trim();
|
|
96
|
-
if (record.brows !== undefined) out.brows = asStrokes(record.brows).map((s) => s.d);
|
|
97
|
-
if (record.eyes !== undefined) {
|
|
98
|
-
if (typeof record.eyes === 'string' && pkg.EYE_OVERRIDES[record.eyes]) out.eyes = record.eyes;
|
|
99
|
-
else warnings.push(`custom expression ${id}: unknown eye override ${JSON.stringify(record.eyes)} — ignored`);
|
|
100
|
-
}
|
|
101
|
-
if (out.mouth === undefined && out.brows === undefined && out.eyes === undefined) {
|
|
102
|
-
warnings.push(`custom expression ${id} carries no drawable geometry — ignored`);
|
|
103
|
-
return null;
|
|
104
|
-
}
|
|
105
|
-
return out;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Assemble the avatar.
|
|
110
|
-
*
|
|
111
|
-
* @param {object} persona a persona record (validated or raw — both survive)
|
|
112
|
-
* @param {object} [opts]
|
|
113
|
-
* @param {string} [opts.expression] an `events.js` expression id
|
|
114
|
-
* @param {boolean} [opts.reducedMotion] the OS preference; forces `minimal`
|
|
115
|
-
* @param {string} [opts.motion] explicit override, mostly for tests
|
|
116
|
-
* @returns {{ svg: string, layers: object[], warnings: string[], missing: string[],
|
|
117
|
-
* expression: string, palette: string, frame: string, motion: string }}
|
|
118
|
-
*/
|
|
119
|
-
function assemble(persona, opts = {}) {
|
|
120
|
-
const warnings = [];
|
|
121
|
-
const missing = [];
|
|
122
|
-
const p = persona && typeof persona === 'object' ? persona : pkg.defaultPersona();
|
|
123
|
-
const presentation = p.presentation && typeof p.presentation === 'object' ? p.presentation : {};
|
|
124
|
-
const appearance = p.appearance && typeof p.appearance === 'object' ? p.appearance : {};
|
|
125
|
-
|
|
126
|
-
// ---- palette / skin / hair colour --------------------------------------
|
|
127
|
-
const paletteId = pkg.PALETTES[presentation.palette] ? presentation.palette : 'amber';
|
|
128
|
-
if (presentation.palette !== undefined && paletteId !== presentation.palette) {
|
|
129
|
-
warnings.push(`unknown palette ${JSON.stringify(presentation.palette)} — using ${paletteId}`);
|
|
130
|
-
}
|
|
131
|
-
const palette = pkg.PALETTES[paletteId];
|
|
132
|
-
const skin = Number.isInteger(presentation.skin) && presentation.skin >= 0 && presentation.skin < pkg.SKIN_TONES.length
|
|
133
|
-
? presentation.skin
|
|
134
|
-
: 2;
|
|
135
|
-
const skinTone = pkg.SKIN_TONES[skin];
|
|
136
|
-
const hairColor = HEX.test(String(presentation.hairColor || '')) ? String(presentation.hairColor) : '#3b2a20';
|
|
137
|
-
|
|
138
|
-
function colorFor(role) {
|
|
139
|
-
switch (role) {
|
|
140
|
-
case 'skin': return skinTone;
|
|
141
|
-
case 'skinShade': return shade(skinTone, -0.18);
|
|
142
|
-
case 'hair': return hairColor;
|
|
143
|
-
case 'hairShade': return shade(hairColor, -0.25);
|
|
144
|
-
case 'cloth': return palette.cloth;
|
|
145
|
-
case 'clothShade': return palette.clothShade;
|
|
146
|
-
case 'accent': return palette.accent;
|
|
147
|
-
case 'metal': return palette.metal;
|
|
148
|
-
case 'bg': return palette.bg;
|
|
149
|
-
case 'ink': return palette.ink;
|
|
150
|
-
case 'sclera': return '#fdfbf7';
|
|
151
|
-
case 'highlight': return '#ffffff';
|
|
152
|
-
default: return palette.ink;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// ---- parts -------------------------------------------------------------
|
|
157
|
-
function part(table, kind, id, fallback) {
|
|
158
|
-
const got = table[id];
|
|
159
|
-
if (got) return got;
|
|
160
|
-
if (id !== undefined && id !== null && id !== '') {
|
|
161
|
-
missing.push(`${kind}:${id}`);
|
|
162
|
-
warnings.push(`unknown ${kind} ${JSON.stringify(id)} — using ${fallback}`);
|
|
163
|
-
}
|
|
164
|
-
return table[fallback];
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const frameId = pkg.FRAMES[presentation.frame] ? presentation.frame : 'A';
|
|
168
|
-
const frame = pkg.FRAMES[frameId];
|
|
169
|
-
const hair = part(pkg.HAIR, 'hair', presentation.hair, 'bob-2');
|
|
170
|
-
const eyes = part(pkg.EYES, 'eyes', presentation.eyes, 'soft');
|
|
171
|
-
|
|
172
|
-
const outfitIds = [];
|
|
173
|
-
for (const item of Array.isArray(presentation.outfit) ? presentation.outfit : []) {
|
|
174
|
-
if (pkg.OUTFITS[item]) outfitIds.push(item);
|
|
175
|
-
else missing.push(`outfit:${item}`);
|
|
176
|
-
}
|
|
177
|
-
if (!outfitIds.length && Array.isArray(presentation.outfit) && presentation.outfit.length) {
|
|
178
|
-
warnings.push('no known outfit layer — wearing the plain tee');
|
|
179
|
-
}
|
|
180
|
-
const outfits = outfitIds.slice(0, 3).map((id) => pkg.OUTFITS[id]);
|
|
181
|
-
|
|
182
|
-
// ---- expression --------------------------------------------------------
|
|
183
|
-
const requested = typeof opts.expression === 'string' && opts.expression ? opts.expression : 'neutral';
|
|
184
|
-
let expression = pkg.EXPRESSIONS[requested];
|
|
185
|
-
let expressionId = requested;
|
|
186
|
-
if (!expression) {
|
|
187
|
-
const custom = customExpression(p, requested, warnings);
|
|
188
|
-
if (custom) {
|
|
189
|
-
expression = custom;
|
|
190
|
-
} else {
|
|
191
|
-
if (requested !== 'neutral') {
|
|
192
|
-
missing.push(`expression:${requested}`);
|
|
193
|
-
warnings.push(`unknown expression ${JSON.stringify(requested)} — using neutral`);
|
|
194
|
-
}
|
|
195
|
-
expression = pkg.EXPRESSIONS.neutral;
|
|
196
|
-
expressionId = 'neutral';
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const eyeOverride = expression.eyes && pkg.EYE_OVERRIDES[expression.eyes] ? pkg.EYE_OVERRIDES[expression.eyes] : null;
|
|
201
|
-
const browShapes = Array.isArray(expression.brows)
|
|
202
|
-
? asStrokes(expression.brows)
|
|
203
|
-
: asStrokes(expression.brows);
|
|
204
|
-
const mouthShapes = asStrokes(expression.mouth);
|
|
205
|
-
|
|
206
|
-
const sheet = {
|
|
207
|
-
shadow: [{ kind: 'ellipse', cx: 100, cy: 254, rx: 62, ry: 10, role: 'clothShade', opacity: 0.5 }],
|
|
208
|
-
hairBack: shapesOf(hair.back),
|
|
209
|
-
body: [
|
|
210
|
-
{ kind: 'path', d: pkg.BODY.neck, role: 'skinShade' },
|
|
211
|
-
{ kind: 'path', d: pkg.BODY.shirt, role: 'cloth' },
|
|
212
|
-
{ kind: 'path', d: pkg.BODY.sleeveL, role: 'cloth' },
|
|
213
|
-
{ kind: 'path', d: pkg.BODY.sleeveR, role: 'cloth' },
|
|
214
|
-
],
|
|
215
|
-
outfitUnder: [],
|
|
216
|
-
head: [
|
|
217
|
-
{ kind: 'path', d: frame.head, role: 'skin' },
|
|
218
|
-
...frame.ear.map((e) => ({ kind: 'circle', cx: e.cx, cy: e.cy, r: e.r, role: 'skin' })),
|
|
219
|
-
],
|
|
220
|
-
face: [
|
|
221
|
-
...(eyeOverride
|
|
222
|
-
? eyeOverride.map((s) => ({ ...s, stroke: true }))
|
|
223
|
-
: [...eyes.left, ...eyes.right]),
|
|
224
|
-
...browShapes,
|
|
225
|
-
...shapesOf(pkg.FACE.nose),
|
|
226
|
-
...mouthShapes,
|
|
227
|
-
],
|
|
228
|
-
hairFront: [...shapesOf(hair.front), ...shapesOf(hair.detail)],
|
|
229
|
-
outfitOver: [],
|
|
230
|
-
accessory: [],
|
|
231
|
-
effect: [],
|
|
232
|
-
};
|
|
233
|
-
|
|
234
|
-
// Outfit layers draw in the order the persona listed them: the last one is
|
|
235
|
-
// the outermost. Items flagged `accessory` (headphones, glasses) go on top of
|
|
236
|
-
// the hair instead of into the clothes pile.
|
|
237
|
-
const under = sheet.outfitUnder;
|
|
238
|
-
for (const item of outfits) {
|
|
239
|
-
under.push(...shapesOf(item.under).map((s) => ({ ...s, defaultRole: 'cloth' })));
|
|
240
|
-
const overShapes = shapesOf(item.over).map((s) => ({ ...s, defaultRole: 'cloth' }));
|
|
241
|
-
if (item.accessory) sheet.accessory.push(...overShapes);
|
|
242
|
-
else sheet.outfitOver.push(...overShapes);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// A celebrating face gets a few sparks — motion-gated in CSS, and the only
|
|
246
|
-
// thing on the `effect` layer.
|
|
247
|
-
if (expressionId === 'happy' || expressionId === 'playful' || expressionId === 'delighted') {
|
|
248
|
-
sheet.effect.push(
|
|
249
|
-
{ kind: 'path', d: 'M28 66 l4 10 l10 4 l-10 4 l-4 10 l-4 -10 l-10 -4 l10 -4 Z', role: 'accent', opacity: 0.9 },
|
|
250
|
-
{ kind: 'path', d: 'M172 88 l3 8 l8 3 l-8 3 l-3 8 l-3 -8 l-8 -3 l8 -3 Z', role: 'accent', opacity: 0.7 }
|
|
251
|
-
);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// ---- markup ------------------------------------------------------------
|
|
255
|
-
/** Per-layer default fill role, so a manifest entry rarely repeats itself. */
|
|
256
|
-
const layerDefaultRole = {
|
|
257
|
-
shadow: 'clothShade',
|
|
258
|
-
hairBack: 'hair',
|
|
259
|
-
body: 'cloth',
|
|
260
|
-
outfitUnder: 'cloth',
|
|
261
|
-
head: 'skin',
|
|
262
|
-
face: 'ink',
|
|
263
|
-
hairFront: 'hair',
|
|
264
|
-
outfitOver: 'cloth',
|
|
265
|
-
accessory: 'metal',
|
|
266
|
-
effect: 'accent',
|
|
267
|
-
};
|
|
268
|
-
|
|
269
|
-
function renderShape(shape, layer) {
|
|
270
|
-
const role = shape.role || shape.defaultRole || layerDefaultRole[layer] || 'ink';
|
|
271
|
-
const fill = colorFor(role);
|
|
272
|
-
const common = [];
|
|
273
|
-
if (shape.stroke) {
|
|
274
|
-
common.push('fill="none"', `stroke="${fill}"`, 'stroke-width="3.5"', 'stroke-linecap="round"', 'stroke-linejoin="round"');
|
|
275
|
-
} else {
|
|
276
|
-
common.push(`fill="${fill}"`);
|
|
277
|
-
}
|
|
278
|
-
if (typeof shape.opacity === 'number') common.push(`opacity="${shape.opacity}"`);
|
|
279
|
-
|
|
280
|
-
switch (shape.kind) {
|
|
281
|
-
case 'circle':
|
|
282
|
-
return `<circle cx="${shape.cx}" cy="${shape.cy}" r="${shape.r}" ${common.join(' ')}/>`;
|
|
283
|
-
case 'ellipse':
|
|
284
|
-
return `<ellipse cx="${shape.cx}" cy="${shape.cy}" rx="${shape.rx}" ry="${shape.ry}" ${common.join(' ')}/>`;
|
|
285
|
-
case 'rect': {
|
|
286
|
-
const rx = shape.rx === undefined ? 0 : shape.rx;
|
|
287
|
-
return `<rect x="${shape.x}" y="${shape.y}" width="${shape.width}" height="${shape.height}" rx="${rx}" ${common.join(' ')}/>`;
|
|
288
|
-
}
|
|
289
|
-
case 'path':
|
|
290
|
-
default:
|
|
291
|
-
return `<path d="${shape.d}" ${common.join(' ')}/>`;
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
const motion = opts.motion
|
|
296
|
-
? String(opts.motion)
|
|
297
|
-
: pkg.motionFor(p, Boolean(opts.reducedMotion));
|
|
298
|
-
const size = ['small', 'medium', 'large'].includes(appearance.size) ? appearance.size : 'medium';
|
|
299
|
-
|
|
300
|
-
const layers = [];
|
|
301
|
-
const groups = [];
|
|
302
|
-
for (const name of pkg.LAYERS) {
|
|
303
|
-
const shapes = sheet[name] || [];
|
|
304
|
-
layers.push({ name, shapes: shapes.length });
|
|
305
|
-
if (!shapes.length) continue;
|
|
306
|
-
const body = shapes.map((s) => renderShape(s, name)).join('');
|
|
307
|
-
groups.push(`<g class="avatar-layer avatar-${name}" data-layer="${name}">${body}</g>`);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
const svg =
|
|
311
|
-
`<svg class="avatar-svg" viewBox="0 0 200 268" xmlns="http://www.w3.org/2000/svg" ` +
|
|
312
|
-
`aria-hidden="true" focusable="false" role="presentation" ` +
|
|
313
|
-
`data-motion="${motion}" data-size="${size}" data-frame="${frameId}" ` +
|
|
314
|
-
`data-palette="${paletteId}" data-expression="${expressionId}">` +
|
|
315
|
-
`<rect class="avatar-bg" x="0" y="0" width="200" height="268" rx="24" fill="${colorFor('bg')}"/>` +
|
|
316
|
-
groups.join('') +
|
|
317
|
-
'</svg>';
|
|
318
|
-
|
|
319
|
-
return {
|
|
320
|
-
svg,
|
|
321
|
-
layers,
|
|
322
|
-
warnings,
|
|
323
|
-
missing,
|
|
324
|
-
expression: expressionId,
|
|
325
|
-
expressionLabel: expression.label || expressionId,
|
|
326
|
-
palette: paletteId,
|
|
327
|
-
frame: frameId,
|
|
328
|
-
hair: hair === pkg.HAIR[presentation.hair] ? presentation.hair : 'bob-2',
|
|
329
|
-
motion,
|
|
330
|
-
size,
|
|
331
|
-
};
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
const API = Object.freeze({ assemble, shade });
|
|
335
|
-
|
|
336
|
-
if (typeof module !== 'undefined' && module.exports) module.exports = API;
|
|
337
|
-
else if (typeof globalThis !== 'undefined') globalThis.AegisAvatarAssemble = API;
|
|
3
|
+
"use strict";const pkg="undefined"!=typeof window&&window.AegisAvatarParts?window.AegisAvatarParts:"undefined"!=typeof module&&module.exports?require("./parts.js"):null;if(!pkg)throw new Error("avatar/assemble.js: avatar/parts.js must be loaded first");const HEX=/^#[0-9a-f]{6}$/i;function shade(e,t){const r=HEX.exec(String(e||""));if(!r)return e;const s=parseInt(r[0].slice(1),16),o=t<0?0:255,a=Math.min(1,Math.abs(t)),n=e=>Math.round(e*(1-a)+o*a);return`#${(n(s>>16&255)<<16|n(s>>8&255)<<8|n(255&s)).toString(16).padStart(6,"0")}`}function shapesOf(e){return Array.isArray(e)?e.filter((e=>e&&"object"==typeof e)):[]}function asStrokes(e){return"string"==typeof e&&e.trim()?[{kind:"path",d:e,stroke:!0}]:Array.isArray(e)?e.map((e=>"string"==typeof e?{kind:"path",d:e,stroke:!0}:e)).filter((e=>e&&"object"==typeof e)):[]}function call(e,...t){return"function"==typeof e?e(...t):void 0}function customExpression(e,t,r){const s=e&&e.expressions&&e.expressions.custom,o=s&&"object"==typeof s?s[t]:null;if(!o||"object"!=typeof o)return null;const a={label:t,custom:!0};return"string"==typeof o.mouth&&o.mouth.trim()&&(a.mouth=o.mouth.trim()),void 0!==o.brows&&(a.brows=asStrokes(o.brows).map((e=>e.d))),void 0!==o.eyes&&("string"==typeof o.eyes&&pkg.EYE_OVERRIDES[o.eyes]?a.eyes=o.eyes:r.push(`custom expression ${t}: unknown eye override ${JSON.stringify(o.eyes)} — ignored`)),void 0===a.mouth&&void 0===a.brows&&void 0===a.eyes?(r.push(`custom expression ${t} carries no drawable geometry — ignored`),null):a}function assemble(e,t={}){const r=[],s=[],o=e&&"object"==typeof e?e:pkg.defaultPersona(),a=o.presentation&&"object"==typeof o.presentation?o.presentation:{},n=o.appearance&&"object"==typeof o.appearance?o.appearance:{},i=pkg.PALETTES[a.palette]?a.palette:"amber";void 0!==a.palette&&i!==a.palette&&r.push(`unknown palette ${JSON.stringify(a.palette)} — using ${i}`);const l=pkg.PALETTES[i],c=Number.isInteger(a.skin)&&a.skin>=0&&a.skin<pkg.SKIN_TONES.length?a.skin:2,p=pkg.SKIN_TONES[c],u=HEX.test(String(a.hairColor||""))?String(a.hairColor):"#3b2a20";function h(e){switch(e){case"skin":return p;case"skinShade":return shade(p,-.18);case"hair":return u;case"hairShade":return shade(u,-.25);case"cloth":return l.cloth;case"clothShade":return l.clothShade;case"accent":return l.accent;case"metal":return l.metal;case"bg":return l.bg;case"ink":default:return l.ink;case"sclera":return"#fdfbf7";case"highlight":return"#ffffff"}}function d(e,t,o,a){return e[o]||(null!=o&&""!==o&&(s.push(`${t}:${o}`),r.push(`unknown ${t} ${JSON.stringify(o)} — using ${a}`)),e[a])}const f=pkg.FRAMES[a.frame]?a.frame:"A",g=pkg.FRAMES[f],y=d(pkg.HAIR,"hair",a.hair,"bob-2"),k=d(pkg.EYES,"eyes",a.eyes,"soft"),m=[];for(const e of Array.isArray(a.outfit)?a.outfit:[])pkg.OUTFITS[e]?m.push(e):s.push(`outfit:${e}`);!m.length&&Array.isArray(a.outfit)&&a.outfit.length&&r.push("no known outfit layer — wearing the plain tee");const b=m.slice(0,3).map((e=>pkg.OUTFITS[e])),$="string"==typeof t.expression&&t.expression?t.expression:"neutral";let S=pkg.EXPRESSIONS[$],x=$;if(!S){const e=customExpression(o,$,r);e?S=e:("neutral"!==$&&(s.push(`expression:${$}`),r.push(`unknown expression ${JSON.stringify($)} — using neutral`)),S=pkg.EXPRESSIONS.neutral,x="neutral")}const w=S.eyes&&pkg.EYE_OVERRIDES[S.eyes]?pkg.EYE_OVERRIDES[S.eyes]:null,E=(Array.isArray(S.brows),asStrokes(S.brows)),v=asStrokes(S.mouth),A={shadow:[{kind:"ellipse",cx:100,cy:254,rx:62,ry:10,role:"clothShade",opacity:.5}],hairBack:shapesOf(y.back),body:[{kind:"path",d:pkg.BODY.neck,role:"skinShade"},{kind:"path",d:pkg.BODY.shirt,role:"cloth"},{kind:"path",d:pkg.BODY.sleeveL,role:"cloth"},{kind:"path",d:pkg.BODY.sleeveR,role:"cloth"}],outfitUnder:[],head:[{kind:"path",d:g.head,role:"skin"},...g.ear.map((e=>({kind:"circle",cx:e.cx,cy:e.cy,r:e.r,role:"skin"})))],face:[...w?w.map((e=>({...e,stroke:!0}))):[...k.left,...k.right],...E,...shapesOf(pkg.FACE.nose),...v],hairFront:[...shapesOf(y.front),...shapesOf(y.detail)],outfitOver:[],accessory:[],effect:[]},O=A.outfitUnder;for(const e of b){O.push(...shapesOf(e.under).map((e=>({...e,defaultRole:"cloth"}))));const t=shapesOf(e.over).map((e=>({...e,defaultRole:"cloth"})));e.accessory?A.accessory.push(...t):A.outfitOver.push(...t)}"happy"!==x&&"playful"!==x&&"delighted"!==x||A.effect.push({kind:"path",d:"M28 66 l4 10 l10 4 l-10 4 l-4 10 l-4 -10 l-10 -4 l10 -4 Z",role:"accent",opacity:.9},{kind:"path",d:"M172 88 l3 8 l8 3 l-8 3 l-3 8 l-3 -8 l-8 -3 l8 -3 Z",role:"accent",opacity:.7});const j={shadow:"clothShade",hairBack:"hair",body:"cloth",outfitUnder:"cloth",head:"skin",face:"ink",hairFront:"hair",outfitOver:"cloth",accessory:"metal",effect:"accent"};function R(e,t){const r=h(e.role||e.defaultRole||j[t]||"ink"),s=[];switch(e.stroke?s.push('fill="none"',`stroke="${r}"`,'stroke-width="3.5"','stroke-linecap="round"','stroke-linejoin="round"'):s.push(`fill="${r}"`),"number"==typeof e.opacity&&s.push(`opacity="${e.opacity}"`),e.kind){case"circle":return`<circle cx="${e.cx}" cy="${e.cy}" r="${e.r}" ${s.join(" ")}/>`;case"ellipse":return`<ellipse cx="${e.cx}" cy="${e.cy}" rx="${e.rx}" ry="${e.ry}" ${s.join(" ")}/>`;case"rect":{const t=void 0===e.rx?0:e.rx;return`<rect x="${e.x}" y="${e.y}" width="${e.width}" height="${e.height}" rx="${t}" ${s.join(" ")}/>`}default:return`<path d="${e.d}" ${s.join(" ")}/>`}}const I=t.motion?String(t.motion):pkg.motionFor(o,Boolean(t.reducedMotion)),T=["small","medium","large"].includes(n.size)?n.size:"medium",N=[],P=[];for(const e of pkg.LAYERS){const t=A[e]||[];if(N.push({name:e,shapes:t.length}),!t.length)continue;const r=t.map((t=>R(t,e))).join("");P.push(`<g class="avatar-layer avatar-${e}" data-layer="${e}">${r}</g>`)}return{svg:`<svg class="avatar-svg" viewBox="0 0 200 268" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" role="presentation" data-motion="${I}" data-size="${T}" data-frame="${f}" data-palette="${i}" data-expression="${x}"><rect class="avatar-bg" x="0" y="0" width="200" height="268" rx="24" fill="${h("bg")}"/>`+P.join("")+"</svg>",layers:N,warnings:r,missing:s,expression:x,expressionLabel:S.label||x,palette:i,frame:f,hair:y===pkg.HAIR[a.hair]?a.hair:"bob-2",motion:I,size:T}}const API=Object.freeze({assemble:assemble,shade:shade});"undefined"!=typeof module&&module.exports?module.exports=API:"undefined"!=typeof globalThis&&(globalThis.AegisAvatarAssemble=API);
|