castle-web-cli 0.4.72 → 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.
Files changed (57) hide show
  1. package/dist/agent-prompts.js +22 -3
  2. package/dist/agent.js +731 -313
  3. package/dist/init.js +1 -1
  4. package/dist/shell/assets/index-Dfn29Bkt.js +108 -0
  5. package/dist/shell/assets/{index-CVEnWuGV.css → index-WNbOHPBj.css} +1 -1
  6. package/dist/shell/index.html +2 -2
  7. package/dist/vitePlugins.js +3 -2
  8. package/kits/basic-2d/CLAUDE.md +29 -8
  9. package/kits/basic-2d/behaviors/Collider.jsx +6 -4
  10. package/kits/basic-2d/behaviors/Layout.jsx +2 -2
  11. package/kits/basic-2d/behaviors/Sprite.jsx +210 -0
  12. package/kits/basic-2d/behaviors/tint.js +47 -0
  13. package/kits/basic-2d/docs/pxart-format.md +298 -0
  14. package/kits/basic-2d/drawings/pig.pxart +59 -0
  15. package/kits/basic-2d/editors/App.jsx +125 -76
  16. package/kits/basic-2d/editors/CodeEditor.jsx +9 -45
  17. package/kits/basic-2d/editors/FileBrowser.jsx +234 -47
  18. package/kits/basic-2d/editors/PlayOnly.jsx +9 -7
  19. package/kits/basic-2d/editors/PxArtEditor.jsx +662 -0
  20. package/kits/basic-2d/editors/SceneEditor.jsx +587 -221
  21. package/kits/basic-2d/editors/SelectionOverlay.jsx +808 -0
  22. package/kits/basic-2d/editors/SingleEditor.jsx +38 -20
  23. package/kits/basic-2d/editors/codeTheme.js +135 -0
  24. package/kits/basic-2d/editors/editorHistory.js +44 -17
  25. package/kits/basic-2d/editors/inspectorSheet.js +23 -0
  26. package/kits/basic-2d/editors/pixelCanvas.js +11 -0
  27. package/kits/basic-2d/editors/pixelEditorChrome.jsx +55 -0
  28. package/kits/basic-2d/editors/pixelGeometry.js +45 -0
  29. package/kits/basic-2d/editors/pixelInspector.jsx +416 -0
  30. package/kits/basic-2d/editors/pxArtEditorModel.js +718 -0
  31. package/kits/basic-2d/editors/pxArtPlayback.js +92 -0
  32. package/kits/basic-2d/editors/pxArtTimeline.jsx +752 -0
  33. package/kits/basic-2d/editors/pxArtTimeline.module.css +506 -0
  34. package/kits/basic-2d/editors/pxArtTools.js +124 -0
  35. package/kits/basic-2d/editors/useArtboardFit.js +102 -0
  36. package/kits/basic-2d/engine/ScenePlayer.jsx +10 -4
  37. package/kits/basic-2d/engine/SceneUI.jsx +3 -11
  38. package/kits/basic-2d/engine/assets.js +15 -0
  39. package/kits/basic-2d/engine/files.js +57 -2
  40. package/kits/basic-2d/engine/pxart.js +985 -0
  41. package/kits/basic-2d/engine/scene.js +222 -41
  42. package/kits/basic-2d/engine/ui.jsx +155 -26
  43. package/kits/basic-2d/engine/ui.module.css +1280 -344
  44. package/kits/basic-2d/eslint.config.js +21 -0
  45. package/kits/basic-2d/index.html +13 -0
  46. package/kits/basic-2d/package.json +1 -0
  47. package/kits/basic-2d/pnpm-lock.yaml +5 -5
  48. package/kits/basic-2d/scenes/main.scene +19 -26
  49. package/kits/basic-2d/scripts/draw.mjs +121 -0
  50. package/kits/basic-3d/editors/PlayOnly.jsx +9 -1
  51. package/kits/basic-3d/engine/ScenePlayer.jsx +7 -1
  52. package/package.json +1 -1
  53. package/dist/shell/assets/index-vmdKwUE1.js +0 -106
  54. package/kits/basic-2d/behaviors/Drawing.jsx +0 -142
  55. package/kits/basic-2d/drawings/block.drawing +0 -70
  56. package/kits/basic-2d/drawings/default.drawing +0 -70
  57. package/kits/basic-2d/editors/DrawingEditor.jsx +0 -224
@@ -1,142 +0,0 @@
1
- import React from 'react';
2
- import { Panel, SelectField } from '../engine/ui';
3
- import { AutoFields } from '../engine/autoInspector';
4
-
5
- export class Drawing {
6
- static behaviorName = 'Drawing';
7
-
8
- static defaultProps = {
9
- file: 'drawings/default.drawing',
10
- tint: '#ffffffff',
11
- };
12
-
13
- constructor(props) {
14
- this.props = props;
15
- }
16
-
17
- draw(actor, scene, ctx) {
18
- if (actor.runtime?.collected) return;
19
- const layout = actor.components.Layout;
20
- if (!layout) return;
21
- // Fall back to default.drawing (a plain block) when the named file does not
22
- // exist yet -- so a scene can reference drawings/foo.drawing before the
23
- // drawing task has created it, and it shows a block until the real art lands.
24
- const drawing = scene.drawings[this.props.file] ?? scene.drawings['drawings/default.drawing'];
25
- if (!drawing) return;
26
- drawPixelDrawing(
27
- ctx,
28
- drawing,
29
- layout.x,
30
- layout.y,
31
- layout.width,
32
- layout.height,
33
- this.props.tint
34
- );
35
- }
36
-
37
- static Inspector({ component, setComponent, files }) {
38
- const drawingFiles = getDrawingFiles(files);
39
- return (
40
- <Panel title="Drawing">
41
- <SelectField
42
- label="File"
43
- value={component.file}
44
- onChange={(file) => setComponent({ file })}
45
- options={drawingFiles}
46
- />
47
- <AutoFields
48
- defaultProps={Drawing.defaultProps}
49
- component={component}
50
- setComponent={setComponent}
51
- only={['tint']}
52
- />
53
- </Panel>
54
- );
55
- }
56
- }
57
-
58
- export function drawPixelDrawing(ctx, drawing, x, y, width, height, tint) {
59
- if (drawing.width <= 0 || drawing.height <= 0) return;
60
- // Blit the cached native-resolution canvas as a single image. Painting each
61
- // pixel as its own `fillRect` left hairline anti-aliased seams under a
62
- // rotation transform; one `drawImage` has no inter-pixel edges.
63
- const canvas = getDrawingCanvas(drawing, tint);
64
- const prevSmoothing = ctx.imageSmoothingEnabled;
65
- ctx.imageSmoothingEnabled = false;
66
- ctx.drawImage(canvas, x, y, width, height);
67
- ctx.imageSmoothingEnabled = prevSmoothing;
68
- }
69
-
70
- // Cache of native-resolution offscreen canvases keyed by drawing data and
71
- // tint. A drawing edit produces a fresh `DrawingData` object (see
72
- // DrawingEditor's `structuredClone`), so a new object naturally misses the
73
- // WeakMap and rebuilds.
74
- const drawingCanvasCache = new WeakMap();
75
-
76
- // Render the pixel art once, unrotated, at 1px per pixel into an offscreen
77
- // canvas, applying the tint. Cached so the per-frame draw is a single blit.
78
- function getDrawingCanvas(drawing, tint) {
79
- const tintKey = tint ?? '';
80
- let byTint = drawingCanvasCache.get(drawing);
81
- if (!byTint) {
82
- byTint = new Map();
83
- drawingCanvasCache.set(drawing, byTint);
84
- }
85
- const cached = byTint.get(tintKey);
86
- if (cached) return cached;
87
-
88
- const canvas = document.createElement('canvas');
89
- canvas.width = drawing.width;
90
- canvas.height = drawing.height;
91
- const octx = canvas.getContext('2d');
92
- if (octx) {
93
- const tintRgba = parseTint(tint);
94
- for (let py = 0; py < drawing.height; py++) {
95
- for (let px = 0; px < drawing.width; px++) {
96
- const color = drawing.pixels[py * drawing.width + px];
97
- if (!color || color === 'transparent' || color.endsWith('00')) continue;
98
- octx.fillStyle = tintRgba ? applyTint(color, tintRgba) : color;
99
- octx.fillRect(px, py, 1, 1);
100
- }
101
- }
102
- }
103
- byTint.set(tintKey, canvas);
104
- return canvas;
105
- }
106
-
107
- // Parse a hex color into [r, g, b, a] channels (0-255). Returns null when the
108
- // string is not a hex color so callers can fall back to the raw color.
109
- function parseHexColor(color) {
110
- const hex = color.trim().replace(/^#/, '');
111
- if (hex.length !== 6 && hex.length !== 8) return null;
112
- if (!/^[0-9a-fA-F]+$/.test(hex)) return null;
113
- const r = parseInt(hex.slice(0, 2), 16);
114
- const g = parseInt(hex.slice(2, 4), 16);
115
- const b = parseInt(hex.slice(4, 6), 16);
116
- const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) : 255;
117
- return [r, g, b, a];
118
- }
119
-
120
- // Returns the tint as [r, g, b, a] channels, or null when there is no tint or
121
- // the tint is white (#ffffffff) -- both meaning "no change".
122
- function parseTint(tint) {
123
- if (!tint) return null;
124
- const rgba = parseHexColor(tint);
125
- if (!rgba) return null;
126
- if (rgba[0] === 255 && rgba[1] === 255 && rgba[2] === 255 && rgba[3] === 255) {
127
- return null;
128
- }
129
- return rgba;
130
- }
131
-
132
- // Multiply each channel of a pixel color by the tint color (both 0-255).
133
- function applyTint(color, tint) {
134
- const rgba = parseHexColor(color);
135
- if (!rgba) return color;
136
- const out = rgba.map((channel, i) => Math.round((channel * tint[i]) / 255));
137
- return '#' + out.map((channel) => channel.toString(16).padStart(2, '0')).join('');
138
- }
139
-
140
- function getDrawingFiles(files) {
141
- return Object.keys(files).filter((path) => path.endsWith('.drawing'));
142
- }
@@ -1,70 +0,0 @@
1
- {
2
- "width": 8,
3
- "height": 8,
4
- "pixels": [
5
- "#ffffffff",
6
- "#ffffffff",
7
- "#ffffffff",
8
- "#ffffffff",
9
- "#ffffffff",
10
- "#ffffffff",
11
- "#ffffffff",
12
- "#ffffffff",
13
- "#ffffffff",
14
- "#d8d8d8ff",
15
- "#d8d8d8ff",
16
- "#d8d8d8ff",
17
- "#d8d8d8ff",
18
- "#d8d8d8ff",
19
- "#d8d8d8ff",
20
- "#ffffffff",
21
- "#ffffffff",
22
- "#d8d8d8ff",
23
- "#ffffffff",
24
- "#ffffffff",
25
- "#ffffffff",
26
- "#ffffffff",
27
- "#d8d8d8ff",
28
- "#ffffffff",
29
- "#ffffffff",
30
- "#d8d8d8ff",
31
- "#ffffffff",
32
- "#ffffffff",
33
- "#ffffffff",
34
- "#ffffffff",
35
- "#d8d8d8ff",
36
- "#ffffffff",
37
- "#ffffffff",
38
- "#d8d8d8ff",
39
- "#ffffffff",
40
- "#ffffffff",
41
- "#ffffffff",
42
- "#ffffffff",
43
- "#d8d8d8ff",
44
- "#ffffffff",
45
- "#ffffffff",
46
- "#d8d8d8ff",
47
- "#ffffffff",
48
- "#ffffffff",
49
- "#ffffffff",
50
- "#ffffffff",
51
- "#d8d8d8ff",
52
- "#ffffffff",
53
- "#ffffffff",
54
- "#d8d8d8ff",
55
- "#d8d8d8ff",
56
- "#d8d8d8ff",
57
- "#d8d8d8ff",
58
- "#d8d8d8ff",
59
- "#d8d8d8ff",
60
- "#ffffffff",
61
- "#ffffffff",
62
- "#ffffffff",
63
- "#ffffffff",
64
- "#ffffffff",
65
- "#ffffffff",
66
- "#ffffffff",
67
- "#ffffffff",
68
- "#ffffffff"
69
- ]
70
- }
@@ -1,70 +0,0 @@
1
- {
2
- "width": 8,
3
- "height": 8,
4
- "pixels": [
5
- "#ffffffff",
6
- "#ffffffff",
7
- "#ffffffff",
8
- "#ffffffff",
9
- "#ffffffff",
10
- "#ffffffff",
11
- "#ffffffff",
12
- "#ffffffff",
13
- "#ffffffff",
14
- "#d8d8d8ff",
15
- "#d8d8d8ff",
16
- "#d8d8d8ff",
17
- "#d8d8d8ff",
18
- "#d8d8d8ff",
19
- "#d8d8d8ff",
20
- "#ffffffff",
21
- "#ffffffff",
22
- "#d8d8d8ff",
23
- "#ffffffff",
24
- "#ffffffff",
25
- "#ffffffff",
26
- "#ffffffff",
27
- "#d8d8d8ff",
28
- "#ffffffff",
29
- "#ffffffff",
30
- "#d8d8d8ff",
31
- "#ffffffff",
32
- "#ffffffff",
33
- "#ffffffff",
34
- "#ffffffff",
35
- "#d8d8d8ff",
36
- "#ffffffff",
37
- "#ffffffff",
38
- "#d8d8d8ff",
39
- "#ffffffff",
40
- "#ffffffff",
41
- "#ffffffff",
42
- "#ffffffff",
43
- "#d8d8d8ff",
44
- "#ffffffff",
45
- "#ffffffff",
46
- "#d8d8d8ff",
47
- "#ffffffff",
48
- "#ffffffff",
49
- "#ffffffff",
50
- "#ffffffff",
51
- "#d8d8d8ff",
52
- "#ffffffff",
53
- "#ffffffff",
54
- "#d8d8d8ff",
55
- "#d8d8d8ff",
56
- "#d8d8d8ff",
57
- "#d8d8d8ff",
58
- "#d8d8d8ff",
59
- "#d8d8d8ff",
60
- "#ffffffff",
61
- "#ffffffff",
62
- "#ffffffff",
63
- "#ffffffff",
64
- "#ffffffff",
65
- "#ffffffff",
66
- "#ffffffff",
67
- "#ffffffff",
68
- "#ffffffff"
69
- ]
70
- }
@@ -1,224 +0,0 @@
1
- import React, { useEffect, useRef, useState } from 'react';
2
- import { basename, formatJson, parseJsonFile } from '../engine/files';
3
- import {
4
- cx,
5
- EditorBody,
6
- EditorHeader,
7
- IconButton,
8
- NumberField,
9
- Panel,
10
- SheetGrabHandle,
11
- styles,
12
- theme,
13
- useMobileSheet,
14
- } from '../engine/ui';
15
- import { useEditHistory } from './editorHistory';
16
- const palette = [
17
- '#00000000',
18
- '#000000FF',
19
- '#050505FF',
20
- '#111111FF',
21
- '#242234FF',
22
- '#444444FF',
23
- '#777777FF',
24
- '#ffffffFF',
25
- '#e3e6ffFF',
26
- '#8db7ffFF',
27
- '#ff5d5dFF',
28
- '#ffe17aFF',
29
- ];
30
- export function DrawingEditor({ path, text, onChange, onToggleFiles, filesOpen, bare = false }) {
31
- const canvasRef = useRef(null);
32
- const strokeRef = useRef({ active: false, recorded: false });
33
- const [color, setColor] = useState('#ffffffFF');
34
- const [inspectorOpen, setInspectorOpen] = useState(true);
35
- const history = useEditHistory(text, onChange);
36
- const inspectorSheet = useInspectorSheet(inspectorOpen);
37
- const { value: drawing, error } = parseJsonFile(path, text);
38
- const isWide = drawing ? drawing.width >= drawing.height : true;
39
- useEffect(() => {
40
- if (!drawing || !canvasRef.current) return;
41
- const canvas = canvasRef.current;
42
- const ctx = canvas.getContext('2d');
43
- if (!ctx) return;
44
- canvas.width = drawing.width;
45
- canvas.height = drawing.height;
46
- ctx.clearRect(0, 0, drawing.width, drawing.height);
47
- for (let y = 0; y < drawing.height; y++) {
48
- for (let x = 0; x < drawing.width; x++) {
49
- const pixel = drawing.pixels[y * drawing.width + x];
50
- if (!pixel || pixel.endsWith('00')) continue;
51
- ctx.fillStyle = pixel;
52
- ctx.fillRect(x, y, 1, 1);
53
- }
54
- }
55
- }, [drawing, text]);
56
- function commitDrawing(nextDrawing) {
57
- history.commit(formatJson(nextDrawing));
58
- }
59
- function resizeDrawing(nextWidth, nextHeight) {
60
- if (!drawing) return;
61
- const width = clampInteger(nextWidth, 1, 128);
62
- const height = clampInteger(nextHeight, 1, 128);
63
- if (width === drawing.width && height === drawing.height) return;
64
- const pixels = Array(width * height).fill('#00000000');
65
- const copiedWidth = Math.min(width, drawing.width);
66
- const copiedHeight = Math.min(height, drawing.height);
67
- for (let y = 0; y < copiedHeight; y++) {
68
- for (let x = 0; x < copiedWidth; x++) {
69
- pixels[y * width + x] = drawing.pixels[y * drawing.width + x] ?? '#00000000';
70
- }
71
- }
72
- commitDrawing({ ...drawing, width, height, pixels });
73
- }
74
- function paintAt(event) {
75
- if (!drawing) return;
76
- const rect = event.currentTarget.getBoundingClientRect();
77
- const x = Math.floor(((event.clientX - rect.left) / rect.width) * drawing.width);
78
- const y = Math.floor(((event.clientY - rect.top) / rect.height) * drawing.height);
79
- if (x < 0 || y < 0 || x >= drawing.width || y >= drawing.height) return;
80
- if (drawing.pixels[y * drawing.width + x] === color) return;
81
- const next = structuredClone(drawing);
82
- next.pixels[y * next.width + x] = color;
83
- // Record one undo snapshot per pointer stroke (not per painted pixel).
84
- if (!strokeRef.current.recorded) {
85
- history.recordSnapshot();
86
- strokeRef.current.recorded = true;
87
- }
88
- onChange(formatJson(next));
89
- }
90
- function startPaint(event) {
91
- strokeRef.current = { active: true, recorded: false };
92
- event.currentTarget.setPointerCapture(event.pointerId);
93
- paintAt(event);
94
- }
95
- function finishPaint() {
96
- strokeRef.current = { active: false, recorded: false };
97
- }
98
- const canvasStyle = drawing
99
- ? {
100
- width: isWide ? 'min(70vh, 520px)' : 'auto',
101
- height: isWide ? 'auto' : 'min(70vh, 520px)',
102
- }
103
- : undefined;
104
- const sharedActionButtons = (
105
- <>
106
- <IconButton icon="undo" label="Undo" onClick={history.undo} disabled={!history.canUndo} />
107
- <IconButton icon="redo" label="Redo" onClick={history.redo} disabled={!history.canRedo} />
108
- </>
109
- );
110
- return (
111
- <>
112
- {bare ? null : (
113
- <EditorHeader
114
- title={basename(path)}
115
- subtitle={drawing ? `${drawing.width} x ${drawing.height} pixels` : error}
116
- right={
117
- <span className={styles.mobileOnly}>
118
- {sharedActionButtons}
119
- <IconButton
120
- icon="palette"
121
- label="Tools"
122
- active={inspectorOpen}
123
- onClick={() => setInspectorOpen((previous) => !previous)}
124
- />
125
- </span>
126
- }
127
- onToggleFiles={onToggleFiles}
128
- filesOpen={filesOpen}
129
- />
130
- )}
131
- <EditorBody>
132
- <div className={styles.drawingEditor}>
133
- <div className={styles.drawingTools}>{sharedActionButtons}</div>
134
- <div className={styles.drawingCanvasWrap}>
135
- <canvas
136
- ref={canvasRef}
137
- className={styles.drawingCanvas}
138
- style={canvasStyle}
139
- onPointerDown={startPaint}
140
- onPointerMove={(event) => {
141
- if (strokeRef.current.active && event.buttons === 1) paintAt(event);
142
- }}
143
- onPointerUp={finishPaint}
144
- onPointerCancel={finishPaint}
145
- />
146
- </div>
147
- <DrawingInspector
148
- sheet={inspectorSheet}
149
- drawing={drawing}
150
- color={color}
151
- onSelectColor={setColor}
152
- onResize={resizeDrawing}
153
- />
154
- </div>
155
- </EditorBody>
156
- </>
157
- );
158
- }
159
- function useInspectorSheet(inspectorOpen) {
160
- const [snap, setSnap] = useState('high');
161
- useEffect(() => {
162
- if (inspectorOpen) setSnap('high');
163
- }, [inspectorOpen]);
164
- const effectiveSnap = inspectorOpen ? snap : 'hidden';
165
- return useMobileSheet({
166
- snap: effectiveSnap,
167
- baseClassName: styles.inspector,
168
- onTransition: (direction) => {
169
- if (!inspectorOpen) return;
170
- if (direction === 'tap') setSnap((previous) => (previous === 'high' ? 'low' : 'high'));
171
- else if (direction === 'down') setSnap('low');
172
- else if (direction === 'up') setSnap('high');
173
- },
174
- });
175
- }
176
- function DrawingInspector({ sheet, drawing, color, onSelectColor, onResize }) {
177
- return (
178
- <aside {...sheet.rootProps} className={cx(sheet.rootProps.className, styles.inspectorAuto)}>
179
- <div {...sheet.grabProps}>
180
- <SheetGrabHandle
181
- label="Inspector"
182
- hint={drawing ? `${drawing.width}x${drawing.height} · color` : 'drawing tools'}
183
- />
184
- </div>
185
- <div className={cx(styles.sheetBody, styles.inspectorBody)}>
186
- <Panel title="Size">
187
- <NumberField
188
- label="Width"
189
- value={drawing?.width ?? 1}
190
- min={1}
191
- max={128}
192
- onChange={(width) => onResize(width, drawing?.height ?? 1)}
193
- />
194
- <NumberField
195
- label="Height"
196
- value={drawing?.height ?? 1}
197
- min={1}
198
- max={128}
199
- onChange={(height) => onResize(drawing?.width ?? 1, height)}
200
- />
201
- </Panel>
202
- <Panel title="Palette">
203
- <div className={styles.palette}>
204
- {palette.map((swatch) => (
205
- <button
206
- key={swatch}
207
- className={cx(styles.swatch, color === swatch && styles.swatchSelected)}
208
- title={swatch}
209
- style={{
210
- background: swatch.endsWith('00') ? theme.transparentChecker : swatch,
211
- }}
212
- onClick={() => onSelectColor(swatch)}
213
- />
214
- ))}
215
- </div>
216
- </Panel>
217
- </div>
218
- </aside>
219
- );
220
- }
221
- function clampInteger(value, min, max) {
222
- if (!Number.isFinite(value)) return min;
223
- return Math.max(min, Math.min(max, Math.round(value)));
224
- }