castle-web-cli 0.4.3 → 0.4.5
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/index.js +8 -2
- package/dist/init.js +1 -1
- package/kits/basic-2d/CLAUDE.md +3 -3
- package/package.json +1 -1
- package/kits/basic-2d-frozen/.prettierrc +0 -8
- package/kits/basic-2d-frozen/CLAUDE.md +0 -131
- package/kits/basic-2d-frozen/behaviors/Camera.jsx +0 -43
- package/kits/basic-2d-frozen/behaviors/Collider.jsx +0 -71
- package/kits/basic-2d-frozen/behaviors/Drawing.jsx +0 -139
- package/kits/basic-2d-frozen/behaviors/Layout.jsx +0 -16
- package/kits/basic-2d-frozen/drawings/floor.drawing +0 -70
- package/kits/basic-2d-frozen/editors/App.jsx +0 -152
- package/kits/basic-2d-frozen/editors/CodeEditor.jsx +0 -112
- package/kits/basic-2d-frozen/editors/DrawingEditor.jsx +0 -222
- package/kits/basic-2d-frozen/editors/FileBrowser.jsx +0 -143
- package/kits/basic-2d-frozen/editors/PlayOnly.jsx +0 -21
- package/kits/basic-2d-frozen/editors/SceneEditor.jsx +0 -1012
- package/kits/basic-2d-frozen/editors/behaviorRegistry.js +0 -24
- package/kits/basic-2d-frozen/editors/editorHistory.js +0 -52
- package/kits/basic-2d-frozen/engine/ScenePlayer.jsx +0 -83
- package/kits/basic-2d-frozen/engine/SceneUI.jsx +0 -67
- package/kits/basic-2d-frozen/engine/TouchControls.jsx +0 -136
- package/kits/basic-2d-frozen/engine/autoInspector.jsx +0 -51
- package/kits/basic-2d-frozen/engine/files.js +0 -62
- package/kits/basic-2d-frozen/engine/scene.js +0 -420
- package/kits/basic-2d-frozen/engine/ui.jsx +0 -344
- package/kits/basic-2d-frozen/engine/ui.module.css +0 -928
- package/kits/basic-2d-frozen/eslint.config.js +0 -50
- package/kits/basic-2d-frozen/index.html +0 -11
- package/kits/basic-2d-frozen/main.jsx +0 -10
- package/kits/basic-2d-frozen/package-lock.json +0 -2706
- package/kits/basic-2d-frozen/package.json +0 -41
- package/kits/basic-2d-frozen/scenes/main.scene +0 -108
- package/kits/basic-2d-frozen/vite.config.js +0 -1
- package/kits/rpg-2d/.prettierrc +0 -8
- package/kits/rpg-2d/behaviors/Camera.tsx +0 -52
- package/kits/rpg-2d/behaviors/Collider.tsx +0 -98
- package/kits/rpg-2d/behaviors/Dialog.tsx +0 -184
- package/kits/rpg-2d/behaviors/Drawing.tsx +0 -161
- package/kits/rpg-2d/behaviors/Friend.tsx +0 -45
- package/kits/rpg-2d/behaviors/Layout.tsx +0 -29
- package/kits/rpg-2d/behaviors/PlayerController.tsx +0 -255
- package/kits/rpg-2d/behaviors/Portal.tsx +0 -60
- package/kits/rpg-2d/behaviors/QuestLog.tsx +0 -90
- package/kits/rpg-2d/behaviors/SaveMenu.tsx +0 -123
- package/kits/rpg-2d/behaviors/Tilemap.tsx +0 -90
- package/kits/rpg-2d/drawings/bld-home.drawing +0 -8136
- package/kits/rpg-2d/drawings/env-crate.drawing +0 -509
- package/kits/rpg-2d/drawings/env-fence.drawing +0 -536
- package/kits/rpg-2d/drawings/env-flower-bed.drawing +0 -607
- package/kits/rpg-2d/drawings/env-fountain.drawing +0 -2622
- package/kits/rpg-2d/drawings/env-hedge.drawing +0 -601
- package/kits/rpg-2d/drawings/env-house-blue.drawing +0 -1
- package/kits/rpg-2d/drawings/env-house-green.drawing +0 -1
- package/kits/rpg-2d/drawings/env-tree-oak.drawing +0 -1540
- package/kits/rpg-2d/drawings/env-tree-pine.drawing +0 -1315
- package/kits/rpg-2d/drawings/floor.drawing +0 -70
- package/kits/rpg-2d/drawings/fx-sparkle.drawing +0 -926
- package/kits/rpg-2d/drawings/npc-juno-idle-down.drawing +0 -1099
- package/kits/rpg-2d/drawings/npc-juno-walk-down.drawing +0 -4177
- package/kits/rpg-2d/drawings/npc-opal-idle-down.drawing +0 -1099
- package/kits/rpg-2d/drawings/npc-opal-walk-down.drawing +0 -4177
- package/kits/rpg-2d/drawings/player-idle-down.drawing +0 -1070
- package/kits/rpg-2d/drawings/player-idle-left.drawing +0 -1070
- package/kits/rpg-2d/drawings/player-idle-right.drawing +0 -1070
- package/kits/rpg-2d/drawings/player-idle-up.drawing +0 -1070
- package/kits/rpg-2d/drawings/player-walk-down.drawing +0 -4148
- package/kits/rpg-2d/drawings/player-walk-left.drawing +0 -4148
- package/kits/rpg-2d/drawings/player-walk-right.drawing +0 -4148
- package/kits/rpg-2d/drawings/player-walk-up.drawing +0 -4148
- package/kits/rpg-2d/editors/App.tsx +0 -163
- package/kits/rpg-2d/editors/CodeEditor.tsx +0 -120
- package/kits/rpg-2d/editors/DrawingEditor.tsx +0 -278
- package/kits/rpg-2d/editors/FileBrowser.tsx +0 -191
- package/kits/rpg-2d/editors/PlayOnly.tsx +0 -26
- package/kits/rpg-2d/editors/SceneEditor.tsx +0 -1093
- package/kits/rpg-2d/editors/behaviorRegistry.ts +0 -33
- package/kits/rpg-2d/editors/editorHistory.ts +0 -75
- package/kits/rpg-2d/editors/editorProps.ts +0 -10
- package/kits/rpg-2d/engine/ScenePlayer.tsx +0 -130
- package/kits/rpg-2d/engine/SceneUI.tsx +0 -74
- package/kits/rpg-2d/engine/TouchControls.tsx +0 -157
- package/kits/rpg-2d/engine/autoInspector.tsx +0 -111
- package/kits/rpg-2d/engine/drawing.ts +0 -81
- package/kits/rpg-2d/engine/files.ts +0 -215
- package/kits/rpg-2d/engine/scene.ts +0 -484
- package/kits/rpg-2d/engine/ui.module.css +0 -928
- package/kits/rpg-2d/engine/ui.tsx +0 -483
- package/kits/rpg-2d/eslint.config.js +0 -46
- package/kits/rpg-2d/index.html +0 -11
- package/kits/rpg-2d/main.tsx +0 -14
- package/kits/rpg-2d/package-lock.json +0 -3149
- package/kits/rpg-2d/package.json +0 -46
- package/kits/rpg-2d/scenes/main.scene +0 -203
- package/kits/rpg-2d/tsconfig.json +0 -17
- package/kits/rpg-2d/vite-env.d.ts +0 -7
- package/kits/rpg-2d/vite.config.js +0 -1
|
@@ -1,1012 +0,0 @@
|
|
|
1
|
-
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
-
import { basename, formatJson, parseJsonFile } from '../engine/files';
|
|
3
|
-
import { TouchControls } from '../engine/TouchControls';
|
|
4
|
-
import {
|
|
5
|
-
addActor,
|
|
6
|
-
configureSceneCanvas,
|
|
7
|
-
duplicateActors,
|
|
8
|
-
makeScene,
|
|
9
|
-
removeActorComponent,
|
|
10
|
-
removeActors,
|
|
11
|
-
screenToCard,
|
|
12
|
-
setActorComponent,
|
|
13
|
-
} from '../engine/scene';
|
|
14
|
-
import {
|
|
15
|
-
cx,
|
|
16
|
-
CheckboxField,
|
|
17
|
-
ColorField,
|
|
18
|
-
EditorBody,
|
|
19
|
-
EditorHeader,
|
|
20
|
-
Icon,
|
|
21
|
-
IconButton,
|
|
22
|
-
NumberField,
|
|
23
|
-
Panel,
|
|
24
|
-
SheetGrabHandle,
|
|
25
|
-
styles,
|
|
26
|
-
TextField,
|
|
27
|
-
useMobileSheet,
|
|
28
|
-
} from '../engine/ui';
|
|
29
|
-
import { AutoInspector } from '../engine/autoInspector';
|
|
30
|
-
import { SceneUI } from '../engine/SceneUI';
|
|
31
|
-
import { behaviorClasses, findBehaviorClass } from './behaviorRegistry';
|
|
32
|
-
import { useEditHistory } from './editorHistory';
|
|
33
|
-
const LONG_PRESS_MS = 500;
|
|
34
|
-
const DRAG_THRESHOLD = 4;
|
|
35
|
-
const DEFAULT_GRID_SIZE = 25;
|
|
36
|
-
export function SceneEditor({
|
|
37
|
-
path,
|
|
38
|
-
text,
|
|
39
|
-
files,
|
|
40
|
-
drawings,
|
|
41
|
-
onChange,
|
|
42
|
-
onToggleFiles,
|
|
43
|
-
filesOpen,
|
|
44
|
-
selectedActorIds,
|
|
45
|
-
onSelectActorIds,
|
|
46
|
-
multiSelectMode,
|
|
47
|
-
onSetMultiSelectMode,
|
|
48
|
-
}) {
|
|
49
|
-
const canvasRef = useRef(null);
|
|
50
|
-
const runtimeRef = useRef(null);
|
|
51
|
-
const marqueeRef = useRef(null);
|
|
52
|
-
const editCameraRef = useRef({ x: 0, y: 0 });
|
|
53
|
-
const selectedActorIdsRef = useRef(selectedActorIds);
|
|
54
|
-
selectedActorIdsRef.current = selectedActorIds;
|
|
55
|
-
const [isPlaying, setIsPlaying] = useState(false);
|
|
56
|
-
const [panMode, setPanMode] = useState(false);
|
|
57
|
-
const history = useEditHistory(text, onChange);
|
|
58
|
-
const showMulti = selectedActorIds.length > 1 || multiSelectMode;
|
|
59
|
-
const inspectorSheet = useSelectionInspectorSheet(true);
|
|
60
|
-
const { value: sceneData, error } = parseJsonFile(path, text);
|
|
61
|
-
const getRuntimeKeys = useCallback(() => runtimeRef.current?.keys ?? null, []);
|
|
62
|
-
const getRuntime = useCallback(() => runtimeRef.current, []);
|
|
63
|
-
useScenePlayLoop({
|
|
64
|
-
sceneData,
|
|
65
|
-
drawings,
|
|
66
|
-
isPlaying,
|
|
67
|
-
text,
|
|
68
|
-
canvasRef,
|
|
69
|
-
runtimeRef,
|
|
70
|
-
editCameraRef,
|
|
71
|
-
selectedActorIds,
|
|
72
|
-
marqueeRef,
|
|
73
|
-
});
|
|
74
|
-
const panGesture = usePanGesture({ canvasRef, editCameraRef, enabled: panMode && !isPlaying });
|
|
75
|
-
useScenePlayKeys(runtimeRef, setIsPlaying);
|
|
76
|
-
const gesture = useSelectionGesture({
|
|
77
|
-
canvasRef,
|
|
78
|
-
sceneData,
|
|
79
|
-
drawings,
|
|
80
|
-
isPlaying,
|
|
81
|
-
selectedActorIds,
|
|
82
|
-
onSelectActorIds,
|
|
83
|
-
multiSelectMode,
|
|
84
|
-
onSetMultiSelectMode,
|
|
85
|
-
marqueeRef,
|
|
86
|
-
editCameraRef,
|
|
87
|
-
applyScene: (next) => onChange(formatJson(next)),
|
|
88
|
-
recordSceneSnapshot: history.recordSnapshot,
|
|
89
|
-
});
|
|
90
|
-
const playPointer = usePlayPointerGesture({ canvasRef, runtimeRef });
|
|
91
|
-
useSelectionKeyboard({
|
|
92
|
-
sceneData,
|
|
93
|
-
selectedActorIds,
|
|
94
|
-
onSelectActorIds,
|
|
95
|
-
multiSelectMode,
|
|
96
|
-
onSetMultiSelectMode,
|
|
97
|
-
isPlaying,
|
|
98
|
-
commitScene: (next) => history.commit(formatJson(next)),
|
|
99
|
-
});
|
|
100
|
-
if (!sceneData) {
|
|
101
|
-
return (
|
|
102
|
-
<>
|
|
103
|
-
<EditorHeader title={basename(path)} onToggleFiles={onToggleFiles} filesOpen={filesOpen} />
|
|
104
|
-
<EditorBody>
|
|
105
|
-
<div className={styles.inspector}>{error}</div>
|
|
106
|
-
</EditorBody>
|
|
107
|
-
</>
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
const selectedActor =
|
|
111
|
-
selectedActorIds.length === 1
|
|
112
|
-
? sceneData.actors.find((actor) => actor.id === selectedActorIds[0])
|
|
113
|
-
: undefined;
|
|
114
|
-
const actions = makeSceneActions({
|
|
115
|
-
sceneData,
|
|
116
|
-
commit: history.commit,
|
|
117
|
-
selectedActorIds,
|
|
118
|
-
onSelectActorIds,
|
|
119
|
-
});
|
|
120
|
-
const snapSettings = getSnapSettings(sceneData);
|
|
121
|
-
const updateSceneSettings = (nextScene) => {
|
|
122
|
-
history.commit(formatJson(setSceneSettings(sceneData, nextScene)));
|
|
123
|
-
};
|
|
124
|
-
const updateSceneEditorSettings = (nextEditor) => {
|
|
125
|
-
history.commit(formatJson(setSceneEditorSettings(sceneData, nextEditor)));
|
|
126
|
-
};
|
|
127
|
-
const inspectorLabel = showMulti ? 'Multi-select' : selectedActor ? 'Inspector' : 'Scene';
|
|
128
|
-
const inspectorHint = showMulti
|
|
129
|
-
? `${selectedActorIds.length} actor${selectedActorIds.length === 1 ? '' : 's'} selected`
|
|
130
|
-
: selectedActor
|
|
131
|
-
? `actor ${selectedActor.id}`
|
|
132
|
-
: 'scene settings';
|
|
133
|
-
const playbackButtons = (
|
|
134
|
-
<PlaybackButtons
|
|
135
|
-
isPlaying={isPlaying}
|
|
136
|
-
setIsPlaying={setIsPlaying}
|
|
137
|
-
history={history}
|
|
138
|
-
panMode={panMode}
|
|
139
|
-
setPanMode={setPanMode}
|
|
140
|
-
/>
|
|
141
|
-
);
|
|
142
|
-
const actorButtons = (
|
|
143
|
-
<ActorToolButtons
|
|
144
|
-
sceneData={sceneData}
|
|
145
|
-
files={files}
|
|
146
|
-
drawings={drawings}
|
|
147
|
-
selectedActorIds={selectedActorIds}
|
|
148
|
-
onSelectActorIds={onSelectActorIds}
|
|
149
|
-
isPlaying={isPlaying}
|
|
150
|
-
onChange={onChange}
|
|
151
|
-
history={history}
|
|
152
|
-
/>
|
|
153
|
-
);
|
|
154
|
-
return (
|
|
155
|
-
<>
|
|
156
|
-
<EditorHeader
|
|
157
|
-
title={sceneData.name ?? basename(path)}
|
|
158
|
-
subtitle={isPlaying ? 'WASD / arrows' : `${sceneData.actors.length} actors`}
|
|
159
|
-
right={
|
|
160
|
-
<span className={styles.mobileOnly}>
|
|
161
|
-
{playbackButtons}
|
|
162
|
-
{actorButtons}
|
|
163
|
-
</span>
|
|
164
|
-
}
|
|
165
|
-
onToggleFiles={onToggleFiles}
|
|
166
|
-
filesOpen={filesOpen}
|
|
167
|
-
/>
|
|
168
|
-
<EditorBody>
|
|
169
|
-
<div className={styles.sceneWorkspace}>
|
|
170
|
-
<div className={styles.sceneTools}>
|
|
171
|
-
<span aria-hidden="true" />
|
|
172
|
-
<div className={styles.sceneToolsGroup}>{playbackButtons}</div>
|
|
173
|
-
<div className={styles.sceneToolsGroup}>{actorButtons}</div>
|
|
174
|
-
</div>
|
|
175
|
-
<div className={styles.stageWrap}>
|
|
176
|
-
<div className={styles.stageCard}>
|
|
177
|
-
<canvas
|
|
178
|
-
ref={canvasRef}
|
|
179
|
-
className={styles.stageCanvas}
|
|
180
|
-
onPointerDown={
|
|
181
|
-
isPlaying
|
|
182
|
-
? playPointer.onPointerDown
|
|
183
|
-
: panMode
|
|
184
|
-
? panGesture.onPointerDown
|
|
185
|
-
: gesture.onPointerDown
|
|
186
|
-
}
|
|
187
|
-
onPointerMove={
|
|
188
|
-
isPlaying
|
|
189
|
-
? playPointer.onPointerMove
|
|
190
|
-
: panMode
|
|
191
|
-
? panGesture.onPointerMove
|
|
192
|
-
: gesture.onPointerMove
|
|
193
|
-
}
|
|
194
|
-
onPointerUp={
|
|
195
|
-
isPlaying
|
|
196
|
-
? playPointer.onPointerUp
|
|
197
|
-
: panMode
|
|
198
|
-
? panGesture.onPointerUp
|
|
199
|
-
: gesture.onPointerUp
|
|
200
|
-
}
|
|
201
|
-
onPointerCancel={
|
|
202
|
-
isPlaying
|
|
203
|
-
? playPointer.onPointerUp
|
|
204
|
-
: panMode
|
|
205
|
-
? panGesture.onPointerUp
|
|
206
|
-
: gesture.onPointerUp
|
|
207
|
-
}
|
|
208
|
-
/>
|
|
209
|
-
{isPlaying && <SceneUI getRuntime={getRuntime} />}
|
|
210
|
-
</div>
|
|
211
|
-
</div>
|
|
212
|
-
<aside {...inspectorSheet.rootProps}>
|
|
213
|
-
<div {...inspectorSheet.grabProps}>
|
|
214
|
-
<SheetGrabHandle label={inspectorLabel} hint={inspectorHint} />
|
|
215
|
-
</div>
|
|
216
|
-
<div className={cx(styles.sheetBody, styles.inspectorBody)}>
|
|
217
|
-
{showMulti ? (
|
|
218
|
-
<MultiSelectInspector
|
|
219
|
-
count={selectedActorIds.length}
|
|
220
|
-
onDelete={actions.deleteSelection}
|
|
221
|
-
onDuplicate={actions.duplicateSelection}
|
|
222
|
-
onDeselectAll={() => {
|
|
223
|
-
onSelectActorIds([]);
|
|
224
|
-
onSetMultiSelectMode(false);
|
|
225
|
-
}}
|
|
226
|
-
/>
|
|
227
|
-
) : selectedActor ? (
|
|
228
|
-
<ActorInspector
|
|
229
|
-
selectedActor={selectedActor}
|
|
230
|
-
files={files}
|
|
231
|
-
onSetComponent={actions.updateComponent}
|
|
232
|
-
onAddBehavior={actions.addBehavior}
|
|
233
|
-
onRemoveBehavior={actions.removeBehavior}
|
|
234
|
-
/>
|
|
235
|
-
) : (
|
|
236
|
-
<SceneInspector
|
|
237
|
-
sceneData={sceneData}
|
|
238
|
-
snapSettings={snapSettings}
|
|
239
|
-
onChangeScene={updateSceneSettings}
|
|
240
|
-
onChangeEditor={updateSceneEditorSettings}
|
|
241
|
-
/>
|
|
242
|
-
)}
|
|
243
|
-
</div>
|
|
244
|
-
</aside>
|
|
245
|
-
</div>
|
|
246
|
-
</EditorBody>
|
|
247
|
-
<TouchControls getKeys={getRuntimeKeys} visible={isPlaying} />
|
|
248
|
-
</>
|
|
249
|
-
);
|
|
250
|
-
}
|
|
251
|
-
function PlaybackButtons({ isPlaying, setIsPlaying, history, panMode, setPanMode }) {
|
|
252
|
-
return (
|
|
253
|
-
<>
|
|
254
|
-
<IconButton
|
|
255
|
-
icon={isPlaying ? 'stop' : 'play'}
|
|
256
|
-
label={isPlaying ? 'Stop' : 'Play'}
|
|
257
|
-
active={isPlaying}
|
|
258
|
-
variant={isPlaying ? 'primary' : ''}
|
|
259
|
-
onClick={() => setIsPlaying(!isPlaying)}
|
|
260
|
-
/>
|
|
261
|
-
<IconButton
|
|
262
|
-
icon="undo"
|
|
263
|
-
label="Undo"
|
|
264
|
-
onClick={history.undo}
|
|
265
|
-
disabled={!history.canUndo || isPlaying}
|
|
266
|
-
/>
|
|
267
|
-
<IconButton
|
|
268
|
-
icon="redo"
|
|
269
|
-
label="Redo"
|
|
270
|
-
onClick={history.redo}
|
|
271
|
-
disabled={!history.canRedo || isPlaying}
|
|
272
|
-
/>
|
|
273
|
-
<span style={{ width: 16 }} aria-hidden="true" />
|
|
274
|
-
<IconButton
|
|
275
|
-
icon="camera"
|
|
276
|
-
label="Pan camera"
|
|
277
|
-
active={panMode}
|
|
278
|
-
onClick={() => setPanMode((value) => !value)}
|
|
279
|
-
disabled={isPlaying}
|
|
280
|
-
/>
|
|
281
|
-
</>
|
|
282
|
-
);
|
|
283
|
-
}
|
|
284
|
-
function ActorToolButtons({
|
|
285
|
-
sceneData,
|
|
286
|
-
files,
|
|
287
|
-
drawings,
|
|
288
|
-
selectedActorIds,
|
|
289
|
-
onSelectActorIds,
|
|
290
|
-
isPlaying,
|
|
291
|
-
onChange,
|
|
292
|
-
history,
|
|
293
|
-
}) {
|
|
294
|
-
const applyScene = (next) => onChange(formatJson(next));
|
|
295
|
-
const onAdd = () => {
|
|
296
|
-
const { sceneData: next, newId } = addActor(sceneData, files, drawings);
|
|
297
|
-
history.recordSnapshot();
|
|
298
|
-
applyScene(next);
|
|
299
|
-
onSelectActorIds([newId]);
|
|
300
|
-
};
|
|
301
|
-
const onDuplicate = () => {
|
|
302
|
-
if (selectedActorIds.length === 0) return;
|
|
303
|
-
const { sceneData: next, newIds } = duplicateActors(sceneData, selectedActorIds);
|
|
304
|
-
history.recordSnapshot();
|
|
305
|
-
applyScene(next);
|
|
306
|
-
if (newIds.length) onSelectActorIds(newIds);
|
|
307
|
-
};
|
|
308
|
-
const onRemove = () => {
|
|
309
|
-
if (selectedActorIds.length === 0) return;
|
|
310
|
-
history.recordSnapshot();
|
|
311
|
-
applyScene(removeActors(sceneData, selectedActorIds));
|
|
312
|
-
onSelectActorIds([]);
|
|
313
|
-
};
|
|
314
|
-
const hasSelection = selectedActorIds.length > 0;
|
|
315
|
-
return (
|
|
316
|
-
<>
|
|
317
|
-
<IconButton icon="plus" label="Add actor" onClick={onAdd} disabled={isPlaying} />
|
|
318
|
-
<IconButton
|
|
319
|
-
icon="clone"
|
|
320
|
-
label="Duplicate"
|
|
321
|
-
onClick={onDuplicate}
|
|
322
|
-
disabled={!hasSelection || isPlaying}
|
|
323
|
-
/>
|
|
324
|
-
<IconButton
|
|
325
|
-
icon="trash"
|
|
326
|
-
label="Remove"
|
|
327
|
-
onClick={onRemove}
|
|
328
|
-
disabled={!hasSelection || isPlaying}
|
|
329
|
-
/>
|
|
330
|
-
</>
|
|
331
|
-
);
|
|
332
|
-
}
|
|
333
|
-
function makeSceneActions({ sceneData, commit, selectedActorIds, onSelectActorIds }) {
|
|
334
|
-
function commitScene(next) {
|
|
335
|
-
commit(formatJson(next));
|
|
336
|
-
}
|
|
337
|
-
return {
|
|
338
|
-
updateComponent: (actorId, behaviorName, nextProps) =>
|
|
339
|
-
commitScene(setActorComponent(sceneData, actorId, behaviorName, nextProps)),
|
|
340
|
-
addBehavior: (actorId, behaviorName) => {
|
|
341
|
-
const Behavior = findBehaviorClass(behaviorName);
|
|
342
|
-
if (!Behavior) return;
|
|
343
|
-
commitScene(
|
|
344
|
-
setActorComponent(sceneData, actorId, behaviorName, { ...Behavior.defaultProps })
|
|
345
|
-
);
|
|
346
|
-
},
|
|
347
|
-
removeBehavior: (actorId, behaviorName) =>
|
|
348
|
-
commitScene(removeActorComponent(sceneData, actorId, behaviorName)),
|
|
349
|
-
deleteSelection: () => {
|
|
350
|
-
if (selectedActorIds.length === 0) return;
|
|
351
|
-
commitScene(removeActors(sceneData, selectedActorIds));
|
|
352
|
-
onSelectActorIds([]);
|
|
353
|
-
},
|
|
354
|
-
duplicateSelection: () => {
|
|
355
|
-
if (selectedActorIds.length === 0) return;
|
|
356
|
-
const { sceneData: next, newIds } = duplicateActors(sceneData, selectedActorIds);
|
|
357
|
-
commitScene(next);
|
|
358
|
-
if (newIds.length) onSelectActorIds(newIds);
|
|
359
|
-
},
|
|
360
|
-
};
|
|
361
|
-
}
|
|
362
|
-
function setSceneSettings(sceneData, nextScene) {
|
|
363
|
-
return {
|
|
364
|
-
...sceneData,
|
|
365
|
-
...nextScene,
|
|
366
|
-
};
|
|
367
|
-
}
|
|
368
|
-
function setSceneEditorSettings(sceneData, nextEditor) {
|
|
369
|
-
return {
|
|
370
|
-
...sceneData,
|
|
371
|
-
editor: {
|
|
372
|
-
...(sceneData.editor ?? {}),
|
|
373
|
-
...nextEditor,
|
|
374
|
-
},
|
|
375
|
-
};
|
|
376
|
-
}
|
|
377
|
-
function useSelectionInspectorSheet(open) {
|
|
378
|
-
const [snap, setSnap] = useState('high');
|
|
379
|
-
// Inspector visibility is driven by selection / multi-mode. Snap state
|
|
380
|
-
// persists across selections so switching keeps the user's chosen snap.
|
|
381
|
-
const effectiveSnap = open ? snap : 'hidden';
|
|
382
|
-
return useMobileSheet({
|
|
383
|
-
snap: effectiveSnap,
|
|
384
|
-
baseClassName: styles.inspector,
|
|
385
|
-
onTransition: (direction) => {
|
|
386
|
-
if (!open) return;
|
|
387
|
-
if (direction === 'tap') setSnap((prev) => (prev === 'high' ? 'low' : 'high'));
|
|
388
|
-
else if (direction === 'down') setSnap('low');
|
|
389
|
-
else if (direction === 'up') setSnap('high');
|
|
390
|
-
},
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
|
-
function useSelectionGesture(args) {
|
|
394
|
-
const dragRef = useRef(null);
|
|
395
|
-
// Pin args in a ref so the timer callback reads the latest values without
|
|
396
|
-
// re-creating the handlers each render.
|
|
397
|
-
const argsRef = useRef(args);
|
|
398
|
-
argsRef.current = args;
|
|
399
|
-
const onPointerDown = useCallback((event) => {
|
|
400
|
-
const current = argsRef.current;
|
|
401
|
-
if (current.isPlaying || !current.canvasRef.current || !current.sceneData) return;
|
|
402
|
-
const raw = screenToCard(current.canvasRef.current, event.clientX, event.clientY);
|
|
403
|
-
const cam = current.editCameraRef.current;
|
|
404
|
-
const point = { x: raw.x + cam.x, y: raw.y + cam.y };
|
|
405
|
-
current.canvasRef.current.setPointerCapture(event.pointerId);
|
|
406
|
-
const scene = makeScene(current.sceneData, behaviorClasses, current.drawings);
|
|
407
|
-
const actor = scene.actorAt(point.x, point.y);
|
|
408
|
-
const drag = {
|
|
409
|
-
pointerId: event.pointerId,
|
|
410
|
-
startPoint: point,
|
|
411
|
-
lastPoint: point,
|
|
412
|
-
startedOnActorId: actor?.id ?? null,
|
|
413
|
-
modeAtStart: current.multiSelectMode,
|
|
414
|
-
movedFar: false,
|
|
415
|
-
longPressTimer: null,
|
|
416
|
-
longPressFired: false,
|
|
417
|
-
kind: 'idle',
|
|
418
|
-
pendingMarquee: false,
|
|
419
|
-
movingActorIds: [],
|
|
420
|
-
moveStarts: {},
|
|
421
|
-
recordedMoveSnapshot: false,
|
|
422
|
-
selectionBeforeMarquee: [...current.selectedActorIds],
|
|
423
|
-
};
|
|
424
|
-
if (event.shiftKey) {
|
|
425
|
-
handleShiftPointerDown(drag, actor, current);
|
|
426
|
-
} else if (current.multiSelectMode) {
|
|
427
|
-
handleModePointerDown(drag, actor, current);
|
|
428
|
-
} else {
|
|
429
|
-
handleDefaultPointerDown(drag, actor, point, current);
|
|
430
|
-
}
|
|
431
|
-
drag.moveStarts = collectMoveStarts(current.sceneData, drag.movingActorIds);
|
|
432
|
-
dragRef.current = drag;
|
|
433
|
-
}, []);
|
|
434
|
-
const onPointerMove = useCallback((event) => {
|
|
435
|
-
const drag = dragRef.current;
|
|
436
|
-
const current = argsRef.current;
|
|
437
|
-
if (!drag || drag.pointerId !== event.pointerId) return;
|
|
438
|
-
if (!current.canvasRef.current || !current.sceneData) return;
|
|
439
|
-
const raw = screenToCard(current.canvasRef.current, event.clientX, event.clientY);
|
|
440
|
-
const cam = current.editCameraRef.current;
|
|
441
|
-
const point = { x: raw.x + cam.x, y: raw.y + cam.y };
|
|
442
|
-
const dxTotal = point.x - drag.startPoint.x;
|
|
443
|
-
const dyTotal = point.y - drag.startPoint.y;
|
|
444
|
-
if (!drag.movedFar && Math.hypot(dxTotal, dyTotal) > DRAG_THRESHOLD) {
|
|
445
|
-
drag.movedFar = true;
|
|
446
|
-
if (drag.longPressTimer !== null) {
|
|
447
|
-
window.clearTimeout(drag.longPressTimer);
|
|
448
|
-
drag.longPressTimer = null;
|
|
449
|
-
}
|
|
450
|
-
if (drag.pendingMarquee && drag.kind === 'idle') drag.kind = 'marquee';
|
|
451
|
-
}
|
|
452
|
-
if (drag.kind === 'move' && drag.movedFar) {
|
|
453
|
-
const snap = getSnapSettings(current.sceneData);
|
|
454
|
-
const dx = snapDelta(dxTotal, snap.gridSize, snap.enabled);
|
|
455
|
-
const dy = snapDelta(dyTotal, snap.gridSize, snap.enabled);
|
|
456
|
-
drag.lastPoint = point;
|
|
457
|
-
const nextScene = moveActorsFromStarts(
|
|
458
|
-
current.sceneData,
|
|
459
|
-
drag.movingActorIds,
|
|
460
|
-
drag.moveStarts,
|
|
461
|
-
dx,
|
|
462
|
-
dy
|
|
463
|
-
);
|
|
464
|
-
if (nextScene === current.sceneData) return;
|
|
465
|
-
if (!drag.recordedMoveSnapshot) {
|
|
466
|
-
current.recordSceneSnapshot();
|
|
467
|
-
drag.recordedMoveSnapshot = true;
|
|
468
|
-
}
|
|
469
|
-
current.applyScene(nextScene);
|
|
470
|
-
} else if (drag.kind === 'marquee') {
|
|
471
|
-
drag.lastPoint = point;
|
|
472
|
-
current.marqueeRef.current = {
|
|
473
|
-
x: Math.min(drag.startPoint.x, point.x),
|
|
474
|
-
y: Math.min(drag.startPoint.y, point.y),
|
|
475
|
-
width: Math.abs(point.x - drag.startPoint.x),
|
|
476
|
-
height: Math.abs(point.y - drag.startPoint.y),
|
|
477
|
-
};
|
|
478
|
-
}
|
|
479
|
-
}, []);
|
|
480
|
-
const onPointerUp = useCallback((event) => {
|
|
481
|
-
const drag = dragRef.current;
|
|
482
|
-
const current = argsRef.current;
|
|
483
|
-
if (!drag || drag.pointerId !== event.pointerId) return;
|
|
484
|
-
if (drag.longPressTimer !== null) {
|
|
485
|
-
window.clearTimeout(drag.longPressTimer);
|
|
486
|
-
drag.longPressTimer = null;
|
|
487
|
-
}
|
|
488
|
-
if (drag.kind === 'marquee') {
|
|
489
|
-
finalizeMarquee(drag, current);
|
|
490
|
-
} else if (drag.kind === 'idle' && !drag.movedFar && !drag.longPressFired) {
|
|
491
|
-
handleTap(drag, current);
|
|
492
|
-
}
|
|
493
|
-
current.marqueeRef.current = null;
|
|
494
|
-
dragRef.current = null;
|
|
495
|
-
}, []);
|
|
496
|
-
return { onPointerDown, onPointerMove, onPointerUp };
|
|
497
|
-
}
|
|
498
|
-
function getSnapSettings(sceneData) {
|
|
499
|
-
const rawSize = sceneData.editor?.gridSize;
|
|
500
|
-
return {
|
|
501
|
-
enabled: sceneData.editor?.snapToGrid ?? true,
|
|
502
|
-
gridSize: Number.isFinite(rawSize) && rawSize && rawSize > 0 ? rawSize : DEFAULT_GRID_SIZE,
|
|
503
|
-
};
|
|
504
|
-
}
|
|
505
|
-
function snapDelta(delta, gridSize, enabled) {
|
|
506
|
-
if (!enabled) return delta;
|
|
507
|
-
return Math.round(delta / gridSize) * gridSize;
|
|
508
|
-
}
|
|
509
|
-
function collectMoveStarts(sceneData, actorIds) {
|
|
510
|
-
const starts = {};
|
|
511
|
-
const movingIds = new Set(actorIds);
|
|
512
|
-
for (const actor of sceneData.actors) {
|
|
513
|
-
if (!movingIds.has(actor.id)) continue;
|
|
514
|
-
const layout = actor.components.Layout;
|
|
515
|
-
if (!layout) continue;
|
|
516
|
-
starts[actor.id] = { x: layout.x, y: layout.y };
|
|
517
|
-
}
|
|
518
|
-
return starts;
|
|
519
|
-
}
|
|
520
|
-
function moveActorsFromStarts(sceneData, actorIds, starts, dx, dy) {
|
|
521
|
-
if (actorIds.length === 0) return sceneData;
|
|
522
|
-
const movingIds = new Set(actorIds);
|
|
523
|
-
const next = structuredClone(sceneData);
|
|
524
|
-
let changed = false;
|
|
525
|
-
for (const actor of next.actors) {
|
|
526
|
-
if (!movingIds.has(actor.id)) continue;
|
|
527
|
-
const start = starts[actor.id];
|
|
528
|
-
const layout = actor.components.Layout;
|
|
529
|
-
if (!start || !layout) continue;
|
|
530
|
-
const nextX = Math.round(start.x + dx);
|
|
531
|
-
const nextY = Math.round(start.y + dy);
|
|
532
|
-
if (layout.x === nextX && layout.y === nextY) continue;
|
|
533
|
-
actor.components.Layout = {
|
|
534
|
-
...layout,
|
|
535
|
-
x: nextX,
|
|
536
|
-
y: nextY,
|
|
537
|
-
};
|
|
538
|
-
changed = true;
|
|
539
|
-
}
|
|
540
|
-
return changed ? next : sceneData;
|
|
541
|
-
}
|
|
542
|
-
function usePlayPointerGesture({ canvasRef, runtimeRef }) {
|
|
543
|
-
const setPointer = useCallback(
|
|
544
|
-
(event, down) => {
|
|
545
|
-
const canvas = canvasRef.current;
|
|
546
|
-
const runtime = runtimeRef.current;
|
|
547
|
-
if (!canvas || !runtime) return;
|
|
548
|
-
runtime.setPointerFromScreen(canvas, event.clientX, event.clientY, down);
|
|
549
|
-
},
|
|
550
|
-
[canvasRef, runtimeRef]
|
|
551
|
-
);
|
|
552
|
-
const onPointerDown = useCallback(
|
|
553
|
-
(event) => {
|
|
554
|
-
event.currentTarget.setPointerCapture(event.pointerId);
|
|
555
|
-
setPointer(event, true);
|
|
556
|
-
},
|
|
557
|
-
[setPointer]
|
|
558
|
-
);
|
|
559
|
-
const onPointerMove = useCallback(
|
|
560
|
-
(event) => {
|
|
561
|
-
setPointer(event);
|
|
562
|
-
},
|
|
563
|
-
[setPointer]
|
|
564
|
-
);
|
|
565
|
-
const onPointerUp = useCallback(
|
|
566
|
-
(event) => {
|
|
567
|
-
setPointer(event, false);
|
|
568
|
-
try {
|
|
569
|
-
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
570
|
-
} catch {
|
|
571
|
-
// Pointer capture may already be gone after cancel/blur.
|
|
572
|
-
}
|
|
573
|
-
},
|
|
574
|
-
[setPointer]
|
|
575
|
-
);
|
|
576
|
-
return { onPointerDown, onPointerMove, onPointerUp };
|
|
577
|
-
}
|
|
578
|
-
function usePanGesture({ canvasRef, editCameraRef, enabled }) {
|
|
579
|
-
const dragRef = useRef(null);
|
|
580
|
-
const onPointerDown = useCallback(
|
|
581
|
-
(event) => {
|
|
582
|
-
if (!enabled) return;
|
|
583
|
-
const canvas = canvasRef.current;
|
|
584
|
-
if (!canvas) return;
|
|
585
|
-
const point = screenToCard(canvas, event.clientX, event.clientY);
|
|
586
|
-
event.currentTarget.setPointerCapture(event.pointerId);
|
|
587
|
-
dragRef.current = { id: event.pointerId, last: point };
|
|
588
|
-
},
|
|
589
|
-
[canvasRef, enabled]
|
|
590
|
-
);
|
|
591
|
-
const onPointerMove = useCallback(
|
|
592
|
-
(event) => {
|
|
593
|
-
const drag = dragRef.current;
|
|
594
|
-
if (!drag || drag.id !== event.pointerId) return;
|
|
595
|
-
const canvas = canvasRef.current;
|
|
596
|
-
if (!canvas) return;
|
|
597
|
-
const point = screenToCard(canvas, event.clientX, event.clientY);
|
|
598
|
-
const dx = point.x - drag.last.x;
|
|
599
|
-
const dy = point.y - drag.last.y;
|
|
600
|
-
drag.last = point;
|
|
601
|
-
editCameraRef.current = {
|
|
602
|
-
x: editCameraRef.current.x - dx,
|
|
603
|
-
y: editCameraRef.current.y - dy,
|
|
604
|
-
};
|
|
605
|
-
},
|
|
606
|
-
[canvasRef, editCameraRef]
|
|
607
|
-
);
|
|
608
|
-
const onPointerUp = useCallback((event) => {
|
|
609
|
-
const drag = dragRef.current;
|
|
610
|
-
if (!drag || drag.id !== event.pointerId) return;
|
|
611
|
-
try {
|
|
612
|
-
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
613
|
-
} catch {
|
|
614
|
-
// pointer capture may already be gone
|
|
615
|
-
}
|
|
616
|
-
dragRef.current = null;
|
|
617
|
-
}, []);
|
|
618
|
-
return { onPointerDown, onPointerMove, onPointerUp };
|
|
619
|
-
}
|
|
620
|
-
function handleShiftPointerDown(drag, actor, current) {
|
|
621
|
-
if (actor) {
|
|
622
|
-
const wasSelected = current.selectedActorIds.includes(actor.id);
|
|
623
|
-
const nextIds = wasSelected
|
|
624
|
-
? current.selectedActorIds.filter((id) => id !== actor.id)
|
|
625
|
-
: [...current.selectedActorIds, actor.id];
|
|
626
|
-
current.onSelectActorIds(nextIds);
|
|
627
|
-
if (!wasSelected) {
|
|
628
|
-
drag.kind = 'move';
|
|
629
|
-
drag.movingActorIds = nextIds;
|
|
630
|
-
}
|
|
631
|
-
} else {
|
|
632
|
-
drag.pendingMarquee = true;
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
function handleModePointerDown(drag, actor, current) {
|
|
636
|
-
if (actor) {
|
|
637
|
-
if (current.selectedActorIds.includes(actor.id)) {
|
|
638
|
-
drag.kind = 'move';
|
|
639
|
-
drag.movingActorIds = [...current.selectedActorIds];
|
|
640
|
-
}
|
|
641
|
-
// else: leave kind='idle' so pointerup triggers the tap-toggle path.
|
|
642
|
-
} else {
|
|
643
|
-
// Defer marquee until movement, so a stationary press/release reaches
|
|
644
|
-
// the tap handler (which exits multi mode on empty tap).
|
|
645
|
-
drag.pendingMarquee = true;
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
function handleDefaultPointerDown(drag, actor, point, current) {
|
|
649
|
-
if (actor) {
|
|
650
|
-
if (!current.selectedActorIds.includes(actor.id)) {
|
|
651
|
-
current.onSelectActorIds([actor.id]);
|
|
652
|
-
drag.movingActorIds = [actor.id];
|
|
653
|
-
} else {
|
|
654
|
-
drag.movingActorIds = [...current.selectedActorIds];
|
|
655
|
-
}
|
|
656
|
-
drag.kind = 'move';
|
|
657
|
-
drag.longPressTimer = window.setTimeout(() => {
|
|
658
|
-
if (drag.movedFar) return;
|
|
659
|
-
drag.longPressFired = true;
|
|
660
|
-
current.onSetMultiSelectMode(true);
|
|
661
|
-
}, LONG_PRESS_MS);
|
|
662
|
-
} else {
|
|
663
|
-
current.onSelectActorIds([]);
|
|
664
|
-
drag.longPressTimer = window.setTimeout(() => {
|
|
665
|
-
if (drag.movedFar) return;
|
|
666
|
-
drag.longPressFired = true;
|
|
667
|
-
current.onSetMultiSelectMode(true);
|
|
668
|
-
drag.kind = 'marquee';
|
|
669
|
-
current.marqueeRef.current = { x: point.x, y: point.y, width: 0, height: 0 };
|
|
670
|
-
}, LONG_PRESS_MS);
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
function finalizeMarquee(drag, current) {
|
|
674
|
-
const m = current.marqueeRef.current;
|
|
675
|
-
if (!m || !current.sceneData) return;
|
|
676
|
-
if (m.width === 0 && m.height === 0) return;
|
|
677
|
-
const scene = makeScene(current.sceneData, behaviorClasses, current.drawings);
|
|
678
|
-
const hits = scene.actorIdsInRect(m);
|
|
679
|
-
const merged = new Set(drag.selectionBeforeMarquee);
|
|
680
|
-
for (const id of hits) merged.add(id);
|
|
681
|
-
current.onSelectActorIds([...merged]);
|
|
682
|
-
}
|
|
683
|
-
function handleTap(drag, current) {
|
|
684
|
-
if (!drag.modeAtStart) return;
|
|
685
|
-
if (drag.startedOnActorId !== null) {
|
|
686
|
-
const id = drag.startedOnActorId;
|
|
687
|
-
const wasSelected = current.selectedActorIds.includes(id);
|
|
688
|
-
current.onSelectActorIds(
|
|
689
|
-
wasSelected
|
|
690
|
-
? current.selectedActorIds.filter((x) => x !== id)
|
|
691
|
-
: [...current.selectedActorIds, id]
|
|
692
|
-
);
|
|
693
|
-
} else {
|
|
694
|
-
current.onSetMultiSelectMode(false);
|
|
695
|
-
current.onSelectActorIds([]);
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
function useSelectionKeyboard(args) {
|
|
699
|
-
const ref = useRef(args);
|
|
700
|
-
ref.current = args;
|
|
701
|
-
useEffect(() => {
|
|
702
|
-
function onKeyDown(event) {
|
|
703
|
-
const current = ref.current;
|
|
704
|
-
if (current.isPlaying || isEditableTarget(event.target)) return;
|
|
705
|
-
if (event.key === 'Escape') {
|
|
706
|
-
if (current.selectedActorIds.length || current.multiSelectMode) {
|
|
707
|
-
event.preventDefault();
|
|
708
|
-
current.onSelectActorIds([]);
|
|
709
|
-
current.onSetMultiSelectMode(false);
|
|
710
|
-
}
|
|
711
|
-
return;
|
|
712
|
-
}
|
|
713
|
-
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'a') {
|
|
714
|
-
if (!current.sceneData) return;
|
|
715
|
-
event.preventDefault();
|
|
716
|
-
current.onSelectActorIds(current.sceneData.actors.map((actor) => actor.id));
|
|
717
|
-
return;
|
|
718
|
-
}
|
|
719
|
-
if (event.key === 'Delete' || event.key === 'Backspace') {
|
|
720
|
-
if (!current.sceneData || current.selectedActorIds.length === 0) return;
|
|
721
|
-
event.preventDefault();
|
|
722
|
-
current.commitScene(removeActors(current.sceneData, current.selectedActorIds));
|
|
723
|
-
current.onSelectActorIds([]);
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
window.addEventListener('keydown', onKeyDown);
|
|
727
|
-
return () => window.removeEventListener('keydown', onKeyDown);
|
|
728
|
-
}, []);
|
|
729
|
-
}
|
|
730
|
-
function useScenePlayLoop({
|
|
731
|
-
sceneData,
|
|
732
|
-
drawings,
|
|
733
|
-
isPlaying,
|
|
734
|
-
text,
|
|
735
|
-
canvasRef,
|
|
736
|
-
runtimeRef,
|
|
737
|
-
editCameraRef,
|
|
738
|
-
selectedActorIds,
|
|
739
|
-
marqueeRef,
|
|
740
|
-
}) {
|
|
741
|
-
// Spin up / tear down the play-mode runtime as the user toggles play. `text`
|
|
742
|
-
// is the stable identity for `sceneData` (which is re-parsed every render);
|
|
743
|
-
// depending on `sceneData` here would reset the runtime — and any
|
|
744
|
-
// behavior-mutated state like Layout.rotation — on every parent re-render.
|
|
745
|
-
useEffect(() => {
|
|
746
|
-
if (!isPlaying) {
|
|
747
|
-
runtimeRef.current = null;
|
|
748
|
-
return;
|
|
749
|
-
}
|
|
750
|
-
if (!sceneData) return;
|
|
751
|
-
runtimeRef.current = makeScene(sceneData, behaviorClasses, drawings).clone();
|
|
752
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
753
|
-
}, [drawings, isPlaying, text, runtimeRef]);
|
|
754
|
-
// Animation loop -- draws edit-mode previews and ticks the play-mode runtime.
|
|
755
|
-
useEffect(() => {
|
|
756
|
-
if (!sceneData || !canvasRef.current) return undefined;
|
|
757
|
-
const canvas = canvasRef.current;
|
|
758
|
-
const ctx = canvas.getContext('2d');
|
|
759
|
-
if (!ctx) return undefined;
|
|
760
|
-
configureSceneCanvas(canvas, ctx);
|
|
761
|
-
let raf = 0;
|
|
762
|
-
let last = performance.now();
|
|
763
|
-
const frame = (now) => {
|
|
764
|
-
const dt = Math.min(0.033, (now - last) / 1000);
|
|
765
|
-
last = now;
|
|
766
|
-
const scene = isPlaying
|
|
767
|
-
? runtimeRef.current
|
|
768
|
-
: makeScene(sceneData, behaviorClasses, drawings);
|
|
769
|
-
if (scene) {
|
|
770
|
-
if (isPlaying) scene.update(dt);
|
|
771
|
-
if (!isPlaying) scene.camera = { ...editCameraRef.current };
|
|
772
|
-
configureSceneCanvas(canvas, ctx);
|
|
773
|
-
scene.draw(ctx, {
|
|
774
|
-
selectedActorIds: isPlaying ? [] : selectedActorIds,
|
|
775
|
-
marquee: isPlaying ? null : marqueeRef.current,
|
|
776
|
-
showGrid: false,
|
|
777
|
-
showDebugColliders: false,
|
|
778
|
-
useCamera: true,
|
|
779
|
-
editPlaceholders: !isPlaying,
|
|
780
|
-
});
|
|
781
|
-
}
|
|
782
|
-
raf = requestAnimationFrame(frame);
|
|
783
|
-
};
|
|
784
|
-
raf = requestAnimationFrame(frame);
|
|
785
|
-
return () => cancelAnimationFrame(raf);
|
|
786
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps -- `text` proxies `sceneData` identity; resetting on every parse churns the loop and wipes runtime state.
|
|
787
|
-
}, [drawings, isPlaying, selectedActorIds, text, canvasRef, runtimeRef, marqueeRef]);
|
|
788
|
-
}
|
|
789
|
-
function useScenePlayKeys(runtimeRef, setIsPlaying) {
|
|
790
|
-
useEffect(() => {
|
|
791
|
-
const down = (event) => {
|
|
792
|
-
if (event.key === ' ' && !isEditableTarget(event.target)) {
|
|
793
|
-
event.preventDefault();
|
|
794
|
-
setIsPlaying((current) => !current);
|
|
795
|
-
return;
|
|
796
|
-
}
|
|
797
|
-
if (!runtimeRef.current) return;
|
|
798
|
-
runtimeRef.current.keys.add(event.key);
|
|
799
|
-
};
|
|
800
|
-
const up = (event) => {
|
|
801
|
-
if (!runtimeRef.current) return;
|
|
802
|
-
runtimeRef.current.keys.delete(event.key);
|
|
803
|
-
};
|
|
804
|
-
window.addEventListener('keydown', down);
|
|
805
|
-
window.addEventListener('keyup', up);
|
|
806
|
-
return () => {
|
|
807
|
-
window.removeEventListener('keydown', down);
|
|
808
|
-
window.removeEventListener('keyup', up);
|
|
809
|
-
};
|
|
810
|
-
}, [runtimeRef, setIsPlaying]);
|
|
811
|
-
}
|
|
812
|
-
function isEditableTarget(target) {
|
|
813
|
-
return (
|
|
814
|
-
target instanceof Element &&
|
|
815
|
-
!!target.closest('input, textarea, select, [contenteditable="true"]')
|
|
816
|
-
);
|
|
817
|
-
}
|
|
818
|
-
function MultiSelectInspector({ count, onDelete, onDuplicate, onDeselectAll }) {
|
|
819
|
-
return (
|
|
820
|
-
<div style={{ padding: '14px 16px', display: 'flex', flexDirection: 'column', gap: 12 }}>
|
|
821
|
-
<div style={{ fontSize: 13, opacity: 0.8 }}>
|
|
822
|
-
{count === 0
|
|
823
|
-
? 'Multi-select mode -- tap actors to add'
|
|
824
|
-
: `${count} actor${count === 1 ? '' : 's'} selected`}
|
|
825
|
-
</div>
|
|
826
|
-
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
|
827
|
-
<button
|
|
828
|
-
type="button"
|
|
829
|
-
onClick={onDuplicate}
|
|
830
|
-
disabled={count === 0}
|
|
831
|
-
style={inspectorActionStyle(count === 0)}>
|
|
832
|
-
<Icon name="clone" /> Duplicate
|
|
833
|
-
</button>
|
|
834
|
-
<button
|
|
835
|
-
type="button"
|
|
836
|
-
onClick={onDelete}
|
|
837
|
-
disabled={count === 0}
|
|
838
|
-
style={inspectorActionStyle(count === 0)}>
|
|
839
|
-
<Icon name="trash" /> Delete
|
|
840
|
-
</button>
|
|
841
|
-
<button type="button" onClick={onDeselectAll} style={inspectorActionStyle(false)}>
|
|
842
|
-
<Icon name="times" /> Deselect all
|
|
843
|
-
</button>
|
|
844
|
-
</div>
|
|
845
|
-
</div>
|
|
846
|
-
);
|
|
847
|
-
}
|
|
848
|
-
function inspectorActionStyle(disabled) {
|
|
849
|
-
return {
|
|
850
|
-
display: 'inline-flex',
|
|
851
|
-
alignItems: 'center',
|
|
852
|
-
gap: 6,
|
|
853
|
-
padding: '6px 10px',
|
|
854
|
-
background: 'transparent',
|
|
855
|
-
color: 'inherit',
|
|
856
|
-
border: '1px solid var(--castle-inspector-divider)',
|
|
857
|
-
borderRadius: 6,
|
|
858
|
-
cursor: disabled ? 'default' : 'pointer',
|
|
859
|
-
opacity: disabled ? 0.5 : 1,
|
|
860
|
-
fontSize: 13,
|
|
861
|
-
};
|
|
862
|
-
}
|
|
863
|
-
function SceneInspector({ sceneData, snapSettings, onChangeScene, onChangeEditor }) {
|
|
864
|
-
return (
|
|
865
|
-
<Panel title="Scene">
|
|
866
|
-
<TextField
|
|
867
|
-
label="Name"
|
|
868
|
-
value={sceneData.name ?? ''}
|
|
869
|
-
onChange={(name) => onChangeScene({ name })}
|
|
870
|
-
/>
|
|
871
|
-
<ColorField
|
|
872
|
-
label="Background"
|
|
873
|
-
value={sceneData.background ?? '#121213'}
|
|
874
|
-
onChange={(background) => onChangeScene({ background })}
|
|
875
|
-
/>
|
|
876
|
-
<CheckboxField
|
|
877
|
-
label="Snap to grid"
|
|
878
|
-
checked={snapSettings.enabled}
|
|
879
|
-
onChange={(snapToGrid) => onChangeEditor({ snapToGrid })}
|
|
880
|
-
/>
|
|
881
|
-
<NumberField
|
|
882
|
-
label="Grid size"
|
|
883
|
-
value={snapSettings.gridSize}
|
|
884
|
-
min={1}
|
|
885
|
-
step={1}
|
|
886
|
-
onChange={(gridSize) => onChangeEditor({ gridSize: Math.round(gridSize) })}
|
|
887
|
-
/>
|
|
888
|
-
</Panel>
|
|
889
|
-
);
|
|
890
|
-
}
|
|
891
|
-
function ActorInspector({ selectedActor, files, onSetComponent, onAddBehavior, onRemoveBehavior }) {
|
|
892
|
-
const presentNames = new Set(
|
|
893
|
-
Object.entries(selectedActor.components)
|
|
894
|
-
.filter(([, component]) => !!component)
|
|
895
|
-
.map(([behaviorName]) => behaviorName)
|
|
896
|
-
);
|
|
897
|
-
const availableBehaviors = behaviorClasses.filter(
|
|
898
|
-
(candidate) => !presentNames.has(candidate.behaviorName)
|
|
899
|
-
);
|
|
900
|
-
const behaviorEntries = Object.entries(selectedActor.components).filter((entry) => !!entry[1]);
|
|
901
|
-
return (
|
|
902
|
-
<>
|
|
903
|
-
<AddBehaviorPicker
|
|
904
|
-
available={availableBehaviors}
|
|
905
|
-
onAdd={(behaviorName) => onAddBehavior(selectedActor.id, behaviorName)}
|
|
906
|
-
/>
|
|
907
|
-
{behaviorEntries.map(([behaviorName, component], index) => {
|
|
908
|
-
const Behavior = behaviorClasses.find(
|
|
909
|
-
(candidate) => candidate.behaviorName === behaviorName
|
|
910
|
-
);
|
|
911
|
-
const Inspector = Behavior?.Inspector;
|
|
912
|
-
const body = Inspector ? (
|
|
913
|
-
<Inspector
|
|
914
|
-
actor={selectedActor}
|
|
915
|
-
component={component}
|
|
916
|
-
files={files}
|
|
917
|
-
setComponent={(nextProps) => onSetComponent(selectedActor.id, behaviorName, nextProps)}
|
|
918
|
-
/>
|
|
919
|
-
) : (
|
|
920
|
-
<AutoInspector
|
|
921
|
-
behaviorName={behaviorName}
|
|
922
|
-
defaultProps={Behavior?.defaultProps ?? component}
|
|
923
|
-
component={component}
|
|
924
|
-
setComponent={(nextProps) => onSetComponent(selectedActor.id, behaviorName, nextProps)}
|
|
925
|
-
/>
|
|
926
|
-
);
|
|
927
|
-
// Layout is core to every actor -- it cannot be removed.
|
|
928
|
-
const removable = behaviorName !== 'Layout';
|
|
929
|
-
const isLast = index === behaviorEntries.length - 1;
|
|
930
|
-
// Trash renders before the panel so the panel stays the wrapper's
|
|
931
|
-
// last child -- the panel's own `:last-child` border zeroes out, and
|
|
932
|
-
// this wrapper owns the divider between panels deterministically.
|
|
933
|
-
return (
|
|
934
|
-
<div
|
|
935
|
-
key={behaviorName}
|
|
936
|
-
style={{
|
|
937
|
-
position: 'relative',
|
|
938
|
-
borderBottom: isLast ? undefined : '1px solid var(--castle-inspector-divider)',
|
|
939
|
-
}}>
|
|
940
|
-
{removable ? (
|
|
941
|
-
<button
|
|
942
|
-
type="button"
|
|
943
|
-
aria-label={`Remove ${behaviorName}`}
|
|
944
|
-
title={`Remove ${behaviorName}`}
|
|
945
|
-
onClick={() => onRemoveBehavior(selectedActor.id, behaviorName)}
|
|
946
|
-
style={{
|
|
947
|
-
position: 'absolute',
|
|
948
|
-
top: 14,
|
|
949
|
-
right: 16,
|
|
950
|
-
border: 'none',
|
|
951
|
-
background: 'transparent',
|
|
952
|
-
padding: 0,
|
|
953
|
-
margin: 0,
|
|
954
|
-
cursor: 'pointer',
|
|
955
|
-
color: 'inherit',
|
|
956
|
-
fontSize: 16,
|
|
957
|
-
lineHeight: 1,
|
|
958
|
-
}}>
|
|
959
|
-
<Icon name="trash" />
|
|
960
|
-
</button>
|
|
961
|
-
) : null}
|
|
962
|
-
{body}
|
|
963
|
-
</div>
|
|
964
|
-
);
|
|
965
|
-
})}
|
|
966
|
-
</>
|
|
967
|
-
);
|
|
968
|
-
}
|
|
969
|
-
function AddBehaviorPicker({ available, onAdd }) {
|
|
970
|
-
if (!available.length) return null;
|
|
971
|
-
return (
|
|
972
|
-
<div
|
|
973
|
-
style={{
|
|
974
|
-
padding: '12px 16px',
|
|
975
|
-
borderBottom: '1px solid var(--castle-inspector-divider)',
|
|
976
|
-
}}>
|
|
977
|
-
<div style={{ position: 'relative' }}>
|
|
978
|
-
<select
|
|
979
|
-
className={styles.select}
|
|
980
|
-
value=""
|
|
981
|
-
onChange={(event) => {
|
|
982
|
-
if (event.target.value) onAdd(event.target.value);
|
|
983
|
-
}}
|
|
984
|
-
style={{
|
|
985
|
-
appearance: 'none',
|
|
986
|
-
WebkitAppearance: 'none',
|
|
987
|
-
MozAppearance: 'none',
|
|
988
|
-
paddingRight: 30,
|
|
989
|
-
}}>
|
|
990
|
-
<option value="">+ Add behavior</option>
|
|
991
|
-
{available.map((behavior) => (
|
|
992
|
-
<option key={behavior.behaviorName} value={behavior.behaviorName}>
|
|
993
|
-
{behavior.behaviorName}
|
|
994
|
-
</option>
|
|
995
|
-
))}
|
|
996
|
-
</select>
|
|
997
|
-
<span
|
|
998
|
-
style={{
|
|
999
|
-
position: 'absolute',
|
|
1000
|
-
right: 11,
|
|
1001
|
-
top: '50%',
|
|
1002
|
-
transform: 'translateY(-50%)',
|
|
1003
|
-
pointerEvents: 'none',
|
|
1004
|
-
fontSize: 12,
|
|
1005
|
-
opacity: 0.6,
|
|
1006
|
-
}}>
|
|
1007
|
-
<Icon name="chevron-down" />
|
|
1008
|
-
</span>
|
|
1009
|
-
</div>
|
|
1010
|
-
</div>
|
|
1011
|
-
);
|
|
1012
|
-
}
|