castle-web-cli 0.4.128 → 0.4.130
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 +4 -4
- package/dist/filesChanged.d.ts +2 -0
- package/dist/filesChanged.js +6 -2
- package/dist/ide.js +15 -4
- package/dist/importBrowse.js +1 -0
- package/dist/index.js +10 -4
- package/dist/init.js +63 -15
- package/dist/native/loop.js +1 -1
- package/dist/native/tools.js +1 -1
- package/dist/serve.d.ts +1 -0
- package/dist/serve.js +20 -1
- package/dist/shell/assets/index-BpOLUyVO.js +441 -0
- package/dist/shell/assets/{index-CAq6f9B5.css → index-D6K-0YDB.css} +1 -1
- package/dist/shell/index.html +2 -2
- package/kits/physics-2d/CLAUDE.md +40 -23
- package/kits/physics-2d/behaviors/Joints.jsx +4 -3
- package/kits/physics-2d/behaviors/SoundPlayer.jsx +266 -0
- package/kits/physics-2d/behaviors/Sprite.jsx +3 -3
- package/kits/physics-2d/behaviors/Video.jsx +4 -3
- package/kits/physics-2d/castle.json +1 -1
- package/kits/physics-2d/editors/PlayOnly.jsx +15 -6
- package/kits/physics-2d/editors/PxArtEditor.jsx +95 -31
- package/kits/physics-2d/editors/SceneEditor.jsx +5 -5
- package/kits/physics-2d/editors/SingleEditor.jsx +37 -8
- package/kits/physics-2d/editors/StyleEditor.jsx +7 -4
- package/kits/physics-2d/editors/pixelEditorChrome.jsx +7 -7
- package/kits/physics-2d/editors/pixelInspector.jsx +13 -7
- package/kits/physics-2d/editors/pxArtTimeline.jsx +29 -11
- package/kits/physics-2d/editors/pxArtTimeline.module.css +11 -0
- package/kits/physics-2d/engine/audioContext.js +41 -0
- package/kits/physics-2d/engine/autoInspector.jsx +50 -33
- package/kits/physics-2d/engine/blueprint.js +15 -3
- package/kits/physics-2d/engine/liveReload.js +33 -21
- package/kits/physics-2d/engine/media.js +29 -39
- package/kits/physics-2d/engine/sound.js +316 -0
- package/kits/physics-2d/engine/systemRegistry.js +1 -1
- package/kits/physics-2d/engine/ui.jsx +274 -5
- package/kits/physics-2d/engine/ui.module.css +137 -16
- package/kits/physics-2d/scripts/testsounds.mjs +84 -0
- package/kits/physics-2d/systems/media.js +31 -15
- package/kits/physics-3d/CLAUDE.md +109 -0
- package/kits/physics-3d/behaviors/Body.jsx +51 -0
- package/kits/physics-3d/behaviors/Door.jsx +31 -0
- package/kits/physics-3d/behaviors/Lookable.jsx +11 -0
- package/kits/physics-3d/behaviors/Model.jsx +14 -0
- package/kits/physics-3d/behaviors/Pickup.jsx +39 -0
- package/kits/physics-3d/behaviors/Player.jsx +172 -0
- package/kits/physics-3d/behaviors/Portal.jsx +19 -0
- package/kits/physics-3d/behaviors/Shape.jsx +75 -0
- package/kits/physics-3d/behaviors/Solid.jsx +11 -0
- package/kits/physics-3d/behaviors/Transform.jsx +24 -0
- package/kits/physics-3d/blueprints/barrel.scene +15 -0
- package/kits/physics-3d/blueprints/crate.scene +29 -0
- package/kits/physics-3d/blueprints/door.scene +35 -0
- package/kits/physics-3d/blueprints/gem.scene +26 -0
- package/kits/physics-3d/blueprints/pillar.scene +25 -0
- package/kits/physics-3d/blueprints/platform.scene +23 -0
- package/kits/physics-3d/blueprints/player.scene +29 -0
- package/kits/physics-3d/blueprints/portal.scene +21 -0
- package/kits/physics-3d/blueprints/rock.scene +14 -0
- package/kits/physics-3d/blueprints/statue.scene +26 -0
- package/kits/physics-3d/blueprints/tree.scene +14 -0
- package/kits/physics-3d/blueprints/wall.scene +26 -0
- package/kits/{basic-2d → physics-3d}/castle.json +23 -5
- package/kits/physics-3d/docs/pxmodel-format.md +111 -0
- package/kits/physics-3d/drawings/crate.pxart +26 -0
- package/kits/physics-3d/drawings/door-edge.pxart +26 -0
- package/kits/physics-3d/drawings/door.pxart +27 -0
- package/kits/physics-3d/drawings/face.pxart +26 -0
- package/kits/physics-3d/drawings/floor.pxart +27 -0
- package/kits/physics-3d/drawings/platform.pxart +27 -0
- package/kits/physics-3d/drawings/statue.pxart +27 -0
- package/kits/physics-3d/drawings/wall.pxart +27 -0
- package/kits/physics-3d/engine3d/PlayOnly3D.jsx +32 -0
- package/kits/physics-3d/engine3d/PxModelEditor.jsx +619 -0
- package/kits/physics-3d/engine3d/Scene3DEditor.jsx +618 -0
- package/kits/physics-3d/engine3d/Scene3DPlayer.jsx +309 -0
- package/kits/physics-3d/engine3d/editor3dData.js +114 -0
- package/kits/physics-3d/engine3d/editor3dInspector.jsx +404 -0
- package/kits/physics-3d/engine3d/editorChrome.jsx +218 -0
- package/kits/physics-3d/engine3d/editorWorld.js +249 -0
- package/kits/physics-3d/engine3d/materials.js +174 -0
- package/kits/physics-3d/engine3d/meshops.js +123 -0
- package/kits/physics-3d/engine3d/modelEditorWorld.js +439 -0
- package/kits/physics-3d/engine3d/modelFiles.js +139 -0
- package/kits/physics-3d/engine3d/pxmodel.js +265 -0
- package/kits/physics-3d/engine3d/thumbnails.js +137 -0
- package/kits/physics-3d/engine3d/world3d.js +216 -0
- package/kits/{basic-2d → physics-3d}/eslint.config.js +10 -25
- package/kits/physics-3d/index.html +20 -0
- package/kits/physics-3d/main.jsx +30 -0
- package/kits/physics-3d/models/barrel.pxmodel +10 -0
- package/kits/physics-3d/models/player.pxmodel +222 -0
- package/kits/physics-3d/models/rock.pxmodel +30 -0
- package/kits/physics-3d/models/tree.pxmodel +10 -0
- package/kits/{basic-2d → physics-3d}/package-lock.json +140 -119
- package/kits/{basic-2d → physics-3d}/package.json +11 -9
- package/kits/{basic-2d → physics-3d}/pnpm-lock.yaml +162 -137
- package/kits/physics-3d/scenes/main.scene +414 -0
- package/kits/physics-3d/scenes/model-lab.scene +127 -0
- package/kits/physics-3d/systems/physics3d.js +356 -0
- package/package.json +5 -2
- package/dist/shell/assets/index-DKu9ejyh.js +0 -436
- package/kits/basic-2d/CLAUDE.md +0 -221
- package/kits/basic-2d/behaviors/Camera.jsx +0 -43
- package/kits/basic-2d/behaviors/Collider.jsx +0 -213
- package/kits/basic-2d/behaviors/Layout.jsx +0 -53
- package/kits/basic-2d/behaviors/Sprite.jsx +0 -357
- package/kits/basic-2d/behaviors/tint.js +0 -47
- package/kits/basic-2d/blueprints/cauldron.scene +0 -20
- package/kits/basic-2d/docs/pxart-format.md +0 -377
- package/kits/basic-2d/drawings/cauldron.pxart +0 -113
- package/kits/basic-2d/editors/BlueprintLibrary.jsx +0 -270
- package/kits/basic-2d/editors/ErrorBoundary.jsx +0 -59
- package/kits/basic-2d/editors/PlayOnly.jsx +0 -31
- package/kits/basic-2d/editors/PxArtEditor.jsx +0 -1092
- package/kits/basic-2d/editors/SceneEditor.jsx +0 -1780
- package/kits/basic-2d/editors/SelectionOverlay.jsx +0 -909
- package/kits/basic-2d/editors/SingleEditor.jsx +0 -122
- package/kits/basic-2d/editors/behaviorRegistry.js +0 -34
- package/kits/basic-2d/editors/editorHistory.js +0 -157
- package/kits/basic-2d/editors/inspectorSheet.js +0 -13
- package/kits/basic-2d/editors/pixelCanvas.js +0 -11
- package/kits/basic-2d/editors/pixelEditorChrome.jsx +0 -74
- package/kits/basic-2d/editors/pixelGeometry.js +0 -140
- package/kits/basic-2d/editors/pixelInspector.jsx +0 -633
- package/kits/basic-2d/editors/pxArtEditorModel.js +0 -732
- package/kits/basic-2d/editors/pxArtPlayback.js +0 -92
- package/kits/basic-2d/editors/pxArtTimeline.jsx +0 -752
- package/kits/basic-2d/editors/pxArtTimeline.module.css +0 -506
- package/kits/basic-2d/editors/pxArtTools.js +0 -232
- package/kits/basic-2d/editors/useArtboardFit.js +0 -105
- package/kits/basic-2d/engine/ScenePlayer.jsx +0 -209
- package/kits/basic-2d/engine/SceneUI.jsx +0 -59
- package/kits/basic-2d/engine/assets.js +0 -15
- package/kits/basic-2d/engine/autoInspector.jsx +0 -70
- package/kits/basic-2d/engine/behaviorExtensions.js +0 -32
- package/kits/basic-2d/engine/blueprint.js +0 -557
- package/kits/basic-2d/engine/collider.js +0 -200
- package/kits/basic-2d/engine/files.js +0 -141
- package/kits/basic-2d/engine/liveReload.js +0 -88
- package/kits/basic-2d/engine/pxart.js +0 -1032
- package/kits/basic-2d/engine/pxartSmooth.js +0 -222
- package/kits/basic-2d/engine/scene.js +0 -696
- package/kits/basic-2d/engine/spriteGeometry.js +0 -32
- package/kits/basic-2d/engine/systemRegistry.js +0 -16
- package/kits/basic-2d/engine/ui.jsx +0 -695
- package/kits/basic-2d/engine/ui.module.css +0 -2287
- package/kits/basic-2d/index.html +0 -24
- package/kits/basic-2d/main.jsx +0 -24
- package/kits/basic-2d/scenes/main.scene +0 -16
- package/kits/basic-2d/scripts/draw.mjs +0 -121
- package/kits/physics-2d/behaviors/Sound.jsx +0 -163
- package/kits/physics-2d/behaviors/Tone.jsx +0 -95
- package/kits/physics-2d/engine/tone.js +0 -112
- /package/kits/{basic-2d → physics-3d}/.prettierrc +0 -0
- /package/kits/{basic-2d → physics-3d}/vite.config.js +0 -0
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
pickerPageIndexFor,
|
|
17
17
|
pickerPagesFor,
|
|
18
18
|
} from '../engine/palettes';
|
|
19
|
-
import { EditorBody, Icon, IconButton, styles } from '../engine/ui';
|
|
19
|
+
import { cx, EditorBody, Icon, IconButton, styles } from '../engine/ui';
|
|
20
20
|
import { eventToCell, eventToPoint } from './pixelCanvas';
|
|
21
21
|
import { forEachDab } from './pixelGeometry';
|
|
22
22
|
import { drawPathOverlay } from './pathOverlay';
|
|
@@ -195,7 +195,22 @@ const PATH_ONLY_TOOL_IDS = new Set(
|
|
|
195
195
|
)
|
|
196
196
|
);
|
|
197
197
|
|
|
198
|
-
|
|
198
|
+
// Layer-visibility overlay for a read-only file. The eye toggle stays live so
|
|
199
|
+
// you can hide reference layers and read the sprite; it never writes.
|
|
200
|
+
function applyLayerVisibility(sprite, overrides) {
|
|
201
|
+
if (!sprite || !overrides) return sprite;
|
|
202
|
+
let changed = false;
|
|
203
|
+
const layers = sprite.layers.map((layer, i) => {
|
|
204
|
+
if (!Object.prototype.hasOwnProperty.call(overrides, i) || overrides[i] === layer.visible) {
|
|
205
|
+
return layer;
|
|
206
|
+
}
|
|
207
|
+
changed = true;
|
|
208
|
+
return { ...layer, visible: overrides[i] };
|
|
209
|
+
});
|
|
210
|
+
return changed ? { ...sprite, layers } : sprite;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function PxArtEditor({ path, text, onChange, files, readOnly = false, ...chrome }) {
|
|
199
214
|
const canvasRef = useRef(null);
|
|
200
215
|
const smoothRef = useRef(null);
|
|
201
216
|
const overlayRef = useRef(null);
|
|
@@ -261,14 +276,22 @@ export function PxArtEditor({ path, text, onChange, files, ...chrome }) {
|
|
|
261
276
|
eraseSize,
|
|
262
277
|
setEraseSize,
|
|
263
278
|
} = toolState;
|
|
264
|
-
const { history, headerShell } = usePixelEditorShell(text, onChange, path);
|
|
279
|
+
const { history, headerShell } = usePixelEditorShell(text, onChange, path, { readOnly });
|
|
265
280
|
const canvasWrapRef = useRef(null);
|
|
266
281
|
const canvasSizeBarRef = useRef(null);
|
|
267
282
|
const colorButtonRef = useRef(null);
|
|
268
283
|
const editorBodyRef = useRef(null);
|
|
269
284
|
const [paletteOpen, setPaletteOpen] = useState(false);
|
|
270
285
|
|
|
271
|
-
const
|
|
286
|
+
const [visibilityOverrides, setVisibilityOverrides] = useState({});
|
|
287
|
+
// Drop browse-only hides when the file is replaced (import update, external
|
|
288
|
+
// edit). Eye toggles don't change `text`, so they survive. Indices would
|
|
289
|
+
// otherwise point at the wrong layer after a rewrite.
|
|
290
|
+
useEffect(() => {
|
|
291
|
+
setVisibilityOverrides({});
|
|
292
|
+
}, [path, text]);
|
|
293
|
+
const fileSprite = deriveSprite(text);
|
|
294
|
+
const sprite = readOnly ? applyLayerVisibility(fileSprite, visibilityOverrides) : fileSprite;
|
|
272
295
|
const deckPalette = useMemo(() => deckPaletteFromFiles(files), [files]);
|
|
273
296
|
const deckHexes = useMemo(() => pickerColorsFor(deckPalette.colors), [deckPalette]);
|
|
274
297
|
const pages = useMemo(() => pickerPagesFor(deckPalette), [deckPalette]);
|
|
@@ -435,8 +458,8 @@ export function PxArtEditor({ path, text, onChange, files, ...chrome }) {
|
|
|
435
458
|
onGestureStart: abortStroke,
|
|
436
459
|
onGestureMove: applyZoomPan,
|
|
437
460
|
// Procreate convention: two-finger tap undoes, three-finger tap redoes.
|
|
438
|
-
onUndo: () => history.undo(),
|
|
439
|
-
onRedo: () => history.redo(),
|
|
461
|
+
onUndo: readOnly ? undefined : () => history.undo(),
|
|
462
|
+
onRedo: readOnly ? undefined : () => history.redo(),
|
|
440
463
|
});
|
|
441
464
|
// Middle-button drag pans by scrolling the viewport. Intercept in the capture
|
|
442
465
|
// phase so the press never reaches the canvas's draw handler (startTool).
|
|
@@ -503,6 +526,7 @@ export function PxArtEditor({ path, text, onChange, files, ...chrome }) {
|
|
|
503
526
|
setEraseSize,
|
|
504
527
|
setBrushMode,
|
|
505
528
|
isPathLayer,
|
|
529
|
+
readOnly,
|
|
506
530
|
});
|
|
507
531
|
const isCompactLayout = useEditorCompactLayout(editorBodyRef);
|
|
508
532
|
useEffect(() => {
|
|
@@ -680,7 +704,7 @@ export function PxArtEditor({ path, text, onChange, files, ...chrome }) {
|
|
|
680
704
|
// Escape deselects; Delete/Backspace clears the selected pixels. Both only
|
|
681
705
|
// while the select tool is active and a marquee exists.
|
|
682
706
|
useEffect(() => {
|
|
683
|
-
if (tool !== 'select' || !marquee) return undefined;
|
|
707
|
+
if (readOnly || tool !== 'select' || !marquee) return undefined;
|
|
684
708
|
function onKeyDown(event) {
|
|
685
709
|
if (event.metaKey || event.ctrlKey || event.altKey) return;
|
|
686
710
|
if (isTypingTarget(event.target || document.activeElement)) return;
|
|
@@ -694,11 +718,11 @@ export function PxArtEditor({ path, text, onChange, files, ...chrome }) {
|
|
|
694
718
|
}
|
|
695
719
|
window.addEventListener('keydown', onKeyDown);
|
|
696
720
|
return () => window.removeEventListener('keydown', onKeyDown);
|
|
697
|
-
}, [tool, marquee, text, selection.layerIndex, selection.frameIndex]);
|
|
721
|
+
}, [readOnly, tool, marquee, text, selection.layerIndex, selection.frameIndex]);
|
|
698
722
|
// Path-layer keyboard: pen close/open, shape/anchor delete / z-order / nudge / duplicate.
|
|
699
723
|
// Plain [ / ] are free on path tools (brush-size adjust only binds on brush/erase).
|
|
700
724
|
useEffect(() => {
|
|
701
|
-
if (!isPathLayer) return undefined;
|
|
725
|
+
if (readOnly || !isPathLayer) return undefined;
|
|
702
726
|
function onKeyDown(event) {
|
|
703
727
|
if (isTypingTarget(event.target || document.activeElement)) return;
|
|
704
728
|
if (tool === 'brush') {
|
|
@@ -769,6 +793,7 @@ export function PxArtEditor({ path, text, onChange, files, ...chrome }) {
|
|
|
769
793
|
window.addEventListener('keydown', onKeyDown);
|
|
770
794
|
return () => window.removeEventListener('keydown', onKeyDown);
|
|
771
795
|
}, [
|
|
796
|
+
readOnly,
|
|
772
797
|
isPathLayer,
|
|
773
798
|
tool,
|
|
774
799
|
brushMode,
|
|
@@ -810,13 +835,15 @@ export function PxArtEditor({ path, text, onChange, files, ...chrome }) {
|
|
|
810
835
|
|
|
811
836
|
const { width, height } = sprite.resolution;
|
|
812
837
|
const overSelection = tool === 'select' && marquee && hoverCell && pointInRect(hoverCell, marquee);
|
|
813
|
-
const cursor =
|
|
814
|
-
? '
|
|
815
|
-
:
|
|
816
|
-
? '
|
|
817
|
-
:
|
|
818
|
-
? '
|
|
819
|
-
:
|
|
838
|
+
const cursor = readOnly
|
|
839
|
+
? 'default'
|
|
840
|
+
: picking
|
|
841
|
+
? 'crosshair'
|
|
842
|
+
: overSelection
|
|
843
|
+
? 'move'
|
|
844
|
+
: isPathLayer && tool === 'brush'
|
|
845
|
+
? 'crosshair'
|
|
846
|
+
: (TOOL_CURSORS[tool] ?? 'default');
|
|
820
847
|
|
|
821
848
|
function togglePalette(next) {
|
|
822
849
|
setPaletteOpen((previous) => (typeof next === 'boolean' ? next : !previous));
|
|
@@ -1826,6 +1853,7 @@ export function PxArtEditor({ path, text, onChange, files, ...chrome }) {
|
|
|
1826
1853
|
// after clicking a non-focusable canvas, so we focus it explicitly. Done
|
|
1827
1854
|
// before the early-return so even a click outside a cell claims focus.
|
|
1828
1855
|
event.currentTarget.focus({ preventScroll: true });
|
|
1856
|
+
if (readOnly) return;
|
|
1829
1857
|
if (isPathLayer) {
|
|
1830
1858
|
event.currentTarget.setPointerCapture(event.pointerId);
|
|
1831
1859
|
const point = eventToPoint(event, width, height);
|
|
@@ -2212,6 +2240,8 @@ export function PxArtEditor({ path, text, onChange, files, ...chrome }) {
|
|
|
2212
2240
|
setPlaying,
|
|
2213
2241
|
playing,
|
|
2214
2242
|
setOnion,
|
|
2243
|
+
readOnly,
|
|
2244
|
+
setVisibilityOverrides,
|
|
2215
2245
|
});
|
|
2216
2246
|
function pickSampledKey(key) {
|
|
2217
2247
|
if (key === TRANSPARENT) return;
|
|
@@ -2241,6 +2271,7 @@ export function PxArtEditor({ path, text, onChange, files, ...chrome }) {
|
|
|
2241
2271
|
setTool(id);
|
|
2242
2272
|
}}
|
|
2243
2273
|
ariaLabel={isPathLayer ? 'Path tools' : 'Sprite tools'}
|
|
2274
|
+
disabled={readOnly}
|
|
2244
2275
|
/>
|
|
2245
2276
|
{/* Undo/redo in their own band below the tools (the flow mount shows
|
|
2246
2277
|
no editor header, so this is the on-screen affordance here). */}
|
|
@@ -2249,7 +2280,7 @@ export function PxArtEditor({ path, text, onChange, files, ...chrome }) {
|
|
|
2249
2280
|
type="button"
|
|
2250
2281
|
title="Undo"
|
|
2251
2282
|
aria-label="Undo"
|
|
2252
|
-
disabled={!history.canUndo}
|
|
2283
|
+
disabled={readOnly || !history.canUndo}
|
|
2253
2284
|
className={styles.drawingToolButton}
|
|
2254
2285
|
onClick={history.undo}>
|
|
2255
2286
|
<Icon name="undo" />
|
|
@@ -2258,7 +2289,7 @@ export function PxArtEditor({ path, text, onChange, files, ...chrome }) {
|
|
|
2258
2289
|
type="button"
|
|
2259
2290
|
title="Redo"
|
|
2260
2291
|
aria-label="Redo"
|
|
2261
|
-
disabled={!history.canRedo}
|
|
2292
|
+
disabled={readOnly || !history.canRedo}
|
|
2262
2293
|
className={styles.drawingToolButton}
|
|
2263
2294
|
onClick={history.redo}>
|
|
2264
2295
|
<Icon name="redo" />
|
|
@@ -2272,11 +2303,13 @@ export function PxArtEditor({ path, text, onChange, files, ...chrome }) {
|
|
|
2272
2303
|
<CanvasSizeBar
|
|
2273
2304
|
width={width}
|
|
2274
2305
|
height={height}
|
|
2306
|
+
disabled={readOnly}
|
|
2275
2307
|
onResize={(w, h) => history.commit(serializeModel(resizeSprite(sprite, w, h)))}
|
|
2276
2308
|
/>
|
|
2277
2309
|
<FinishBar
|
|
2278
2310
|
renderer={sprite.renderer}
|
|
2279
2311
|
cornerRadius={sprite.cornerRadius}
|
|
2312
|
+
disabled={readOnly}
|
|
2280
2313
|
onChange={(finish) => history.commit(serializeModel(setFinish(sprite, finish)))}
|
|
2281
2314
|
/>
|
|
2282
2315
|
</div>
|
|
@@ -2299,18 +2332,25 @@ export function PxArtEditor({ path, text, onChange, files, ...chrome }) {
|
|
|
2299
2332
|
...latticeBackground(isPathLayer, artboardCanvasStyle, width, height),
|
|
2300
2333
|
cursor,
|
|
2301
2334
|
}}
|
|
2302
|
-
handlers={
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2335
|
+
handlers={
|
|
2336
|
+
readOnly
|
|
2337
|
+
? {}
|
|
2338
|
+
: {
|
|
2339
|
+
onPointerDown: startTool,
|
|
2340
|
+
onPointerMove: handlePointerMove,
|
|
2341
|
+
onPointerUp: finishTool,
|
|
2342
|
+
onPointerCancel: finishTool,
|
|
2343
|
+
onPointerLeave: clearHover,
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2309
2346
|
/>
|
|
2310
2347
|
</div>
|
|
2311
2348
|
</div>
|
|
2312
2349
|
</div>
|
|
2313
|
-
<div
|
|
2350
|
+
<div
|
|
2351
|
+
className={cx(styles.paintColumn, readOnly && styles.drawingReadOnly)}
|
|
2352
|
+
aria-disabled={readOnly || undefined}
|
|
2353
|
+
inert={readOnly || undefined}>
|
|
2314
2354
|
<PaintStrip
|
|
2315
2355
|
toolState={toolState}
|
|
2316
2356
|
activeKey={paintKey}
|
|
@@ -2352,6 +2392,7 @@ export function PxArtEditor({ path, text, onChange, files, ...chrome }) {
|
|
|
2352
2392
|
playing={playing}
|
|
2353
2393
|
onion={onion}
|
|
2354
2394
|
actions={actions}
|
|
2395
|
+
readOnly={readOnly}
|
|
2355
2396
|
/>
|
|
2356
2397
|
</div>
|
|
2357
2398
|
<PxArtInspectorDock
|
|
@@ -2369,6 +2410,7 @@ export function PxArtEditor({ path, text, onChange, files, ...chrome }) {
|
|
|
2369
2410
|
moveActions={moveActions}
|
|
2370
2411
|
selectActions={selectActions}
|
|
2371
2412
|
pathActions={isPathLayer ? pathActions : null}
|
|
2413
|
+
readOnly={readOnly}
|
|
2372
2414
|
/>
|
|
2373
2415
|
</div>
|
|
2374
2416
|
</PxArtShell>
|
|
@@ -2378,9 +2420,22 @@ export function PxArtEditor({ path, text, onChange, files, ...chrome }) {
|
|
|
2378
2420
|
// Build the timeline's mutation contract. Each action composes an immutable
|
|
2379
2421
|
// model op with a history commit, updating the (layer, frame) selection where a
|
|
2380
2422
|
// structural change moves it.
|
|
2381
|
-
function buildActions({
|
|
2423
|
+
function buildActions({
|
|
2424
|
+
sprite,
|
|
2425
|
+
selection,
|
|
2426
|
+
history,
|
|
2427
|
+
setSelection,
|
|
2428
|
+
setPlaying,
|
|
2429
|
+
playing,
|
|
2430
|
+
setOnion,
|
|
2431
|
+
readOnly,
|
|
2432
|
+
setVisibilityOverrides,
|
|
2433
|
+
}) {
|
|
2382
2434
|
const { layerIndex, frameIndex } = selection;
|
|
2383
|
-
const commit = (next) =>
|
|
2435
|
+
const commit = (next) => {
|
|
2436
|
+
if (readOnly) return;
|
|
2437
|
+
history.commit(serializeModel(next));
|
|
2438
|
+
};
|
|
2384
2439
|
return {
|
|
2385
2440
|
selectCell: (li, fi) => setSelection({ layerIndex: li, frameIndex: fi }),
|
|
2386
2441
|
addLayer: (li, kind = 'pixel') => {
|
|
@@ -2399,7 +2454,15 @@ function buildActions({ sprite, selection, history, setSelection, setPlaying, pl
|
|
|
2399
2454
|
commit(moveLayer(sprite, li, dir));
|
|
2400
2455
|
setSelection({ layerIndex: li + dir, frameIndex });
|
|
2401
2456
|
},
|
|
2402
|
-
patchLayer: (li, patch) =>
|
|
2457
|
+
patchLayer: (li, patch) => {
|
|
2458
|
+
if (readOnly) {
|
|
2459
|
+
if (typeof patch.visible === 'boolean') {
|
|
2460
|
+
setVisibilityOverrides((prev) => ({ ...prev, [li]: patch.visible }));
|
|
2461
|
+
}
|
|
2462
|
+
return;
|
|
2463
|
+
}
|
|
2464
|
+
commit(patchLayer(sprite, li, patch));
|
|
2465
|
+
},
|
|
2403
2466
|
setLayerKind: (li, kind) => commit(setLayerKind(sprite, li, kind)),
|
|
2404
2467
|
addFrame: (fi) => {
|
|
2405
2468
|
commit(addFrame(sprite, fi));
|
|
@@ -2529,15 +2592,16 @@ function latticeBackground(isPathLayer, canvasStyle, gridWidth, gridHeight) {
|
|
|
2529
2592
|
|
|
2530
2593
|
// Window-scoped tool keyboard shortcuts, mounted only while the editor is.
|
|
2531
2594
|
function useToolShortcuts(ctx) {
|
|
2532
|
-
const { tool, picking, brushMode, setTool, setPicking, setBrushSize, setEraseSize, setBrushMode, isPathLayer } =
|
|
2595
|
+
const { tool, picking, brushMode, setTool, setPicking, setBrushSize, setEraseSize, setBrushMode, isPathLayer, readOnly } =
|
|
2533
2596
|
ctx;
|
|
2534
2597
|
useEffect(() => {
|
|
2535
2598
|
function onKeyDown(event) {
|
|
2599
|
+
if (readOnly) return;
|
|
2536
2600
|
if (handleToolShortcut(event, ctx)) event.preventDefault();
|
|
2537
2601
|
}
|
|
2538
2602
|
window.addEventListener('keydown', onKeyDown);
|
|
2539
2603
|
return () => window.removeEventListener('keydown', onKeyDown);
|
|
2540
|
-
}, [tool, picking, brushMode, setTool, setPicking, setBrushSize, setEraseSize, setBrushMode, isPathLayer]);
|
|
2604
|
+
}, [tool, picking, brushMode, setTool, setPicking, setBrushSize, setEraseSize, setBrushMode, isPathLayer, readOnly]);
|
|
2541
2605
|
}
|
|
2542
2606
|
|
|
2543
2607
|
function renderArtboard(canvas, sprite, frameIndex, onion, playing, preview) {
|
|
@@ -164,11 +164,11 @@ export function SceneEditor({
|
|
|
164
164
|
const runtimeRef = useRef(null);
|
|
165
165
|
const marqueeRef = useRef(null);
|
|
166
166
|
// Ephemeral editor state (camera, play mode, inspected blueprint) restored
|
|
167
|
-
// across a reload:
|
|
168
|
-
// otherwise reset the viewport/play state
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
//
|
|
167
|
+
// across a reload: picking up a code change means reloading the panel, which
|
|
168
|
+
// would otherwise reset the viewport/play state. Data edits fold in live
|
|
169
|
+
// without a reload, so they never need this. `takeReloadState` is one-shot
|
|
170
|
+
// (clears on read), so read it once here. Selection is stashed separately by
|
|
171
|
+
// SingleEditor.
|
|
172
172
|
const reloadStashKey = `scene-editor:${path}`;
|
|
173
173
|
const [reloadStash] = useState(() => takeReloadState(reloadStashKey));
|
|
174
174
|
const editCameraRef = useRef(reloadStash?.editCamera ?? { x: 0, y: 0, zoom: 1 });
|
|
@@ -5,7 +5,13 @@
|
|
|
5
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
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
isImportedFile,
|
|
10
|
+
onBeforeRestart,
|
|
11
|
+
onSaveReloadState,
|
|
12
|
+
takeReloadState,
|
|
13
|
+
writeFile,
|
|
14
|
+
} from 'castle-web-sdk';
|
|
9
15
|
import { getFileKind } from '../engine/files';
|
|
10
16
|
import { hasEditorModule, loadEditorModule } from './editorRegistry';
|
|
11
17
|
import { useLiveDeckFiles } from '../engine/liveReload';
|
|
@@ -24,6 +30,9 @@ function useFileSaver() {
|
|
|
24
30
|
const versions = useRef({});
|
|
25
31
|
const pending = useRef({});
|
|
26
32
|
function commit(path, text, version) {
|
|
33
|
+
// Imports are someone else's deck. The serve refuses these writes anyway;
|
|
34
|
+
// skip the request so a missed UI lock doesn't look like a save that failed.
|
|
35
|
+
if (isImportedFile(path)) return Promise.resolve();
|
|
27
36
|
return writeFile(path, text)
|
|
28
37
|
.then(() => {
|
|
29
38
|
if (versions.current[path] === version && pending.current[path] === text) {
|
|
@@ -37,6 +46,7 @@ function useFileSaver() {
|
|
|
37
46
|
});
|
|
38
47
|
}
|
|
39
48
|
function schedule(path, text) {
|
|
49
|
+
if (isImportedFile(path)) return;
|
|
40
50
|
const version = (versions.current[path] ?? 0) + 1;
|
|
41
51
|
versions.current[path] = version;
|
|
42
52
|
pending.current[path] = text;
|
|
@@ -71,7 +81,7 @@ function useFileSaver() {
|
|
|
71
81
|
// render it, rather than matching the path against this kit's own list. This is
|
|
72
82
|
// what lets a deck hold two kits -- each type reaches its own declarer's editor,
|
|
73
83
|
// and neither kit has to know the other exists.
|
|
74
|
-
function DeclaredEditor({ path, module: modulePath, text, onChange, files, onChangeFile }) {
|
|
84
|
+
function DeclaredEditor({ path, module: modulePath, text, onChange, files, onChangeFile, readOnly }) {
|
|
75
85
|
const [Editor, setEditor] = useState(null);
|
|
76
86
|
const [error, setError] = useState(null);
|
|
77
87
|
useEffect(() => {
|
|
@@ -89,7 +99,14 @@ function DeclaredEditor({ path, module: modulePath, text, onChange, files, onCha
|
|
|
89
99
|
if (error) return <div className={styles.editorBody}>{error}</div>;
|
|
90
100
|
if (!Editor) return <div className={styles.editorBody}>loading editor…</div>;
|
|
91
101
|
return (
|
|
92
|
-
<Editor
|
|
102
|
+
<Editor
|
|
103
|
+
path={path}
|
|
104
|
+
text={text}
|
|
105
|
+
onChange={onChange}
|
|
106
|
+
files={files}
|
|
107
|
+
onChangeFile={onChangeFile}
|
|
108
|
+
readOnly={readOnly}
|
|
109
|
+
/>
|
|
93
110
|
);
|
|
94
111
|
}
|
|
95
112
|
|
|
@@ -101,7 +118,7 @@ export function SingleEditor({ path, editor, editorModule }) {
|
|
|
101
118
|
const { schedule, hasPending } = useFileSaver();
|
|
102
119
|
// Live deck files; our own in-flight (debounced, unsaved) edits win over the
|
|
103
120
|
// fs echo of a stale write, so a drag in progress isn't clobbered.
|
|
104
|
-
const { files, setFiles } = useLiveDeckFiles({ shouldSkipPath: hasPending });
|
|
121
|
+
const { files, setFiles, hydrated } = useLiveDeckFiles({ shouldSkipPath: hasPending, ensurePath: path });
|
|
105
122
|
const [selectedActorIds, setSelectedActorIds] = useState(stash?.selectedActorIds ?? []);
|
|
106
123
|
const [multiSelectMode, setMultiSelectMode] = useState(stash?.multiSelectMode ?? false);
|
|
107
124
|
useEffect(
|
|
@@ -109,12 +126,17 @@ export function SingleEditor({ path, editor, editorModule }) {
|
|
|
109
126
|
[stashKey, selectedActorIds, multiSelectMode]
|
|
110
127
|
);
|
|
111
128
|
const { sprites } = collectAssets(files);
|
|
129
|
+
// The serve refuses writes under `imports/`. Lock the UI to match so an
|
|
130
|
+
// editor doesn't collect keystrokes / clicks that can never be saved.
|
|
131
|
+
const readOnly = isImportedFile(path);
|
|
112
132
|
// Optimistic cross-file edit: fold the new text into live files state NOW
|
|
113
133
|
// (so merged previews update this frame) and debounce the real write; the
|
|
114
134
|
// fs echo of our own write is skipped while pending (shouldSkipPath above).
|
|
115
135
|
// Used for this editor's own file AND for blueprint-file edits made from a
|
|
116
|
-
// scene panel's blueprint inspector.
|
|
136
|
+
// scene panel's blueprint inspector. An imported target is skipped entirely
|
|
137
|
+
// -- even the optimistic fold -- so a missed lock can't fake a local edit.
|
|
117
138
|
function onChangeFile(targetPath, nextText) {
|
|
139
|
+
if (isImportedFile(targetPath)) return;
|
|
118
140
|
setFiles((current) => ({ ...current, [targetPath]: nextText }));
|
|
119
141
|
schedule(targetPath, nextText);
|
|
120
142
|
}
|
|
@@ -128,7 +150,13 @@ export function SingleEditor({ path, editor, editorModule }) {
|
|
|
128
150
|
// over its own types, so the kit's editors are reachable the same way anyone
|
|
129
151
|
// else's are once physics-2d names them in castle.json.
|
|
130
152
|
if (editorModule && hasEditorModule(editorModule)) {
|
|
131
|
-
|
|
153
|
+
// A declared type is not in engine/files.js's glob, so its text arrives from
|
|
154
|
+
// the serve a moment after mount. Wait for it: an editor handed '' for a
|
|
155
|
+
// file that exists renders its empty state, and one edit there overwrites
|
|
156
|
+
// the real document.
|
|
157
|
+
body = !hydrated && files[path] === undefined ? (
|
|
158
|
+
<div className={styles.editorBody}>loading…</div>
|
|
159
|
+
) : (
|
|
132
160
|
<DeclaredEditor
|
|
133
161
|
path={path}
|
|
134
162
|
module={editorModule}
|
|
@@ -136,6 +164,7 @@ export function SingleEditor({ path, editor, editorModule }) {
|
|
|
136
164
|
onChange={onChange}
|
|
137
165
|
files={files}
|
|
138
166
|
onChangeFile={onChangeFile}
|
|
167
|
+
readOnly={readOnly}
|
|
139
168
|
/>
|
|
140
169
|
);
|
|
141
170
|
} else if (kind === 'scene') {
|
|
@@ -154,9 +183,9 @@ export function SingleEditor({ path, editor, editorModule }) {
|
|
|
154
183
|
/>
|
|
155
184
|
);
|
|
156
185
|
} else if (kind === 'pxart') {
|
|
157
|
-
body = <PxArtEditor path={path} text={text} onChange={onChange} files={files} />;
|
|
186
|
+
body = <PxArtEditor path={path} text={text} onChange={onChange} files={files} readOnly={readOnly} />;
|
|
158
187
|
} else if (kind === 'style') {
|
|
159
|
-
body = <StyleEditor path={path} text={text} onChange={onChange} />;
|
|
188
|
+
body = <StyleEditor path={path} text={text} onChange={onChange} readOnly={readOnly} />;
|
|
160
189
|
} else if (kind === 'image') {
|
|
161
190
|
// Viewers read their file's BYTES over the serve, not the text file map --
|
|
162
191
|
// `files` holds source, and an image decoded as utf-8 is nothing.
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// Deck-palette picker for `theme.style`. Official entries come from
|
|
2
2
|
// OFFICIAL_PALETTES; a custom `{ name, colors }` in the file shows as a
|
|
3
3
|
// read-only selected card. Opening the editor never writes — only an
|
|
4
|
-
// explicit official pick does.
|
|
4
|
+
// explicit official pick does. An imported file is the same static-card
|
|
5
|
+
// treatment applied to the whole catalog: still a viewer of what's selected,
|
|
6
|
+
// never a picker.
|
|
5
7
|
|
|
6
8
|
import { createElement } from 'react';
|
|
7
9
|
import { OFFICIAL_PALETTES, parseThemeData, resolveDeckPalette } from '../engine/palettes';
|
|
@@ -58,10 +60,11 @@ function customCardName(palette) {
|
|
|
58
60
|
return name && name !== 'custom' ? name : 'Custom';
|
|
59
61
|
}
|
|
60
62
|
|
|
61
|
-
export function StyleEditor({ text, onChange }) {
|
|
63
|
+
export function StyleEditor({ text, onChange, readOnly = false }) {
|
|
62
64
|
const current = resolveDeckPalette(parseThemeData(text));
|
|
63
65
|
const isCustom = current.id == null;
|
|
64
66
|
function pick(id) {
|
|
67
|
+
if (readOnly) return;
|
|
65
68
|
onChange(applyOfficialPalette(text, id));
|
|
66
69
|
}
|
|
67
70
|
return createElement(
|
|
@@ -76,7 +79,7 @@ export function StyleEditor({ text, onChange }) {
|
|
|
76
79
|
badge: 'Custom',
|
|
77
80
|
})
|
|
78
81
|
: null}
|
|
79
|
-
{isCustom ? (
|
|
82
|
+
{isCustom && !readOnly ? (
|
|
80
83
|
<p className={styles.replaceNote}>Picking an official palette replaces this custom palette.</p>
|
|
81
84
|
) : null}
|
|
82
85
|
<div className={styles.list}>
|
|
@@ -86,7 +89,7 @@ export function StyleEditor({ text, onChange }) {
|
|
|
86
89
|
name: palette.name,
|
|
87
90
|
colors: palette.colors,
|
|
88
91
|
selected: current.id === palette.id,
|
|
89
|
-
onPick: () => pick(palette.id),
|
|
92
|
+
onPick: readOnly ? undefined : () => pick(palette.id),
|
|
90
93
|
})
|
|
91
94
|
)}
|
|
92
95
|
</div>
|
|
@@ -5,18 +5,18 @@ import { useEditHistory, useUndoRedoShortcuts } from './editorHistory';
|
|
|
5
5
|
// Shared shell state for the pixel editors: text undo/redo history and undo/redo
|
|
6
6
|
// keyboard shortcuts. Returns the `history` controller and `headerShell` for the
|
|
7
7
|
// header (undo/redo only — paint controls live in the artboard layout).
|
|
8
|
-
export function usePixelEditorShell(text, onChange, path) {
|
|
8
|
+
export function usePixelEditorShell(text, onChange, path, { readOnly = false } = {}) {
|
|
9
9
|
const history = useEditHistory(text, onChange, path);
|
|
10
|
-
useUndoRedoShortcuts(history);
|
|
11
|
-
const headerShell = { history };
|
|
10
|
+
useUndoRedoShortcuts(history, !readOnly);
|
|
11
|
+
const headerShell = { history, readOnly };
|
|
12
12
|
return { history, headerShell };
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
function PixelEditorTools({ history }) {
|
|
15
|
+
function PixelEditorTools({ history, readOnly }) {
|
|
16
16
|
return (
|
|
17
17
|
<>
|
|
18
|
-
<IconButton icon="undo" label="Undo" onClick={history.undo} disabled={!history.canUndo} />
|
|
19
|
-
<IconButton icon="redo" label="Redo" onClick={history.redo} disabled={!history.canRedo} />
|
|
18
|
+
<IconButton icon="undo" label="Undo" onClick={history.undo} disabled={readOnly || !history.canUndo} />
|
|
19
|
+
<IconButton icon="redo" label="Redo" onClick={history.redo} disabled={readOnly || !history.canRedo} />
|
|
20
20
|
</>
|
|
21
21
|
);
|
|
22
22
|
}
|
|
@@ -27,7 +27,7 @@ export function PixelEditorHeader({ title, subtitle, shell, chrome }) {
|
|
|
27
27
|
<EditorHeader
|
|
28
28
|
title={title}
|
|
29
29
|
subtitle={subtitle}
|
|
30
|
-
right={<PixelEditorTools history={shell.history} />}
|
|
30
|
+
right={<PixelEditorTools history={shell.history} readOnly={shell.readOnly} />}
|
|
31
31
|
onToggleFiles={chrome.onToggleFiles}
|
|
32
32
|
filesOpen={chrome.filesOpen}
|
|
33
33
|
headerHost={chrome.headerHost}
|
|
@@ -132,14 +132,14 @@ export function PixelToolStrip({ tools, tool, onSelectTool, ariaLabel, disabled
|
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
// Compact W × H selects above the artboard (not a sidebar panel).
|
|
135
|
-
export function CanvasSizeBar({ width, height, onResize }) {
|
|
135
|
+
export function CanvasSizeBar({ width, height, onResize, disabled = false }) {
|
|
136
136
|
return (
|
|
137
137
|
<div className={styles.canvasSizeBar} aria-label="Canvas size">
|
|
138
|
-
<ResolutionSelect label="Width" value={width} onChange={(w) => onResize(w, height)} />
|
|
138
|
+
<ResolutionSelect label="Width" value={width} onChange={(w) => onResize(w, height)} disabled={disabled} />
|
|
139
139
|
<span className={styles.canvasSizeSep} aria-hidden="true">
|
|
140
140
|
×
|
|
141
141
|
</span>
|
|
142
|
-
<ResolutionSelect label="Height" value={height} onChange={(h) => onResize(width, h)} />
|
|
142
|
+
<ResolutionSelect label="Height" value={height} onChange={(h) => onResize(width, h)} disabled={disabled} />
|
|
143
143
|
</div>
|
|
144
144
|
);
|
|
145
145
|
}
|
|
@@ -175,13 +175,13 @@ const FINISH_STEPS = [
|
|
|
175
175
|
// (properties of the .pxart asset — see docs/pxart-format.md — not per-actor
|
|
176
176
|
// Sprite props). Highlights whichever preset the pair exactly matches, or none
|
|
177
177
|
// when it matches no preset.
|
|
178
|
-
export function FinishBar({ renderer, cornerRadius, onChange }) {
|
|
178
|
+
export function FinishBar({ renderer, cornerRadius, onChange, disabled = false }) {
|
|
179
179
|
const isOn = (step) =>
|
|
180
180
|
step.renderer === (renderer ?? GRID_RENDERER) && step.cornerRadius === (cornerRadius ?? 0);
|
|
181
181
|
return (
|
|
182
182
|
<div className={styles.finishBar} aria-label="Finish">
|
|
183
183
|
<span>Finish</span>
|
|
184
|
-
<div className={styles.finishSegments} role="radiogroup" aria-label="Finish">
|
|
184
|
+
<div className={styles.finishSegments} role="radiogroup" aria-label="Finish" aria-disabled={disabled || undefined}>
|
|
185
185
|
{FINISH_STEPS.map((step) => (
|
|
186
186
|
<button
|
|
187
187
|
key={step.id}
|
|
@@ -189,6 +189,7 @@ export function FinishBar({ renderer, cornerRadius, onChange }) {
|
|
|
189
189
|
role="radio"
|
|
190
190
|
aria-label={step.label}
|
|
191
191
|
aria-checked={isOn(step) ? 'true' : 'false'}
|
|
192
|
+
disabled={disabled}
|
|
192
193
|
className={cx(styles.finishSegment, isOn(step) && styles.finishSegmentOn)}
|
|
193
194
|
title={step.title}
|
|
194
195
|
onClick={() =>
|
|
@@ -202,7 +203,7 @@ export function FinishBar({ renderer, cornerRadius, onChange }) {
|
|
|
202
203
|
);
|
|
203
204
|
}
|
|
204
205
|
|
|
205
|
-
function ResolutionSelect({ label, value, onChange }) {
|
|
206
|
+
function ResolutionSelect({ label, value, onChange, disabled = false }) {
|
|
206
207
|
const options = RESOLUTION_STEPS.includes(value)
|
|
207
208
|
? RESOLUTION_STEPS
|
|
208
209
|
: [...RESOLUTION_STEPS, value].sort((a, b) => a - b);
|
|
@@ -211,6 +212,7 @@ function ResolutionSelect({ label, value, onChange }) {
|
|
|
211
212
|
className={styles.canvasSizeSelect}
|
|
212
213
|
aria-label={label}
|
|
213
214
|
value={value}
|
|
215
|
+
disabled={disabled}
|
|
214
216
|
onChange={(event) => onChange(Number(event.target.value))}>
|
|
215
217
|
{options.map((step) => (
|
|
216
218
|
<option key={step} value={step}>
|
|
@@ -940,10 +942,14 @@ export function PxArtInspectorDock({
|
|
|
940
942
|
moveActions,
|
|
941
943
|
selectActions,
|
|
942
944
|
pathActions,
|
|
945
|
+
readOnly = false,
|
|
943
946
|
}) {
|
|
944
947
|
const { tool, picking, setPicking } = toolState;
|
|
945
948
|
return (
|
|
946
|
-
<aside
|
|
949
|
+
<aside
|
|
950
|
+
className={cx(styles.pxArtInspector, styles.drawingToolsPanel, readOnly && styles.drawingReadOnly)}
|
|
951
|
+
aria-disabled={readOnly || undefined}
|
|
952
|
+
inert={readOnly || undefined}>
|
|
947
953
|
<div className={cx(styles.sheetBody, styles.inspectorBody, styles.drawingSettingsColumn)}>
|
|
948
954
|
{PALETTE_TOOLS.has(tool) || picking ? (
|
|
949
955
|
<Panel
|