castle-web-cli 0.4.129 → 0.4.131

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.
Files changed (144) hide show
  1. package/dist/agent-prompts.js +4 -4
  2. package/dist/filesChanged.d.ts +2 -0
  3. package/dist/filesChanged.js +6 -2
  4. package/dist/ide.js +15 -4
  5. package/dist/importBrowse.js +1 -0
  6. package/dist/index.js +10 -4
  7. package/dist/init.js +63 -15
  8. package/dist/native/loop.js +1 -1
  9. package/dist/native/tools.js +1 -1
  10. package/dist/serve.d.ts +1 -0
  11. package/dist/serve.js +20 -1
  12. package/dist/shell/assets/index-BpOLUyVO.js +441 -0
  13. package/dist/shell/assets/{index-CAq6f9B5.css → index-D6K-0YDB.css} +1 -1
  14. package/dist/shell/index.html +2 -2
  15. package/kits/physics-2d/CLAUDE.md +9 -10
  16. package/kits/physics-2d/castle.json +1 -1
  17. package/kits/physics-2d/editors/PlayOnly.jsx +16 -6
  18. package/kits/physics-2d/editors/PxArtEditor.jsx +95 -31
  19. package/kits/physics-2d/editors/SceneEditor.jsx +10 -10
  20. package/kits/physics-2d/editors/SingleEditor.jsx +37 -8
  21. package/kits/physics-2d/editors/StyleEditor.jsx +7 -4
  22. package/kits/physics-2d/editors/pixelEditorChrome.jsx +7 -7
  23. package/kits/physics-2d/editors/pixelInspector.jsx +13 -7
  24. package/kits/physics-2d/editors/pxArtTimeline.jsx +29 -11
  25. package/kits/physics-2d/editors/pxArtTimeline.module.css +11 -0
  26. package/kits/physics-2d/engine/ScenePlayer.jsx +2 -2
  27. package/kits/physics-2d/engine/blueprint.js +23 -5
  28. package/kits/physics-2d/engine/liveReload.js +33 -21
  29. package/kits/physics-2d/engine/scene.js +17 -5
  30. package/kits/physics-2d/engine/systemRegistry.js +1 -1
  31. package/kits/physics-2d/engine/ui.jsx +2 -2
  32. package/kits/physics-2d/engine/ui.module.css +42 -0
  33. package/kits/physics-3d/CLAUDE.md +108 -0
  34. package/kits/physics-3d/behaviors/Body.jsx +51 -0
  35. package/kits/physics-3d/behaviors/Door.jsx +31 -0
  36. package/kits/physics-3d/behaviors/Lookable.jsx +11 -0
  37. package/kits/physics-3d/behaviors/Model.jsx +14 -0
  38. package/kits/physics-3d/behaviors/Pickup.jsx +39 -0
  39. package/kits/physics-3d/behaviors/Player.jsx +172 -0
  40. package/kits/physics-3d/behaviors/Shape.jsx +75 -0
  41. package/kits/physics-3d/behaviors/Solid.jsx +11 -0
  42. package/kits/physics-3d/behaviors/Transform.jsx +24 -0
  43. package/kits/physics-3d/blueprints/barrel.scene +15 -0
  44. package/kits/physics-3d/blueprints/crate.scene +29 -0
  45. package/kits/physics-3d/blueprints/door.scene +35 -0
  46. package/kits/physics-3d/blueprints/gem.scene +26 -0
  47. package/kits/physics-3d/blueprints/pillar.scene +25 -0
  48. package/kits/physics-3d/blueprints/platform.scene +23 -0
  49. package/kits/physics-3d/blueprints/player.scene +29 -0
  50. package/kits/physics-3d/blueprints/rock.scene +14 -0
  51. package/kits/physics-3d/blueprints/statue.scene +26 -0
  52. package/kits/physics-3d/blueprints/tree.scene +14 -0
  53. package/kits/physics-3d/blueprints/wall.scene +26 -0
  54. package/kits/{basic-2d → physics-3d}/castle.json +23 -5
  55. package/kits/physics-3d/docs/pxmodel-format.md +111 -0
  56. package/kits/physics-3d/drawings/crate.pxart +26 -0
  57. package/kits/physics-3d/drawings/door-edge.pxart +26 -0
  58. package/kits/physics-3d/drawings/door.pxart +27 -0
  59. package/kits/physics-3d/drawings/face.pxart +26 -0
  60. package/kits/physics-3d/drawings/floor.pxart +27 -0
  61. package/kits/physics-3d/drawings/platform.pxart +27 -0
  62. package/kits/physics-3d/drawings/statue.pxart +27 -0
  63. package/kits/physics-3d/drawings/wall.pxart +27 -0
  64. package/kits/physics-3d/engine3d/PlayOnly3D.jsx +36 -0
  65. package/kits/physics-3d/engine3d/PxModelEditor.jsx +619 -0
  66. package/kits/physics-3d/engine3d/Scene3DEditor.jsx +618 -0
  67. package/kits/physics-3d/engine3d/Scene3DPlayer.jsx +309 -0
  68. package/kits/physics-3d/engine3d/editor3dData.js +114 -0
  69. package/kits/physics-3d/engine3d/editor3dInspector.jsx +404 -0
  70. package/kits/physics-3d/engine3d/editorChrome.jsx +218 -0
  71. package/kits/physics-3d/engine3d/editorWorld.js +249 -0
  72. package/kits/physics-3d/engine3d/materials.js +174 -0
  73. package/kits/physics-3d/engine3d/meshops.js +123 -0
  74. package/kits/physics-3d/engine3d/modelEditorWorld.js +439 -0
  75. package/kits/physics-3d/engine3d/modelFiles.js +136 -0
  76. package/kits/physics-3d/engine3d/pxmodel.js +265 -0
  77. package/kits/physics-3d/engine3d/thumbnails.js +137 -0
  78. package/kits/physics-3d/engine3d/world3d.js +216 -0
  79. package/kits/{basic-2d → physics-3d}/eslint.config.js +10 -25
  80. package/kits/physics-3d/index.html +20 -0
  81. package/kits/physics-3d/main.jsx +30 -0
  82. package/kits/physics-3d/models/barrel.pxmodel +10 -0
  83. package/kits/physics-3d/models/player.pxmodel +222 -0
  84. package/kits/physics-3d/models/rock.pxmodel +30 -0
  85. package/kits/physics-3d/models/tree.pxmodel +10 -0
  86. package/kits/{basic-2d → physics-3d}/package-lock.json +140 -119
  87. package/kits/{basic-2d → physics-3d}/package.json +11 -9
  88. package/kits/{basic-2d → physics-3d}/pnpm-lock.yaml +162 -137
  89. package/kits/physics-3d/scenes/main.scene +398 -0
  90. package/kits/physics-3d/scenes/model-lab.scene +111 -0
  91. package/kits/physics-3d/systems/physics3d.js +356 -0
  92. package/package.json +5 -2
  93. package/dist/shell/assets/index-DKu9ejyh.js +0 -436
  94. package/kits/basic-2d/CLAUDE.md +0 -221
  95. package/kits/basic-2d/behaviors/Camera.jsx +0 -43
  96. package/kits/basic-2d/behaviors/Collider.jsx +0 -213
  97. package/kits/basic-2d/behaviors/Layout.jsx +0 -53
  98. package/kits/basic-2d/behaviors/Sprite.jsx +0 -357
  99. package/kits/basic-2d/behaviors/tint.js +0 -47
  100. package/kits/basic-2d/blueprints/cauldron.scene +0 -20
  101. package/kits/basic-2d/docs/pxart-format.md +0 -377
  102. package/kits/basic-2d/drawings/cauldron.pxart +0 -113
  103. package/kits/basic-2d/editors/BlueprintLibrary.jsx +0 -270
  104. package/kits/basic-2d/editors/ErrorBoundary.jsx +0 -59
  105. package/kits/basic-2d/editors/PlayOnly.jsx +0 -31
  106. package/kits/basic-2d/editors/PxArtEditor.jsx +0 -1092
  107. package/kits/basic-2d/editors/SceneEditor.jsx +0 -1780
  108. package/kits/basic-2d/editors/SelectionOverlay.jsx +0 -909
  109. package/kits/basic-2d/editors/SingleEditor.jsx +0 -122
  110. package/kits/basic-2d/editors/behaviorRegistry.js +0 -34
  111. package/kits/basic-2d/editors/editorHistory.js +0 -157
  112. package/kits/basic-2d/editors/inspectorSheet.js +0 -13
  113. package/kits/basic-2d/editors/pixelCanvas.js +0 -11
  114. package/kits/basic-2d/editors/pixelEditorChrome.jsx +0 -74
  115. package/kits/basic-2d/editors/pixelGeometry.js +0 -140
  116. package/kits/basic-2d/editors/pixelInspector.jsx +0 -633
  117. package/kits/basic-2d/editors/pxArtEditorModel.js +0 -732
  118. package/kits/basic-2d/editors/pxArtPlayback.js +0 -92
  119. package/kits/basic-2d/editors/pxArtTimeline.jsx +0 -752
  120. package/kits/basic-2d/editors/pxArtTimeline.module.css +0 -506
  121. package/kits/basic-2d/editors/pxArtTools.js +0 -232
  122. package/kits/basic-2d/editors/useArtboardFit.js +0 -105
  123. package/kits/basic-2d/engine/ScenePlayer.jsx +0 -209
  124. package/kits/basic-2d/engine/SceneUI.jsx +0 -59
  125. package/kits/basic-2d/engine/assets.js +0 -15
  126. package/kits/basic-2d/engine/autoInspector.jsx +0 -70
  127. package/kits/basic-2d/engine/behaviorExtensions.js +0 -32
  128. package/kits/basic-2d/engine/blueprint.js +0 -557
  129. package/kits/basic-2d/engine/collider.js +0 -200
  130. package/kits/basic-2d/engine/files.js +0 -141
  131. package/kits/basic-2d/engine/liveReload.js +0 -88
  132. package/kits/basic-2d/engine/pxart.js +0 -1032
  133. package/kits/basic-2d/engine/pxartSmooth.js +0 -222
  134. package/kits/basic-2d/engine/scene.js +0 -696
  135. package/kits/basic-2d/engine/spriteGeometry.js +0 -32
  136. package/kits/basic-2d/engine/systemRegistry.js +0 -16
  137. package/kits/basic-2d/engine/ui.jsx +0 -695
  138. package/kits/basic-2d/engine/ui.module.css +0 -2287
  139. package/kits/basic-2d/index.html +0 -24
  140. package/kits/basic-2d/main.jsx +0 -24
  141. package/kits/basic-2d/scenes/main.scene +0 -16
  142. package/kits/basic-2d/scripts/draw.mjs +0 -121
  143. /package/kits/{basic-2d → physics-3d}/.prettierrc +0 -0
  144. /package/kits/{basic-2d → physics-3d}/vite.config.js +0 -0
@@ -0,0 +1,618 @@
1
+ // The 3d scene editor: the 2d kit's SceneEditor structure ported to a 3d
2
+ // stage. Same chrome and flows -- the BlueprintLibrary hotbar (tap to inspect,
3
+ // drag onto the stage to place), the floating selection toolbar, the docked
4
+ // behavior-panel inspector, text-level undo/redo -- with the 2d canvas
5
+ // replaced by the orbit + gizmo editor world. The FILE is the source of truth;
6
+ // every edit lands as sparse JSON through the shared history + debounced save.
7
+ import React, { useEffect, useMemo, useRef, useState } from 'react';
8
+ import { onSaveReloadState, takeReloadState } from 'castle-web-sdk';
9
+ import { parseJsonFile, formatJson, basename } from '@imports/castle.physics-2d/engine/files';
10
+ import { useLiveDeckFiles3D, useStableSprites } from './modelFiles';
11
+ import {
12
+ behaviorClasses,
13
+ findBehaviorClass,
14
+ } from '@imports/castle.physics-2d/editors/behaviorRegistry';
15
+ import { useEditHistory } from '@imports/castle.physics-2d/editors/editorHistory';
16
+ import { BlueprintLibrary } from '@imports/castle.physics-2d/editors/BlueprintLibrary';
17
+ import {
18
+ EditorBody,
19
+ MainEditor,
20
+ SheetGrabHandle,
21
+ cx,
22
+ styles,
23
+ useMobileSheet,
24
+ } from '@imports/castle.physics-2d/engine/ui';
25
+ import {
26
+ cascadeDeleteBlueprint,
27
+ getBlueprintTemplate,
28
+ isBlueprintPath,
29
+ setBlueprintName,
30
+ setBlueprintTemplateComponents,
31
+ setInstanceComponent,
32
+ } from '@imports/castle.physics-2d/engine/blueprint';
33
+ import {
34
+ makeScene,
35
+ removeActorComponent,
36
+ removeActors,
37
+ setActorComponent,
38
+ } from '@imports/castle.physics-2d/engine/scene';
39
+ import {
40
+ createEditorWorld,
41
+ setEditorScene,
42
+ disposeEditorWorld,
43
+ raycastGroundPoint,
44
+ } from './editorWorld';
45
+ import {
46
+ TEMPLATE_ACTOR_ID,
47
+ withTemplateId,
48
+ stripTemplateId,
49
+ placeBlueprintInstance3D,
50
+ duplicateActors3D,
51
+ addActorWithBlueprint3D,
52
+ transformPatch,
53
+ } from './editor3dData';
54
+ import {
55
+ ActorInspector3D,
56
+ BlueprintInspector3D,
57
+ MultiSelectInspector3D,
58
+ SceneInspector3D,
59
+ initialBehaviorProps,
60
+ } from './editor3dInspector';
61
+ import { THUMB_PREFIX, thumbArtFor, withThumbRef } from './thumbnails';
62
+ import { StageCanvas, StageControls, useEditorShortcuts, useFileSaver3D } from './editorChrome';
63
+
64
+ export function Scene3DEditor({ path }) {
65
+ const stashKey = `scene3d-editor:${path}`;
66
+ const [stash] = useState(() => takeReloadState(stashKey));
67
+ const saver = useFileSaver3D();
68
+ const { files, setFiles, dataVersion } = useLiveDeckFiles3D({
69
+ shouldSkipPath: saver.hasPending,
70
+ });
71
+ const text = files[path] ?? '';
72
+ const onChangeText = (next) => {
73
+ setFiles((current) => ({ ...current, [path]: next }));
74
+ saver.schedule(path, next);
75
+ };
76
+ const onChangeFile = (filePath, nextText) => {
77
+ setFiles((current) => ({ ...current, [filePath]: nextText }));
78
+ saver.schedule(filePath, nextText);
79
+ };
80
+ const history = useEditHistory(text, onChangeText, path);
81
+ const isBlueprintFile = isBlueprintPath(path);
82
+
83
+ const [selectedActorIds, setSelectedActorIdsRaw] = useState(stash?.selectedActorIds ?? []);
84
+ const [selectedBlueprintPath, setSelectedBlueprintPath] = useState(
85
+ stash?.selectedBlueprintPath ?? null
86
+ );
87
+ const [mode, setMode] = useState('move');
88
+ const onSelectActorIds = (ids) => {
89
+ setSelectedBlueprintPath(null);
90
+ setSelectedActorIdsRaw(ids);
91
+ };
92
+ const onSelectBlueprint = (blueprintPath) => {
93
+ setSelectedActorIdsRaw([]);
94
+ setSelectedBlueprintPath(blueprintPath);
95
+ };
96
+ useEffect(
97
+ () => onSaveReloadState(stashKey, () => ({ selectedActorIds, selectedBlueprintPath })),
98
+ [stashKey, selectedActorIds, selectedBlueprintPath]
99
+ );
100
+ // A blueprint file edits its single template actor; keep it selected.
101
+ useEffect(() => {
102
+ if (isBlueprintFile && selectedActorIds.length === 0) {
103
+ setSelectedActorIdsRaw([TEMPLATE_ACTOR_ID]);
104
+ }
105
+ // eslint-disable-next-line react-hooks/exhaustive-deps
106
+ }, [isBlueprintFile]);
107
+
108
+ const { value: parsedRaw, error } = parseJsonFile(path, text);
109
+ // A freshly-created (empty) scene file edits as an empty scene; the first
110
+ // commit writes it out for real.
111
+ const parsed = parsedRaw ?? (text.trim() === '' ? { name: 'Scene', actors: [] } : null);
112
+ const sceneData = withTemplateId(parsed, isBlueprintFile);
113
+ const sprites = useStableSprites(files);
114
+ const previewSceneData = sceneData
115
+ ? makeScene(sceneData, behaviorClasses, sprites, files, path).data
116
+ : null;
117
+ const serialize = (next) => formatJson(isBlueprintFile ? stripTemplateId(next) : next);
118
+ const commitScene = (next, options) => history.commit(serialize(next), options);
119
+
120
+ const canvasRef = useRef(null);
121
+ const edRef = useRef(null);
122
+ const callbacks = {
123
+ onPick: (hits, additive) => {
124
+ if (additive) {
125
+ const firstNew = hits.find((id) => !selectedActorIds.includes(id));
126
+ if (firstNew) onSelectActorIds([...selectedActorIds, firstNew]);
127
+ return;
128
+ }
129
+ if (hits.length === 0) return onSelectActorIds([]);
130
+ const current = hits.findIndex((id) => selectedActorIds.includes(id));
131
+ const next = current === -1 ? 0 : (current + 1) % hits.length;
132
+ onSelectActorIds([hits[next]]);
133
+ },
134
+ onCommitTransforms: (changes) => {
135
+ if (!sceneData) return;
136
+ const next = structuredClone(sceneData);
137
+ for (const change of changes) {
138
+ const actor = next.actors?.find((candidate) => candidate.id === change.id);
139
+ if (!actor) continue;
140
+ actor.components = {
141
+ ...(actor.components ?? {}),
142
+ Transform: transformPatch(actor.components?.Transform, change),
143
+ };
144
+ }
145
+ commitScene(next);
146
+ },
147
+ onAddAt: () => {},
148
+ };
149
+
150
+ useEffect(() => {
151
+ const canvas = canvasRef.current;
152
+ if (!canvas) return undefined;
153
+ const ed = createEditorWorld(canvas, callbacks);
154
+ edRef.current = ed;
155
+ return () => {
156
+ disposeEditorWorld(ed);
157
+ edRef.current = null;
158
+ };
159
+ // eslint-disable-next-line react-hooks/exhaustive-deps
160
+ }, []);
161
+ useEffect(() => {
162
+ const ed = edRef.current;
163
+ if (ed) ed.callbacks = callbacks;
164
+ });
165
+ useEffect(() => {
166
+ const ed = edRef.current;
167
+ if (!ed || !sceneData || ed.gizmoDragging) return;
168
+ if (ed.lastAppliedText === text && ed.lastDataVersion === dataVersion) return;
169
+ ed.lastAppliedText = text;
170
+ ed.lastDataVersion = dataVersion;
171
+ setEditorScene(ed, sceneData, behaviorClasses, sprites, files, path);
172
+ // eslint-disable-next-line react-hooks/exhaustive-deps
173
+ }, [text, dataVersion]);
174
+ useEffect(() => {
175
+ if (edRef.current) edRef.current.mode = mode;
176
+ }, [mode]);
177
+ useEffect(() => {
178
+ if (edRef.current) edRef.current.selection = selectedActorIds;
179
+ }, [selectedActorIds]);
180
+
181
+ const actions = makeActions({
182
+ files,
183
+ sceneData,
184
+ previewSceneData,
185
+ sprites,
186
+ commitScene,
187
+ selectedActorIds,
188
+ onSelectActorIds,
189
+ isBlueprintFile,
190
+ });
191
+ const dragPlace = useDragPlace({
192
+ edRef,
193
+ sceneData,
194
+ files,
195
+ serialize,
196
+ history,
197
+ onChangeText,
198
+ onSelectActorIds,
199
+ });
200
+ useEditorShortcuts({
201
+ history,
202
+ setMode,
203
+ onDuplicate: actions.duplicateSelection,
204
+ onDelete: actions.deleteSelection,
205
+ onEscape: () => onSelectActorIds([]),
206
+ });
207
+
208
+ // What the imported hotbar sees: the 2d kit's own blueprints hidden (they
209
+ // mean nothing to a 3d scene), and each remaining blueprint's text rewritten
210
+ // so its `Sprite.file` points at a virtual `@thumb/` ref backed by a live 3d
211
+ // corner-view render of the template (see engine3d/thumbnails.js).
212
+ const hotbarInjectRef = useRef(new Map());
213
+ const { hotbarFiles, hotbarSprites } = useMemo(() => {
214
+ const outFiles = {};
215
+ const outSprites = { ...sprites };
216
+ for (const [filePath, fileText] of Object.entries(files)) {
217
+ if (filePath.startsWith('imports/') && filePath.includes('/blueprints/')) continue;
218
+ if (/^blueprints\/[^/]+\.scene$/.test(filePath)) {
219
+ let cached = hotbarInjectRef.current.get(filePath);
220
+ if (!cached || cached.text !== fileText) {
221
+ let injected = fileText;
222
+ try {
223
+ const json = JSON.parse(fileText);
224
+ if (json.actors?.[0]) {
225
+ json.actors[0].components = {
226
+ ...(json.actors[0].components ?? {}),
227
+ Sprite: { file: `${THUMB_PREFIX}${filePath}` },
228
+ };
229
+ injected = JSON.stringify(json);
230
+ }
231
+ } catch {
232
+ // unparsable blueprint: hand it through untouched
233
+ }
234
+ cached = { text: fileText, injected };
235
+ hotbarInjectRef.current.set(filePath, cached);
236
+ }
237
+ outFiles[filePath] = cached.injected;
238
+ const template = getBlueprintTemplate(files, filePath);
239
+ if (template) {
240
+ outSprites[`${THUMB_PREFIX}${filePath}`] = thumbArtFor(
241
+ filePath,
242
+ template,
243
+ sprites,
244
+ files
245
+ );
246
+ }
247
+ } else {
248
+ outFiles[filePath] = fileText;
249
+ }
250
+ }
251
+ return { hotbarFiles: outFiles, hotbarSprites: outSprites };
252
+ // eslint-disable-next-line react-hooks/exhaustive-deps
253
+ }, [files, sprites]);
254
+
255
+ if (!sceneData) {
256
+ return (
257
+ <div className={styles.panelBare}>
258
+ <MainEditor>
259
+ <EditorBody>
260
+ <div className={styles.inspector}>{error}</div>
261
+ </EditorBody>
262
+ </MainEditor>
263
+ </div>
264
+ );
265
+ }
266
+
267
+ const rawSelectedActor =
268
+ selectedActorIds.length === 1
269
+ ? sceneData.actors.find((actor) => actor.id === selectedActorIds[0])
270
+ : undefined;
271
+ const selectedActor =
272
+ selectedActorIds.length === 1
273
+ ? previewSceneData.actors.find((actor) => actor.id === selectedActorIds[0])
274
+ : undefined;
275
+ const selectedBlueprintTemplate = rawSelectedActor?.blueprint
276
+ ? getBlueprintTemplate(files, rawSelectedActor.blueprint)
277
+ : undefined;
278
+ const selectedBlueprintName = rawSelectedActor?.blueprint
279
+ ? (selectedBlueprintTemplate?.name ?? basename(rawSelectedActor.blueprint))
280
+ : undefined;
281
+ const hotbarBlueprint =
282
+ !isBlueprintFile && selectedBlueprintPath
283
+ ? getBlueprintTemplate(files, selectedBlueprintPath)
284
+ : null;
285
+ const showMulti = !isBlueprintFile && selectedActorIds.length > 1;
286
+
287
+ const blueprintActions = makeBlueprintActions({
288
+ files,
289
+ selectedBlueprintPath,
290
+ hotbarBlueprint,
291
+ onChangeFile,
292
+ sprites,
293
+ });
294
+ const onAddActor = () => {
295
+ const { sceneData: next, newId, blueprintFile } = addActorWithBlueprint3D(sceneData, files);
296
+ onChangeFile(blueprintFile.path, blueprintFile.text);
297
+ commitScene(next);
298
+ onSelectActorIds([newId]);
299
+ };
300
+ const onDeleteBlueprint = (blueprintPath) => {
301
+ for (const write of cascadeDeleteBlueprint(files, blueprintPath)) {
302
+ if (write.path === path) history.commit(write.text);
303
+ else onChangeFile(write.path, write.text);
304
+ }
305
+ onSelectActorIds([]);
306
+ };
307
+
308
+ return (
309
+ <div className={styles.panelBare}>
310
+ <MainEditor>
311
+ <EditorBody>
312
+ <div className={styles.sceneWorkspace}>
313
+ <div className={styles.sceneTools}>
314
+ {isBlueprintFile ? null : (
315
+ <BlueprintLibrary
316
+ files={hotbarFiles}
317
+ sprites={hotbarSprites}
318
+ canvasRef={canvasRef}
319
+ editCameraRef={{ current: { x: 0, y: 0 } }}
320
+ isPlaying={false}
321
+ selectedBlueprintPath={selectedBlueprintPath}
322
+ instanceBlueprintPath={rawSelectedActor?.blueprint ?? null}
323
+ onSelectBlueprint={onSelectBlueprint}
324
+ onAddActor={onAddActor}
325
+ dragPlace={dragPlace}
326
+ onDeleteBlueprint={onDeleteBlueprint}
327
+ />
328
+ )}
329
+ </div>
330
+ <div className={styles.stageWrap} style={{ padding: 0, position: 'relative' }}>
331
+ <StageCanvas ref={canvasRef} />
332
+ <StageControls
333
+ history={history}
334
+ mode={mode}
335
+ setMode={setMode}
336
+ onDuplicate={isBlueprintFile ? undefined : actions.duplicateSelection}
337
+ onDelete={isBlueprintFile ? undefined : actions.deleteSelection}
338
+ actionsDisabled={selectedActorIds.length === 0}
339
+ />
340
+ </div>
341
+ <InspectorPane
342
+ hotbarBlueprint={
343
+ hotbarBlueprint ? withThumbRef(hotbarBlueprint, selectedBlueprintPath) : null
344
+ }
345
+ selectedBlueprintPath={selectedBlueprintPath}
346
+ blueprintActions={blueprintActions}
347
+ showMulti={showMulti}
348
+ selectedActorIds={selectedActorIds}
349
+ onSelectActorIds={onSelectActorIds}
350
+ selectedActor={selectedActor}
351
+ rawSelectedActor={rawSelectedActor}
352
+ selectedBlueprintName={selectedBlueprintName}
353
+ selectedBlueprintTemplate={
354
+ selectedBlueprintTemplate && rawSelectedActor?.blueprint
355
+ ? withThumbRef(selectedBlueprintTemplate, rawSelectedActor.blueprint)
356
+ : selectedBlueprintTemplate
357
+ }
358
+ isBlueprintFile={isBlueprintFile}
359
+ sprites={hotbarSprites}
360
+ files={files}
361
+ actions={actions}
362
+ sceneData={sceneData}
363
+ commitScene={commitScene}
364
+ onSelectBlueprint={onSelectBlueprint}
365
+ />
366
+ </div>
367
+ </EditorBody>
368
+ </MainEditor>
369
+ </div>
370
+ );
371
+ }
372
+
373
+ function makeActions({
374
+ files,
375
+ sceneData,
376
+ previewSceneData,
377
+ sprites,
378
+ commitScene,
379
+ selectedActorIds,
380
+ onSelectActorIds,
381
+ isBlueprintFile,
382
+ }) {
383
+ return {
384
+ updateComponent: (actorId, behaviorName, nextProps) =>
385
+ commitScene(
386
+ setInstanceComponent(files, behaviorClasses, sceneData, actorId, behaviorName, nextProps),
387
+ { coalesceKey: `${actorId}:${behaviorName}:${Object.keys(nextProps).sort().join(',')}` }
388
+ ),
389
+ addBehavior: (actorId, behaviorName) => {
390
+ const Behavior = findBehaviorClass(behaviorName);
391
+ if (!Behavior) return;
392
+ const resolved = previewSceneData?.actors.find((actor) => actor.id === actorId);
393
+ commitScene(
394
+ setActorComponent(
395
+ sceneData,
396
+ actorId,
397
+ behaviorName,
398
+ initialBehaviorProps(Behavior, resolved, sprites)
399
+ )
400
+ );
401
+ },
402
+ removeBehavior: (actorId, behaviorName) =>
403
+ commitScene(removeActorComponent(sceneData, actorId, behaviorName)),
404
+ deleteSelection: () => {
405
+ if (isBlueprintFile || selectedActorIds.length === 0) return;
406
+ commitScene(removeActors(sceneData, selectedActorIds));
407
+ onSelectActorIds([]);
408
+ },
409
+ duplicateSelection: () => {
410
+ if (isBlueprintFile || selectedActorIds.length === 0) return;
411
+ const { sceneData: next, newIds } = duplicateActors3D(
412
+ sceneData,
413
+ selectedActorIds,
414
+ previewSceneData?.actors
415
+ );
416
+ commitScene(next);
417
+ if (newIds.length) onSelectActorIds(newIds);
418
+ },
419
+ };
420
+ }
421
+
422
+ // Template edits from the hotbar sidebar write the BLUEPRINT file, not this
423
+ // scene -- optimistic files fold + debounced save, same as the 2d editor.
424
+ function makeBlueprintActions({
425
+ files,
426
+ selectedBlueprintPath,
427
+ hotbarBlueprint,
428
+ onChangeFile,
429
+ sprites,
430
+ }) {
431
+ const updateTemplate = (mutate) => {
432
+ if (!selectedBlueprintPath || !hotbarBlueprint) return;
433
+ const components = structuredClone(hotbarBlueprint.components);
434
+ mutate(components);
435
+ const nextText = setBlueprintTemplateComponents(files, selectedBlueprintPath, components);
436
+ if (nextText != null) onChangeFile(selectedBlueprintPath, nextText);
437
+ };
438
+ return {
439
+ setComponent: (behaviorName, nextProps) =>
440
+ updateTemplate((components) => {
441
+ components[behaviorName] = { ...(components[behaviorName] ?? {}), ...nextProps };
442
+ }),
443
+ addBehavior: (behaviorName) => {
444
+ const Behavior = findBehaviorClass(behaviorName);
445
+ if (!Behavior) return;
446
+ updateTemplate((components) => {
447
+ components[behaviorName] = initialBehaviorProps(Behavior, { components }, sprites);
448
+ });
449
+ },
450
+ removeBehavior: (behaviorName) =>
451
+ updateTemplate((components) => {
452
+ delete components[behaviorName];
453
+ }),
454
+ rename: (name) => {
455
+ if (!selectedBlueprintPath) return;
456
+ const nextText = setBlueprintName(files, selectedBlueprintPath, name);
457
+ if (nextText != null) onChangeFile(selectedBlueprintPath, nextText);
458
+ },
459
+ };
460
+ }
461
+
462
+ // Drag-out from the hotbar places a REAL instance the moment the pointer
463
+ // enters the stage (the imported BlueprintLibrary drives enter/move/leave/
464
+ // drop; its 2d stage coordinates are ignored -- the drop point comes from a
465
+ // ground-plane raycast under the live pointer).
466
+ function useDragPlace({
467
+ edRef,
468
+ sceneData,
469
+ files,
470
+ serialize,
471
+ history,
472
+ onChangeText,
473
+ onSelectActorIds,
474
+ }) {
475
+ const pointerRef = useRef({ x: 0, y: 0 });
476
+ useEffect(() => {
477
+ const onPointerMove = (event) => {
478
+ pointerRef.current = { x: event.clientX, y: event.clientY };
479
+ };
480
+ window.addEventListener('pointermove', onPointerMove, { passive: true });
481
+ return () => window.removeEventListener('pointermove', onPointerMove);
482
+ }, []);
483
+ const dragRef = useRef(null);
484
+ const groundPoint = () =>
485
+ edRef.current
486
+ ? raycastGroundPoint(edRef.current, pointerRef.current.x, pointerRef.current.y)
487
+ : null;
488
+ return {
489
+ enter: (blueprintPath) => {
490
+ const point = groundPoint();
491
+ if (!point || !sceneData) return;
492
+ history.recordSnapshot();
493
+ const { sceneData: next, newId } = placeBlueprintInstance3D(
494
+ sceneData,
495
+ files,
496
+ blueprintPath,
497
+ point
498
+ );
499
+ dragRef.current = { actorId: newId, sceneData: next };
500
+ onChangeText(serialize(next));
501
+ onSelectActorIds([newId]);
502
+ },
503
+ move: () => {
504
+ const drag = dragRef.current;
505
+ const point = drag ? groundPoint() : null;
506
+ if (!drag || !point) return;
507
+ const next = structuredClone(drag.sceneData);
508
+ const actor = next.actors.find((candidate) => candidate.id === drag.actorId);
509
+ if (!actor) return;
510
+ actor.components.Transform = {
511
+ ...actor.components.Transform,
512
+ x: Math.round(point.x * 10) / 10,
513
+ z: Math.round(point.z * 10) / 10,
514
+ };
515
+ drag.sceneData = next;
516
+ onChangeText(serialize(next));
517
+ },
518
+ leave: () => {
519
+ const drag = dragRef.current;
520
+ if (!drag) return;
521
+ dragRef.current = null;
522
+ onChangeText(serialize(removeActors(drag.sceneData, [drag.actorId])));
523
+ onSelectActorIds([]);
524
+ },
525
+ drop: () => {
526
+ const drag = dragRef.current;
527
+ if (!drag) return;
528
+ dragRef.current = null;
529
+ onSelectActorIds([drag.actorId]);
530
+ },
531
+ };
532
+ }
533
+
534
+ function InspectorPane(props) {
535
+ const inspectorSheet = useMobileSheet({
536
+ open: true,
537
+ baseClassName: styles.inspector,
538
+ storageKey: 'scene3d-inspector',
539
+ });
540
+ const {
541
+ hotbarBlueprint,
542
+ selectedBlueprintPath,
543
+ blueprintActions,
544
+ showMulti,
545
+ selectedActorIds,
546
+ onSelectActorIds,
547
+ selectedActor,
548
+ rawSelectedActor,
549
+ selectedBlueprintName,
550
+ selectedBlueprintTemplate,
551
+ isBlueprintFile,
552
+ sprites,
553
+ files,
554
+ actions,
555
+ sceneData,
556
+ commitScene,
557
+ onSelectBlueprint,
558
+ } = props;
559
+ let body;
560
+ if (hotbarBlueprint) {
561
+ body = (
562
+ <BlueprintInspector3D
563
+ name={hotbarBlueprint.name}
564
+ template={hotbarBlueprint}
565
+ sprites={sprites}
566
+ blueprintPath={selectedBlueprintPath}
567
+ files={files}
568
+ actions={blueprintActions}
569
+ />
570
+ );
571
+ } else if (showMulti) {
572
+ body = (
573
+ <MultiSelectInspector3D
574
+ count={selectedActorIds.length}
575
+ onDeselectAll={() => onSelectActorIds([])}
576
+ />
577
+ );
578
+ } else if (selectedActor) {
579
+ body = (
580
+ <ActorInspector3D
581
+ key={selectedActor.id}
582
+ selectedActor={selectedActor}
583
+ rawActor={rawSelectedActor}
584
+ blueprintName={selectedBlueprintName}
585
+ blueprintTemplate={selectedBlueprintTemplate}
586
+ sprites={sprites}
587
+ files={files}
588
+ showAddBehavior={isBlueprintFile}
589
+ onSetComponent={actions.updateComponent}
590
+ onAddBehavior={actions.addBehavior}
591
+ onRemoveBehavior={actions.removeBehavior}
592
+ onEditBlueprint={
593
+ isBlueprintFile || !rawSelectedActor?.blueprint
594
+ ? undefined
595
+ : () => onSelectBlueprint(rawSelectedActor.blueprint)
596
+ }
597
+ />
598
+ );
599
+ } else {
600
+ body = (
601
+ <SceneInspector3D
602
+ sceneData={sceneData}
603
+ onChangeScene={(patch) => commitScene({ ...sceneData, ...patch })}
604
+ onChangeGround={(patch) =>
605
+ commitScene({ ...sceneData, ground: { ...(sceneData.ground ?? {}), ...patch } })
606
+ }
607
+ />
608
+ );
609
+ }
610
+ return (
611
+ <aside {...inspectorSheet.rootProps}>
612
+ <div {...inspectorSheet.grabProps}>
613
+ <SheetGrabHandle />
614
+ </div>
615
+ <div className={cx(styles.sheetBody, styles.inspectorBody)}>{body}</div>
616
+ </aside>
617
+ );
618
+ }