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
|
@@ -146,7 +146,7 @@ function useResizableTimelineHeight() {
|
|
|
146
146
|
// the top, Aseprite-style) by frame COLUMNS, with thumbnails per cell, layer +
|
|
147
147
|
// frame controls, linked-cell affordances, onion-skin toggles, playback, and a
|
|
148
148
|
// tags editor. All mutation flows through `actions` (wired in PxArtEditor).
|
|
149
|
-
export function PxArtTimeline({ sprite, text, selection, playing, onion, actions }) {
|
|
149
|
+
export function PxArtTimeline({ sprite, text, selection, playing, onion, actions, readOnly = false }) {
|
|
150
150
|
// Hooks run unconditionally (order-stable); only the render branches on viewport.
|
|
151
151
|
const compact = useCompactViewport();
|
|
152
152
|
const resizable = useResizableTimelineHeight();
|
|
@@ -159,7 +159,13 @@ export function PxArtTimeline({ sprite, text, selection, playing, onion, actions
|
|
|
159
159
|
});
|
|
160
160
|
const grid = (
|
|
161
161
|
<div className={tl.scroll}>
|
|
162
|
-
<TimelineGrid
|
|
162
|
+
<TimelineGrid
|
|
163
|
+
sprite={sprite}
|
|
164
|
+
text={text}
|
|
165
|
+
selection={selection}
|
|
166
|
+
actions={actions}
|
|
167
|
+
readOnly={readOnly}
|
|
168
|
+
/>
|
|
163
169
|
</div>
|
|
164
170
|
);
|
|
165
171
|
if (compact) {
|
|
@@ -178,6 +184,7 @@ export function PxArtTimeline({ sprite, text, selection, playing, onion, actions
|
|
|
178
184
|
playing={playing}
|
|
179
185
|
onion={onion}
|
|
180
186
|
actions={actions}
|
|
187
|
+
readOnly={readOnly}
|
|
181
188
|
/>
|
|
182
189
|
{grid}
|
|
183
190
|
</section>
|
|
@@ -198,6 +205,7 @@ export function PxArtTimeline({ sprite, text, selection, playing, onion, actions
|
|
|
198
205
|
playing={playing}
|
|
199
206
|
onion={onion}
|
|
200
207
|
actions={actions}
|
|
208
|
+
readOnly={readOnly}
|
|
201
209
|
/>
|
|
202
210
|
{grid}
|
|
203
211
|
{SHOW_TAGS ? <TagsPanel sprite={sprite} actions={actions} /> : null}
|
|
@@ -211,7 +219,7 @@ export function PxArtTimeline({ sprite, text, selection, playing, onion, actions
|
|
|
211
219
|
// (duplicate/move/delete/rename/link/unlink/visibility) lives in the right-click
|
|
212
220
|
// context menus on the grid's layer rows, frame headers, and cells (see
|
|
213
221
|
// TimelineGrid + ContextMenu).
|
|
214
|
-
function TimelineBar({ sprite, selection, playing, onion, actions }) {
|
|
222
|
+
function TimelineBar({ sprite, selection, playing, onion, actions, readOnly = false }) {
|
|
215
223
|
const { layerIndex, frameIndex } = selection;
|
|
216
224
|
// A single-frame sprite has no animation, so the three animation controls
|
|
217
225
|
// (global duration, onion skin, play/pause) are meaningless — gate them on
|
|
@@ -227,19 +235,20 @@ function TimelineBar({ sprite, selection, playing, onion, actions }) {
|
|
|
227
235
|
return (
|
|
228
236
|
<div className={tl.bar}>
|
|
229
237
|
<div className={tl.barGroup}>
|
|
230
|
-
<IconButton icon="grid" label="Pixel" onClick={() => actions.addLayer(layerIndex)} />
|
|
238
|
+
<IconButton icon="grid" label="Pixel" disabled={readOnly} onClick={() => actions.addLayer(layerIndex)} />
|
|
231
239
|
<span className={tl.barLabel}>Pixel</span>
|
|
232
240
|
</div>
|
|
233
241
|
<div className={tl.barGroup}>
|
|
234
242
|
<IconButton
|
|
235
243
|
icon="draw-polygon"
|
|
236
244
|
label="Path"
|
|
245
|
+
disabled={readOnly}
|
|
237
246
|
onClick={() => actions.addLayer(layerIndex, 'path')}
|
|
238
247
|
/>
|
|
239
248
|
<span className={tl.barLabel}>Path</span>
|
|
240
249
|
</div>
|
|
241
250
|
<div className={tl.barGroup}>
|
|
242
|
-
<IconButton icon="film" label="Frame" onClick={() => actions.addFrame(frameIndex)} />
|
|
251
|
+
<IconButton icon="film" label="Frame" disabled={readOnly} onClick={() => actions.addFrame(frameIndex)} />
|
|
243
252
|
<span className={tl.barLabel}>Frame</span>
|
|
244
253
|
</div>
|
|
245
254
|
<div className={tl.barSpacer} />
|
|
@@ -249,7 +258,11 @@ function TimelineBar({ sprite, selection, playing, onion, actions }) {
|
|
|
249
258
|
<span className={tl.durationIcon} title="Default frame duration (ms) — applies to all frames">
|
|
250
259
|
<FaGlyph icon={faStopwatch} className={tl.durationGlyph} />
|
|
251
260
|
</span>
|
|
252
|
-
<GlobalDurationInput
|
|
261
|
+
<GlobalDurationInput
|
|
262
|
+
value={sprite.defaultDurationMs}
|
|
263
|
+
onCommit={actions.setDefaultDuration}
|
|
264
|
+
disabled={readOnly}
|
|
265
|
+
/>
|
|
253
266
|
</div>
|
|
254
267
|
<OnionControls onion={onion} actions={actions} />
|
|
255
268
|
<div className={tl.barGroup}>
|
|
@@ -268,12 +281,13 @@ function TimelineBar({ sprite, selection, playing, onion, actions }) {
|
|
|
268
281
|
|
|
269
282
|
// The sprite's GLOBAL duration. Commits a finite, positive value on blur/Enter;
|
|
270
283
|
// an empty/invalid entry snaps back to the current value (see CommitInput).
|
|
271
|
-
function GlobalDurationInput({ value, onCommit }) {
|
|
284
|
+
function GlobalDurationInput({ value, onCommit, disabled = false }) {
|
|
272
285
|
return (
|
|
273
286
|
<CommitInput
|
|
274
287
|
text={String(value)}
|
|
275
288
|
className={tl.globalDuration}
|
|
276
289
|
title="Global frame duration (ms) — inherited by every frame without its own override"
|
|
290
|
+
disabled={disabled}
|
|
277
291
|
onCommit={(draft) => {
|
|
278
292
|
const ms = Number(draft);
|
|
279
293
|
if (Number.isFinite(ms) && ms > 0) onCommit(ms);
|
|
@@ -322,7 +336,7 @@ function OnionControls({ onion, actions }) {
|
|
|
322
336
|
);
|
|
323
337
|
}
|
|
324
338
|
|
|
325
|
-
function TimelineGrid({ sprite, text, selection, actions }) {
|
|
339
|
+
function TimelineGrid({ sprite, text, selection, actions, readOnly = false }) {
|
|
326
340
|
const frames = sprite.frames;
|
|
327
341
|
// Render rows top-down: the last layer (top of the composite stack) first.
|
|
328
342
|
const rows = sprite.layers.map((_, i) => i).reverse();
|
|
@@ -353,12 +367,13 @@ function TimelineGrid({ sprite, text, selection, actions }) {
|
|
|
353
367
|
const openMenu = useCallback(
|
|
354
368
|
(target, event) => {
|
|
355
369
|
selectTarget(target);
|
|
370
|
+
if (readOnly) return;
|
|
356
371
|
const rect = event.currentTarget?.getBoundingClientRect?.();
|
|
357
372
|
const x = Number.isFinite(event.clientX) ? event.clientX : rect?.left + rect?.width / 2;
|
|
358
373
|
const y = Number.isFinite(event.clientY) ? event.clientY : rect?.top + rect?.height / 2;
|
|
359
374
|
setMenu({ ...target, x: x ?? 0, y: y ?? 0 });
|
|
360
375
|
},
|
|
361
|
-
[selectTarget]
|
|
376
|
+
[selectTarget, readOnly]
|
|
362
377
|
);
|
|
363
378
|
const press = useTimelineItemPress(openMenu);
|
|
364
379
|
const activateTarget = (target, event) => {
|
|
@@ -412,6 +427,7 @@ function TimelineGrid({ sprite, text, selection, actions }) {
|
|
|
412
427
|
onPointerDown={(event) => press.onPointerDown(event, { kind: 'layer', layerIndex: li })}
|
|
413
428
|
actions={actions}
|
|
414
429
|
onContextMenu={openLayerMenu}
|
|
430
|
+
readOnly={readOnly}
|
|
415
431
|
/>
|
|
416
432
|
{frames.map((_, f) => (
|
|
417
433
|
<CellThumb
|
|
@@ -744,7 +760,7 @@ const kindBadgeStyle = {
|
|
|
744
760
|
// on blur/Enter, re-syncing to `text` when idle. The caller's `onCommit(draft)`
|
|
745
761
|
// interprets the raw string (set / clear / revert). Shared by the per-frame and
|
|
746
762
|
// global duration fields so their edit/commit behavior stays identical.
|
|
747
|
-
function CommitInput({ text, className, placeholder, title, onCommit }) {
|
|
763
|
+
function CommitInput({ text, className, placeholder, title, onCommit, disabled = false }) {
|
|
748
764
|
const [draft, setDraft] = useState(text);
|
|
749
765
|
const editing = useRef(false);
|
|
750
766
|
if (!editing.current && draft !== text) setDraft(text);
|
|
@@ -759,6 +775,7 @@ function CommitInput({ text, className, placeholder, title, onCommit }) {
|
|
|
759
775
|
placeholder={placeholder}
|
|
760
776
|
title={title}
|
|
761
777
|
inputMode="numeric"
|
|
778
|
+
disabled={disabled}
|
|
762
779
|
onFocus={() => (editing.current = true)}
|
|
763
780
|
onChange={(event) => setDraft(event.target.value)}
|
|
764
781
|
onBlur={commit}
|
|
@@ -807,7 +824,7 @@ function FaGlyph({ icon, className = tl.visGlyph }) {
|
|
|
807
824
|
);
|
|
808
825
|
}
|
|
809
826
|
|
|
810
|
-
function LayerHeader({ layer, index, active, onActivate, onPointerDown, actions, onContextMenu }) {
|
|
827
|
+
function LayerHeader({ layer, index, active, onActivate, onPointerDown, actions, onContextMenu, readOnly = false }) {
|
|
811
828
|
return (
|
|
812
829
|
<div
|
|
813
830
|
className={active ? tl.layerHead + ' ' + tl.layerHeadActive : tl.layerHead}
|
|
@@ -832,6 +849,7 @@ function LayerHeader({ layer, index, active, onActivate, onPointerDown, actions,
|
|
|
832
849
|
max="100"
|
|
833
850
|
value={Math.round(layer.opacity * 100)}
|
|
834
851
|
title={`Opacity ${Math.round(layer.opacity * 100)}%`}
|
|
852
|
+
disabled={readOnly}
|
|
835
853
|
onClick={(event) => event.stopPropagation()}
|
|
836
854
|
onChange={(event) => actions.patchLayer(index, { opacity: Number(event.target.value) / 100 })}
|
|
837
855
|
/>
|
|
@@ -108,6 +108,12 @@
|
|
|
108
108
|
text-align: center;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
.globalDuration:disabled {
|
|
112
|
+
opacity: 0.55;
|
|
113
|
+
cursor: default;
|
|
114
|
+
color: var(--castle-inspector-muted);
|
|
115
|
+
}
|
|
116
|
+
|
|
111
117
|
/* Icon-only label (stopwatch) sitting next to the global duration input, in
|
|
112
118
|
place of the old "Duration … ms · all frames" text. */
|
|
113
119
|
.durationIcon {
|
|
@@ -333,6 +339,11 @@
|
|
|
333
339
|
accent-color: var(--castle-selected);
|
|
334
340
|
}
|
|
335
341
|
|
|
342
|
+
.opacity:disabled {
|
|
343
|
+
opacity: 0.4;
|
|
344
|
+
cursor: default;
|
|
345
|
+
}
|
|
346
|
+
|
|
336
347
|
.opacity::-webkit-slider-runnable-track {
|
|
337
348
|
height: 3px;
|
|
338
349
|
border-radius: 2px;
|
|
@@ -32,3 +32,44 @@ export function resumeAudioContext() {
|
|
|
32
32
|
});
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
+
|
|
36
|
+
// Where a gesture that could unlock audio might land. This deck's own window
|
|
37
|
+
// always -- plus the EDITOR's, because there the deck runs inside a panel iframe
|
|
38
|
+
// and a click on the editor's own chrome (its Play button, a file, an inspector
|
|
39
|
+
// field) is a gesture the browser grants this frame but that this frame never
|
|
40
|
+
// hears about. Without the parent, a deck's first sound stays queued until the
|
|
41
|
+
// player happens to click the game itself, which in the editor reads as sound
|
|
42
|
+
// being broken.
|
|
43
|
+
//
|
|
44
|
+
// Measured rather than assumed: a same-origin parent's pointerdown both fires
|
|
45
|
+
// here AND satisfies the autoplay policy, so the queued sound really does start.
|
|
46
|
+
//
|
|
47
|
+
// A published deck's parent is a Castle host on another origin. Touching it
|
|
48
|
+
// throws, there is nothing to listen to, and the deck's own window is the whole
|
|
49
|
+
// answer -- which is why this is a try/catch rather than a capability check.
|
|
50
|
+
export function unlockTargets() {
|
|
51
|
+
const targets = [window];
|
|
52
|
+
try {
|
|
53
|
+
const parent = window.parent;
|
|
54
|
+
if (parent && parent !== window && parent.document) targets.push(parent);
|
|
55
|
+
} catch {
|
|
56
|
+
// Cross-origin parent: embedded by a host rather than open in the editor.
|
|
57
|
+
}
|
|
58
|
+
return targets;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let resumeBound = false;
|
|
62
|
+
|
|
63
|
+
// Resume the context on the first gesture, wherever it lands. A deck that only
|
|
64
|
+
// plays buffers never goes through engine/media.js, so this is what makes its
|
|
65
|
+
// sound audible after the player finally taps: without it the context stays
|
|
66
|
+
// suspended until some later play happens to call resume again, losing the one
|
|
67
|
+
// in between. Idempotent, so callers need not track whether it ran.
|
|
68
|
+
export function bindAudioResume() {
|
|
69
|
+
if (resumeBound || typeof window === 'undefined') return;
|
|
70
|
+
resumeBound = true;
|
|
71
|
+
for (const target of unlockTargets()) {
|
|
72
|
+
target.addEventListener('pointerdown', resumeAudioContext, { passive: true });
|
|
73
|
+
target.addEventListener('keydown', resumeAudioContext, { passive: true });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { CheckboxField, ColorField, NumberField, Panel, TextField, isHexColor } from './ui';
|
|
2
|
+
import { CheckboxField, ColorField, FieldNote, NumberField, Panel, TextField, isHexColor } from './ui';
|
|
3
3
|
// Build the override-indicator field props (purple tint + "Default: X [Reset]")
|
|
4
4
|
// for a single property from a behavior panel's override context. Returns an
|
|
5
5
|
// empty object when there's no context (blueprint template editing) or the
|
|
@@ -17,6 +17,43 @@ export function overrideProps(override, prop) {
|
|
|
17
17
|
// defaults -- which are an unbounded scrubber stepping by whole numbers, wrong
|
|
18
18
|
// for anything that lives in 0..1. Declaring BOTH min and max makes it a true
|
|
19
19
|
// slider (fill + thumb). Props that declare nothing are unchanged.
|
|
20
|
+
//
|
|
21
|
+
// Any prop can also declare `hint`, a line of explanation shown under the field.
|
|
22
|
+
// As a function it receives the prop's current value and the whole component, so
|
|
23
|
+
// a hint can say what the CURRENT setting means rather than describing both
|
|
24
|
+
// halves of a toggle at once -- which is the only way a one-line note beats the
|
|
25
|
+
// prop's own name.
|
|
26
|
+
function fieldFor({ label, current, set, sample, entry, ov }) {
|
|
27
|
+
if (typeof sample === 'number') {
|
|
28
|
+
const { min, max, step } = entry ?? {};
|
|
29
|
+
return (
|
|
30
|
+
<NumberField
|
|
31
|
+
label={label}
|
|
32
|
+
value={current}
|
|
33
|
+
onChange={set}
|
|
34
|
+
{...(min == null ? {} : { min })}
|
|
35
|
+
{...(max == null ? {} : { max })}
|
|
36
|
+
{...(step == null ? {} : { step })}
|
|
37
|
+
{...ov}
|
|
38
|
+
/>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
if (typeof sample === 'boolean') {
|
|
42
|
+
return <CheckboxField label={label} checked={current} onChange={set} {...ov} />;
|
|
43
|
+
}
|
|
44
|
+
if (isHexColor(sample)) {
|
|
45
|
+
return <ColorField label={label} value={current} onChange={set} {...ov} />;
|
|
46
|
+
}
|
|
47
|
+
return (
|
|
48
|
+
<TextField label={label} value={current == null ? '' : String(current)} onChange={set} {...ov} />
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function hintFor(entry, current, component) {
|
|
53
|
+
if (typeof entry?.hint === 'function') return entry.hint(current, component);
|
|
54
|
+
return entry?.hint ?? null;
|
|
55
|
+
}
|
|
56
|
+
|
|
20
57
|
export function AutoFields({ defaultProps, component, setComponent, only, exclude, override, meta }) {
|
|
21
58
|
const keys = Object.keys(defaultProps).filter((key) => {
|
|
22
59
|
if (only) return only.includes(key);
|
|
@@ -28,39 +65,19 @@ export function AutoFields({ defaultProps, component, setComponent, only, exclud
|
|
|
28
65
|
{keys.map((key) => {
|
|
29
66
|
const fallback = defaultProps[key];
|
|
30
67
|
const current = key in component ? component[key] : fallback;
|
|
31
|
-
const
|
|
32
|
-
const label = humanizeKey(key);
|
|
33
|
-
const sample = fallback ?? current;
|
|
34
|
-
const ov = overrideProps(override, key);
|
|
35
|
-
if (typeof sample === 'number') {
|
|
36
|
-
const { min, max, step } = meta?.[key] ?? {};
|
|
37
|
-
return (
|
|
38
|
-
<NumberField
|
|
39
|
-
key={key}
|
|
40
|
-
label={label}
|
|
41
|
-
value={current}
|
|
42
|
-
onChange={set}
|
|
43
|
-
{...(min == null ? {} : { min })}
|
|
44
|
-
{...(max == null ? {} : { max })}
|
|
45
|
-
{...(step == null ? {} : { step })}
|
|
46
|
-
{...ov}
|
|
47
|
-
/>
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
if (typeof sample === 'boolean') {
|
|
51
|
-
return <CheckboxField key={key} label={label} checked={current} onChange={set} {...ov} />;
|
|
52
|
-
}
|
|
53
|
-
if (isHexColor(sample)) {
|
|
54
|
-
return <ColorField key={key} label={label} value={current} onChange={set} {...ov} />;
|
|
55
|
-
}
|
|
68
|
+
const entry = meta?.[key];
|
|
56
69
|
return (
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
70
|
+
<React.Fragment key={key}>
|
|
71
|
+
{fieldFor({
|
|
72
|
+
label: humanizeKey(key),
|
|
73
|
+
current,
|
|
74
|
+
set: (value) => setComponent({ [key]: value }),
|
|
75
|
+
sample: fallback ?? current,
|
|
76
|
+
entry,
|
|
77
|
+
ov: overrideProps(override, key),
|
|
78
|
+
})}
|
|
79
|
+
<FieldNote>{hintFor(entry, current, component)}</FieldNote>
|
|
80
|
+
</React.Fragment>
|
|
64
81
|
);
|
|
65
82
|
})}
|
|
66
83
|
</>
|
|
@@ -69,13 +69,25 @@ function parseBlueprintText(text) {
|
|
|
69
69
|
function resolveTemplateRefs(template, files, fromPath) {
|
|
70
70
|
if (!template) return template;
|
|
71
71
|
for (const props of Object.values(template.components ?? {})) {
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
resolveRefsIn(props, files, fromPath);
|
|
73
|
+
}
|
|
74
|
+
return template;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Nested because a ref is not always a bare prop: `Shape.art` can be one string
|
|
78
|
+
// for the whole shape OR an object naming a file per face, and an unresolved
|
|
79
|
+
// face ref means an untextured surface with no error anywhere. Rewriting only
|
|
80
|
+
// when the resolved path is actually in the file map is what keeps this from
|
|
81
|
+
// mangling strings that were never refs.
|
|
82
|
+
function resolveRefsIn(props, files, fromPath) {
|
|
83
|
+
for (const [key, value] of Object.entries(props ?? {})) {
|
|
84
|
+
if (typeof value === 'string' && value) {
|
|
74
85
|
const resolved = resolveDeckFile(value, fromPath);
|
|
75
86
|
if (resolved !== value && files?.[resolved] !== undefined) props[key] = resolved;
|
|
87
|
+
} else if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
88
|
+
resolveRefsIn(value, files, fromPath);
|
|
76
89
|
}
|
|
77
90
|
}
|
|
78
|
-
return template;
|
|
79
91
|
}
|
|
80
92
|
|
|
81
93
|
// Resolve a blueprint's template from the LIVE files map -- the load-bearing
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
// Consumer side of the serve's `files_changed` broadcast.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
1
|
+
// Consumer side of the serve's `files_changed` broadcast. Data files are
|
|
2
|
+
// re-read over the serve's files API and folded into React state, so an editor
|
|
3
|
+
// shows a drawing someone else just changed without reloading anything.
|
|
4
|
+
//
|
|
5
|
+
// A CODE change does nothing here. Only a page reload picks up new code, and a
|
|
6
|
+
// reload thrown at a panel someone is working in loses their place, so the
|
|
7
|
+
// decision belongs to the person: every panel header carries a reload control,
|
|
8
|
+
// and the shell offers the Play panel a restart when it has fallen behind.
|
|
9
9
|
import { useEffect, useRef, useState } from 'react';
|
|
10
|
-
import { onFilesChanged
|
|
10
|
+
import { onFilesChanged } from 'castle-web-sdk';
|
|
11
11
|
import { initialFiles } from './files';
|
|
12
12
|
|
|
13
13
|
const DATA_EXTS = ['.scene', '.pxart', '.sprite', '.style', '.drawing'];
|
|
@@ -17,7 +17,20 @@ export function isDataFile(path) {
|
|
|
17
17
|
return DATA_EXTS.some((ext) => path.endsWith(ext));
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
// Which iframe is speaking -- every panel runs its own copy of this.
|
|
21
|
+
function panelTag() {
|
|
22
|
+
try {
|
|
23
|
+
const q = new URLSearchParams(location.search);
|
|
24
|
+
return q.get('panel') || q.get('file') || 'deck';
|
|
25
|
+
} catch {
|
|
26
|
+
return 'deck';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Whether a change means this bundle's code is stale. Only used for the log
|
|
31
|
+
// line -- nothing acts on it -- but knowing a panel went stale and when is what
|
|
32
|
+
// makes a vague "it looked wrong" report checkable against `.castle/serve.log`.
|
|
33
|
+
function isCodeChange(change) {
|
|
21
34
|
if (isDataFile(change.path)) return false;
|
|
22
35
|
if (change.path.endsWith('.html')) return true;
|
|
23
36
|
// In the module graph (directly or as someone's import) -> our code changed.
|
|
@@ -37,34 +50,34 @@ async function readDeckFile(path) {
|
|
|
37
50
|
// Deck files as live state: starts from the build-time glob snapshot and stays
|
|
38
51
|
// current as files change on disk from any source (editors, agents, terminal).
|
|
39
52
|
// `shouldSkipPath` lets an editor keep its own in-flight edits from being
|
|
40
|
-
// clobbered by the fs echo of a stale write.
|
|
41
|
-
// applied data change — key a player on it to rebuild from fresh data.
|
|
53
|
+
// clobbered by the fs echo of a stale write.
|
|
42
54
|
export function useLiveDeckFiles({ shouldSkipPath } = {}) {
|
|
43
55
|
const [files, setFiles] = useState(initialFiles);
|
|
44
|
-
const [dataVersion, setDataVersion] = useState(0);
|
|
45
56
|
const skipRef = useRef(shouldSkipPath);
|
|
46
57
|
skipRef.current = shouldSkipPath;
|
|
47
58
|
|
|
48
59
|
useEffect(
|
|
49
60
|
() =>
|
|
50
61
|
onFilesChanged((event) => {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
62
|
+
const stale = event.changes.find(isCodeChange);
|
|
63
|
+
if (stale) console.log(`[reload] ${panelTag()} code stale <- ${stale.path}`);
|
|
64
|
+
// Data still applies in the same event: a change that carries both is
|
|
65
|
+
// one edit, and holding the data back would leave the panel showing
|
|
66
|
+
// neither the old state nor the new one.
|
|
55
67
|
const dataChanges = event.changes.filter(
|
|
56
68
|
(change) => isDataFile(change.path) && !skipRef.current?.(change.path)
|
|
57
69
|
);
|
|
58
70
|
if (dataChanges.length === 0) return;
|
|
59
|
-
|
|
71
|
+
console.log(`[reload] ${panelTag()} data <- ${dataChanges.map((c) => c.path).join(', ')}`);
|
|
72
|
+
void applyDataChanges(dataChanges, setFiles);
|
|
60
73
|
}),
|
|
61
74
|
[]
|
|
62
75
|
);
|
|
63
76
|
|
|
64
|
-
return { files, setFiles
|
|
77
|
+
return { files, setFiles };
|
|
65
78
|
}
|
|
66
79
|
|
|
67
|
-
async function applyDataChanges(changes, setFiles
|
|
80
|
+
async function applyDataChanges(changes, setFiles) {
|
|
68
81
|
const updates = await Promise.all(
|
|
69
82
|
changes.map(async (change) => {
|
|
70
83
|
if (change.event === 'delete') return { path: change.path, text: null };
|
|
@@ -85,5 +98,4 @@ async function applyDataChanges(changes, setFiles, setDataVersion) {
|
|
|
85
98
|
}
|
|
86
99
|
return next;
|
|
87
100
|
});
|
|
88
|
-
setDataVersion((version) => version + 1);
|
|
89
101
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// The pool of playing media elements: one `<audio>` / `<video>` per actor per
|
|
2
|
-
// file. Behaviors (
|
|
3
|
-
// they want on it; this module owns creating, reusing, and
|
|
2
|
+
// file. Behaviors (SoundPlayer with `stream` on, Video) ask for theirs each frame
|
|
3
|
+
// and set the state they want on it; this module owns creating, reusing, and
|
|
4
|
+
// reaping them. Short sound effects don't come through here at all -- they are
|
|
5
|
+
// decoded buffers, in engine/sound.js.
|
|
4
6
|
//
|
|
5
7
|
// The pool is MODULE-level, not per-SceneRuntime, because the editor builds a
|
|
6
8
|
// throwaway runtime every frame to draw the edit preview -- a per-runtime pool
|
|
@@ -15,7 +17,7 @@
|
|
|
15
17
|
// editor's video preview draws a paused element's first frame.
|
|
16
18
|
|
|
17
19
|
import { initialMedia } from './files';
|
|
18
|
-
import { getAudioContext, resumeAudioContext } from './audioContext';
|
|
20
|
+
import { getAudioContext, resumeAudioContext, unlockTargets } from './audioContext';
|
|
19
21
|
|
|
20
22
|
// Reaped after this long without being asked for. Long enough to survive a
|
|
21
23
|
// scene's worth of frames where an actor is briefly not drawn (offscreen,
|
|
@@ -24,10 +26,6 @@ import { getAudioContext, resumeAudioContext } from './audioContext';
|
|
|
24
26
|
const IDLE_REAP_MS = 1500;
|
|
25
27
|
|
|
26
28
|
const entries = new Map();
|
|
27
|
-
// One-shot plays: copies of an element that overlap the original instead of
|
|
28
|
-
// cutting it off (a sound effect fired again before the last one finished).
|
|
29
|
-
// They are not keyed by anything -- they exist until they end.
|
|
30
|
-
const oneShots = new Set();
|
|
31
29
|
// Elements routed through WebAudio for panning, and their panner nodes. An
|
|
32
30
|
// element can only be connected to the graph ONCE, so this is also the record
|
|
33
31
|
// of which ones already are.
|
|
@@ -63,8 +61,10 @@ function bindUnlock() {
|
|
|
63
61
|
);
|
|
64
62
|
}
|
|
65
63
|
};
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
for (const target of unlockTargets()) {
|
|
65
|
+
target.addEventListener('pointerdown', retry, { passive: true });
|
|
66
|
+
target.addEventListener('keydown', retry, { passive: true });
|
|
67
|
+
}
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
// Play, tolerating the autoplay policy: a refused play is queued for the next
|
|
@@ -81,6 +81,26 @@ export function playMedia(element) {
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
// Retune a sound along with its speed, the way a tape does -- and the way the
|
|
85
|
+
// buffer path's playbackRate already does.
|
|
86
|
+
//
|
|
87
|
+
// An element does the OPPOSITE by default. `preservesPitch` starts TRUE, so it
|
|
88
|
+
// time-stretches to hold the pitch steady while the speed changes: right for a
|
|
89
|
+
// podcast at 1.5x, wrong for a game sound, and audibly a phase vocoder rather
|
|
90
|
+
// than a clean retune. Measured on a 110Hz tone at rate 2: 111Hz with the
|
|
91
|
+
// default, 220Hz with this off -- the latter matching an AudioBufferSourceNode
|
|
92
|
+
// exactly, which is what makes one `playbackRate` prop mean one thing across
|
|
93
|
+
// both of this kit's playback paths.
|
|
94
|
+
//
|
|
95
|
+
// The prefixed spellings are for browsers older than the standard property;
|
|
96
|
+
// assigning one that doesn't exist is harmless.
|
|
97
|
+
export function setMediaPlaybackRate(element, rate) {
|
|
98
|
+
element.preservesPitch = false;
|
|
99
|
+
element.webkitPreservesPitch = false;
|
|
100
|
+
element.mozPreservesPitch = false;
|
|
101
|
+
element.playbackRate = rate;
|
|
102
|
+
}
|
|
103
|
+
|
|
84
104
|
// Pan a sound left/right (-1..1). The element has no panning of its own, so the
|
|
85
105
|
// first non-zero pan routes it through a WebAudio graph -- permanently, since a
|
|
86
106
|
// media element can only be connected once. Which is why pan 0 (the default)
|
|
@@ -107,31 +127,6 @@ export function setMediaPan(element, pan) {
|
|
|
107
127
|
resumeAudioContext();
|
|
108
128
|
}
|
|
109
129
|
|
|
110
|
-
// Play a copy of this sound, overlapping whatever is already playing. Returns a
|
|
111
|
-
// promise that resolves when the copy finishes, so a caller can wait for it.
|
|
112
|
-
// Copies are never reused: a one-shot is done when it's done.
|
|
113
|
-
export function playOneShot(path, { volume = 1, playbackRate = 1, pan = 0 } = {}) {
|
|
114
|
-
const url = mediaUrl(path);
|
|
115
|
-
if (!url) return Promise.resolve();
|
|
116
|
-
const element = document.createElement('audio');
|
|
117
|
-
element.src = url;
|
|
118
|
-
element.volume = volume;
|
|
119
|
-
element.playbackRate = playbackRate;
|
|
120
|
-
oneShots.add(element);
|
|
121
|
-
const finished = new Promise((resolve) => {
|
|
122
|
-
const done = () => {
|
|
123
|
-
oneShots.delete(element);
|
|
124
|
-
element.removeAttribute('src');
|
|
125
|
-
resolve();
|
|
126
|
-
};
|
|
127
|
-
element.addEventListener('ended', done, { once: true });
|
|
128
|
-
element.addEventListener('error', done, { once: true });
|
|
129
|
-
});
|
|
130
|
-
if (pan !== 0) setMediaPan(element, pan);
|
|
131
|
-
playMedia(element);
|
|
132
|
-
return finished;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
130
|
export function stopMedia(element) {
|
|
136
131
|
blocked.delete(element);
|
|
137
132
|
element.pause();
|
|
@@ -202,11 +197,6 @@ export function stopAllMedia() {
|
|
|
202
197
|
stopMedia(entry.element);
|
|
203
198
|
entry.element.removeAttribute('src');
|
|
204
199
|
}
|
|
205
|
-
for (const element of oneShots) {
|
|
206
|
-
stopMedia(element);
|
|
207
|
-
element.removeAttribute('src');
|
|
208
|
-
}
|
|
209
200
|
entries.clear();
|
|
210
|
-
oneShots.clear();
|
|
211
201
|
blocked.clear();
|
|
212
202
|
}
|