@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
package/dist/handlers/terrain.js
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { createTerrainSnapshot, editableTerrainFromSnapshot, } from "@jgengine/core/world/terraform";
|
|
2
2
|
import { resolveScatter, resolveScatterRegion, scatterRegionEstimate, scatterRegionFromPath, SCATTER_PATH_KIND, } from "@jgengine/core/world/scatterRegion";
|
|
3
3
|
import { TERRAIN_MATERIALS } from "../uiStore.js";
|
|
4
|
+
function brushError(request) {
|
|
5
|
+
if (![request.x, request.z, request.radius ?? 8, request.strength ?? 1].every(Number.isFinite))
|
|
6
|
+
return "brush coordinates, radius and strength must be finite";
|
|
7
|
+
if ((request.radius ?? 8) <= 0)
|
|
8
|
+
return "brush radius must be positive";
|
|
9
|
+
if ((request.strength ?? 1) < 0)
|
|
10
|
+
return "brush strength must be nonnegative";
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
4
13
|
/** Sculpt heightfield, material painting, terrain layers, and foliage/scatter verbs. */
|
|
5
14
|
export const terrainHandlers = {
|
|
6
15
|
create_terrain: (ctx, request) => {
|
|
@@ -8,17 +17,35 @@ export const terrainHandlers = {
|
|
|
8
17
|
const depth = request.depth ?? 200;
|
|
9
18
|
const cx = request.centerX ?? 0;
|
|
10
19
|
const cz = request.centerZ ?? 0;
|
|
20
|
+
const cellSize = request.cellSize ?? 2;
|
|
21
|
+
if (![width, depth, cx, cz, cellSize].every(Number.isFinite) || width <= 0 || depth <= 0 || cellSize <= 0) {
|
|
22
|
+
return { ok: false, error: "terrain dimensions and cellSize must be finite and positive; center must be finite" };
|
|
23
|
+
}
|
|
24
|
+
if ((Math.max(1, Math.round(width / cellSize)) + 1) * (Math.max(1, Math.round(depth / cellSize)) + 1) > 1_000_000) {
|
|
25
|
+
return { ok: false, error: "terrain exceeds 1,000,000 vertices — increase cellSize or reduce width/depth" };
|
|
26
|
+
}
|
|
27
|
+
const bounds = { minX: cx - width / 2, minZ: cz - depth / 2, maxX: cx + width / 2, maxZ: cz + depth / 2 };
|
|
28
|
+
if (!Object.values(bounds).every(Number.isFinite) || bounds.maxX <= bounds.minX || bounds.maxZ <= bounds.minZ) {
|
|
29
|
+
return { ok: false, error: "terrain bounds must have a finite, nonzero extent" };
|
|
30
|
+
}
|
|
11
31
|
const terrain = createTerrainSnapshot({
|
|
12
|
-
bounds
|
|
13
|
-
cellSize
|
|
32
|
+
bounds,
|
|
33
|
+
cellSize,
|
|
14
34
|
});
|
|
15
35
|
ctx.session.dispatch({ type: "setTerrain", terrain });
|
|
16
36
|
return { ok: true, result: { cols: terrain.cols, rows: terrain.rows, cellSize: terrain.cellSize } };
|
|
17
37
|
},
|
|
18
38
|
sculpt_terrain: (ctx, request) => {
|
|
39
|
+
const invalid = brushError(request);
|
|
40
|
+
if (invalid !== null)
|
|
41
|
+
return { ok: false, error: invalid };
|
|
19
42
|
const terrain = ctx.session.getState().document.terrain;
|
|
20
43
|
if (terrain === undefined)
|
|
21
44
|
return { ok: false, error: "no terrain — call create_terrain first" };
|
|
45
|
+
if (request.mode === "ramp" && (request.toX === undefined || request.toZ === undefined))
|
|
46
|
+
return { ok: false, error: "ramp requires toX and toZ" };
|
|
47
|
+
if ([request.target, request.toX, request.toZ, request.seed].some((value) => value !== undefined && !Number.isFinite(value)))
|
|
48
|
+
return { ok: false, error: "sculpt target, ramp endpoint and seed must be finite" };
|
|
22
49
|
const live = editableTerrainFromSnapshot(terrain);
|
|
23
50
|
const edit = {
|
|
24
51
|
mode: request.mode,
|
|
@@ -66,6 +93,9 @@ export const terrainHandlers = {
|
|
|
66
93
|
};
|
|
67
94
|
},
|
|
68
95
|
paint_terrain: (ctx, request) => {
|
|
96
|
+
const invalid = brushError(request);
|
|
97
|
+
if (invalid !== null)
|
|
98
|
+
return { ok: false, error: invalid };
|
|
69
99
|
const terrain = ctx.session.getState().document.terrain;
|
|
70
100
|
if (terrain === undefined)
|
|
71
101
|
return { ok: false, error: "no terrain — call create_terrain first" };
|
|
@@ -120,23 +150,31 @@ export const terrainHandlers = {
|
|
|
120
150
|
if (ctx.session.getState().document.terrain === undefined) {
|
|
121
151
|
return { ok: false, error: "no terrain — call create_terrain first" };
|
|
122
152
|
}
|
|
153
|
+
if (new Set(request.layers.map((layer) => layer.id)).size !== request.layers.length || request.layers.some((layer) => layer.id.length === 0 || layer.surface.length === 0)) {
|
|
154
|
+
return { ok: false, error: "terrain layers require unique nonempty ids and nonempty surfaces" };
|
|
155
|
+
}
|
|
123
156
|
ctx.session.dispatch({ type: "setTerrainLayers", layers: request.layers });
|
|
124
157
|
return { ok: true, result: { layers: ctx.session.getState().document.terrain?.layers ?? [] } };
|
|
125
158
|
},
|
|
126
159
|
blend_terrain: (ctx, request) => {
|
|
160
|
+
const invalid = brushError(request);
|
|
161
|
+
if (invalid !== null)
|
|
162
|
+
return { ok: false, error: invalid };
|
|
127
163
|
const terrain = ctx.session.getState().document.terrain;
|
|
128
164
|
if (terrain === undefined)
|
|
129
165
|
return { ok: false, error: "no terrain — call create_terrain first" };
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
166
|
+
const live = editableTerrainFromSnapshot(terrain);
|
|
167
|
+
if ((request.strength ?? 1) > 1)
|
|
168
|
+
return { ok: false, error: "blend strength must be between 0 and 1" };
|
|
169
|
+
const addLayer = !live.layers.some((layer) => layer.surface === request.surface);
|
|
170
|
+
if (addLayer) {
|
|
171
|
+
let id = request.surface;
|
|
172
|
+
let suffix = 1;
|
|
173
|
+
while (live.layers.some((layer) => layer.id === id))
|
|
174
|
+
id = `${request.surface}_${suffix++}`;
|
|
175
|
+
const layers = [...live.layers, { id, surface: request.surface }];
|
|
176
|
+
live.setLayers(layers);
|
|
135
177
|
}
|
|
136
|
-
const snapshot = ctx.session.getState().document.terrain;
|
|
137
|
-
if (snapshot === undefined)
|
|
138
|
-
return { ok: false, error: "no terrain — call create_terrain first" };
|
|
139
|
-
const live = editableTerrainFromSnapshot(snapshot);
|
|
140
178
|
const delta = live.blendPaintDelta({
|
|
141
179
|
mode: "paint",
|
|
142
180
|
center: [request.x, request.z],
|
|
@@ -147,7 +185,9 @@ export const terrainHandlers = {
|
|
|
147
185
|
});
|
|
148
186
|
if (delta.indices.length === 0)
|
|
149
187
|
return { ok: false, error: "blend touched no cells (check radius/position)" };
|
|
150
|
-
ctx.session.dispatch(
|
|
188
|
+
ctx.session.dispatch(addLayer
|
|
189
|
+
? { type: "setTerrain", terrain: live.snapshot() }
|
|
190
|
+
: { type: "blendTerrain", delta });
|
|
151
191
|
return { ok: true, result: { changed: delta.indices.length, layers: ctx.session.getState().document.terrain?.layers ?? [] } };
|
|
152
192
|
},
|
|
153
193
|
convert_scatter: (ctx, request) => {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { EditorApp, type EditorAppProps, type EditorSaveFn } from "./EditorApp.js";
|
|
2
|
+
export { CreatorApp, type CreatorConfig } from "./CreatorApp.js";
|
|
2
3
|
export { buildEditorNetworkSnapshot, isNetworkMultiplayerConfigured, type EditorNetworkAdapterKind, type EditorNetworkPresenceActor, type EditorNetworkPresenceInput, type EditorNetworkSession, type EditorNetworkSnapshot, } from "./networkSnapshot.js";
|
|
3
4
|
export { StandaloneEditor, createBlankPlayable, blankWorld, downloadSaver, importAssetToHost, loadDroppedAssets, mergeStandaloneAssets, type StandaloneEditorProps, type StandaloneAsset, type AssetImporter, type BlankPlayableOptions, } from "./StandaloneEditor.js";
|
|
4
5
|
export { EditorLayerOverlays, PathDraftPreview } from "./DebugDraw.js";
|
|
@@ -9,7 +10,7 @@ export { TerrainSculpt } from "./TerrainSculpt.js";
|
|
|
9
10
|
export { ScatterPreview } from "./ScatterPreview.js";
|
|
10
11
|
export { EditorCameraDriver } from "./EditorCameraDriver.js";
|
|
11
12
|
export { AssetBrowser, assetsFromCatalog, editorAssetFromImport, mergeEditorAssets, type EditorAssetEntry, } from "./AssetBrowser.js";
|
|
12
|
-
export { createEditorHost, getEditorHost, installEditorHost, type EditorAssetInfo, type EditorBridgeRequest, type EditorBridgeResponse, type EditorHostApi, type EditorPerfSample, type EditorRunMode, } from "./session.js";
|
|
13
|
+
export { createEditorHost, getEditorHost, installEditorHost, type EditorAssetInfo, type EditorBridgeRequest, type EditorBridgeResponse, type EditorHostApi, type EditorMaterialSlotInfo, type EditorMaterialSlotReport, type EditorMaterialSlotInventory, type EditorPerfSample, type EditorRunMode, } from "./session.js";
|
|
13
14
|
export { PerfProbe } from "./PerfProbe.js";
|
|
14
15
|
export { useStoreSelector, shallowArrayEqual, virtualWindow, type SubscribableStore, type VirtualWindow, } from "./useStoreSelector.js";
|
|
15
16
|
export { useF2Chord } from "./useF2Chord.js";
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { EditorApp } from "./EditorApp.js";
|
|
2
|
+
export { CreatorApp } from "./CreatorApp.js";
|
|
2
3
|
export { buildEditorNetworkSnapshot, isNetworkMultiplayerConfigured, } from "./networkSnapshot.js";
|
|
3
4
|
export { StandaloneEditor, createBlankPlayable, blankWorld, downloadSaver, importAssetToHost, loadDroppedAssets, mergeStandaloneAssets, } from "./StandaloneEditor.js";
|
|
4
5
|
export { EditorLayerOverlays, PathDraftPreview } from "./DebugDraw.js";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type MaterialAsset, type MaterialFamily, type MaterialSurfaceParameters } from "@jgengine/core/material/materialAsset";
|
|
2
|
+
/** Prepare an explicit artist edit by retaining declarations and adding its shader requirements. @internal */
|
|
3
|
+
export declare function prepareMaterialAuthoringEdit(asset: MaterialAsset): MaterialAsset;
|
|
4
|
+
/** One physical surface field in the material workspace. */
|
|
5
|
+
export interface MaterialControl {
|
|
6
|
+
key: keyof MaterialSurfaceParameters;
|
|
7
|
+
label: string;
|
|
8
|
+
kind: "number" | "color";
|
|
9
|
+
fallback: number | string;
|
|
10
|
+
min?: number;
|
|
11
|
+
max?: number;
|
|
12
|
+
step?: number;
|
|
13
|
+
hint?: string;
|
|
14
|
+
}
|
|
15
|
+
/** A family-specific group of primary controls; maps and full physical fields remain available. */
|
|
16
|
+
export interface MaterialControlGroup {
|
|
17
|
+
label: string;
|
|
18
|
+
hint: string;
|
|
19
|
+
controls: readonly MaterialControl[];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Returns six primary groups suited to the material family without hiding the physical model.
|
|
23
|
+
* @capability editor-material-controls Group physical controls by surface family while retaining advanced fields.
|
|
24
|
+
*/
|
|
25
|
+
export declare function materialControlGroups(family: MaterialFamily): readonly MaterialControlGroup[];
|
|
26
|
+
/** Complete supported physical scalar/color fields, independent of the six primary groups. */
|
|
27
|
+
export declare const advancedMaterialControls: readonly MaterialControl[];
|
|
28
|
+
/**
|
|
29
|
+
* Reports geometry prerequisites and renderer approximations to authors.
|
|
30
|
+
* @capability editor-material-controls Explain geometry prerequisites and approximations for a material asset.
|
|
31
|
+
*/
|
|
32
|
+
export declare function materialAuthoringNotes(asset: MaterialAsset): readonly string[];
|
|
@@ -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.
|