@jgengine/editor 0.11.0 → 0.12.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 +26 -0
- package/README.md +12 -0
- package/dist/AssetBrowser.d.ts +26 -1
- package/dist/AssetBrowser.js +54 -1
- package/dist/AssetDropZone.d.ts +9 -0
- package/dist/AssetDropZone.js +65 -0
- package/dist/CatalogsPanel.d.ts +4 -2
- package/dist/CatalogsPanel.js +162 -9
- package/dist/CityPreview.d.ts +11 -0
- package/dist/CityPreview.js +45 -0
- package/dist/CollectionsPanel.js +5 -5
- package/dist/DebugDraw.d.ts +3 -1
- package/dist/DebugDraw.js +37 -19
- package/dist/EditorApp.d.ts +9 -1
- package/dist/EditorApp.js +147 -35
- package/dist/EditorChrome.d.ts +25 -6
- package/dist/EditorChrome.js +310 -130
- package/dist/InspectorPanel.d.ts +17 -7
- package/dist/InspectorPanel.js +173 -27
- package/dist/LightingPanel.d.ts +11 -0
- package/dist/LightingPanel.js +99 -0
- package/dist/MaterialsWorkspacePanel.d.ts +12 -0
- package/dist/MaterialsWorkspacePanel.js +86 -0
- package/dist/ParentPickerMenu.d.ts +13 -0
- package/dist/ParentPickerMenu.js +33 -0
- package/dist/PerfProbe.js +33 -0
- package/dist/PrefabsPanel.js +5 -5
- package/dist/RuntimePlayBridge.d.ts +5 -3
- package/dist/RuntimePlayBridge.js +6 -13
- package/dist/ScatterPreview.d.ts +3 -1
- package/dist/ScatterPreview.js +10 -7
- package/dist/SchemaInspector.js +21 -8
- package/dist/ScriptingPanel.d.ts +15 -0
- package/dist/ScriptingPanel.js +46 -0
- package/dist/SelectionGizmo.js +114 -30
- package/dist/StandaloneEditor.d.ts +16 -9
- package/dist/StandaloneEditor.js +19 -20
- package/dist/TerrainPanel.d.ts +3 -2
- package/dist/TerrainPanel.js +16 -5
- package/dist/TerrainSculpt.js +98 -59
- package/dist/agent/AgentPanel.d.ts +5 -2
- package/dist/agent/AgentPanel.js +11 -6
- package/dist/agent/toolBridge.js +5 -0
- package/dist/assetImport.d.ts +36 -0
- package/dist/assetImport.js +66 -0
- package/dist/authoredComponentMeta.d.ts +25 -0
- package/dist/authoredComponentMeta.js +65 -0
- package/dist/gizmoPivot.d.ts +4 -0
- package/dist/gizmoPivot.js +34 -0
- package/dist/handlers/catalogs.d.ts +3 -0
- package/dist/handlers/catalogs.js +137 -0
- package/dist/handlers/collections.d.ts +3 -0
- package/dist/handlers/collections.js +57 -0
- package/dist/handlers/context.d.ts +36 -0
- package/dist/handlers/context.js +1 -0
- package/dist/handlers/document.d.ts +3 -0
- package/dist/handlers/document.js +350 -0
- package/dist/handlers/grids.d.ts +3 -0
- package/dist/handlers/grids.js +142 -0
- package/dist/handlers/hierarchy.d.ts +3 -0
- package/dist/handlers/hierarchy.js +55 -0
- package/dist/handlers/index.d.ts +8 -0
- package/dist/handlers/index.js +23 -0
- package/dist/handlers/minimap.d.ts +9 -0
- package/dist/handlers/minimap.js +32 -0
- package/dist/handlers/runtime.d.ts +3 -0
- package/dist/handlers/runtime.js +191 -0
- package/dist/handlers/terrain.d.ts +3 -0
- package/dist/handlers/terrain.js +204 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/listDocumentTriggers.d.ts +42 -0
- package/dist/listDocumentTriggers.js +49 -0
- package/dist/materialAssignments.d.ts +63 -0
- package/dist/materialAssignments.js +84 -0
- package/dist/mcp/fieldSpec.d.ts +32 -0
- package/dist/mcp/fieldSpec.js +173 -0
- package/dist/mcp/rpcRequest.d.ts +8 -5
- package/dist/mcp/rpcRequest.js +72 -93
- package/dist/mcp/tools.js +104 -912
- package/dist/networkSnapshot.d.ts +82 -0
- package/dist/networkSnapshot.js +73 -0
- package/dist/parentCandidates.d.ts +12 -0
- package/dist/parentCandidates.js +25 -0
- package/dist/session.d.ts +77 -4
- package/dist/session.js +36 -1017
- package/dist/shell/AnimationPanel.d.ts +17 -0
- package/dist/shell/AnimationPanel.js +134 -0
- package/dist/shell/AssetThumbnail.d.ts +12 -0
- package/dist/shell/AssetThumbnail.js +23 -0
- package/dist/shell/BottomDock.d.ts +25 -0
- package/dist/shell/BottomDock.js +40 -0
- package/dist/shell/CommandPalette.d.ts +12 -0
- package/dist/shell/CommandPalette.js +56 -0
- package/dist/shell/ConsolePanel.d.ts +8 -0
- package/dist/shell/ConsolePanel.js +45 -0
- package/dist/shell/ContentBrowser.d.ts +21 -0
- package/dist/shell/ContentBrowser.js +130 -0
- package/dist/shell/HierarchyPanel.d.ts +26 -0
- package/dist/shell/HierarchyPanel.js +387 -0
- package/dist/shell/NetworkWorkspacePanel.d.ts +11 -0
- package/dist/shell/NetworkWorkspacePanel.js +49 -0
- package/dist/shell/PlayModeBar.d.ts +12 -0
- package/dist/shell/PlayModeBar.js +27 -0
- package/dist/shell/ProfilerPanel.d.ts +10 -0
- package/dist/shell/ProfilerPanel.js +41 -0
- package/dist/shell/SceneToolbar.d.ts +43 -0
- package/dist/shell/SceneToolbar.js +122 -0
- package/dist/shell/StatusBar.d.ts +18 -0
- package/dist/shell/StatusBar.js +41 -0
- package/dist/shell/TopAppBar.d.ts +33 -0
- package/dist/shell/TopAppBar.js +64 -0
- package/dist/shell/ViewportOverlays.d.ts +23 -0
- package/dist/shell/ViewportOverlays.js +136 -0
- package/dist/shell/WorkspaceRail.d.ts +20 -0
- package/dist/shell/WorkspaceRail.js +31 -0
- package/dist/shell/cameraTelemetry.d.ts +27 -0
- package/dist/shell/cameraTelemetry.js +24 -0
- package/dist/shell/commandRegistry.d.ts +58 -0
- package/dist/shell/commandRegistry.js +133 -0
- package/dist/shell/consoleSink.d.ts +11 -0
- package/dist/shell/consoleSink.js +17 -0
- package/dist/shell/consoleStore.d.ts +27 -0
- package/dist/shell/consoleStore.js +43 -0
- package/dist/shell/fields.d.ts +39 -0
- package/dist/shell/fields.js +104 -0
- package/dist/shell/formatSavedRelative.d.ts +2 -0
- package/dist/shell/formatSavedRelative.js +14 -0
- package/dist/shell/glbThumbnailCache.d.ts +45 -0
- package/dist/shell/glbThumbnailCache.js +241 -0
- package/dist/shell/hierarchyA11y.d.ts +21 -0
- package/dist/shell/hierarchyA11y.js +35 -0
- package/dist/shell/icons.d.ts +13 -0
- package/dist/shell/icons.js +85 -0
- package/dist/shell/layoutStore.d.ts +68 -0
- package/dist/shell/layoutStore.js +156 -0
- package/dist/shell/pathFlythrough.d.ts +37 -0
- package/dist/shell/pathFlythrough.js +54 -0
- package/dist/shell/perfHistory.d.ts +70 -0
- package/dist/shell/perfHistory.js +130 -0
- package/dist/shell/renameObject.d.ts +3 -0
- package/dist/shell/renameObject.js +25 -0
- package/dist/shell/theme.d.ts +36 -0
- package/dist/shell/theme.js +37 -0
- package/dist/shell/ui.d.ts +84 -0
- package/dist/shell/ui.js +90 -0
- package/dist/shell/useGlbThumbnail.d.ts +8 -0
- package/dist/shell/useGlbThumbnail.js +19 -0
- package/dist/uiStore.d.ts +27 -0
- package/dist/uiStore.js +10 -0
- package/dist/viewportContextMenu.d.ts +4 -2
- package/dist/viewportContextMenu.js +5 -1
- package/package.json +4 -4
- package/skills/jgengine/SKILL.md +7 -4
- package/skills/jgengine/api.md +172 -79
- package/skills/jgengine/capabilities.md +104 -2
- package/skills/jgengine/recipes/minimal-game.md +88 -0
- package/skills/jgengine-editor/SKILL.md +18 -3
- package/skills/jgengine-editor/api.md +397 -18
- package/skills/jgengine-editor/capabilities.md +46 -0
- package/skills/jgengine-editor/reference.md +87 -0
- package/skills/level-design/SKILL.md +54 -0
- package/skills/level-design/agents/openai.yaml +4 -0
- package/skills/level-design/references/application-playbook.md +192 -0
- package/skills/level-design/references/field-guide.md +337 -0
- package/skills/level-design/references/live-editor-loop.md +179 -0
- package/dist/OutlinerPanel.d.ts +0 -14
- package/dist/OutlinerPanel.js +0 -84
- package/skills/jgengine/reference-cartridge.md +0 -72
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { findEditorCatalog, findEditorCatalogEntry } from "@jgengine/core/editor/index";
|
|
2
|
+
import { parseParams, validateParams } from "@jgengine/core/scene/sceneKinds";
|
|
3
|
+
/** Gameplay data catalog verbs (list / get / merge-patch / add / remove rows). */
|
|
4
|
+
export const catalogHandlers = {
|
|
5
|
+
list_catalogs: (ctx) => {
|
|
6
|
+
const state = ctx.session.getState();
|
|
7
|
+
const catalogs = ctx.catalogDefinitions.map((definition) => {
|
|
8
|
+
const data = findEditorCatalog(state.document, definition.id);
|
|
9
|
+
return {
|
|
10
|
+
id: definition.id,
|
|
11
|
+
label: definition.label,
|
|
12
|
+
schema: definition.schema,
|
|
13
|
+
entryCount: data?.entries.length ?? definition.entries.length,
|
|
14
|
+
entries: (data?.entries ?? definition.entries).map((entry) => ({ id: entry.id, label: entry.label })),
|
|
15
|
+
};
|
|
16
|
+
});
|
|
17
|
+
return { ok: true, result: { catalogs } };
|
|
18
|
+
},
|
|
19
|
+
get_catalog_entry: (ctx, request) => {
|
|
20
|
+
const definition = ctx.catalogById.get(request.catalogId);
|
|
21
|
+
if (definition === undefined)
|
|
22
|
+
return { ok: false, error: `catalog not found: ${request.catalogId}` };
|
|
23
|
+
const entry = findEditorCatalogEntry(ctx.session.getState().document, request.catalogId, request.entryId);
|
|
24
|
+
if (entry === undefined)
|
|
25
|
+
return { ok: false, error: `catalog entry not found: ${request.catalogId}/${request.entryId}` };
|
|
26
|
+
return { ok: true, result: { catalogId: request.catalogId, label: definition.label, schema: definition.schema, entry } };
|
|
27
|
+
},
|
|
28
|
+
set_catalog_entry: (ctx, request) => {
|
|
29
|
+
const definition = ctx.catalogById.get(request.catalogId);
|
|
30
|
+
if (definition === undefined)
|
|
31
|
+
return { ok: false, error: `catalog not found: ${request.catalogId}` };
|
|
32
|
+
const current = findEditorCatalogEntry(ctx.session.getState().document, request.catalogId, request.entryId);
|
|
33
|
+
if (current === undefined)
|
|
34
|
+
return { ok: false, error: `catalog entry not found: ${request.catalogId}/${request.entryId}` };
|
|
35
|
+
const merged = { ...current.meta, ...request.patch };
|
|
36
|
+
const invalid = validateParams(definition.schema, merged);
|
|
37
|
+
if (invalid.length > 0) {
|
|
38
|
+
return { ok: false, error: `invalid ${request.catalogId} params: ${invalid.map((issue) => `${issue.key} (${issue.message})`).join(", ")}` };
|
|
39
|
+
}
|
|
40
|
+
const coalesceKey = Object.keys(request.patch).length === 1
|
|
41
|
+
? `catalog:${request.catalogId}:${request.entryId}:${Object.keys(request.patch)[0]}`
|
|
42
|
+
: `catalog:${request.catalogId}:${request.entryId}`;
|
|
43
|
+
const before = ctx.session.getState();
|
|
44
|
+
const state = ctx.session.dispatch({
|
|
45
|
+
type: "setCatalogEntry",
|
|
46
|
+
catalogId: request.catalogId,
|
|
47
|
+
entryId: request.entryId,
|
|
48
|
+
patch: { meta: merged, ...(request.label === undefined ? {} : { label: request.label }) },
|
|
49
|
+
}, { coalesce: coalesceKey });
|
|
50
|
+
if (state === before) {
|
|
51
|
+
return { ok: false, error: `set_catalog_entry rejected: ${request.catalogId}/${request.entryId}` };
|
|
52
|
+
}
|
|
53
|
+
const entry = findEditorCatalogEntry(state.document, request.catalogId, request.entryId);
|
|
54
|
+
return { ok: true, result: { catalogId: request.catalogId, entry } };
|
|
55
|
+
},
|
|
56
|
+
add_catalog_entry: (ctx, request) => {
|
|
57
|
+
const definition = ctx.catalogById.get(request.catalogId);
|
|
58
|
+
if (definition === undefined)
|
|
59
|
+
return { ok: false, error: `catalog not found: ${request.catalogId}` };
|
|
60
|
+
const entryId = request.entryId.trim();
|
|
61
|
+
if (entryId.length === 0)
|
|
62
|
+
return { ok: false, error: "add_catalog_entry: entryId is required" };
|
|
63
|
+
if (findEditorCatalogEntry(ctx.session.getState().document, request.catalogId, entryId) !== undefined) {
|
|
64
|
+
return { ok: false, error: `catalog entry already exists: ${request.catalogId}/${entryId}` };
|
|
65
|
+
}
|
|
66
|
+
// Seed a new row's meta from the schema defaults, then overlay any provided values, so the
|
|
67
|
+
// row is schema-valid the moment it lands — the same contract set_catalog_entry enforces.
|
|
68
|
+
const seeded = parseParams(definition.schema, request.meta);
|
|
69
|
+
const meta = { ...seeded, ...request.meta };
|
|
70
|
+
const invalid = validateParams(definition.schema, meta);
|
|
71
|
+
if (invalid.length > 0) {
|
|
72
|
+
return { ok: false, error: `invalid ${request.catalogId} params: ${invalid.map((issue) => `${issue.key} (${issue.message})`).join(", ")}` };
|
|
73
|
+
}
|
|
74
|
+
ctx.session.dispatch({
|
|
75
|
+
type: "addCatalogEntry",
|
|
76
|
+
catalogId: request.catalogId,
|
|
77
|
+
entry: { id: entryId, ...(request.label === undefined ? {} : { label: request.label }), meta },
|
|
78
|
+
});
|
|
79
|
+
const entry = findEditorCatalogEntry(ctx.session.getState().document, request.catalogId, entryId);
|
|
80
|
+
return { ok: true, result: { catalogId: request.catalogId, entry } };
|
|
81
|
+
},
|
|
82
|
+
remove_catalog_entry: (ctx, request) => {
|
|
83
|
+
const definition = ctx.catalogById.get(request.catalogId);
|
|
84
|
+
if (definition === undefined)
|
|
85
|
+
return { ok: false, error: `catalog not found: ${request.catalogId}` };
|
|
86
|
+
const before = ctx.session.getState();
|
|
87
|
+
const state = ctx.session.dispatch({ type: "removeCatalogEntry", catalogId: request.catalogId, entryId: request.entryId });
|
|
88
|
+
if (state === before) {
|
|
89
|
+
return { ok: false, error: `catalog entry not found: ${request.catalogId}/${request.entryId}` };
|
|
90
|
+
}
|
|
91
|
+
return { ok: true, result: { catalogId: request.catalogId, entryId: request.entryId } };
|
|
92
|
+
},
|
|
93
|
+
add_catalog: (ctx, request) => {
|
|
94
|
+
const catalogId = request.catalogId.trim();
|
|
95
|
+
if (catalogId.length === 0)
|
|
96
|
+
return { ok: false, error: "add_catalog: catalogId is required" };
|
|
97
|
+
if (ctx.catalogById.has(catalogId) || findEditorCatalog(ctx.session.getState().document, catalogId) !== undefined) {
|
|
98
|
+
return { ok: false, error: `catalog already exists: ${catalogId}` };
|
|
99
|
+
}
|
|
100
|
+
// Default to an empty schema so the catalog is a document-authored one — carried on the document,
|
|
101
|
+
// addressable via list_catalogs, and ready for set_catalog_schema to add fields.
|
|
102
|
+
const before = ctx.session.getState();
|
|
103
|
+
const state = ctx.session.dispatch({
|
|
104
|
+
type: "addCatalog",
|
|
105
|
+
id: catalogId,
|
|
106
|
+
...(request.label === undefined ? {} : { label: request.label }),
|
|
107
|
+
schema: request.schema ?? { fields: [] },
|
|
108
|
+
});
|
|
109
|
+
if (state === before)
|
|
110
|
+
return { ok: false, error: `add_catalog rejected: ${catalogId}` };
|
|
111
|
+
const catalog = findEditorCatalog(state.document, catalogId);
|
|
112
|
+
return { ok: true, result: { catalogId, catalog } };
|
|
113
|
+
},
|
|
114
|
+
remove_catalog: (ctx, request) => {
|
|
115
|
+
const before = ctx.session.getState();
|
|
116
|
+
const state = ctx.session.dispatch({ type: "removeCatalog", id: request.catalogId });
|
|
117
|
+
if (state === before)
|
|
118
|
+
return { ok: false, error: `catalog not found: ${request.catalogId}` };
|
|
119
|
+
return { ok: true, result: { catalogId: request.catalogId } };
|
|
120
|
+
},
|
|
121
|
+
set_catalog_schema: (ctx, request) => {
|
|
122
|
+
if (findEditorCatalog(ctx.session.getState().document, request.catalogId) === undefined) {
|
|
123
|
+
return { ok: false, error: `catalog not found: ${request.catalogId}` };
|
|
124
|
+
}
|
|
125
|
+
const before = ctx.session.getState();
|
|
126
|
+
const state = ctx.session.dispatch({
|
|
127
|
+
type: "setCatalogSchema",
|
|
128
|
+
id: request.catalogId,
|
|
129
|
+
schema: request.schema,
|
|
130
|
+
...(request.label === undefined ? {} : { label: request.label }),
|
|
131
|
+
});
|
|
132
|
+
if (state === before)
|
|
133
|
+
return { ok: false, error: `set_catalog_schema rejected: ${request.catalogId}` };
|
|
134
|
+
const catalog = findEditorCatalog(state.document, request.catalogId);
|
|
135
|
+
return { ok: true, result: { catalogId: request.catalogId, entries: catalog?.entries ?? [] } };
|
|
136
|
+
},
|
|
137
|
+
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { HandlerTable } from "./context.js";
|
|
2
|
+
/** Named collection / selection-set verbs. */
|
|
3
|
+
export declare const collectionHandlers: Pick<HandlerTable, "list_collections" | "create_collection" | "rename_collection" | "delete_collection" | "set_collection_members" | "add_to_collection" | "remove_from_collection" | "set_collection_flags" | "select_collection">;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { findEditorCollection } from "@jgengine/core/editor/index";
|
|
2
|
+
/** Runs `fn` with the named collection, or short-circuits with an honest not-found response. */
|
|
3
|
+
function withCollection(ctx, id, fn) {
|
|
4
|
+
const collection = findEditorCollection(ctx.session.getState().document, id);
|
|
5
|
+
if (collection === undefined)
|
|
6
|
+
return { ok: false, error: `collection not found: ${id}` };
|
|
7
|
+
return fn(collection);
|
|
8
|
+
}
|
|
9
|
+
/** Named collection / selection-set verbs. */
|
|
10
|
+
export const collectionHandlers = {
|
|
11
|
+
list_collections: (ctx) => ({ ok: true, result: { collections: ctx.session.getState().document.collections } }),
|
|
12
|
+
create_collection: (ctx, request) => {
|
|
13
|
+
ctx.session.dispatch({
|
|
14
|
+
type: "createCollection",
|
|
15
|
+
id: request.id,
|
|
16
|
+
name: request.name,
|
|
17
|
+
...(request.memberIds === undefined ? {} : { memberIds: request.memberIds }),
|
|
18
|
+
});
|
|
19
|
+
return { ok: true, result: findEditorCollection(ctx.session.getState().document, request.id) };
|
|
20
|
+
},
|
|
21
|
+
rename_collection: (ctx, request) => withCollection(ctx, request.id, () => {
|
|
22
|
+
ctx.session.dispatch({ type: "renameCollection", id: request.id, name: request.name });
|
|
23
|
+
return { ok: true, result: findEditorCollection(ctx.session.getState().document, request.id) };
|
|
24
|
+
}),
|
|
25
|
+
delete_collection: (ctx, request) => withCollection(ctx, request.id, () => {
|
|
26
|
+
ctx.session.dispatch({ type: "deleteCollection", id: request.id });
|
|
27
|
+
return { ok: true, result: { collections: ctx.session.getState().document.collections } };
|
|
28
|
+
}),
|
|
29
|
+
set_collection_members: (ctx, request) => withCollection(ctx, request.id, () => {
|
|
30
|
+
ctx.session.dispatch({ type: "setCollectionMembers", id: request.id, memberIds: request.memberIds });
|
|
31
|
+
return { ok: true, result: findEditorCollection(ctx.session.getState().document, request.id) };
|
|
32
|
+
}),
|
|
33
|
+
add_to_collection: (ctx, request) => withCollection(ctx, request.id, () => {
|
|
34
|
+
ctx.session.dispatch({ type: "addToCollection", id: request.id, ids: request.ids });
|
|
35
|
+
return { ok: true, result: findEditorCollection(ctx.session.getState().document, request.id) };
|
|
36
|
+
}),
|
|
37
|
+
remove_from_collection: (ctx, request) => withCollection(ctx, request.id, () => {
|
|
38
|
+
ctx.session.dispatch({ type: "removeFromCollection", id: request.id, ids: request.ids });
|
|
39
|
+
return { ok: true, result: findEditorCollection(ctx.session.getState().document, request.id) };
|
|
40
|
+
}),
|
|
41
|
+
set_collection_flags: (ctx, request) => withCollection(ctx, request.id, () => {
|
|
42
|
+
ctx.session.dispatch({
|
|
43
|
+
type: "setCollectionFlags",
|
|
44
|
+
id: request.id,
|
|
45
|
+
patch: {
|
|
46
|
+
...(request.color === undefined ? {} : { color: request.color }),
|
|
47
|
+
...(request.locked === undefined ? {} : { locked: request.locked }),
|
|
48
|
+
...(request.visible === undefined ? {} : { visible: request.visible }),
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
return { ok: true, result: findEditorCollection(ctx.session.getState().document, request.id) };
|
|
52
|
+
}),
|
|
53
|
+
select_collection: (ctx, request) => withCollection(ctx, request.id, () => {
|
|
54
|
+
ctx.session.dispatch({ type: "selectCollection", id: request.id });
|
|
55
|
+
return { ok: true, result: { selection: ctx.session.getState().selection } };
|
|
56
|
+
}),
|
|
57
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { DocumentLiveSync, EditorCatalogDefinition, EditorCommand, EditorSession, EditorSessionState } from "@jgengine/core/editor/index";
|
|
2
|
+
import type { TerrainField } from "@jgengine/core/world/terrain";
|
|
3
|
+
import type { EditorBridgeRequest, EditorBridgeResponse, EditorHostApi } from "../session.js";
|
|
4
|
+
/** Outcome of a guarded dispatch: whether it mutated the session, plus the resulting state. */
|
|
5
|
+
export interface DispatchOutcome {
|
|
6
|
+
applied: boolean;
|
|
7
|
+
state: EditorSessionState;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Everything a per-domain RPC handler needs from the host. Mutable host state (focus, mode, play
|
|
11
|
+
* control, assets, visibility, perf) is reached through `api`'s getters/setters so handlers stay
|
|
12
|
+
* closure-free and independently testable; `dispatchGuarded` and the catalog lookups are threaded in
|
|
13
|
+
* because they are built once when the host is constructed.
|
|
14
|
+
*/
|
|
15
|
+
export interface HandlerContext {
|
|
16
|
+
api: EditorHostApi;
|
|
17
|
+
session: EditorSession;
|
|
18
|
+
liveSync: DocumentLiveSync;
|
|
19
|
+
gameId: string;
|
|
20
|
+
catalogDefinitions: readonly EditorCatalogDefinition[];
|
|
21
|
+
catalogById: ReadonlyMap<string, EditorCatalogDefinition>;
|
|
22
|
+
dispatchGuarded: (command: EditorCommand) => DispatchOutcome;
|
|
23
|
+
/** The live composed ground field from the mounted viewport (null when headless). Used by `bake_minimap`. */
|
|
24
|
+
getTerrainSampler: () => TerrainField | null;
|
|
25
|
+
}
|
|
26
|
+
/** One method's handler, with `request` narrowed to that method's union member. */
|
|
27
|
+
export type HandlerFor<M extends EditorBridgeRequest["method"]> = (ctx: HandlerContext, request: Extract<EditorBridgeRequest, {
|
|
28
|
+
method: M;
|
|
29
|
+
}>) => EditorBridgeResponse;
|
|
30
|
+
/**
|
|
31
|
+
* The full method → handler map. Spreading the per-domain tables into a value of this type is the
|
|
32
|
+
* lockstep guarantee that every union method has exactly one handler: a missing verb fails to compile.
|
|
33
|
+
*/
|
|
34
|
+
export type HandlerTable = {
|
|
35
|
+
[M in EditorBridgeRequest["method"]]: HandlerFor<M>;
|
|
36
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { HandlerTable } from "./context.js";
|
|
2
|
+
/** Document, selection, camera, mode, asset placement, and status verbs. */
|
|
3
|
+
export declare const documentHandlers: Pick<HandlerTable, "editor_status" | "set_mode" | "perf_report" | "list_layers" | "list_selection" | "get_marker" | "get_volume" | "set_transform" | "set_volume" | "set_path" | "set_marker" | "set_note" | "set_meta" | "apply_preset" | "select" | "clear_selection" | "camera_goto" | "camera_frame" | "scene_summary" | "export_document" | "import_document" | "dispatch" | "undo" | "redo" | "list_assets" | "place_asset" | "batch_set_properties" | "assign_material" | "set_object_flags">;
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import { editorDocumentBounds, listEditorKinds, summarizeEditorSession, } from "@jgengine/core/editor/index";
|
|
2
|
+
import { resolvePlaceAsset, toEditorMarker } from "@jgengine/core/world/placeAsset";
|
|
3
|
+
import { findSchemaPreset, getSceneKind, validateParams } from "@jgengine/core/scene/sceneKinds";
|
|
4
|
+
import { EDITOR_RUN_MODES } from "../session.js";
|
|
5
|
+
/** True when any of `ids` names a placeable object (marker/volume/path/note) in the document — used
|
|
6
|
+
* to reject batch mutations that would match nothing, so the caller hears an honest `ok:false`. */
|
|
7
|
+
function documentHasAnyId(doc, ids) {
|
|
8
|
+
if (ids.length === 0)
|
|
9
|
+
return false;
|
|
10
|
+
const wanted = new Set(ids);
|
|
11
|
+
return (doc.markers.some((m) => wanted.has(m.id)) ||
|
|
12
|
+
doc.volumes.some((v) => wanted.has(v.id)) ||
|
|
13
|
+
doc.paths.some((p) => wanted.has(p.id)) ||
|
|
14
|
+
doc.annotations.some((n) => wanted.has(n.id)));
|
|
15
|
+
}
|
|
16
|
+
/** Which document collection an object lives in, plus its current kind + meta — for generic `set_meta`. */
|
|
17
|
+
function findMetaTarget(doc, id) {
|
|
18
|
+
const marker = doc.markers.find((m) => m.id === id);
|
|
19
|
+
if (marker !== undefined)
|
|
20
|
+
return { target: "marker", kind: marker.kind, meta: marker.meta };
|
|
21
|
+
const volume = doc.volumes.find((v) => v.id === id);
|
|
22
|
+
if (volume !== undefined)
|
|
23
|
+
return { target: "volume", kind: volume.kind, meta: volume.meta };
|
|
24
|
+
const path = doc.paths.find((p) => p.id === id);
|
|
25
|
+
if (path !== undefined)
|
|
26
|
+
return { target: "path", kind: path.kind, meta: path.meta };
|
|
27
|
+
const note = doc.annotations.find((n) => n.id === id);
|
|
28
|
+
if (note !== undefined)
|
|
29
|
+
return { target: "note", kind: "note", meta: note.meta };
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
/** Validates a merged meta bag against a registered kind's schema; returns an error string or null. */
|
|
33
|
+
function validateMetaForKind(kind, meta) {
|
|
34
|
+
const definition = getSceneKind(kind);
|
|
35
|
+
if (definition === undefined || meta === undefined)
|
|
36
|
+
return null;
|
|
37
|
+
const issues = validateParams(definition.schema, meta);
|
|
38
|
+
if (issues.length === 0)
|
|
39
|
+
return null;
|
|
40
|
+
return `invalid ${kind} params: ${issues.map((issue) => `${issue.key} (${issue.message})`).join(", ")}`;
|
|
41
|
+
}
|
|
42
|
+
/** Document, selection, camera, mode, asset placement, and status verbs. */
|
|
43
|
+
export const documentHandlers = {
|
|
44
|
+
editor_status: (ctx) => ({
|
|
45
|
+
ok: true,
|
|
46
|
+
result: {
|
|
47
|
+
gameId: ctx.gameId,
|
|
48
|
+
connected: true,
|
|
49
|
+
...summarizeEditorSession(ctx.session.getState()),
|
|
50
|
+
canUndo: ctx.session.canUndo(),
|
|
51
|
+
canRedo: ctx.session.canRedo(),
|
|
52
|
+
perf: ctx.api.getPerf(),
|
|
53
|
+
mode: ctx.api.getMode(),
|
|
54
|
+
},
|
|
55
|
+
}),
|
|
56
|
+
set_mode: (ctx, request) => {
|
|
57
|
+
if (!EDITOR_RUN_MODES.includes(request.mode)) {
|
|
58
|
+
return { ok: false, error: `unknown mode: ${String(request.mode)} (edit | walk | play | hud)` };
|
|
59
|
+
}
|
|
60
|
+
ctx.api.setMode(request.mode);
|
|
61
|
+
return { ok: true, result: { mode: request.mode } };
|
|
62
|
+
},
|
|
63
|
+
perf_report: (ctx) => {
|
|
64
|
+
const devtoolsGlobal = globalThis.__JG_DEVTOOLS;
|
|
65
|
+
if (devtoolsGlobal?.snapshot === undefined) {
|
|
66
|
+
return { ok: false, error: "engine devtools not mounted — open the browser editor (F2+D panel) first" };
|
|
67
|
+
}
|
|
68
|
+
return { ok: true, result: { perf: ctx.api.getPerf(), report: devtoolsGlobal.snapshot() } };
|
|
69
|
+
},
|
|
70
|
+
list_layers: (ctx) => {
|
|
71
|
+
const state = ctx.session.getState();
|
|
72
|
+
return { ok: true, result: { kinds: listEditorKinds(state.document), visibility: ctx.api.getVisibility(), document: state.document } };
|
|
73
|
+
},
|
|
74
|
+
list_selection: (ctx) => ({ ok: true, result: { selection: ctx.session.getState().selection } }),
|
|
75
|
+
get_marker: (ctx, request) => {
|
|
76
|
+
const marker = ctx.session.getState().document.markers.find((m) => m.id === request.id);
|
|
77
|
+
return marker === undefined ? { ok: false, error: `marker not found: ${request.id}` } : { ok: true, result: marker };
|
|
78
|
+
},
|
|
79
|
+
get_volume: (ctx, request) => {
|
|
80
|
+
const volume = ctx.session.getState().document.volumes.find((v) => v.id === request.id);
|
|
81
|
+
return volume === undefined ? { ok: false, error: `volume not found: ${request.id}` } : { ok: true, result: volume };
|
|
82
|
+
},
|
|
83
|
+
set_transform: (ctx, request) => {
|
|
84
|
+
const marker = ctx.session.getState().document.markers.find((m) => m.id === request.id);
|
|
85
|
+
const volume = ctx.session.getState().document.volumes.find((v) => v.id === request.id);
|
|
86
|
+
const base = marker?.position ?? volume?.center;
|
|
87
|
+
if (base === undefined)
|
|
88
|
+
return { ok: false, error: `id not found: ${request.id}` };
|
|
89
|
+
const { applied, state } = ctx.dispatchGuarded({
|
|
90
|
+
type: "setTransform",
|
|
91
|
+
id: request.id,
|
|
92
|
+
position: { x: request.x ?? base.x, y: request.y ?? base.y, z: request.z ?? base.z },
|
|
93
|
+
...(request.rotationY === undefined ? {} : { rotationY: request.rotationY }),
|
|
94
|
+
});
|
|
95
|
+
if (!applied)
|
|
96
|
+
return { ok: false, error: `set_transform rejected: ${request.id} is locked` };
|
|
97
|
+
return { ok: true, result: summarizeEditorSession(state) };
|
|
98
|
+
},
|
|
99
|
+
set_volume: (ctx, request) => {
|
|
100
|
+
const volume = ctx.session.getState().document.volumes.find((v) => v.id === request.id);
|
|
101
|
+
if (volume === undefined)
|
|
102
|
+
return { ok: false, error: `volume not found: ${request.id}` };
|
|
103
|
+
ctx.session.dispatch({
|
|
104
|
+
type: "setVolume",
|
|
105
|
+
id: request.id,
|
|
106
|
+
patch: {
|
|
107
|
+
...(request.radius === undefined ? {} : { radius: request.radius }),
|
|
108
|
+
...(request.height === undefined ? {} : { height: request.height }),
|
|
109
|
+
...(request.x === undefined && request.y === undefined && request.z === undefined
|
|
110
|
+
? {}
|
|
111
|
+
: { center: { x: request.x ?? volume.center.x, y: request.y ?? volume.center.y, z: request.z ?? volume.center.z } }),
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
return { ok: true, result: ctx.session.getState().document.volumes.find((v) => v.id === request.id) };
|
|
115
|
+
},
|
|
116
|
+
set_path: (ctx, request) => {
|
|
117
|
+
const path = ctx.session.getState().document.paths.find((p) => p.id === request.id);
|
|
118
|
+
if (path === undefined)
|
|
119
|
+
return { ok: false, error: `path not found: ${request.id}` };
|
|
120
|
+
const merged = request.meta === undefined ? undefined : { ...path.meta, ...request.meta };
|
|
121
|
+
const invalid = validateMetaForKind(request.kind ?? path.kind, merged);
|
|
122
|
+
if (invalid !== null)
|
|
123
|
+
return { ok: false, error: invalid };
|
|
124
|
+
ctx.session.dispatch({
|
|
125
|
+
type: "setPath",
|
|
126
|
+
id: request.id,
|
|
127
|
+
patch: {
|
|
128
|
+
...(request.kind === undefined ? {} : { kind: request.kind }),
|
|
129
|
+
...(request.width === undefined ? {} : { width: request.width }),
|
|
130
|
+
...(request.color === undefined ? {} : { color: request.color }),
|
|
131
|
+
...(request.label === undefined ? {} : { label: request.label }),
|
|
132
|
+
...(merged === undefined ? {} : { meta: merged }),
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
return { ok: true, result: ctx.session.getState().document.paths.find((p) => p.id === request.id) };
|
|
136
|
+
},
|
|
137
|
+
set_marker: (ctx, request) => {
|
|
138
|
+
const marker = ctx.session.getState().document.markers.find((m) => m.id === request.id);
|
|
139
|
+
if (marker === undefined)
|
|
140
|
+
return { ok: false, error: `marker not found: ${request.id}` };
|
|
141
|
+
const merged = request.meta === undefined ? undefined : { ...marker.meta, ...request.meta };
|
|
142
|
+
const invalid = validateMetaForKind(request.kind ?? marker.kind, merged);
|
|
143
|
+
if (invalid !== null)
|
|
144
|
+
return { ok: false, error: invalid };
|
|
145
|
+
ctx.session.dispatch({
|
|
146
|
+
type: "setMarker",
|
|
147
|
+
id: request.id,
|
|
148
|
+
patch: {
|
|
149
|
+
...(request.kind === undefined ? {} : { kind: request.kind }),
|
|
150
|
+
...(request.color === undefined ? {} : { color: request.color }),
|
|
151
|
+
...(request.label === undefined ? {} : { label: request.label }),
|
|
152
|
+
...(request.rotationY === undefined ? {} : { rotationY: request.rotationY }),
|
|
153
|
+
...(merged === undefined ? {} : { meta: merged }),
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
return { ok: true, result: ctx.session.getState().document.markers.find((m) => m.id === request.id) };
|
|
157
|
+
},
|
|
158
|
+
set_note: (ctx, request) => {
|
|
159
|
+
const note = ctx.session.getState().document.annotations.find((n) => n.id === request.id);
|
|
160
|
+
if (note === undefined)
|
|
161
|
+
return { ok: false, error: `note not found: ${request.id}` };
|
|
162
|
+
const merged = request.meta === undefined ? undefined : { ...note.meta, ...request.meta };
|
|
163
|
+
ctx.session.dispatch({
|
|
164
|
+
type: "setNote",
|
|
165
|
+
id: request.id,
|
|
166
|
+
patch: { ...(request.text === undefined ? {} : { text: request.text }), ...(merged === undefined ? {} : { meta: merged }) },
|
|
167
|
+
});
|
|
168
|
+
return { ok: true, result: ctx.session.getState().document.annotations.find((n) => n.id === request.id) };
|
|
169
|
+
},
|
|
170
|
+
set_meta: (ctx, request) => {
|
|
171
|
+
const target = findMetaTarget(ctx.session.getState().document, request.id);
|
|
172
|
+
if (target === null)
|
|
173
|
+
return { ok: false, error: `object not found: ${request.id}` };
|
|
174
|
+
const merged = { ...target.meta, ...request.patch };
|
|
175
|
+
const invalid = validateMetaForKind(target.kind, merged);
|
|
176
|
+
if (invalid !== null)
|
|
177
|
+
return { ok: false, error: invalid };
|
|
178
|
+
ctx.session.dispatch(target.target === "marker"
|
|
179
|
+
? { type: "setMarker", id: request.id, patch: { meta: merged } }
|
|
180
|
+
: target.target === "volume"
|
|
181
|
+
? { type: "setVolume", id: request.id, patch: { meta: merged } }
|
|
182
|
+
: target.target === "path"
|
|
183
|
+
? { type: "setPath", id: request.id, patch: { meta: merged } }
|
|
184
|
+
: { type: "setNote", id: request.id, patch: { meta: merged } });
|
|
185
|
+
return { ok: true, result: { id: request.id, kind: target.kind, meta: merged } };
|
|
186
|
+
},
|
|
187
|
+
apply_preset: (ctx, request) => {
|
|
188
|
+
const target = findMetaTarget(ctx.session.getState().document, request.id);
|
|
189
|
+
if (target === null)
|
|
190
|
+
return { ok: false, error: `object not found: ${request.id}` };
|
|
191
|
+
const definition = getSceneKind(target.kind);
|
|
192
|
+
if (definition === undefined)
|
|
193
|
+
return { ok: false, error: `kind "${target.kind}" has no registered schema` };
|
|
194
|
+
const preset = findSchemaPreset(definition.schema, request.preset);
|
|
195
|
+
if (preset === undefined) {
|
|
196
|
+
const available = (definition.schema.presets ?? []).map((entry) => entry.id).join(", ") || "none";
|
|
197
|
+
return { ok: false, error: `unknown preset "${request.preset}" for kind "${target.kind}" (available: ${available})` };
|
|
198
|
+
}
|
|
199
|
+
const merged = { ...target.meta, ...preset.values };
|
|
200
|
+
const invalid = validateMetaForKind(target.kind, merged);
|
|
201
|
+
if (invalid !== null)
|
|
202
|
+
return { ok: false, error: invalid };
|
|
203
|
+
ctx.session.dispatch(target.target === "marker"
|
|
204
|
+
? { type: "setMarker", id: request.id, patch: { meta: merged } }
|
|
205
|
+
: target.target === "volume"
|
|
206
|
+
? { type: "setVolume", id: request.id, patch: { meta: merged } }
|
|
207
|
+
: target.target === "path"
|
|
208
|
+
? { type: "setPath", id: request.id, patch: { meta: merged } }
|
|
209
|
+
: { type: "setNote", id: request.id, patch: { meta: merged } });
|
|
210
|
+
return { ok: true, result: { id: request.id, kind: target.kind, preset: preset.id, meta: merged } };
|
|
211
|
+
},
|
|
212
|
+
select: (ctx, request) => {
|
|
213
|
+
ctx.session.dispatch({ type: "select", ids: request.ids });
|
|
214
|
+
return { ok: true, result: { selection: ctx.session.getState().selection } };
|
|
215
|
+
},
|
|
216
|
+
clear_selection: (ctx) => {
|
|
217
|
+
ctx.session.dispatch({ type: "clearSelection" });
|
|
218
|
+
return { ok: true, result: { selection: [] } };
|
|
219
|
+
},
|
|
220
|
+
camera_goto: (ctx, request) => {
|
|
221
|
+
if (request.id !== undefined) {
|
|
222
|
+
const state = ctx.session.getState();
|
|
223
|
+
const marker = state.document.markers.find((m) => m.id === request.id);
|
|
224
|
+
const volume = state.document.volumes.find((v) => v.id === request.id);
|
|
225
|
+
const point = marker?.position ?? volume?.center;
|
|
226
|
+
if (point === undefined)
|
|
227
|
+
return { ok: false, error: `id not found: ${request.id}` };
|
|
228
|
+
ctx.api.setFocusTarget({ ...point });
|
|
229
|
+
ctx.session.dispatch({ type: "select", ids: [request.id] });
|
|
230
|
+
return { ok: true, result: { target: point } };
|
|
231
|
+
}
|
|
232
|
+
if (request.x === undefined || request.z === undefined) {
|
|
233
|
+
return { ok: false, error: "camera_goto requires id or x/z" };
|
|
234
|
+
}
|
|
235
|
+
const target = { x: request.x, y: request.y ?? 0, z: request.z };
|
|
236
|
+
ctx.api.setFocusTarget(target);
|
|
237
|
+
return { ok: true, result: { target } };
|
|
238
|
+
},
|
|
239
|
+
camera_frame: (ctx) => {
|
|
240
|
+
const bounds = editorDocumentBounds(ctx.session.getState().document);
|
|
241
|
+
if (bounds === null)
|
|
242
|
+
return { ok: false, error: "document is empty" };
|
|
243
|
+
const target = {
|
|
244
|
+
x: (bounds.min.x + bounds.max.x) / 2,
|
|
245
|
+
y: (bounds.min.y + bounds.max.y) / 2,
|
|
246
|
+
z: (bounds.min.z + bounds.max.z) / 2,
|
|
247
|
+
};
|
|
248
|
+
ctx.api.setFocusTarget(target);
|
|
249
|
+
return { ok: true, result: { target, bounds } };
|
|
250
|
+
},
|
|
251
|
+
scene_summary: (ctx) => ({
|
|
252
|
+
ok: true,
|
|
253
|
+
result: {
|
|
254
|
+
gameId: ctx.gameId,
|
|
255
|
+
kinds: listEditorKinds(ctx.session.getState().document),
|
|
256
|
+
...summarizeEditorSession(ctx.session.getState()),
|
|
257
|
+
bounds: editorDocumentBounds(ctx.session.getState().document),
|
|
258
|
+
},
|
|
259
|
+
}),
|
|
260
|
+
export_document: (ctx) => ({ ok: true, result: { json: ctx.session.exportJson(true) } }),
|
|
261
|
+
import_document: (ctx, request) => {
|
|
262
|
+
ctx.session.dispatch({ type: "importJson", json: request.json });
|
|
263
|
+
return { ok: true, result: summarizeEditorSession(ctx.session.getState()) };
|
|
264
|
+
},
|
|
265
|
+
dispatch: (ctx, request) => {
|
|
266
|
+
const { applied, state } = ctx.dispatchGuarded(request.command);
|
|
267
|
+
if (!applied)
|
|
268
|
+
return { ok: false, error: `${request.command.type} rejected: no effect` };
|
|
269
|
+
return { ok: true, result: summarizeEditorSession(state) };
|
|
270
|
+
},
|
|
271
|
+
undo: (ctx) => {
|
|
272
|
+
const { applied, state } = ctx.dispatchGuarded({ type: "undo" });
|
|
273
|
+
if (!applied)
|
|
274
|
+
return { ok: false, error: "nothing to undo" };
|
|
275
|
+
return { ok: true, result: summarizeEditorSession(state) };
|
|
276
|
+
},
|
|
277
|
+
redo: (ctx) => {
|
|
278
|
+
const { applied, state } = ctx.dispatchGuarded({ type: "redo" });
|
|
279
|
+
if (!applied)
|
|
280
|
+
return { ok: false, error: "nothing to redo" };
|
|
281
|
+
return { ok: true, result: summarizeEditorSession(state) };
|
|
282
|
+
},
|
|
283
|
+
list_assets: (ctx) => ({ ok: true, result: { assets: ctx.api.getAssets() } }),
|
|
284
|
+
place_asset: (ctx, request) => {
|
|
285
|
+
const focus = ctx.api.getFocusTarget() ?? { x: 0, y: 0, z: 0 };
|
|
286
|
+
const asset = ctx.api.getAssets().find((entry) => entry.id === request.id);
|
|
287
|
+
const placed = resolvePlaceAsset({
|
|
288
|
+
assetId: request.id,
|
|
289
|
+
position: { x: request.x ?? focus.x, y: request.y ?? focus.y, z: request.z ?? focus.z },
|
|
290
|
+
kind: request.kind,
|
|
291
|
+
knownKind: asset?.kind,
|
|
292
|
+
knownLabel: asset?.label,
|
|
293
|
+
knownUrl: asset?.url,
|
|
294
|
+
});
|
|
295
|
+
const placedMarker = toEditorMarker(placed);
|
|
296
|
+
// URL-backed catalog models need a first-class catalogId so AuthoredObjects places the mesh;
|
|
297
|
+
// generator assets keep meta.assetId only (no url) and render through AuthoredGenerators.
|
|
298
|
+
const marker = typeof asset?.url === "string" && asset.url.length > 0
|
|
299
|
+
? { ...placedMarker, catalogId: request.id }
|
|
300
|
+
: placedMarker;
|
|
301
|
+
ctx.session.dispatch({ type: "addMarker", marker });
|
|
302
|
+
return {
|
|
303
|
+
ok: true,
|
|
304
|
+
result: {
|
|
305
|
+
id: marker.id,
|
|
306
|
+
position: marker.position,
|
|
307
|
+
marker: ctx.session.getState().document.markers.find((entry) => entry.id === marker.id),
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
},
|
|
311
|
+
batch_set_properties: (ctx, request) => {
|
|
312
|
+
if (!documentHasAnyId(ctx.session.getState().document, request.ids)) {
|
|
313
|
+
return { ok: false, error: "batch_set_properties matched no objects" };
|
|
314
|
+
}
|
|
315
|
+
ctx.session.dispatch({
|
|
316
|
+
type: "batchSetProperties",
|
|
317
|
+
ids: request.ids,
|
|
318
|
+
patch: {
|
|
319
|
+
...(request.color === undefined ? {} : { color: request.color }),
|
|
320
|
+
...(request.label === undefined ? {} : { label: request.label }),
|
|
321
|
+
...(request.meta === undefined ? {} : { meta: request.meta }),
|
|
322
|
+
},
|
|
323
|
+
});
|
|
324
|
+
return { ok: true, result: summarizeEditorSession(ctx.session.getState()) };
|
|
325
|
+
},
|
|
326
|
+
set_object_flags: (ctx, request) => {
|
|
327
|
+
if (!documentHasAnyId(ctx.session.getState().document, request.ids)) {
|
|
328
|
+
return { ok: false, error: "set_object_flags matched no objects" };
|
|
329
|
+
}
|
|
330
|
+
if (request.locked === undefined && request.hidden === undefined) {
|
|
331
|
+
return { ok: false, error: "set_object_flags requires locked and/or hidden" };
|
|
332
|
+
}
|
|
333
|
+
ctx.session.dispatch({
|
|
334
|
+
type: "setObjectFlags",
|
|
335
|
+
ids: request.ids,
|
|
336
|
+
patch: {
|
|
337
|
+
...(request.locked === undefined ? {} : { locked: request.locked }),
|
|
338
|
+
...(request.hidden === undefined ? {} : { hidden: request.hidden }),
|
|
339
|
+
},
|
|
340
|
+
});
|
|
341
|
+
return { ok: true, result: summarizeEditorSession(ctx.session.getState()) };
|
|
342
|
+
},
|
|
343
|
+
assign_material: (ctx, request) => {
|
|
344
|
+
if (!documentHasAnyId(ctx.session.getState().document, request.ids)) {
|
|
345
|
+
return { ok: false, error: "assign_material matched no objects" };
|
|
346
|
+
}
|
|
347
|
+
ctx.session.dispatch({ type: "assignMaterial", ids: request.ids, materialId: request.materialId });
|
|
348
|
+
return { ok: true, result: summarizeEditorSession(ctx.session.getState()) };
|
|
349
|
+
},
|
|
350
|
+
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { HandlerTable } from "./context.js";
|
|
2
|
+
/** Grid/tile layer verbs — create, edit, paint, fill, and import. */
|
|
3
|
+
export declare const gridHandlers: Pick<HandlerTable, "list_grids" | "get_grid_cell" | "add_grid_layer" | "remove_grid_layer" | "set_grid_layer" | "paint_grid_cells" | "fill_grid_rect" | "flood_fill_grid" | "resize_grid_layer" | "import_grid">;
|