@jgengine/editor 0.19.0 → 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 +346 -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 +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 +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/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 +22 -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 +102 -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
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { materialCapabilitiesForAsset } from "@jgengine/core/material/materialAsset";
|
|
2
|
+
/** Prepare an explicit artist edit by retaining declarations and adding its shader requirements. @internal */
|
|
3
|
+
export function prepareMaterialAuthoringEdit(asset) {
|
|
4
|
+
return { ...asset, capabilities: [...new Set([...asset.capabilities, ...materialCapabilitiesForAsset(asset)])] };
|
|
5
|
+
}
|
|
6
|
+
const color = { key: "color", label: "Base color", kind: "color", fallback: "#ffffff" };
|
|
7
|
+
const roughness = { key: "roughness", label: "Roughness", kind: "number", fallback: 1, min: 0, max: 1, step: 0.01 };
|
|
8
|
+
const metalness = { key: "metalness", label: "Metalness", kind: "number", fallback: 0, min: 0, max: 1, step: 0.01 };
|
|
9
|
+
const anisotropy = { key: "anisotropy", label: "Directional highlight", kind: "number", fallback: 0, min: 0, max: 1, step: 0.01 };
|
|
10
|
+
const rotation = { key: "anisotropyRotation", label: "Highlight direction (radians)", kind: "number", fallback: 0, min: -Math.PI, max: Math.PI, step: 0.01 };
|
|
11
|
+
const sheen = { key: "sheen", label: "Grazing sheen", kind: "number", fallback: 0, min: 0, max: 1, step: 0.01 };
|
|
12
|
+
const sheenRoughness = { key: "sheenRoughness", label: "Sheen roughness", kind: "number", fallback: 1, min: 0.07, max: 1, step: 0.01 };
|
|
13
|
+
const coat = { key: "clearcoat", label: "Coating", kind: "number", fallback: 0, min: 0, max: 1, step: 0.01 };
|
|
14
|
+
const coatRoughness = { key: "clearcoatRoughness", label: "Coating roughness", kind: "number", fallback: 0, min: 0, max: 1, step: 0.01 };
|
|
15
|
+
const ior = { key: "ior", label: "Index of refraction", kind: "number", fallback: 1.5, min: 1, max: 2.333, step: 0.01 };
|
|
16
|
+
const specular = { key: "specularIntensity", label: "Specular intensity", kind: "number", fallback: 1, min: 0, max: 1, step: 0.01 };
|
|
17
|
+
const transmission = { key: "transmission", label: "Transmission", kind: "number", fallback: 0, min: 0, max: 1, step: 0.01, hint: "Light through a surface. Keep opacity at 1 for physical transmission." };
|
|
18
|
+
const thickness = { key: "thickness", label: "Thickness", kind: "number", fallback: 0, min: 0, step: 0.01 };
|
|
19
|
+
const opacity = { key: "opacity", label: "Opacity", kind: "number", fallback: 1, min: 0, max: 1, step: 0.01 };
|
|
20
|
+
const cutoff = { key: "alphaCutoff", label: "Mask cutoff", kind: "number", fallback: 0.5, min: 0, max: 1, step: 0.01 };
|
|
21
|
+
const base = { label: "Base", hint: "Surface color multiplies the base-color map.", controls: [color] };
|
|
22
|
+
const highlights = { label: "Highlights", hint: "Microsurface roughness controls the width of reflections.", controls: [roughness] };
|
|
23
|
+
const detail = { label: "Detail", hint: "Assign normal and roughness maps below; their UVs and scale belong to each texture.", controls: [] };
|
|
24
|
+
const coverage = { label: "Coverage", hint: "Alpha masking cuts holes; blending changes coverage. Neither is physical transmission.", controls: [opacity, cutoff] };
|
|
25
|
+
/**
|
|
26
|
+
* Returns six primary groups suited to the material family without hiding the physical model.
|
|
27
|
+
* @capability editor-material-controls Group physical controls by surface family while retaining advanced fields.
|
|
28
|
+
*/
|
|
29
|
+
export function materialControlGroups(family) {
|
|
30
|
+
switch (family) {
|
|
31
|
+
case "fabric": return [base, highlights,
|
|
32
|
+
{ label: "Fibre sheen", hint: "Sheen approximates grazing fuzz; it does not add fibres to the silhouette.", controls: [sheen, sheenRoughness] },
|
|
33
|
+
{ label: "Weave direction", hint: "Directional silk highlights need valid UVs and normals. Tangents improve authored orientation.", controls: [anisotropy, rotation] }, detail, coverage];
|
|
34
|
+
case "hair": return [base, highlights,
|
|
35
|
+
{ label: "Strand highlights", hint: "A directional physical lobe approximates card or strand highlights; this is not a dedicated hair scattering model.", controls: [anisotropy, rotation] },
|
|
36
|
+
{ label: "Backlighting", hint: "The hair-card adapter adds an approximate light response from behind. It does not provide hair multiple scattering.", controls: [] }, detail, coverage];
|
|
37
|
+
case "glass": return [base, highlights,
|
|
38
|
+
{ label: "Transmission", hint: "Use opaque coverage with transmission. Refraction needs content behind the glass.", controls: [transmission, ior] },
|
|
39
|
+
{ label: "Volume", hint: "Thickness and attenuation approximate the distance travelled through the medium.", controls: [thickness, { key: "attenuationColor", label: "Attenuation color", kind: "color", fallback: "#ffffff" }, { key: "attenuationDistance", label: "Attenuation distance", kind: "number", fallback: 1, min: 0.001, step: 0.01 }] }, detail, coverage];
|
|
40
|
+
case "metal": return [base, highlights,
|
|
41
|
+
{ label: "Metal response", hint: "Metalness blends conductor and dielectric response.", controls: [metalness] },
|
|
42
|
+
{ label: "Brush direction", hint: "Directional highlights require UVs and normals; geometry and a direction map define the brushing.", controls: [anisotropy, rotation] }, detail,
|
|
43
|
+
{ label: "Coating", hint: "A separate clear surface lobe overlays the substrate.", controls: [coat, coatRoughness] }];
|
|
44
|
+
default: return [base, highlights,
|
|
45
|
+
{ label: "Surface response", hint: family === "skin" ? "Standard surface response approximates skin; subsurface scattering is not provided." : "Dielectric specular response and metallic coverage.", controls: [metalness, specular, ior] },
|
|
46
|
+
{ label: "Coating", hint: "Clearcoat adds a surface lobe; geometry remains unchanged.", controls: [coat, coatRoughness] }, detail, coverage];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** Complete supported physical scalar/color fields, independent of the six primary groups. */
|
|
50
|
+
export const advancedMaterialControls = [
|
|
51
|
+
color, roughness, metalness, specular, ior,
|
|
52
|
+
{ key: "specularColor", label: "Specular color", kind: "color", fallback: "#ffffff" },
|
|
53
|
+
sheen, sheenRoughness, { key: "sheenColor", label: "Sheen color", kind: "color", fallback: "#000000" },
|
|
54
|
+
anisotropy, rotation, coat, coatRoughness, transmission, thickness,
|
|
55
|
+
{ key: "attenuationColor", label: "Attenuation color", kind: "color", fallback: "#ffffff" },
|
|
56
|
+
{ key: "attenuationDistance", label: "Attenuation distance", kind: "number", fallback: 1, min: 0.001, step: 0.01 },
|
|
57
|
+
{ key: "iridescence", label: "Iridescence", kind: "number", fallback: 0, min: 0, max: 1, step: 0.01 },
|
|
58
|
+
{ key: "iridescenceIOR", label: "Film index of refraction", kind: "number", fallback: 1.3, min: 1, step: 0.01 },
|
|
59
|
+
{ key: "emissive", label: "Emissive color", kind: "color", fallback: "#000000" },
|
|
60
|
+
{ key: "emissiveIntensity", label: "Emissive intensity", kind: "number", fallback: 1, min: 0, step: 0.1 }, opacity, cutoff,
|
|
61
|
+
];
|
|
62
|
+
/**
|
|
63
|
+
* Reports geometry prerequisites and renderer approximations to authors.
|
|
64
|
+
* @capability editor-material-controls Explain geometry prerequisites and approximations for a material asset.
|
|
65
|
+
*/
|
|
66
|
+
export function materialAuthoringNotes(asset) {
|
|
67
|
+
const notes = [];
|
|
68
|
+
if (asset.family === "fabric")
|
|
69
|
+
notes.push("Material controls do not bend cloth, add collision, or create fibre geometry.");
|
|
70
|
+
if (asset.family === "hair")
|
|
71
|
+
notes.push("Cards or strand geometry must be authored separately. This approximation supplies coverage and directional highlights, not a groom, collision, motion, or a hair scattering model.");
|
|
72
|
+
if (asset.family === "skin")
|
|
73
|
+
notes.push("Skin uses physical surface shading. Subsurface scattering is not implemented.");
|
|
74
|
+
if ((asset.surface.anisotropy ?? 0) > 0)
|
|
75
|
+
notes.push("Directional highlights need valid mesh UVs and normals; authored tangents or a direction map control orientation.");
|
|
76
|
+
if ((asset.surface.transmission ?? 0) > 0)
|
|
77
|
+
notes.push("Transmission adds a renderer pass. Use opacity 1; inspect front, grazing, and backlit views.");
|
|
78
|
+
return notes;
|
|
79
|
+
}
|
package/dist/mcp/fieldSpec.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ export interface RpcFieldSpec {
|
|
|
18
18
|
* `EditorHostApi.handle` (whose per-method guards return honest `ok:false` diagnostics for
|
|
19
19
|
* missing/unknown targets). Type-checking presented fields is what keeps a fuzzed value — a string
|
|
20
20
|
* where a number belongs, a scalar where an object belongs — from reaching a live session uncoerced.
|
|
21
|
-
*
|
|
21
|
+
* Undeclared top-level fields are rejected by the decoder for every method.
|
|
22
22
|
*
|
|
23
23
|
* The same table generates the MCP tool `inputSchema`s (see `mcp/tools.ts`), so `required`/`oneOf`/
|
|
24
24
|
* `of` here also shape what agents are told a tool accepts. The `Record<EditorBridgeRequest["method"],
|
package/dist/mcp/fieldSpec.js
CHANGED
|
@@ -18,7 +18,7 @@ const AXES = ["xz", "xy"];
|
|
|
18
18
|
* `EditorHostApi.handle` (whose per-method guards return honest `ok:false` diagnostics for
|
|
19
19
|
* missing/unknown targets). Type-checking presented fields is what keeps a fuzzed value — a string
|
|
20
20
|
* where a number belongs, a scalar where an object belongs — from reaching a live session uncoerced.
|
|
21
|
-
*
|
|
21
|
+
* Undeclared top-level fields are rejected by the decoder for every method.
|
|
22
22
|
*
|
|
23
23
|
* The same table generates the MCP tool `inputSchema`s (see `mcp/tools.ts`), so `required`/`oneOf`/
|
|
24
24
|
* `of` here also shape what agents are told a tool accepts. The `Record<EditorBridgeRequest["method"],
|
|
@@ -55,6 +55,8 @@ export const RPC_FIELD_SCHEMAS = {
|
|
|
55
55
|
camera_goto: [s("id"), n("x"), n("y"), n("z"), n("distance"), n("pitch"), n("yaw"), n("height")],
|
|
56
56
|
camera_frame: [n("distance"), n("pitch"), n("yaw"), n("height")],
|
|
57
57
|
scene_summary: [],
|
|
58
|
+
get_simulation: [],
|
|
59
|
+
set_simulation: [req(val("simulation"))],
|
|
58
60
|
export_document: [],
|
|
59
61
|
import_document: [req(s("json"))],
|
|
60
62
|
dispatch: [req(o("command"))],
|
|
@@ -81,6 +83,7 @@ export const RPC_FIELD_SCHEMAS = {
|
|
|
81
83
|
set_parent: [req(sa("ids")), req(sn("parentId"))],
|
|
82
84
|
hierarchy: [],
|
|
83
85
|
list_prefabs: [],
|
|
86
|
+
set_prefab_static_bake: [req(s("prefabId")), req({ name: "bake", kind: "object", nullable: true, of: [req(s("assetId")), oa("collisionBoxes", [req(val("min")), req(val("max"))]), oa("clearances", [req(s("id")), req(val("min")), req(val("max"))])] })],
|
|
84
87
|
create_prefab: [req(s("id")), req(s("name")), req(sa("ids"))],
|
|
85
88
|
insert_prefab: [req(s("prefabId")), n("x"), n("y"), n("z")],
|
|
86
89
|
detach_prefab_instance: [req(s("instanceId"))],
|
|
@@ -96,6 +99,12 @@ export const RPC_FIELD_SCHEMAS = {
|
|
|
96
99
|
select_collection: [req(s("id"))],
|
|
97
100
|
set_object_flags: [req(sa("ids")), b("locked"), b("hidden")],
|
|
98
101
|
batch_set_properties: [req(sa("ids")), s("color"), s("label"), o("meta")],
|
|
102
|
+
list_material_assets: [],
|
|
103
|
+
list_material_slots: [req(s("id"))],
|
|
104
|
+
upsert_material_asset: [req(o("asset")), s("coalesce")],
|
|
105
|
+
remove_material_asset: [req(s("id"))],
|
|
106
|
+
assign_material_asset: [req(sa("ids")), req(s("materialId")), req(o("selector"))],
|
|
107
|
+
clear_material_assets: [req(sa("ids"))],
|
|
99
108
|
assign_material: [req(sa("ids")), req(s("materialId"))],
|
|
100
109
|
list_grids: [],
|
|
101
110
|
get_grid_cell: [req(s("id")), req(n("col")), req(n("row"))],
|
|
@@ -107,7 +116,7 @@ export const RPC_FIELD_SCHEMAS = {
|
|
|
107
116
|
flood_fill_grid: [req(s("id")), req(n("col")), req(n("row")), req(s("value"))],
|
|
108
117
|
resize_grid_layer: [req(s("id")), req(n("cols")), req(n("rows"))],
|
|
109
118
|
import_grid: [req(s("id")), req(s("kind")), req(s("format", ["ascii", "csv"])), req(s("text")), s("empty"), n("cellSize"), v3("origin"), o("glyphMap"), oa("palette")],
|
|
110
|
-
push_document_patch: [req(o("patch")), b("force")],
|
|
119
|
+
push_document_patch: [{ ...req(o("patch")), of: [req(s("type", ["snapshot", "commands"])), req(n("baseRevision")), oa("commands"), o("document"), n("revision")] }, b("force")],
|
|
111
120
|
pull_document_patches: [n("sinceRevision")],
|
|
112
121
|
document_revision: [b("includeDocument")],
|
|
113
122
|
push_runtime_delta: [n("at"), oa("entities"), sa("removeIds"), o("tunables")],
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { access } from "node:fs/promises";
|
|
2
|
+
/** @internal Missing optional files are valid; existing modules must load successfully. */
|
|
3
|
+
export async function importOptionalGameModule(path) {
|
|
4
|
+
try {
|
|
5
|
+
await access(path);
|
|
6
|
+
}
|
|
7
|
+
catch (error) {
|
|
8
|
+
if (error.code === "ENOENT")
|
|
9
|
+
return undefined;
|
|
10
|
+
throw error;
|
|
11
|
+
}
|
|
12
|
+
return import(path.href);
|
|
13
|
+
}
|
|
@@ -1,12 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
try {
|
|
3
|
-
const path = new URL(`../../../../Games/${gameId}/src/editorCatalogs.ts`, import.meta.url);
|
|
4
|
-
return (await import(path.href));
|
|
5
|
-
}
|
|
6
|
-
catch {
|
|
7
|
-
return undefined;
|
|
8
|
-
}
|
|
9
|
-
}
|
|
1
|
+
import { importOptionalGameModule } from "./importOptionalGameModule.js";
|
|
10
2
|
function isPlainObject(value) {
|
|
11
3
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
12
4
|
}
|
|
@@ -72,11 +64,12 @@ export function decodeGameCatalogs(resolved) {
|
|
|
72
64
|
* @internal
|
|
73
65
|
*/
|
|
74
66
|
export async function loadGameCatalogs(gameId) {
|
|
75
|
-
const mod = await importOptionalEditorCatalogsModule(gameId);
|
|
76
|
-
if (mod?.editorCatalogs === undefined)
|
|
77
|
-
return { ok: true, catalogs: [] };
|
|
78
67
|
let resolved;
|
|
79
68
|
try {
|
|
69
|
+
const path = new URL(`../../../../Games/${gameId}/src/editorCatalogs.ts`, import.meta.url);
|
|
70
|
+
const mod = await importOptionalGameModule(path);
|
|
71
|
+
if (mod?.editorCatalogs === undefined)
|
|
72
|
+
return { ok: true, catalogs: [] };
|
|
80
73
|
const value = mod.editorCatalogs;
|
|
81
74
|
resolved = typeof value === "function" ? value() : value;
|
|
82
75
|
}
|
package/dist/mcp/rpcRequest.d.ts
CHANGED
|
@@ -18,8 +18,8 @@ export type DecodeRpcRequestResult = {
|
|
|
18
18
|
* call) before it reaches `EditorHostApi.handle`: confirms it is a plain object carrying a known
|
|
19
19
|
* `method`, then type-checks every field the method understands against {@link RPC_FIELD_SCHEMAS}.
|
|
20
20
|
* A garbled method, or a field whose value is the wrong type (a string where a number belongs, a
|
|
21
|
-
* scalar where an object belongs), is rejected here with a path-specific
|
|
22
|
-
* flowing into a live session on a blind cast. Missing fields
|
|
23
|
-
* `handle` to interpret
|
|
21
|
+
* scalar where an object belongs), or an undeclared field is rejected here with a path-specific
|
|
22
|
+
* diagnostic instead of flowing into a live session on a blind cast. Missing fields are left for
|
|
23
|
+
* `handle` to interpret.
|
|
24
24
|
*/
|
|
25
25
|
export declare function decodeEditorBridgeRequest(raw: unknown): DecodeRpcRequestResult;
|
package/dist/mcp/rpcRequest.js
CHANGED
|
@@ -60,9 +60,9 @@ function validateField(value, spec, errors) {
|
|
|
60
60
|
* call) before it reaches `EditorHostApi.handle`: confirms it is a plain object carrying a known
|
|
61
61
|
* `method`, then type-checks every field the method understands against {@link RPC_FIELD_SCHEMAS}.
|
|
62
62
|
* A garbled method, or a field whose value is the wrong type (a string where a number belongs, a
|
|
63
|
-
* scalar where an object belongs), is rejected here with a path-specific
|
|
64
|
-
* flowing into a live session on a blind cast. Missing fields
|
|
65
|
-
* `handle` to interpret
|
|
63
|
+
* scalar where an object belongs), or an undeclared field is rejected here with a path-specific
|
|
64
|
+
* diagnostic instead of flowing into a live session on a blind cast. Missing fields are left for
|
|
65
|
+
* `handle` to interpret.
|
|
66
66
|
*/
|
|
67
67
|
export function decodeEditorBridgeRequest(raw) {
|
|
68
68
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
@@ -77,7 +77,13 @@ export function decodeEditorBridgeRequest(raw) {
|
|
|
77
77
|
return { ok: false, errors: [{ path: "$.method", message: `unknown method "${method}"` }] };
|
|
78
78
|
}
|
|
79
79
|
const errors = [];
|
|
80
|
-
|
|
80
|
+
const specs = RPC_FIELD_SCHEMAS[method];
|
|
81
|
+
const allowed = new Set(["method", ...specs.map((spec) => spec.name)]);
|
|
82
|
+
for (const field of Object.keys(record)) {
|
|
83
|
+
if (!allowed.has(field))
|
|
84
|
+
errors.push({ path: `$.${field}`, message: `unknown field "${field}" for method "${method}"` });
|
|
85
|
+
}
|
|
86
|
+
for (const spec of specs) {
|
|
81
87
|
validateField(record[spec.name], spec, errors);
|
|
82
88
|
}
|
|
83
89
|
if (errors.length > 0)
|
package/dist/mcp/tools.js
CHANGED
|
@@ -31,10 +31,12 @@ const TOOL_DESCRIPTIONS = [
|
|
|
31
31
|
{ name: "select", description: "Select editor object ids." },
|
|
32
32
|
{ name: "camera_goto", description: "Focus the editor camera on an id or world x/z. Optional distance/pitch/yaw/height reposition the orbit camera (pitch is degrees above the horizon; 90 = straight-down aerial) so an aerial composes in one call." },
|
|
33
33
|
{ name: "camera_frame", description: "Frame the whole editor document. Optional distance/pitch/yaw/height compose an aerial; when pitch is set without distance the region is auto-fit into view so the camera never buries in terrain." },
|
|
34
|
+
{ name: "get_simulation", description: "Read authored weather profiles/schedules, wind zones, named emitters, fire areas and flock habitats. Distances are metres, time is game seconds, wind is XZ metres/second." },
|
|
35
|
+
{ name: "set_simulation", description: "Replace validated authored simulation inputs atomically as one undo step. Pass null to clear. Runtime heat, particles and accumulation never overwrite authored inputs." },
|
|
34
36
|
{ name: "scene_summary", description: "Compact summary of the editor document and bounds." },
|
|
35
37
|
{ name: "export_document", description: "Export the editor document as JSON text." },
|
|
36
38
|
{ name: "import_document", description: "Replace the session document from JSON text." },
|
|
37
|
-
{ name: "push_document_patch", description: "Apply a versioned document patch (snapshot or commands)
|
|
39
|
+
{ name: "push_document_patch", description: "Apply a versioned document patch (snapshot or commands). Command batches validate atomically, publish once, and form one undo step; failure reports commands[index] without applying any edits. Same-kind add commands upsert stable ids. History commands are rejected. Force skips baseRevision checks." },
|
|
38
40
|
{ name: "pull_document_patches", description: "Pull document patches after a known revision (live-sync stream for a running game)." },
|
|
39
41
|
{ name: "document_revision", description: "Current live-sync document revision; optionally include the full document." },
|
|
40
42
|
{ name: "push_runtime_delta", description: "Publish ephemeral runtime state (entities/tunables) on the reverse channel — does not mutate the document." },
|
|
@@ -74,6 +76,7 @@ const TOOL_DESCRIPTIONS = [
|
|
|
74
76
|
{ name: "set_parent", description: "Parent objects under another (or null to unparent); cycles are refused. Moving a parent moves its subtree." },
|
|
75
77
|
{ name: "hierarchy", description: "The scene's parent/child tree: root ids and each root's direct children." },
|
|
76
78
|
{ name: "list_prefabs", description: "List the document's reusable prefab stamps (id, name, fragment content)." },
|
|
79
|
+
{ name: "set_prefab_static_bake", description: "Author static prefab export settings (assetId, local collisionBoxes, named clearance boxes); null clears settings. One undo step. Export verified source bytes offline through assets/staticPrefabBake." },
|
|
77
80
|
{ name: "create_prefab", description: "Make a reusable prefab from selected ids, centered on their own bounds." },
|
|
78
81
|
{ name: "insert_prefab", description: "Insert a fresh, tagged instance of a prefab at the camera focus or given x/y/z." },
|
|
79
82
|
{ name: "detach_prefab_instance", description: "Strip the prefab link from every object in an instance, keeping its content." },
|
|
@@ -89,6 +92,12 @@ const TOOL_DESCRIPTIONS = [
|
|
|
89
92
|
{ name: "select_collection", description: "Restore the current selection to a collection's member ids." },
|
|
90
93
|
{ name: "set_object_flags", description: "Set per-object locked (blocks transform/delete) and/or hidden (viewport hide) flags on placeables by id. false clears the field." },
|
|
91
94
|
{ name: "batch_set_properties", description: "Patch color/label/meta across every listed id in one dispatch, regardless of kind." },
|
|
95
|
+
{ name: "list_material_assets", description: "List reusable authored material assets and per-marker named slot assignments." },
|
|
96
|
+
{ name: "list_material_slots", description: "Read a marker's canonical loaded mesh/material slot names, indices, material type, UV sets and tangents. Select the marker and open Materials to load it; loading/unavailable states return explicit status rather than empty ready data." },
|
|
97
|
+
{ name: "upsert_material_asset", description: "Validate and save a reusable physical material asset as one undo step; omitted surface fields preserve imported appearance." },
|
|
98
|
+
{ name: "remove_material_asset", description: "Remove an unreferenced material asset; assigned materials fail with a diagnostic." },
|
|
99
|
+
{ name: "assign_material_asset", description: "Assign a material asset to exact mesh/material-slot names or slot index on selected markers. Untouched imported slots remain intact." },
|
|
100
|
+
{ name: "clear_material_assets", description: "Clear selected markers authored slot assignments and restore imported materials." },
|
|
92
101
|
{ name: "assign_material", description: "Stamp meta.materialId on every listed object — the drag-drop material assignment primitive." },
|
|
93
102
|
{ name: "list_grids", description: "List the scene document's grid/tile layers (id, kind, bounds, cell count, palette ids)." },
|
|
94
103
|
{ name: "get_grid_cell", description: "Eyedrop: read the value id at col,row of a grid layer (empty value for unset/out-of-bounds cells)." },
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { AnimGraph } from "@jgengine/core/anim/animGraph";
|
|
2
|
+
import { type AuthoredAnimationDiagnostic, type AuthoredAnimationDocumentLike } from "@jgengine/core/world/authoredAnimation";
|
|
2
3
|
/**
|
|
3
4
|
* Authoring model for a placement's `ModelConfig.animation`, persisted on a marker's `meta.animation`
|
|
4
5
|
* and consumed by the shell exactly like a game-authored config. Pure data + reducers, no JSX — the
|
|
@@ -39,11 +40,15 @@ export interface AuthoredAnimationStates {
|
|
|
39
40
|
}
|
|
40
41
|
/** Structurally compatible with `ModelAnimationConfig`, but every field optional for authoring. */
|
|
41
42
|
export interface AuthoredAnimationConfig {
|
|
43
|
+
auto?: true;
|
|
44
|
+
clock?: "real" | "game";
|
|
42
45
|
clip?: string;
|
|
43
46
|
loop?: boolean;
|
|
44
47
|
timeScale?: number;
|
|
48
|
+
paused?: boolean;
|
|
49
|
+
time?: number;
|
|
45
50
|
states?: AuthoredAnimationStates;
|
|
46
|
-
oneShots?: Record<string, string>;
|
|
51
|
+
oneShots?: Record<string, string | readonly string[]>;
|
|
47
52
|
/** A stored animation graph; takes over from `states`/`oneShots` at play time. */
|
|
48
53
|
graph?: AnimGraph;
|
|
49
54
|
}
|
|
@@ -51,8 +56,13 @@ export interface AuthoredAnimationConfig {
|
|
|
51
56
|
export type AnimationSetting = AuthoredAnimationConfig | "auto" | "none";
|
|
52
57
|
/** Which authoring mode a stored setting represents. `default` = no override key. */
|
|
53
58
|
export type AnimationMode = "default" | "auto" | "none" | "custom";
|
|
54
|
-
/** Reads
|
|
59
|
+
/** Reads a whole valid override, retaining authored fields; malformed values return undefined. */
|
|
55
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
|
+
};
|
|
56
66
|
/** The authoring mode a stored setting maps to. */
|
|
57
67
|
export declare function animationMode(setting: AnimationSetting | undefined): AnimationMode;
|
|
58
68
|
/**
|
|
@@ -64,11 +74,21 @@ export declare function setAnimationMode(setting: AnimationSetting | undefined,
|
|
|
64
74
|
/** A concrete custom config derived from a rigged asset's clip roles — the `custom` mode seed. */
|
|
65
75
|
export declare function defaultCustomConfig(clips: readonly string[]): AuthoredAnimationConfig;
|
|
66
76
|
/** Sets or clears (null) a locomotion state clip; forces the setting into `custom`. */
|
|
67
|
-
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;
|
|
68
78
|
/** Sets or clears (null) a numeric locomotion tuning. */
|
|
69
|
-
export declare function setLocomotionNumber(setting: AnimationSetting | undefined, key: LocomotionNumber, value: number | null): AuthoredAnimationConfig;
|
|
70
|
-
/** Binds or clears (null) a one-shot event
|
|
71
|
-
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;
|
|
72
92
|
/**
|
|
73
93
|
* The meta patch that persists a setting (or clears the override when undefined). Shallow-merges onto
|
|
74
94
|
* `marker.meta` via `setMarker`; an undefined value drops out of the saved JSON document.
|
|
@@ -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
|