castle-web-cli 0.4.123 → 0.4.124

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 (50) hide show
  1. package/dist/castleJson.d.ts +2 -0
  2. package/dist/castleJson.js +39 -0
  3. package/dist/headlessCover.d.ts +13 -0
  4. package/dist/headlessCover.js +99 -0
  5. package/dist/ide.js +12 -42
  6. package/dist/imports.js +42 -2
  7. package/dist/index.js +1 -9
  8. package/dist/init.d.ts +1 -0
  9. package/dist/init.js +15 -1
  10. package/dist/preview.d.ts +0 -1
  11. package/dist/preview.js +0 -58
  12. package/dist/save-deck.js +52 -1
  13. package/dist/serve.js +16 -0
  14. package/dist/shell/assets/Basteleur-Bold-CK8LF7Pt.woff +0 -0
  15. package/dist/shell/assets/Basteleur-Bold-DKFKedNb.woff2 +0 -0
  16. package/dist/shell/assets/index-BfOPkSej.css +1 -0
  17. package/dist/shell/assets/index-u0nYFqbF.js +434 -0
  18. package/dist/shell/index.html +2 -2
  19. package/kits/physics-2d/CLAUDE.md +2 -2
  20. package/kits/physics-2d/castle.json +10 -2
  21. package/kits/physics-2d/docs/pxart-format.md +33 -26
  22. package/kits/physics-2d/editors/PxArtEditor.jsx +120 -49
  23. package/kits/physics-2d/editors/SingleEditor.jsx +6 -3
  24. package/kits/physics-2d/editors/StyleEditor.jsx +95 -0
  25. package/kits/physics-2d/editors/pathOverlay.js +1 -1
  26. package/kits/physics-2d/editors/pathTools.js +9 -1
  27. package/kits/physics-2d/editors/pixelGeometry.js +14 -13
  28. package/kits/physics-2d/editors/pixelInspector.jsx +202 -53
  29. package/kits/physics-2d/editors/pxArtEditorModel.js +8 -63
  30. package/kits/physics-2d/editors/pxArtTools.js +3 -43
  31. package/kits/physics-2d/editors/styleEditor.module.css +105 -0
  32. package/kits/physics-2d/editors/styleTheme.js +16 -0
  33. package/kits/physics-2d/engine/files.js +2 -1
  34. package/kits/physics-2d/engine/liveReload.js +4 -3
  35. package/kits/physics-2d/engine/palettes.js +636 -0
  36. package/kits/physics-2d/engine/pxart.js +6 -6
  37. package/kits/physics-2d/engine/svgImport.js +1056 -0
  38. package/kits/physics-2d/engine/ui.jsx +2 -0
  39. package/kits/physics-2d/engine/ui.module.css +54 -9
  40. package/kits/physics-2d/package-lock.json +1 -1
  41. package/kits/physics-2d/package.json +1 -0
  42. package/kits/physics-2d/scripts/deckTheme.mjs +25 -0
  43. package/kits/physics-2d/scripts/draw.mjs +5 -3
  44. package/kits/physics-2d/scripts/import-svg.mjs +16 -1069
  45. package/kits/physics-2d/scripts/palette.mjs +10 -0
  46. package/kits/physics-2d/scripts/svg-emission-guide.md +5 -3
  47. package/kits/physics-2d/theme.style +3 -0
  48. package/package.json +1 -1
  49. package/dist/shell/assets/index-CARLHafh.css +0 -1
  50. package/dist/shell/assets/index-DWgt4KzC.js +0 -434
@@ -0,0 +1,636 @@
1
+ // Deck palette resolution + the agent generation subset.
2
+ //
3
+ // A deck may declare a palette in `theme.style`. The editor uses that palette
4
+ // as a picker (the sprite's own palette stays truth). Generation (`draw` /
5
+ // `import-svg`) quantizes to a derived subset so the model stays coherent.
6
+ //
7
+ // Derivation (validated by castle-px-eval): the default Endesga-64 deck keeps
8
+ // the hand-curated AGENT_PALETTE_16; a custom palette larger than 16 is cut
9
+ // by greedy farthest-point in Oklab; 16 or fewer colors are used as-is.
10
+
11
+ import { AGENT_PALETTE_16, EDG64, KEY_ALPHABET, normalizeHex } from './pxart.js';
12
+
13
+ export const THEME_STYLE_PATH = 'theme.style';
14
+ export const OFFICIAL_ENDESGA_64 = 'endesga-64';
15
+ export const PALETTE_KEY_CAP = 64;
16
+
17
+ // Endesga-64 stays first (the default). AAP-64-Castle is the classic Castle
18
+ // editor default (castle-client Palettes.js `default`, not in the GraphQL
19
+ // list). The rest are Castle's published palettes (ghost-server palettes.ts),
20
+ // minus the three already registered from the pixel-art eval (Mushroom /
21
+ // SOBEACHY8 / Grape Soda — those keep the eval hex lists).
22
+ export const OFFICIAL_PALETTES = {
23
+ [OFFICIAL_ENDESGA_64]: {
24
+ id: OFFICIAL_ENDESGA_64,
25
+ name: 'Endesga 64',
26
+ colors: EDG64,
27
+ },
28
+ 'aap-64-castle': {
29
+ id: 'aap-64-castle',
30
+ name: 'AAP-64-Castle',
31
+ colors: [
32
+ '#3b1725',
33
+ '#73172d',
34
+ '#b4202a',
35
+ '#df3e23',
36
+ '#fa6a0a',
37
+ '#f9a31b',
38
+ '#ffd541',
39
+ '#fffc40',
40
+ '#fffd98',
41
+ '#d6f264',
42
+ '#9cdb43',
43
+ '#59c135',
44
+ '#14a02e',
45
+ '#1a7a3e',
46
+ '#24523b',
47
+ '#122020',
48
+ '#143464',
49
+ '#285cc4',
50
+ '#249fde',
51
+ '#20d6c7',
52
+ '#a6fcdb',
53
+ '#fff9db',
54
+ '#fef3c0',
55
+ '#fad6b8',
56
+ '#f5a097',
57
+ '#f2838b',
58
+ '#e86a73',
59
+ '#bc4a9b',
60
+ '#793a80',
61
+ '#403353',
62
+ '#242234',
63
+ '#322b28',
64
+ '#71413b',
65
+ '#bb7547',
66
+ '#dba463',
67
+ '#f4d29c',
68
+ '#edeff2',
69
+ '#dae0ea',
70
+ '#b3b9d1',
71
+ '#8b93af',
72
+ '#6d758d',
73
+ '#4a5462',
74
+ '#333941',
75
+ '#422433',
76
+ '#5b3138',
77
+ '#8e5252',
78
+ '#ba756a',
79
+ '#e9b5a3',
80
+ '#e3e6ff',
81
+ '#b9bffb',
82
+ '#849be4',
83
+ '#588dbe',
84
+ '#477d85',
85
+ '#23674e',
86
+ '#328464',
87
+ '#5daf8d',
88
+ '#92dcba',
89
+ '#cdf7e2',
90
+ '#e4d2aa',
91
+ '#c7b08b',
92
+ '#a08662',
93
+ '#796755',
94
+ '#5a4e44',
95
+ '#423934',
96
+ ],
97
+ },
98
+ mushroom: {
99
+ id: 'mushroom',
100
+ name: 'Mushroom',
101
+ colors: [
102
+ '#2e222f',
103
+ '#45293f',
104
+ '#7a3045',
105
+ '#993d41',
106
+ '#cd683d',
107
+ '#fbb954',
108
+ '#f2ec8b',
109
+ '#b0a987',
110
+ '#997f73',
111
+ '#665964',
112
+ '#443846',
113
+ '#576069',
114
+ '#788a87',
115
+ '#a9b2a2',
116
+ ],
117
+ },
118
+ sobeachy8: {
119
+ id: 'sobeachy8',
120
+ name: 'SOBEACHY8',
121
+ colors: [
122
+ '#e55388',
123
+ '#e57d88',
124
+ '#e59f88',
125
+ '#e5d988',
126
+ '#e3d5cc',
127
+ '#bad5cc',
128
+ '#6dd5cc',
129
+ '#5ac5cc',
130
+ ],
131
+ },
132
+ 'grape-soda': {
133
+ id: 'grape-soda',
134
+ name: 'Grape Soda',
135
+ colors: [
136
+ '#aa81ff',
137
+ '#b837ea',
138
+ '#aa11bb',
139
+ '#90006c',
140
+ '#56002a',
141
+ '#25000d',
142
+ '#000000',
143
+ '#15021e',
144
+ '#47054f',
145
+ '#c9298c',
146
+ '#ee699e',
147
+ '#ffbac0',
148
+ '#fff6f6',
149
+ ],
150
+ },
151
+ 'chocomilk-8': {
152
+ id: 'chocomilk-8',
153
+ name: 'Chocomilk-8',
154
+ colors: [
155
+ '#d6f5e4',
156
+ '#f7ffc5',
157
+ '#b4c788',
158
+ '#8a8969',
159
+ '#8b6a44',
160
+ '#62554c',
161
+ '#463c3c',
162
+ '#312629',
163
+ ],
164
+ },
165
+ 'deep-maze': {
166
+ id: 'deep-maze',
167
+ name: 'Deep Maze',
168
+ colors: ['#f2ff66', '#9af089', '#38d88e', '#00be91', '#009a98', '#085562', '#001d2a'],
169
+ },
170
+ 'fantasy-24': {
171
+ id: 'fantasy-24',
172
+ name: 'Fantasy 24',
173
+ colors: [
174
+ '#3c9f9c',
175
+ '#276468',
176
+ '#183f39',
177
+ '#1f240a',
178
+ '#39571c',
179
+ '#a58c27',
180
+ '#efac28',
181
+ '#efd8a1',
182
+ '#300f0a',
183
+ '#550f0a',
184
+ '#9b1a0a',
185
+ '#ef3a0c',
186
+ '#ef692f',
187
+ '#efb775',
188
+ '#9c6549',
189
+ '#6f3826',
190
+ '#36170c',
191
+ '#45230d',
192
+ '#724113',
193
+ '#ab5c1c',
194
+ '#2a1d0d',
195
+ '#392a1c',
196
+ '#684c3c',
197
+ '#927e6a',
198
+ ],
199
+ },
200
+ chasm: {
201
+ id: 'chasm',
202
+ name: 'Chasm',
203
+ colors: [
204
+ '#85daeb',
205
+ '#5fc9e7',
206
+ '#5fa1e7',
207
+ '#5f6ee7',
208
+ '#4c60aa',
209
+ '#444774',
210
+ '#32313b',
211
+ '#463c5e',
212
+ '#5d4776',
213
+ '#855395',
214
+ '#ab58a8',
215
+ '#ca60ae',
216
+ '#f3a787',
217
+ '#f5daa7',
218
+ '#8dd894',
219
+ '#5dc190',
220
+ '#4ab9a3',
221
+ '#4593a5',
222
+ '#ffffff',
223
+ '#5efdf7',
224
+ '#ff5dcc',
225
+ '#fdfe89',
226
+ ],
227
+ },
228
+ 'db-32': {
229
+ id: 'db-32',
230
+ name: 'DB-32',
231
+ colors: [
232
+ '#000000',
233
+ '#222034',
234
+ '#45283c',
235
+ '#663931',
236
+ '#8f563b',
237
+ '#df7126',
238
+ '#d9a066',
239
+ '#eec39a',
240
+ '#fbf236',
241
+ '#99e550',
242
+ '#6abe30',
243
+ '#37946e',
244
+ '#4b692f',
245
+ '#524b24',
246
+ '#323c39',
247
+ '#3f3f74',
248
+ '#306082',
249
+ '#5b6ee1',
250
+ '#639bff',
251
+ '#5fcde4',
252
+ '#cbdbfc',
253
+ '#ffffff',
254
+ '#9badb7',
255
+ '#847e87',
256
+ '#696a6a',
257
+ '#595652',
258
+ '#76428a',
259
+ '#ac3232',
260
+ '#d95763',
261
+ '#d77bba',
262
+ '#8f974a',
263
+ '#8a6f30',
264
+ ],
265
+ },
266
+ indecision: {
267
+ id: 'indecision',
268
+ name: 'Indecision',
269
+ colors: [
270
+ '#fff4e0',
271
+ '#8fcccb',
272
+ '#449489',
273
+ '#285763',
274
+ '#2f2b5c',
275
+ '#4b3b9c',
276
+ '#457cd6',
277
+ '#f2b63d',
278
+ '#d46e33',
279
+ '#e34262',
280
+ '#94353d',
281
+ '#57253b',
282
+ '#9c656c',
283
+ '#d1b48c',
284
+ '#b4ba47',
285
+ '#6d8c32',
286
+ '#2c1b2e',
287
+ ],
288
+ },
289
+ 'devil-food': {
290
+ id: 'devil-food',
291
+ name: 'Devil Food',
292
+ colors: [
293
+ '#160d10',
294
+ '#503146',
295
+ '#6a3c58',
296
+ '#934f65',
297
+ '#7f3265',
298
+ '#6a2249',
299
+ '#912749',
300
+ '#a6333c',
301
+ '#b54846',
302
+ '#bf5e4d',
303
+ '#a13f2f',
304
+ '#7b2c32',
305
+ '#58232f',
306
+ '#351d32',
307
+ '#3e2544',
308
+ '#4a3360',
309
+ '#674268',
310
+ '#b97884',
311
+ '#4b5e7d',
312
+ '#34377e',
313
+ '#2b2948',
314
+ '#1e1c32',
315
+ '#221425',
316
+ '#471f2f',
317
+ '#652c39',
318
+ '#773a40',
319
+ '#8e524c',
320
+ '#5d4143',
321
+ '#3a363c',
322
+ '#37272a',
323
+ '#4b3233',
324
+ ],
325
+ },
326
+ logcabin: {
327
+ id: 'logcabin',
328
+ name: 'Logcabin',
329
+ colors: [
330
+ '#73383e',
331
+ '#ad5845',
332
+ '#cf784c',
333
+ '#e8ad80',
334
+ '#f5e5bf',
335
+ '#f7ffed',
336
+ '#b5f5ec',
337
+ '#5bace3',
338
+ '#636ab8',
339
+ '#574175',
340
+ '#2f2338',
341
+ ],
342
+ },
343
+ wisteric: {
344
+ id: 'wisteric',
345
+ name: 'Wisteric',
346
+ colors: [
347
+ '#c9dab8',
348
+ '#86c1a3',
349
+ '#569082',
350
+ '#2d535b',
351
+ '#162841',
352
+ '#100b27',
353
+ '#331541',
354
+ '#732d72',
355
+ '#a75b80',
356
+ '#cd7a87',
357
+ '#f3b5a8',
358
+ '#ffddcb',
359
+ '#e6c9c3',
360
+ '#cea3ad',
361
+ '#b37fa1',
362
+ '#8d5489',
363
+ '#53315a',
364
+ ],
365
+ },
366
+ 'interstate-28': {
367
+ id: 'interstate-28',
368
+ name: 'Interstate 28',
369
+ colors: [
370
+ '#fefedf',
371
+ '#f4eaa3',
372
+ '#f4d565',
373
+ '#edb048',
374
+ '#bb5e25',
375
+ '#903a25',
376
+ '#6f1519',
377
+ '#a11920',
378
+ '#da453c',
379
+ '#f2846c',
380
+ '#e8676a',
381
+ '#c14166',
382
+ '#942a5e',
383
+ '#631649',
384
+ '#23192d',
385
+ '#342e4c',
386
+ '#465374',
387
+ '#5a8090',
388
+ '#78ccb9',
389
+ '#57a386',
390
+ '#3a716c',
391
+ '#27424b',
392
+ '#1f2a2c',
393
+ '#474214',
394
+ '#55632e',
395
+ '#89954c',
396
+ '#a1b974',
397
+ '#b6e3b3',
398
+ ],
399
+ },
400
+ };
401
+
402
+ function hexToRgb(hex) {
403
+ const h = hex.replace('#', '');
404
+ return [
405
+ parseInt(h.slice(0, 2), 16) / 255,
406
+ parseInt(h.slice(2, 4), 16) / 255,
407
+ parseInt(h.slice(4, 6), 16) / 255,
408
+ ];
409
+ }
410
+
411
+ function linearize(c) {
412
+ return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
413
+ }
414
+
415
+ function toOklab(hex) {
416
+ const [r, g, b] = hexToRgb(hex).map(linearize);
417
+ const l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b;
418
+ const m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b;
419
+ const s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b;
420
+ const l_ = Math.cbrt(l);
421
+ const m_ = Math.cbrt(m);
422
+ const s_ = Math.cbrt(s);
423
+ return [
424
+ 0.2104542553 * l_ + 0.793617785 * m_ - 0.0040720468 * s_,
425
+ 1.9779984951 * l_ - 2.357995934 * m_ + 0.1802819701 * s_,
426
+ 0.0259040371 * l_ + 0.7827717662 * m_ - 0.808675766 * s_,
427
+ ];
428
+ }
429
+
430
+ function oklabDist(a, b) {
431
+ const [L1, A1, B1] = toOklab(a);
432
+ const [L2, A2, B2] = toOklab(b);
433
+ return Math.hypot(L1 - L2, A1 - A2, B1 - B2);
434
+ }
435
+
436
+ /** Greedy farthest-point subset: seed with the two most-distant hexes, then add
437
+ * the hex maximizing min-distance to the chosen set. Deterministic. */
438
+ export function maxDistinctSubset(hexes, n) {
439
+ if (n >= hexes.length) return [...hexes];
440
+ if (n <= 0) return [];
441
+ if (n === 1) return [hexes[0]];
442
+
443
+ let bestI = 0;
444
+ let bestJ = 1;
445
+ let bestD = -1;
446
+ for (let i = 0; i < hexes.length; i++) {
447
+ for (let j = i + 1; j < hexes.length; j++) {
448
+ const d = oklabDist(hexes[i], hexes[j]);
449
+ if (d > bestD) {
450
+ bestD = d;
451
+ bestI = i;
452
+ bestJ = j;
453
+ }
454
+ }
455
+ }
456
+
457
+ const chosen = [hexes[bestI], hexes[bestJ]];
458
+ const remaining = hexes.filter((_, idx) => idx !== bestI && idx !== bestJ);
459
+
460
+ while (chosen.length < n && remaining.length) {
461
+ let bestIdx = 0;
462
+ let bestMin = -1;
463
+ for (let i = 0; i < remaining.length; i++) {
464
+ const minD = Math.min(...chosen.map((c) => oklabDist(remaining[i], c)));
465
+ if (minD > bestMin) {
466
+ bestMin = minD;
467
+ bestIdx = i;
468
+ }
469
+ }
470
+ chosen.push(remaining.splice(bestIdx, 1)[0]);
471
+ }
472
+ return chosen;
473
+ }
474
+
475
+ function normalizeColors(raw) {
476
+ if (!Array.isArray(raw)) return [];
477
+ const out = [];
478
+ const seen = new Set();
479
+ for (const entry of raw) {
480
+ let hex = normalizeHex(typeof entry === 'string' ? entry : '');
481
+ if (!hex) continue;
482
+ // A palette entry is a color choice only — opacity is a separate choice in
483
+ // the UX, so #rrggbbaa collapses to its opaque color rather than becoming
484
+ // a distinct swatch. (Sprite files may still carry alpha palette entries.)
485
+ if (hex.length === 9) hex = hex.slice(0, 7);
486
+ if (seen.has(hex)) continue;
487
+ seen.add(hex);
488
+ out.push(hex);
489
+ }
490
+ return out;
491
+ }
492
+
493
+ function hexSetsEqual(a, b) {
494
+ if (a.length !== b.length) return false;
495
+ const other = new Set(b);
496
+ return a.every((hex) => other.has(hex));
497
+ }
498
+
499
+ export function defaultDeckPalette() {
500
+ return { ...OFFICIAL_PALETTES[OFFICIAL_ENDESGA_64] };
501
+ }
502
+
503
+ /** Parse theme.style JSON text. Returns the object or null when empty/invalid. */
504
+ export function parseThemeData(text) {
505
+ if (typeof text !== 'string' || !text.trim()) return null;
506
+ try {
507
+ const value = JSON.parse(text);
508
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
509
+ } catch {
510
+ return null;
511
+ }
512
+ }
513
+
514
+ /** Resolve a theme.style object to `{ id, name, colors }`. Missing/malformed
515
+ * data falls back to Endesga-64 so default decks stay byte-identical. */
516
+ export function resolveDeckPalette(themeData) {
517
+ const fallback = defaultDeckPalette();
518
+ if (!themeData || typeof themeData !== 'object') return fallback;
519
+ const raw = themeData.palette;
520
+ if (typeof raw === 'string') {
521
+ return OFFICIAL_PALETTES[raw] ? { ...OFFICIAL_PALETTES[raw] } : fallback;
522
+ }
523
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return fallback;
524
+ if (typeof raw.id === 'string' && OFFICIAL_PALETTES[raw.id] && !Array.isArray(raw.colors)) {
525
+ return { ...OFFICIAL_PALETTES[raw.id] };
526
+ }
527
+ const colors = normalizeColors(raw.colors);
528
+ if (!colors.length) return fallback;
529
+ const name = typeof raw.name === 'string' && raw.name.trim() ? raw.name.trim() : 'custom';
530
+ return { id: null, name, colors };
531
+ }
532
+
533
+ export function isDefaultDeckPalette(palette) {
534
+ return palette?.id === OFFICIAL_ENDESGA_64 || hexSetsEqual(palette?.colors ?? [], EDG64);
535
+ }
536
+
537
+ /** Generation-path subset. Default EDG64 keeps the curated 16; larger custom
538
+ * palettes are cut mechanically; small palettes are used as-is. */
539
+ export function agentPaletteFor(colors, n = 16) {
540
+ const hexes = normalizeColors(colors);
541
+ if (hexSetsEqual(hexes, EDG64)) return [...AGENT_PALETTE_16];
542
+ if (hexes.length > n) return maxDistinctSubset(hexes, n);
543
+ return hexes;
544
+ }
545
+
546
+ /** Swatches the editor's deck-palette picker shows. Caps at KEY_ALPHABET's
547
+ * first 64 so a huge uploaded list still fits the key budget. */
548
+ export function pickerColorsFor(colors) {
549
+ const hexes = normalizeColors(colors);
550
+ return hexes.length > PALETTE_KEY_CAP ? maxDistinctSubset(hexes, PALETTE_KEY_CAP) : hexes;
551
+ }
552
+
553
+ const OFFICIAL_PICKER_PAGES = Object.values(OFFICIAL_PALETTES).map((palette) => ({
554
+ id: palette.id,
555
+ name: palette.name,
556
+ colors: pickerColorsFor(palette.colors),
557
+ }));
558
+
559
+ /** Named pages the sprite/art picker browses. Official palettes first (Endesga
560
+ * leading), then a custom `{ name, colors }` deck page when theme.style isn't
561
+ * an official id. Paging these does not write theme.style. */
562
+ export function pickerPagesFor(deckPalette) {
563
+ if (deckPalette?.id == null && deckPalette?.colors?.length) {
564
+ return [
565
+ ...OFFICIAL_PICKER_PAGES,
566
+ {
567
+ id: 'custom',
568
+ name: deckPalette.name || 'custom',
569
+ colors: pickerColorsFor(deckPalette.colors),
570
+ },
571
+ ];
572
+ }
573
+ return OFFICIAL_PICKER_PAGES;
574
+ }
575
+
576
+ /** Index of the deck's current palette in `pickerPagesFor` output. */
577
+ export function pickerPageIndexFor(pages, deckPalette) {
578
+ const id = deckPalette?.id ?? 'custom';
579
+ const index = pages.findIndex((page) => page.id === id);
580
+ return index >= 0 ? index : 0;
581
+ }
582
+
583
+ export function themeDataFromFiles(files) {
584
+ return parseThemeData(files?.[THEME_STYLE_PATH]);
585
+ }
586
+
587
+ export function deckPaletteFromFiles(files) {
588
+ return resolveDeckPalette(themeDataFromFiles(files));
589
+ }
590
+
591
+ /** key -> hex record from an ordered `{ key, hex }[]` palette. */
592
+ export function paletteRecord(palette) {
593
+ const rec = {};
594
+ for (const entry of palette ?? []) {
595
+ if (entry?.key && entry.hex) rec[entry.key] = entry.hex;
596
+ }
597
+ return rec;
598
+ }
599
+
600
+ export function hexForPaletteKey(palette, key) {
601
+ if (!key) return null;
602
+ const entry = (palette ?? []).find((item) => item.key === key);
603
+ return entry?.hex ?? null;
604
+ }
605
+
606
+ export function keyForPaletteHex(palette, hex) {
607
+ const norm = normalizeHex(hex ?? '');
608
+ if (!norm) return null;
609
+ const entry = (palette ?? []).find((item) => normalizeHex(item.hex) === norm);
610
+ return entry?.key ?? null;
611
+ }
612
+
613
+ /** Find or allocate a sprite-palette key for `hex`. Does not remap existing
614
+ * pixels. Returns `{ sprite, key }` or `{ sprite, key: null, error }`. */
615
+ export function allocateSpriteColor(sprite, hex) {
616
+ const norm = normalizeHex(typeof hex === 'string' ? hex : '');
617
+ if (!norm || !sprite) return { sprite, key: null };
618
+ const existing = keyForPaletteHex(sprite.palette, norm);
619
+ if (existing) return { sprite, key: existing };
620
+ if ((sprite.palette?.length ?? 0) >= PALETTE_KEY_CAP) {
621
+ return { sprite, key: null, error: 'palette-full' };
622
+ }
623
+ const used = new Set((sprite.palette ?? []).map((entry) => entry.key));
624
+ let key = null;
625
+ for (const ch of KEY_ALPHABET) {
626
+ if (!used.has(ch)) {
627
+ key = ch;
628
+ break;
629
+ }
630
+ }
631
+ if (!key) return { sprite, key: null, error: 'palette-full' };
632
+ return {
633
+ sprite: { ...sprite, palette: [...(sprite.palette ?? []), { key, hex: norm }] },
634
+ key,
635
+ };
636
+ }
@@ -395,13 +395,13 @@ export function snapResolutionDim(n) {
395
395
  export const DEFAULT_RESOLUTION = { width: 16, height: 16 };
396
396
 
397
397
  // ---------------------------------------------------------------------------
398
- // Two-tier Endesga-64 palette system
398
+ // Default deck palette (Endesga-64) + curated agent subset
399
399
  //
400
- // EDG64 is the FULL painting palette (Lospec "Endesga 64", canonical order) the
401
- // editor offers to USERS. AGENT_PALETTE_16 is a fixed 16-color SUBSET of EDG64
402
- // the constrained set the LLM generation path is told to use and that the
403
- // `draw` svg-rect quantizer snaps to. Every AGENT_PALETTE_16 color is a member
404
- // of EDG64.
400
+ // EDG64 is the default deck palette (Lospec "Endesga 64"). A deck may override
401
+ // it in theme.style — see engine/palettes.js. AGENT_PALETTE_16 is the curated
402
+ // 16-color subset used when the deck palette is EDG64; custom palettes derive
403
+ // their generation subset via agentPaletteFor. Every AGENT_PALETTE_16 color is
404
+ // a member of EDG64.
405
405
  // ---------------------------------------------------------------------------
406
406
 
407
407
  /** lospec.com/palette-list/endesga-64 — the full 64-color painting palette.