castle-web-cli 0.4.129 → 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 (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 +15 -6
  18. package/kits/physics-2d/editors/PxArtEditor.jsx +95 -31
  19. package/kits/physics-2d/editors/SceneEditor.jsx +5 -5
  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/blueprint.js +15 -3
  27. package/kits/physics-2d/engine/liveReload.js +33 -21
  28. package/kits/physics-2d/engine/systemRegistry.js +1 -1
  29. package/kits/physics-2d/engine/ui.jsx +2 -2
  30. package/kits/physics-2d/engine/ui.module.css +42 -0
  31. package/kits/physics-3d/CLAUDE.md +109 -0
  32. package/kits/physics-3d/behaviors/Body.jsx +51 -0
  33. package/kits/physics-3d/behaviors/Door.jsx +31 -0
  34. package/kits/physics-3d/behaviors/Lookable.jsx +11 -0
  35. package/kits/physics-3d/behaviors/Model.jsx +14 -0
  36. package/kits/physics-3d/behaviors/Pickup.jsx +39 -0
  37. package/kits/physics-3d/behaviors/Player.jsx +172 -0
  38. package/kits/physics-3d/behaviors/Portal.jsx +19 -0
  39. package/kits/physics-3d/behaviors/Shape.jsx +75 -0
  40. package/kits/physics-3d/behaviors/Solid.jsx +11 -0
  41. package/kits/physics-3d/behaviors/Transform.jsx +24 -0
  42. package/kits/physics-3d/blueprints/barrel.scene +15 -0
  43. package/kits/physics-3d/blueprints/crate.scene +29 -0
  44. package/kits/physics-3d/blueprints/door.scene +35 -0
  45. package/kits/physics-3d/blueprints/gem.scene +26 -0
  46. package/kits/physics-3d/blueprints/pillar.scene +25 -0
  47. package/kits/physics-3d/blueprints/platform.scene +23 -0
  48. package/kits/physics-3d/blueprints/player.scene +29 -0
  49. package/kits/physics-3d/blueprints/portal.scene +21 -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 +32 -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 +139 -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 +414 -0
  90. package/kits/physics-3d/scenes/model-lab.scene +127 -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,404 @@
1
+ // Inspector components for the 3d scene editor -- a direct port of the 2d
2
+ // SceneEditor's (unexported) inspector internals, running against the shared
3
+ // behavior registry, ui primitives, and blueprint helpers from the imported
4
+ // physics-2d kit. Behavior classes may carry a custom `static Inspector`;
5
+ // everything else renders through AutoInspector.
6
+ import React, { useState } from 'react';
7
+ import {
8
+ CheckboxField,
9
+ ColorField,
10
+ Icon,
11
+ NumberField,
12
+ Panel,
13
+ TextField,
14
+ styles,
15
+ } from '@imports/castle.physics-2d/engine/ui';
16
+ import { AutoInspector } from '@imports/castle.physics-2d/engine/autoInspector';
17
+ import {
18
+ behaviorClasses,
19
+ findBehaviorClass,
20
+ } from '@imports/castle.physics-2d/editors/behaviorRegistry';
21
+ import {
22
+ countBlueprintInstances,
23
+ isInheritedProp,
24
+ } from '@imports/castle.physics-2d/engine/blueprint';
25
+ import { BlueprintThumbnail } from '@imports/castle.physics-2d/editors/BlueprintLibrary';
26
+
27
+ // Only 3d-vocabulary behaviors belong in the add-behavior picker and panels --
28
+ // the imported 2d kit's behaviors are also in the registry (Layout, Sprite,
29
+ // Collider, ...), but attaching them to 3d actors does nothing.
30
+ const HIDDEN_BEHAVIORS = new Set([
31
+ 'Layout',
32
+ 'Sprite',
33
+ 'Collider',
34
+ 'Camera',
35
+ 'RigidBody',
36
+ 'Joints',
37
+ 'Draggable',
38
+ 'Slingshot',
39
+ 'AnalogStick',
40
+ 'Goal',
41
+ 'Sound',
42
+ 'Tone',
43
+ 'Video',
44
+ ]);
45
+
46
+ export function editorBehaviors() {
47
+ return behaviorClasses.filter((Behavior) => !HIDDEN_BEHAVIORS.has(Behavior.behaviorName));
48
+ }
49
+
50
+ export function initialBehaviorProps(Behavior, actor, sprites) {
51
+ if (Behavior.initialProps && actor) return Behavior.initialProps(actor, { sprites });
52
+ return { ...Behavior.defaultProps };
53
+ }
54
+
55
+ function makeOverrideContext({ Behavior, rawComponent, templateProps, setComponent }) {
56
+ const baselineOf = (prop) =>
57
+ templateProps && prop in templateProps ? templateProps[prop] : Behavior?.defaultProps?.[prop];
58
+ return {
59
+ isOverridden: (prop) =>
60
+ isInheritedProp(Behavior, prop) && !!rawComponent && prop in rawComponent,
61
+ anyOverridden: () =>
62
+ !!rawComponent && Object.keys(rawComponent).some((prop) => isInheritedProp(Behavior, prop)),
63
+ baseline: baselineOf,
64
+ reset: (prop) => setComponent({ [prop]: baselineOf(prop) }),
65
+ };
66
+ }
67
+
68
+ export function SceneInspector3D({ sceneData, onChangeScene, onChangeGround }) {
69
+ const ground = sceneData.ground ?? {};
70
+ return (
71
+ <>
72
+ <Panel title="Scene">
73
+ <TextField
74
+ label="Name"
75
+ value={sceneData.name ?? ''}
76
+ onChange={(name) => onChangeScene({ name })}
77
+ />
78
+ </Panel>
79
+ <Panel title="Ground">
80
+ <TextField
81
+ label="Art"
82
+ value={ground.art ?? ''}
83
+ onChange={(art) => onChangeGround({ art })}
84
+ />
85
+ <NumberField
86
+ label="Tile Size"
87
+ value={ground.tile ?? 2}
88
+ min={0.5}
89
+ step={0.5}
90
+ onChange={(tile) => onChangeGround({ tile })}
91
+ />
92
+ <ColorField
93
+ label="Color"
94
+ value={ground.color ?? '#22222b'}
95
+ onChange={(color) => onChangeGround({ color })}
96
+ />
97
+ </Panel>
98
+ </>
99
+ );
100
+ }
101
+
102
+ export function MultiSelectInspector3D({ count, onDeselectAll }) {
103
+ return (
104
+ <div style={{ padding: '14px 16px', display: 'flex', flexDirection: 'column', gap: 12 }}>
105
+ <div style={{ fontSize: 13, opacity: 0.8 }}>
106
+ {count} actor{count === 1 ? '' : 's'} selected
107
+ </div>
108
+ <div style={{ display: 'flex', gap: 8 }}>
109
+ <button
110
+ type="button"
111
+ onClick={onDeselectAll}
112
+ style={{
113
+ display: 'inline-flex',
114
+ alignItems: 'center',
115
+ gap: 6,
116
+ padding: '6px 10px',
117
+ background: 'transparent',
118
+ color: 'inherit',
119
+ border: '1px solid var(--castle-inspector-divider)',
120
+ borderRadius: 6,
121
+ cursor: 'pointer',
122
+ fontSize: 13,
123
+ }}>
124
+ <Icon name="times" /> Deselect all
125
+ </button>
126
+ </div>
127
+ </div>
128
+ );
129
+ }
130
+
131
+ // Sidebar inspector for a hotbar-selected blueprint: badge + name header, then
132
+ // the template's components through ActorInspector3D as a synthetic actor.
133
+ export function BlueprintInspector3D({ name, template, sprites, blueprintPath, files, actions }) {
134
+ const templateActor = { id: '__blueprint__', components: template.components };
135
+ const instanceCount = countBlueprintInstances(files, blueprintPath);
136
+ return (
137
+ <>
138
+ <div className={styles.instanceHeader}>
139
+ <div className={styles.instanceHeaderName}>
140
+ <BlueprintThumbnail blueprint={template} sprites={sprites} />
141
+ <div className={styles.instanceHeaderNameCol}>
142
+ <input
143
+ className={styles.blueprintNameInput}
144
+ value={name}
145
+ onChange={(event) => actions.rename(event.target.value)}
146
+ aria-label="Blueprint name"
147
+ spellCheck={false}
148
+ />
149
+ <span className={styles.instanceHeaderId}>
150
+ {instanceCount} {instanceCount === 1 ? 'actor' : 'actors'}
151
+ </span>
152
+ </div>
153
+ </div>
154
+ </div>
155
+ <ActorInspector3D
156
+ selectedActor={templateActor}
157
+ rawActor={templateActor}
158
+ sprites={sprites}
159
+ files={files}
160
+ onSetComponent={(actorId, behaviorName, nextProps) =>
161
+ actions.setComponent(behaviorName, nextProps)
162
+ }
163
+ onAddBehavior={(actorId, behaviorName) => actions.addBehavior(behaviorName)}
164
+ onRemoveBehavior={(actorId, behaviorName) => actions.removeBehavior(behaviorName)}
165
+ />
166
+ </>
167
+ );
168
+ }
169
+
170
+ export function ActorInspector3D({
171
+ selectedActor,
172
+ rawActor,
173
+ blueprintName,
174
+ blueprintTemplate,
175
+ sprites,
176
+ files,
177
+ showAddBehavior = true,
178
+ onSetComponent,
179
+ onAddBehavior,
180
+ onRemoveBehavior,
181
+ onEditBlueprint,
182
+ }) {
183
+ const isInstance = Boolean(rawActor?.blueprint);
184
+ const presentNames = new Set(
185
+ Object.entries(selectedActor.components)
186
+ .filter(([, component]) => !!component)
187
+ .map(([behaviorName]) => behaviorName)
188
+ );
189
+ const availableBehaviors = editorBehaviors().filter(
190
+ (candidate) => !presentNames.has(candidate.behaviorName)
191
+ );
192
+ // 2d-vocabulary components are hidden from the panels too -- a 3d blueprint
193
+ // may carry a `Sprite` purely as its hotbar thumbnail icon.
194
+ const PANEL_ORDER = ['Transform', 'Model', 'Shape', 'Body', 'Solid'];
195
+ const panelRank = (name) => {
196
+ const index = PANEL_ORDER.indexOf(name);
197
+ return index === -1 ? PANEL_ORDER.length : index;
198
+ };
199
+ const behaviorEntries = Object.entries(selectedActor.components)
200
+ .filter(([behaviorName, component]) => !!component && !HIDDEN_BEHAVIORS.has(behaviorName))
201
+ .sort(([a], [b]) => panelRank(a) - panelRank(b) || a.localeCompare(b));
202
+ // For an instance, behaviors it doesn't already override are collapsed
203
+ // behind a reveal button; Transform and overridden behaviors stay visible.
204
+ const [showAllBehaviors, setShowAllBehaviors] = useState(false);
205
+ const isPinnedBehavior = (behaviorName) =>
206
+ behaviorName === 'Transform' || Boolean(rawActor?.components?.[behaviorName]);
207
+ const collapsed = isInstance && !showAllBehaviors;
208
+ const visibleEntries = collapsed
209
+ ? behaviorEntries.filter(([behaviorName]) => isPinnedBehavior(behaviorName))
210
+ : behaviorEntries;
211
+ const showReveal = collapsed && visibleEntries.length < behaviorEntries.length;
212
+ return (
213
+ <>
214
+ {isInstance ? (
215
+ <InstanceHeader
216
+ selectedActor={selectedActor}
217
+ blueprintName={blueprintName}
218
+ blueprintTemplate={blueprintTemplate}
219
+ sprites={sprites}
220
+ onEditBlueprint={onEditBlueprint}
221
+ />
222
+ ) : null}
223
+ {showAddBehavior ? (
224
+ <AddBehaviorPicker
225
+ available={availableBehaviors}
226
+ onAdd={(behaviorName) => onAddBehavior(selectedActor.id, behaviorName)}
227
+ />
228
+ ) : null}
229
+ {visibleEntries.map(([behaviorName, component], index) => (
230
+ <BehaviorPanel
231
+ key={behaviorName}
232
+ behaviorName={behaviorName}
233
+ component={component}
234
+ selectedActor={selectedActor}
235
+ rawActor={rawActor}
236
+ blueprintTemplate={blueprintTemplate}
237
+ isInstance={isInstance}
238
+ sprites={sprites}
239
+ files={files}
240
+ isLast={index === visibleEntries.length - 1 && !showReveal}
241
+ onSetComponent={onSetComponent}
242
+ onRemoveBehavior={onRemoveBehavior}
243
+ />
244
+ ))}
245
+ {showReveal ? (
246
+ <button
247
+ type="button"
248
+ className={styles.revealOverridesButton}
249
+ onClick={() => setShowAllBehaviors(true)}>
250
+ <Icon name="chevron-down" />
251
+ <span>Override behavior properties</span>
252
+ </button>
253
+ ) : null}
254
+ </>
255
+ );
256
+ }
257
+
258
+ function InstanceHeader({
259
+ selectedActor,
260
+ blueprintName,
261
+ blueprintTemplate,
262
+ sprites,
263
+ onEditBlueprint,
264
+ }) {
265
+ return (
266
+ <div className={styles.instanceHeader}>
267
+ <div className={styles.instanceHeaderName}>
268
+ <BlueprintThumbnail
269
+ blueprint={blueprintTemplate ?? { components: selectedActor.components }}
270
+ sprites={sprites}
271
+ />
272
+ <div className={styles.instanceHeaderNameCol}>
273
+ <span className={styles.instanceHeaderNameText}>
274
+ <strong className={styles.instanceHeaderNameStrong}>{blueprintName}</strong> Actor
275
+ </span>
276
+ <span className={styles.instanceHeaderId}>id: {selectedActor.id}</span>
277
+ </div>
278
+ </div>
279
+ {onEditBlueprint ? (
280
+ <button
281
+ type="button"
282
+ className={styles.instanceForkButton}
283
+ onClick={onEditBlueprint}
284
+ title="Select this blueprint in the hotbar">
285
+ <Icon name="pencil" />
286
+ <span>Edit blueprint</span>
287
+ </button>
288
+ ) : null}
289
+ </div>
290
+ );
291
+ }
292
+
293
+ function BehaviorPanel({
294
+ behaviorName,
295
+ component,
296
+ selectedActor,
297
+ rawActor,
298
+ blueprintTemplate,
299
+ isInstance,
300
+ sprites,
301
+ files,
302
+ isLast,
303
+ onSetComponent,
304
+ onRemoveBehavior,
305
+ }) {
306
+ const Behavior = findBehaviorClass(behaviorName);
307
+ const Inspector = Behavior?.Inspector;
308
+ const setComponent = (nextProps) => onSetComponent(selectedActor.id, behaviorName, nextProps);
309
+ const override = isInstance
310
+ ? makeOverrideContext({
311
+ Behavior,
312
+ rawComponent: rawActor?.components?.[behaviorName],
313
+ templateProps: blueprintTemplate?.components?.[behaviorName],
314
+ setComponent,
315
+ })
316
+ : null;
317
+ const body = Inspector ? (
318
+ <Panel title={behaviorName} overridden={override?.anyOverridden()}>
319
+ <Inspector
320
+ actor={selectedActor}
321
+ component={component}
322
+ files={files}
323
+ sprites={sprites}
324
+ setComponent={setComponent}
325
+ override={override}
326
+ />
327
+ </Panel>
328
+ ) : (
329
+ <AutoInspector
330
+ behaviorName={behaviorName}
331
+ defaultProps={Behavior?.defaultProps ?? component}
332
+ component={component}
333
+ setComponent={setComponent}
334
+ override={override}
335
+ />
336
+ );
337
+ // Transform is core to every actor; a component an instance only has via
338
+ // its blueprint has nothing to remove either.
339
+ const removable =
340
+ behaviorName !== 'Transform' && (!isInstance || Boolean(rawActor.components?.[behaviorName]));
341
+ return (
342
+ <div
343
+ style={{
344
+ position: 'relative',
345
+ borderBottom: isLast ? undefined : '1px solid var(--castle-inspector-divider)',
346
+ }}>
347
+ {removable ? (
348
+ <button
349
+ type="button"
350
+ aria-label={`Remove ${behaviorName}`}
351
+ title={`Remove ${behaviorName}`}
352
+ onClick={() => onRemoveBehavior(selectedActor.id, behaviorName)}
353
+ style={{
354
+ position: 'absolute',
355
+ top: 14,
356
+ right: 16,
357
+ border: 'none',
358
+ background: 'transparent',
359
+ padding: 0,
360
+ margin: 0,
361
+ cursor: 'pointer',
362
+ color: 'inherit',
363
+ fontSize: 16,
364
+ lineHeight: 1,
365
+ }}>
366
+ <Icon name="trash" />
367
+ </button>
368
+ ) : null}
369
+ {body}
370
+ </div>
371
+ );
372
+ }
373
+
374
+ function AddBehaviorPicker({ available, onAdd }) {
375
+ if (!available.length) return null;
376
+ return (
377
+ <div
378
+ style={{
379
+ height: 'var(--castle-hotbar-row-full)',
380
+ padding: '0 16px',
381
+ display: 'flex',
382
+ alignItems: 'center',
383
+ borderBottom: '1px solid var(--castle-inspector-divider)',
384
+ }}>
385
+ <select
386
+ className={styles.select}
387
+ value=""
388
+ onChange={(event) => {
389
+ if (event.target.value) onAdd(event.target.value);
390
+ }}>
391
+ <option value="">+ Add behavior</option>
392
+ {available.map((behavior) => (
393
+ <option key={behavior.behaviorName} value={behavior.behaviorName}>
394
+ {behavior.behaviorName}
395
+ </option>
396
+ ))}
397
+ </select>
398
+ </div>
399
+ );
400
+ }
401
+
402
+ // Unused import keepalive: CheckboxField is part of the field set custom
403
+ // behavior inspectors reach for; re-export so they can import from one place.
404
+ export { CheckboxField };
@@ -0,0 +1,218 @@
1
+ // Chrome shared by the kit's editors (scene + model): the debounced file
2
+ // saver, the stage canvas, the undo/redo + mode/action button overlays, and
3
+ // the common keyboard shortcuts. One place so the editors stay in the same
4
+ // design system by construction.
5
+ import React, { forwardRef, useEffect, useRef } from 'react';
6
+ import { onBeforeRestart, writeFile } from 'castle-web-sdk';
7
+ import { Button, Icon, IconButton } from '@imports/castle.physics-2d/engine/ui';
8
+
9
+ // Custom glyphs for concepts the kit's icon set doesn't cover, drawn to match
10
+ // its filled-silhouette look (512 viewbox, currentColor fill).
11
+ const CUSTOM_GLYPHS = {
12
+ cylinder: 'M96 116 a160 60 0 0 1 320 0 V396 a160 60 0 0 1 -320 0 Z',
13
+ capsule: 'M256 32 a128 128 0 0 1 128 128 v192 a128 128 0 0 1 -256 0 V160 A128 128 0 0 1 256 32 Z',
14
+ face: 'M256 72 L456 256 L256 440 L56 256 Z',
15
+ vertices:
16
+ 'M128 112 a56 56 0 1 0 0.1 0 Z M384 112 a56 56 0 1 0 0.1 0 Z M256 344 a56 56 0 1 0 0.1 0 Z',
17
+ extrude: 'M256 24 L368 136 H296 V232 H216 V136 H144 Z M96 288 H416 V456 H96 Z',
18
+ };
19
+
20
+ function CustomIcon({ glyph }) {
21
+ const path = CUSTOM_GLYPHS[glyph];
22
+ if (!path) return null;
23
+ return (
24
+ <svg
25
+ viewBox="0 0 512 512"
26
+ aria-hidden="true"
27
+ style={{ width: '1em', height: '1em', display: 'inline-block', fill: 'currentColor' }}>
28
+ <path d={path} fillRule="evenodd" />
29
+ </svg>
30
+ );
31
+ }
32
+
33
+ // Files-panel-style hotbar button: icon + name, never wrapping. `icon` names
34
+ // the kit's icon set; `glyph` picks a custom one above.
35
+ export function HotbarButton({ icon, glyph, label, ...props }) {
36
+ return (
37
+ <Button {...props}>
38
+ <span
39
+ style={{
40
+ display: 'inline-flex',
41
+ alignItems: 'center',
42
+ gap: 7,
43
+ whiteSpace: 'nowrap',
44
+ }}>
45
+ {glyph ? <CustomIcon glyph={glyph} /> : icon ? <Icon name={icon} /> : null}
46
+ <span>{label}</span>
47
+ </span>
48
+ </Button>
49
+ );
50
+ }
51
+
52
+ // Stage tool buttons run a bit bigger than the default IconButton.
53
+ export const TOOL_BUTTON_STYLE = { width: 40, height: 40, fontSize: 17 };
54
+
55
+ const UNDO_STYLE = {
56
+ position: 'absolute',
57
+ left: 10,
58
+ top: 10,
59
+ display: 'flex',
60
+ flexDirection: 'row',
61
+ gap: 6,
62
+ zIndex: 5,
63
+ };
64
+ const MODE_STYLE = {
65
+ position: 'absolute',
66
+ right: 8,
67
+ top: 10,
68
+ display: 'flex',
69
+ flexDirection: 'column',
70
+ gap: 4,
71
+ zIndex: 5,
72
+ };
73
+
74
+ // Debounced per-path writeFile, flushed before a reload -- the same save
75
+ // behavior as the imported kit's SingleEditor.
76
+ export function useFileSaver3D() {
77
+ const pending = useRef({});
78
+ const timers = useRef({});
79
+ function schedule(path, text) {
80
+ pending.current[path] = text;
81
+ if (timers.current[path]) window.clearTimeout(timers.current[path]);
82
+ timers.current[path] = window.setTimeout(() => {
83
+ delete timers.current[path];
84
+ const value = pending.current[path];
85
+ delete pending.current[path];
86
+ void writeFile(path, value).catch((error) => console.error(`save failed: ${error}`));
87
+ }, 500);
88
+ }
89
+ useEffect(
90
+ () =>
91
+ onBeforeRestart(async () => {
92
+ for (const timer of Object.values(timers.current)) window.clearTimeout(timer);
93
+ timers.current = {};
94
+ const writes = Object.entries(pending.current);
95
+ pending.current = {};
96
+ await Promise.all(writes.map(([path, value]) => writeFile(path, value)));
97
+ }),
98
+ []
99
+ );
100
+ return { schedule, hasPending: (path) => pending.current[path] !== undefined };
101
+ }
102
+
103
+ export function isEditableTarget(target) {
104
+ return (
105
+ target?.tagName === 'INPUT' ||
106
+ target?.tagName === 'TEXTAREA' ||
107
+ target?.tagName === 'SELECT' ||
108
+ target?.getAttribute?.('contenteditable') === 'true'
109
+ );
110
+ }
111
+
112
+ // The editors' shared keys: cmd+z / cmd+shift+z undo/redo, cmd+d duplicate,
113
+ // delete/backspace remove, 1/2/3 pick select/move/rotate, escape deselects.
114
+ // Handlers are read through a latest-ref so callers can pass fresh closures.
115
+ export function useEditorShortcuts(handlers) {
116
+ const latest = useRef(null);
117
+ latest.current = handlers;
118
+ useEffect(() => {
119
+ const onKeyDown = (event) => {
120
+ if (isEditableTarget(event.target)) return;
121
+ const h = latest.current;
122
+ const meta = event.metaKey || event.ctrlKey;
123
+ if (meta && event.key.toLowerCase() === 'z') {
124
+ event.preventDefault();
125
+ if (event.shiftKey) h.history.redo();
126
+ else h.history.undo();
127
+ } else if (meta && event.key.toLowerCase() === 'd') {
128
+ event.preventDefault();
129
+ h.onDuplicate?.();
130
+ } else if (event.key === 'Backspace' || event.key === 'Delete') {
131
+ h.onDelete?.();
132
+ } else if (event.key === '1') h.setMode('select');
133
+ else if (event.key === '2') h.setMode('move');
134
+ else if (event.key === '3') h.setMode('rotate');
135
+ else if (event.key === 'Escape') h.onEscape?.();
136
+ };
137
+ window.addEventListener('keydown', onKeyDown);
138
+ return () => window.removeEventListener('keydown', onKeyDown);
139
+ }, []);
140
+ }
141
+
142
+ export const StageCanvas = forwardRef(function StageCanvas(props, ref) {
143
+ return (
144
+ <canvas
145
+ ref={ref}
146
+ tabIndex={-1}
147
+ style={{
148
+ position: 'absolute',
149
+ inset: 0,
150
+ width: '100%',
151
+ height: '100%',
152
+ display: 'block',
153
+ outline: 'none',
154
+ touchAction: 'none',
155
+ }}
156
+ onPointerDown={(event) => event.currentTarget.focus({ preventScroll: true })}
157
+ {...props}
158
+ />
159
+ );
160
+ });
161
+
162
+ // Undo/redo top-left; select/move/rotate (plus duplicate/delete when handlers
163
+ // are given) down the right edge.
164
+ export function StageControls({ history, mode, setMode, onDuplicate, onDelete, actionsDisabled }) {
165
+ const modeButton = (value, icon, label) => (
166
+ <IconButton
167
+ icon={icon}
168
+ label={label}
169
+ active={mode === value}
170
+ style={TOOL_BUTTON_STYLE}
171
+ onClick={() => setMode(value)}
172
+ />
173
+ );
174
+ return (
175
+ <>
176
+ <div style={UNDO_STYLE}>
177
+ <IconButton
178
+ icon="undo"
179
+ label="Undo"
180
+ style={TOOL_BUTTON_STYLE}
181
+ onClick={history.undo}
182
+ disabled={!history.canUndo}
183
+ />
184
+ <IconButton
185
+ icon="redo"
186
+ label="Redo"
187
+ style={TOOL_BUTTON_STYLE}
188
+ onClick={history.redo}
189
+ disabled={!history.canRedo}
190
+ />
191
+ </div>
192
+ <div style={MODE_STYLE}>
193
+ {modeButton('select', 'marquee', 'Select (1)')}
194
+ {modeButton('move', 'move', 'Move (2)')}
195
+ {modeButton('rotate', 'rotate', 'Rotate (3)')}
196
+ {onDuplicate || onDelete ? <div style={{ height: 8 }} /> : null}
197
+ {onDuplicate ? (
198
+ <IconButton
199
+ icon="clone"
200
+ label="Duplicate"
201
+ style={TOOL_BUTTON_STYLE}
202
+ disabled={actionsDisabled}
203
+ onClick={onDuplicate}
204
+ />
205
+ ) : null}
206
+ {onDelete ? (
207
+ <IconButton
208
+ icon="trash"
209
+ label="Delete"
210
+ style={TOOL_BUTTON_STYLE}
211
+ disabled={actionsDisabled}
212
+ onClick={onDelete}
213
+ />
214
+ ) : null}
215
+ </div>
216
+ </>
217
+ );
218
+ }