@jgengine/editor 0.18.1 → 0.21.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 +504 -7
- package/dist/ClipPreviewLayer.d.ts +2 -3
- package/dist/ClipPreviewLayer.js +24 -26
- 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 +61 -35
- package/dist/InspectorPanel.d.ts +3 -1
- package/dist/InspectorPanel.js +12 -8
- package/dist/LightingPanel.js +5 -1
- 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 +3 -1
- 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 +31 -1
- package/dist/handlers/hierarchy.d.ts +1 -1
- package/dist/handlers/hierarchy.js +4 -0
- package/dist/handlers/navmesh.d.ts +2 -0
- package/dist/handlers/navmesh.js +16 -4
- 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 +13 -4
- 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 +13 -4
- package/dist/modelAnimationAuthoring.d.ts +49 -6
- package/dist/modelAnimationAuthoring.js +118 -64
- package/dist/session.d.ts +64 -0
- package/dist/session.js +58 -7
- package/dist/shell/AnimationGraphPanel.d.ts +16 -0
- package/dist/shell/AnimationGraphPanel.js +180 -0
- package/dist/shell/AnimationPanel.d.ts +3 -1
- package/dist/shell/AnimationPanel.js +9 -7
- 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/WorkspaceRail.js +1 -1
- package/dist/shell/animGraphPreview.d.ts +58 -0
- package/dist/shell/animGraphPreview.js +95 -0
- package/dist/shell/clipPreview.d.ts +9 -0
- package/dist/useDocumentSave.d.ts +20 -0
- package/dist/useDocumentSave.js +33 -0
- package/package.json +6 -5
- package/skills/ce-handoff/SKILL.md +2 -2
- package/skills/jgengine/SKILL.md +28 -6
- package/skills/jgengine/api.md +139 -99
- package/skills/jgengine/capabilities.md +28 -0
- package/skills/jgengine/recipes/minimal-game.md +10 -2
- package/skills/jgengine-editor/SKILL.md +12 -2
- package/skills/jgengine-editor/api.md +120 -16
- package/skills/jgengine-editor/capabilities.md +40 -0
- package/skills/jgengine-editor/reference.md +161 -3
- package/skills/jgengine-editor/references/live-sync.md +15 -0
- package/skills/level-design/SKILL.md +1 -1
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { AnimGraph } from "@jgengine/core/anim/animGraph";
|
|
2
|
+
import { type AuthoredAnimationDiagnostic, type AuthoredAnimationDocumentLike } from "@jgengine/core/world/authoredAnimation";
|
|
1
3
|
/**
|
|
2
4
|
* Authoring model for a placement's `ModelConfig.animation`, persisted on a marker's `meta.animation`
|
|
3
5
|
* and consumed by the shell exactly like a game-authored config. Pure data + reducers, no JSX — the
|
|
@@ -38,18 +40,29 @@ export interface AuthoredAnimationStates {
|
|
|
38
40
|
}
|
|
39
41
|
/** Structurally compatible with `ModelAnimationConfig`, but every field optional for authoring. */
|
|
40
42
|
export interface AuthoredAnimationConfig {
|
|
43
|
+
auto?: true;
|
|
44
|
+
clock?: "real" | "game";
|
|
41
45
|
clip?: string;
|
|
42
46
|
loop?: boolean;
|
|
43
47
|
timeScale?: number;
|
|
48
|
+
paused?: boolean;
|
|
49
|
+
time?: number;
|
|
44
50
|
states?: AuthoredAnimationStates;
|
|
45
|
-
oneShots?: Record<string, string>;
|
|
51
|
+
oneShots?: Record<string, string | readonly string[]>;
|
|
52
|
+
/** A stored animation graph; takes over from `states`/`oneShots` at play time. */
|
|
53
|
+
graph?: AnimGraph;
|
|
46
54
|
}
|
|
47
55
|
/** The value stored at `meta.animation`. */
|
|
48
56
|
export type AnimationSetting = AuthoredAnimationConfig | "auto" | "none";
|
|
49
57
|
/** Which authoring mode a stored setting represents. `default` = no override key. */
|
|
50
58
|
export type AnimationMode = "default" | "auto" | "none" | "custom";
|
|
51
|
-
/** Reads
|
|
59
|
+
/** Reads a whole valid override, retaining authored fields; malformed values return undefined. */
|
|
52
60
|
export declare function readAnimationSetting(meta: Record<string, unknown> | undefined): AnimationSetting | undefined;
|
|
61
|
+
/** Validates a placement's animation with its saved-document repair locations. @internal */
|
|
62
|
+
export declare function readAnimationSettingResult(document: AuthoredAnimationDocumentLike, markerId: string): {
|
|
63
|
+
setting: AnimationSetting | undefined;
|
|
64
|
+
diagnostics: readonly AuthoredAnimationDiagnostic[];
|
|
65
|
+
};
|
|
53
66
|
/** The authoring mode a stored setting maps to. */
|
|
54
67
|
export declare function animationMode(setting: AnimationSetting | undefined): AnimationMode;
|
|
55
68
|
/**
|
|
@@ -61,11 +74,21 @@ export declare function setAnimationMode(setting: AnimationSetting | undefined,
|
|
|
61
74
|
/** A concrete custom config derived from a rigged asset's clip roles — the `custom` mode seed. */
|
|
62
75
|
export declare function defaultCustomConfig(clips: readonly string[]): AuthoredAnimationConfig;
|
|
63
76
|
/** Sets or clears (null) a locomotion state clip; forces the setting into `custom`. */
|
|
64
|
-
export declare function setLocomotionClip(setting: AnimationSetting | undefined, role: LocomotionRole, clipName: string | null): AuthoredAnimationConfig;
|
|
77
|
+
export declare function setLocomotionClip(setting: AnimationSetting | undefined, role: LocomotionRole, clipName: string | null, clips?: readonly string[]): AuthoredAnimationConfig;
|
|
65
78
|
/** Sets or clears (null) a numeric locomotion tuning. */
|
|
66
|
-
export declare function setLocomotionNumber(setting: AnimationSetting | undefined, key: LocomotionNumber, value: number | null): AuthoredAnimationConfig;
|
|
67
|
-
/** Binds or clears (null) a one-shot event
|
|
68
|
-
export declare function setOneShotClip(setting: AnimationSetting | undefined, event: string, clipName: string | null): AuthoredAnimationConfig;
|
|
79
|
+
export declare function setLocomotionNumber(setting: AnimationSetting | undefined, key: LocomotionNumber, value: number | null, clips?: readonly string[]): AuthoredAnimationConfig;
|
|
80
|
+
/** Binds or clears (null) a one-shot event's clip or variant list. */
|
|
81
|
+
export declare function setOneShotClip(setting: AnimationSetting | undefined, event: string, clipName: string | readonly string[] | null): AuthoredAnimationConfig;
|
|
82
|
+
/** Sets a single playback clip, explicitly replacing speed-driven states and a stored graph. @internal */
|
|
83
|
+
export declare function setPlaybackClip(setting: AnimationSetting | undefined, clip: string | null): AuthoredAnimationConfig;
|
|
84
|
+
/** Changes a playback scalar while preserving role mappings, variants and the stored graph. @internal */
|
|
85
|
+
export declare function setPlaybackNumber(setting: AnimationSetting | undefined, key: "time" | "timeScale", value: number | null): AuthoredAnimationConfig;
|
|
86
|
+
/** Chooses the playback clock without replacing mappings, graph, or other authored fields. @internal */
|
|
87
|
+
export declare function setPlaybackClock(setting: AnimationSetting | undefined, clock: "real" | "game"): AuthoredAnimationConfig;
|
|
88
|
+
/** Requests loaded-rig role inference, or removes that request, retaining explicit overrides whole. @internal */
|
|
89
|
+
export declare function setAutomaticRoles(setting: AnimationSetting | undefined, derive: boolean): AuthoredAnimationConfig;
|
|
90
|
+
/** Changes a playback toggle without replacing the remaining authored settings. @internal */
|
|
91
|
+
export declare function setPlaybackBoolean(setting: AnimationSetting | undefined, key: "paused" | "loop", value: boolean): AuthoredAnimationConfig;
|
|
69
92
|
/**
|
|
70
93
|
* The meta patch that persists a setting (or clears the override when undefined). Shallow-merges onto
|
|
71
94
|
* `marker.meta` via `setMarker`; an undefined value drops out of the saved JSON document.
|
|
@@ -73,3 +96,23 @@ export declare function setOneShotClip(setting: AnimationSetting | undefined, ev
|
|
|
73
96
|
export declare function animationMetaPatch(setting: AnimationSetting | undefined): {
|
|
74
97
|
animation: AnimationSetting | undefined;
|
|
75
98
|
};
|
|
99
|
+
/** Where the graph a placement plays comes from. */
|
|
100
|
+
export type AnimGraphSource = "authored" | "locomotion" | "auto";
|
|
101
|
+
/**
|
|
102
|
+
* The graph a placement plays at run time: its stored `graph`, else the locomotion graph of its
|
|
103
|
+
* custom `states`/`oneShots`, else the one clip roles derive for `"auto"` and no-override placements.
|
|
104
|
+
* `null` for `"none"`, a single-clip config, or a rig whose clips have no idle.
|
|
105
|
+
*/
|
|
106
|
+
export declare function effectiveAnimGraph(setting: AnimationSetting | undefined, clips: readonly string[]): {
|
|
107
|
+
graph: AnimGraph;
|
|
108
|
+
source: AnimGraphSource;
|
|
109
|
+
} | null;
|
|
110
|
+
/** Stores `graph` on the placement, keeping its other fields; the stored graph wins at play time. */
|
|
111
|
+
export declare function storeAnimGraph(setting: AnimationSetting | undefined, graph: AnimGraph): AuthoredAnimationConfig;
|
|
112
|
+
/** Removes a stored graph so `states`/`oneShots` (or clip roles) drive the placement again. */
|
|
113
|
+
export declare function clearAnimGraph(setting: AnimationSetting | undefined): AnimationSetting | undefined;
|
|
114
|
+
/**
|
|
115
|
+
* Sets one transition's crossfade seconds in `graph` and stores the result, so editing a derived
|
|
116
|
+
* graph turns it into an authored one.
|
|
117
|
+
*/
|
|
118
|
+
export declare function setTransitionDuration(setting: AnimationSetting | undefined, graph: AnimGraph, layerId: string, index: number, duration: number): AuthoredAnimationConfig;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readAuthoredAnimation } from "@jgengine/core/world/authoredAnimation";
|
|
2
|
+
import { animGraphFromConfig } from "@jgengine/core/anim/locomotionGraph";
|
|
3
|
+
import { resolveAnimationConfig, rolesFromClips } from "@jgengine/core/game/clipRoles";
|
|
2
4
|
/**
|
|
3
5
|
* Authoring model for a placement's `ModelConfig.animation`, persisted on a marker's `meta.animation`
|
|
4
6
|
* and consumed by the shell exactly like a game-authored config. Pure data + reducers, no JSX — the
|
|
@@ -22,48 +24,14 @@ export const LOCOMOTION_NUMBERS = ["walkSpeed", "runSpeed", "fadeSec"];
|
|
|
22
24
|
* `combat.hitReaction` / `entity.died`; the rest fire when the game calls `playEntityAnimation`.
|
|
23
25
|
*/
|
|
24
26
|
export const ONE_SHOT_EVENTS = ["attack", "hit", "death", "jump", "interact", "cheer"];
|
|
25
|
-
|
|
26
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
27
|
-
}
|
|
28
|
-
/** 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. */
|
|
29
28
|
export function readAnimationSetting(meta) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (typeof value["clip"] === "string")
|
|
37
|
-
config.clip = value["clip"];
|
|
38
|
-
if (typeof value["loop"] === "boolean")
|
|
39
|
-
config.loop = value["loop"];
|
|
40
|
-
if (typeof value["timeScale"] === "number")
|
|
41
|
-
config.timeScale = value["timeScale"];
|
|
42
|
-
if (isRecord(value["states"])) {
|
|
43
|
-
const raw = value["states"];
|
|
44
|
-
const states = {};
|
|
45
|
-
for (const role of LOCOMOTION_ROLES)
|
|
46
|
-
if (typeof raw[role] === "string")
|
|
47
|
-
states[role] = raw[role];
|
|
48
|
-
for (const key of LOCOMOTION_NUMBERS)
|
|
49
|
-
if (typeof raw[key] === "number")
|
|
50
|
-
states[key] = raw[key];
|
|
51
|
-
if (Object.keys(states).length > 0)
|
|
52
|
-
config.states = states;
|
|
53
|
-
}
|
|
54
|
-
if (isRecord(value["oneShots"])) {
|
|
55
|
-
const raw = value["oneShots"];
|
|
56
|
-
const oneShots = {};
|
|
57
|
-
for (const [event, clip] of Object.entries(raw)) {
|
|
58
|
-
if (typeof clip === "string")
|
|
59
|
-
oneShots[event] = clip;
|
|
60
|
-
else if (Array.isArray(clip) && typeof clip[0] === "string")
|
|
61
|
-
oneShots[event] = clip[0];
|
|
62
|
-
}
|
|
63
|
-
if (Object.keys(oneShots).length > 0)
|
|
64
|
-
config.oneShots = oneShots;
|
|
65
|
-
}
|
|
66
|
-
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 };
|
|
67
35
|
}
|
|
68
36
|
/** The authoring mode a stored setting maps to. */
|
|
69
37
|
export function animationMode(setting) {
|
|
@@ -78,14 +46,6 @@ export function animationMode(setting) {
|
|
|
78
46
|
function asConfig(setting) {
|
|
79
47
|
return setting !== undefined && setting !== "auto" && setting !== "none" ? setting : {};
|
|
80
48
|
}
|
|
81
|
-
function normalizeConfig(config) {
|
|
82
|
-
const next = { ...config };
|
|
83
|
-
if (next.states !== undefined && Object.keys(next.states).length === 0)
|
|
84
|
-
delete next.states;
|
|
85
|
-
if (next.oneShots !== undefined && Object.keys(next.oneShots).length === 0)
|
|
86
|
-
delete next.oneShots;
|
|
87
|
-
return next;
|
|
88
|
-
}
|
|
89
49
|
/**
|
|
90
50
|
* Switches authoring mode. `custom` seeds from the asset's clip-role defaults (so the user starts from
|
|
91
51
|
* a working config, then overrides) when there is no existing object config; the other modes are the
|
|
@@ -100,7 +60,7 @@ export function setAnimationMode(setting, mode, clips = []) {
|
|
|
100
60
|
return "none";
|
|
101
61
|
if (setting !== undefined && setting !== "auto" && setting !== "none")
|
|
102
62
|
return setting;
|
|
103
|
-
return
|
|
63
|
+
return defaultCustomConfig(clips);
|
|
104
64
|
}
|
|
105
65
|
/** A concrete custom config derived from a rigged asset's clip roles — the `custom` mode seed. */
|
|
106
66
|
export function defaultCustomConfig(clips) {
|
|
@@ -114,9 +74,9 @@ export function defaultCustomConfig(clips) {
|
|
|
114
74
|
states.run = roles.run[0];
|
|
115
75
|
const oneShots = {};
|
|
116
76
|
for (const event of ONE_SHOT_EVENTS) {
|
|
117
|
-
const
|
|
118
|
-
if (
|
|
119
|
-
oneShots[event] =
|
|
77
|
+
const variants = roles[event];
|
|
78
|
+
if (variants !== undefined && variants.length > 0)
|
|
79
|
+
oneShots[event] = variants.length === 1 ? variants[0] : [...variants];
|
|
120
80
|
}
|
|
121
81
|
const config = {};
|
|
122
82
|
if (Object.keys(states).length > 0)
|
|
@@ -126,34 +86,72 @@ export function defaultCustomConfig(clips) {
|
|
|
126
86
|
return config;
|
|
127
87
|
}
|
|
128
88
|
/** Sets or clears (null) a locomotion state clip; forces the setting into `custom`. */
|
|
129
|
-
export function setLocomotionClip(setting, role, clipName) {
|
|
89
|
+
export function setLocomotionClip(setting, role, clipName, clips = []) {
|
|
130
90
|
const config = asConfig(setting);
|
|
131
|
-
const states = { ...config.states };
|
|
91
|
+
const states = { ...(config.states ?? (clipName === null ? undefined : defaultCustomConfig(clips).states)) };
|
|
132
92
|
if (clipName === null)
|
|
133
93
|
delete states[role];
|
|
134
94
|
else
|
|
135
95
|
states[role] = clipName;
|
|
136
|
-
return
|
|
96
|
+
return { ...config, ...(config.states !== undefined || Object.keys(states).length > 0 ? { states } : {}) };
|
|
137
97
|
}
|
|
138
98
|
/** Sets or clears (null) a numeric locomotion tuning. */
|
|
139
|
-
export function setLocomotionNumber(setting, key, value) {
|
|
99
|
+
export function setLocomotionNumber(setting, key, value, clips = []) {
|
|
140
100
|
const config = asConfig(setting);
|
|
141
|
-
const states = { ...config.states };
|
|
101
|
+
const states = { ...(config.states ?? (value === null || !Number.isFinite(value) ? undefined : defaultCustomConfig(clips).states)) };
|
|
142
102
|
if (value === null || !Number.isFinite(value))
|
|
143
103
|
delete states[key];
|
|
144
104
|
else
|
|
145
105
|
states[key] = value;
|
|
146
|
-
return
|
|
106
|
+
return { ...config, ...(config.states !== undefined || Object.keys(states).length > 0 ? { states } : {}) };
|
|
147
107
|
}
|
|
148
|
-
/** Binds or clears (null) a one-shot event
|
|
108
|
+
/** Binds or clears (null) a one-shot event's clip or variant list. */
|
|
149
109
|
export function setOneShotClip(setting, event, clipName) {
|
|
150
110
|
const config = asConfig(setting);
|
|
151
111
|
const oneShots = { ...config.oneShots };
|
|
152
|
-
if (clipName === null)
|
|
112
|
+
if (clipName === null || (Array.isArray(clipName) && clipName.length === 0))
|
|
153
113
|
delete oneShots[event];
|
|
154
114
|
else
|
|
155
|
-
oneShots[event] = clipName;
|
|
156
|
-
|
|
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 };
|
|
157
155
|
}
|
|
158
156
|
/**
|
|
159
157
|
* The meta patch that persists a setting (or clears the override when undefined). Shallow-merges onto
|
|
@@ -162,3 +160,59 @@ export function setOneShotClip(setting, event, clipName) {
|
|
|
162
160
|
export function animationMetaPatch(setting) {
|
|
163
161
|
return { animation: setting };
|
|
164
162
|
}
|
|
163
|
+
/**
|
|
164
|
+
* The graph a placement plays at run time: its stored `graph`, else the locomotion graph of its
|
|
165
|
+
* custom `states`/`oneShots`, else the one clip roles derive for `"auto"` and no-override placements.
|
|
166
|
+
* `null` for `"none"`, a single-clip config, or a rig whose clips have no idle.
|
|
167
|
+
*/
|
|
168
|
+
export function effectiveAnimGraph(setting, clips) {
|
|
169
|
+
if (setting === "none")
|
|
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
|
+
}
|
|
176
|
+
if (setting !== undefined && setting !== "auto") {
|
|
177
|
+
if (setting.graph !== undefined)
|
|
178
|
+
return { graph: setting.graph, source: "authored" };
|
|
179
|
+
const states = setting.states;
|
|
180
|
+
if (states?.idle === undefined || states.idle.trim().length === 0)
|
|
181
|
+
return null;
|
|
182
|
+
const graph = animGraphFromConfig({
|
|
183
|
+
states: { ...states, idle: states.idle, walk: states.walk === undefined || states.walk.trim().length === 0 ? states.idle : states.walk },
|
|
184
|
+
...(setting.oneShots === undefined ? {} : { oneShots: setting.oneShots }),
|
|
185
|
+
});
|
|
186
|
+
return graph === undefined ? null : { graph, source: "locomotion" };
|
|
187
|
+
}
|
|
188
|
+
const resolved = resolveAnimationConfig("auto", clips);
|
|
189
|
+
const graph = resolved === undefined ? undefined : animGraphFromConfig(resolved);
|
|
190
|
+
return graph === undefined ? null : { graph, source: "auto" };
|
|
191
|
+
}
|
|
192
|
+
/** Stores `graph` on the placement, keeping its other fields; the stored graph wins at play time. */
|
|
193
|
+
export function storeAnimGraph(setting, graph) {
|
|
194
|
+
return { ...asConfig(setting), graph };
|
|
195
|
+
}
|
|
196
|
+
/** Removes a stored graph so `states`/`oneShots` (or clip roles) drive the placement again. */
|
|
197
|
+
export function clearAnimGraph(setting) {
|
|
198
|
+
if (setting === undefined || setting === "auto" || setting === "none")
|
|
199
|
+
return setting;
|
|
200
|
+
const { graph: _graph, ...rest } = setting;
|
|
201
|
+
return Object.keys(rest).length === 0 ? undefined : rest;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Sets one transition's crossfade seconds in `graph` and stores the result, so editing a derived
|
|
205
|
+
* graph turns it into an authored one.
|
|
206
|
+
*/
|
|
207
|
+
export function setTransitionDuration(setting, graph, layerId, index, duration) {
|
|
208
|
+
const next = {
|
|
209
|
+
...graph,
|
|
210
|
+
layers: graph.layers.map((layer) => layer.id !== layerId
|
|
211
|
+
? layer
|
|
212
|
+
: {
|
|
213
|
+
...layer,
|
|
214
|
+
transitions: layer.transitions.map((transition, i) => i === index ? { ...transition, duration: Math.max(0, Number.isFinite(duration) ? duration : 0) } : transition),
|
|
215
|
+
}),
|
|
216
|
+
};
|
|
217
|
+
return storeAnimGraph(setting, next);
|
|
218
|
+
}
|
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 */
|
|
@@ -105,6 +109,7 @@ export type EditorBridgeRequest = {
|
|
|
105
109
|
color?: string;
|
|
106
110
|
label?: string;
|
|
107
111
|
rotationY?: number;
|
|
112
|
+
catalogId?: string;
|
|
108
113
|
meta?: Record<string, unknown>;
|
|
109
114
|
} | {
|
|
110
115
|
method: "set_marker";
|
|
@@ -113,6 +118,7 @@ export type EditorBridgeRequest = {
|
|
|
113
118
|
color?: string;
|
|
114
119
|
label?: string;
|
|
115
120
|
rotationY?: number;
|
|
121
|
+
catalogId?: string;
|
|
116
122
|
meta?: Record<string, unknown>;
|
|
117
123
|
} | {
|
|
118
124
|
method: "set_note";
|
|
@@ -150,6 +156,11 @@ export type EditorBridgeRequest = {
|
|
|
150
156
|
height?: number;
|
|
151
157
|
} | {
|
|
152
158
|
method: "scene_summary";
|
|
159
|
+
} | {
|
|
160
|
+
method: "get_simulation";
|
|
161
|
+
} | {
|
|
162
|
+
method: "set_simulation";
|
|
163
|
+
simulation: EditorSimulation | null;
|
|
153
164
|
} | {
|
|
154
165
|
method: "export_document";
|
|
155
166
|
} | {
|
|
@@ -292,6 +303,10 @@ export type EditorBridgeRequest = {
|
|
|
292
303
|
} | {
|
|
293
304
|
method: "delete_prefab";
|
|
294
305
|
prefabId: string;
|
|
306
|
+
} | {
|
|
307
|
+
method: "set_prefab_static_bake";
|
|
308
|
+
prefabId: string;
|
|
309
|
+
bake: StaticPrefabBake | null;
|
|
295
310
|
} | {
|
|
296
311
|
method: "list_collections";
|
|
297
312
|
} | {
|
|
@@ -338,6 +353,26 @@ export type EditorBridgeRequest = {
|
|
|
338
353
|
color?: string;
|
|
339
354
|
label?: string;
|
|
340
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[];
|
|
341
376
|
} | {
|
|
342
377
|
method: "assign_material";
|
|
343
378
|
ids: string[];
|
|
@@ -496,6 +531,25 @@ export interface EditorAssetInfo {
|
|
|
496
531
|
kind: string;
|
|
497
532
|
url?: string;
|
|
498
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
|
+
};
|
|
499
553
|
/** Rolling frame-rate sample published by the in-canvas PerfProbe. */
|
|
500
554
|
export interface EditorPerfSample {
|
|
501
555
|
/**
|
|
@@ -573,6 +627,10 @@ export interface EditorHostApi {
|
|
|
573
627
|
subscribeFocus(listener: (target: EditorFocusTarget | null) => void): () => void;
|
|
574
628
|
getAssets(): readonly EditorAssetInfo[];
|
|
575
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;
|
|
576
634
|
getCatalogDefinitions(): readonly EditorCatalogDefinition[];
|
|
577
635
|
getPerf(): EditorPerfSample | null;
|
|
578
636
|
setPerf(sample: EditorPerfSample): void;
|
|
@@ -584,6 +642,8 @@ export interface EditorHostApi {
|
|
|
584
642
|
getTerrainSampler(): TerrainField | null;
|
|
585
643
|
setTerrainSampler(field: TerrainField | null): void;
|
|
586
644
|
getMode(): EditorRunMode;
|
|
645
|
+
/** Immutable authored snapshot captured on entering an isolated playtest. */
|
|
646
|
+
getPlayDocument(): EditorDocument | null;
|
|
587
647
|
setMode(mode: EditorRunMode): void;
|
|
588
648
|
subscribeMode(listener: (mode: EditorRunMode) => void): () => void;
|
|
589
649
|
/** Play-mode pause/step gate consumed by the runtime publisher. */
|
|
@@ -609,6 +669,10 @@ export declare function createEditorHost(options: {
|
|
|
609
669
|
y: number;
|
|
610
670
|
z: number;
|
|
611
671
|
} | null) => void;
|
|
672
|
+
validateDocument?: (document: EditorDocument) => void;
|
|
673
|
+
isolatedPlaytest?: boolean;
|
|
674
|
+
publishGlobal?: boolean;
|
|
675
|
+
persistAssetUrls?: boolean;
|
|
612
676
|
}): {
|
|
613
677
|
session: EditorSession;
|
|
614
678
|
api: EditorHostApi;
|
package/dist/session.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
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
|
+
import { warmNavBake } from "./handlers/navmesh.js";
|
|
4
5
|
import { emitEditorConsole } from "./shell/consoleSink.js";
|
|
5
6
|
registerBuiltinSceneKinds();
|
|
6
7
|
/** The accepted run modes, in canonical order — the runtime guard behind {@link EditorRunMode}. @internal */
|
|
@@ -22,11 +23,19 @@ export function getEditorHost() {
|
|
|
22
23
|
}
|
|
23
24
|
/** Builds and installs an editor host for a game: session, visibility, assets, and RPC handling. @internal */
|
|
24
25
|
export function createEditorHost(options) {
|
|
26
|
+
warmNavBake();
|
|
25
27
|
const catalogDefinitions = options.catalogs ?? [];
|
|
26
28
|
const document = seedEditorCatalogs(normalizeEditorLayers(options.layers), catalogDefinitions);
|
|
27
|
-
|
|
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
|
+
});
|
|
28
35
|
const liveSync = createDocumentLiveSync(document);
|
|
29
|
-
|
|
36
|
+
let uninstallLiveSync = installDocumentLiveSync(liveSync);
|
|
37
|
+
let playDocument = null;
|
|
38
|
+
let playSync = null;
|
|
30
39
|
let lastMirroredDocument = session.getState().document;
|
|
31
40
|
const unsubscribeSessionMirror = session.subscribe((state) => {
|
|
32
41
|
if (state.document === lastMirroredDocument)
|
|
@@ -37,9 +46,17 @@ export function createEditorHost(options) {
|
|
|
37
46
|
let visibility = {};
|
|
38
47
|
let focusTarget = null;
|
|
39
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
|
+
};
|
|
40
58
|
let perf = null;
|
|
41
59
|
let terrainSampler = null;
|
|
42
|
-
let mode = "edit";
|
|
43
60
|
let playControl = createRuntimePlayControl(false);
|
|
44
61
|
const visibilityListeners = new Set();
|
|
45
62
|
const focusListeners = new Set();
|
|
@@ -70,7 +87,7 @@ export function createEditorHost(options) {
|
|
|
70
87
|
const api = {
|
|
71
88
|
gameId: options.gameId,
|
|
72
89
|
getSession: () => session,
|
|
73
|
-
getLiveSync: () => liveSync,
|
|
90
|
+
getLiveSync: () => playSync ?? liveSync,
|
|
74
91
|
getVisibility: () => visibility,
|
|
75
92
|
setVisibility(next) {
|
|
76
93
|
visibility = { ...next };
|
|
@@ -99,6 +116,31 @@ export function createEditorHost(options) {
|
|
|
99
116
|
getAssets: () => assets,
|
|
100
117
|
setAssets(next) {
|
|
101
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;
|
|
102
144
|
},
|
|
103
145
|
getCatalogDefinitions: () => resolveCatalogDefinitions(session.getState().document, catalogDefinitions),
|
|
104
146
|
getPerf: () => perf,
|
|
@@ -110,9 +152,16 @@ export function createEditorHost(options) {
|
|
|
110
152
|
terrainSampler = field;
|
|
111
153
|
},
|
|
112
154
|
getMode: () => mode,
|
|
155
|
+
getPlayDocument: () => playDocument === null ? null : cloneEditorDocument(playDocument),
|
|
113
156
|
setMode(next) {
|
|
114
157
|
if (next === mode)
|
|
115
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
|
+
}
|
|
116
165
|
mode = next;
|
|
117
166
|
if (next === "play") {
|
|
118
167
|
playControl = createRuntimePlayControl(false);
|
|
@@ -155,7 +204,8 @@ export function createEditorHost(options) {
|
|
|
155
204
|
const ctx = {
|
|
156
205
|
api,
|
|
157
206
|
session,
|
|
158
|
-
liveSync,
|
|
207
|
+
liveSync: playSync ?? liveSync,
|
|
208
|
+
persistAssetUrls: options.persistAssetUrls,
|
|
159
209
|
gameId: options.gameId,
|
|
160
210
|
catalogDefinitions: defs,
|
|
161
211
|
catalogById: new Map(defs.map((definition) => [definition.id, definition])),
|
|
@@ -179,8 +229,9 @@ export function createEditorHost(options) {
|
|
|
179
229
|
}
|
|
180
230
|
},
|
|
181
231
|
};
|
|
182
|
-
const uninstallHost = installEditorHost(api);
|
|
232
|
+
const uninstallHost = options.publishGlobal === false ? () => { } : installEditorHost(api);
|
|
183
233
|
const dispose = () => {
|
|
234
|
+
materialSlotReports.clear();
|
|
184
235
|
unsubscribeSessionMirror();
|
|
185
236
|
uninstallLiveSync();
|
|
186
237
|
uninstallHost();
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { EditorSession } from "@jgengine/core/editor/index";
|
|
2
|
+
import type { EditorAssetEntry } from "../AssetBrowser.js";
|
|
3
|
+
import type { EditorUiStore } from "../uiStore.js";
|
|
4
|
+
/**
|
|
5
|
+
* Graph mode of the Animation dock: the selected placement's animation graph (layers, states,
|
|
6
|
+
* transitions), a scrubbed preview driven by the headless graph runtime and shown on the rig in the
|
|
7
|
+
* viewport, trigger buttons that record presses on the timeline, and crossfade editing that stores
|
|
8
|
+
* the graph on the placement as an undoable edit.
|
|
9
|
+
*
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
export declare function AnimationGraphPanel({ session, ui, rigged, }: {
|
|
13
|
+
session: EditorSession;
|
|
14
|
+
ui: EditorUiStore;
|
|
15
|
+
rigged: readonly EditorAssetEntry[];
|
|
16
|
+
}): import("react").JSX.Element;
|