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.
- package/dist/agent-prompts.js +4 -4
- package/dist/filesChanged.d.ts +2 -0
- package/dist/filesChanged.js +6 -2
- package/dist/ide.js +15 -4
- package/dist/importBrowse.js +1 -0
- package/dist/index.js +10 -4
- package/dist/init.js +63 -15
- package/dist/native/loop.js +1 -1
- package/dist/native/tools.js +1 -1
- package/dist/serve.d.ts +1 -0
- package/dist/serve.js +20 -1
- package/dist/shell/assets/index-BpOLUyVO.js +441 -0
- package/dist/shell/assets/{index-CAq6f9B5.css → index-D6K-0YDB.css} +1 -1
- package/dist/shell/index.html +2 -2
- package/kits/physics-2d/CLAUDE.md +9 -10
- package/kits/physics-2d/castle.json +1 -1
- package/kits/physics-2d/editors/PlayOnly.jsx +15 -6
- package/kits/physics-2d/editors/PxArtEditor.jsx +95 -31
- package/kits/physics-2d/editors/SceneEditor.jsx +5 -5
- package/kits/physics-2d/editors/SingleEditor.jsx +37 -8
- package/kits/physics-2d/editors/StyleEditor.jsx +7 -4
- package/kits/physics-2d/editors/pixelEditorChrome.jsx +7 -7
- package/kits/physics-2d/editors/pixelInspector.jsx +13 -7
- package/kits/physics-2d/editors/pxArtTimeline.jsx +29 -11
- package/kits/physics-2d/editors/pxArtTimeline.module.css +11 -0
- package/kits/physics-2d/engine/blueprint.js +15 -3
- package/kits/physics-2d/engine/liveReload.js +33 -21
- package/kits/physics-2d/engine/systemRegistry.js +1 -1
- package/kits/physics-2d/engine/ui.jsx +2 -2
- package/kits/physics-2d/engine/ui.module.css +42 -0
- package/kits/physics-3d/CLAUDE.md +109 -0
- package/kits/physics-3d/behaviors/Body.jsx +51 -0
- package/kits/physics-3d/behaviors/Door.jsx +31 -0
- package/kits/physics-3d/behaviors/Lookable.jsx +11 -0
- package/kits/physics-3d/behaviors/Model.jsx +14 -0
- package/kits/physics-3d/behaviors/Pickup.jsx +39 -0
- package/kits/physics-3d/behaviors/Player.jsx +172 -0
- package/kits/physics-3d/behaviors/Portal.jsx +19 -0
- package/kits/physics-3d/behaviors/Shape.jsx +75 -0
- package/kits/physics-3d/behaviors/Solid.jsx +11 -0
- package/kits/physics-3d/behaviors/Transform.jsx +24 -0
- package/kits/physics-3d/blueprints/barrel.scene +15 -0
- package/kits/physics-3d/blueprints/crate.scene +29 -0
- package/kits/physics-3d/blueprints/door.scene +35 -0
- package/kits/physics-3d/blueprints/gem.scene +26 -0
- package/kits/physics-3d/blueprints/pillar.scene +25 -0
- package/kits/physics-3d/blueprints/platform.scene +23 -0
- package/kits/physics-3d/blueprints/player.scene +29 -0
- package/kits/physics-3d/blueprints/portal.scene +21 -0
- package/kits/physics-3d/blueprints/rock.scene +14 -0
- package/kits/physics-3d/blueprints/statue.scene +26 -0
- package/kits/physics-3d/blueprints/tree.scene +14 -0
- package/kits/physics-3d/blueprints/wall.scene +26 -0
- package/kits/{basic-2d → physics-3d}/castle.json +23 -5
- package/kits/physics-3d/docs/pxmodel-format.md +111 -0
- package/kits/physics-3d/drawings/crate.pxart +26 -0
- package/kits/physics-3d/drawings/door-edge.pxart +26 -0
- package/kits/physics-3d/drawings/door.pxart +27 -0
- package/kits/physics-3d/drawings/face.pxart +26 -0
- package/kits/physics-3d/drawings/floor.pxart +27 -0
- package/kits/physics-3d/drawings/platform.pxart +27 -0
- package/kits/physics-3d/drawings/statue.pxart +27 -0
- package/kits/physics-3d/drawings/wall.pxart +27 -0
- package/kits/physics-3d/engine3d/PlayOnly3D.jsx +32 -0
- package/kits/physics-3d/engine3d/PxModelEditor.jsx +619 -0
- package/kits/physics-3d/engine3d/Scene3DEditor.jsx +618 -0
- package/kits/physics-3d/engine3d/Scene3DPlayer.jsx +309 -0
- package/kits/physics-3d/engine3d/editor3dData.js +114 -0
- package/kits/physics-3d/engine3d/editor3dInspector.jsx +404 -0
- package/kits/physics-3d/engine3d/editorChrome.jsx +218 -0
- package/kits/physics-3d/engine3d/editorWorld.js +249 -0
- package/kits/physics-3d/engine3d/materials.js +174 -0
- package/kits/physics-3d/engine3d/meshops.js +123 -0
- package/kits/physics-3d/engine3d/modelEditorWorld.js +439 -0
- package/kits/physics-3d/engine3d/modelFiles.js +139 -0
- package/kits/physics-3d/engine3d/pxmodel.js +265 -0
- package/kits/physics-3d/engine3d/thumbnails.js +137 -0
- package/kits/physics-3d/engine3d/world3d.js +216 -0
- package/kits/{basic-2d → physics-3d}/eslint.config.js +10 -25
- package/kits/physics-3d/index.html +20 -0
- package/kits/physics-3d/main.jsx +30 -0
- package/kits/physics-3d/models/barrel.pxmodel +10 -0
- package/kits/physics-3d/models/player.pxmodel +222 -0
- package/kits/physics-3d/models/rock.pxmodel +30 -0
- package/kits/physics-3d/models/tree.pxmodel +10 -0
- package/kits/{basic-2d → physics-3d}/package-lock.json +140 -119
- package/kits/{basic-2d → physics-3d}/package.json +11 -9
- package/kits/{basic-2d → physics-3d}/pnpm-lock.yaml +162 -137
- package/kits/physics-3d/scenes/main.scene +414 -0
- package/kits/physics-3d/scenes/model-lab.scene +127 -0
- package/kits/physics-3d/systems/physics3d.js +356 -0
- package/package.json +5 -2
- package/dist/shell/assets/index-DKu9ejyh.js +0 -436
- package/kits/basic-2d/CLAUDE.md +0 -221
- package/kits/basic-2d/behaviors/Camera.jsx +0 -43
- package/kits/basic-2d/behaviors/Collider.jsx +0 -213
- package/kits/basic-2d/behaviors/Layout.jsx +0 -53
- package/kits/basic-2d/behaviors/Sprite.jsx +0 -357
- package/kits/basic-2d/behaviors/tint.js +0 -47
- package/kits/basic-2d/blueprints/cauldron.scene +0 -20
- package/kits/basic-2d/docs/pxart-format.md +0 -377
- package/kits/basic-2d/drawings/cauldron.pxart +0 -113
- package/kits/basic-2d/editors/BlueprintLibrary.jsx +0 -270
- package/kits/basic-2d/editors/ErrorBoundary.jsx +0 -59
- package/kits/basic-2d/editors/PlayOnly.jsx +0 -31
- package/kits/basic-2d/editors/PxArtEditor.jsx +0 -1092
- package/kits/basic-2d/editors/SceneEditor.jsx +0 -1780
- package/kits/basic-2d/editors/SelectionOverlay.jsx +0 -909
- package/kits/basic-2d/editors/SingleEditor.jsx +0 -122
- package/kits/basic-2d/editors/behaviorRegistry.js +0 -34
- package/kits/basic-2d/editors/editorHistory.js +0 -157
- package/kits/basic-2d/editors/inspectorSheet.js +0 -13
- package/kits/basic-2d/editors/pixelCanvas.js +0 -11
- package/kits/basic-2d/editors/pixelEditorChrome.jsx +0 -74
- package/kits/basic-2d/editors/pixelGeometry.js +0 -140
- package/kits/basic-2d/editors/pixelInspector.jsx +0 -633
- package/kits/basic-2d/editors/pxArtEditorModel.js +0 -732
- package/kits/basic-2d/editors/pxArtPlayback.js +0 -92
- package/kits/basic-2d/editors/pxArtTimeline.jsx +0 -752
- package/kits/basic-2d/editors/pxArtTimeline.module.css +0 -506
- package/kits/basic-2d/editors/pxArtTools.js +0 -232
- package/kits/basic-2d/editors/useArtboardFit.js +0 -105
- package/kits/basic-2d/engine/ScenePlayer.jsx +0 -209
- package/kits/basic-2d/engine/SceneUI.jsx +0 -59
- package/kits/basic-2d/engine/assets.js +0 -15
- package/kits/basic-2d/engine/autoInspector.jsx +0 -70
- package/kits/basic-2d/engine/behaviorExtensions.js +0 -32
- package/kits/basic-2d/engine/blueprint.js +0 -557
- package/kits/basic-2d/engine/collider.js +0 -200
- package/kits/basic-2d/engine/files.js +0 -141
- package/kits/basic-2d/engine/liveReload.js +0 -88
- package/kits/basic-2d/engine/pxart.js +0 -1032
- package/kits/basic-2d/engine/pxartSmooth.js +0 -222
- package/kits/basic-2d/engine/scene.js +0 -696
- package/kits/basic-2d/engine/spriteGeometry.js +0 -32
- package/kits/basic-2d/engine/systemRegistry.js +0 -16
- package/kits/basic-2d/engine/ui.jsx +0 -695
- package/kits/basic-2d/engine/ui.module.css +0 -2287
- package/kits/basic-2d/index.html +0 -24
- package/kits/basic-2d/main.jsx +0 -24
- package/kits/basic-2d/scenes/main.scene +0 -16
- package/kits/basic-2d/scripts/draw.mjs +0 -121
- /package/kits/{basic-2d → physics-3d}/.prettierrc +0 -0
- /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
|
+
}
|