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.
- package/dist/castleJson.d.ts +2 -0
- package/dist/castleJson.js +39 -0
- package/dist/headlessCover.d.ts +13 -0
- package/dist/headlessCover.js +99 -0
- package/dist/ide.js +12 -42
- package/dist/imports.js +42 -2
- package/dist/index.js +1 -9
- package/dist/init.d.ts +1 -0
- package/dist/init.js +15 -1
- 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/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-CARLHafh.css +0 -1
- package/dist/shell/assets/index-DWgt4KzC.js +0 -434
package/dist/shell/index.html
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
<link rel="icon" type="image/png" sizes="32x32" href="/__castle/ide/favicon-32x32.png" />
|
|
11
11
|
<link rel="icon" type="image/png" sizes="16x16" href="/__castle/ide/favicon-16x16.png" />
|
|
12
12
|
<link rel="icon" href="/__castle/ide/favicon.ico" sizes="any" />
|
|
13
|
-
<script type="module" crossorigin src="/__castle/ide/assets/index-
|
|
14
|
-
<link rel="stylesheet" crossorigin href="/__castle/ide/assets/index-
|
|
13
|
+
<script type="module" crossorigin src="/__castle/ide/assets/index-u0nYFqbF.js"></script>
|
|
14
|
+
<link rel="stylesheet" crossorigin href="/__castle/ide/assets/index-BfOPkSej.css">
|
|
15
15
|
</head>
|
|
16
16
|
<body>
|
|
17
17
|
<div id="root"></div>
|
|
@@ -448,11 +448,11 @@ Physics is just this kit's use of it.
|
|
|
448
448
|
Don't hand-write pixel grids. Generate sprites with the `draw` command: **emit a terse svg-rect** — a tiny `<svg viewBox="0 0 16 16">` with **one `<rect>` per pixel** — and pipe it to `npm run draw`. It quantizes every fill to the agent palette and writes a single-frame `drawings/<name>.sprite`.
|
|
449
449
|
|
|
450
450
|
- **Resolution is 16×16.** Use `viewBox="0 0 16 16"` and **one 1×1 `<rect>` per filled pixel** (`width="1" height="1"` at integer `x`/`y`). Leave background pixels out (omitted = transparent). Keep every rect 1×1 — the decoder infers cell size from the smallest rect, so mixing in larger blocks shrinks the whole grid.
|
|
451
|
-
- **Palette is the
|
|
451
|
+
- **Palette is the deck's generation palette.** The default (no `theme.style`, or Endesga-64) is this 16-color subset — every `fill` must be one of:
|
|
452
452
|
|
|
453
453
|
`#e69c69` `#bf6f4a` `#8a4836` `#391f21` `#891e2b` `#ea323c` `#ffa214` `#ffeb57` `#5ac54f` `#1e6f50` `#134c4c` `#657392` `#c7cfdd` `#ffffff` `#0cf1ff` `#0098dc`
|
|
454
454
|
|
|
455
|
-
|
|
455
|
+
If the deck defines a palette in `theme.style`, that governs instead. `npm run palette` prints the active generation palette. Off-palette fills snap to the nearest of these, so stay on-palette to keep what you intend. People editing in the app paint from the deck palette (and the sprite's own colors); only this generation path is quantized.
|
|
456
456
|
- **Run it** (sprite name as the arg; stdin is the svg):
|
|
457
457
|
|
|
458
458
|
```bash
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
"scenes/**",
|
|
24
24
|
"blueprints/**",
|
|
25
25
|
"behaviors/**",
|
|
26
|
-
"assets/**"
|
|
26
|
+
"assets/**",
|
|
27
|
+
"theme.style"
|
|
27
28
|
],
|
|
28
29
|
"fileTypes": [
|
|
29
30
|
{
|
|
@@ -55,6 +56,13 @@
|
|
|
55
56
|
"new": "New behavior",
|
|
56
57
|
"icon": "code"
|
|
57
58
|
},
|
|
59
|
+
{
|
|
60
|
+
"ext": ".style",
|
|
61
|
+
"label": "Style",
|
|
62
|
+
"icon": "file",
|
|
63
|
+
"editor": "kit",
|
|
64
|
+
"data": true
|
|
65
|
+
},
|
|
58
66
|
{
|
|
59
67
|
"ext": ".png",
|
|
60
68
|
"label": "Image",
|
|
@@ -123,5 +131,5 @@
|
|
|
123
131
|
"main": "main.jsx",
|
|
124
132
|
"autoUpdateWhenImported": true,
|
|
125
133
|
"title": "physics-2d",
|
|
126
|
-
"publishedVersion": "2026-08-
|
|
134
|
+
"publishedVersion": "2026-08-18T21:05:46.663Z"
|
|
127
135
|
}
|
|
@@ -88,36 +88,42 @@ temporal versions — that the kit treats as one in-memory model:
|
|
|
88
88
|
transparent.
|
|
89
89
|
- Indexed-with-hex only: **not** RGBA-per-pixel, **not** grayscale.
|
|
90
90
|
|
|
91
|
-
###
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
picking
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
91
|
+
### Deck palette + derived agent subset
|
|
92
|
+
|
|
93
|
+
Each sprite file carries its **own** ordered `{ key, hex }` palette. That list
|
|
94
|
+
is truth: opening a sprite for editing never remaps its colors. The editor
|
|
95
|
+
paints with those keys, and picking a new color **allocates** a key into the
|
|
96
|
+
sprite's palette (`KEY_ALPHABET`, capped at 64) rather than snapping existing
|
|
97
|
+
pixels onto a global swatch.
|
|
98
|
+
|
|
99
|
+
The **deck palette** is a picker source, not a normalization target. It lives
|
|
100
|
+
in `theme.style` at the deck root (`engine/palettes.js`). Missing or malformed
|
|
101
|
+
theme data falls back to Lospec's [Endesga 64](https://lospec.com/palette-list/endesga-64)
|
|
102
|
+
(`EDG64`). The inspector picker is an unlabeled row of the sprite's own colors
|
|
103
|
+
(when that palette is non-empty) plus an 8-column grid of official palettes,
|
|
104
|
+
paged with named arrows. The deck's `theme.style` palette is the default page;
|
|
105
|
+
paging does not write the theme. A custom `{ name, colors }` deck adds one
|
|
106
|
+
extra read-only page. Picking a hex still allocates onto the sprite.
|
|
107
|
+
|
|
108
|
+
**Agent subset — what the LLM generates with.** The generation path (`draw`
|
|
109
|
+
svg-rect → `.sprite`, and `import-svg`) quantizes to a derived subset of the
|
|
110
|
+
deck palette (`agentPaletteFor`):
|
|
111
|
+
|
|
112
|
+
- default Endesga-64 → the hand-curated `AGENT_PALETTE_16`
|
|
113
|
+
- custom palette larger than 16 → greedy farthest-point subset in Oklab
|
|
114
|
+
- 16 or fewer colors → used as-is
|
|
115
|
+
|
|
116
|
+
`npm run palette` prints both the deck palette and the derived generation
|
|
117
|
+
subset. The default 16, in agent-facing order (every entry is a member of
|
|
118
|
+
`EDG64`):
|
|
112
119
|
|
|
113
120
|
```
|
|
114
121
|
#e69c69 #bf6f4a #8a4836 #391f21 #891e2b #ea323c #ffa214 #ffeb57
|
|
115
122
|
#5ac54f #1e6f50 #134c4c #657392 #c7cfdd #ffffff #0cf1ff #0098dc
|
|
116
123
|
```
|
|
117
124
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
`engine/pxart.js`.
|
|
125
|
+
`EDG64` and `AGENT_PALETTE_16` live in `engine/pxart.js`; derivation and
|
|
126
|
+
theme resolution live in `engine/palettes.js`.
|
|
121
127
|
|
|
122
128
|
---
|
|
123
129
|
|
|
@@ -656,8 +662,9 @@ system treats it as a full (degenerate) Sprite.
|
|
|
656
662
|
### Single-layer / single-frame generation contract
|
|
657
663
|
|
|
658
664
|
The **generation contract** for this kit is to emit the **compact shorthand**: a
|
|
659
|
-
single 16×16 (default) `{ palette, grid }` sprite
|
|
660
|
-
(`
|
|
665
|
+
single 16×16 (default) `{ palette, grid }` sprite quantized to the deck's
|
|
666
|
+
generation palette (`agentPaletteFor` of the `theme.style` palette, or
|
|
667
|
+
`AGENT_PALETTE_16` when the deck uses the Endesga-64 default).
|
|
661
668
|
The CLI generation worker and the kit's `PxArtEditor` both produce/store this
|
|
662
669
|
compact form when nothing is lost by it. Multi-layer / multi-frame structure is
|
|
663
670
|
authored by tools later, not hand-generated — and the minimal `PxArtEditor`
|
|
@@ -7,6 +7,15 @@ import {
|
|
|
7
7
|
renderSmoothSpriteFrame,
|
|
8
8
|
} from '../engine/pxartSmooth';
|
|
9
9
|
import { basename } from '../engine/files';
|
|
10
|
+
import {
|
|
11
|
+
allocateSpriteColor,
|
|
12
|
+
deckPaletteFromFiles,
|
|
13
|
+
hexForPaletteKey,
|
|
14
|
+
paletteRecord,
|
|
15
|
+
pickerColorsFor,
|
|
16
|
+
pickerPageIndexFor,
|
|
17
|
+
pickerPagesFor,
|
|
18
|
+
} from '../engine/palettes';
|
|
10
19
|
import { EditorBody, Icon, IconButton, styles } from '../engine/ui';
|
|
11
20
|
import { eventToCell, eventToPoint } from './pixelCanvas';
|
|
12
21
|
import { forEachDab } from './pixelGeometry';
|
|
@@ -84,9 +93,6 @@ import {
|
|
|
84
93
|
import { useArtboardFit, useEditorCompactLayout } from './useArtboardFit';
|
|
85
94
|
import { useTwoFingerZoomPan } from '../engine/gestureZoomPan';
|
|
86
95
|
import {
|
|
87
|
-
DEFAULT_KEY,
|
|
88
|
-
EDITOR_KEYS,
|
|
89
|
-
EDITOR_PALETTE,
|
|
90
96
|
clearRegion,
|
|
91
97
|
fillRegion,
|
|
92
98
|
flipCellsH,
|
|
@@ -137,8 +143,8 @@ import tl from './pxArtTimeline.module.css';
|
|
|
137
143
|
// Aseprite-style editor for the full .pxart sprite format: a full layers x frames
|
|
138
144
|
// timeline below a compositing artboard. The artboard composites the active
|
|
139
145
|
// frame's visible layers (renderSpriteFrame) so the artist sees the whole image
|
|
140
|
-
// while painting targets the active layer's cell, over
|
|
141
|
-
// palette and power-of-two resolutions. Sprites that lose nothing in the compact
|
|
146
|
+
// while painting targets the active layer's cell, over the sprite's own palette
|
|
147
|
+
// (deck palette is a picker) and power-of-two resolutions. Sprites that lose nothing in the compact
|
|
142
148
|
// form serialize to compact; anything richer serializes to the full form.
|
|
143
149
|
const ONION_WARM = '#ff7a3c';
|
|
144
150
|
const ONION_COOL = '#3ca0ff';
|
|
@@ -189,7 +195,7 @@ const PATH_ONLY_TOOL_IDS = new Set(
|
|
|
189
195
|
)
|
|
190
196
|
);
|
|
191
197
|
|
|
192
|
-
export function PxArtEditor({ path, text, onChange, ...chrome }) {
|
|
198
|
+
export function PxArtEditor({ path, text, onChange, files, ...chrome }) {
|
|
193
199
|
const canvasRef = useRef(null);
|
|
194
200
|
const smoothRef = useRef(null);
|
|
195
201
|
const overlayRef = useRef(null);
|
|
@@ -199,7 +205,7 @@ export function PxArtEditor({ path, text, onChange, ...chrome }) {
|
|
|
199
205
|
// edits apart from external rewrites (undo/redo) so the marquee can be dropped
|
|
200
206
|
// when the art changes out from under it.
|
|
201
207
|
const lastSelfTextRef = useRef(text);
|
|
202
|
-
const [
|
|
208
|
+
const [activeHex, setActiveHex] = useState(null);
|
|
203
209
|
const [rawSelection, setRawSelection] = useState({ layerIndex: 0, frameIndex: 0 });
|
|
204
210
|
const [playing, setPlaying] = useState(false);
|
|
205
211
|
const [onion, setOnion] = useState({ enabled: false, range: 1 });
|
|
@@ -263,6 +269,29 @@ export function PxArtEditor({ path, text, onChange, ...chrome }) {
|
|
|
263
269
|
const [paletteOpen, setPaletteOpen] = useState(false);
|
|
264
270
|
|
|
265
271
|
const sprite = deriveSprite(text);
|
|
272
|
+
const deckPalette = useMemo(() => deckPaletteFromFiles(files), [files]);
|
|
273
|
+
const deckHexes = useMemo(() => pickerColorsFor(deckPalette.colors), [deckPalette]);
|
|
274
|
+
const pages = useMemo(() => pickerPagesFor(deckPalette), [deckPalette]);
|
|
275
|
+
const [pageIndex, setPageIndex] = useState(() => pickerPageIndexFor(pages, deckPalette));
|
|
276
|
+
const onPage = useCallback((delta) => {
|
|
277
|
+
setPageIndex((index) => {
|
|
278
|
+
const n = pages.length;
|
|
279
|
+
return n ? (index + delta + n) % n : 0;
|
|
280
|
+
});
|
|
281
|
+
}, [pages.length]);
|
|
282
|
+
const workingPalette = useMemo(() => paletteRecord(sprite?.palette), [sprite]);
|
|
283
|
+
const workingKeys = useMemo(() => (sprite?.palette ?? []).map((entry) => entry.key), [sprite]);
|
|
284
|
+
const paintReady = useMemo(
|
|
285
|
+
() => (sprite && activeHex ? allocateSpriteColor(sprite, activeHex) : { sprite, key: null }),
|
|
286
|
+
[sprite, activeHex]
|
|
287
|
+
);
|
|
288
|
+
const paintKey = paintReady.key;
|
|
289
|
+
// Derived, not stored: the note shows exactly while the active color can't be
|
|
290
|
+
// allocated, and clears itself when undo or a palette edit frees a slot.
|
|
291
|
+
const paletteFull = paintReady.error === 'palette-full';
|
|
292
|
+
useEffect(() => {
|
|
293
|
+
if (!activeHex && deckHexes[0]) setActiveHex(deckHexes[0]);
|
|
294
|
+
}, [activeHex, deckHexes]);
|
|
266
295
|
const selection = sprite
|
|
267
296
|
? clampSelection(sprite, rawSelection.layerIndex, rawSelection.frameIndex)
|
|
268
297
|
: rawSelection;
|
|
@@ -308,7 +337,7 @@ export function PxArtEditor({ path, text, onChange, ...chrome }) {
|
|
|
308
337
|
picking,
|
|
309
338
|
brushSize,
|
|
310
339
|
eraseSize,
|
|
311
|
-
|
|
340
|
+
activeHex,
|
|
312
341
|
pathBakePreview,
|
|
313
342
|
});
|
|
314
343
|
const { canvasStyle, displaySize } = useArtboardFit({
|
|
@@ -577,7 +606,7 @@ export function PxArtEditor({ path, text, onChange, ...chrome }) {
|
|
|
577
606
|
drawPathOverlay(overlayRef.current, sprite?.resolution, playing ? null : activePath, {
|
|
578
607
|
tool,
|
|
579
608
|
brushMode,
|
|
580
|
-
palette:
|
|
609
|
+
palette: workingPalette,
|
|
581
610
|
brushInk,
|
|
582
611
|
brushGhost,
|
|
583
612
|
hoverPoint,
|
|
@@ -589,7 +618,7 @@ export function PxArtEditor({ path, text, onChange, ...chrome }) {
|
|
|
589
618
|
hoverEdge,
|
|
590
619
|
shapeMarquee,
|
|
591
620
|
fillHoverTarget,
|
|
592
|
-
activeColor:
|
|
621
|
+
activeColor: activeHex,
|
|
593
622
|
smoothFinish: isVectorRenderer(sprite?.renderer),
|
|
594
623
|
});
|
|
595
624
|
} else {
|
|
@@ -605,7 +634,7 @@ export function PxArtEditor({ path, text, onChange, ...chrome }) {
|
|
|
605
634
|
picking,
|
|
606
635
|
brushSize,
|
|
607
636
|
eraseSize,
|
|
608
|
-
|
|
637
|
+
activeHex,
|
|
609
638
|
playing,
|
|
610
639
|
});
|
|
611
640
|
renderPixelOverlay(overlayRef.current, sprite?.resolution, {
|
|
@@ -634,7 +663,8 @@ export function PxArtEditor({ path, text, onChange, ...chrome }) {
|
|
|
634
663
|
hoverEdge,
|
|
635
664
|
shapeMarquee,
|
|
636
665
|
fillHoverTarget,
|
|
637
|
-
|
|
666
|
+
paintKey,
|
|
667
|
+
activeHex,
|
|
638
668
|
hoverCell,
|
|
639
669
|
picking,
|
|
640
670
|
brushSize,
|
|
@@ -747,7 +777,7 @@ export function PxArtEditor({ path, text, onChange, ...chrome }) {
|
|
|
747
777
|
text,
|
|
748
778
|
selection.layerIndex,
|
|
749
779
|
selection.frameIndex,
|
|
750
|
-
|
|
780
|
+
paintKey,
|
|
751
781
|
activePath,
|
|
752
782
|
]);
|
|
753
783
|
// Shift/Alt during a brush primitive drag only update on pointermove unless
|
|
@@ -851,8 +881,8 @@ export function PxArtEditor({ path, text, onChange, ...chrome }) {
|
|
|
851
881
|
stroke.draftCells = nextCells;
|
|
852
882
|
liveSprite(withCellsAt(stroke.base, stroke.layerIndex, stroke.frameIndex, nextCells));
|
|
853
883
|
}
|
|
854
|
-
function commitCells(nextCells) {
|
|
855
|
-
const nextText = serializeModel(withCellsAt(
|
|
884
|
+
function commitCells(nextCells, base = sprite) {
|
|
885
|
+
const nextText = serializeModel(withCellsAt(base, selection.layerIndex, selection.frameIndex, nextCells));
|
|
856
886
|
lastSelfTextRef.current = nextText;
|
|
857
887
|
history.commit(nextText);
|
|
858
888
|
}
|
|
@@ -901,12 +931,12 @@ export function PxArtEditor({ path, text, onChange, ...chrome }) {
|
|
|
901
931
|
history.cancelSnapshot();
|
|
902
932
|
}
|
|
903
933
|
}
|
|
904
|
-
function commitPath(nextPath, options) {
|
|
934
|
+
function commitPath(nextPath, options, base = sprite) {
|
|
905
935
|
// Hold the invariant that an edge exists once, after any edit rather than
|
|
906
936
|
// only the one that happened to produce a duplicate. Skipped mid-limb: a
|
|
907
937
|
// half-drawn run can lie on the outline on its way somewhere else.
|
|
908
938
|
const settled = penLimbRef.current ? nextPath : absorbRetracedSubpaths(nextPath);
|
|
909
|
-
const nextText = serializeModel(withPathAt(
|
|
939
|
+
const nextText = serializeModel(withPathAt(base, selection.layerIndex, selection.frameIndex, settled));
|
|
910
940
|
lastSelfTextRef.current = nextText;
|
|
911
941
|
history.commit(nextText, options);
|
|
912
942
|
}
|
|
@@ -956,7 +986,7 @@ export function PxArtEditor({ path, text, onChange, ...chrome }) {
|
|
|
956
986
|
}
|
|
957
987
|
function refitBrushGhost(ink, halfPixel = false) {
|
|
958
988
|
const fit = fitBrushInk(ink, { halfPixel, cellPull: pathCellPull });
|
|
959
|
-
const shape = fit ? shapeFromBrushFit(fit,
|
|
989
|
+
const shape = fit ? shapeFromBrushFit(fit, paintKey) : null;
|
|
960
990
|
// Cyan ghost stays visible even when the bake blends into existing pixels.
|
|
961
991
|
// Skip clearing on a null fit so the preview doesn't blink in the
|
|
962
992
|
// degenerate snap cell around the gesture start.
|
|
@@ -966,7 +996,7 @@ export function PxArtEditor({ path, text, onChange, ...chrome }) {
|
|
|
966
996
|
function refitBrushPrimitiveGhost(start, end, halfPixel = false, constrain = false) {
|
|
967
997
|
const stroke = strokeRef.current;
|
|
968
998
|
const mode = stroke?.brushMode ?? brushMode;
|
|
969
|
-
const shape = ghostFromBrushDrag(mode, start, end, { halfPixel, constrain, activeKey, cellPull: pathCellPull });
|
|
999
|
+
const shape = ghostFromBrushDrag(mode, start, end, { halfPixel, constrain, activeKey: paintKey, cellPull: pathCellPull });
|
|
970
1000
|
if (shape) setBrushGhost(shape);
|
|
971
1001
|
liveBrushBake(shape);
|
|
972
1002
|
}
|
|
@@ -978,14 +1008,14 @@ export function PxArtEditor({ path, text, onChange, ...chrome }) {
|
|
|
978
1008
|
let shape = null;
|
|
979
1009
|
if (stroke.brushMode === 'freehand') {
|
|
980
1010
|
const fit = fitBrushInk(brushInkRef.current, { halfPixel, cellPull: pathCellPull });
|
|
981
|
-
shape = fit ? shapeFromBrushFit(fit,
|
|
1011
|
+
shape = fit ? shapeFromBrushFit(fit, paintKey) : null;
|
|
982
1012
|
} else {
|
|
983
1013
|
const start = stroke.startPoint;
|
|
984
1014
|
const end = endPoint ?? stroke.lastPoint ?? start;
|
|
985
1015
|
shape = shapeFromBrushDrag(stroke.brushMode, start, end, {
|
|
986
1016
|
halfPixel,
|
|
987
1017
|
constrain,
|
|
988
|
-
activeKey,
|
|
1018
|
+
activeKey: paintKey,
|
|
989
1019
|
cellPull: pathCellPull,
|
|
990
1020
|
});
|
|
991
1021
|
}
|
|
@@ -1052,7 +1082,7 @@ export function PxArtEditor({ path, text, onChange, ...chrome }) {
|
|
|
1052
1082
|
return appendAnchorToSubpath(path, target.shapeIndex, target.subIndex, [sx, sy]);
|
|
1053
1083
|
}
|
|
1054
1084
|
|
|
1055
|
-
return appendShape(path, shapeFromPenDraft([[sx, sy]], { closed: false, activeKey }));
|
|
1085
|
+
return appendShape(path, shapeFromPenDraft([[sx, sy]], { closed: false, activeKey: paintKey }));
|
|
1056
1086
|
}
|
|
1057
1087
|
function updatePenHoverBake(point, halfPixel) {
|
|
1058
1088
|
const now = performance.now();
|
|
@@ -1356,13 +1386,25 @@ export function PxArtEditor({ path, text, onChange, ...chrome }) {
|
|
|
1356
1386
|
if (!selectedShapeIndices.length) return;
|
|
1357
1387
|
commitPath(rotateShapes90CW(activePath, selectedShapeIndices));
|
|
1358
1388
|
},
|
|
1359
|
-
setFill: (
|
|
1389
|
+
setFill: (hex) => {
|
|
1360
1390
|
if (!selectedShapeIndices.length) return;
|
|
1361
|
-
|
|
1391
|
+
if (hex == null) {
|
|
1392
|
+
commitPath(setShapesFill(activePath, selectedShapeIndices, null));
|
|
1393
|
+
return;
|
|
1394
|
+
}
|
|
1395
|
+
const allocated = allocateSpriteColor(sprite, hex);
|
|
1396
|
+
if (!allocated.key) return;
|
|
1397
|
+
commitPath(setShapesFill(activePath, selectedShapeIndices, allocated.key), undefined, allocated.sprite);
|
|
1362
1398
|
},
|
|
1363
|
-
setStroke: (
|
|
1399
|
+
setStroke: (hex) => {
|
|
1364
1400
|
if (!selectedShapeIndices.length) return;
|
|
1365
|
-
|
|
1401
|
+
if (hex == null) {
|
|
1402
|
+
commitPath(setShapesStroke(activePath, selectedShapeIndices, null));
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1405
|
+
const allocated = allocateSpriteColor(sprite, hex);
|
|
1406
|
+
if (!allocated.key) return;
|
|
1407
|
+
commitPath(setShapesStroke(activePath, selectedShapeIndices, allocated.key), undefined, allocated.sprite);
|
|
1366
1408
|
},
|
|
1367
1409
|
};
|
|
1368
1410
|
function startPenStroke(point, event) {
|
|
@@ -1426,11 +1468,12 @@ export function PxArtEditor({ path, text, onChange, ...chrome }) {
|
|
|
1426
1468
|
}
|
|
1427
1469
|
|
|
1428
1470
|
// Start a new open stroked shape at this anchor.
|
|
1429
|
-
|
|
1471
|
+
if (!paintReady.key) return;
|
|
1472
|
+
const shape = shapeFromPenDraft([[sx, sy]], { closed: false, activeKey: paintKey });
|
|
1430
1473
|
const next = appendShape(activePath, shape);
|
|
1431
1474
|
const top = (next.shapes?.length ?? 1) - 1;
|
|
1432
1475
|
penStubIndexRef.current = top;
|
|
1433
|
-
commitPath(next);
|
|
1476
|
+
commitPath(next, undefined, paintReady.sprite);
|
|
1434
1477
|
setSelectedShapeIndices([top]);
|
|
1435
1478
|
setSelectedAnchors([]);
|
|
1436
1479
|
}
|
|
@@ -1697,8 +1740,9 @@ export function PxArtEditor({ path, text, onChange, ...chrome }) {
|
|
|
1697
1740
|
startPenStroke(point, event);
|
|
1698
1741
|
return;
|
|
1699
1742
|
}
|
|
1743
|
+
if (!paintReady.key) return;
|
|
1700
1744
|
const { halfPixel, constrain } = brushModifiersFromEvent(event);
|
|
1701
|
-
const base = sprite;
|
|
1745
|
+
const base = paintReady.sprite;
|
|
1702
1746
|
const baseText = serializeModel(sprite);
|
|
1703
1747
|
const path = activePath;
|
|
1704
1748
|
if (brushMode === 'freehand') {
|
|
@@ -1764,8 +1808,9 @@ export function PxArtEditor({ path, text, onChange, ...chrome }) {
|
|
|
1764
1808
|
}
|
|
1765
1809
|
function startPathTool(point, event) {
|
|
1766
1810
|
if (tool === 'fill') {
|
|
1767
|
-
|
|
1768
|
-
|
|
1811
|
+
if (!paintReady.key) return;
|
|
1812
|
+
const next = recolorTopShapeAt(activePath, point.x, point.y, paintReady.key);
|
|
1813
|
+
if (next !== activePath) commitPath(next, undefined, paintReady.sprite);
|
|
1769
1814
|
} else if (tool === 'brush') {
|
|
1770
1815
|
startBrushStroke(point, event);
|
|
1771
1816
|
} else if (tool === 'path-select') {
|
|
@@ -1786,7 +1831,7 @@ export function PxArtEditor({ path, text, onChange, ...chrome }) {
|
|
|
1786
1831
|
const point = eventToPoint(event, width, height);
|
|
1787
1832
|
if (picking) {
|
|
1788
1833
|
const cells = cellsAt(sprite, selection.layerIndex, selection.frameIndex);
|
|
1789
|
-
|
|
1834
|
+
pickSampledKey(
|
|
1790
1835
|
sampleKey(cells, {
|
|
1791
1836
|
x: Math.min(width - 1, Math.floor(point.x)),
|
|
1792
1837
|
y: Math.min(height - 1, Math.floor(point.y)),
|
|
@@ -1806,7 +1851,7 @@ export function PxArtEditor({ path, text, onChange, ...chrome }) {
|
|
|
1806
1851
|
// engaged it intercepts the pick, leaves the underlying tool untouched, and
|
|
1807
1852
|
// exits picker mode so the previously selected tool resumes after one pick.
|
|
1808
1853
|
if (picking) {
|
|
1809
|
-
|
|
1854
|
+
pickSampledKey(sampleKey(cells, point));
|
|
1810
1855
|
setPicking(false);
|
|
1811
1856
|
return;
|
|
1812
1857
|
}
|
|
@@ -1833,8 +1878,11 @@ export function PxArtEditor({ path, text, onChange, ...chrome }) {
|
|
|
1833
1878
|
return;
|
|
1834
1879
|
}
|
|
1835
1880
|
if (tool === 'fill') {
|
|
1836
|
-
|
|
1837
|
-
|
|
1881
|
+
if (!paintReady.key) return;
|
|
1882
|
+
const next = fillSwapMode
|
|
1883
|
+
? swapKey(cells, point, paintReady.key)
|
|
1884
|
+
: fillRegion(cells, point, paintReady.key);
|
|
1885
|
+
if (next !== cells) commitCells(next, paintReady.sprite);
|
|
1838
1886
|
return;
|
|
1839
1887
|
}
|
|
1840
1888
|
if (tool === 'erase' && eraseFillMode) {
|
|
@@ -1842,11 +1890,13 @@ export function PxArtEditor({ path, text, onChange, ...chrome }) {
|
|
|
1842
1890
|
if (next !== cells) commitCells(next);
|
|
1843
1891
|
return;
|
|
1844
1892
|
}
|
|
1893
|
+
if ((tool === 'brush' || tool === 'shape') && !paintReady.key) return;
|
|
1845
1894
|
strokeRef.current = {
|
|
1846
1895
|
tool,
|
|
1847
1896
|
startPoint: point,
|
|
1848
1897
|
lastPoint: point,
|
|
1849
|
-
base: sprite,
|
|
1898
|
+
base: paintReady.key ? paintReady.sprite : sprite,
|
|
1899
|
+
paintKey: paintReady.key,
|
|
1850
1900
|
layerIndex: selection.layerIndex,
|
|
1851
1901
|
frameIndex: selection.frameIndex,
|
|
1852
1902
|
// Offset of the cel at gesture start, so move applies an ABSOLUTE offset
|
|
@@ -1963,7 +2013,7 @@ export function PxArtEditor({ path, text, onChange, ...chrome }) {
|
|
|
1963
2013
|
return;
|
|
1964
2014
|
}
|
|
1965
2015
|
if (stroke.tool === 'brush' && stroke.brushMode == null) {
|
|
1966
|
-
liveStroke(paintLine(stroke.draftCells, stroke.lastPoint, point,
|
|
2016
|
+
liveStroke(paintLine(stroke.draftCells, stroke.lastPoint, point, stroke.paintKey, brushSize));
|
|
1967
2017
|
stroke.lastPoint = point;
|
|
1968
2018
|
} else if (stroke.tool === 'erase') {
|
|
1969
2019
|
liveStroke(paintLine(stroke.draftCells, stroke.lastPoint, point, TRANSPARENT, eraseSize));
|
|
@@ -1971,7 +2021,7 @@ export function PxArtEditor({ path, text, onChange, ...chrome }) {
|
|
|
1971
2021
|
} else if (stroke.tool === 'shape') {
|
|
1972
2022
|
// Recompute from the gesture-start snapshot so the shape rubber-bands to
|
|
1973
2023
|
// the current point rather than stacking each intermediate draft.
|
|
1974
|
-
liveStroke(paintShape(stroke.originCells, stroke.startPoint, point,
|
|
2024
|
+
liveStroke(paintShape(stroke.originCells, stroke.startPoint, point, stroke.paintKey, shapeMode, shapeFill));
|
|
1975
2025
|
} else if (stroke.tool === 'move-all') {
|
|
1976
2026
|
// Translate the cel's OFFSET by the drag delta instead of shifting and
|
|
1977
2027
|
// clipping pixels — the full cel image is preserved, so pushing art off
|
|
@@ -2163,8 +2213,18 @@ export function PxArtEditor({ path, text, onChange, ...chrome }) {
|
|
|
2163
2213
|
playing,
|
|
2164
2214
|
setOnion,
|
|
2165
2215
|
});
|
|
2216
|
+
function pickSampledKey(key) {
|
|
2217
|
+
if (key === TRANSPARENT) return;
|
|
2218
|
+
const hex = hexForPaletteKey(sprite?.palette, key);
|
|
2219
|
+
if (hex) setActiveHex(hex);
|
|
2220
|
+
}
|
|
2166
2221
|
function selectPaletteKey(key) {
|
|
2167
|
-
|
|
2222
|
+
const hex = workingPalette[key];
|
|
2223
|
+
if (hex) setActiveHex(hex);
|
|
2224
|
+
}
|
|
2225
|
+
function selectPaletteHex(hex) {
|
|
2226
|
+
if (!hex) return;
|
|
2227
|
+
setActiveHex(hex);
|
|
2168
2228
|
}
|
|
2169
2229
|
|
|
2170
2230
|
return (
|
|
@@ -2253,14 +2313,19 @@ export function PxArtEditor({ path, text, onChange, ...chrome }) {
|
|
|
2253
2313
|
<div className={styles.paintColumn}>
|
|
2254
2314
|
<PaintStrip
|
|
2255
2315
|
toolState={toolState}
|
|
2256
|
-
activeKey={
|
|
2257
|
-
activeColor={
|
|
2316
|
+
activeKey={paintKey}
|
|
2317
|
+
activeColor={activeHex}
|
|
2258
2318
|
paletteOpen={paletteOpen}
|
|
2259
2319
|
onTogglePalette={togglePalette}
|
|
2260
2320
|
colorButtonRef={colorButtonRef}
|
|
2261
2321
|
onSelectKey={selectPaletteKey}
|
|
2262
|
-
|
|
2263
|
-
|
|
2322
|
+
onSelectHex={selectPaletteHex}
|
|
2323
|
+
paletteKeys={workingKeys}
|
|
2324
|
+
palette={workingPalette}
|
|
2325
|
+
pages={pages}
|
|
2326
|
+
pageIndex={pageIndex}
|
|
2327
|
+
onPage={onPage}
|
|
2328
|
+
paletteFull={paletteFull}
|
|
2264
2329
|
moveActions={moveActions}
|
|
2265
2330
|
selectActions={selectActions}
|
|
2266
2331
|
pathActions={isPathLayer ? pathActions : null}
|
|
@@ -2291,10 +2356,16 @@ export function PxArtEditor({ path, text, onChange, ...chrome }) {
|
|
|
2291
2356
|
</div>
|
|
2292
2357
|
<PxArtInspectorDock
|
|
2293
2358
|
toolState={toolState}
|
|
2294
|
-
activeKey={
|
|
2359
|
+
activeKey={paintKey}
|
|
2360
|
+
activeHex={activeHex}
|
|
2295
2361
|
onSelectKey={selectPaletteKey}
|
|
2296
|
-
|
|
2297
|
-
|
|
2362
|
+
onSelectHex={selectPaletteHex}
|
|
2363
|
+
paletteKeys={workingKeys}
|
|
2364
|
+
palette={workingPalette}
|
|
2365
|
+
pages={pages}
|
|
2366
|
+
pageIndex={pageIndex}
|
|
2367
|
+
onPage={onPage}
|
|
2368
|
+
paletteFull={paletteFull}
|
|
2298
2369
|
moveActions={moveActions}
|
|
2299
2370
|
selectActions={selectActions}
|
|
2300
2371
|
pathActions={isPathLayer ? pathActions : null}
|
|
@@ -2399,7 +2470,7 @@ function useArtboardRender(canvasRef, text, sprite, previewIndex, onion, playing
|
|
|
2399
2470
|
p.picking,
|
|
2400
2471
|
p.brushSize,
|
|
2401
2472
|
p.eraseSize,
|
|
2402
|
-
p.
|
|
2473
|
+
p.activeHex,
|
|
2403
2474
|
p.selection.layerIndex,
|
|
2404
2475
|
p.selection.frameIndex,
|
|
2405
2476
|
p.pathBakePreview,
|
|
@@ -2731,9 +2802,9 @@ function fillPreviewCells(cells, start) {
|
|
|
2731
2802
|
|
|
2732
2803
|
// Describe the overlay to draw for the hovered cell, or null when nothing should
|
|
2733
2804
|
// show (no hover, playback, or the move-all tool which has no footprint).
|
|
2734
|
-
function buildToolPreview({ sprite, selection, hoverCell, tool, picking, brushSize, eraseSize,
|
|
2805
|
+
function buildToolPreview({ sprite, selection, hoverCell, tool, picking, brushSize, eraseSize, activeHex, playing }) {
|
|
2735
2806
|
if (!hoverCell || playing) return null;
|
|
2736
|
-
const color =
|
|
2807
|
+
const color = activeHex || '#ffffff';
|
|
2737
2808
|
if (picking) return { kind: 'cell', cell: hoverCell };
|
|
2738
2809
|
if (tool === 'brush') return { kind: 'footprint', cell: hoverCell, size: brushSize, color };
|
|
2739
2810
|
if (tool === 'erase') return { kind: 'erase', cell: hoverCell, size: eraseSize };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Single-editor mount for the panel system. The shell (dockview) opens this as
|
|
2
2
|
// its own iframe via the deck route in main.jsx:
|
|
3
3
|
// ?file=<path>[&editor=<id>] -> <SingleEditor> (one editor for one file)
|
|
4
|
-
// The shell only routes the kit's RICH file types here (scene / pxart);
|
|
5
|
-
// browsing and the code/text editor are now builtin shell panels.
|
|
4
|
+
// The shell only routes the kit's RICH file types here (scene / pxart / style);
|
|
5
|
+
// file browsing and the code/text editor are now builtin shell panels.
|
|
6
6
|
|
|
7
7
|
import React, { useEffect, useRef, useState } from 'react';
|
|
8
8
|
import { onBeforeRestart, onSaveReloadState, takeReloadState, writeFile } from 'castle-web-sdk';
|
|
@@ -15,6 +15,7 @@ import { ImageViewer } from './ImageViewer';
|
|
|
15
15
|
import { MediaPlayer } from './MediaPlayer';
|
|
16
16
|
import { PxArtEditor } from './PxArtEditor';
|
|
17
17
|
import { SceneEditor } from './SceneEditor';
|
|
18
|
+
import { StyleEditor } from './StyleEditor';
|
|
18
19
|
|
|
19
20
|
// Debounced writeFile per path, flushed on reload -- the same save behavior the
|
|
20
21
|
// combined editor (App.jsx) uses, factored for a single-editor mount.
|
|
@@ -153,7 +154,9 @@ export function SingleEditor({ path, editor, editorModule }) {
|
|
|
153
154
|
/>
|
|
154
155
|
);
|
|
155
156
|
} else if (kind === 'pxart') {
|
|
156
|
-
body = <PxArtEditor path={path} text={text} onChange={onChange} />;
|
|
157
|
+
body = <PxArtEditor path={path} text={text} onChange={onChange} files={files} />;
|
|
158
|
+
} else if (kind === 'style') {
|
|
159
|
+
body = <StyleEditor path={path} text={text} onChange={onChange} />;
|
|
157
160
|
} else if (kind === 'image') {
|
|
158
161
|
// Viewers read their file's BYTES over the serve, not the text file map --
|
|
159
162
|
// `files` holds source, and an image decoded as utf-8 is nothing.
|