castle-web-cli 0.4.71 → 0.4.73
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 +22 -3
- package/dist/agent.js +731 -313
- package/dist/init.js +1 -1
- package/dist/shell/assets/index-Dfn29Bkt.js +108 -0
- package/dist/shell/assets/{index-CVEnWuGV.css → index-WNbOHPBj.css} +1 -1
- package/dist/shell/index.html +2 -2
- package/dist/vitePlugins.js +3 -2
- package/kits/basic-2d/CLAUDE.md +29 -8
- package/kits/basic-2d/behaviors/Collider.jsx +6 -4
- package/kits/basic-2d/behaviors/Layout.jsx +2 -2
- package/kits/basic-2d/behaviors/Sprite.jsx +210 -0
- package/kits/basic-2d/behaviors/tint.js +47 -0
- package/kits/basic-2d/docs/pxart-format.md +298 -0
- package/kits/basic-2d/drawings/pig.pxart +59 -0
- package/kits/basic-2d/editors/App.jsx +125 -76
- package/kits/basic-2d/editors/CodeEditor.jsx +9 -45
- package/kits/basic-2d/editors/FileBrowser.jsx +234 -47
- package/kits/basic-2d/editors/PlayOnly.jsx +9 -7
- package/kits/basic-2d/editors/PxArtEditor.jsx +662 -0
- package/kits/basic-2d/editors/SceneEditor.jsx +587 -221
- package/kits/basic-2d/editors/SelectionOverlay.jsx +808 -0
- package/kits/basic-2d/editors/SingleEditor.jsx +38 -20
- package/kits/basic-2d/editors/codeTheme.js +135 -0
- package/kits/basic-2d/editors/editorHistory.js +44 -17
- package/kits/basic-2d/editors/inspectorSheet.js +23 -0
- package/kits/basic-2d/editors/pixelCanvas.js +11 -0
- package/kits/basic-2d/editors/pixelEditorChrome.jsx +55 -0
- package/kits/basic-2d/editors/pixelGeometry.js +45 -0
- package/kits/basic-2d/editors/pixelInspector.jsx +416 -0
- package/kits/basic-2d/editors/pxArtEditorModel.js +718 -0
- package/kits/basic-2d/editors/pxArtPlayback.js +92 -0
- package/kits/basic-2d/editors/pxArtTimeline.jsx +752 -0
- package/kits/basic-2d/editors/pxArtTimeline.module.css +506 -0
- package/kits/basic-2d/editors/pxArtTools.js +124 -0
- package/kits/basic-2d/editors/useArtboardFit.js +102 -0
- package/kits/basic-2d/engine/ScenePlayer.jsx +10 -4
- package/kits/basic-2d/engine/SceneUI.jsx +3 -11
- package/kits/basic-2d/engine/assets.js +15 -0
- package/kits/basic-2d/engine/files.js +57 -2
- package/kits/basic-2d/engine/pxart.js +985 -0
- package/kits/basic-2d/engine/scene.js +222 -41
- package/kits/basic-2d/engine/ui.jsx +155 -26
- package/kits/basic-2d/engine/ui.module.css +1280 -344
- package/kits/basic-2d/eslint.config.js +21 -0
- package/kits/basic-2d/index.html +13 -0
- package/kits/basic-2d/package.json +1 -0
- package/kits/basic-2d/pnpm-lock.yaml +5 -5
- package/kits/basic-2d/scenes/main.scene +19 -26
- package/kits/basic-2d/scripts/draw.mjs +121 -0
- package/kits/basic-3d/editors/PlayOnly.jsx +9 -1
- package/kits/basic-3d/engine/ScenePlayer.jsx +7 -1
- package/package.json +1 -1
- package/dist/shell/assets/index-BY21Og40.js +0 -106
- package/kits/basic-2d/behaviors/Drawing.jsx +0 -142
- package/kits/basic-2d/drawings/block.drawing +0 -70
- package/kits/basic-2d/drawings/default.drawing +0 -70
- package/kits/basic-2d/editors/DrawingEditor.jsx +0 -224
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { useCallback, useEffect, useLayoutEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
const MAX_ART = 460;
|
|
4
|
+
const STACK_GAP = 10;
|
|
5
|
+
|
|
6
|
+
// Matches @container editor (max-width) in ui.module.css.
|
|
7
|
+
export const PXART_COMPACT_MAX_WIDTH = 620;
|
|
8
|
+
|
|
9
|
+
// True when .editorBody is at or below the compact-layout container threshold.
|
|
10
|
+
export function useEditorCompactLayout(bodyRef, maxWidth = PXART_COMPACT_MAX_WIDTH) {
|
|
11
|
+
const [compact, setCompact] = useState(false);
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
const body = bodyRef.current;
|
|
15
|
+
if (!body || typeof ResizeObserver === 'undefined') return undefined;
|
|
16
|
+
|
|
17
|
+
const update = () => setCompact(body.clientWidth <= maxWidth);
|
|
18
|
+
update();
|
|
19
|
+
const observer = new ResizeObserver(update);
|
|
20
|
+
observer.observe(body);
|
|
21
|
+
return () => observer.disconnect();
|
|
22
|
+
}, [bodyRef, maxWidth]);
|
|
23
|
+
|
|
24
|
+
return compact;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Fit the artboard canvas inside its wrap region, accounting for padding, the
|
|
28
|
+
// canvas-size bar above the art, and the sprite's aspect ratio. Re-runs when the
|
|
29
|
+
// wrap resizes (timeline drag, window resize, compact layout flip).
|
|
30
|
+
export function useArtboardFit({ wrapRef, sizeBarRef, resolution }) {
|
|
31
|
+
const { width, height } = resolution;
|
|
32
|
+
const isWide = width >= height;
|
|
33
|
+
const [displaySize, setDisplaySize] = useState(null);
|
|
34
|
+
|
|
35
|
+
const fit = useCallback(() => {
|
|
36
|
+
const wrap = wrapRef.current;
|
|
37
|
+
if (!wrap) return;
|
|
38
|
+
const wrapStyle = getComputedStyle(wrap);
|
|
39
|
+
const padX = parseFloat(wrapStyle.paddingLeft) + parseFloat(wrapStyle.paddingRight);
|
|
40
|
+
const padY = parseFloat(wrapStyle.paddingTop) + parseFloat(wrapStyle.paddingBottom);
|
|
41
|
+
const barHeight = sizeBarRef.current?.offsetHeight ?? 0;
|
|
42
|
+
const maxW = wrap.clientWidth - padX;
|
|
43
|
+
const maxH = wrap.clientHeight - padY - barHeight - (barHeight ? STACK_GAP : 0);
|
|
44
|
+
if (maxW <= 0 || maxH <= 0) {
|
|
45
|
+
setDisplaySize(null);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let displayW;
|
|
50
|
+
let displayH;
|
|
51
|
+
if (width === height) {
|
|
52
|
+
const size = Math.floor(Math.min(maxW, maxH, MAX_ART));
|
|
53
|
+
displayW = size;
|
|
54
|
+
displayH = size;
|
|
55
|
+
} else if (isWide) {
|
|
56
|
+
displayW = Math.floor(Math.min(maxW, MAX_ART));
|
|
57
|
+
displayH = Math.floor((displayW * height) / width);
|
|
58
|
+
if (displayH > maxH) {
|
|
59
|
+
displayH = Math.floor(maxH);
|
|
60
|
+
displayW = Math.floor((displayH * width) / height);
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
displayH = Math.floor(Math.min(maxH, MAX_ART));
|
|
64
|
+
displayW = Math.floor((displayH * width) / height);
|
|
65
|
+
if (displayW > maxW) {
|
|
66
|
+
displayW = Math.floor(maxW);
|
|
67
|
+
displayH = Math.floor((displayW * height) / width);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!Number.isFinite(displayW) || !Number.isFinite(displayH) || displayW <= 0 || displayH <= 0) {
|
|
72
|
+
setDisplaySize(null);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
setDisplaySize({ width: displayW, height: displayH });
|
|
76
|
+
}, [width, height, isWide, wrapRef, sizeBarRef]);
|
|
77
|
+
|
|
78
|
+
useLayoutEffect(() => {
|
|
79
|
+
fit();
|
|
80
|
+
}, [fit]);
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
const wrap = wrapRef.current;
|
|
84
|
+
if (!wrap) return undefined;
|
|
85
|
+
window.addEventListener('resize', fit);
|
|
86
|
+
if (typeof ResizeObserver === 'undefined') {
|
|
87
|
+
return () => window.removeEventListener('resize', fit);
|
|
88
|
+
}
|
|
89
|
+
const observer = new ResizeObserver(fit);
|
|
90
|
+
observer.observe(wrap);
|
|
91
|
+
return () => {
|
|
92
|
+
observer.disconnect();
|
|
93
|
+
window.removeEventListener('resize', fit);
|
|
94
|
+
};
|
|
95
|
+
}, [fit, wrapRef]);
|
|
96
|
+
|
|
97
|
+
const canvasStyle = displaySize
|
|
98
|
+
? { width: `${displaySize.width}px`, height: `${displaySize.height}px` }
|
|
99
|
+
: undefined;
|
|
100
|
+
|
|
101
|
+
return { canvasStyle, refit: fit };
|
|
102
|
+
}
|
|
@@ -6,7 +6,7 @@ import { TouchControls } from './TouchControls';
|
|
|
6
6
|
// keyboard / pointer input, run the update+draw loop, and render the
|
|
7
7
|
// behavior-driven UI overlay. No game logic lives here -- behaviors and
|
|
8
8
|
// scenes are the place for that.
|
|
9
|
-
export function ScenePlayer({ sceneData,
|
|
9
|
+
export function ScenePlayer({ sceneData, sprites, behaviorClasses, onFirstFrame }) {
|
|
10
10
|
const canvasRef = useRef(null);
|
|
11
11
|
const runtimeRef = useRef(null);
|
|
12
12
|
const getKeys = useCallback(() => runtimeRef.current?.keys ?? null, []);
|
|
@@ -17,7 +17,7 @@ export function ScenePlayer({ sceneData, drawings, behaviorClasses }) {
|
|
|
17
17
|
const ctx = canvas.getContext('2d');
|
|
18
18
|
if (!ctx) return undefined;
|
|
19
19
|
configureSceneCanvas(canvas, ctx);
|
|
20
|
-
const runtime = makeScene(sceneData, behaviorClasses,
|
|
20
|
+
const runtime = makeScene(sceneData, behaviorClasses, sprites).clone();
|
|
21
21
|
runtimeRef.current = runtime;
|
|
22
22
|
// Store BOTH the physical code ('KeyX', 'ArrowLeft', 'Space') and the
|
|
23
23
|
// logical key ('x', 'ArrowLeft', ' ') so behaviors can match either. Codes
|
|
@@ -56,7 +56,7 @@ export function ScenePlayer({ sceneData, drawings, behaviorClasses }) {
|
|
|
56
56
|
canvas.addEventListener('pointerup', onPointerUp);
|
|
57
57
|
canvas.addEventListener('pointercancel', onPointerUp);
|
|
58
58
|
canvas.focus();
|
|
59
|
-
const stopLoop = startPlayerLoop(canvas, ctx, runtime);
|
|
59
|
+
const stopLoop = startPlayerLoop(canvas, ctx, runtime, onFirstFrame);
|
|
60
60
|
return () => {
|
|
61
61
|
stopLoop();
|
|
62
62
|
window.removeEventListener('keydown', onKeyDown);
|
|
@@ -81,15 +81,21 @@ export function ScenePlayer({ sceneData, drawings, behaviorClasses }) {
|
|
|
81
81
|
</div>
|
|
82
82
|
);
|
|
83
83
|
}
|
|
84
|
-
function startPlayerLoop(canvas, ctx, runtime) {
|
|
84
|
+
function startPlayerLoop(canvas, ctx, runtime, onFirstFrame) {
|
|
85
85
|
let raf = 0;
|
|
86
86
|
let previousTime = performance.now();
|
|
87
|
+
let firstFrameSignaled = false;
|
|
87
88
|
const tick = (now) => {
|
|
88
89
|
const dt = Math.min(0.033, (now - previousTime) / 1000);
|
|
89
90
|
previousTime = now;
|
|
90
91
|
runtime.update(dt);
|
|
91
92
|
configureSceneCanvas(canvas, ctx);
|
|
92
93
|
runtime.draw(ctx, { useCamera: true });
|
|
94
|
+
if (!firstFrameSignaled) {
|
|
95
|
+
firstFrameSignaled = true;
|
|
96
|
+
// Wait one frame so the draw composites before reveal, avoiding a blank flash.
|
|
97
|
+
requestAnimationFrame(() => onFirstFrame?.());
|
|
98
|
+
}
|
|
93
99
|
raf = requestAnimationFrame(tick);
|
|
94
100
|
};
|
|
95
101
|
raf = requestAnimationFrame(tick);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { useEffect, useRef, useState } from 'react';
|
|
2
2
|
import styles from './ui.module.css';
|
|
3
|
+
import { useElementSize } from './ui';
|
|
3
4
|
import { cardSize } from './scene';
|
|
4
5
|
// Game-time UI overlay.
|
|
5
6
|
//
|
|
@@ -11,18 +12,9 @@ import { cardSize } from './scene';
|
|
|
11
12
|
// hidden`, so deck UI can never render outside the card.
|
|
12
13
|
export function SceneUI({ getRuntime }) {
|
|
13
14
|
const rootRef = useRef(null);
|
|
14
|
-
const [box, setBox] = useState({ width: 0, height: 0 });
|
|
15
|
-
const [, forceRender] = useState(0);
|
|
16
15
|
// Track the canvas-sized overlay box so the card-unit layer scales onto it.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if (!root) return undefined;
|
|
20
|
-
const measure = () => setBox({ width: root.clientWidth, height: root.clientHeight });
|
|
21
|
-
const observer = new ResizeObserver(measure);
|
|
22
|
-
observer.observe(root);
|
|
23
|
-
measure();
|
|
24
|
-
return () => observer.disconnect();
|
|
25
|
-
}, []);
|
|
16
|
+
const box = useElementSize(rootRef);
|
|
17
|
+
const [, forceRender] = useState(0);
|
|
26
18
|
// Re-render every frame so behavior UI reflects live runtime state.
|
|
27
19
|
useEffect(() => {
|
|
28
20
|
let raf = 0;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { parseFull } from './pxart';
|
|
2
|
+
|
|
3
|
+
// Build the runtime asset map from a path->text file map: parsed `.pxart`
|
|
4
|
+
// Sprites keyed by file path (nulls skipped). Shared by the editor App and the
|
|
5
|
+
// play-only entry point so both hand the SceneRuntime the same map.
|
|
6
|
+
export function collectAssets(files) {
|
|
7
|
+
const sprites = {};
|
|
8
|
+
for (const [path, text] of Object.entries(files)) {
|
|
9
|
+
if (path.endsWith('.pxart')) {
|
|
10
|
+
const sprite = parseFull(text);
|
|
11
|
+
if (sprite) sprites[path] = sprite;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return { sprites };
|
|
15
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Seed the file map by scanning the deck dir. Vite module-cache is invalidated
|
|
2
2
|
// on restart, so a newly-created file just shows up on the next reload.
|
|
3
3
|
const rawModules = import.meta.glob(
|
|
4
|
-
['../scenes/*.scene', '../drawings/*.
|
|
4
|
+
['../scenes/*.scene', '../drawings/*.pxart', '../behaviors/*.jsx'],
|
|
5
5
|
{ query: '?raw', import: 'default', eager: true }
|
|
6
6
|
);
|
|
7
7
|
export const initialFiles = Object.fromEntries(
|
|
@@ -11,7 +11,7 @@ export const initialFiles = Object.fromEntries(
|
|
|
11
11
|
);
|
|
12
12
|
export function getFileKind(path) {
|
|
13
13
|
if (path.endsWith('.scene')) return 'scene';
|
|
14
|
-
if (path.endsWith('.
|
|
14
|
+
if (path.endsWith('.pxart')) return 'pxart';
|
|
15
15
|
if (path.endsWith('.js') || path.endsWith('.jsx')) return 'code';
|
|
16
16
|
return 'text';
|
|
17
17
|
}
|
|
@@ -29,6 +29,61 @@ export function formatJson(value) {
|
|
|
29
29
|
export function basename(path) {
|
|
30
30
|
return path.split('/').pop() ?? path;
|
|
31
31
|
}
|
|
32
|
+
// Parent directory of a path ('' for a root-level path).
|
|
33
|
+
export function dirname(path) {
|
|
34
|
+
const index = path.lastIndexOf('/');
|
|
35
|
+
return index === -1 ? '' : path.slice(0, index);
|
|
36
|
+
}
|
|
37
|
+
// Join a directory and a name, tolerating a '' (root) directory.
|
|
38
|
+
export function joinPath(dir, name) {
|
|
39
|
+
return dir ? `${dir}/${name}` : name;
|
|
40
|
+
}
|
|
41
|
+
// Split a filename into stem + extension (the extension includes the leading
|
|
42
|
+
// dot). Leading-dot names (e.g. '.gitignore') count as all-stem.
|
|
43
|
+
export function splitFileName(name) {
|
|
44
|
+
const dot = name.lastIndexOf('.');
|
|
45
|
+
if (dot <= 0) return { stem: name, ext: '' };
|
|
46
|
+
return { stem: name.slice(0, dot), ext: name.slice(dot) };
|
|
47
|
+
}
|
|
48
|
+
// A filename in `dir` that doesn't collide with any existing path. If
|
|
49
|
+
// `desiredName` is free it's returned as-is; otherwise '-2', '-3', ... is
|
|
50
|
+
// inserted before the extension until a free name is found.
|
|
51
|
+
export function uniqueFileName(existingPaths, dir, desiredName) {
|
|
52
|
+
const taken = new Set(existingPaths);
|
|
53
|
+
if (!taken.has(joinPath(dir, desiredName))) return desiredName;
|
|
54
|
+
const { stem, ext } = splitFileName(desiredName);
|
|
55
|
+
for (let n = 2; ; n++) {
|
|
56
|
+
const candidate = `${stem}-${n}${ext}`;
|
|
57
|
+
if (!taken.has(joinPath(dir, candidate))) return candidate;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Top-level child segment names (files and virtual folders) directly inside
|
|
61
|
+
// `parentDir`. Used to keep new sibling folder names collision-free.
|
|
62
|
+
export function childNamesIn(existingPaths, parentDir) {
|
|
63
|
+
const prefix = parentDir ? `${parentDir}/` : '';
|
|
64
|
+
const names = new Set();
|
|
65
|
+
for (const path of existingPaths) {
|
|
66
|
+
if (prefix && !path.startsWith(prefix)) continue;
|
|
67
|
+
const segment = path.slice(prefix.length).split('/')[0];
|
|
68
|
+
if (segment) names.add(segment);
|
|
69
|
+
}
|
|
70
|
+
return names;
|
|
71
|
+
}
|
|
72
|
+
// A folder name inside `parentDir` that doesn't collide with an existing
|
|
73
|
+
// sibling file or folder. Appends '-2', '-3', ... until free.
|
|
74
|
+
export function uniqueFolderName(existingPaths, parentDir, desiredName) {
|
|
75
|
+
const taken = childNamesIn(existingPaths, parentDir);
|
|
76
|
+
if (!taken.has(desiredName)) return desiredName;
|
|
77
|
+
for (let n = 2; ; n++) {
|
|
78
|
+
const candidate = `${desiredName}-${n}`;
|
|
79
|
+
if (!taken.has(candidate)) return candidate;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// All file paths under a virtual folder (paths prefixed with `dirPath/`).
|
|
83
|
+
export function filesUnder(existingPaths, dirPath) {
|
|
84
|
+
const prefix = `${dirPath}/`;
|
|
85
|
+
return existingPaths.filter((path) => path.startsWith(prefix));
|
|
86
|
+
}
|
|
32
87
|
// Flat list of file paths in the order FileBrowser renders them: a
|
|
33
88
|
// depth-first walk of the directory tree, mirroring buildFileTree there.
|
|
34
89
|
export function flatFileOrder(paths) {
|