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
|
@@ -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.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Deck-palette picker for `theme.style`. Official entries come from
|
|
2
|
+
// OFFICIAL_PALETTES; a custom `{ name, colors }` in the file shows as a
|
|
3
|
+
// read-only selected card. Opening the editor never writes — only an
|
|
4
|
+
// explicit official pick does.
|
|
5
|
+
|
|
6
|
+
import { createElement } from 'react';
|
|
7
|
+
import { OFFICIAL_PALETTES, parseThemeData, resolveDeckPalette } from '../engine/palettes';
|
|
8
|
+
import { EditorBody } from '../engine/ui';
|
|
9
|
+
import { applyOfficialPalette } from './styleTheme';
|
|
10
|
+
import styles from './styleEditor.module.css';
|
|
11
|
+
|
|
12
|
+
function cardClassName(selected, clickable) {
|
|
13
|
+
return [styles.card, selected ? styles.cardSelected : '', clickable ? '' : styles.cardStatic]
|
|
14
|
+
.filter(Boolean)
|
|
15
|
+
.join(' ');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function swatchStrip(colors) {
|
|
19
|
+
return (
|
|
20
|
+
<div className={styles.strip} aria-hidden="true">
|
|
21
|
+
{colors.map((hex, index) => (
|
|
22
|
+
<span key={`${hex}:${index}`} className={styles.chip} style={{ backgroundColor: hex }} />
|
|
23
|
+
))}
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function paletteCard({ name, colors, selected, onPick, badge, cardKey }) {
|
|
29
|
+
const className = cardClassName(selected, Boolean(onPick));
|
|
30
|
+
const body = (
|
|
31
|
+
<>
|
|
32
|
+
<div className={styles.cardHead}>
|
|
33
|
+
<div className={styles.cardName}>{name}</div>
|
|
34
|
+
<div className={styles.cardMeta}>
|
|
35
|
+
{badge ? <span className={styles.badge}>{badge}</span> : null}
|
|
36
|
+
<span>{colors.length} colors</span>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
{swatchStrip(colors)}
|
|
40
|
+
</>
|
|
41
|
+
);
|
|
42
|
+
if (!onPick) {
|
|
43
|
+
return (
|
|
44
|
+
<div key={cardKey} className={className}>
|
|
45
|
+
{body}
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
return (
|
|
50
|
+
<button key={cardKey} type="button" className={className} onClick={onPick} aria-pressed={selected}>
|
|
51
|
+
{body}
|
|
52
|
+
</button>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function customCardName(palette) {
|
|
57
|
+
const name = palette?.name?.trim();
|
|
58
|
+
return name && name !== 'custom' ? name : 'Custom';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function StyleEditor({ text, onChange }) {
|
|
62
|
+
const current = resolveDeckPalette(parseThemeData(text));
|
|
63
|
+
const isCustom = current.id == null;
|
|
64
|
+
function pick(id) {
|
|
65
|
+
onChange(applyOfficialPalette(text, id));
|
|
66
|
+
}
|
|
67
|
+
return createElement(
|
|
68
|
+
EditorBody,
|
|
69
|
+
null,
|
|
70
|
+
<div className={styles.root}>
|
|
71
|
+
{isCustom
|
|
72
|
+
? paletteCard({
|
|
73
|
+
name: customCardName(current),
|
|
74
|
+
colors: current.colors,
|
|
75
|
+
selected: true,
|
|
76
|
+
badge: 'Custom',
|
|
77
|
+
})
|
|
78
|
+
: null}
|
|
79
|
+
{isCustom ? (
|
|
80
|
+
<p className={styles.replaceNote}>Picking an official palette replaces this custom palette.</p>
|
|
81
|
+
) : null}
|
|
82
|
+
<div className={styles.list}>
|
|
83
|
+
{Object.values(OFFICIAL_PALETTES).map((palette) =>
|
|
84
|
+
paletteCard({
|
|
85
|
+
cardKey: palette.id,
|
|
86
|
+
name: palette.name,
|
|
87
|
+
colors: palette.colors,
|
|
88
|
+
selected: current.id === palette.id,
|
|
89
|
+
onPick: () => pick(palette.id),
|
|
90
|
+
})
|
|
91
|
+
)}
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
@@ -171,7 +171,7 @@ function drawFillToolPreview(ctx, path, ui, at, sx, sy) {
|
|
|
171
171
|
if (!shape) return;
|
|
172
172
|
|
|
173
173
|
ctx.save();
|
|
174
|
-
if (target.slot === 'fill'
|
|
174
|
+
if (target.slot === 'fill') {
|
|
175
175
|
const geometry = buildShapeFillPath2D(shape, at);
|
|
176
176
|
if (geometry) {
|
|
177
177
|
ctx.fillStyle = color;
|
|
@@ -813,12 +813,20 @@ export function setEdgeBow(path, shapeIndex, subIndex, edgeIndex, bow) {
|
|
|
813
813
|
});
|
|
814
814
|
}
|
|
815
815
|
|
|
816
|
+
/**
|
|
817
|
+
* Fill-tool target at (x,y): stroke first, then interior, top-most shape down.
|
|
818
|
+
*
|
|
819
|
+
* An interior counts whether or not the shape already has a fill, so the tool
|
|
820
|
+
* ADDS one to a stroke-only shape — which also means an unfilled shape is not a
|
|
821
|
+
* hole here the way it is for `hitTestShape`. Only closed subpaths bound a fill,
|
|
822
|
+
* so an all-open shape has no interior to hit even if it still carries a key.
|
|
823
|
+
*/
|
|
816
824
|
export function hitTestFillToolTarget(path, x, y) {
|
|
817
825
|
const shapes = path?.shapes ?? [];
|
|
818
826
|
for (let i = shapes.length - 1; i >= 0; i--) {
|
|
819
827
|
const shape = shapes[i];
|
|
820
828
|
if (nearStrokedOutline(shape, x, y)) return { shapeIndex: i, slot: 'stroke' };
|
|
821
|
-
if (
|
|
829
|
+
if (shapeContainsPoint(shape, x, y)) return { shapeIndex: i, slot: 'fill' };
|
|
822
830
|
}
|
|
823
831
|
return null;
|
|
824
832
|
}
|
|
@@ -107,15 +107,16 @@ function edgeCross(a, b, cx, cy) {
|
|
|
107
107
|
return (b.x - a.x) * (cy - a.y) - (b.y - a.y) * (cx - a.x);
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
// Visit the cells of
|
|
111
|
-
//
|
|
112
|
-
// barycentric point-in-triangle test over the
|
|
113
|
-
// edges. A shared `seen` set dedups cells
|
|
110
|
+
// Visit the cells of a right triangle whose hypotenuse is the drag `from`→`to`
|
|
111
|
+
// and whose right angle sits at (from.x, to.y) — same corner rule as the path
|
|
112
|
+
// brush triangle. `filled` uses a barycentric point-in-triangle test over the
|
|
113
|
+
// box; the outline walks the three edges. A shared `seen` set dedups cells
|
|
114
|
+
// shared by adjacent edges/scanlines.
|
|
114
115
|
export function forEachTriangleCells(from, to, filled, visit) {
|
|
115
116
|
const { x0, x1, y0, y1 } = shapeBounds(from, to);
|
|
116
|
-
const
|
|
117
|
-
const
|
|
118
|
-
const
|
|
117
|
+
const start = { x: from.x, y: from.y };
|
|
118
|
+
const right = { x: from.x, y: to.y };
|
|
119
|
+
const end = { x: to.x, y: to.y };
|
|
119
120
|
const seen = new Set();
|
|
120
121
|
const emit = (x, y) => {
|
|
121
122
|
const key = `${x},${y}`;
|
|
@@ -126,9 +127,9 @@ export function forEachTriangleCells(from, to, filled, visit) {
|
|
|
126
127
|
if (filled) {
|
|
127
128
|
for (let y = y0; y <= y1; y++) {
|
|
128
129
|
for (let x = x0; x <= x1; x++) {
|
|
129
|
-
const w0 = edgeCross(
|
|
130
|
-
const w1 = edgeCross(
|
|
131
|
-
const w2 = edgeCross(
|
|
130
|
+
const w0 = edgeCross(right, end, x, y);
|
|
131
|
+
const w1 = edgeCross(end, start, x, y);
|
|
132
|
+
const w2 = edgeCross(start, right, x, y);
|
|
132
133
|
const hasNeg = w0 < 0 || w1 < 0 || w2 < 0;
|
|
133
134
|
const hasPos = w0 > 0 || w1 > 0 || w2 > 0;
|
|
134
135
|
if (!(hasNeg && hasPos)) emit(x, y);
|
|
@@ -136,7 +137,7 @@ export function forEachTriangleCells(from, to, filled, visit) {
|
|
|
136
137
|
}
|
|
137
138
|
return;
|
|
138
139
|
}
|
|
139
|
-
forEachLinePoint(
|
|
140
|
-
forEachLinePoint(
|
|
141
|
-
forEachLinePoint(
|
|
140
|
+
forEachLinePoint(start, right, emit);
|
|
141
|
+
forEachLinePoint(right, end, emit);
|
|
142
|
+
forEachLinePoint(end, start, emit);
|
|
142
143
|
}
|