castle-web-cli 0.4.83 → 0.4.85
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-failures.d.ts +17 -0
- package/dist/agent-failures.js +151 -0
- package/dist/agent.d.ts +26 -0
- package/dist/agent.js +317 -74
- package/dist/ide.js +35 -13
- package/dist/native/loop.js +35 -0
- package/dist/native/openrouter.d.ts +7 -0
- package/dist/native/openrouter.js +25 -1
- package/dist/native/types.d.ts +2 -0
- package/dist/native/types.js +0 -38
- package/dist/openrouter-catalog.d.ts +28 -0
- package/dist/openrouter-catalog.js +299 -0
- package/dist/shell/assets/{index-C9Zhmien.js → index-BJLaUTJE.js} +60 -58
- package/dist/shell/assets/{index-BMkQt27u.css → index-DonnH--m.css} +1 -1
- package/dist/shell/index.html +2 -2
- package/kits/physics-2d/.prettierrc +8 -0
- package/kits/physics-2d/CLAUDE.md +329 -0
- package/kits/physics-2d/behaviors/Camera.jsx +43 -0
- package/kits/physics-2d/behaviors/Collider.jsx +199 -0
- package/kits/physics-2d/behaviors/Goal.jsx +29 -0
- package/kits/physics-2d/behaviors/Layout.jsx +53 -0
- package/kits/physics-2d/behaviors/Sprite.jsx +352 -0
- package/kits/physics-2d/behaviors/tint.js +47 -0
- package/kits/physics-2d/blueprints/ball.scene +14 -0
- package/kits/physics-2d/blueprints/block.scene +12 -0
- package/kits/physics-2d/blueprints/cauldron.scene +18 -0
- package/kits/physics-2d/blueprints/crate.scene +14 -0
- package/kits/physics-2d/blueprints/goal.scene +12 -0
- package/kits/physics-2d/castle.json +13 -0
- package/kits/physics-2d/docs/pxart-format.md +377 -0
- package/kits/physics-2d/drawings/block.pxart +25 -0
- package/kits/physics-2d/drawings/cauldron.pxart +113 -0
- package/kits/physics-2d/editors/BlueprintLibrary.jsx +247 -0
- package/kits/physics-2d/editors/ErrorBoundary.jsx +59 -0
- package/kits/physics-2d/editors/PlayOnly.jsx +31 -0
- package/kits/physics-2d/editors/PxArtEditor.jsx +954 -0
- package/kits/physics-2d/editors/SceneEditor.jsx +1681 -0
- package/kits/physics-2d/editors/SelectionOverlay.jsx +909 -0
- package/kits/physics-2d/editors/SingleEditor.jsx +122 -0
- package/kits/physics-2d/editors/behaviorRegistry.js +30 -0
- package/kits/physics-2d/editors/editorHistory.js +157 -0
- package/kits/physics-2d/editors/inspectorSheet.js +13 -0
- package/kits/physics-2d/editors/pixelCanvas.js +11 -0
- package/kits/physics-2d/editors/pixelEditorChrome.jsx +74 -0
- package/kits/physics-2d/editors/pixelGeometry.js +140 -0
- package/kits/physics-2d/editors/pixelInspector.jsx +633 -0
- package/kits/physics-2d/editors/pxArtEditorModel.js +732 -0
- package/kits/physics-2d/editors/pxArtPlayback.js +92 -0
- package/kits/physics-2d/editors/pxArtTimeline.jsx +752 -0
- package/kits/physics-2d/editors/pxArtTimeline.module.css +506 -0
- package/kits/physics-2d/editors/pxArtTools.js +232 -0
- package/kits/physics-2d/editors/useArtboardFit.js +102 -0
- package/kits/physics-2d/engine/ScenePlayer.jsx +196 -0
- package/kits/physics-2d/engine/SceneUI.jsx +59 -0
- package/kits/physics-2d/engine/assets.js +15 -0
- package/kits/physics-2d/engine/autoInspector.jsx +70 -0
- package/kits/physics-2d/engine/blueprint.js +521 -0
- package/kits/physics-2d/engine/collider.js +196 -0
- package/kits/physics-2d/engine/files.js +117 -0
- package/kits/physics-2d/engine/liveReload.js +88 -0
- package/kits/physics-2d/engine/pxart.js +1032 -0
- package/kits/physics-2d/engine/pxartSmooth.js +222 -0
- package/kits/physics-2d/engine/scene.js +686 -0
- package/kits/physics-2d/engine/spriteGeometry.js +32 -0
- package/kits/physics-2d/engine/ui.jsx +688 -0
- package/kits/physics-2d/engine/ui.module.css +2287 -0
- package/kits/physics-2d/eslint.config.js +71 -0
- package/kits/physics-2d/index.html +24 -0
- package/kits/physics-2d/main.jsx +24 -0
- package/kits/physics-2d/package-lock.json +2706 -0
- package/kits/physics-2d/package.json +42 -0
- package/kits/physics-2d/physics/PhysicsSystem.js +290 -0
- package/kits/physics-2d/physics/behaviors/AnalogStick.jsx +101 -0
- package/kits/physics-2d/physics/behaviors/Draggable.jsx +79 -0
- package/kits/physics-2d/physics/behaviors/RigidBody.jsx +55 -0
- package/kits/physics-2d/physics/behaviors/Slingshot.jsx +118 -0
- package/kits/physics-2d/physics/controls.js +79 -0
- package/kits/physics-2d/physics/index.js +26 -0
- package/kits/physics-2d/physics/matterBridge.js +126 -0
- package/kits/physics-2d/pnpm-lock.yaml +1761 -0
- package/kits/physics-2d/scenes/main.scene +12 -0
- package/kits/physics-2d/scenes/sandbox.scene +13 -0
- package/kits/physics-2d/scripts/draw.mjs +121 -0
- package/kits/physics-2d/vite.config.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
3
|
+
import { RESOLUTION_STEPS } from '../engine/pxart';
|
|
4
|
+
import { cx, Icon, IconButton, Panel, styles } from '../engine/ui';
|
|
5
|
+
|
|
6
|
+
// Brush/erase diameters offered by the pixel editors' size sliders.
|
|
7
|
+
export const BRUSH_SIZES = [1, 2, 3, 4, 6, 8, 12, 16, 24, 32];
|
|
8
|
+
|
|
9
|
+
// The tool strip for the pixel (PxArt) editor. Eyedropper is not in the strip —
|
|
10
|
+
// it lives in the palette panel / popover action button.
|
|
11
|
+
export const PIXEL_TOOLS = [
|
|
12
|
+
{ id: 'brush', label: 'Brush', icon: 'pencil' },
|
|
13
|
+
{ id: 'fill', label: 'Fill', icon: 'fill' },
|
|
14
|
+
{ id: 'shape', label: 'Shape', icon: 'shapes' },
|
|
15
|
+
{ id: 'select', label: 'Select', icon: 'marquee' },
|
|
16
|
+
{ id: 'move-all', label: 'Move all', icon: 'move' },
|
|
17
|
+
{ id: 'erase', label: 'Erase', icon: 'eraser' },
|
|
18
|
+
];
|
|
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
|
+
|
|
38
|
+
// Tools whose work is driven by the active palette color, so the palette panel
|
|
39
|
+
// (and its eyedropper action) shows.
|
|
40
|
+
export const PALETTE_TOOLS = new Set(['brush', 'fill', 'shape', 'eyedropper']);
|
|
41
|
+
|
|
42
|
+
// Shared tool-mode state for the pixel editors: the selected tool plus the
|
|
43
|
+
// brush/erase sizes and the fill/erase mode toggles. The active color/key stays
|
|
44
|
+
// editor-local (free hex vs fixed palette key).
|
|
45
|
+
//
|
|
46
|
+
// `picking` is a TRANSIENT eyedropper overlay, kept separate from `tool` so the
|
|
47
|
+
// real tool (and its settings panel) never changes when the picker is engaged —
|
|
48
|
+
// it just sits on top of whatever tool is active until you pick or toggle it off.
|
|
49
|
+
export function usePixelToolState() {
|
|
50
|
+
const [tool, setTool] = useState('brush');
|
|
51
|
+
const [picking, setPicking] = useState(false);
|
|
52
|
+
const [brushSize, setBrushSize] = useState(1);
|
|
53
|
+
const [eraseSize, setEraseSize] = useState(4);
|
|
54
|
+
const [eraseFillMode, setEraseFillMode] = useState(false);
|
|
55
|
+
const [fillSwapMode, setFillSwapMode] = useState(false);
|
|
56
|
+
const [shapeMode, setShapeMode] = useState('line');
|
|
57
|
+
const [shapeFill, setShapeFill] = useState(false);
|
|
58
|
+
return {
|
|
59
|
+
tool,
|
|
60
|
+
setTool,
|
|
61
|
+
picking,
|
|
62
|
+
setPicking,
|
|
63
|
+
brushSize,
|
|
64
|
+
setBrushSize,
|
|
65
|
+
eraseSize,
|
|
66
|
+
setEraseSize,
|
|
67
|
+
eraseFillMode,
|
|
68
|
+
setEraseFillMode,
|
|
69
|
+
fillSwapMode,
|
|
70
|
+
setFillSwapMode,
|
|
71
|
+
shapeMode,
|
|
72
|
+
setShapeMode,
|
|
73
|
+
shapeFill,
|
|
74
|
+
setShapeFill,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Vertical icon tool-strip shared by the pixel editors. `tools` is a list of
|
|
79
|
+
// { id, label, icon }; `disabled` greys out the whole strip (read-only files).
|
|
80
|
+
export function PixelToolStrip({ tools, tool, onSelectTool, ariaLabel, disabled = false }) {
|
|
81
|
+
return (
|
|
82
|
+
<div className={styles.drawingToolStrip} aria-label={ariaLabel}>
|
|
83
|
+
{tools.map((candidate) => (
|
|
84
|
+
<button
|
|
85
|
+
key={candidate.id}
|
|
86
|
+
type="button"
|
|
87
|
+
title={candidate.label}
|
|
88
|
+
aria-label={candidate.label}
|
|
89
|
+
aria-pressed={tool === candidate.id ? 'true' : 'false'}
|
|
90
|
+
disabled={disabled}
|
|
91
|
+
className={cx(
|
|
92
|
+
styles.drawingToolButton,
|
|
93
|
+
tool === candidate.id && styles.drawingToolSelected
|
|
94
|
+
)}
|
|
95
|
+
onClick={() => onSelectTool(candidate.id)}>
|
|
96
|
+
<Icon name={candidate.icon} />
|
|
97
|
+
</button>
|
|
98
|
+
))}
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Compact W × H selects above the artboard (not a sidebar panel).
|
|
104
|
+
export function CanvasSizeBar({ width, height, onResize }) {
|
|
105
|
+
return (
|
|
106
|
+
<div className={styles.canvasSizeBar} aria-label="Canvas size">
|
|
107
|
+
<ResolutionSelect label="Width" value={width} onChange={(w) => onResize(w, height)} />
|
|
108
|
+
<span className={styles.canvasSizeSep} aria-hidden="true">
|
|
109
|
+
×
|
|
110
|
+
</span>
|
|
111
|
+
<ResolutionSelect label="Height" value={height} onChange={(h) => onResize(width, h)} />
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// The 3 corner-radius presets offered by CornerRadiusBar. 0 is sharp/pixel;
|
|
117
|
+
// the other two are "nice" rounded amounts. The file format itself isn't
|
|
118
|
+
// limited to these three (any value up to MAX_CORNER_RADIUS is valid — see
|
|
119
|
+
// pxart.js) — this is just the curated set the editor's segmented control
|
|
120
|
+
// exposes, so hand-picking a value never means hunting a slider.
|
|
121
|
+
const CORNER_RADIUS_STEPS = [
|
|
122
|
+
{ value: 0, label: '0' },
|
|
123
|
+
{ value: 0.25, label: '¼' },
|
|
124
|
+
{ value: 0.5, label: '½' },
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
// Corner-radius segmented control, meant to sit next to CanvasSizeBar above
|
|
128
|
+
// the artboard. Sets the sprite's file-level `cornerRadius` field (a property
|
|
129
|
+
// of the .pxart asset — see docs/pxart-format.md — not a per-actor Sprite
|
|
130
|
+
// prop). `radius` is a plain number (0 = sharp/pixel corners); highlights
|
|
131
|
+
// whichever preset it exactly matches, or none if it's a value from outside
|
|
132
|
+
// this set (e.g. hand-edited JSON).
|
|
133
|
+
export function CornerRadiusBar({ radius, onChange }) {
|
|
134
|
+
return (
|
|
135
|
+
<div className={styles.cornerRadiusBar} aria-label="Corner rounding">
|
|
136
|
+
<span>Round</span>
|
|
137
|
+
<div className={styles.cornerRadiusSegments} role="radiogroup" aria-label="Corner radius">
|
|
138
|
+
{CORNER_RADIUS_STEPS.map((step) => (
|
|
139
|
+
<button
|
|
140
|
+
key={step.value}
|
|
141
|
+
type="button"
|
|
142
|
+
role="radio"
|
|
143
|
+
aria-checked={radius === step.value ? 'true' : 'false'}
|
|
144
|
+
className={cx(styles.cornerRadiusSegment, radius === step.value && styles.cornerRadiusSegmentOn)}
|
|
145
|
+
title={step.value === 0 ? 'Sharp (pixel) corners' : `Round corners, radius ${step.value}`}
|
|
146
|
+
onClick={() => onChange(step.value)}>
|
|
147
|
+
{step.label}
|
|
148
|
+
</button>
|
|
149
|
+
))}
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function ResolutionSelect({ label, value, onChange }) {
|
|
156
|
+
const options = RESOLUTION_STEPS.includes(value)
|
|
157
|
+
? RESOLUTION_STEPS
|
|
158
|
+
: [...RESOLUTION_STEPS, value].sort((a, b) => a - b);
|
|
159
|
+
return (
|
|
160
|
+
<select
|
|
161
|
+
className={styles.canvasSizeSelect}
|
|
162
|
+
aria-label={label}
|
|
163
|
+
value={value}
|
|
164
|
+
onChange={(event) => onChange(Number(event.target.value))}>
|
|
165
|
+
{options.map((step) => (
|
|
166
|
+
<option key={step} value={step}>
|
|
167
|
+
{step}
|
|
168
|
+
</option>
|
|
169
|
+
))}
|
|
170
|
+
</select>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// 12-column palette grid shared by the docked sidebar and the compact popover.
|
|
175
|
+
export function PaletteGrid({ keys, palette, activeKey, onSelectKey }) {
|
|
176
|
+
return (
|
|
177
|
+
<div className={styles.palette}>
|
|
178
|
+
{keys.map((key) => (
|
|
179
|
+
<button
|
|
180
|
+
key={key}
|
|
181
|
+
type="button"
|
|
182
|
+
className={cx(styles.swatch, activeKey === key && styles.swatchSelected)}
|
|
183
|
+
title={palette[key]}
|
|
184
|
+
style={{ background: palette[key] }}
|
|
185
|
+
onClick={() => onSelectKey(key)}
|
|
186
|
+
/>
|
|
187
|
+
))}
|
|
188
|
+
</div>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Anchored palette + eyedropper popover for compact/mobile paint-strip color tap.
|
|
193
|
+
export function PalettePopover({ open, anchorRef, onClose, children }) {
|
|
194
|
+
const popoverRef = useRef(null);
|
|
195
|
+
const [position, setPosition] = useState({ top: 0, left: 0 });
|
|
196
|
+
|
|
197
|
+
useLayoutEffect(() => {
|
|
198
|
+
if (!open || !anchorRef.current) return;
|
|
199
|
+
const anchor = anchorRef.current.getBoundingClientRect();
|
|
200
|
+
const popoverWidth = 280;
|
|
201
|
+
const margin = 8;
|
|
202
|
+
let left = anchor.left - popoverWidth - margin;
|
|
203
|
+
if (left < margin) left = anchor.right + margin;
|
|
204
|
+
let top = anchor.top;
|
|
205
|
+
const maxTop = window.innerHeight - margin;
|
|
206
|
+
if (top + 320 > maxTop) top = Math.max(margin, maxTop - 320);
|
|
207
|
+
setPosition({ top, left });
|
|
208
|
+
}, [open, anchorRef]);
|
|
209
|
+
|
|
210
|
+
useEffect(() => {
|
|
211
|
+
if (!open) return undefined;
|
|
212
|
+
function onKeyDown(event) {
|
|
213
|
+
if (event.key === 'Escape') onClose();
|
|
214
|
+
}
|
|
215
|
+
function onPointerDown(event) {
|
|
216
|
+
if (popoverRef.current?.contains(event.target) || anchorRef.current?.contains(event.target)) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
onClose();
|
|
220
|
+
}
|
|
221
|
+
window.addEventListener('keydown', onKeyDown);
|
|
222
|
+
window.addEventListener('pointerdown', onPointerDown);
|
|
223
|
+
return () => {
|
|
224
|
+
window.removeEventListener('keydown', onKeyDown);
|
|
225
|
+
window.removeEventListener('pointerdown', onPointerDown);
|
|
226
|
+
};
|
|
227
|
+
}, [open, onClose, anchorRef]);
|
|
228
|
+
|
|
229
|
+
if (!open) return null;
|
|
230
|
+
return createPortal(
|
|
231
|
+
<div
|
|
232
|
+
ref={popoverRef}
|
|
233
|
+
className={styles.palettePopover}
|
|
234
|
+
style={{ top: position.top, left: position.left }}
|
|
235
|
+
role="dialog"
|
|
236
|
+
aria-label="Palette">
|
|
237
|
+
{children}
|
|
238
|
+
</div>,
|
|
239
|
+
document.body
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// The shape tool's sub-mode picker, shared by the compact paint strip and the
|
|
244
|
+
// wide-layout settings panel. `buttonClass` styles each glyph button for its
|
|
245
|
+
// host strip; `radio` switches the a11y semantics to a radiogroup.
|
|
246
|
+
function ShapeModeButtons({ shapeMode, setShapeMode, buttonClass, radio = false }) {
|
|
247
|
+
return (
|
|
248
|
+
<>
|
|
249
|
+
{SHAPE_MODES.map((mode) => {
|
|
250
|
+
const selected = shapeMode === mode.id;
|
|
251
|
+
return (
|
|
252
|
+
<button
|
|
253
|
+
key={mode.id}
|
|
254
|
+
type="button"
|
|
255
|
+
role={radio ? 'radio' : undefined}
|
|
256
|
+
className={cx(buttonClass, selected && styles.drawingToolSelected)}
|
|
257
|
+
title={mode.label}
|
|
258
|
+
aria-label={mode.label}
|
|
259
|
+
aria-pressed={radio ? undefined : selected ? 'true' : 'false'}
|
|
260
|
+
aria-checked={radio ? (selected ? 'true' : 'false') : undefined}
|
|
261
|
+
onClick={() => setShapeMode(mode.id)}>
|
|
262
|
+
<Icon name={mode.icon} />
|
|
263
|
+
</button>
|
|
264
|
+
);
|
|
265
|
+
})}
|
|
266
|
+
</>
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// The move tool's transform actions (flip H / flip V / rotate CW), shared by the
|
|
271
|
+
// compact paint strip and the wide-layout settings panel. These are momentary
|
|
272
|
+
// buttons (no selected state); `buttonClass` styles them for the host strip.
|
|
273
|
+
function MoveActionButtons({ moveActions, buttonClass }) {
|
|
274
|
+
return (
|
|
275
|
+
<>
|
|
276
|
+
{MOVE_ACTIONS.map((action) => (
|
|
277
|
+
<button
|
|
278
|
+
key={action.id}
|
|
279
|
+
type="button"
|
|
280
|
+
className={buttonClass}
|
|
281
|
+
title={action.label}
|
|
282
|
+
aria-label={action.label}
|
|
283
|
+
onClick={moveActions?.[action.id]}>
|
|
284
|
+
<Icon name={action.icon} />
|
|
285
|
+
</button>
|
|
286
|
+
))}
|
|
287
|
+
</>
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Right-side paint strip (compact layout): color swatch, vertical size slider, and
|
|
292
|
+
// tool-contextual mode toggles. Color tap opens the palette popover.
|
|
293
|
+
export function PaintStrip({
|
|
294
|
+
toolState,
|
|
295
|
+
activeKey,
|
|
296
|
+
activeColor,
|
|
297
|
+
paletteOpen,
|
|
298
|
+
onTogglePalette,
|
|
299
|
+
colorButtonRef,
|
|
300
|
+
onSelectKey,
|
|
301
|
+
paletteKeys,
|
|
302
|
+
palette,
|
|
303
|
+
moveActions,
|
|
304
|
+
selectActions,
|
|
305
|
+
}) {
|
|
306
|
+
const {
|
|
307
|
+
tool,
|
|
308
|
+
picking,
|
|
309
|
+
setPicking,
|
|
310
|
+
brushSize,
|
|
311
|
+
eraseSize,
|
|
312
|
+
eraseFillMode,
|
|
313
|
+
fillSwapMode,
|
|
314
|
+
shapeMode,
|
|
315
|
+
shapeFill,
|
|
316
|
+
setBrushSize,
|
|
317
|
+
setEraseSize,
|
|
318
|
+
setEraseFillMode,
|
|
319
|
+
setFillSwapMode,
|
|
320
|
+
setShapeMode,
|
|
321
|
+
setShapeFill,
|
|
322
|
+
} = toolState;
|
|
323
|
+
|
|
324
|
+
const showBrushSize = tool === 'brush';
|
|
325
|
+
const showEraseSize = tool === 'erase';
|
|
326
|
+
const size = showBrushSize ? brushSize : eraseSize;
|
|
327
|
+
const onSizeChange = showBrushSize ? setBrushSize : setEraseSize;
|
|
328
|
+
const sliderIndex = Math.max(0, BRUSH_SIZES.indexOf(size));
|
|
329
|
+
const sliderDisabled = showEraseSize && eraseFillMode;
|
|
330
|
+
|
|
331
|
+
return (
|
|
332
|
+
<>
|
|
333
|
+
<div className={styles.paintStrip} aria-label="Paint controls">
|
|
334
|
+
{PALETTE_TOOLS.has(tool) || picking ? (
|
|
335
|
+
<div className={styles.paintStripSection}>
|
|
336
|
+
<button
|
|
337
|
+
ref={colorButtonRef}
|
|
338
|
+
type="button"
|
|
339
|
+
className={styles.paintStripColor}
|
|
340
|
+
title={activeColor}
|
|
341
|
+
aria-label={`Active color ${activeColor}. Open palette.`}
|
|
342
|
+
aria-expanded={paletteOpen ? 'true' : 'false'}
|
|
343
|
+
style={{ background: activeColor }}
|
|
344
|
+
onClick={onTogglePalette}
|
|
345
|
+
/>
|
|
346
|
+
</div>
|
|
347
|
+
) : null}
|
|
348
|
+
{showBrushSize || showEraseSize ? (
|
|
349
|
+
<div className={styles.paintStripSection}>
|
|
350
|
+
<div className={styles.paintStripSlider}>
|
|
351
|
+
<input
|
|
352
|
+
className={styles.drawingSizeSliderVertical}
|
|
353
|
+
type="range"
|
|
354
|
+
min="0"
|
|
355
|
+
max={BRUSH_SIZES.length - 1}
|
|
356
|
+
step="1"
|
|
357
|
+
value={sliderIndex}
|
|
358
|
+
disabled={sliderDisabled}
|
|
359
|
+
aria-label={showBrushSize ? 'Brush size' : 'Erase size'}
|
|
360
|
+
title={`${showBrushSize ? 'Brush' : 'Erase'} size ${size}`}
|
|
361
|
+
onChange={(event) => onSizeChange(BRUSH_SIZES[Number(event.target.value)])}
|
|
362
|
+
/>
|
|
363
|
+
</div>
|
|
364
|
+
</div>
|
|
365
|
+
) : null}
|
|
366
|
+
{tool === 'shape' ? (
|
|
367
|
+
<div className={styles.paintStripSection}>
|
|
368
|
+
<ShapeModeButtons
|
|
369
|
+
shapeMode={shapeMode}
|
|
370
|
+
setShapeMode={setShapeMode}
|
|
371
|
+
buttonClass={styles.paintStripModeButton}
|
|
372
|
+
/>
|
|
373
|
+
</div>
|
|
374
|
+
) : null}
|
|
375
|
+
{tool === 'move-all' ? (
|
|
376
|
+
<div className={styles.paintStripSection}>
|
|
377
|
+
<MoveActionButtons moveActions={moveActions} buttonClass={styles.paintStripModeButton} />
|
|
378
|
+
</div>
|
|
379
|
+
) : null}
|
|
380
|
+
{tool === 'select' ? (
|
|
381
|
+
<div className={styles.paintStripSection}>
|
|
382
|
+
<button
|
|
383
|
+
type="button"
|
|
384
|
+
className={styles.paintStripModeButton}
|
|
385
|
+
title="Delete selection"
|
|
386
|
+
aria-label="Delete selection"
|
|
387
|
+
disabled={!selectActions?.hasSelection}
|
|
388
|
+
onClick={selectActions?.deletePixels}>
|
|
389
|
+
<Icon name="trash" />
|
|
390
|
+
</button>
|
|
391
|
+
</div>
|
|
392
|
+
) : null}
|
|
393
|
+
{tool === 'erase' ? (
|
|
394
|
+
<div className={styles.paintStripSection}>
|
|
395
|
+
<button
|
|
396
|
+
type="button"
|
|
397
|
+
className={cx(
|
|
398
|
+
styles.paintStripModeButton,
|
|
399
|
+
eraseFillMode && styles.drawingToolSelected
|
|
400
|
+
)}
|
|
401
|
+
title="Fill mode"
|
|
402
|
+
aria-label="Fill mode"
|
|
403
|
+
aria-pressed={eraseFillMode ? 'true' : 'false'}
|
|
404
|
+
onClick={() => setEraseFillMode(!eraseFillMode)}>
|
|
405
|
+
<Icon name="fill" />
|
|
406
|
+
</button>
|
|
407
|
+
</div>
|
|
408
|
+
) : null}
|
|
409
|
+
{tool === 'fill' ? (
|
|
410
|
+
<div className={styles.paintStripSection}>
|
|
411
|
+
<button
|
|
412
|
+
type="button"
|
|
413
|
+
className={cx(
|
|
414
|
+
styles.paintStripModeButton,
|
|
415
|
+
fillSwapMode && styles.drawingToolSelected
|
|
416
|
+
)}
|
|
417
|
+
title="Swap mode"
|
|
418
|
+
aria-label="Swap mode"
|
|
419
|
+
aria-pressed={fillSwapMode ? 'true' : 'false'}
|
|
420
|
+
onClick={() => setFillSwapMode(!fillSwapMode)}>
|
|
421
|
+
<Icon name="rotate" />
|
|
422
|
+
</button>
|
|
423
|
+
</div>
|
|
424
|
+
) : null}
|
|
425
|
+
{tool === 'shape' ? (
|
|
426
|
+
<div className={styles.paintStripSection}>
|
|
427
|
+
<button
|
|
428
|
+
type="button"
|
|
429
|
+
className={cx(
|
|
430
|
+
styles.paintStripModeButton,
|
|
431
|
+
shapeFill && styles.drawingToolSelected
|
|
432
|
+
)}
|
|
433
|
+
title="Fill shape"
|
|
434
|
+
aria-label="Fill shape"
|
|
435
|
+
aria-pressed={shapeFill ? 'true' : 'false'}
|
|
436
|
+
onClick={() => setShapeFill(!shapeFill)}>
|
|
437
|
+
<Icon name="fill" />
|
|
438
|
+
</button>
|
|
439
|
+
</div>
|
|
440
|
+
) : null}
|
|
441
|
+
</div>
|
|
442
|
+
<PalettePopover open={paletteOpen} anchorRef={colorButtonRef} onClose={() => onTogglePalette(false)}>
|
|
443
|
+
<div className={styles.palettePopoverHeader}>
|
|
444
|
+
<span className={styles.palettePopoverTitle}>Palette</span>
|
|
445
|
+
<IconButton
|
|
446
|
+
icon="eyedropper"
|
|
447
|
+
label="Pick color"
|
|
448
|
+
active={picking}
|
|
449
|
+
aria-pressed={picking ? 'true' : 'false'}
|
|
450
|
+
onClick={() => setPicking(!picking)}
|
|
451
|
+
/>
|
|
452
|
+
</div>
|
|
453
|
+
<PaletteGrid
|
|
454
|
+
keys={paletteKeys}
|
|
455
|
+
palette={palette}
|
|
456
|
+
activeKey={activeKey}
|
|
457
|
+
onSelectKey={(key) => {
|
|
458
|
+
onSelectKey(key);
|
|
459
|
+
onTogglePalette(false);
|
|
460
|
+
}}
|
|
461
|
+
/>
|
|
462
|
+
</PalettePopover>
|
|
463
|
+
</>
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Wide-layout docked inspector: palette (when relevant) + tool settings only.
|
|
468
|
+
export function PxArtInspectorDock({ toolState, activeKey, onSelectKey, paletteKeys, palette, moveActions, selectActions }) {
|
|
469
|
+
const { tool, picking, setPicking } = toolState;
|
|
470
|
+
return (
|
|
471
|
+
<aside className={cx(styles.pxArtInspector, styles.drawingToolsPanel)}>
|
|
472
|
+
<div className={cx(styles.sheetBody, styles.inspectorBody, styles.drawingSettingsColumn)}>
|
|
473
|
+
{PALETTE_TOOLS.has(tool) || picking ? (
|
|
474
|
+
<Panel
|
|
475
|
+
title="Palette"
|
|
476
|
+
action={
|
|
477
|
+
<IconButton
|
|
478
|
+
icon="eyedropper"
|
|
479
|
+
label="Pick color"
|
|
480
|
+
active={picking}
|
|
481
|
+
aria-pressed={picking ? 'true' : 'false'}
|
|
482
|
+
onClick={() => setPicking(!picking)}
|
|
483
|
+
/>
|
|
484
|
+
}>
|
|
485
|
+
<PaletteGrid
|
|
486
|
+
keys={paletteKeys}
|
|
487
|
+
palette={palette}
|
|
488
|
+
activeKey={activeKey}
|
|
489
|
+
onSelectKey={onSelectKey}
|
|
490
|
+
/>
|
|
491
|
+
</Panel>
|
|
492
|
+
) : null}
|
|
493
|
+
<ToolSettingsPanel toolState={toolState} moveActions={moveActions} selectActions={selectActions} />
|
|
494
|
+
</div>
|
|
495
|
+
</aside>
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// A labeled on/off switch used for the editors' boolean tool modes.
|
|
500
|
+
export function ModeToggle({ label, active, onChange }) {
|
|
501
|
+
return (
|
|
502
|
+
<div className={styles.drawingModeRow}>
|
|
503
|
+
<span>{label}</span>
|
|
504
|
+
<button
|
|
505
|
+
type="button"
|
|
506
|
+
role="switch"
|
|
507
|
+
className={cx(styles.drawingSwitch, active && styles.drawingSwitchOn)}
|
|
508
|
+
aria-checked={active ? 'true' : 'false'}
|
|
509
|
+
onClick={() => onChange(!active)}>
|
|
510
|
+
<span className={styles.drawingSwitchKnob} />
|
|
511
|
+
</button>
|
|
512
|
+
</div>
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// The "Tool Settings" panel for the pixel editor: brush/erase size sliders plus
|
|
517
|
+
// the per-tool mode toggle / hint. It reads the whole usePixelToolState bundle
|
|
518
|
+
// to keep callers from re-spreading the same prop list.
|
|
519
|
+
export function ToolSettingsPanel({ toolState, moveActions, selectActions }) {
|
|
520
|
+
const {
|
|
521
|
+
tool,
|
|
522
|
+
brushSize,
|
|
523
|
+
eraseSize,
|
|
524
|
+
eraseFillMode,
|
|
525
|
+
fillSwapMode,
|
|
526
|
+
shapeMode,
|
|
527
|
+
shapeFill,
|
|
528
|
+
setBrushSize,
|
|
529
|
+
setEraseSize,
|
|
530
|
+
setEraseFillMode,
|
|
531
|
+
setFillSwapMode,
|
|
532
|
+
setShapeMode,
|
|
533
|
+
setShapeFill,
|
|
534
|
+
} = toolState;
|
|
535
|
+
return (
|
|
536
|
+
<Panel title="Tool Settings">
|
|
537
|
+
<div className={styles.drawingToolSettings}>
|
|
538
|
+
<ToolSizePicker
|
|
539
|
+
label="Brush"
|
|
540
|
+
value={brushSize}
|
|
541
|
+
visible={tool === 'brush'}
|
|
542
|
+
onChange={setBrushSize}
|
|
543
|
+
/>
|
|
544
|
+
<ToolSizePicker
|
|
545
|
+
label="Erase"
|
|
546
|
+
value={eraseSize}
|
|
547
|
+
visible={tool === 'erase'}
|
|
548
|
+
onChange={setEraseSize}
|
|
549
|
+
fill={eraseFillMode}
|
|
550
|
+
onFillChange={setEraseFillMode}
|
|
551
|
+
/>
|
|
552
|
+
{tool === 'fill' ? (
|
|
553
|
+
<ModeToggle label="Swap Mode" active={fillSwapMode} onChange={setFillSwapMode} />
|
|
554
|
+
) : null}
|
|
555
|
+
{tool === 'shape' ? (
|
|
556
|
+
<div className={styles.drawingSizePicker}>
|
|
557
|
+
<div className={styles.drawingSizeLabel}>
|
|
558
|
+
<span>Shape</span>
|
|
559
|
+
</div>
|
|
560
|
+
<div className={styles.drawingShapeModeRow} role="radiogroup" aria-label="Shape">
|
|
561
|
+
<ShapeModeButtons
|
|
562
|
+
shapeMode={shapeMode}
|
|
563
|
+
setShapeMode={setShapeMode}
|
|
564
|
+
buttonClass={styles.drawingToolButton}
|
|
565
|
+
radio
|
|
566
|
+
/>
|
|
567
|
+
</div>
|
|
568
|
+
<ModeToggle label="Fill Shape" active={shapeFill} onChange={setShapeFill} />
|
|
569
|
+
</div>
|
|
570
|
+
) : null}
|
|
571
|
+
{tool === 'move-all' ? (
|
|
572
|
+
<div className={styles.drawingSizePicker}>
|
|
573
|
+
<div className={styles.drawingSizeLabel}>
|
|
574
|
+
<span>Transform</span>
|
|
575
|
+
</div>
|
|
576
|
+
<div className={styles.drawingShapeModeRow}>
|
|
577
|
+
<MoveActionButtons moveActions={moveActions} buttonClass={styles.drawingToolButton} />
|
|
578
|
+
</div>
|
|
579
|
+
</div>
|
|
580
|
+
) : null}
|
|
581
|
+
{tool === 'select' ? (
|
|
582
|
+
<div className={styles.drawingSizePicker}>
|
|
583
|
+
<div className={styles.drawingSizeLabel}>
|
|
584
|
+
<span>Selection</span>
|
|
585
|
+
</div>
|
|
586
|
+
<div className={styles.drawingShapeModeRow}>
|
|
587
|
+
<button
|
|
588
|
+
type="button"
|
|
589
|
+
className={styles.drawingToolButton}
|
|
590
|
+
title="Delete selection"
|
|
591
|
+
aria-label="Delete selection"
|
|
592
|
+
disabled={!selectActions?.hasSelection}
|
|
593
|
+
onClick={selectActions?.deletePixels}>
|
|
594
|
+
<Icon name="trash" />
|
|
595
|
+
</button>
|
|
596
|
+
</div>
|
|
597
|
+
</div>
|
|
598
|
+
) : null}
|
|
599
|
+
{tool === 'eyedropper' ? (
|
|
600
|
+
<div className={styles.drawingToolHint}>Click a pixel to make it the active color.</div>
|
|
601
|
+
) : null}
|
|
602
|
+
</div>
|
|
603
|
+
</Panel>
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Discrete brush/erase size slider (steps through BRUSH_SIZES). Hidden unless
|
|
608
|
+
// `visible`. An optional fill toggle disables the slider when fill mode is on.
|
|
609
|
+
export function ToolSizePicker({ label, value, visible, onChange, fill = false, onFillChange }) {
|
|
610
|
+
if (!visible) return null;
|
|
611
|
+
const sliderIndex = Math.max(0, BRUSH_SIZES.indexOf(value));
|
|
612
|
+
return (
|
|
613
|
+
<div className={styles.drawingSizePicker}>
|
|
614
|
+
<div className={styles.drawingSizeLabel}>
|
|
615
|
+
<span>{label} size</span>
|
|
616
|
+
<span>{value}</span>
|
|
617
|
+
</div>
|
|
618
|
+
<div className={styles.drawingSizeControlRow}>
|
|
619
|
+
<input
|
|
620
|
+
className={styles.drawingSizeSlider}
|
|
621
|
+
type="range"
|
|
622
|
+
min="0"
|
|
623
|
+
max={BRUSH_SIZES.length - 1}
|
|
624
|
+
step="1"
|
|
625
|
+
value={sliderIndex}
|
|
626
|
+
disabled={fill}
|
|
627
|
+
onChange={(event) => onChange(BRUSH_SIZES[Number(event.target.value)])}
|
|
628
|
+
/>
|
|
629
|
+
</div>
|
|
630
|
+
{onFillChange ? <ModeToggle label="Fill Mode" active={fill} onChange={onFillChange} /> : null}
|
|
631
|
+
</div>
|
|
632
|
+
);
|
|
633
|
+
}
|