castle-web-cli 0.4.122 → 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 (60) hide show
  1. package/dist/agent-prompts.js +6 -2
  2. package/dist/castleJson.d.ts +2 -0
  3. package/dist/castleJson.js +39 -0
  4. package/dist/diffText.d.ts +14 -0
  5. package/dist/diffText.js +138 -0
  6. package/dist/headlessCover.d.ts +13 -0
  7. package/dist/headlessCover.js +99 -0
  8. package/dist/ide.d.ts +2 -1
  9. package/dist/ide.js +100 -43
  10. package/dist/imports.js +42 -2
  11. package/dist/index.js +49 -11
  12. package/dist/init.d.ts +1 -0
  13. package/dist/init.js +45 -1
  14. package/dist/localPaths.d.ts +1 -0
  15. package/dist/localPaths.js +5 -0
  16. package/dist/preview.d.ts +0 -1
  17. package/dist/preview.js +0 -58
  18. package/dist/save-deck.js +52 -1
  19. package/dist/serve.js +16 -0
  20. package/dist/shell/assets/Basteleur-Bold-CK8LF7Pt.woff +0 -0
  21. package/dist/shell/assets/Basteleur-Bold-DKFKedNb.woff2 +0 -0
  22. package/dist/shell/assets/index-BfOPkSej.css +1 -0
  23. package/dist/shell/assets/index-u0nYFqbF.js +434 -0
  24. package/dist/shell/index.html +2 -2
  25. package/dist/versionStore.d.ts +54 -0
  26. package/dist/versionStore.js +281 -0
  27. package/dist/versions.d.ts +82 -0
  28. package/dist/versions.js +446 -0
  29. package/kits/physics-2d/CLAUDE.md +2 -2
  30. package/kits/physics-2d/castle.json +10 -2
  31. package/kits/physics-2d/docs/pxart-format.md +33 -26
  32. package/kits/physics-2d/editors/PxArtEditor.jsx +120 -49
  33. package/kits/physics-2d/editors/SingleEditor.jsx +6 -3
  34. package/kits/physics-2d/editors/StyleEditor.jsx +95 -0
  35. package/kits/physics-2d/editors/pathOverlay.js +1 -1
  36. package/kits/physics-2d/editors/pathTools.js +9 -1
  37. package/kits/physics-2d/editors/pixelGeometry.js +14 -13
  38. package/kits/physics-2d/editors/pixelInspector.jsx +202 -53
  39. package/kits/physics-2d/editors/pxArtEditorModel.js +8 -63
  40. package/kits/physics-2d/editors/pxArtTools.js +3 -43
  41. package/kits/physics-2d/editors/styleEditor.module.css +105 -0
  42. package/kits/physics-2d/editors/styleTheme.js +16 -0
  43. package/kits/physics-2d/engine/files.js +2 -1
  44. package/kits/physics-2d/engine/liveReload.js +4 -3
  45. package/kits/physics-2d/engine/palettes.js +636 -0
  46. package/kits/physics-2d/engine/pxart.js +6 -6
  47. package/kits/physics-2d/engine/svgImport.js +1056 -0
  48. package/kits/physics-2d/engine/ui.jsx +2 -0
  49. package/kits/physics-2d/engine/ui.module.css +54 -9
  50. package/kits/physics-2d/package-lock.json +1 -1
  51. package/kits/physics-2d/package.json +1 -0
  52. package/kits/physics-2d/scripts/deckTheme.mjs +25 -0
  53. package/kits/physics-2d/scripts/draw.mjs +5 -3
  54. package/kits/physics-2d/scripts/import-svg.mjs +16 -1069
  55. package/kits/physics-2d/scripts/palette.mjs +10 -0
  56. package/kits/physics-2d/scripts/svg-emission-guide.md +5 -3
  57. package/kits/physics-2d/theme.style +3 -0
  58. package/package.json +1 -1
  59. package/dist/shell/assets/index-BkJ87APM.css +0 -1
  60. package/dist/shell/assets/index-DXBpj3-y.js +0 -434
@@ -18,7 +18,8 @@ export const PIXEL_TOOLS = [
18
18
  ];
19
19
 
20
20
  // Path-layer tools: brush (pen / freehand / primitives), select (move/z-order),
21
- // bend (drag bows on any hit edge), fill (recolor fill or stroke).
21
+ // bend (drag bows on any hit edge), fill (paint a shape's fill or stroke
22
+ // an interior click adds a fill to a stroke-only shape).
22
23
  export const PATH_TOOLS = [
23
24
  { id: 'brush', label: 'Brush', icon: 'pencil', shortcut: 'B' },
24
25
  { id: 'fill', label: 'Fill', icon: 'fill', shortcut: 'G' },
@@ -27,8 +28,9 @@ export const PATH_TOOLS = [
27
28
  ];
28
29
 
29
30
  // Sub-modes for the shape tool. Each drags out an axis-aligned box: line draws
30
- // the diagonal, square the rectangle, circle the inscribed ellipse, triangle an
31
- // isosceles triangle. `filled` (a separate toggle) applies to the closed three.
31
+ // the diagonal, square the rectangle, circle the inscribed ellipse, triangle a
32
+ // right triangle (hypotenuse is the drag; right angle at (start.x, end.y)).
33
+ // `filled` (a separate toggle) applies to the closed three.
32
34
  export const SHAPE_MODES = [
33
35
  { id: 'line', label: 'Line', icon: 'slash' },
34
36
  { id: 'square', label: 'Square', icon: 'square' },
@@ -219,51 +221,132 @@ function ResolutionSelect({ label, value, onChange }) {
219
221
  );
220
222
  }
221
223
 
222
- // 12-column palette grid shared by the docked sidebar and the compact popover.
224
+ function sameHex(a, b) {
225
+ return !!a && !!b && a.toLowerCase() === b.toLowerCase();
226
+ }
227
+
228
+ function NoneSwatch({ selected, onSelect }) {
229
+ return (
230
+ <button
231
+ type="button"
232
+ className={cx(styles.swatch, selected && styles.swatchSelected)}
233
+ title="None"
234
+ aria-label="None"
235
+ style={{ position: 'relative', background: 'transparent' }}
236
+ onClick={onSelect}>
237
+ <span
238
+ aria-hidden
239
+ style={{
240
+ position: 'absolute',
241
+ inset: 4,
242
+ background:
243
+ 'linear-gradient(to top right, transparent calc(50% - 1px), #e66 calc(50% - 1px), #e66 calc(50% + 1px), transparent calc(50% + 1px))',
244
+ }}
245
+ />
246
+ </button>
247
+ );
248
+ }
249
+
250
+ // Swatch strip shared by the docked sidebar and the compact popover.
251
+ // `keys` paints a sprite's working palette; `hexes` paints a picker list.
252
+ // `wrap` is the unlabeled sprite row; the named official page is an 8-col grid.
223
253
  export function PaletteGrid({
224
254
  keys,
255
+ hexes,
225
256
  palette,
226
257
  activeKey,
258
+ activeHex,
227
259
  onSelectKey,
260
+ onSelectHex,
228
261
  allowNone = false,
229
262
  noneSelected = false,
230
263
  onSelectNone,
264
+ wrap = false,
231
265
  }) {
266
+ const items = hexes
267
+ ? hexes.map((hex) => ({ id: hex, hex, selected: sameHex(activeHex, hex), onClick: () => onSelectHex?.(hex) }))
268
+ : (keys ?? []).map((key) => ({
269
+ id: key,
270
+ hex: palette?.[key],
271
+ selected: activeKey === key || sameHex(activeHex, palette?.[key]),
272
+ onClick: () => onSelectKey?.(key),
273
+ }));
274
+ if (!allowNone && !items.length) return null;
232
275
  return (
233
- <div className={styles.palette}>
234
- {allowNone ? (
276
+ <div className={wrap ? styles.paletteSpriteRow : styles.palette}>
277
+ {allowNone ? <NoneSwatch selected={noneSelected} onSelect={onSelectNone} /> : null}
278
+ {items.map((item) => (
235
279
  <button
280
+ key={item.id}
236
281
  type="button"
237
- className={cx(styles.swatch, noneSelected && styles.swatchSelected)}
238
- title="None"
239
- aria-label="None"
240
- style={{ position: 'relative', background: 'transparent' }}
241
- onClick={onSelectNone}>
242
- <span
243
- aria-hidden
244
- style={{
245
- position: 'absolute',
246
- inset: 4,
247
- background:
248
- 'linear-gradient(to top right, transparent calc(50% - 1px), #e66 calc(50% - 1px), #e66 calc(50% + 1px), transparent calc(50% + 1px))',
249
- }}
250
- />
251
- </button>
252
- ) : null}
253
- {keys.map((key) => (
254
- <button
255
- key={key}
256
- type="button"
257
- className={cx(styles.swatch, activeKey === key && styles.swatchSelected)}
258
- title={palette[key]}
259
- style={{ background: palette[key] }}
260
- onClick={() => onSelectKey(key)}
282
+ className={cx(styles.swatch, item.selected && styles.swatchSelected)}
283
+ title={item.hex}
284
+ style={{ background: item.hex }}
285
+ onClick={item.onClick}
261
286
  />
262
287
  ))}
263
288
  </div>
264
289
  );
265
290
  }
266
291
 
292
+ function PalettePager({ name, onPage }) {
293
+ return (
294
+ <div className={styles.palettePager}>
295
+ <span className={styles.palettePagerName}>{name}</span>
296
+ <div className={styles.palettePagerBtns}>
297
+ <IconButton icon="chevron-left" label="Previous palette" onClick={() => onPage(-1)} />
298
+ <IconButton icon="chevron-right" label="Next palette" onClick={() => onPage(1)} />
299
+ </div>
300
+ </div>
301
+ );
302
+ }
303
+
304
+ // Unlabeled sprite-color row (when the sprite has colors, or when fill/stroke
305
+ // needs a none swatch) plus a paged official-palette grid. Paging only changes
306
+ // which named page is shown — it does not write theme.style.
307
+ export function PaletteSections({
308
+ spriteKeys,
309
+ spritePalette,
310
+ pages,
311
+ pageIndex,
312
+ onPage,
313
+ activeKey,
314
+ activeHex,
315
+ onSelectKey,
316
+ onSelectHex,
317
+ allowNone = false,
318
+ noneSelected = false,
319
+ onSelectNone,
320
+ paletteFull = false,
321
+ }) {
322
+ const page = pages?.[pageIndex] ?? pages?.[0];
323
+ const showSpriteRow = !!spriteKeys?.length || allowNone;
324
+ return (
325
+ <div>
326
+ {showSpriteRow ? (
327
+ <PaletteGrid
328
+ keys={spriteKeys}
329
+ palette={spritePalette}
330
+ activeKey={activeKey}
331
+ activeHex={activeHex}
332
+ onSelectKey={onSelectKey}
333
+ allowNone={allowNone}
334
+ noneSelected={noneSelected}
335
+ onSelectNone={onSelectNone}
336
+ wrap
337
+ />
338
+ ) : null}
339
+ {page ? (
340
+ <>
341
+ <PalettePager name={page.name} onPage={onPage} />
342
+ <PaletteGrid hexes={page.colors} activeHex={activeHex} onSelectHex={onSelectHex} />
343
+ </>
344
+ ) : null}
345
+ {paletteFull ? <div className={styles.paletteFullNote}>Palette is full (64 colors). Recolor an existing swatch instead.</div> : null}
346
+ </div>
347
+ );
348
+ }
349
+
267
350
  // Anchored palette + eyedropper popover for compact/mobile paint-strip color tap.
268
351
  export function PalettePopover({ open, anchorRef, onClose, children }) {
269
352
  const popoverRef = useRef(null);
@@ -276,7 +359,7 @@ export function PalettePopover({ open, anchorRef, onClose, children }) {
276
359
  const anchor = anchorRef.current.getBoundingClientRect();
277
360
  const rect = popoverRef.current?.getBoundingClientRect();
278
361
  const margin = 8;
279
- const width = rect?.width ?? 280;
362
+ const width = rect?.width ?? 231;
280
363
  const height = rect?.height ?? 320;
281
364
  const maxLeft = Math.max(margin, window.innerWidth - width - margin);
282
365
  const maxTop = Math.max(margin, window.innerHeight - height - margin);
@@ -464,7 +547,18 @@ const ShapeSwatch = React.forwardRef(function ShapeSwatch(
464
547
  );
465
548
  });
466
549
 
467
- function ShapeInspectorButtons({ pathActions, palette, paletteKeys, buttonClass, groupClass }) {
550
+ function ShapeInspectorButtons({
551
+ pathActions,
552
+ palette,
553
+ paletteKeys,
554
+ pages,
555
+ pageIndex,
556
+ onPage,
557
+ onSelectHex,
558
+ paletteFull,
559
+ buttonClass,
560
+ groupClass,
561
+ }) {
468
562
  const [slotPicker, setSlotPicker] = useState(null);
469
563
  const fillRef = useRef(null);
470
564
  const strokeRef = useRef(null);
@@ -485,9 +579,10 @@ function ShapeInspectorButtons({ pathActions, palette, paletteKeys, buttonClass,
485
579
  setSlotPicker(null);
486
580
  }
487
581
 
488
- function applyPicker(key) {
489
- if (slotPicker === 'fill') pathActions?.setFill(key);
490
- else if (slotPicker === 'stroke') pathActions?.setStroke(key);
582
+ function applyColor(hex) {
583
+ if (hex) onSelectHex?.(hex);
584
+ if (slotPicker === 'fill') pathActions?.setFill(hex ?? null);
585
+ else if (slotPicker === 'stroke') pathActions?.setStroke(hex ?? null);
491
586
  closePicker();
492
587
  }
493
588
 
@@ -590,14 +685,20 @@ function ShapeInspectorButtons({ pathActions, palette, paletteKeys, buttonClass,
590
685
  {slotPicker === 'fill' ? 'Fill' : 'Stroke'}
591
686
  </span>
592
687
  </div>
593
- <PaletteGrid
594
- keys={paletteKeys}
595
- palette={palette}
688
+ <PaletteSections
689
+ spriteKeys={paletteKeys}
690
+ spritePalette={palette}
691
+ pages={pages}
692
+ pageIndex={pageIndex}
693
+ onPage={onPage}
596
694
  activeKey={pickerKey}
695
+ activeHex={pickerKey ? palette?.[pickerKey] : null}
696
+ onSelectKey={(key) => applyColor(palette?.[key])}
697
+ onSelectHex={applyColor}
597
698
  allowNone
598
699
  noneSelected={pickerNoneSelected}
599
- onSelectNone={() => applyPicker(null)}
600
- onSelectKey={applyPicker}
700
+ onSelectNone={() => applyColor(null)}
701
+ paletteFull={paletteFull}
601
702
  />
602
703
  </PalettePopover>
603
704
  </>
@@ -614,8 +715,13 @@ export function PaintStrip({
614
715
  onTogglePalette,
615
716
  colorButtonRef,
616
717
  onSelectKey,
718
+ onSelectHex,
617
719
  paletteKeys,
618
720
  palette,
721
+ pages,
722
+ pageIndex,
723
+ onPage,
724
+ paletteFull,
619
725
  moveActions,
620
726
  selectActions,
621
727
  pathActions,
@@ -662,6 +768,15 @@ export function PaintStrip({
662
768
  style={{ background: activeColor }}
663
769
  onClick={onTogglePalette}
664
770
  />
771
+ <button
772
+ type="button"
773
+ className={cx(styles.paintStripModeButton, picking && styles.drawingToolSelected)}
774
+ title="Pick color"
775
+ aria-label="Pick color"
776
+ aria-pressed={picking ? 'true' : 'false'}
777
+ onClick={() => setPicking(!picking)}>
778
+ <Icon name="eyedropper" />
779
+ </button>
665
780
  </div>
666
781
  ) : null}
667
782
  {showBrushSize || showEraseSize ? (
@@ -772,6 +887,11 @@ export function PaintStrip({
772
887
  pathActions={pathActions}
773
888
  palette={palette}
774
889
  paletteKeys={paletteKeys}
890
+ pages={pages}
891
+ pageIndex={pageIndex}
892
+ onPage={onPage}
893
+ onSelectHex={onSelectHex}
894
+ paletteFull={paletteFull}
775
895
  buttonClass={styles.paintStripModeButton}
776
896
  groupClass={styles.paintStripSection}
777
897
  />
@@ -780,22 +900,24 @@ export function PaintStrip({
780
900
  <PalettePopover open={paletteOpen} anchorRef={colorButtonRef} onClose={() => onTogglePalette(false)}>
781
901
  <div className={styles.palettePopoverHeader}>
782
902
  <span className={styles.palettePopoverTitle}>Palette</span>
783
- <IconButton
784
- icon="eyedropper"
785
- label="Pick color"
786
- active={picking}
787
- aria-pressed={picking ? 'true' : 'false'}
788
- onClick={() => setPicking(!picking)}
789
- />
790
903
  </div>
791
- <PaletteGrid
792
- keys={paletteKeys}
793
- palette={palette}
904
+ <PaletteSections
905
+ spriteKeys={paletteKeys}
906
+ spritePalette={palette}
907
+ pages={pages}
908
+ pageIndex={pageIndex}
909
+ onPage={onPage}
794
910
  activeKey={activeKey}
911
+ activeHex={activeColor}
795
912
  onSelectKey={(key) => {
796
913
  onSelectKey(key);
797
914
  onTogglePalette(false);
798
915
  }}
916
+ onSelectHex={(hex) => {
917
+ onSelectHex(hex);
918
+ onTogglePalette(false);
919
+ }}
920
+ paletteFull={paletteFull}
799
921
  />
800
922
  </PalettePopover>
801
923
  </>
@@ -806,9 +928,15 @@ export function PaintStrip({
806
928
  export function PxArtInspectorDock({
807
929
  toolState,
808
930
  activeKey,
931
+ activeHex,
809
932
  onSelectKey,
933
+ onSelectHex,
810
934
  paletteKeys,
811
935
  palette,
936
+ pages,
937
+ pageIndex,
938
+ onPage,
939
+ paletteFull,
812
940
  moveActions,
813
941
  selectActions,
814
942
  pathActions,
@@ -829,11 +957,17 @@ export function PxArtInspectorDock({
829
957
  onClick={() => setPicking(!picking)}
830
958
  />
831
959
  }>
832
- <PaletteGrid
833
- keys={paletteKeys}
834
- palette={palette}
960
+ <PaletteSections
961
+ spriteKeys={paletteKeys}
962
+ spritePalette={palette}
963
+ pages={pages}
964
+ pageIndex={pageIndex}
965
+ onPage={onPage}
835
966
  activeKey={activeKey}
967
+ activeHex={activeHex}
836
968
  onSelectKey={onSelectKey}
969
+ onSelectHex={onSelectHex}
970
+ paletteFull={paletteFull}
837
971
  />
838
972
  </Panel>
839
973
  ) : null}
@@ -844,6 +978,11 @@ export function PxArtInspectorDock({
844
978
  pathActions={pathActions}
845
979
  palette={palette}
846
980
  paletteKeys={paletteKeys}
981
+ pages={pages}
982
+ pageIndex={pageIndex}
983
+ onPage={onPage}
984
+ onSelectHex={onSelectHex}
985
+ paletteFull={paletteFull}
847
986
  />
848
987
  </div>
849
988
  </aside>
@@ -876,6 +1015,11 @@ export function ToolSettingsPanel({
876
1015
  pathActions,
877
1016
  palette,
878
1017
  paletteKeys,
1018
+ pages,
1019
+ pageIndex,
1020
+ onPage,
1021
+ onSelectHex,
1022
+ paletteFull,
879
1023
  }) {
880
1024
  const {
881
1025
  tool,
@@ -987,6 +1131,11 @@ export function ToolSettingsPanel({
987
1131
  pathActions={pathActions}
988
1132
  palette={palette}
989
1133
  paletteKeys={paletteKeys}
1134
+ pages={pages}
1135
+ pageIndex={pageIndex}
1136
+ onPage={onPage}
1137
+ onSelectHex={onSelectHex}
1138
+ paletteFull={paletteFull}
990
1139
  buttonClass={styles.drawingToolButton}
991
1140
  groupClass={styles.drawingShapeModeRow}
992
1141
  />
@@ -19,12 +19,8 @@ import {
19
19
  TRANSPARENT,
20
20
  VECTOR_RENDERER,
21
21
  } from '../engine/pxart';
22
- import {
23
- blankCells,
24
- EDITOR_KEYS,
25
- EDITOR_PALETTE,
26
- editorKeyForHex,
27
- } from './pxArtTools';
22
+ import { paletteRecord } from '../engine/palettes';
23
+ import { blankCells } from './pxArtTools';
28
24
  import {
29
25
  bakePathCell,
30
26
  clonePathData,
@@ -36,15 +32,11 @@ import {
36
32
  // ============================================================================
37
33
  // Working-state helpers for the full Sprite the PxArt editor edits in memory.
38
34
  //
39
- // The editor always works on a Sprite normalized onto the FIXED Endesga-64
40
- // palette, so painting + serialization stay consistent no matter what palette
41
- // an externally-authored file arrived with. Every mutation is immutable
42
- // (returns a fresh Sprite) so it flows straight into the text undo/redo history.
35
+ // The editor works on the sprite's own palette. Opening never remaps colors;
36
+ // painting allocates keys into that palette as needed. Every mutation is
37
+ // immutable (returns a fresh Sprite) so it flows into the text undo/redo history.
43
38
  // ============================================================================
44
39
 
45
- // The Endesga-64 palette as an ordered { key, hex } list (the full palette shape).
46
- export const EDITOR_ORDERED = EDITOR_KEYS.map((key) => ({ key, hex: EDITOR_PALETTE[key] }));
47
-
48
40
  // Size a canvas to a sprite resolution, disable smoothing, and clear it.
49
41
  // Returns the 2D context (or null). Shared by the artboard + thumbnail renders.
50
42
  export function setupSpriteCanvas(canvas, resolution) {
@@ -75,14 +67,14 @@ export function forEachGridCell(grid, width, height, visit) {
75
67
  export function deriveSprite(text) {
76
68
  if (!(text ?? '').trim()) return blankSprite();
77
69
  const parsed = parseFull(text);
78
- return parsed ? normalizeToPalette(parsed) : null;
70
+ return parsed ?? null;
79
71
  }
80
72
 
81
73
  function blankSprite() {
82
74
  const { width, height } = DEFAULT_RESOLUTION;
83
75
  return {
84
76
  resolution: { width, height },
85
- palette: EDITOR_ORDERED,
77
+ palette: [],
86
78
  // The lone frame inherits the global duration (no per-frame override).
87
79
  frames: [{}],
88
80
  defaultDurationMs: DEFAULT_DURATION_MS,
@@ -119,53 +111,6 @@ function gridCell(grid, x, y) {
119
111
  return x || y ? { grid, x, y } : { grid };
120
112
  }
121
113
 
122
- // Remap every grid char onto the nearest Endesga-64 key and swap in the full
123
- // palette, leaving links / null cells / structure untouched. Idempotent for an
124
- // already-normalized sprite.
125
- function normalizeToPalette(sprite) {
126
- const lookup = paletteLookup(sprite.palette);
127
- const layers = sprite.layers.map((layer) => ({
128
- ...layer,
129
- cells: layer.cells.map((cell) => remapCell(cell, lookup)),
130
- }));
131
- return { ...sprite, palette: EDITOR_ORDERED, layers };
132
- }
133
-
134
- function remapCell(cell, lookup) {
135
- if (cell == null) return null;
136
- if ('link' in cell) return { link: cell.link };
137
- // Preserve the cel's offset while remapping its palette keys.
138
- const next = gridCell(cell.grid.map((row) => remapRow(row, lookup)), cell.x ?? 0, cell.y ?? 0);
139
- return cell.path ? { ...next, path: remapPathKeys(cell.path, lookup) } : next;
140
- }
141
-
142
- function remapPathKeys(path, lookup) {
143
- const next = clonePathData(path);
144
- for (const shape of next.shapes) {
145
- if (shape.fill) shape.fill = remapKey(shape.fill, lookup);
146
- if (shape.stroke) shape.stroke = remapKey(shape.stroke, lookup);
147
- }
148
- return next;
149
- }
150
-
151
- function remapKey(key, lookup) {
152
- const hex = colorForKeyV2(lookup, key);
153
- return hex ? editorKeyForHex(hex) : key;
154
- }
155
-
156
- function remapRow(row, lookup) {
157
- let out = '';
158
- for (const ch of row) {
159
- if (ch === TRANSPARENT) {
160
- out += TRANSPARENT;
161
- continue;
162
- }
163
- const hex = colorForKeyV2(lookup, ch);
164
- out += hex ? editorKeyForHex(hex) : TRANSPARENT;
165
- }
166
- return out;
167
- }
168
-
169
114
  // ---------------------------------------------------------------------------
170
115
  // cells <-> (layer, frame)
171
116
  // ---------------------------------------------------------------------------
@@ -798,7 +743,7 @@ function shiftCellOffset(cell, dx, dy, width, height) {
798
743
  // representable; if not, it correctly forces the full form.
799
744
  export function serializeModel(sprite) {
800
745
  const compact = serializeCompact({
801
- ...fromCells(EDITOR_PALETTE, cellsAt(sprite, 0, 0)),
746
+ ...fromCells(paletteRecord(sprite.palette), cellsAt(sprite, 0, 0)),
802
747
  renderer: sprite.renderer,
803
748
  cornerRadius: sprite.cornerRadius,
804
749
  });
@@ -1,4 +1,4 @@
1
- import { EDG64, KEY_ALPHABET, normalizeHex, TRANSPARENT } from '../engine/pxart';
1
+ import { TRANSPARENT } from '../engine/pxart';
2
2
  import {
3
3
  forEachDab,
4
4
  forEachEllipseCells,
@@ -7,54 +7,14 @@ import {
7
7
  forEachTriangleCells,
8
8
  } from './pixelGeometry';
9
9
 
10
- // The PxArt editor paints over the FIXED full Endesga-64 palette (no free color
11
- // picking). Each palette color gets a stable single-char key (KEY_ALPHABET, 64
12
- // distinct keys); the transparent cell is the reserved "." (TRANSPARENT).
13
- export const EDITOR_KEYS = EDG64.map((_, index) => KEY_ALPHABET[index]);
14
-
15
- // key -> "#rrggbb" record, in palette order. Passed to the SDK's fromCells so
16
- // serialized files carry only the palette entries actually used.
17
- export const EDITOR_PALETTE = Object.fromEntries(
18
- EDG64.map((hex, index) => [EDITOR_KEYS[index], hex])
19
- );
20
-
21
- // hex -> palette key, for mapping a loaded sprite's colors onto the swatch.
22
- const KEY_BY_HEX = new Map(EDG64.map((hex, index) => [hex, EDITOR_KEYS[index]]));
10
+ // Paint helpers are key-agnostic: the editor allocates palette keys on the
11
+ // sprite and passes them in. The reserved "." (TRANSPARENT) is the erase key.
23
12
 
24
13
  // Build a `height x width` matrix of "." (transparent) cells.
25
14
  export function blankCells(width, height) {
26
15
  return Array.from({ length: height }, () => Array.from({ length: width }, () => TRANSPARENT));
27
16
  }
28
17
 
29
- // Snap an arbitrary hex color to the nearest palette key by RGB distance, so a
30
- // loaded sprite whose palette isn't exactly the Endesga-64 set still maps onto
31
- // the fixed swatch. Returns "." for invalid/transparent input.
32
- export function editorKeyForHex(hex) {
33
- const norm = typeof hex === 'string' ? normalizeHex(hex) : null;
34
- if (!norm) return TRANSPARENT;
35
- const exact = KEY_BY_HEX.get('#' + norm.slice(1, 7));
36
- if (exact) return exact;
37
- const r = parseInt(norm.slice(1, 3), 16);
38
- const g = parseInt(norm.slice(3, 5), 16);
39
- const b = parseInt(norm.slice(5, 7), 16);
40
- let bestKey = EDITOR_KEYS[0];
41
- let bestDist = Infinity;
42
- EDG64.forEach((swatch, index) => {
43
- const dist =
44
- (r - parseInt(swatch.slice(1, 3), 16)) ** 2 +
45
- (g - parseInt(swatch.slice(3, 5), 16)) ** 2 +
46
- (b - parseInt(swatch.slice(5, 7), 16)) ** 2;
47
- if (dist < bestDist) {
48
- bestDist = dist;
49
- bestKey = EDITOR_KEYS[index];
50
- }
51
- });
52
- return bestKey;
53
- }
54
-
55
- // The swatch the editor opens on: Endesga-64's lead bright red (#ff0040).
56
- export const DEFAULT_KEY = editorKeyForHex('#ff0040');
57
-
58
18
  function isInside(cells, x, y) {
59
19
  return y >= 0 && y < cells.length && x >= 0 && x < (cells[0]?.length ?? 0);
60
20
  }
@@ -0,0 +1,105 @@
1
+ /* Palette picker for theme.style. Tokens match engine/ui.module.css. */
2
+
3
+ .root {
4
+ flex: 1 1 auto;
5
+ min-width: 0;
6
+ min-height: 0;
7
+ width: 100%;
8
+ overflow-x: hidden;
9
+ overflow-y: auto;
10
+ overscroll-behavior: contain;
11
+ padding: 16px 18px 24px;
12
+ color: var(--castle-inspector-text, #e6e6e6);
13
+ }
14
+
15
+ .replaceNote {
16
+ margin: 8px 0 16px;
17
+ font-size: 12px;
18
+ line-height: 1.4;
19
+ color: var(--castle-inspector-muted, #888);
20
+ }
21
+
22
+ .list {
23
+ display: flex;
24
+ flex-direction: column;
25
+ gap: 10px;
26
+ }
27
+
28
+ .card {
29
+ display: flex;
30
+ flex-direction: column;
31
+ gap: 8px;
32
+ width: 100%;
33
+ margin: 0;
34
+ padding: 12px;
35
+ border: 1px solid var(--castle-inspector-border, #2a2a2a);
36
+ border-radius: var(--castle-radius, 4px);
37
+ background: var(--castle-card, #121213);
38
+ color: inherit;
39
+ text-align: left;
40
+ cursor: pointer;
41
+ }
42
+
43
+ .card:hover {
44
+ background: var(--castle-panel-raised, #101010);
45
+ }
46
+
47
+ .cardSelected {
48
+ border-color: var(--castle-selected, #e6e6e6);
49
+ box-shadow: inset 0 0 0 1px var(--castle-selected, #e6e6e6);
50
+ background: var(--castle-panel-raised, #101010);
51
+ }
52
+
53
+ .cardStatic {
54
+ cursor: default;
55
+ }
56
+
57
+ .cardStatic:hover {
58
+ background: var(--castle-card, #121213);
59
+ }
60
+
61
+ .cardSelected.cardStatic:hover {
62
+ background: var(--castle-panel-raised, #101010);
63
+ }
64
+
65
+ .cardHead {
66
+ display: flex;
67
+ align-items: baseline;
68
+ justify-content: space-between;
69
+ gap: 12px;
70
+ }
71
+
72
+ .cardName {
73
+ font-size: 14px;
74
+ line-height: 1.2;
75
+ }
76
+
77
+ .cardMeta {
78
+ display: flex;
79
+ align-items: center;
80
+ gap: 8px;
81
+ font-size: 11px;
82
+ color: var(--castle-inspector-muted, #888);
83
+ white-space: nowrap;
84
+ }
85
+
86
+ .badge {
87
+ padding: 1px 6px;
88
+ border: 1px solid var(--castle-inspector-border, #2a2a2a);
89
+ border-radius: 999px;
90
+ color: var(--castle-inspector-text, #e6e6e6);
91
+ }
92
+
93
+ .strip {
94
+ display: flex;
95
+ height: 18px;
96
+ overflow: hidden;
97
+ border: 1px solid var(--castle-inspector-border, #2a2a2a);
98
+ border-radius: 2px;
99
+ }
100
+
101
+ .chip {
102
+ flex: 1 1 0;
103
+ min-width: 2px;
104
+ height: 100%;
105
+ }
@@ -0,0 +1,16 @@
1
+ // Read-modify-write for theme.style. Parse the existing JSON, set `palette`
2
+ // to an official id, and serialize — unknown top-level keys survive.
3
+
4
+ import { parseThemeData } from '../engine/palettes.js';
5
+
6
+ export function formatThemeJson(value) {
7
+ return `${JSON.stringify(value, null, 2)}\n`;
8
+ }
9
+
10
+ /** Replace `palette` with an official id. Empty / unparseable text starts `{}`. */
11
+ export function applyOfficialPalette(text, paletteId) {
12
+ const parsed = parseThemeData(text);
13
+ const data = parsed ? { ...parsed } : {};
14
+ data.palette = paletteId;
15
+ return formatThemeJson(data);
16
+ }