castle-web-cli 0.4.75 → 0.4.77
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.d.ts +13 -0
- package/dist/agent-prompts.js +43 -13
- package/dist/agent.d.ts +29 -0
- package/dist/agent.js +1004 -257
- package/dist/castle-host/host.js +59 -5
- package/dist/commonInstructions.d.ts +1 -1
- package/dist/commonInstructions.js +11 -1
- package/dist/filesChanged.d.ts +25 -0
- package/dist/filesChanged.js +140 -0
- package/dist/ide.d.ts +1 -0
- package/dist/ide.js +250 -1
- package/dist/init.js +46 -4
- package/dist/save-deck.js +8 -1
- package/dist/serve.js +45 -3
- package/dist/shell/assets/index-CvHiGhAV.js +141 -0
- package/dist/shell/assets/{index-WNbOHPBj.css → index-QteLRDnK.css} +1 -1
- package/dist/shell/index.html +2 -2
- package/kits/basic-2d/CLAUDE.md +13 -5
- package/kits/basic-2d/castle.json +15 -0
- package/kits/basic-2d/drawings/pig.pxart +22 -55
- package/kits/basic-2d/editors/PlayOnly.jsx +8 -3
- package/kits/basic-2d/editors/PxArtEditor.jsx +237 -4
- package/kits/basic-2d/editors/SingleEditor.jsx +30 -59
- package/kits/basic-2d/editors/pixelEditorChrome.jsx +17 -11
- package/kits/basic-2d/editors/pixelGeometry.js +95 -0
- package/kits/basic-2d/editors/pixelInspector.jsx +229 -51
- package/kits/basic-2d/editors/pxArtTools.js +109 -1
- package/kits/basic-2d/engine/ScenePlayer.jsx +11 -160
- package/kits/basic-2d/engine/liveReload.js +88 -0
- package/kits/basic-2d/engine/ui.jsx +15 -0
- package/kits/basic-2d/engine/ui.module.css +44 -117
- package/kits/basic-2d/main.jsx +6 -10
- package/package.json +10 -1
- package/dist/shell/assets/index-Dfn29Bkt.js +0 -108
- package/kits/basic-2d/editors/App.jsx +0 -226
- package/kits/basic-2d/editors/CodeEditor.jsx +0 -79
- package/kits/basic-2d/editors/FileBrowser.jsx +0 -349
- package/kits/basic-2d/editors/codeTheme.js +0 -135
- package/kits/basic-2d/engine/playConsole.js +0 -66
- package/kits/basic-2d/pnpm-workspace.yaml +0 -3
- package/kits/basic-3d/.prettierrc +0 -8
- package/kits/basic-3d/CLAUDE.md +0 -162
- package/kits/basic-3d/behaviors/Camera.jsx +0 -56
- package/kits/basic-3d/behaviors/Collider.jsx +0 -78
- package/kits/basic-3d/behaviors/Mesh.jsx +0 -82
- package/kits/basic-3d/behaviors/Model.jsx +0 -61
- package/kits/basic-3d/behaviors/Transform.jsx +0 -35
- package/kits/basic-3d/editors/App.jsx +0 -147
- package/kits/basic-3d/editors/CodeEditor.jsx +0 -112
- package/kits/basic-3d/editors/FileBrowser.jsx +0 -143
- package/kits/basic-3d/editors/ModelEditor.jsx +0 -400
- package/kits/basic-3d/editors/PlayOnly.jsx +0 -22
- package/kits/basic-3d/editors/SceneEditor.jsx +0 -1081
- package/kits/basic-3d/editors/behaviorRegistry.js +0 -24
- package/kits/basic-3d/editors/editorHistory.js +0 -52
- package/kits/basic-3d/editors/viewportRig.js +0 -90
- package/kits/basic-3d/engine/ScenePlayer.jsx +0 -58
- package/kits/basic-3d/engine/SceneUI.jsx +0 -67
- package/kits/basic-3d/engine/SceneViewport.jsx +0 -102
- package/kits/basic-3d/engine/autoInspector.jsx +0 -51
- package/kits/basic-3d/engine/files.js +0 -73
- package/kits/basic-3d/engine/scene.js +0 -502
- package/kits/basic-3d/engine/threeUtil.js +0 -260
- package/kits/basic-3d/engine/ui.jsx +0 -352
- package/kits/basic-3d/engine/ui.module.css +0 -944
- package/kits/basic-3d/eslint.config.js +0 -51
- package/kits/basic-3d/index.html +0 -11
- package/kits/basic-3d/main.jsx +0 -10
- package/kits/basic-3d/models/block.model +0 -14
- package/kits/basic-3d/package-lock.json +0 -2713
- package/kits/basic-3d/package.json +0 -41
- package/kits/basic-3d/pnpm-lock.yaml +0 -1769
- package/kits/basic-3d/scenes/main.scene +0 -76
- package/kits/basic-3d/vite.config.js +0 -1
|
@@ -11,13 +11,33 @@ export const BRUSH_SIZES = [1, 2, 3, 4, 6, 8, 12, 16, 24, 32];
|
|
|
11
11
|
export const PIXEL_TOOLS = [
|
|
12
12
|
{ id: 'brush', label: 'Brush', icon: 'pencil' },
|
|
13
13
|
{ id: 'fill', label: 'Fill', icon: 'fill' },
|
|
14
|
+
{ id: 'shape', label: 'Shape', icon: 'shapes' },
|
|
15
|
+
{ id: 'select', label: 'Select', icon: 'marquee' },
|
|
14
16
|
{ id: 'move-all', label: 'Move all', icon: 'move' },
|
|
15
17
|
{ id: 'erase', label: 'Erase', icon: 'eraser' },
|
|
16
18
|
];
|
|
17
19
|
|
|
20
|
+
// Sub-modes for the shape tool. Each drags out an axis-aligned box: line draws
|
|
21
|
+
// the diagonal, square the rectangle, circle the inscribed ellipse, triangle an
|
|
22
|
+
// isosceles triangle. `filled` (a separate toggle) applies to the closed three.
|
|
23
|
+
export const SHAPE_MODES = [
|
|
24
|
+
{ id: 'line', label: 'Line', icon: 'slash' },
|
|
25
|
+
{ id: 'square', label: 'Square', icon: 'square' },
|
|
26
|
+
{ id: 'circle', label: 'Circle', icon: 'circle' },
|
|
27
|
+
{ id: 'triangle', label: 'Triangle', icon: 'triangle' },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
// Momentary transform actions the move tool exposes (it has no persistent
|
|
31
|
+
// settings). Each `id` maps to a handler on the editor's `moveActions`.
|
|
32
|
+
export const MOVE_ACTIONS = [
|
|
33
|
+
{ id: 'flipH', label: 'Flip horizontal', icon: 'flip-h' },
|
|
34
|
+
{ id: 'flipV', label: 'Flip vertical', icon: 'flip-v' },
|
|
35
|
+
{ id: 'rotateCW', label: 'Rotate clockwise', icon: 'redo' },
|
|
36
|
+
];
|
|
37
|
+
|
|
18
38
|
// Tools whose work is driven by the active palette color, so the palette panel
|
|
19
39
|
// (and its eyedropper action) shows.
|
|
20
|
-
export const PALETTE_TOOLS = new Set(['brush', 'fill', 'eyedropper']);
|
|
40
|
+
export const PALETTE_TOOLS = new Set(['brush', 'fill', 'shape', 'eyedropper']);
|
|
21
41
|
|
|
22
42
|
// Shared tool-mode state for the pixel editors: the selected tool plus the
|
|
23
43
|
// brush/erase sizes and the fill/erase mode toggles. The active color/key stays
|
|
@@ -33,6 +53,8 @@ export function usePixelToolState() {
|
|
|
33
53
|
const [eraseSize, setEraseSize] = useState(4);
|
|
34
54
|
const [eraseFillMode, setEraseFillMode] = useState(false);
|
|
35
55
|
const [fillSwapMode, setFillSwapMode] = useState(false);
|
|
56
|
+
const [shapeMode, setShapeMode] = useState('line');
|
|
57
|
+
const [shapeFill, setShapeFill] = useState(false);
|
|
36
58
|
return {
|
|
37
59
|
tool,
|
|
38
60
|
setTool,
|
|
@@ -46,6 +68,10 @@ export function usePixelToolState() {
|
|
|
46
68
|
setEraseFillMode,
|
|
47
69
|
fillSwapMode,
|
|
48
70
|
setFillSwapMode,
|
|
71
|
+
shapeMode,
|
|
72
|
+
setShapeMode,
|
|
73
|
+
shapeFill,
|
|
74
|
+
setShapeFill,
|
|
49
75
|
};
|
|
50
76
|
}
|
|
51
77
|
|
|
@@ -175,6 +201,54 @@ export function PalettePopover({ open, anchorRef, onClose, children }) {
|
|
|
175
201
|
);
|
|
176
202
|
}
|
|
177
203
|
|
|
204
|
+
// The shape tool's sub-mode picker, shared by the compact paint strip and the
|
|
205
|
+
// wide-layout settings panel. `buttonClass` styles each glyph button for its
|
|
206
|
+
// host strip; `radio` switches the a11y semantics to a radiogroup.
|
|
207
|
+
function ShapeModeButtons({ shapeMode, setShapeMode, buttonClass, radio = false }) {
|
|
208
|
+
return (
|
|
209
|
+
<>
|
|
210
|
+
{SHAPE_MODES.map((mode) => {
|
|
211
|
+
const selected = shapeMode === mode.id;
|
|
212
|
+
return (
|
|
213
|
+
<button
|
|
214
|
+
key={mode.id}
|
|
215
|
+
type="button"
|
|
216
|
+
role={radio ? 'radio' : undefined}
|
|
217
|
+
className={cx(buttonClass, selected && styles.drawingToolSelected)}
|
|
218
|
+
title={mode.label}
|
|
219
|
+
aria-label={mode.label}
|
|
220
|
+
aria-pressed={radio ? undefined : selected ? 'true' : 'false'}
|
|
221
|
+
aria-checked={radio ? (selected ? 'true' : 'false') : undefined}
|
|
222
|
+
onClick={() => setShapeMode(mode.id)}>
|
|
223
|
+
<Icon name={mode.icon} />
|
|
224
|
+
</button>
|
|
225
|
+
);
|
|
226
|
+
})}
|
|
227
|
+
</>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// The move tool's transform actions (flip H / flip V / rotate CW), shared by the
|
|
232
|
+
// compact paint strip and the wide-layout settings panel. These are momentary
|
|
233
|
+
// buttons (no selected state); `buttonClass` styles them for the host strip.
|
|
234
|
+
function MoveActionButtons({ moveActions, buttonClass }) {
|
|
235
|
+
return (
|
|
236
|
+
<>
|
|
237
|
+
{MOVE_ACTIONS.map((action) => (
|
|
238
|
+
<button
|
|
239
|
+
key={action.id}
|
|
240
|
+
type="button"
|
|
241
|
+
className={buttonClass}
|
|
242
|
+
title={action.label}
|
|
243
|
+
aria-label={action.label}
|
|
244
|
+
onClick={moveActions?.[action.id]}>
|
|
245
|
+
<Icon name={action.icon} />
|
|
246
|
+
</button>
|
|
247
|
+
))}
|
|
248
|
+
</>
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
178
252
|
// Right-side paint strip (compact layout): color swatch, vertical size slider, and
|
|
179
253
|
// tool-contextual mode toggles. Color tap opens the palette popover.
|
|
180
254
|
export function PaintStrip({
|
|
@@ -187,6 +261,8 @@ export function PaintStrip({
|
|
|
187
261
|
onSelectKey,
|
|
188
262
|
paletteKeys,
|
|
189
263
|
palette,
|
|
264
|
+
moveActions,
|
|
265
|
+
selectActions,
|
|
190
266
|
}) {
|
|
191
267
|
const {
|
|
192
268
|
tool,
|
|
@@ -196,10 +272,14 @@ export function PaintStrip({
|
|
|
196
272
|
eraseSize,
|
|
197
273
|
eraseFillMode,
|
|
198
274
|
fillSwapMode,
|
|
275
|
+
shapeMode,
|
|
276
|
+
shapeFill,
|
|
199
277
|
setBrushSize,
|
|
200
278
|
setEraseSize,
|
|
201
279
|
setEraseFillMode,
|
|
202
280
|
setFillSwapMode,
|
|
281
|
+
setShapeMode,
|
|
282
|
+
setShapeFill,
|
|
203
283
|
} = toolState;
|
|
204
284
|
|
|
205
285
|
const showBrushSize = tool === 'brush';
|
|
@@ -212,59 +292,112 @@ export function PaintStrip({
|
|
|
212
292
|
return (
|
|
213
293
|
<>
|
|
214
294
|
<div className={styles.paintStrip} aria-label="Paint controls">
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
295
|
+
{PALETTE_TOOLS.has(tool) || picking ? (
|
|
296
|
+
<div className={styles.paintStripSection}>
|
|
297
|
+
<button
|
|
298
|
+
ref={colorButtonRef}
|
|
299
|
+
type="button"
|
|
300
|
+
className={styles.paintStripColor}
|
|
301
|
+
title={activeColor}
|
|
302
|
+
aria-label={`Active color ${activeColor}. Open palette.`}
|
|
303
|
+
aria-expanded={paletteOpen ? 'true' : 'false'}
|
|
304
|
+
style={{ background: activeColor }}
|
|
305
|
+
onClick={onTogglePalette}
|
|
306
|
+
/>
|
|
307
|
+
</div>
|
|
308
|
+
) : null}
|
|
225
309
|
{showBrushSize || showEraseSize ? (
|
|
226
|
-
<div className={styles.
|
|
227
|
-
<
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
310
|
+
<div className={styles.paintStripSection}>
|
|
311
|
+
<div className={styles.paintStripSlider}>
|
|
312
|
+
<input
|
|
313
|
+
className={styles.drawingSizeSliderVertical}
|
|
314
|
+
type="range"
|
|
315
|
+
min="0"
|
|
316
|
+
max={BRUSH_SIZES.length - 1}
|
|
317
|
+
step="1"
|
|
318
|
+
value={sliderIndex}
|
|
319
|
+
disabled={sliderDisabled}
|
|
320
|
+
aria-label={showBrushSize ? 'Brush size' : 'Erase size'}
|
|
321
|
+
title={`${showBrushSize ? 'Brush' : 'Erase'} size ${size}`}
|
|
322
|
+
onChange={(event) => onSizeChange(BRUSH_SIZES[Number(event.target.value)])}
|
|
323
|
+
/>
|
|
324
|
+
</div>
|
|
325
|
+
</div>
|
|
326
|
+
) : null}
|
|
327
|
+
{tool === 'shape' ? (
|
|
328
|
+
<div className={styles.paintStripSection}>
|
|
329
|
+
<ShapeModeButtons
|
|
330
|
+
shapeMode={shapeMode}
|
|
331
|
+
setShapeMode={setShapeMode}
|
|
332
|
+
buttonClass={styles.paintStripModeButton}
|
|
238
333
|
/>
|
|
239
334
|
</div>
|
|
240
335
|
) : null}
|
|
336
|
+
{tool === 'move-all' ? (
|
|
337
|
+
<div className={styles.paintStripSection}>
|
|
338
|
+
<MoveActionButtons moveActions={moveActions} buttonClass={styles.paintStripModeButton} />
|
|
339
|
+
</div>
|
|
340
|
+
) : null}
|
|
341
|
+
{tool === 'select' ? (
|
|
342
|
+
<div className={styles.paintStripSection}>
|
|
343
|
+
<button
|
|
344
|
+
type="button"
|
|
345
|
+
className={styles.paintStripModeButton}
|
|
346
|
+
title="Delete selection"
|
|
347
|
+
aria-label="Delete selection"
|
|
348
|
+
disabled={!selectActions?.hasSelection}
|
|
349
|
+
onClick={selectActions?.deletePixels}>
|
|
350
|
+
<Icon name="trash" />
|
|
351
|
+
</button>
|
|
352
|
+
</div>
|
|
353
|
+
) : null}
|
|
241
354
|
{tool === 'erase' ? (
|
|
242
|
-
<
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
355
|
+
<div className={styles.paintStripSection}>
|
|
356
|
+
<button
|
|
357
|
+
type="button"
|
|
358
|
+
className={cx(
|
|
359
|
+
styles.paintStripModeButton,
|
|
360
|
+
eraseFillMode && styles.drawingToolSelected
|
|
361
|
+
)}
|
|
362
|
+
title="Fill mode"
|
|
363
|
+
aria-label="Fill mode"
|
|
364
|
+
aria-pressed={eraseFillMode ? 'true' : 'false'}
|
|
365
|
+
onClick={() => setEraseFillMode(!eraseFillMode)}>
|
|
366
|
+
<Icon name="fill" />
|
|
367
|
+
</button>
|
|
368
|
+
</div>
|
|
254
369
|
) : null}
|
|
255
370
|
{tool === 'fill' ? (
|
|
256
|
-
<
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
371
|
+
<div className={styles.paintStripSection}>
|
|
372
|
+
<button
|
|
373
|
+
type="button"
|
|
374
|
+
className={cx(
|
|
375
|
+
styles.paintStripModeButton,
|
|
376
|
+
fillSwapMode && styles.drawingToolSelected
|
|
377
|
+
)}
|
|
378
|
+
title="Swap mode"
|
|
379
|
+
aria-label="Swap mode"
|
|
380
|
+
aria-pressed={fillSwapMode ? 'true' : 'false'}
|
|
381
|
+
onClick={() => setFillSwapMode(!fillSwapMode)}>
|
|
382
|
+
<Icon name="rotate" />
|
|
383
|
+
</button>
|
|
384
|
+
</div>
|
|
385
|
+
) : null}
|
|
386
|
+
{tool === 'shape' ? (
|
|
387
|
+
<div className={styles.paintStripSection}>
|
|
388
|
+
<button
|
|
389
|
+
type="button"
|
|
390
|
+
className={cx(
|
|
391
|
+
styles.paintStripModeButton,
|
|
392
|
+
shapeFill && styles.drawingToolSelected
|
|
393
|
+
)}
|
|
394
|
+
title="Fill shape"
|
|
395
|
+
aria-label="Fill shape"
|
|
396
|
+
aria-pressed={shapeFill ? 'true' : 'false'}
|
|
397
|
+
onClick={() => setShapeFill(!shapeFill)}>
|
|
398
|
+
<Icon name="fill" />
|
|
399
|
+
</button>
|
|
400
|
+
</div>
|
|
268
401
|
) : null}
|
|
269
402
|
</div>
|
|
270
403
|
<PalettePopover open={paletteOpen} anchorRef={colorButtonRef} onClose={() => onTogglePalette(false)}>
|
|
@@ -293,7 +426,7 @@ export function PaintStrip({
|
|
|
293
426
|
}
|
|
294
427
|
|
|
295
428
|
// Wide-layout docked inspector: palette (when relevant) + tool settings only.
|
|
296
|
-
export function PxArtInspectorDock({ toolState, activeKey, onSelectKey, paletteKeys, palette }) {
|
|
429
|
+
export function PxArtInspectorDock({ toolState, activeKey, onSelectKey, paletteKeys, palette, moveActions, selectActions }) {
|
|
297
430
|
const { tool, picking, setPicking } = toolState;
|
|
298
431
|
return (
|
|
299
432
|
<aside className={cx(styles.pxArtInspector, styles.drawingToolsPanel)}>
|
|
@@ -318,7 +451,7 @@ export function PxArtInspectorDock({ toolState, activeKey, onSelectKey, paletteK
|
|
|
318
451
|
/>
|
|
319
452
|
</Panel>
|
|
320
453
|
) : null}
|
|
321
|
-
<ToolSettingsPanel toolState={toolState} />
|
|
454
|
+
<ToolSettingsPanel toolState={toolState} moveActions={moveActions} selectActions={selectActions} />
|
|
322
455
|
</div>
|
|
323
456
|
</aside>
|
|
324
457
|
);
|
|
@@ -344,17 +477,21 @@ export function ModeToggle({ label, active, onChange }) {
|
|
|
344
477
|
// The "Tool Settings" panel for the pixel editor: brush/erase size sliders plus
|
|
345
478
|
// the per-tool mode toggle / hint. It reads the whole usePixelToolState bundle
|
|
346
479
|
// to keep callers from re-spreading the same prop list.
|
|
347
|
-
export function ToolSettingsPanel({ toolState }) {
|
|
480
|
+
export function ToolSettingsPanel({ toolState, moveActions, selectActions }) {
|
|
348
481
|
const {
|
|
349
482
|
tool,
|
|
350
483
|
brushSize,
|
|
351
484
|
eraseSize,
|
|
352
485
|
eraseFillMode,
|
|
353
486
|
fillSwapMode,
|
|
487
|
+
shapeMode,
|
|
488
|
+
shapeFill,
|
|
354
489
|
setBrushSize,
|
|
355
490
|
setEraseSize,
|
|
356
491
|
setEraseFillMode,
|
|
357
492
|
setFillSwapMode,
|
|
493
|
+
setShapeMode,
|
|
494
|
+
setShapeFill,
|
|
358
495
|
} = toolState;
|
|
359
496
|
return (
|
|
360
497
|
<Panel title="Tool Settings">
|
|
@@ -376,8 +513,49 @@ export function ToolSettingsPanel({ toolState }) {
|
|
|
376
513
|
{tool === 'fill' ? (
|
|
377
514
|
<ModeToggle label="Swap Mode" active={fillSwapMode} onChange={setFillSwapMode} />
|
|
378
515
|
) : null}
|
|
516
|
+
{tool === 'shape' ? (
|
|
517
|
+
<div className={styles.drawingSizePicker}>
|
|
518
|
+
<div className={styles.drawingSizeLabel}>
|
|
519
|
+
<span>Shape</span>
|
|
520
|
+
</div>
|
|
521
|
+
<div className={styles.drawingShapeModeRow} role="radiogroup" aria-label="Shape">
|
|
522
|
+
<ShapeModeButtons
|
|
523
|
+
shapeMode={shapeMode}
|
|
524
|
+
setShapeMode={setShapeMode}
|
|
525
|
+
buttonClass={styles.drawingToolButton}
|
|
526
|
+
radio
|
|
527
|
+
/>
|
|
528
|
+
</div>
|
|
529
|
+
<ModeToggle label="Fill Shape" active={shapeFill} onChange={setShapeFill} />
|
|
530
|
+
</div>
|
|
531
|
+
) : null}
|
|
379
532
|
{tool === 'move-all' ? (
|
|
380
|
-
<div className={styles.
|
|
533
|
+
<div className={styles.drawingSizePicker}>
|
|
534
|
+
<div className={styles.drawingSizeLabel}>
|
|
535
|
+
<span>Transform</span>
|
|
536
|
+
</div>
|
|
537
|
+
<div className={styles.drawingShapeModeRow}>
|
|
538
|
+
<MoveActionButtons moveActions={moveActions} buttonClass={styles.drawingToolButton} />
|
|
539
|
+
</div>
|
|
540
|
+
</div>
|
|
541
|
+
) : null}
|
|
542
|
+
{tool === 'select' ? (
|
|
543
|
+
<div className={styles.drawingSizePicker}>
|
|
544
|
+
<div className={styles.drawingSizeLabel}>
|
|
545
|
+
<span>Selection</span>
|
|
546
|
+
</div>
|
|
547
|
+
<div className={styles.drawingShapeModeRow}>
|
|
548
|
+
<button
|
|
549
|
+
type="button"
|
|
550
|
+
className={styles.drawingToolButton}
|
|
551
|
+
title="Delete selection"
|
|
552
|
+
aria-label="Delete selection"
|
|
553
|
+
disabled={!selectActions?.hasSelection}
|
|
554
|
+
onClick={selectActions?.deletePixels}>
|
|
555
|
+
<Icon name="trash" />
|
|
556
|
+
</button>
|
|
557
|
+
</div>
|
|
558
|
+
</div>
|
|
381
559
|
) : null}
|
|
382
560
|
{tool === 'eyedropper' ? (
|
|
383
561
|
<div className={styles.drawingToolHint}>Click a pixel to make it the active color.</div>
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { EDG64, KEY_ALPHABET, normalizeHex, TRANSPARENT } from '../engine/pxart';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
forEachDab,
|
|
4
|
+
forEachEllipseCells,
|
|
5
|
+
forEachLinePoint,
|
|
6
|
+
forEachRectCells,
|
|
7
|
+
forEachTriangleCells,
|
|
8
|
+
} from './pixelGeometry';
|
|
3
9
|
|
|
4
10
|
// The PxArt editor paints over the FIXED full Endesga-64 palette (no free color
|
|
5
11
|
// picking). Each palette color gets a stable single-char key (KEY_ALPHABET, 64
|
|
@@ -46,6 +52,9 @@ export function editorKeyForHex(hex) {
|
|
|
46
52
|
return bestKey;
|
|
47
53
|
}
|
|
48
54
|
|
|
55
|
+
// The swatch the editor opens on: Endesga-64's lead bright red (#ff0040).
|
|
56
|
+
export const DEFAULT_KEY = editorKeyForHex('#ff0040');
|
|
57
|
+
|
|
49
58
|
function isInside(cells, x, y) {
|
|
50
59
|
return y >= 0 && y < cells.length && x >= 0 && x < (cells[0]?.length ?? 0);
|
|
51
60
|
}
|
|
@@ -75,6 +84,28 @@ export function paintLine(cells, from, to, key, size = 1) {
|
|
|
75
84
|
return changed ? next : cells;
|
|
76
85
|
}
|
|
77
86
|
|
|
87
|
+
// Draw a shape with `key` into a copy of `cells`, defined by the drag from
|
|
88
|
+
// `from` to `to`. `mode` selects line / square / circle / triangle; `filled`
|
|
89
|
+
// applies to the closed shapes (the line ignores it). Returns a new matrix when
|
|
90
|
+
// anything changed, else the original (so callers can skip no-op commits).
|
|
91
|
+
export function paintShape(cells, from, to, key, mode, filled = false) {
|
|
92
|
+
const next = copyCells(cells);
|
|
93
|
+
let changed = false;
|
|
94
|
+
const write = (x, y) => {
|
|
95
|
+
changed = writeCell(next, x, y, key) || changed;
|
|
96
|
+
};
|
|
97
|
+
if (mode === 'line') {
|
|
98
|
+
forEachLinePoint(from, to, write);
|
|
99
|
+
} else if (mode === 'square') {
|
|
100
|
+
forEachRectCells(from, to, filled, write);
|
|
101
|
+
} else if (mode === 'circle') {
|
|
102
|
+
forEachEllipseCells(from, to, filled, write);
|
|
103
|
+
} else if (mode === 'triangle') {
|
|
104
|
+
forEachTriangleCells(from, to, filled, write);
|
|
105
|
+
}
|
|
106
|
+
return changed ? next : cells;
|
|
107
|
+
}
|
|
108
|
+
|
|
78
109
|
// Both region tools share the same guard: bail when the start is out of bounds
|
|
79
110
|
// or already the target key, else return a working copy plus the key to replace.
|
|
80
111
|
function beginKeyReplace(cells, start, key) {
|
|
@@ -122,3 +153,80 @@ export function swapKey(cells, start, key) {
|
|
|
122
153
|
export function sampleKey(cells, point) {
|
|
123
154
|
return isInside(cells, point.x, point.y) ? cells[point.y][point.x] : TRANSPARENT;
|
|
124
155
|
}
|
|
156
|
+
|
|
157
|
+
// Mirror the canvas-window matrix left-to-right (dimensions unchanged).
|
|
158
|
+
export function flipCellsH(cells) {
|
|
159
|
+
return cells.map((row) => row.slice().reverse());
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Mirror the canvas-window matrix top-to-bottom (dimensions unchanged).
|
|
163
|
+
export function flipCellsV(cells) {
|
|
164
|
+
return cells.slice().reverse().map((row) => row.slice());
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Clear a rectangular region of the canvas-window matrix to transparent (the
|
|
168
|
+
// marquee delete). `rect` is { x, y, w, h } in canvas cells. Returns a new
|
|
169
|
+
// matrix when anything changed, else the original.
|
|
170
|
+
export function clearRegion(cells, rect) {
|
|
171
|
+
const next = copyCells(cells);
|
|
172
|
+
let changed = false;
|
|
173
|
+
for (let y = rect.y; y < rect.y + rect.h; y++) {
|
|
174
|
+
for (let x = rect.x; x < rect.x + rect.w; x++) {
|
|
175
|
+
changed = writeCell(next, x, y, TRANSPARENT) || changed;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return changed ? next : cells;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Lift the pixels in `rect`, clear that source region to transparent, then stamp
|
|
182
|
+
// the lifted OPAQUE pixels back at (dx, dy) offset — the marquee move's lift &
|
|
183
|
+
// float, applied as one combined result. Transparent cells in the lifted region
|
|
184
|
+
// don't erase the destination (they let underlying art show through). Pixels
|
|
185
|
+
// pushed outside the canvas window are clipped. Returns the original matrix on a
|
|
186
|
+
// zero delta (so a click-without-drag records no history); otherwise a new copy.
|
|
187
|
+
export function moveRegion(cells, rect, dx, dy) {
|
|
188
|
+
if (dx === 0 && dy === 0) return cells;
|
|
189
|
+
const buffer = [];
|
|
190
|
+
for (let y = 0; y < rect.h; y++) {
|
|
191
|
+
const row = [];
|
|
192
|
+
for (let x = 0; x < rect.w; x++) {
|
|
193
|
+
const sx = rect.x + x;
|
|
194
|
+
const sy = rect.y + y;
|
|
195
|
+
row.push(isInside(cells, sx, sy) ? cells[sy][sx] : TRANSPARENT);
|
|
196
|
+
}
|
|
197
|
+
buffer.push(row);
|
|
198
|
+
}
|
|
199
|
+
const next = copyCells(cells);
|
|
200
|
+
for (let y = 0; y < rect.h; y++) {
|
|
201
|
+
for (let x = 0; x < rect.w; x++) writeCell(next, rect.x + x, rect.y + y, TRANSPARENT);
|
|
202
|
+
}
|
|
203
|
+
for (let y = 0; y < rect.h; y++) {
|
|
204
|
+
for (let x = 0; x < rect.w; x++) {
|
|
205
|
+
const key = buffer[y][x];
|
|
206
|
+
if (key !== TRANSPARENT) writeCell(next, rect.x + x + dx, rect.y + y + dy, key);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return next;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Rotate the canvas-window matrix 90° clockwise about the canvas center, keeping
|
|
213
|
+
// the same width × height window (so it merges back through withCellsAt). For a
|
|
214
|
+
// square canvas this is the exact lossless rotation; on a non-square canvas the
|
|
215
|
+
// rotated corners that fall outside the window are clipped (filled transparent).
|
|
216
|
+
export function rotateCellsCW(cells) {
|
|
217
|
+
const height = cells.length;
|
|
218
|
+
const width = height ? cells[0].length : 0;
|
|
219
|
+
const cx = (width - 1) / 2;
|
|
220
|
+
const cy = (height - 1) / 2;
|
|
221
|
+
const out = [];
|
|
222
|
+
for (let oy = 0; oy < height; oy++) {
|
|
223
|
+
const row = [];
|
|
224
|
+
for (let ox = 0; ox < width; ox++) {
|
|
225
|
+
const sx = Math.round(cx + (oy - cy));
|
|
226
|
+
const sy = Math.round(cy - (ox - cx));
|
|
227
|
+
row.push(sy >= 0 && sy < height && sx >= 0 && sx < width ? cells[sy][sx] : TRANSPARENT);
|
|
228
|
+
}
|
|
229
|
+
out.push(row);
|
|
230
|
+
}
|
|
231
|
+
return out;
|
|
232
|
+
}
|