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.
- package/dist/agent-prompts.js +6 -2
- package/dist/castleJson.d.ts +2 -0
- package/dist/castleJson.js +39 -0
- package/dist/diffText.d.ts +14 -0
- package/dist/diffText.js +138 -0
- package/dist/headlessCover.d.ts +13 -0
- package/dist/headlessCover.js +99 -0
- package/dist/ide.d.ts +2 -1
- package/dist/ide.js +100 -43
- package/dist/imports.js +42 -2
- package/dist/index.js +49 -11
- package/dist/init.d.ts +1 -0
- package/dist/init.js +45 -1
- package/dist/localPaths.d.ts +1 -0
- package/dist/localPaths.js +5 -0
- package/dist/preview.d.ts +0 -1
- package/dist/preview.js +0 -58
- package/dist/save-deck.js +52 -1
- package/dist/serve.js +16 -0
- package/dist/shell/assets/Basteleur-Bold-CK8LF7Pt.woff +0 -0
- package/dist/shell/assets/Basteleur-Bold-DKFKedNb.woff2 +0 -0
- package/dist/shell/assets/index-BfOPkSej.css +1 -0
- package/dist/shell/assets/index-u0nYFqbF.js +434 -0
- package/dist/shell/index.html +2 -2
- package/dist/versionStore.d.ts +54 -0
- package/dist/versionStore.js +281 -0
- package/dist/versions.d.ts +82 -0
- package/dist/versions.js +446 -0
- package/kits/physics-2d/CLAUDE.md +2 -2
- package/kits/physics-2d/castle.json +10 -2
- package/kits/physics-2d/docs/pxart-format.md +33 -26
- package/kits/physics-2d/editors/PxArtEditor.jsx +120 -49
- package/kits/physics-2d/editors/SingleEditor.jsx +6 -3
- package/kits/physics-2d/editors/StyleEditor.jsx +95 -0
- package/kits/physics-2d/editors/pathOverlay.js +1 -1
- package/kits/physics-2d/editors/pathTools.js +9 -1
- package/kits/physics-2d/editors/pixelGeometry.js +14 -13
- package/kits/physics-2d/editors/pixelInspector.jsx +202 -53
- package/kits/physics-2d/editors/pxArtEditorModel.js +8 -63
- package/kits/physics-2d/editors/pxArtTools.js +3 -43
- package/kits/physics-2d/editors/styleEditor.module.css +105 -0
- package/kits/physics-2d/editors/styleTheme.js +16 -0
- package/kits/physics-2d/engine/files.js +2 -1
- package/kits/physics-2d/engine/liveReload.js +4 -3
- package/kits/physics-2d/engine/palettes.js +636 -0
- package/kits/physics-2d/engine/pxart.js +6 -6
- package/kits/physics-2d/engine/svgImport.js +1056 -0
- package/kits/physics-2d/engine/ui.jsx +2 -0
- package/kits/physics-2d/engine/ui.module.css +54 -9
- package/kits/physics-2d/package-lock.json +1 -1
- package/kits/physics-2d/package.json +1 -0
- package/kits/physics-2d/scripts/deckTheme.mjs +25 -0
- package/kits/physics-2d/scripts/draw.mjs +5 -3
- package/kits/physics-2d/scripts/import-svg.mjs +16 -1069
- package/kits/physics-2d/scripts/palette.mjs +10 -0
- package/kits/physics-2d/scripts/svg-emission-guide.md +5 -3
- package/kits/physics-2d/theme.style +3 -0
- package/package.json +1 -1
- package/dist/shell/assets/index-BkJ87APM.css +0 -1
- 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 (
|
|
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
|
|
31
|
-
//
|
|
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
|
-
|
|
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,
|
|
238
|
-
title=
|
|
239
|
-
|
|
240
|
-
|
|
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 ??
|
|
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({
|
|
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
|
|
489
|
-
if (
|
|
490
|
-
|
|
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
|
-
<
|
|
594
|
-
|
|
595
|
-
|
|
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={() =>
|
|
600
|
-
|
|
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
|
-
<
|
|
792
|
-
|
|
793
|
-
|
|
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
|
-
<
|
|
833
|
-
|
|
834
|
-
|
|
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
|
-
|
|
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
|
|
40
|
-
//
|
|
41
|
-
//
|
|
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
|
|
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:
|
|
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(
|
|
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 {
|
|
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
|
-
//
|
|
11
|
-
//
|
|
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
|
+
}
|