@jgengine/editor 0.19.0 → 0.22.0
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/CHANGELOG.md +364 -0
- package/dist/ClipPreviewLayer.d.ts +2 -3
- package/dist/ClipPreviewLayer.js +10 -28
- package/dist/CreatorApp.d.ts +12 -0
- package/dist/CreatorApp.js +107 -0
- package/dist/DebugDraw.js +3 -1
- package/dist/EditorApp.d.ts +14 -2
- package/dist/EditorApp.js +29 -14
- package/dist/EditorChrome.d.ts +9 -1
- package/dist/EditorChrome.js +54 -35
- package/dist/InspectorPanel.d.ts +3 -1
- package/dist/InspectorPanel.js +12 -8
- package/dist/MaterialAssetEditor.d.ts +13 -0
- package/dist/MaterialAssetEditor.js +109 -0
- package/dist/MaterialPreview.d.ts +16 -0
- package/dist/MaterialPreview.js +41 -0
- package/dist/MaterialsWorkspacePanel.d.ts +14 -2
- package/dist/MaterialsWorkspacePanel.js +78 -4
- package/dist/RuntimePlayBridge.js +1 -6
- package/dist/SelectionGizmo.js +23 -14
- package/dist/SimulationInspector.d.ts +9 -0
- package/dist/SimulationInspector.js +47 -0
- package/dist/TerrainSculpt.js +12 -59
- package/dist/agent/toolBridge.js +2 -0
- package/dist/authoringPlacement.d.ts +21 -0
- package/dist/authoringPlacement.js +34 -0
- package/dist/handlers/context.d.ts +1 -0
- package/dist/handlers/document.d.ts +1 -1
- package/dist/handlers/document.js +29 -1
- package/dist/handlers/hierarchy.d.ts +1 -1
- package/dist/handlers/hierarchy.js +4 -0
- package/dist/handlers/runtime.js +20 -17
- package/dist/handlers/terrain.js +52 -12
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/materialControls.d.ts +32 -0
- package/dist/materialControls.js +79 -0
- package/dist/mcp/fieldSpec.d.ts +1 -1
- package/dist/mcp/fieldSpec.js +11 -2
- package/dist/mcp/importOptionalGameModule.d.ts +2 -0
- package/dist/mcp/importOptionalGameModule.js +13 -0
- package/dist/mcp/loadGameCatalogs.js +5 -12
- package/dist/mcp/rpcRequest.d.ts +3 -3
- package/dist/mcp/rpcRequest.js +10 -4
- package/dist/mcp/tools.js +10 -1
- package/dist/modelAnimationAuthoring.d.ts +27 -7
- package/dist/modelAnimationAuthoring.js +69 -72
- package/dist/session.d.ts +62 -0
- package/dist/session.js +56 -7
- package/dist/shell/AnimationGraphPanel.js +18 -10
- package/dist/shell/PlayModeBar.d.ts +2 -1
- package/dist/shell/PlayModeBar.js +2 -2
- package/dist/shell/SceneToolbar.d.ts +2 -1
- package/dist/shell/SceneToolbar.js +5 -4
- package/dist/shell/TopAppBar.d.ts +2 -1
- package/dist/shell/TopAppBar.js +2 -2
- package/dist/shell/animGraphPreview.d.ts +2 -0
- package/dist/shell/animGraphPreview.js +5 -2
- package/dist/shell/clipPreview.d.ts +1 -0
- package/dist/uiStore.d.ts +1 -10
- package/dist/uiStore.js +1 -13
- package/dist/useDocumentSave.d.ts +20 -0
- package/dist/useDocumentSave.js +33 -0
- package/package.json +6 -5
- package/skills/ce-handoff/SKILL.md +1 -1
- package/skills/jgengine/SKILL.md +23 -2
- package/skills/jgengine/api.md +28 -17
- package/skills/jgengine/recipes/minimal-game.md +7 -2
- package/skills/jgengine-editor/SKILL.md +9 -1
- package/skills/jgengine-editor/api.md +111 -16
- package/skills/jgengine-editor/capabilities.md +40 -0
- package/skills/jgengine-editor/reference.md +149 -1
- package/skills/jgengine-editor/references/live-sync.md +15 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readAuthoredAnimation } from "@jgengine/core/world/authoredAnimation";
|
|
2
2
|
import { animGraphFromConfig } from "@jgengine/core/anim/locomotionGraph";
|
|
3
3
|
import { resolveAnimationConfig, rolesFromClips } from "@jgengine/core/game/clipRoles";
|
|
4
4
|
/**
|
|
@@ -24,51 +24,14 @@ export const LOCOMOTION_NUMBERS = ["walkSpeed", "runSpeed", "fadeSec"];
|
|
|
24
24
|
* `combat.hitReaction` / `entity.died`; the rest fire when the game calls `playEntityAnimation`.
|
|
25
25
|
*/
|
|
26
26
|
export const ONE_SHOT_EVENTS = ["attack", "hit", "death", "jump", "interact", "cheer"];
|
|
27
|
-
|
|
28
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
29
|
-
}
|
|
30
|
-
/** Reads and shape-validates the animation override from a marker's meta; undefined when absent/invalid. */
|
|
27
|
+
/** Reads a whole valid override, retaining authored fields; malformed values return undefined. */
|
|
31
28
|
export function readAnimationSetting(meta) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (typeof value["clip"] === "string")
|
|
39
|
-
config.clip = value["clip"];
|
|
40
|
-
if (typeof value["loop"] === "boolean")
|
|
41
|
-
config.loop = value["loop"];
|
|
42
|
-
if (typeof value["timeScale"] === "number")
|
|
43
|
-
config.timeScale = value["timeScale"];
|
|
44
|
-
if (isRecord(value["states"])) {
|
|
45
|
-
const raw = value["states"];
|
|
46
|
-
const states = {};
|
|
47
|
-
for (const role of LOCOMOTION_ROLES)
|
|
48
|
-
if (typeof raw[role] === "string")
|
|
49
|
-
states[role] = raw[role];
|
|
50
|
-
for (const key of LOCOMOTION_NUMBERS)
|
|
51
|
-
if (typeof raw[key] === "number")
|
|
52
|
-
states[key] = raw[key];
|
|
53
|
-
if (Object.keys(states).length > 0)
|
|
54
|
-
config.states = states;
|
|
55
|
-
}
|
|
56
|
-
if (isRecord(value["oneShots"])) {
|
|
57
|
-
const raw = value["oneShots"];
|
|
58
|
-
const oneShots = {};
|
|
59
|
-
for (const [event, clip] of Object.entries(raw)) {
|
|
60
|
-
if (typeof clip === "string")
|
|
61
|
-
oneShots[event] = clip;
|
|
62
|
-
else if (Array.isArray(clip) && typeof clip[0] === "string")
|
|
63
|
-
oneShots[event] = clip[0];
|
|
64
|
-
}
|
|
65
|
-
if (Object.keys(oneShots).length > 0)
|
|
66
|
-
config.oneShots = oneShots;
|
|
67
|
-
}
|
|
68
|
-
const graph = parseAnimGraph(value["graph"]);
|
|
69
|
-
if (graph !== undefined)
|
|
70
|
-
config.graph = graph;
|
|
71
|
-
return config;
|
|
29
|
+
return readAnimationSettingResult({ markers: [{ id: "animation", meta }] }, "animation").setting;
|
|
30
|
+
}
|
|
31
|
+
/** Validates a placement's animation with its saved-document repair locations. @internal */
|
|
32
|
+
export function readAnimationSettingResult(document, markerId) {
|
|
33
|
+
const result = readAuthoredAnimation(document, markerId);
|
|
34
|
+
return { setting: result.animation, diagnostics: result.diagnostics };
|
|
72
35
|
}
|
|
73
36
|
/** The authoring mode a stored setting maps to. */
|
|
74
37
|
export function animationMode(setting) {
|
|
@@ -83,14 +46,6 @@ export function animationMode(setting) {
|
|
|
83
46
|
function asConfig(setting) {
|
|
84
47
|
return setting !== undefined && setting !== "auto" && setting !== "none" ? setting : {};
|
|
85
48
|
}
|
|
86
|
-
function normalizeConfig(config) {
|
|
87
|
-
const next = { ...config };
|
|
88
|
-
if (next.states !== undefined && Object.keys(next.states).length === 0)
|
|
89
|
-
delete next.states;
|
|
90
|
-
if (next.oneShots !== undefined && Object.keys(next.oneShots).length === 0)
|
|
91
|
-
delete next.oneShots;
|
|
92
|
-
return next;
|
|
93
|
-
}
|
|
94
49
|
/**
|
|
95
50
|
* Switches authoring mode. `custom` seeds from the asset's clip-role defaults (so the user starts from
|
|
96
51
|
* a working config, then overrides) when there is no existing object config; the other modes are the
|
|
@@ -105,7 +60,7 @@ export function setAnimationMode(setting, mode, clips = []) {
|
|
|
105
60
|
return "none";
|
|
106
61
|
if (setting !== undefined && setting !== "auto" && setting !== "none")
|
|
107
62
|
return setting;
|
|
108
|
-
return
|
|
63
|
+
return defaultCustomConfig(clips);
|
|
109
64
|
}
|
|
110
65
|
/** A concrete custom config derived from a rigged asset's clip roles — the `custom` mode seed. */
|
|
111
66
|
export function defaultCustomConfig(clips) {
|
|
@@ -119,9 +74,9 @@ export function defaultCustomConfig(clips) {
|
|
|
119
74
|
states.run = roles.run[0];
|
|
120
75
|
const oneShots = {};
|
|
121
76
|
for (const event of ONE_SHOT_EVENTS) {
|
|
122
|
-
const
|
|
123
|
-
if (
|
|
124
|
-
oneShots[event] =
|
|
77
|
+
const variants = roles[event];
|
|
78
|
+
if (variants !== undefined && variants.length > 0)
|
|
79
|
+
oneShots[event] = variants.length === 1 ? variants[0] : [...variants];
|
|
125
80
|
}
|
|
126
81
|
const config = {};
|
|
127
82
|
if (Object.keys(states).length > 0)
|
|
@@ -131,34 +86,72 @@ export function defaultCustomConfig(clips) {
|
|
|
131
86
|
return config;
|
|
132
87
|
}
|
|
133
88
|
/** Sets or clears (null) a locomotion state clip; forces the setting into `custom`. */
|
|
134
|
-
export function setLocomotionClip(setting, role, clipName) {
|
|
89
|
+
export function setLocomotionClip(setting, role, clipName, clips = []) {
|
|
135
90
|
const config = asConfig(setting);
|
|
136
|
-
const states = { ...config.states };
|
|
91
|
+
const states = { ...(config.states ?? (clipName === null ? undefined : defaultCustomConfig(clips).states)) };
|
|
137
92
|
if (clipName === null)
|
|
138
93
|
delete states[role];
|
|
139
94
|
else
|
|
140
95
|
states[role] = clipName;
|
|
141
|
-
return
|
|
96
|
+
return { ...config, ...(config.states !== undefined || Object.keys(states).length > 0 ? { states } : {}) };
|
|
142
97
|
}
|
|
143
98
|
/** Sets or clears (null) a numeric locomotion tuning. */
|
|
144
|
-
export function setLocomotionNumber(setting, key, value) {
|
|
99
|
+
export function setLocomotionNumber(setting, key, value, clips = []) {
|
|
145
100
|
const config = asConfig(setting);
|
|
146
|
-
const states = { ...config.states };
|
|
101
|
+
const states = { ...(config.states ?? (value === null || !Number.isFinite(value) ? undefined : defaultCustomConfig(clips).states)) };
|
|
147
102
|
if (value === null || !Number.isFinite(value))
|
|
148
103
|
delete states[key];
|
|
149
104
|
else
|
|
150
105
|
states[key] = value;
|
|
151
|
-
return
|
|
106
|
+
return { ...config, ...(config.states !== undefined || Object.keys(states).length > 0 ? { states } : {}) };
|
|
152
107
|
}
|
|
153
|
-
/** Binds or clears (null) a one-shot event
|
|
108
|
+
/** Binds or clears (null) a one-shot event's clip or variant list. */
|
|
154
109
|
export function setOneShotClip(setting, event, clipName) {
|
|
155
110
|
const config = asConfig(setting);
|
|
156
111
|
const oneShots = { ...config.oneShots };
|
|
157
|
-
if (clipName === null)
|
|
112
|
+
if (clipName === null || (Array.isArray(clipName) && clipName.length === 0))
|
|
158
113
|
delete oneShots[event];
|
|
159
114
|
else
|
|
160
|
-
oneShots[event] = clipName;
|
|
161
|
-
|
|
115
|
+
oneShots[event] = typeof clipName === "string" ? clipName : clipName.length === 1 ? clipName[0] : [...clipName];
|
|
116
|
+
const next = { ...config, oneShots };
|
|
117
|
+
if (Object.keys(oneShots).length === 0)
|
|
118
|
+
delete next.oneShots;
|
|
119
|
+
return next;
|
|
120
|
+
}
|
|
121
|
+
/** Sets a single playback clip, explicitly replacing speed-driven states and a stored graph. @internal */
|
|
122
|
+
export function setPlaybackClip(setting, clip) {
|
|
123
|
+
if (clip === null) {
|
|
124
|
+
const { clip: _clip, ...config } = asConfig(setting);
|
|
125
|
+
return config;
|
|
126
|
+
}
|
|
127
|
+
const { states: _states, graph: _graph, auto: _auto, ...config } = asConfig(setting);
|
|
128
|
+
return { ...config, clip };
|
|
129
|
+
}
|
|
130
|
+
/** Changes a playback scalar while preserving role mappings, variants and the stored graph. @internal */
|
|
131
|
+
export function setPlaybackNumber(setting, key, value) {
|
|
132
|
+
const config = { ...asConfig(setting) };
|
|
133
|
+
if (value === null || !Number.isFinite(value))
|
|
134
|
+
delete config[key];
|
|
135
|
+
else
|
|
136
|
+
config[key] = key === "time" ? Math.max(0, value) : value;
|
|
137
|
+
return config;
|
|
138
|
+
}
|
|
139
|
+
/** Chooses the playback clock without replacing mappings, graph, or other authored fields. @internal */
|
|
140
|
+
export function setPlaybackClock(setting, clock) {
|
|
141
|
+
return { ...(setting === "auto" ? { auto: true } : asConfig(setting)), clock };
|
|
142
|
+
}
|
|
143
|
+
/** Requests loaded-rig role inference, or removes that request, retaining explicit overrides whole. @internal */
|
|
144
|
+
export function setAutomaticRoles(setting, derive) {
|
|
145
|
+
const config = { ...asConfig(setting) };
|
|
146
|
+
if (derive)
|
|
147
|
+
config.auto = true;
|
|
148
|
+
else
|
|
149
|
+
delete config.auto;
|
|
150
|
+
return config;
|
|
151
|
+
}
|
|
152
|
+
/** Changes a playback toggle without replacing the remaining authored settings. @internal */
|
|
153
|
+
export function setPlaybackBoolean(setting, key, value) {
|
|
154
|
+
return { ...asConfig(setting), [key]: value };
|
|
162
155
|
}
|
|
163
156
|
/**
|
|
164
157
|
* The meta patch that persists a setting (or clears the override when undefined). Shallow-merges onto
|
|
@@ -175,14 +168,19 @@ export function animationMetaPatch(setting) {
|
|
|
175
168
|
export function effectiveAnimGraph(setting, clips) {
|
|
176
169
|
if (setting === "none")
|
|
177
170
|
return null;
|
|
171
|
+
if (setting !== undefined && setting !== "auto" && setting.auto === true) {
|
|
172
|
+
const resolved = resolveAnimationConfig(setting, clips);
|
|
173
|
+
const graph = resolved === undefined ? undefined : animGraphFromConfig(resolved);
|
|
174
|
+
return graph === undefined ? null : { graph, source: setting.graph !== undefined ? "authored" : setting.states !== undefined ? "locomotion" : "auto" };
|
|
175
|
+
}
|
|
178
176
|
if (setting !== undefined && setting !== "auto") {
|
|
179
177
|
if (setting.graph !== undefined)
|
|
180
178
|
return { graph: setting.graph, source: "authored" };
|
|
181
179
|
const states = setting.states;
|
|
182
|
-
if (states?.idle === undefined)
|
|
180
|
+
if (states?.idle === undefined || states.idle.trim().length === 0)
|
|
183
181
|
return null;
|
|
184
182
|
const graph = animGraphFromConfig({
|
|
185
|
-
states: { ...states, idle: states.idle, walk: states.walk
|
|
183
|
+
states: { ...states, idle: states.idle, walk: states.walk === undefined || states.walk.trim().length === 0 ? states.idle : states.walk },
|
|
186
184
|
...(setting.oneShots === undefined ? {} : { oneShots: setting.oneShots }),
|
|
187
185
|
});
|
|
188
186
|
return graph === undefined ? null : { graph, source: "locomotion" };
|
|
@@ -193,15 +191,14 @@ export function effectiveAnimGraph(setting, clips) {
|
|
|
193
191
|
}
|
|
194
192
|
/** Stores `graph` on the placement, keeping its other fields; the stored graph wins at play time. */
|
|
195
193
|
export function storeAnimGraph(setting, graph) {
|
|
196
|
-
return
|
|
194
|
+
return { ...asConfig(setting), graph };
|
|
197
195
|
}
|
|
198
196
|
/** Removes a stored graph so `states`/`oneShots` (or clip roles) drive the placement again. */
|
|
199
197
|
export function clearAnimGraph(setting) {
|
|
200
198
|
if (setting === undefined || setting === "auto" || setting === "none")
|
|
201
199
|
return setting;
|
|
202
200
|
const { graph: _graph, ...rest } = setting;
|
|
203
|
-
|
|
204
|
-
return Object.keys(next).length === 0 ? undefined : next;
|
|
201
|
+
return Object.keys(rest).length === 0 ? undefined : rest;
|
|
205
202
|
}
|
|
206
203
|
/**
|
|
207
204
|
* Sets one transition's crossfade seconds in `graph` and stores the result, so editing a derived
|
package/dist/session.d.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
import type { EditorSimulation } from "@jgengine/core/editor/simulation";
|
|
2
|
+
import type { MaterialAsset, MaterialSelector } from "@jgengine/core/material/materialAsset";
|
|
3
|
+
import type { StaticPrefabBake } from "@jgengine/core/editor/staticPrefab";
|
|
1
4
|
import { type DocumentLiveSync, type DocumentPatch, type EditorCatalogDefinition, type EditorCommand, type EditorDocument, type EditorGridPaletteEntry, type EditorKindVisibility, type EditorLayersInput, type EditorSession, type EditorSessionState, type RuntimeEntityState, type RuntimePlayControl } from "@jgengine/core/editor/index";
|
|
2
5
|
import type { TerraformMode, TerrainMaterialLayer } from "@jgengine/core/world/terraform";
|
|
3
6
|
import type { ParamSchema } from "@jgengine/core/scene/sceneKinds";
|
|
4
7
|
import type { TerrainField } from "@jgengine/core/world/terrain";
|
|
8
|
+
import type { ModelMaterialSlotInfo } from "@jgengine/shell/render/materialAsset";
|
|
5
9
|
/** How the editor hosts the game: frozen placement view, roamable world, the real game, or HUD-layout authoring. */
|
|
6
10
|
export type EditorRunMode = "edit" | "walk" | "play" | "hud";
|
|
7
11
|
/** The accepted run modes, in canonical order — the runtime guard behind {@link EditorRunMode}. @internal */
|
|
@@ -152,6 +156,11 @@ export type EditorBridgeRequest = {
|
|
|
152
156
|
height?: number;
|
|
153
157
|
} | {
|
|
154
158
|
method: "scene_summary";
|
|
159
|
+
} | {
|
|
160
|
+
method: "get_simulation";
|
|
161
|
+
} | {
|
|
162
|
+
method: "set_simulation";
|
|
163
|
+
simulation: EditorSimulation | null;
|
|
155
164
|
} | {
|
|
156
165
|
method: "export_document";
|
|
157
166
|
} | {
|
|
@@ -294,6 +303,10 @@ export type EditorBridgeRequest = {
|
|
|
294
303
|
} | {
|
|
295
304
|
method: "delete_prefab";
|
|
296
305
|
prefabId: string;
|
|
306
|
+
} | {
|
|
307
|
+
method: "set_prefab_static_bake";
|
|
308
|
+
prefabId: string;
|
|
309
|
+
bake: StaticPrefabBake | null;
|
|
297
310
|
} | {
|
|
298
311
|
method: "list_collections";
|
|
299
312
|
} | {
|
|
@@ -340,6 +353,26 @@ export type EditorBridgeRequest = {
|
|
|
340
353
|
color?: string;
|
|
341
354
|
label?: string;
|
|
342
355
|
meta?: Record<string, unknown>;
|
|
356
|
+
} | {
|
|
357
|
+
method: "list_material_assets";
|
|
358
|
+
} | {
|
|
359
|
+
method: "list_material_slots";
|
|
360
|
+
id: string;
|
|
361
|
+
} | {
|
|
362
|
+
method: "upsert_material_asset";
|
|
363
|
+
asset: MaterialAsset;
|
|
364
|
+
coalesce?: string;
|
|
365
|
+
} | {
|
|
366
|
+
method: "remove_material_asset";
|
|
367
|
+
id: string;
|
|
368
|
+
} | {
|
|
369
|
+
method: "assign_material_asset";
|
|
370
|
+
ids: string[];
|
|
371
|
+
materialId: string;
|
|
372
|
+
selector: MaterialSelector;
|
|
373
|
+
} | {
|
|
374
|
+
method: "clear_material_assets";
|
|
375
|
+
ids: string[];
|
|
343
376
|
} | {
|
|
344
377
|
method: "assign_material";
|
|
345
378
|
ids: string[];
|
|
@@ -498,6 +531,25 @@ export interface EditorAssetInfo {
|
|
|
498
531
|
kind: string;
|
|
499
532
|
url?: string;
|
|
500
533
|
}
|
|
534
|
+
/** Canonical imported slot names and mesh prerequisites reported by the loaded model. */
|
|
535
|
+
export type EditorMaterialSlotInfo = Readonly<Omit<ModelMaterialSlotInfo, "uvSets">> & {
|
|
536
|
+
readonly uvSets: readonly number[];
|
|
537
|
+
};
|
|
538
|
+
/** A live model-source observation; loading and unavailable reports never contain fabricated slots. */
|
|
539
|
+
export type EditorMaterialSlotReport = {
|
|
540
|
+
status: "ready";
|
|
541
|
+
slots: readonly EditorMaterialSlotInfo[];
|
|
542
|
+
} | {
|
|
543
|
+
status: "loading";
|
|
544
|
+
} | {
|
|
545
|
+
status: "unavailable";
|
|
546
|
+
reason: string;
|
|
547
|
+
};
|
|
548
|
+
/** A marker's current imported-model inventory, including an explicit availability state. */
|
|
549
|
+
export type EditorMaterialSlotInventory = EditorMaterialSlotReport & {
|
|
550
|
+
id: string;
|
|
551
|
+
sourceUrl?: string;
|
|
552
|
+
};
|
|
501
553
|
/** Rolling frame-rate sample published by the in-canvas PerfProbe. */
|
|
502
554
|
export interface EditorPerfSample {
|
|
503
555
|
/**
|
|
@@ -575,6 +627,10 @@ export interface EditorHostApi {
|
|
|
575
627
|
subscribeFocus(listener: (target: EditorFocusTarget | null) => void): () => void;
|
|
576
628
|
getAssets(): readonly EditorAssetInfo[];
|
|
577
629
|
setAssets(assets: readonly EditorAssetInfo[]): void;
|
|
630
|
+
/** Read observed slot data for the marker's current model URL; headless or unloaded sources are unavailable. */
|
|
631
|
+
getMaterialSlots(id: string): EditorMaterialSlotInventory;
|
|
632
|
+
/** Publish a loaded-model observation only while its marker still resolves to this source URL. */
|
|
633
|
+
reportMaterialSlots(id: string, sourceUrl: string, report: EditorMaterialSlotReport): boolean;
|
|
578
634
|
getCatalogDefinitions(): readonly EditorCatalogDefinition[];
|
|
579
635
|
getPerf(): EditorPerfSample | null;
|
|
580
636
|
setPerf(sample: EditorPerfSample): void;
|
|
@@ -586,6 +642,8 @@ export interface EditorHostApi {
|
|
|
586
642
|
getTerrainSampler(): TerrainField | null;
|
|
587
643
|
setTerrainSampler(field: TerrainField | null): void;
|
|
588
644
|
getMode(): EditorRunMode;
|
|
645
|
+
/** Immutable authored snapshot captured on entering an isolated playtest. */
|
|
646
|
+
getPlayDocument(): EditorDocument | null;
|
|
589
647
|
setMode(mode: EditorRunMode): void;
|
|
590
648
|
subscribeMode(listener: (mode: EditorRunMode) => void): () => void;
|
|
591
649
|
/** Play-mode pause/step gate consumed by the runtime publisher. */
|
|
@@ -611,6 +669,10 @@ export declare function createEditorHost(options: {
|
|
|
611
669
|
y: number;
|
|
612
670
|
z: number;
|
|
613
671
|
} | null) => void;
|
|
672
|
+
validateDocument?: (document: EditorDocument) => void;
|
|
673
|
+
isolatedPlaytest?: boolean;
|
|
674
|
+
publishGlobal?: boolean;
|
|
675
|
+
persistAssetUrls?: boolean;
|
|
614
676
|
}): {
|
|
615
677
|
session: EditorSession;
|
|
616
678
|
api: EditorHostApi;
|
package/dist/session.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createDocumentLiveSync, createEditorSession, createRuntimePlayControl, installDocumentLiveSync, listEditorKinds, normalizeEditorLayers, resolveCatalogDefinitions, seedEditorCatalogs, } from "@jgengine/core/editor/index";
|
|
1
|
+
import { createDocumentLiveSync, cloneEditorDocument, createEditorSession, createRuntimePlayControl, installDocumentLiveSync, listEditorKinds, normalizeEditorLayers, resolveCatalogDefinitions, seedEditorCatalogs, } from "@jgengine/core/editor/index";
|
|
2
2
|
import { registerBuiltinSceneKinds } from "@jgengine/core/scene/builtinSceneKinds";
|
|
3
3
|
import { EDITOR_RPC_HANDLERS } from "./handlers/index.js";
|
|
4
4
|
import { warmNavBake } from "./handlers/navmesh.js";
|
|
@@ -26,9 +26,16 @@ export function createEditorHost(options) {
|
|
|
26
26
|
warmNavBake();
|
|
27
27
|
const catalogDefinitions = options.catalogs ?? [];
|
|
28
28
|
const document = seedEditorCatalogs(normalizeEditorLayers(options.layers), catalogDefinitions);
|
|
29
|
-
|
|
29
|
+
let mode = "edit";
|
|
30
|
+
const session = createEditorSession(document, 100, (candidate) => {
|
|
31
|
+
if (options.isolatedPlaytest && mode === "play")
|
|
32
|
+
throw new Error("Return to editor before changing the authored scene");
|
|
33
|
+
options.validateDocument?.(candidate);
|
|
34
|
+
});
|
|
30
35
|
const liveSync = createDocumentLiveSync(document);
|
|
31
|
-
|
|
36
|
+
let uninstallLiveSync = installDocumentLiveSync(liveSync);
|
|
37
|
+
let playDocument = null;
|
|
38
|
+
let playSync = null;
|
|
32
39
|
let lastMirroredDocument = session.getState().document;
|
|
33
40
|
const unsubscribeSessionMirror = session.subscribe((state) => {
|
|
34
41
|
if (state.document === lastMirroredDocument)
|
|
@@ -39,9 +46,17 @@ export function createEditorHost(options) {
|
|
|
39
46
|
let visibility = {};
|
|
40
47
|
let focusTarget = null;
|
|
41
48
|
let assets = [...(options.assets ?? [])];
|
|
49
|
+
const materialSlotReports = new Map();
|
|
50
|
+
const materialSource = (id) => {
|
|
51
|
+
const marker = session.getState().document.markers.find((item) => item.id === id);
|
|
52
|
+
if (!marker)
|
|
53
|
+
return { reason: `No authored model marker ${id}.` };
|
|
54
|
+
const catalogId = marker.catalogId ?? marker.meta?.catalogId;
|
|
55
|
+
const sourceUrl = assets.find((asset) => asset.id === catalogId)?.url;
|
|
56
|
+
return sourceUrl ? { sourceUrl } : { reason: `Marker ${id} has no URL-backed catalog model.` };
|
|
57
|
+
};
|
|
42
58
|
let perf = null;
|
|
43
59
|
let terrainSampler = null;
|
|
44
|
-
let mode = "edit";
|
|
45
60
|
let playControl = createRuntimePlayControl(false);
|
|
46
61
|
const visibilityListeners = new Set();
|
|
47
62
|
const focusListeners = new Set();
|
|
@@ -72,7 +87,7 @@ export function createEditorHost(options) {
|
|
|
72
87
|
const api = {
|
|
73
88
|
gameId: options.gameId,
|
|
74
89
|
getSession: () => session,
|
|
75
|
-
getLiveSync: () => liveSync,
|
|
90
|
+
getLiveSync: () => playSync ?? liveSync,
|
|
76
91
|
getVisibility: () => visibility,
|
|
77
92
|
setVisibility(next) {
|
|
78
93
|
visibility = { ...next };
|
|
@@ -101,6 +116,31 @@ export function createEditorHost(options) {
|
|
|
101
116
|
getAssets: () => assets,
|
|
102
117
|
setAssets(next) {
|
|
103
118
|
assets = [...next];
|
|
119
|
+
for (const [id, inventory] of materialSlotReports) {
|
|
120
|
+
if (materialSource(id).sourceUrl !== inventory.sourceUrl)
|
|
121
|
+
materialSlotReports.delete(id);
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
getMaterialSlots(id) {
|
|
125
|
+
const source = materialSource(id);
|
|
126
|
+
const inventory = materialSlotReports.get(id);
|
|
127
|
+
if (source.sourceUrl === undefined || inventory?.sourceUrl !== source.sourceUrl) {
|
|
128
|
+
materialSlotReports.delete(id);
|
|
129
|
+
return { id, ...source, status: "unavailable", reason: source.reason ?? "This model has no loaded slot inventory. Select it and open the Materials workspace." };
|
|
130
|
+
}
|
|
131
|
+
return structuredClone(inventory);
|
|
132
|
+
},
|
|
133
|
+
reportMaterialSlots(id, sourceUrl, report) {
|
|
134
|
+
if (materialSource(id).sourceUrl !== sourceUrl)
|
|
135
|
+
return false;
|
|
136
|
+
const previous = materialSlotReports.get(id);
|
|
137
|
+
if (previous?.sourceUrl === sourceUrl && previous.status !== "loading" && report.status === "loading")
|
|
138
|
+
return true;
|
|
139
|
+
materialSlotReports.delete(id);
|
|
140
|
+
materialSlotReports.set(id, structuredClone({ ...report, id, sourceUrl }));
|
|
141
|
+
if (materialSlotReports.size > 64)
|
|
142
|
+
materialSlotReports.delete(materialSlotReports.keys().next().value);
|
|
143
|
+
return true;
|
|
104
144
|
},
|
|
105
145
|
getCatalogDefinitions: () => resolveCatalogDefinitions(session.getState().document, catalogDefinitions),
|
|
106
146
|
getPerf: () => perf,
|
|
@@ -112,9 +152,16 @@ export function createEditorHost(options) {
|
|
|
112
152
|
terrainSampler = field;
|
|
113
153
|
},
|
|
114
154
|
getMode: () => mode,
|
|
155
|
+
getPlayDocument: () => playDocument === null ? null : cloneEditorDocument(playDocument),
|
|
115
156
|
setMode(next) {
|
|
116
157
|
if (next === mode)
|
|
117
158
|
return;
|
|
159
|
+
if (options.isolatedPlaytest) {
|
|
160
|
+
uninstallLiveSync();
|
|
161
|
+
playDocument = next === "play" ? cloneEditorDocument(session.getState().document) : null;
|
|
162
|
+
playSync = playDocument === null ? null : createDocumentLiveSync(playDocument);
|
|
163
|
+
uninstallLiveSync = installDocumentLiveSync(playSync ?? liveSync);
|
|
164
|
+
}
|
|
118
165
|
mode = next;
|
|
119
166
|
if (next === "play") {
|
|
120
167
|
playControl = createRuntimePlayControl(false);
|
|
@@ -157,7 +204,8 @@ export function createEditorHost(options) {
|
|
|
157
204
|
const ctx = {
|
|
158
205
|
api,
|
|
159
206
|
session,
|
|
160
|
-
liveSync,
|
|
207
|
+
liveSync: playSync ?? liveSync,
|
|
208
|
+
persistAssetUrls: options.persistAssetUrls,
|
|
161
209
|
gameId: options.gameId,
|
|
162
210
|
catalogDefinitions: defs,
|
|
163
211
|
catalogById: new Map(defs.map((definition) => [definition.id, definition])),
|
|
@@ -181,8 +229,9 @@ export function createEditorHost(options) {
|
|
|
181
229
|
}
|
|
182
230
|
},
|
|
183
231
|
};
|
|
184
|
-
const uninstallHost = installEditorHost(api);
|
|
232
|
+
const uninstallHost = options.publishGlobal === false ? () => { } : installEditorHost(api);
|
|
185
233
|
const dispose = () => {
|
|
234
|
+
materialSlotReports.clear();
|
|
186
235
|
unsubscribeSessionMirror();
|
|
187
236
|
uninstallLiveSync();
|
|
188
237
|
uninstallHost();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { markerCatalogId } from "@jgengine/core/world/authoredObjects";
|
|
4
|
-
import { animationMetaPatch, clearAnimGraph, effectiveAnimGraph,
|
|
4
|
+
import { animationMetaPatch, clearAnimGraph, effectiveAnimGraph, readAnimationSettingResult, setTransitionDuration, storeAnimGraph, } from "../modelAnimationAuthoring.js";
|
|
5
5
|
import { shallowArrayEqual, useStoreSelector } from "../useStoreSelector.js";
|
|
6
6
|
import { graphParamControls, graphTriggers, MAX_PREVIEW_SECONDS, recordTrigger, simulateGraphPreview, } from "./animGraphPreview.js";
|
|
7
7
|
import { initialClipPreviewState } from "./clipPreview.js";
|
|
@@ -38,6 +38,8 @@ export function AnimationGraphPanel({ session, ui, rigged, }) {
|
|
|
38
38
|
const marker = markers.find((entry) => entry.id === id);
|
|
39
39
|
if (marker === undefined)
|
|
40
40
|
continue;
|
|
41
|
+
if (readAnimationSettingResult({ markers }, marker.id).diagnostics.length > 0)
|
|
42
|
+
return { marker, asset: undefined };
|
|
41
43
|
const catalogId = markerCatalogId(marker);
|
|
42
44
|
const asset = catalogId === null ? undefined : rigged.find((entry) => entry.id === catalogId);
|
|
43
45
|
if (asset?.url !== undefined && asset.clips !== undefined)
|
|
@@ -51,9 +53,11 @@ export function AnimationGraphPanel({ session, ui, rigged, }) {
|
|
|
51
53
|
return _jsx(GraphInspector, { session: session, ui: ui, marker: target.marker, asset: target.asset }, target.marker.id);
|
|
52
54
|
}
|
|
53
55
|
function GraphInspector({ session, ui, marker, asset, }) {
|
|
54
|
-
const clips = asset
|
|
55
|
-
const
|
|
56
|
-
const
|
|
56
|
+
const clips = asset?.clips ?? [];
|
|
57
|
+
const document = useStoreSelector(session, (state) => state.document);
|
|
58
|
+
const { setting, diagnostics } = readAnimationSettingResult(document, marker.id);
|
|
59
|
+
const invalid = diagnostics.length > 0;
|
|
60
|
+
const effective = useMemo(() => invalid || asset === undefined ? null : effectiveAnimGraph(setting, clips), [JSON.stringify(setting), invalid, asset, clips]);
|
|
57
61
|
const graph = effective?.graph ?? null;
|
|
58
62
|
const controls = useMemo(() => (graph === null ? [] : graphParamControls(graph)), [graph]);
|
|
59
63
|
const triggerNames = useMemo(() => (graph === null ? [] : graphTriggers(graph)), [graph]);
|
|
@@ -61,6 +65,8 @@ function GraphInspector({ session, ui, marker, asset, }) {
|
|
|
61
65
|
const [playing, setPlaying] = useState(false);
|
|
62
66
|
const [params, setParams] = useState({});
|
|
63
67
|
const [triggers, setTriggers] = useState([]);
|
|
68
|
+
useEffect(() => { if (invalid)
|
|
69
|
+
setPlaying(false); }, [invalid]);
|
|
64
70
|
const durations = useStoreSelector(ui, (state) => state.clipPreview?.clipDurations);
|
|
65
71
|
const frame = useMemo(() => {
|
|
66
72
|
if (graph === null)
|
|
@@ -71,7 +77,7 @@ function GraphInspector({ session, ui, marker, asset, }) {
|
|
|
71
77
|
return simulateGraphPreview({ graph, durations: durations ?? {}, time, params: merged, triggers });
|
|
72
78
|
}, [graph, controls, params, triggers, time, durations]);
|
|
73
79
|
useEffect(() => {
|
|
74
|
-
if (asset
|
|
80
|
+
if (asset?.url === undefined)
|
|
75
81
|
return;
|
|
76
82
|
const current = ui.getState().clipPreview;
|
|
77
83
|
const sameAsset = current?.source.assetId === asset.id;
|
|
@@ -87,18 +93,18 @@ function GraphInspector({ session, ui, marker, asset, }) {
|
|
|
87
93
|
if (ui.getState().clipPreview?.source.assetId === asset.id)
|
|
88
94
|
ui.patch({ clipPreview: null });
|
|
89
95
|
};
|
|
90
|
-
}, [asset
|
|
96
|
+
}, [asset?.id, asset?.url, asset?.label, clips, ui]);
|
|
91
97
|
useEffect(() => {
|
|
92
98
|
const current = ui.getState().clipPreview;
|
|
93
|
-
if (current === null || current.source.assetId !== asset.id)
|
|
99
|
+
if (asset === undefined || current === null || current.source.assetId !== asset.id)
|
|
94
100
|
return;
|
|
95
101
|
if (graph === null || frame === null) {
|
|
96
102
|
if (current.graphPose !== undefined)
|
|
97
103
|
ui.patch({ clipPreview: { ...current, graphPose: undefined } });
|
|
98
104
|
return;
|
|
99
105
|
}
|
|
100
|
-
ui.patch({ clipPreview: { ...current, graphPose: { graph, clips: frame.clips } } });
|
|
101
|
-
}, [graph, frame, asset
|
|
106
|
+
ui.patch({ clipPreview: { ...current, graphPose: { graph, clips: frame.clips, ...(frame.rootMotion === true ? { rootMotion: true } : {}) } } });
|
|
107
|
+
}, [graph, frame, asset?.id, ui, durations]);
|
|
102
108
|
const timeRef = useRef(time);
|
|
103
109
|
timeRef.current = time;
|
|
104
110
|
useEffect(() => {
|
|
@@ -121,7 +127,9 @@ function GraphInspector({ session, ui, marker, asset, }) {
|
|
|
121
127
|
return () => cancelAnimationFrame(handle);
|
|
122
128
|
}, [playing]);
|
|
123
129
|
const commit = (next) => session.dispatch({ type: "setMarker", id: marker.id, patch: { meta: { ...marker.meta, ...animationMetaPatch(next) } } }, { coalesce: `animation:${marker.id}` });
|
|
124
|
-
if (
|
|
130
|
+
if (invalid)
|
|
131
|
+
return (_jsxs("div", { className: "space-y-2 p-3 text-[11px]", children: [_jsxs("div", { role: "alert", "aria-label": "Animation diagnostics", className: "space-y-1 text-amber-200", children: [_jsx("p", { children: "The saved animation override is invalid. Its graph cannot be previewed or edited until repaired or replaced." }), diagnostics.map((entry) => _jsxs("div", { children: [_jsxs("div", { className: "break-all", children: [entry.path, ": ", entry.message] }), _jsx("div", { children: entry.repair })] }, `${entry.path}:${entry.message}`))] }), _jsxs("div", { className: "flex gap-2", children: [_jsx("button", { type: "button", className: SMALL_BUTTON, onClick: () => commit(undefined), children: "Remove invalid animation override" }), _jsx("button", { type: "button", className: SMALL_BUTTON, onClick: () => commit("auto"), children: "Replace invalid override with Auto" })] }), _jsx("p", { className: "text-neutral-500", children: "Replacement discards the invalid override and can be undone. Repair its saved fields through the existing marker metadata API to retain the intended graph." })] }));
|
|
132
|
+
if (graph === null || effective === null || frame === null || asset === undefined) {
|
|
125
133
|
return (_jsx(EmptyState, { icon: "film", title: "No graph", badge: marker.label ?? marker.id, description: "This placement plays no graph: its animation is off, a single clip, or the rig has no idle clip. Pick Custom or Auto in the inspector's Animation section." }));
|
|
126
134
|
}
|
|
127
135
|
const weights = new Map(frame.clips.map((clip) => [`${clip.layer}:${clip.clip}`, clip.weight]));
|
|
@@ -12,8 +12,9 @@ export declare function usePlayControl(api: EditorHostApi): RuntimePlayControl;
|
|
|
12
12
|
* controls (`runtime_pause` / `runtime_resume` / `runtime_step`); Exit returns to edit.
|
|
13
13
|
* @internal
|
|
14
14
|
*/
|
|
15
|
-
export declare function PlayModeBar({ gameId, api, onExit, }: {
|
|
15
|
+
export declare function PlayModeBar({ gameId, api, onExit, exitLabel, }: {
|
|
16
16
|
gameId: string;
|
|
17
17
|
api: EditorHostApi;
|
|
18
18
|
onExit: () => void;
|
|
19
|
+
exitLabel?: string;
|
|
19
20
|
}): import("react").JSX.Element;
|
|
@@ -19,7 +19,7 @@ export function usePlayControl(api) {
|
|
|
19
19
|
* controls (`runtime_pause` / `runtime_resume` / `runtime_step`); Exit returns to edit.
|
|
20
20
|
* @internal
|
|
21
21
|
*/
|
|
22
|
-
export function PlayModeBar({ gameId, api, onExit, }) {
|
|
22
|
+
export function PlayModeBar({ gameId, api, onExit, exitLabel = "Editor", }) {
|
|
23
23
|
const play = usePlayControl(api);
|
|
24
24
|
const pause = () => {
|
|
25
25
|
api.handle({ method: "runtime_pause" });
|
|
@@ -32,5 +32,5 @@ export function PlayModeBar({ gameId, api, onExit, }) {
|
|
|
32
32
|
};
|
|
33
33
|
return (_jsxs("header", { className: `pointer-events-auto relative z-[60] flex h-12 shrink-0 items-center gap-2 border-b ${BORDER} bg-[#111318] px-2.5`, role: "banner", "aria-label": "Play mode", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-2.5", children: [_jsx("div", { className: "flex h-7 w-7 shrink-0 items-center justify-center rounded-[6px] bg-gradient-to-br from-emerald-500/30 to-cyan-500/25 text-[11px] font-bold tracking-wide text-emerald-200 ring-1 ring-inset ring-emerald-400/30", "aria-hidden": "true", children: "JG" }), _jsxs("div", { className: "min-w-0 leading-tight", children: [_jsx("div", { className: "truncate text-[13px] font-semibold text-neutral-100", children: gameId }), _jsx("div", { className: "text-[10px] text-neutral-500", children: "Play mode" })] }), _jsxs("span", { className: `ml-1 hidden items-center gap-1.5 rounded-full px-2 py-0.5 text-[10px] sm:flex ${play.paused
|
|
34
34
|
? "bg-amber-500/15 text-amber-200 ring-1 ring-inset ring-amber-400/25"
|
|
35
|
-
: "bg-emerald-500/15 text-emerald-200 ring-1 ring-inset ring-emerald-400/25"}`, children: [_jsx("span", { className: `h-1.5 w-1.5 rounded-full ${play.paused ? "bg-amber-400" : "bg-emerald-400 animate-pulse"}` }), play.paused ? "Paused" : "Playing"] })] }), _jsxs("div", { className: "ml-auto flex shrink-0 items-center gap-1", children: [_jsx(IconButton, { icon: play.paused ? "play" : "pause", label: play.paused ? "Resume simulation" : "Pause simulation", onClick: () => (play.paused ? resume() : pause()), active: play.paused }), _jsx(IconButton, { icon: "step", label: "Step one frame (pause required)", onClick: step, disabled: !play.paused }), _jsx(ToolbarDivider, {}), _jsxs("button", { type: "button", onClick: onExit, title: "Return to the editor (F2+E)", className: `flex h-8 items-center gap-1.5 rounded-[6px] border border-white/[0.07] bg-[#191d24] px-3 text-[12px] font-medium text-neutral-200 transition-colors hover:bg-[#1f242d] ${FOCUS_RING}`, children: [_jsx(Icon, { name: "panel", size: 12 }),
|
|
35
|
+
: "bg-emerald-500/15 text-emerald-200 ring-1 ring-inset ring-emerald-400/25"}`, children: [_jsx("span", { className: `h-1.5 w-1.5 rounded-full ${play.paused ? "bg-amber-400" : "bg-emerald-400 animate-pulse"}` }), play.paused ? "Paused" : "Playing"] })] }), _jsxs("div", { className: "ml-auto flex shrink-0 items-center gap-1", children: [_jsx(IconButton, { icon: play.paused ? "play" : "pause", label: play.paused ? "Resume simulation" : "Pause simulation", onClick: () => (play.paused ? resume() : pause()), active: play.paused }), _jsx(IconButton, { icon: "step", label: "Step one frame (pause required)", onClick: step, disabled: !play.paused }), _jsx(ToolbarDivider, {}), _jsxs("button", { type: "button", onClick: onExit, title: "Return to the editor (F2+E)", className: `flex h-8 items-center gap-1.5 rounded-[6px] border border-white/[0.07] bg-[#191d24] px-3 text-[12px] font-medium text-neutral-200 transition-colors hover:bg-[#1f242d] ${FOCUS_RING}`, children: [_jsx(Icon, { name: "panel", size: 12 }), exitLabel] })] })] }));
|
|
36
36
|
}
|
|
@@ -8,7 +8,7 @@ export declare const ADD_VOLUME_ENTRIES: readonly {
|
|
|
8
8
|
* Contextual scene toolbar under the app bar: tools, gizmo modes, gizmo space, pivot,
|
|
9
9
|
* snapping, viewport overlays, framing, projection, and the Add menu.
|
|
10
10
|
*/
|
|
11
|
-
export declare function SceneToolbar({ tool, gizmoMode, gizmoSpace, gizmoPivot, snapMode, gridSize, rotationSnapDeg, scaleSnap, cameraProjection, showGrid, showContours, showSurfaceGrid, showElevation, placementActive, onSetTool, onSetGizmoMode, onSetGizmoSpace, onSetGizmoPivot, onSetSnapMode, onSetGridSize, onSetRotationSnapDeg, onSetScaleSnap, onSetCameraProjection, onToggleGrid, onToggleContours, onToggleSurfaceGrid, onToggleElevation, onFrame, onStartPlacement, onOpenAssistant, assistantOpen, }: {
|
|
11
|
+
export declare function SceneToolbar({ tool, gizmoMode, gizmoSpace, gizmoPivot, snapMode, gridSize, rotationSnapDeg, scaleSnap, cameraProjection, showGrid, showContours, showSurfaceGrid, showElevation, placementActive, onSetTool, onSetGizmoMode, onSetGizmoSpace, onSetGizmoPivot, onSetSnapMode, onSetGridSize, onSetRotationSnapDeg, onSetScaleSnap, onSetCameraProjection, onToggleGrid, onToggleContours, onToggleSurfaceGrid, onToggleElevation, onFrame, onStartPlacement, onOpenAssistant, assistantOpen, allowedKinds, }: {
|
|
12
12
|
tool: EditorTool;
|
|
13
13
|
gizmoMode: GizmoMode;
|
|
14
14
|
gizmoSpace: GizmoSpace;
|
|
@@ -40,4 +40,5 @@ export declare function SceneToolbar({ tool, gizmoMode, gizmoSpace, gizmoPivot,
|
|
|
40
40
|
onStartPlacement: (tool: PlacementTool) => void;
|
|
41
41
|
onOpenAssistant: () => void;
|
|
42
42
|
assistantOpen: boolean;
|
|
43
|
+
allowedKinds?: readonly string[];
|
|
43
44
|
}): import("react").JSX.Element;
|
|
@@ -70,7 +70,7 @@ const PIVOT_LABEL = {
|
|
|
70
70
|
* Contextual scene toolbar under the app bar: tools, gizmo modes, gizmo space, pivot,
|
|
71
71
|
* snapping, viewport overlays, framing, projection, and the Add menu.
|
|
72
72
|
*/
|
|
73
|
-
export function SceneToolbar({ tool, gizmoMode, gizmoSpace, gizmoPivot, snapMode, gridSize, rotationSnapDeg, scaleSnap, cameraProjection, showGrid, showContours, showSurfaceGrid, showElevation, placementActive, onSetTool, onSetGizmoMode, onSetGizmoSpace, onSetGizmoPivot, onSetSnapMode, onSetGridSize, onSetRotationSnapDeg, onSetScaleSnap, onSetCameraProjection, onToggleGrid, onToggleContours, onToggleSurfaceGrid, onToggleElevation, onFrame, onStartPlacement, onOpenAssistant, assistantOpen, }) {
|
|
73
|
+
export function SceneToolbar({ tool, gizmoMode, gizmoSpace, gizmoPivot, snapMode, gridSize, rotationSnapDeg, scaleSnap, cameraProjection, showGrid, showContours, showSurfaceGrid, showElevation, placementActive, onSetTool, onSetGizmoMode, onSetGizmoSpace, onSetGizmoPivot, onSetSnapMode, onSetGridSize, onSetRotationSnapDeg, onSetScaleSnap, onSetCameraProjection, onToggleGrid, onToggleContours, onToggleSurfaceGrid, onToggleElevation, onFrame, onStartPlacement, onOpenAssistant, assistantOpen, allowedKinds, }) {
|
|
74
74
|
const [addOpen, setAddOpen] = useState(false);
|
|
75
75
|
const [snapOpen, setSnapOpen] = useState(false);
|
|
76
76
|
const [pivotOpen, setPivotOpen] = useState(false);
|
|
@@ -79,7 +79,8 @@ export function SceneToolbar({ tool, gizmoMode, gizmoSpace, gizmoPivot, snapMode
|
|
|
79
79
|
lastSnapRef.current = snapMode;
|
|
80
80
|
// Recomputed every render (opening the menu rerenders): studios may register after the chrome
|
|
81
81
|
// mounts (game modules load lazily), and a once-on-mount snapshot would silently hide them.
|
|
82
|
-
const
|
|
82
|
+
const allowed = (kind) => allowedKinds === undefined || allowedKinds.includes(kind);
|
|
83
|
+
const studioKinds = listSceneKinds().filter((definition) => definition.addCategory !== undefined && allowed(definition.kind));
|
|
83
84
|
const pick = (placement) => {
|
|
84
85
|
setAddOpen(false);
|
|
85
86
|
onStartPlacement(placement);
|
|
@@ -112,7 +113,7 @@ export function SceneToolbar({ tool, gizmoMode, gizmoSpace, gizmoPivot, snapMode
|
|
|
112
113
|
? "border-cyan-400/30 bg-cyan-500/15 text-cyan-100"
|
|
113
114
|
: "border-white/[0.07] bg-[#191d24] text-neutral-300 hover:bg-[#1f242d]"}`, children: [_jsx(Icon, { name: "camera", size: 12 }), cameraProjection === "perspective" ? "Persp" : "Ortho"] }), _jsx(ToolbarDivider, {}), _jsxs("div", { className: "relative", children: [_jsxs("button", { type: "button", onClick: () => setAddOpen((value) => !value), "aria-expanded": addOpen, className: `flex h-7 items-center gap-1.5 rounded-[5px] px-2.5 text-[12px] font-medium transition-colors ${FOCUS_RING} ${addOpen || placementActive
|
|
114
115
|
? "bg-cyan-600 text-white shadow-sm shadow-cyan-950/40"
|
|
115
|
-
: "border border-white/[0.07] bg-[#191d24] text-neutral-200 hover:bg-[#1f242d]"}`, children: [_jsx(Icon, { name: "plus", size: 12 }), "Add"] }), _jsxs(MenuShell, { open: addOpen, onClose: () => setAddOpen(false), children: [_jsx("div", { className: `px-2 pb-1 pt-1.5 ${MICRO_LABEL}`, children: "Markers" }), WELL_KNOWN_MARKER_KINDS.map((kind) => (_jsx("button", { type: "button", className: MENU_ITEM, onClick: () => pick({ tool: "marker", kind }), children: kind }, kind))), _jsx("div", { className: `px-2 pb-1 pt-2 ${MICRO_LABEL}`, children: "Volumes" }), ADD_VOLUME_ENTRIES.map((entry) => (_jsx("button", { type: "button", className: MENU_ITEM, onClick: () => pick(entry.tool), children: entry.label }, entry.label))), [...studioKinds.reduce((groups, definition) => {
|
|
116
|
+
: "border border-white/[0.07] bg-[#191d24] text-neutral-200 hover:bg-[#1f242d]"}`, children: [_jsx(Icon, { name: "plus", size: 12 }), "Add"] }), _jsxs(MenuShell, { open: addOpen, onClose: () => setAddOpen(false), children: [_jsx("div", { className: `px-2 pb-1 pt-1.5 ${MICRO_LABEL}`, children: "Markers" }), WELL_KNOWN_MARKER_KINDS.filter(allowed).map((kind) => (_jsx("button", { type: "button", className: MENU_ITEM, onClick: () => pick({ tool: "marker", kind }), children: kind }, kind))), _jsx("div", { className: `px-2 pb-1 pt-2 ${MICRO_LABEL}`, children: "Volumes" }), ADD_VOLUME_ENTRIES.filter((entry) => ("kind" in entry.tool && allowed(entry.tool.kind))).map((entry) => (_jsx("button", { type: "button", className: MENU_ITEM, onClick: () => pick(entry.tool), children: entry.label }, entry.label))), [...studioKinds.reduce((groups, definition) => {
|
|
116
117
|
const category = definition.addCategory ?? "Studios";
|
|
117
118
|
(groups.get(category) ?? groups.set(category, []).get(category)).push(definition);
|
|
118
119
|
return groups;
|
|
@@ -120,7 +121,7 @@ export function SceneToolbar({ tool, gizmoMode, gizmoSpace, gizmoPivot, snapMode
|
|
|
120
121
|
? { tool: "path", kind: definition.kind }
|
|
121
122
|
: definition.target === "volume"
|
|
122
123
|
? { tool: "volume", kind: definition.kind, shape: "box" }
|
|
123
|
-
: { tool: "marker", kind: definition.kind }), children: [definition.label, definition.target === "path" ? (definition.pathShape === "line" ? " (draw line)" : " (lasso)") : ""] }, definition.kind)))] }, category))), _jsx("div", { className: `px-2 pb-1 pt-2 ${MICRO_LABEL}`, children: "Paths & notes" }), _jsx("button", { type: "button", className: MENU_ITEM, onClick: () => pick({ tool: "path", kind: "route" }), children: "Draw path (route)" }), _jsx("button", { type: "button", className: MENU_ITEM, onClick: () => pick({ tool: "path", kind: "road" }), children: "Draw road" }), _jsx("button", { type: "button", className: MENU_ITEM, onClick: () => pick({ tool: "note" }), children: "Note" })] })] }), _jsx("div", { className: "ml-auto flex items-center gap-1", children: _jsxs("button", { type: "button", onClick: onOpenAssistant, className: `flex h-7 items-center gap-1.5 rounded-[5px] px-2.5 text-[12px] transition-colors ${FOCUS_RING} ${assistantOpen
|
|
124
|
+
: { tool: "marker", kind: definition.kind }), children: [definition.label, definition.target === "path" ? (definition.pathShape === "line" ? " (draw line)" : " (lasso)") : ""] }, definition.kind)))] }, category))), _jsx("div", { className: `px-2 pb-1 pt-2 ${MICRO_LABEL}`, children: "Paths & notes" }), _jsx("button", { type: "button", disabled: !allowed("route"), className: MENU_ITEM, onClick: () => pick({ tool: "path", kind: "route" }), children: "Draw path (route)" }), _jsx("button", { type: "button", disabled: !allowed("road"), className: MENU_ITEM, onClick: () => pick({ tool: "path", kind: "road" }), children: "Draw road" }), _jsx("button", { type: "button", className: MENU_ITEM, onClick: () => pick({ tool: "note" }), children: "Note" })] })] }), _jsx("div", { className: "ml-auto flex items-center gap-1", children: _jsxs("button", { type: "button", onClick: onOpenAssistant, className: `flex h-7 items-center gap-1.5 rounded-[5px] px-2.5 text-[12px] transition-colors ${FOCUS_RING} ${assistantOpen
|
|
124
125
|
? "border border-violet-400/40 bg-violet-500/20 text-violet-100"
|
|
125
126
|
: "border border-white/[0.07] bg-[#191d24] text-neutral-300 hover:bg-[#1f242d] hover:text-neutral-100"}`, title: "AI assistant \u2014 drives the scene through the same RPC and undo as the toolbar", children: [_jsx(Icon, { name: "sparkle", size: 13 }), "AI"] }) })] }));
|
|
126
127
|
}
|