castle-web-cli 0.4.168 → 0.4.170
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 +17 -0
- package/dist/agent.js +23 -13
- package/dist/atomicFile.d.ts +29 -0
- package/dist/atomicFile.js +120 -0
- package/dist/castle-host/host.js +37 -0
- package/dist/get-deck.js +2 -11
- package/dist/headlessCover.d.ts +4 -0
- package/dist/headlessCover.js +7 -5
- package/dist/ide.js +14 -6
- package/dist/index.js +10 -6
- package/dist/init.d.ts +1 -1
- package/dist/init.js +11 -1
- package/dist/remix.js +2 -14
- package/dist/save-deck.d.ts +3 -0
- package/dist/save-deck.js +51 -74
- package/dist/saveCover.d.ts +18 -0
- package/dist/saveCover.js +61 -0
- package/dist/serve.js +18 -5
- package/dist/serveSecurity.d.ts +11 -1
- package/dist/serveSecurity.js +38 -4
- package/dist/shell/assets/index-BvQmVwlO.css +1 -0
- package/dist/shell/assets/index-CV5sBby1.js +445 -0
- package/dist/shell/index.html +2 -2
- package/dist/versionStore.js +13 -4
- package/kits/base/CLAUDE.md +9 -0
- package/kits/base/castle.json +16 -11
- package/kits/base/sdk/README.md +66 -1
- package/kits/base/sdk/commands.d.ts +23 -1
- package/kits/base/sdk/commands.js +9 -0
- package/kits/base/sdk/lifecycle.d.ts +20 -0
- package/kits/base/sdk/lifecycle.js +7 -1
- package/kits/base/sdk/resumeState.d.ts +2 -0
- package/kits/base/sdk/resumeState.js +84 -0
- package/kits/base/sdk/runtime.js +65 -0
- package/kits/base/sdk/transport.d.ts +3 -0
- package/kits/base/sdk/transport.js +39 -5
- package/kits/base/sdk/unloadHandshake.d.ts +6 -0
- package/kits/base/sdk/unloadHandshake.js +47 -0
- package/kits/multiplayer-2d/castle.json +3 -3
- package/kits/multiplayer-2d/package-lock.json +0 -27
- package/kits/multiplayer-2d/package.json +0 -1
- package/kits/multiplayer-3d/castle.json +4 -4
- package/kits/multiplayer-3d/package-lock.json +0 -27
- package/kits/multiplayer-3d/package.json +0 -1
- package/kits/physics-2d/CLAUDE.md +75 -43
- package/kits/physics-2d/behaviors/AnalogStick.jsx +75 -45
- package/kits/physics-2d/behaviors/Slingshot.jsx +1 -2
- package/kits/physics-2d/castle.json +6 -6
- package/kits/physics-2d/editors/PxArtEditor.jsx +83 -180
- package/kits/physics-2d/editors/SceneEditor.jsx +25 -4
- package/kits/physics-2d/editors/StyleEditor.jsx +6 -2
- package/kits/physics-2d/editors/overlayCanvas.js +77 -0
- package/kits/physics-2d/editors/pathOverlay.js +5 -13
- package/kits/physics-2d/editors/pixelEditorChrome.jsx +5 -1
- package/kits/physics-2d/editors/pixelInspector.jsx +4 -4
- package/kits/physics-2d/editors/pxArtTimeline.jsx +8 -36
- package/kits/physics-2d/editors/pxArtTimeline.module.css +4 -28
- package/kits/physics-2d/editors/styleEditor.module.css +23 -4
- package/kits/physics-2d/editors/useArtboardZoomPan.js +322 -0
- package/kits/physics-2d/engine/SceneUI.jsx +6 -12
- package/kits/physics-2d/engine/icons.js +56 -0
- package/kits/physics-2d/engine/panelSorting.jsx +7 -14
- package/kits/physics-2d/engine/physics/PhysicsSystem.js +66 -11
- package/kits/physics-2d/engine/physics/controls.js +76 -12
- package/kits/physics-2d/engine/scene.js +47 -9
- package/kits/physics-2d/engine/ui.jsx +13 -125
- package/kits/physics-2d/engine/ui.module.css +53 -28
- package/kits/physics-2d/package-lock.json +0 -29
- package/kits/physics-2d/package.json +3 -1
- package/kits/physics-2d/pnpm-lock.yaml +1 -17
- package/kits/physics-3d/castle.json +5 -5
- package/kits/physics-3d/package-lock.json +0 -29
- package/kits/physics-3d/package.json +0 -1
- package/kits/physics-3d/pnpm-lock.yaml +0 -17
- package/kits/turn-based/castle.json +3 -3
- package/kits/turn-based/package-lock.json +0 -27
- package/kits/turn-based/package.json +0 -1
- package/package.json +5 -4
- package/dist/shell/assets/index-CVHj503j.css +0 -1
- package/dist/shell/assets/index-D-qJ5VGp.js +0 -425
- package/kits/blank/.castle/agent/children.json +0 -1
- package/kits/blank/.castle/agent/messages.json +0 -9
- package/kits/blank/.castle/agent/pending-sends.json +0 -1
- package/kits/blank/.castle/agent/tasks/index.md +0 -2
- package/kits/blank/.castle/docs/platform.md +0 -36
- package/kits/blank/node_modules/.vite/deps/_metadata.json +0 -8
- package/kits/blank/node_modules/.vite/deps/package.json +0 -3
|
@@ -40,14 +40,14 @@
|
|
|
40
40
|
"ext": ".sprite",
|
|
41
41
|
"label": "Sprite",
|
|
42
42
|
"new": "New sprite",
|
|
43
|
-
"icon": "
|
|
43
|
+
"icon": "images-square",
|
|
44
44
|
"editor": "kit",
|
|
45
45
|
"data": true
|
|
46
46
|
},
|
|
47
47
|
{
|
|
48
48
|
"ext": ".pxart",
|
|
49
49
|
"label": "Sprite",
|
|
50
|
-
"icon": "
|
|
50
|
+
"icon": "images-square",
|
|
51
51
|
"editor": "kit",
|
|
52
52
|
"data": true
|
|
53
53
|
},
|
|
@@ -55,12 +55,12 @@
|
|
|
55
55
|
"ext": ".jsx",
|
|
56
56
|
"label": "Behavior",
|
|
57
57
|
"new": "New behavior",
|
|
58
|
-
"icon": "code"
|
|
58
|
+
"icon": "file-code"
|
|
59
59
|
},
|
|
60
60
|
{
|
|
61
61
|
"ext": ".style",
|
|
62
62
|
"label": "Style",
|
|
63
|
-
"icon": "
|
|
63
|
+
"icon": "palette",
|
|
64
64
|
"editor": "kit",
|
|
65
65
|
"data": true
|
|
66
66
|
}
|
|
@@ -72,11 +72,11 @@
|
|
|
72
72
|
"main": "main.jsx",
|
|
73
73
|
"autoUpdateWhenImported": true,
|
|
74
74
|
"title": "physics-2d",
|
|
75
|
-
"publishedVersion": "2026-09-
|
|
75
|
+
"publishedVersion": "2026-09-10T00:57:31.408Z",
|
|
76
76
|
"imports": {
|
|
77
77
|
"castle.base": {
|
|
78
78
|
"deckId": "yRcmH4_aYllE",
|
|
79
|
-
"version": "2026-09-
|
|
79
|
+
"version": "2026-09-10T00:56:59.388Z"
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
}
|
|
@@ -92,6 +92,8 @@ import {
|
|
|
92
92
|
} from './pixelInspector';
|
|
93
93
|
import { useArtboardFit, useEditorCompactLayout } from './useArtboardFit';
|
|
94
94
|
import { useTwoFingerZoomPan } from '../engine/gestureZoomPan';
|
|
95
|
+
import { useArtboardZoomPan } from './useArtboardZoomPan';
|
|
96
|
+
import { renderSnapLattice, setupOverlayContext } from './overlayCanvas';
|
|
95
97
|
import {
|
|
96
98
|
clearRegion,
|
|
97
99
|
fillRegion,
|
|
@@ -149,11 +151,6 @@ import tl from './pxArtTimeline.module.css';
|
|
|
149
151
|
const ONION_WARM = '#ff7a3c';
|
|
150
152
|
const ONION_COOL = '#3ca0ff';
|
|
151
153
|
|
|
152
|
-
// Artboard zoom bounds and wheel sensitivity. zoom 1 = fit (the artboard fills
|
|
153
|
-
// its region); zooming in scrolls the surrounding viewport to pan.
|
|
154
|
-
const MIN_ART_ZOOM = 1;
|
|
155
|
-
const MAX_ART_ZOOM = 12;
|
|
156
|
-
const ART_ZOOM_WHEEL_SPEED = 0.01;
|
|
157
154
|
// Floating zoom/pan control cluster, tucked into the true bottom-right corner of
|
|
158
155
|
// the artboard region (over the palette column's empty lower area — the palette
|
|
159
156
|
// keeps its swatches top-aligned).
|
|
@@ -214,6 +211,7 @@ export function PxArtEditor({ path, text, onChange, files, readOnly = false, ...
|
|
|
214
211
|
const canvasRef = useRef(null);
|
|
215
212
|
const smoothRef = useRef(null);
|
|
216
213
|
const overlayRef = useRef(null);
|
|
214
|
+
const latticeRef = useRef(null);
|
|
217
215
|
const strokeRef = useRef(null);
|
|
218
216
|
const refitActiveBrushStrokeRef = useRef(() => {});
|
|
219
217
|
// The serialized text this editor last wrote itself. Lets us tell our own
|
|
@@ -369,14 +367,22 @@ export function PxArtEditor({ path, text, onChange, files, readOnly = false, ...
|
|
|
369
367
|
resolution: sprite?.resolution ?? { width: 16, height: 16 },
|
|
370
368
|
});
|
|
371
369
|
// Artboard zoom + pan. `zoom` scales the canvas above its fit size; the
|
|
372
|
-
// surrounding viewport
|
|
373
|
-
//
|
|
374
|
-
//
|
|
370
|
+
// surrounding viewport scrolls (programmatically -- see useArtboardZoomPan) to
|
|
371
|
+
// pan, over half a viewport of overscroll on every side so any part of the
|
|
372
|
+
// sprite can be brought to the middle of the screen.
|
|
375
373
|
const viewportRef = useRef(null);
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
374
|
+
const {
|
|
375
|
+
zoom,
|
|
376
|
+
zoomBy,
|
|
377
|
+
resetZoom,
|
|
378
|
+
applyZoomPan,
|
|
379
|
+
beginGesture,
|
|
380
|
+
endGesture,
|
|
381
|
+
viewportHandlers,
|
|
382
|
+
viewportStyle: artboardViewportStyle,
|
|
383
|
+
contentStyle: artboardContentStyle,
|
|
384
|
+
canvasSize,
|
|
385
|
+
} = useArtboardZoomPan({ viewportRef, displaySize });
|
|
380
386
|
const pathCellPull =
|
|
381
387
|
displaySize?.width && sprite?.resolution?.width
|
|
382
388
|
? magneticCellPull(sprite.resolution.width, displaySize.width, zoom)
|
|
@@ -391,62 +397,6 @@ export function PxArtEditor({ path, text, onChange, files, readOnly = false, ...
|
|
|
391
397
|
stroke: hitRadiusCells(EDGE_HIT_SCREEN_PX, pathCellPx),
|
|
392
398
|
openEnd: hitRadiusCells(OPEN_END_HIT_SCREEN_PX, pathCellPx),
|
|
393
399
|
};
|
|
394
|
-
// Set zoom to `nextRaw` (clamped) while pinning the point under (anchorX,
|
|
395
|
-
// anchorY) client coords — defaulting to the viewport center. Scroll is fixed
|
|
396
|
-
// up on the next frame, once the resized canvas has laid out.
|
|
397
|
-
const applyZoom = useCallback((nextRaw, anchorX, anchorY) => {
|
|
398
|
-
const vp = viewportRef.current;
|
|
399
|
-
const z0 = zoomRef.current;
|
|
400
|
-
const z1 = Math.max(MIN_ART_ZOOM, Math.min(MAX_ART_ZOOM, nextRaw));
|
|
401
|
-
if (!vp || z1 === z0) return;
|
|
402
|
-
const rect = vp.getBoundingClientRect();
|
|
403
|
-
const ax = (Number.isFinite(anchorX) ? anchorX : rect.left + rect.width / 2) - rect.left;
|
|
404
|
-
const ay = (Number.isFinite(anchorY) ? anchorY : rect.top + rect.height / 2) - rect.top;
|
|
405
|
-
const contentX = vp.scrollLeft + ax;
|
|
406
|
-
const contentY = vp.scrollTop + ay;
|
|
407
|
-
const ratio = z1 / z0;
|
|
408
|
-
setZoom(z1);
|
|
409
|
-
requestAnimationFrame(() => {
|
|
410
|
-
vp.scrollLeft = contentX * ratio - ax;
|
|
411
|
-
vp.scrollTop = contentY * ratio - ay;
|
|
412
|
-
});
|
|
413
|
-
}, []);
|
|
414
|
-
const resetZoom = useCallback(() => {
|
|
415
|
-
setZoom(1);
|
|
416
|
-
const vp = viewportRef.current;
|
|
417
|
-
if (vp) requestAnimationFrame(() => {
|
|
418
|
-
vp.scrollLeft = 0;
|
|
419
|
-
vp.scrollTop = 0;
|
|
420
|
-
});
|
|
421
|
-
}, []);
|
|
422
|
-
// Two-finger gesture step. Pan SYNCHRONOUSLY by scrolling — smooth and
|
|
423
|
-
// accumulative, exactly like the middle-drag pan. (Applying the pan inside the
|
|
424
|
-
// zoom's rAF made successive moves recompute from a scroll that hadn't been
|
|
425
|
-
// written yet, dropping intermediate deltas — the "funky" pan.) Zoom about the
|
|
426
|
-
// centroid only when the pinch actually changed scale; `applyZoom` reads the
|
|
427
|
-
// just-scrolled position, so the pan is preserved. The epsilon keeps finger
|
|
428
|
-
// jitter during a pure pan from flipping the zoom on and off.
|
|
429
|
-
const applyZoomPan = useCallback(({ factor, cx, cy, dx, dy }) => {
|
|
430
|
-
const vp = viewportRef.current;
|
|
431
|
-
if (!vp) return;
|
|
432
|
-
if (dx) vp.scrollLeft -= dx;
|
|
433
|
-
if (dy) vp.scrollTop -= dy;
|
|
434
|
-
if (Math.abs(factor - 1) > 0.002) applyZoom(zoomRef.current * factor, cx, cy);
|
|
435
|
-
}, [applyZoom]);
|
|
436
|
-
// Wheel zoom as a NATIVE non-passive listener so preventDefault stops the
|
|
437
|
-
// page/browser zoom underneath. Delta is clamped so a chunky mouse notch is a
|
|
438
|
-
// sane step while trackpad deltas stay smooth.
|
|
439
|
-
useEffect(() => {
|
|
440
|
-
const vp = viewportRef.current;
|
|
441
|
-
if (!vp) return undefined;
|
|
442
|
-
const onWheel = (event) => {
|
|
443
|
-
event.preventDefault();
|
|
444
|
-
const clamped = Math.max(-50, Math.min(50, event.deltaY));
|
|
445
|
-
applyZoom(zoomRef.current * Math.exp(-clamped * ART_ZOOM_WHEEL_SPEED), event.clientX, event.clientY);
|
|
446
|
-
};
|
|
447
|
-
vp.addEventListener('wheel', onWheel, { passive: false });
|
|
448
|
-
return () => vp.removeEventListener('wheel', onWheel, { passive: false });
|
|
449
|
-
}, [applyZoom]);
|
|
450
400
|
// Touch: one finger draws, a second finger pinch-zooms about the centroid and
|
|
451
401
|
// pans (the viewport already has touch-action:none). The second finger aborts
|
|
452
402
|
// the in-flight stroke so it doesn't leave a stray mark. See gestureZoomPan.
|
|
@@ -455,59 +405,18 @@ export function PxArtEditor({ path, text, onChange, files, readOnly = false, ...
|
|
|
455
405
|
// lands entirely inside the canvas. Zoom/pan still applies to the viewport.
|
|
456
406
|
useTwoFingerZoomPan({
|
|
457
407
|
targetRef: editorBodyRef,
|
|
458
|
-
onGestureStart:
|
|
408
|
+
onGestureStart: () => {
|
|
409
|
+
abortStroke();
|
|
410
|
+
beginGesture();
|
|
411
|
+
},
|
|
459
412
|
onGestureMove: applyZoomPan,
|
|
413
|
+
// Releasing a pinch near fit settles onto exactly fit.
|
|
414
|
+
onGestureEnd: endGesture,
|
|
460
415
|
// Procreate convention: two-finger tap undoes, three-finger tap redoes.
|
|
461
416
|
onUndo: readOnly ? undefined : () => history.undo(),
|
|
462
417
|
onRedo: readOnly ? undefined : () => history.redo(),
|
|
463
418
|
});
|
|
464
|
-
|
|
465
|
-
// phase so the press never reaches the canvas's draw handler (startTool).
|
|
466
|
-
const onViewportPointerDownCapture = (event) => {
|
|
467
|
-
// Pan on a middle-button drag or a ⌘+left-drag (metaKey). Both are caught in
|
|
468
|
-
// the capture phase so the press never reaches the canvas's draw handler.
|
|
469
|
-
const startsPan = event.button === 1 || (event.button === 0 && event.metaKey);
|
|
470
|
-
if (!startsPan) return;
|
|
471
|
-
event.preventDefault();
|
|
472
|
-
event.stopPropagation();
|
|
473
|
-
const vp = viewportRef.current;
|
|
474
|
-
vp.setPointerCapture(event.pointerId);
|
|
475
|
-
panRef.current = { id: event.pointerId, x: event.clientX, y: event.clientY };
|
|
476
|
-
};
|
|
477
|
-
const onViewportPointerMove = (event) => {
|
|
478
|
-
const pan = panRef.current;
|
|
479
|
-
if (!pan || pan.id !== event.pointerId) return;
|
|
480
|
-
const vp = viewportRef.current;
|
|
481
|
-
vp.scrollLeft -= event.clientX - pan.x;
|
|
482
|
-
vp.scrollTop -= event.clientY - pan.y;
|
|
483
|
-
pan.x = event.clientX;
|
|
484
|
-
pan.y = event.clientY;
|
|
485
|
-
};
|
|
486
|
-
const onViewportPointerUp = (event) => {
|
|
487
|
-
const pan = panRef.current;
|
|
488
|
-
if (!pan || pan.id !== event.pointerId) return;
|
|
489
|
-
try {
|
|
490
|
-
viewportRef.current?.releasePointerCapture(event.pointerId);
|
|
491
|
-
} catch {
|
|
492
|
-
// pointer capture may already be gone
|
|
493
|
-
}
|
|
494
|
-
panRef.current = null;
|
|
495
|
-
};
|
|
496
|
-
// Viewport is sized to the fit baseline; the canvas inside scales by zoom, so
|
|
497
|
-
// zoom > 1 overflows and the viewport scrolls (= pan).
|
|
498
|
-
const artboardViewportStyle = displaySize
|
|
499
|
-
? {
|
|
500
|
-
width: displaySize.width,
|
|
501
|
-
height: displaySize.height,
|
|
502
|
-
maxWidth: '100%',
|
|
503
|
-
maxHeight: '100%',
|
|
504
|
-
overflow: 'auto',
|
|
505
|
-
touchAction: 'none',
|
|
506
|
-
}
|
|
507
|
-
: { overflow: 'auto' };
|
|
508
|
-
const artboardCanvasStyle = displaySize
|
|
509
|
-
? { width: displaySize.width * zoom, height: displaySize.height * zoom }
|
|
510
|
-
: canvasStyle;
|
|
419
|
+
const artboardCanvasStyle = canvasSize ?? canvasStyle;
|
|
511
420
|
// `canvasStyle`'s width is the artboard's CURRENT display size (recomputed
|
|
512
421
|
// by useArtboardFit's own ResizeObserver whenever the wrap resizes — e.g. a
|
|
513
422
|
// dockview panel drag). Feeding it in here is what keeps the supersampled
|
|
@@ -625,6 +534,12 @@ export function PxArtEditor({ path, text, onChange, files, readOnly = false, ...
|
|
|
625
534
|
const id = setInterval(() => setAntsOffset((o) => (o + 1) % 1000), 80);
|
|
626
535
|
return () => clearInterval(id);
|
|
627
536
|
}, [tool, marquee]);
|
|
537
|
+
// Snap lattice for path layers. Redrawn on the artboard's CURRENT display size
|
|
538
|
+
// (`artboardCanvasStyle.width` carries the zoom), because the dots are placed
|
|
539
|
+
// per cell in screen space -- there is nothing tiled here to stretch.
|
|
540
|
+
useEffect(() => {
|
|
541
|
+
renderSnapLattice(latticeRef.current, sprite?.resolution, isPathLayer);
|
|
542
|
+
}, [isPathLayer, sprite?.resolution, artboardCanvasStyle?.width, artboardCanvasStyle?.height]);
|
|
628
543
|
useEffect(() => {
|
|
629
544
|
if (isPathLayer) {
|
|
630
545
|
drawPathOverlay(overlayRef.current, sprite?.resolution, playing ? null : activePath, {
|
|
@@ -2314,36 +2229,32 @@ export function PxArtEditor({ path, text, onChange, files, readOnly = false, ...
|
|
|
2314
2229
|
/>
|
|
2315
2230
|
</div>
|
|
2316
2231
|
</div>
|
|
2317
|
-
<div
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
onPointerLeave: clearHover,
|
|
2344
|
-
}
|
|
2345
|
-
}
|
|
2346
|
-
/>
|
|
2232
|
+
<div ref={viewportRef} style={artboardViewportStyle} {...viewportHandlers}>
|
|
2233
|
+
<div style={artboardContentStyle}>
|
|
2234
|
+
<PixelArtboard
|
|
2235
|
+
canvasRef={canvasRef}
|
|
2236
|
+
smoothRef={smoothRef}
|
|
2237
|
+
overlayRef={overlayRef}
|
|
2238
|
+
latticeRef={latticeRef}
|
|
2239
|
+
smoothPreview={displayFinish(sprite).smooth}
|
|
2240
|
+
style={{
|
|
2241
|
+
...artboardCanvasStyle,
|
|
2242
|
+
...artboardBackground(isPathLayer, artboardCanvasStyle, zoom),
|
|
2243
|
+
cursor,
|
|
2244
|
+
}}
|
|
2245
|
+
handlers={
|
|
2246
|
+
readOnly
|
|
2247
|
+
? {}
|
|
2248
|
+
: {
|
|
2249
|
+
onPointerDown: startTool,
|
|
2250
|
+
onPointerMove: handlePointerMove,
|
|
2251
|
+
onPointerUp: finishTool,
|
|
2252
|
+
onPointerCancel: finishTool,
|
|
2253
|
+
onPointerLeave: clearHover,
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
/>
|
|
2257
|
+
</div>
|
|
2347
2258
|
</div>
|
|
2348
2259
|
</div>
|
|
2349
2260
|
</div>
|
|
@@ -2375,12 +2286,12 @@ export function PxArtEditor({ path, text, onChange, files, readOnly = false, ...
|
|
|
2375
2286
|
<IconButton
|
|
2376
2287
|
icon="zoom-in"
|
|
2377
2288
|
label="Zoom in"
|
|
2378
|
-
onClick={() =>
|
|
2289
|
+
onClick={() => zoomBy(1.25)}
|
|
2379
2290
|
/>
|
|
2380
2291
|
<IconButton
|
|
2381
2292
|
icon="zoom-out"
|
|
2382
2293
|
label="Zoom out"
|
|
2383
|
-
onClick={() =>
|
|
2294
|
+
onClick={() => zoomBy(0.8)}
|
|
2384
2295
|
/>
|
|
2385
2296
|
<IconButton icon="fit" label="Fit" onClick={resetZoom} />
|
|
2386
2297
|
</div>
|
|
@@ -2573,21 +2484,23 @@ function useSmoothPreviewRender(smoothRef, text, sprite, frameIndex, displayWidt
|
|
|
2573
2484
|
}, [text, frameIndex, smoothRef, cornerRadius, vector, displayWidth, bakePreview]);
|
|
2574
2485
|
}
|
|
2575
2486
|
|
|
2576
|
-
//
|
|
2577
|
-
//
|
|
2578
|
-
//
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2487
|
+
// What the artboard frame paints UNDER the art. On a path layer that is a flat
|
|
2488
|
+
// ground for the snap lattice (drawn on its own canvas -- see renderSnapLattice,
|
|
2489
|
+
// which explains why it is not a background); everywhere else it is the
|
|
2490
|
+
// transparency checkerboard, scaled so it zooms with the sprite instead of
|
|
2491
|
+
// sitting still behind it.
|
|
2492
|
+
//
|
|
2493
|
+
// Either way it belongs to the FRAME, not to a canvas, so it shows through
|
|
2494
|
+
// transparent art in every finish (Pixel / ¼ / ½ / Smooth) without painting over
|
|
2495
|
+
// the smooth-preview canvas underneath.
|
|
2496
|
+
//
|
|
2497
|
+
// The checkerboard check at fit; `--checker-size` scales it from there, so the
|
|
2498
|
+
// pattern travels with a pinch instead of re-tiling underneath it.
|
|
2499
|
+
const CHECKER_FIT_SIZE = 16;
|
|
2500
|
+
function artboardBackground(isPathLayer, canvasStyle, zoom) {
|
|
2501
|
+
if (!canvasStyle) return {};
|
|
2502
|
+
if (isPathLayer) return { background: '#121212' };
|
|
2503
|
+
return { '--checker-size': `${CHECKER_FIT_SIZE * zoom}px` };
|
|
2591
2504
|
}
|
|
2592
2505
|
|
|
2593
2506
|
// Window-scoped tool keyboard shortcuts, mounted only while the editor is.
|
|
@@ -2628,24 +2541,14 @@ function renderArtboard(canvas, sprite, frameIndex, onion, playing, preview) {
|
|
|
2628
2541
|
}
|
|
2629
2542
|
|
|
2630
2543
|
function setupOverlayCanvas(canvas, resolution) {
|
|
2631
|
-
if (!
|
|
2632
|
-
const
|
|
2633
|
-
if (!
|
|
2634
|
-
const cssW = canvas.clientWidth;
|
|
2635
|
-
const cssH = canvas.clientHeight;
|
|
2636
|
-
if (!cssW || !cssH) return null;
|
|
2637
|
-
const dpr = window.devicePixelRatio || 1;
|
|
2638
|
-
const bw = Math.round(cssW * dpr);
|
|
2639
|
-
const bh = Math.round(cssH * dpr);
|
|
2640
|
-
if (canvas.width !== bw) canvas.width = bw;
|
|
2641
|
-
if (canvas.height !== bh) canvas.height = bh;
|
|
2642
|
-
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
2643
|
-
ctx.clearRect(0, 0, bw, bh);
|
|
2544
|
+
if (!resolution) return null;
|
|
2545
|
+
const surface = setupOverlayContext(canvas);
|
|
2546
|
+
if (!surface) return null;
|
|
2644
2547
|
return {
|
|
2645
|
-
ctx,
|
|
2646
|
-
dpr,
|
|
2647
|
-
sx: bw / resolution.width,
|
|
2648
|
-
sy: bh / resolution.height,
|
|
2548
|
+
ctx: surface.ctx,
|
|
2549
|
+
dpr: surface.dpr,
|
|
2550
|
+
sx: surface.bw / resolution.width,
|
|
2551
|
+
sy: surface.bh / resolution.height,
|
|
2649
2552
|
};
|
|
2650
2553
|
}
|
|
2651
2554
|
|
|
@@ -886,6 +886,7 @@ export function SceneEditor({
|
|
|
886
886
|
? undefined
|
|
887
887
|
: () => onSelectBlueprint(selectedActorBlueprint)
|
|
888
888
|
}
|
|
889
|
+
onEditSprite={onEditSprite}
|
|
889
890
|
onBeginPick={beginPick}
|
|
890
891
|
pickActive={Boolean(pickRequest)}
|
|
891
892
|
onSelectShape={setColliderShapeSel}
|
|
@@ -2083,6 +2084,7 @@ function ActorInspector({
|
|
|
2083
2084
|
onRemoveBehavior,
|
|
2084
2085
|
onNewBlueprint,
|
|
2085
2086
|
onEditBlueprint,
|
|
2087
|
+
onEditSprite,
|
|
2086
2088
|
onBeginPick,
|
|
2087
2089
|
pickActive,
|
|
2088
2090
|
onSelectShape,
|
|
@@ -2096,6 +2098,14 @@ function ActorInspector({
|
|
|
2096
2098
|
readOnly = false,
|
|
2097
2099
|
}) {
|
|
2098
2100
|
const isInstance = Boolean(rawActor?.blueprint);
|
|
2101
|
+
// Same art the thumbnail shows (blueprint face, else the actor's own Sprite).
|
|
2102
|
+
const thumbBlueprint = blueprintTemplate ?? { components: selectedActor.components };
|
|
2103
|
+
const spritePath = thumbBlueprint.components?.Sprite?.file;
|
|
2104
|
+
const canEditSprite =
|
|
2105
|
+
Boolean(spritePath) &&
|
|
2106
|
+
!readOnly &&
|
|
2107
|
+
!isImportedPath(spritePath) &&
|
|
2108
|
+
files?.[spritePath] !== undefined;
|
|
2099
2109
|
const presentNames = new Set(
|
|
2100
2110
|
Object.entries(selectedActor.components)
|
|
2101
2111
|
.filter(([, component]) => !!component)
|
|
@@ -2148,10 +2158,21 @@ function ActorInspector({
|
|
|
2148
2158
|
{isInstance ? (
|
|
2149
2159
|
<div className={styles.instanceHeader}>
|
|
2150
2160
|
<div className={styles.instanceHeaderName}>
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2161
|
+
{canEditSprite ? (
|
|
2162
|
+
<button
|
|
2163
|
+
type="button"
|
|
2164
|
+
className={styles.editSpriteButton}
|
|
2165
|
+
title={`Edit ${spritePath}`}
|
|
2166
|
+
aria-label="Edit sprite"
|
|
2167
|
+
onClick={() => void onEditSprite?.(spritePath)}>
|
|
2168
|
+
<BlueprintThumbnail blueprint={thumbBlueprint} sprites={sprites} />
|
|
2169
|
+
<span className={styles.editSpriteHint}>
|
|
2170
|
+
<Icon name="pencil" />
|
|
2171
|
+
</span>
|
|
2172
|
+
</button>
|
|
2173
|
+
) : (
|
|
2174
|
+
<BlueprintThumbnail blueprint={thumbBlueprint} sprites={sprites} />
|
|
2175
|
+
)}
|
|
2155
2176
|
<div className={styles.instanceHeaderNameCol}>
|
|
2156
2177
|
<span className={styles.instanceHeaderNameText}>
|
|
2157
2178
|
<strong className={styles.instanceHeaderNameStrong}>{blueprintName}</strong> Actor
|
|
@@ -31,14 +31,18 @@ function cardClassName(selected, clickable) {
|
|
|
31
31
|
// One card, palette or font: a head (name + meta) over whatever the type shows
|
|
32
32
|
// of itself. A button when it can be picked, a plain div when it can't.
|
|
33
33
|
function styleCard({ cardKey, name, meta, preview, selected, onPick }) {
|
|
34
|
+
// Inner wrapper is the flex column, not the <button>. WebKit still wraps
|
|
35
|
+
// button contents in a shrink-to-fit anonymous box, so a flex button never
|
|
36
|
+
// stretches its children on some iOS versions — the palette strip stays
|
|
37
|
+
// colorCount × min-width wide. A block button + width:100% inner avoids that.
|
|
34
38
|
const body = (
|
|
35
|
-
|
|
39
|
+
<div className={styles.cardBody}>
|
|
36
40
|
<div className={styles.cardHead}>
|
|
37
41
|
<div className={styles.cardName}>{name}</div>
|
|
38
42
|
{meta ? <div className={styles.cardMeta}>{meta}</div> : null}
|
|
39
43
|
</div>
|
|
40
44
|
{preview}
|
|
41
|
-
|
|
45
|
+
</div>
|
|
42
46
|
);
|
|
43
47
|
const className = cardClassName(selected, Boolean(onPick));
|
|
44
48
|
if (!onPick) {
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Backing store for the display-resolution overlay canvases (marching ants, path
|
|
2
|
+
// chrome). They are sized from the artboard's CSS size, which grows with zoom --
|
|
3
|
+
// so at 12x on a 3x screen a 460px artboard asks for a 16560 x 16560 store,
|
|
4
|
+
// around 274 megapixels. WebKit refuses an allocation that large (the overlay
|
|
5
|
+
// silently stops drawing) and every zoom step reallocates it, which is most of
|
|
6
|
+
// what made a deep pinch stutter on a phone.
|
|
7
|
+
//
|
|
8
|
+
// So cap the AREA and let the effective ratio fall below the device's. Past the
|
|
9
|
+
// cap the art underneath is upscaled anyway, so nothing goes soft that was still
|
|
10
|
+
// sharp; at fit on any normal artboard the cap is not reached and the overlay is
|
|
11
|
+
// exactly as crisp as before.
|
|
12
|
+
const MAX_OVERLAY_PIXELS = 4e6;
|
|
13
|
+
|
|
14
|
+
// Returns the cleared 2D context plus the ratio actually used, or null when the
|
|
15
|
+
// canvas has no layout yet. `dpr` is the CSS-pixel -> backing-store scale to draw
|
|
16
|
+
// with; `bw`/`bh` are the backing store, `cssW`/`cssH` the element.
|
|
17
|
+
export function setupOverlayContext(canvas) {
|
|
18
|
+
if (!canvas) return null;
|
|
19
|
+
const ctx = canvas.getContext('2d');
|
|
20
|
+
if (!ctx) return null;
|
|
21
|
+
// The measured box, NOT clientWidth: the artboard is sized by a continuous
|
|
22
|
+
// zoom, so its width is fractional at nearly every zoom, and clientWidth
|
|
23
|
+
// rounds it to a whole pixel. Dividing a rounded width by the grid gives a
|
|
24
|
+
// pitch that is wrong by up to half a pixel per cell, which accumulates
|
|
25
|
+
// across the row -- the marks drift further from their cells the further
|
|
26
|
+
// right you look, and the drift resets each time the rounding flips.
|
|
27
|
+
const { width: cssW, height: cssH } = canvas.getBoundingClientRect();
|
|
28
|
+
if (!cssW || !cssH) return null;
|
|
29
|
+
const device = window.devicePixelRatio || 1;
|
|
30
|
+
const wanted = cssW * cssH * device * device;
|
|
31
|
+
const dpr = wanted > MAX_OVERLAY_PIXELS ? device * Math.sqrt(MAX_OVERLAY_PIXELS / wanted) : device;
|
|
32
|
+
const bw = Math.max(1, Math.round(cssW * dpr));
|
|
33
|
+
const bh = Math.max(1, Math.round(cssH * dpr));
|
|
34
|
+
if (canvas.width !== bw) canvas.width = bw;
|
|
35
|
+
if (canvas.height !== bh) canvas.height = bh;
|
|
36
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
37
|
+
ctx.clearRect(0, 0, bw, bh);
|
|
38
|
+
return { ctx, dpr, cssW, cssH, bw, bh };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Snap-lattice dot field for path layers: one dot at the centre of every cell.
|
|
42
|
+
//
|
|
43
|
+
// This used to be a repeating radial-gradient on the artboard frame, which
|
|
44
|
+
// cannot do the job: the pitch is the artboard width over the grid, so it is
|
|
45
|
+
// fractional at nearly every zoom, and while the browser repeats the tile at the
|
|
46
|
+
// exact fractional pitch it rounds the dot's offset WITHIN the tile to whole
|
|
47
|
+
// device pixels. The dots therefore sat up to a pixel off the cells they mark,
|
|
48
|
+
// drifting further out and snapping back as the pitch crossed each pixel
|
|
49
|
+
// boundary -- a sawtooth you could watch while pinching. Marks that say "snap
|
|
50
|
+
// here" have to be where the snap actually is, so they are drawn instead.
|
|
51
|
+
const LATTICE_DOT_RADIUS = 1.3;
|
|
52
|
+
const LATTICE_DOT_FILL = 'rgba(255, 255, 255, 0.28)';
|
|
53
|
+
// Below this the cells are too small for a dot per cell to read as anything but
|
|
54
|
+
// noise, so the field is left off.
|
|
55
|
+
const LATTICE_MIN_PITCH = 5;
|
|
56
|
+
|
|
57
|
+
export function renderSnapLattice(canvas, resolution, enabled) {
|
|
58
|
+
const surface = setupOverlayContext(canvas);
|
|
59
|
+
if (!surface) return;
|
|
60
|
+
const { ctx, dpr, bw, bh } = surface;
|
|
61
|
+
if (!enabled || !resolution?.width || !resolution?.height) return;
|
|
62
|
+
// Placed in BACKING pixels, not CSS pixels through a transform: the store is a
|
|
63
|
+
// whole number of pixels spanning exactly the artboard, so a cell centre is
|
|
64
|
+
// exact by construction and no rounding is left to accumulate. The radius is
|
|
65
|
+
// the only thing scaled by the ratio, so the dots stay one screen size.
|
|
66
|
+
const pitchX = bw / resolution.width;
|
|
67
|
+
const pitchY = bh / resolution.height;
|
|
68
|
+
if (Math.min(pitchX, pitchY) < LATTICE_MIN_PITCH * dpr) return;
|
|
69
|
+
ctx.fillStyle = LATTICE_DOT_FILL;
|
|
70
|
+
for (let y = 0; y < resolution.height; y++) {
|
|
71
|
+
for (let x = 0; x < resolution.width; x++) {
|
|
72
|
+
ctx.beginPath();
|
|
73
|
+
ctx.arc((x + 0.5) * pitchX, (y + 0.5) * pitchY, LATTICE_DOT_RADIUS * dpr, 0, Math.PI * 2);
|
|
74
|
+
ctx.fill();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { absoluteSegment, subpathEdgeFrom, subpathPoints } from '../engine/pxartPath.js';
|
|
2
2
|
import { primitiveHandleIndices } from '../engine/pxartPath.js';
|
|
3
3
|
import { anchorKey, coincidentSiteKeys, isOpenPenShape } from './pathTools';
|
|
4
|
+
import { setupOverlayContext } from './overlayCanvas';
|
|
4
5
|
|
|
5
6
|
// Render-only overlay for path-layer tools: selection chrome (outline + anchor
|
|
6
7
|
// dots), pen rubber-band to hover, fill-tool hover preview, and shape marquee.
|
|
@@ -40,19 +41,10 @@ function toCanvas(sx, sy, x, y) {
|
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
export function drawPathOverlay(canvas, resolution, path, ui) {
|
|
43
|
-
if (!
|
|
44
|
-
const
|
|
45
|
-
if (!
|
|
46
|
-
const cssW =
|
|
47
|
-
const cssH = canvas.clientHeight;
|
|
48
|
-
if (!cssW || !cssH) return;
|
|
49
|
-
const dpr = window.devicePixelRatio || 1;
|
|
50
|
-
const bw = Math.round(cssW * dpr);
|
|
51
|
-
const bh = Math.round(cssH * dpr);
|
|
52
|
-
if (canvas.width !== bw) canvas.width = bw;
|
|
53
|
-
if (canvas.height !== bh) canvas.height = bh;
|
|
54
|
-
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
55
|
-
ctx.clearRect(0, 0, bw, bh);
|
|
44
|
+
if (!resolution) return;
|
|
45
|
+
const surface = setupOverlayContext(canvas);
|
|
46
|
+
if (!surface) return;
|
|
47
|
+
const { ctx, dpr, cssW, cssH } = surface;
|
|
56
48
|
// From here on the drawing units are CSS pixels while the backing store stays
|
|
57
49
|
// at device resolution, so the chrome is both crisp and a fixed screen size.
|
|
58
50
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
@@ -45,14 +45,18 @@ export function PixelEditorHeader({ title, subtitle, shell, chrome }) {
|
|
|
45
45
|
// callbacks the editor wires for tools. `overlayRef` is a sibling canvas at
|
|
46
46
|
// display resolution used for crisp overlays (marching-ants selection) that would
|
|
47
47
|
// otherwise render sub-pixel on the upscaled native canvas. `smoothRef` is a
|
|
48
|
+
// `latticeRef` is a sibling canvas BEHIND even that one, holding the path-layer
|
|
49
|
+
// snap lattice, so the dots sit under the art exactly as the frame background
|
|
50
|
+
// they replaced did. `smoothRef` is a
|
|
48
51
|
// sibling canvas BEHIND the main one, holding the corner-rounded render for
|
|
49
52
|
// "smooth"-mode sprites (empty/unused in "pixel" mode) so the interactive canvas
|
|
50
53
|
// above it can stay at native resolution — pointer math and tool-preview overlays
|
|
51
54
|
// are unaffected by the render mode.
|
|
52
|
-
export function PixelArtboard({ canvasRef, smoothRef, overlayRef, style, handlers, smoothPreview = false }) {
|
|
55
|
+
export function PixelArtboard({ canvasRef, smoothRef, overlayRef, latticeRef, style, handlers, smoothPreview = false }) {
|
|
53
56
|
return (
|
|
54
57
|
<div className={styles.drawingArtboard}>
|
|
55
58
|
<div className={styles.drawingArtboardFrame} style={style}>
|
|
59
|
+
<canvas ref={latticeRef} className={styles.drawingLattice} aria-hidden="true" />
|
|
56
60
|
<canvas ref={smoothRef} className={styles.drawingSmooth} aria-hidden="true" />
|
|
57
61
|
<canvas
|
|
58
62
|
ref={canvasRef}
|
|
@@ -23,7 +23,7 @@ export const PIXEL_TOOLS = [
|
|
|
23
23
|
export const PATH_TOOLS = [
|
|
24
24
|
{ id: 'brush', label: 'Brush', icon: 'pencil', shortcut: 'B' },
|
|
25
25
|
{ id: 'fill', label: 'Fill', icon: 'fill', shortcut: 'G' },
|
|
26
|
-
{ id: 'path-select', label: 'Select', icon: '
|
|
26
|
+
{ id: 'path-select', label: 'Select', icon: 'navigation-arrow', shortcut: 'V' },
|
|
27
27
|
{ id: 'bend', label: 'Bend', icon: 'bezier-curve', shortcut: 'N' },
|
|
28
28
|
];
|
|
29
29
|
|
|
@@ -40,7 +40,7 @@ export const SHAPE_MODES = [
|
|
|
40
40
|
|
|
41
41
|
// Sub-modes for the path brush tool: click-to-place pen, freehand fit, drag primitives.
|
|
42
42
|
export const BRUSH_MODES = [
|
|
43
|
-
{ id: 'freehand', label: 'Freehand', icon: '
|
|
43
|
+
{ id: 'freehand', label: 'Freehand', icon: 'scribble' },
|
|
44
44
|
{ id: 'pen', label: 'Pen', icon: 'pen-nib', shortcut: 'P' },
|
|
45
45
|
{ id: 'line', label: 'Line', icon: 'slash', shortcut: 'L' },
|
|
46
46
|
{ id: 'rect', label: 'Rectangle', icon: 'square', shortcut: 'R' },
|
|
@@ -58,7 +58,7 @@ export const PATH_BRUSH_MODE_BY_KEY = Object.fromEntries(
|
|
|
58
58
|
export const MOVE_ACTIONS = [
|
|
59
59
|
{ id: 'flipH', label: 'Flip horizontal', icon: 'flip-h' },
|
|
60
60
|
{ id: 'flipV', label: 'Flip vertical', icon: 'flip-v' },
|
|
61
|
-
{ id: 'rotateCW', label: 'Rotate clockwise', icon: '
|
|
61
|
+
{ id: 'rotateCW', label: 'Rotate clockwise', icon: 'rotate' },
|
|
62
62
|
];
|
|
63
63
|
|
|
64
64
|
// Tools whose work is driven by the active palette color, so the palette panel
|
|
@@ -636,7 +636,7 @@ function ShapeInspectorButtons({
|
|
|
636
636
|
aria-label="Rotate 90 degrees clockwise"
|
|
637
637
|
disabled={!pathActions?.hasSelection}
|
|
638
638
|
onClick={pathActions?.rotateCW}>
|
|
639
|
-
<Icon name="
|
|
639
|
+
<Icon name="rotate" />
|
|
640
640
|
</button>
|
|
641
641
|
<button
|
|
642
642
|
type="button"
|