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.
Files changed (157) 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 +40 -23
  16. package/kits/physics-2d/behaviors/Joints.jsx +4 -3
  17. package/kits/physics-2d/behaviors/SoundPlayer.jsx +266 -0
  18. package/kits/physics-2d/behaviors/Sprite.jsx +3 -3
  19. package/kits/physics-2d/behaviors/Video.jsx +4 -3
  20. package/kits/physics-2d/castle.json +1 -1
  21. package/kits/physics-2d/editors/PlayOnly.jsx +15 -6
  22. package/kits/physics-2d/editors/PxArtEditor.jsx +95 -31
  23. package/kits/physics-2d/editors/SceneEditor.jsx +5 -5
  24. package/kits/physics-2d/editors/SingleEditor.jsx +37 -8
  25. package/kits/physics-2d/editors/StyleEditor.jsx +7 -4
  26. package/kits/physics-2d/editors/pixelEditorChrome.jsx +7 -7
  27. package/kits/physics-2d/editors/pixelInspector.jsx +13 -7
  28. package/kits/physics-2d/editors/pxArtTimeline.jsx +29 -11
  29. package/kits/physics-2d/editors/pxArtTimeline.module.css +11 -0
  30. package/kits/physics-2d/engine/audioContext.js +41 -0
  31. package/kits/physics-2d/engine/autoInspector.jsx +50 -33
  32. package/kits/physics-2d/engine/blueprint.js +15 -3
  33. package/kits/physics-2d/engine/liveReload.js +33 -21
  34. package/kits/physics-2d/engine/media.js +29 -39
  35. package/kits/physics-2d/engine/sound.js +316 -0
  36. package/kits/physics-2d/engine/systemRegistry.js +1 -1
  37. package/kits/physics-2d/engine/ui.jsx +274 -5
  38. package/kits/physics-2d/engine/ui.module.css +137 -16
  39. package/kits/physics-2d/scripts/testsounds.mjs +84 -0
  40. package/kits/physics-2d/systems/media.js +31 -15
  41. package/kits/physics-3d/CLAUDE.md +109 -0
  42. package/kits/physics-3d/behaviors/Body.jsx +51 -0
  43. package/kits/physics-3d/behaviors/Door.jsx +31 -0
  44. package/kits/physics-3d/behaviors/Lookable.jsx +11 -0
  45. package/kits/physics-3d/behaviors/Model.jsx +14 -0
  46. package/kits/physics-3d/behaviors/Pickup.jsx +39 -0
  47. package/kits/physics-3d/behaviors/Player.jsx +172 -0
  48. package/kits/physics-3d/behaviors/Portal.jsx +19 -0
  49. package/kits/physics-3d/behaviors/Shape.jsx +75 -0
  50. package/kits/physics-3d/behaviors/Solid.jsx +11 -0
  51. package/kits/physics-3d/behaviors/Transform.jsx +24 -0
  52. package/kits/physics-3d/blueprints/barrel.scene +15 -0
  53. package/kits/physics-3d/blueprints/crate.scene +29 -0
  54. package/kits/physics-3d/blueprints/door.scene +35 -0
  55. package/kits/physics-3d/blueprints/gem.scene +26 -0
  56. package/kits/physics-3d/blueprints/pillar.scene +25 -0
  57. package/kits/physics-3d/blueprints/platform.scene +23 -0
  58. package/kits/physics-3d/blueprints/player.scene +29 -0
  59. package/kits/physics-3d/blueprints/portal.scene +21 -0
  60. package/kits/physics-3d/blueprints/rock.scene +14 -0
  61. package/kits/physics-3d/blueprints/statue.scene +26 -0
  62. package/kits/physics-3d/blueprints/tree.scene +14 -0
  63. package/kits/physics-3d/blueprints/wall.scene +26 -0
  64. package/kits/{basic-2d → physics-3d}/castle.json +23 -5
  65. package/kits/physics-3d/docs/pxmodel-format.md +111 -0
  66. package/kits/physics-3d/drawings/crate.pxart +26 -0
  67. package/kits/physics-3d/drawings/door-edge.pxart +26 -0
  68. package/kits/physics-3d/drawings/door.pxart +27 -0
  69. package/kits/physics-3d/drawings/face.pxart +26 -0
  70. package/kits/physics-3d/drawings/floor.pxart +27 -0
  71. package/kits/physics-3d/drawings/platform.pxart +27 -0
  72. package/kits/physics-3d/drawings/statue.pxart +27 -0
  73. package/kits/physics-3d/drawings/wall.pxart +27 -0
  74. package/kits/physics-3d/engine3d/PlayOnly3D.jsx +32 -0
  75. package/kits/physics-3d/engine3d/PxModelEditor.jsx +619 -0
  76. package/kits/physics-3d/engine3d/Scene3DEditor.jsx +618 -0
  77. package/kits/physics-3d/engine3d/Scene3DPlayer.jsx +309 -0
  78. package/kits/physics-3d/engine3d/editor3dData.js +114 -0
  79. package/kits/physics-3d/engine3d/editor3dInspector.jsx +404 -0
  80. package/kits/physics-3d/engine3d/editorChrome.jsx +218 -0
  81. package/kits/physics-3d/engine3d/editorWorld.js +249 -0
  82. package/kits/physics-3d/engine3d/materials.js +174 -0
  83. package/kits/physics-3d/engine3d/meshops.js +123 -0
  84. package/kits/physics-3d/engine3d/modelEditorWorld.js +439 -0
  85. package/kits/physics-3d/engine3d/modelFiles.js +139 -0
  86. package/kits/physics-3d/engine3d/pxmodel.js +265 -0
  87. package/kits/physics-3d/engine3d/thumbnails.js +137 -0
  88. package/kits/physics-3d/engine3d/world3d.js +216 -0
  89. package/kits/{basic-2d → physics-3d}/eslint.config.js +10 -25
  90. package/kits/physics-3d/index.html +20 -0
  91. package/kits/physics-3d/main.jsx +30 -0
  92. package/kits/physics-3d/models/barrel.pxmodel +10 -0
  93. package/kits/physics-3d/models/player.pxmodel +222 -0
  94. package/kits/physics-3d/models/rock.pxmodel +30 -0
  95. package/kits/physics-3d/models/tree.pxmodel +10 -0
  96. package/kits/{basic-2d → physics-3d}/package-lock.json +140 -119
  97. package/kits/{basic-2d → physics-3d}/package.json +11 -9
  98. package/kits/{basic-2d → physics-3d}/pnpm-lock.yaml +162 -137
  99. package/kits/physics-3d/scenes/main.scene +414 -0
  100. package/kits/physics-3d/scenes/model-lab.scene +127 -0
  101. package/kits/physics-3d/systems/physics3d.js +356 -0
  102. package/package.json +5 -2
  103. package/dist/shell/assets/index-DKu9ejyh.js +0 -436
  104. package/kits/basic-2d/CLAUDE.md +0 -221
  105. package/kits/basic-2d/behaviors/Camera.jsx +0 -43
  106. package/kits/basic-2d/behaviors/Collider.jsx +0 -213
  107. package/kits/basic-2d/behaviors/Layout.jsx +0 -53
  108. package/kits/basic-2d/behaviors/Sprite.jsx +0 -357
  109. package/kits/basic-2d/behaviors/tint.js +0 -47
  110. package/kits/basic-2d/blueprints/cauldron.scene +0 -20
  111. package/kits/basic-2d/docs/pxart-format.md +0 -377
  112. package/kits/basic-2d/drawings/cauldron.pxart +0 -113
  113. package/kits/basic-2d/editors/BlueprintLibrary.jsx +0 -270
  114. package/kits/basic-2d/editors/ErrorBoundary.jsx +0 -59
  115. package/kits/basic-2d/editors/PlayOnly.jsx +0 -31
  116. package/kits/basic-2d/editors/PxArtEditor.jsx +0 -1092
  117. package/kits/basic-2d/editors/SceneEditor.jsx +0 -1780
  118. package/kits/basic-2d/editors/SelectionOverlay.jsx +0 -909
  119. package/kits/basic-2d/editors/SingleEditor.jsx +0 -122
  120. package/kits/basic-2d/editors/behaviorRegistry.js +0 -34
  121. package/kits/basic-2d/editors/editorHistory.js +0 -157
  122. package/kits/basic-2d/editors/inspectorSheet.js +0 -13
  123. package/kits/basic-2d/editors/pixelCanvas.js +0 -11
  124. package/kits/basic-2d/editors/pixelEditorChrome.jsx +0 -74
  125. package/kits/basic-2d/editors/pixelGeometry.js +0 -140
  126. package/kits/basic-2d/editors/pixelInspector.jsx +0 -633
  127. package/kits/basic-2d/editors/pxArtEditorModel.js +0 -732
  128. package/kits/basic-2d/editors/pxArtPlayback.js +0 -92
  129. package/kits/basic-2d/editors/pxArtTimeline.jsx +0 -752
  130. package/kits/basic-2d/editors/pxArtTimeline.module.css +0 -506
  131. package/kits/basic-2d/editors/pxArtTools.js +0 -232
  132. package/kits/basic-2d/editors/useArtboardFit.js +0 -105
  133. package/kits/basic-2d/engine/ScenePlayer.jsx +0 -209
  134. package/kits/basic-2d/engine/SceneUI.jsx +0 -59
  135. package/kits/basic-2d/engine/assets.js +0 -15
  136. package/kits/basic-2d/engine/autoInspector.jsx +0 -70
  137. package/kits/basic-2d/engine/behaviorExtensions.js +0 -32
  138. package/kits/basic-2d/engine/blueprint.js +0 -557
  139. package/kits/basic-2d/engine/collider.js +0 -200
  140. package/kits/basic-2d/engine/files.js +0 -141
  141. package/kits/basic-2d/engine/liveReload.js +0 -88
  142. package/kits/basic-2d/engine/pxart.js +0 -1032
  143. package/kits/basic-2d/engine/pxartSmooth.js +0 -222
  144. package/kits/basic-2d/engine/scene.js +0 -696
  145. package/kits/basic-2d/engine/spriteGeometry.js +0 -32
  146. package/kits/basic-2d/engine/systemRegistry.js +0 -16
  147. package/kits/basic-2d/engine/ui.jsx +0 -695
  148. package/kits/basic-2d/engine/ui.module.css +0 -2287
  149. package/kits/basic-2d/index.html +0 -24
  150. package/kits/basic-2d/main.jsx +0 -24
  151. package/kits/basic-2d/scenes/main.scene +0 -16
  152. package/kits/basic-2d/scripts/draw.mjs +0 -121
  153. package/kits/physics-2d/behaviors/Sound.jsx +0 -163
  154. package/kits/physics-2d/behaviors/Tone.jsx +0 -95
  155. package/kits/physics-2d/engine/tone.js +0 -112
  156. /package/kits/{basic-2d → physics-3d}/.prettierrc +0 -0
  157. /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).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);
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
+ }