castle-web-cli 0.4.129 → 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 +9 -10
- 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/blueprint.js +15 -3
- package/kits/physics-2d/engine/liveReload.js +33 -21
- package/kits/physics-2d/engine/systemRegistry.js +1 -1
- package/kits/physics-2d/engine/ui.jsx +2 -2
- package/kits/physics-2d/engine/ui.module.css +42 -0
- 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/{basic-2d → physics-3d}/.prettierrc +0 -0
- /package/kits/{basic-2d → physics-3d}/vite.config.js +0 -0
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
# physics-2d kit
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
**physics** system (matter-js)
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
See `## Physics` below.
|
|
3
|
+
An actor / behavior / scene framework for 2D games, with a built-in
|
|
4
|
+
**physics** system (matter-js): `RigidBody`, physics fields on `Collider`, a
|
|
5
|
+
world-gravity scene setting, collision callbacks, `Joints` that link actors
|
|
6
|
+
(spring/rod/pin/weld/rope, several per actor), and ready-made touch-first
|
|
7
|
+
controls (`Draggable`, `Slingshot`, `AnalogStick`). See `## Physics` below.
|
|
9
8
|
|
|
10
9
|
## Welcome message
|
|
11
10
|
|
|
@@ -25,7 +24,7 @@ Do you already know what you want to make, or do you want to figure it out toget
|
|
|
25
24
|
- A deck can also hold uploaded media (the Files panel's Upload button): `Sprite` draws image files as well as `.sprite`, and `Video` plays video files. Point their `file` at the deck path (`assets/logo.png`). For audio see `## Sound` below -- sound effects need no component at all.
|
|
26
25
|
- **Supported media formats are exactly**: `.png` `.jpg` `.jpeg` `.gif` `.webp` `.svg` images, `.mp3` `.wav` `.m4a` audio, `.mp4` (H.264) video. Nothing else — a published deck inlines every asset as a `data:` URI, where the browser trusts the declared type instead of sniffing the bytes, so a format that merely works while serving (`.mov` is the classic case) can be dead once published. Convert, don't improvise.
|
|
27
26
|
- This kit is plain JavaScript. Use `.jsx` for files with JSX, `.js` otherwise; do not add TypeScript files or a new build step.
|
|
28
|
-
-
|
|
27
|
+
- Do not reload the deck after an edit. Nothing auto-reloads any more: the person applies your changes when they are ready, from the Play panel's restart button or a panel's own reload control.
|
|
29
28
|
- Space is reserved by the editor for play/stop; do not bind Space to gameplay.
|
|
30
29
|
- Do not read `engine/`, `editors/`, or built-in behaviors (`Layout.jsx`, `Sprite.jsx`, `Collider.jsx`, `Camera.jsx`, and the physics ones — `RigidBody.jsx`, `Joints.jsx`, `Draggable.jsx`, `Slingshot.jsx`, `AnalogStick.jsx`) to build a game. Their public API is documented below.
|
|
31
30
|
- Details below: `## Behavior shape`, `## Scene file`, `## Blueprints`, `## Built-in behaviors`, `## Creating pixel art`, `## SceneRuntime API`, and `## Input shortcuts`.
|
|
@@ -57,8 +56,8 @@ Write the smallest game that satisfies what the user asked for. No sound, partic
|
|
|
57
56
|
|
|
58
57
|
The deck is already serving when you start (`castle-web init` set that up; see `.castle/serve.json` for the URL). The user is watching that page right now. Your job is to make it interesting incrementally:
|
|
59
58
|
|
|
60
|
-
1. **Build incrementally.** Start with the smallest playable thing (one mechanic, one scene change),
|
|
61
|
-
2. **
|
|
59
|
+
1. **Build incrementally.** Start with the smallest playable thing (one mechanic, one scene change), then add the next piece. Do NOT write the whole game in one shot.
|
|
60
|
+
2. **Do not reload.** Data edits (scenes, drawings, sprites) appear in the open editors on their own. Code edits wait for the person to apply them -- reloading a panel someone is working in loses their place.
|
|
62
61
|
3. **Prefer real, editable assets.** For game objects, characters, and scenery, make actual pixel-art sprites and place them as real actors in `scenes/*.scene` — not shapes drawn in code. Real assets let the creator move and re-skin things in the editor and let other creators remix the deck. Make art as `.sprite` via the `draw` command (see **Creating pixel art** below). Data-driven UI (health bars, score/text, HUD gauges) and dynamic things (bullets, particles, effects) are correctly procedural/code — don't force those into sprites.
|
|
63
62
|
4. **Separate scenes per screen.** Use a separate `scenes/*.scene` file for each distinct screen — menu/title, each level, game-over, etc. — not one mega-scene. Each stays independently editable in the editor. Switch at runtime with `scene.loadFromFile('gameover.scene')` (reads the file and transitions) on play / win / level change. NEVER `import` a `.scene` file as a module — scene files are data, not modules; use `scene.readFromFile` / `scene.loadFromFile`.
|
|
64
63
|
|
|
@@ -477,7 +476,7 @@ Don't hand-write pixel grids. Generate sprites with the `draw` command: **emit a
|
|
|
477
476
|
```
|
|
478
477
|
|
|
479
478
|
writes `drawings/ship.sprite`. You can also pass `--from file.svg` instead of stdin. An undecodable svg (no usable rects) errors with a nonzero exit and writes nothing.
|
|
480
|
-
- **
|
|
479
|
+
- **A newly created file is not in the kit's static glob until the panel reloads**, so until then the missing-sprite fallback renders the placeholder -- an actor pointing at a not-yet-drawn `.sprite` is still playable. Do not force a reload to fix it.
|
|
481
480
|
|
|
482
481
|
Point a `Sprite` component's `file` at the generated `drawings/<name>.sprite` to put the art on an actor.
|
|
483
482
|
|
|
@@ -1,26 +1,35 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Lifecycle } from 'castle-web-sdk';
|
|
3
|
-
import { parseJsonFile } from '../engine/files';
|
|
4
|
-
import { useLiveDeckFiles } from '../engine/liveReload';
|
|
3
|
+
import { initialFiles, parseJsonFile } from '../engine/files';
|
|
5
4
|
import { collectAssets } from '../engine/assets';
|
|
6
5
|
import { ScenePlayer } from '../engine/ScenePlayer';
|
|
7
6
|
import { behaviorClasses } from './behaviorRegistry';
|
|
8
7
|
// Play-mode entry point. Plays the scene named by `?scene=<path>` (the shell's
|
|
9
8
|
// Play panel sets this), defaulting to `scenes/main.scene`. It renders ONLY the
|
|
10
9
|
// game — the scene picker lives in the Play panel chrome (the shell), not here,
|
|
11
|
-
// so nothing floats over the game surface and steals input.
|
|
12
|
-
//
|
|
10
|
+
// so nothing floats over the game surface and steals input.
|
|
11
|
+
//
|
|
12
|
+
// Nothing is folded into a run in progress -- not a drawing, not a scene. A
|
|
13
|
+
// file changing on disk can always have altered how the run would have gone up
|
|
14
|
+
// to this point, so applying one mid-run shows a state the deck could never
|
|
15
|
+
// have reached.
|
|
16
|
+
//
|
|
17
|
+
// Getting current is therefore always a whole-panel reload, and the shell owns
|
|
18
|
+
// when: it reloads Play on arrival if changes landed while it was hidden, and
|
|
19
|
+
// offers the restart button when it is the panel you are looking at. A reload
|
|
20
|
+
// remounts this and re-runs the build-time glob, so code and data arrive
|
|
21
|
+
// together.
|
|
13
22
|
const DEFAULT_SCENE = 'scenes/main.scene';
|
|
14
23
|
|
|
15
24
|
export function PlayOnly({ initialScene } = {}) {
|
|
16
|
-
const
|
|
25
|
+
const files = initialFiles;
|
|
17
26
|
const scenePath = initialScene && files[initialScene] !== undefined ? initialScene : DEFAULT_SCENE;
|
|
18
27
|
const { value: sceneData } = parseJsonFile(scenePath, files[scenePath] ?? '');
|
|
19
28
|
if (!sceneData) return null;
|
|
20
29
|
const { sprites } = collectAssets(files);
|
|
21
30
|
return (
|
|
22
31
|
<ScenePlayer
|
|
23
|
-
key={
|
|
32
|
+
key={scenePath}
|
|
24
33
|
sceneData={sceneData}
|
|
25
34
|
sprites={sprites}
|
|
26
35
|
files={files}
|
|
@@ -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}
|