@jgengine/editor 0.11.0 → 0.13.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 +52 -0
- package/README.md +12 -0
- package/dist/AssetBrowser.d.ts +33 -1
- package/dist/AssetBrowser.js +64 -2
- 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 +15 -1
- package/dist/EditorApp.js +147 -35
- package/dist/EditorChrome.d.ts +27 -6
- package/dist/EditorChrome.js +314 -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 +25 -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/streets.d.ts +3 -0
- package/dist/handlers/streets.js +159 -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 +174 -0
- package/dist/mcp/rpcRequest.d.ts +8 -5
- package/dist/mcp/rpcRequest.js +72 -93
- package/dist/mcp/tools.js +105 -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 +91 -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 +35 -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 +8 -4
- package/skills/jgengine/api.md +182 -81
- 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 +403 -20
- 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
package/dist/session.js
CHANGED
|
@@ -1,52 +1,10 @@
|
|
|
1
|
-
import { createDocumentLiveSync, createEditorSession, createRuntimePlayControl,
|
|
2
|
-
import { createTerrainSnapshot, editableTerrainFromSnapshot, } from "@jgengine/core/world/terraform";
|
|
3
|
-
import { resolvePlaceAsset, toEditorMarker } from "@jgengine/core/world/placeAsset";
|
|
4
|
-
import { resolveScatter, resolveScatterRegion, scatterRegionEstimate, scatterRegionFromPath, SCATTER_PATH_KIND, } from "@jgengine/core/world/scatterRegion";
|
|
1
|
+
import { createDocumentLiveSync, createEditorSession, createRuntimePlayControl, installDocumentLiveSync, listEditorKinds, normalizeEditorLayers, resolveCatalogDefinitions, seedEditorCatalogs, } from "@jgengine/core/editor/index";
|
|
5
2
|
import { registerBuiltinSceneKinds } from "@jgengine/core/scene/builtinSceneKinds";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
3
|
+
import { EDITOR_RPC_HANDLERS } from "./handlers/index.js";
|
|
4
|
+
import { emitEditorConsole } from "./shell/consoleSink.js";
|
|
8
5
|
registerBuiltinSceneKinds();
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
const marker = doc.markers.find((m) => m.id === id);
|
|
12
|
-
if (marker !== undefined)
|
|
13
|
-
return { target: "marker", kind: marker.kind, meta: marker.meta };
|
|
14
|
-
const volume = doc.volumes.find((v) => v.id === id);
|
|
15
|
-
if (volume !== undefined)
|
|
16
|
-
return { target: "volume", kind: volume.kind, meta: volume.meta };
|
|
17
|
-
const path = doc.paths.find((p) => p.id === id);
|
|
18
|
-
if (path !== undefined)
|
|
19
|
-
return { target: "path", kind: path.kind, meta: path.meta };
|
|
20
|
-
const note = doc.annotations.find((n) => n.id === id);
|
|
21
|
-
if (note !== undefined)
|
|
22
|
-
return { target: "note", kind: "note", meta: note.meta };
|
|
23
|
-
return null;
|
|
24
|
-
}
|
|
25
|
-
/** Compact grid-layer summary returned by the grid RPC verbs — never the full sparse cell map. */
|
|
26
|
-
function gridLayerSummary(layer) {
|
|
27
|
-
return {
|
|
28
|
-
id: layer.id,
|
|
29
|
-
kind: layer.kind,
|
|
30
|
-
...(layer.label === undefined ? {} : { label: layer.label }),
|
|
31
|
-
cols: layer.cols,
|
|
32
|
-
rows: layer.rows,
|
|
33
|
-
cellSize: layer.cellSize,
|
|
34
|
-
cellCount: gridCellCount(layer),
|
|
35
|
-
visible: layer.visible ?? true,
|
|
36
|
-
paletteIds: (layer.palette ?? []).map((entry) => entry.id),
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
/** Validates a merged meta bag against a registered kind's schema; returns an error string or null. */
|
|
40
|
-
function validateMetaForKind(kind, meta) {
|
|
41
|
-
const definition = getSceneKind(kind);
|
|
42
|
-
if (definition === undefined || meta === undefined)
|
|
43
|
-
return null;
|
|
44
|
-
const issues = validateParams(definition.schema, meta);
|
|
45
|
-
if (issues.length === 0)
|
|
46
|
-
return null;
|
|
47
|
-
return `invalid ${kind} params: ${issues.map((issue) => `${issue.key} (${issue.message})`).join(", ")}`;
|
|
48
|
-
}
|
|
49
|
-
const EDITOR_RUN_MODES = ["edit", "walk", "play"];
|
|
6
|
+
/** The accepted run modes, in canonical order — the runtime guard behind {@link EditorRunMode}. @internal */
|
|
7
|
+
export const EDITOR_RUN_MODES = ["edit", "walk", "play", "hud"];
|
|
50
8
|
const GLOBAL_KEY = "__jgengineEditorHost";
|
|
51
9
|
/** Publishes an editor host globally so devtools and MCP agents can reach it; returns a cleanup fn. */
|
|
52
10
|
export function installEditorHost(api) {
|
|
@@ -65,7 +23,6 @@ export function getEditorHost() {
|
|
|
65
23
|
/** Builds and installs an editor host for a game: session, visibility, assets, and RPC handling. @internal */
|
|
66
24
|
export function createEditorHost(options) {
|
|
67
25
|
const catalogDefinitions = options.catalogs ?? [];
|
|
68
|
-
const catalogById = new Map(catalogDefinitions.map((definition) => [definition.id, definition]));
|
|
69
26
|
const document = seedEditorCatalogs(normalizeEditorLayers(options.layers), catalogDefinitions);
|
|
70
27
|
const session = createEditorSession(document);
|
|
71
28
|
const liveSync = createDocumentLiveSync(document);
|
|
@@ -81,6 +38,7 @@ export function createEditorHost(options) {
|
|
|
81
38
|
let focusTarget = null;
|
|
82
39
|
let assets = [...(options.assets ?? [])];
|
|
83
40
|
let perf = null;
|
|
41
|
+
let terrainSampler = null;
|
|
84
42
|
let mode = "edit";
|
|
85
43
|
let playControl = createRuntimePlayControl(false);
|
|
86
44
|
const visibilityListeners = new Set();
|
|
@@ -142,11 +100,15 @@ export function createEditorHost(options) {
|
|
|
142
100
|
setAssets(next) {
|
|
143
101
|
assets = [...next];
|
|
144
102
|
},
|
|
145
|
-
getCatalogDefinitions: () => catalogDefinitions,
|
|
103
|
+
getCatalogDefinitions: () => resolveCatalogDefinitions(session.getState().document, catalogDefinitions),
|
|
146
104
|
getPerf: () => perf,
|
|
147
105
|
setPerf(sample) {
|
|
148
106
|
perf = sample;
|
|
149
107
|
},
|
|
108
|
+
getTerrainSampler: () => terrainSampler,
|
|
109
|
+
setTerrainSampler(field) {
|
|
110
|
+
terrainSampler = field;
|
|
111
|
+
},
|
|
150
112
|
getMode: () => mode,
|
|
151
113
|
setMode(next) {
|
|
152
114
|
if (next === mode)
|
|
@@ -174,6 +136,9 @@ export function createEditorHost(options) {
|
|
|
174
136
|
};
|
|
175
137
|
for (const listener of playControlListeners)
|
|
176
138
|
listener(playControl);
|
|
139
|
+
// Return the normalized control so callers thread the applied value instead of re-reading a
|
|
140
|
+
// binding that may have been reassigned meanwhile (keeps a published delta in step with it).
|
|
141
|
+
return playControl;
|
|
177
142
|
},
|
|
178
143
|
subscribePlayControl(listener) {
|
|
179
144
|
playControlListeners.add(listener);
|
|
@@ -182,980 +147,34 @@ export function createEditorHost(options) {
|
|
|
182
147
|
};
|
|
183
148
|
},
|
|
184
149
|
handle(request) {
|
|
150
|
+
// One shared context + one try/catch envelope; each verb lives in its per-domain handler module,
|
|
151
|
+
// dispatched through the method → handler table so a missing verb is a compile error, not a fall-through.
|
|
152
|
+
// Recompute the merged catalog set per request so a catalog just added via `add_catalog`
|
|
153
|
+
// (carried on the document, not the static game export) is RPC-addressable immediately.
|
|
154
|
+
const defs = resolveCatalogDefinitions(session.getState().document, catalogDefinitions);
|
|
155
|
+
const ctx = {
|
|
156
|
+
api,
|
|
157
|
+
session,
|
|
158
|
+
liveSync,
|
|
159
|
+
gameId: options.gameId,
|
|
160
|
+
catalogDefinitions: defs,
|
|
161
|
+
catalogById: new Map(defs.map((definition) => [definition.id, definition])),
|
|
162
|
+
dispatchGuarded,
|
|
163
|
+
getTerrainSampler: api.getTerrainSampler,
|
|
164
|
+
};
|
|
185
165
|
try {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
ok: true,
|
|
190
|
-
result: {
|
|
191
|
-
gameId: options.gameId,
|
|
192
|
-
connected: true,
|
|
193
|
-
...summarizeEditorSession(session.getState()),
|
|
194
|
-
canUndo: session.canUndo(),
|
|
195
|
-
canRedo: session.canRedo(),
|
|
196
|
-
perf,
|
|
197
|
-
mode,
|
|
198
|
-
},
|
|
199
|
-
};
|
|
200
|
-
case "set_mode": {
|
|
201
|
-
if (!EDITOR_RUN_MODES.includes(request.mode)) {
|
|
202
|
-
return { ok: false, error: `unknown mode: ${String(request.mode)} (edit | walk | play)` };
|
|
203
|
-
}
|
|
204
|
-
api.setMode(request.mode);
|
|
205
|
-
return { ok: true, result: { mode: request.mode } };
|
|
206
|
-
}
|
|
207
|
-
case "perf_report": {
|
|
208
|
-
const devtoolsGlobal = globalThis.__JG_DEVTOOLS;
|
|
209
|
-
if (devtoolsGlobal?.snapshot === undefined) {
|
|
210
|
-
return {
|
|
211
|
-
ok: false,
|
|
212
|
-
error: "engine devtools not mounted ΓÇö open the browser editor (F2+D panel) first",
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
return { ok: true, result: { perf, report: devtoolsGlobal.snapshot() } };
|
|
216
|
-
}
|
|
217
|
-
case "list_layers": {
|
|
218
|
-
const state = session.getState();
|
|
219
|
-
return {
|
|
220
|
-
ok: true,
|
|
221
|
-
result: {
|
|
222
|
-
kinds: listEditorKinds(state.document),
|
|
223
|
-
visibility,
|
|
224
|
-
document: state.document,
|
|
225
|
-
},
|
|
226
|
-
};
|
|
227
|
-
}
|
|
228
|
-
case "list_catalogs": {
|
|
229
|
-
const state = session.getState();
|
|
230
|
-
const catalogs = catalogDefinitions.map((definition) => {
|
|
231
|
-
const data = findEditorCatalog(state.document, definition.id);
|
|
232
|
-
return {
|
|
233
|
-
id: definition.id,
|
|
234
|
-
label: definition.label,
|
|
235
|
-
schema: definition.schema,
|
|
236
|
-
entryCount: data?.entries.length ?? definition.entries.length,
|
|
237
|
-
entries: (data?.entries ?? definition.entries).map((entry) => ({
|
|
238
|
-
id: entry.id,
|
|
239
|
-
label: entry.label,
|
|
240
|
-
})),
|
|
241
|
-
};
|
|
242
|
-
});
|
|
243
|
-
return { ok: true, result: { catalogs } };
|
|
244
|
-
}
|
|
245
|
-
case "get_catalog_entry": {
|
|
246
|
-
const definition = catalogById.get(request.catalogId);
|
|
247
|
-
if (definition === undefined)
|
|
248
|
-
return { ok: false, error: `catalog not found: ${request.catalogId}` };
|
|
249
|
-
const entry = findEditorCatalogEntry(session.getState().document, request.catalogId, request.entryId);
|
|
250
|
-
if (entry === undefined)
|
|
251
|
-
return { ok: false, error: `catalog entry not found: ${request.catalogId}/${request.entryId}` };
|
|
252
|
-
return {
|
|
253
|
-
ok: true,
|
|
254
|
-
result: {
|
|
255
|
-
catalogId: request.catalogId,
|
|
256
|
-
label: definition.label,
|
|
257
|
-
schema: definition.schema,
|
|
258
|
-
entry,
|
|
259
|
-
},
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
case "set_catalog_entry": {
|
|
263
|
-
const definition = catalogById.get(request.catalogId);
|
|
264
|
-
if (definition === undefined)
|
|
265
|
-
return { ok: false, error: `catalog not found: ${request.catalogId}` };
|
|
266
|
-
const current = findEditorCatalogEntry(session.getState().document, request.catalogId, request.entryId);
|
|
267
|
-
if (current === undefined)
|
|
268
|
-
return { ok: false, error: `catalog entry not found: ${request.catalogId}/${request.entryId}` };
|
|
269
|
-
const merged = { ...current.meta, ...request.patch };
|
|
270
|
-
const invalid = validateParams(definition.schema, merged);
|
|
271
|
-
if (invalid.length > 0) {
|
|
272
|
-
return {
|
|
273
|
-
ok: false,
|
|
274
|
-
error: `invalid ${request.catalogId} params: ${invalid.map((issue) => `${issue.key} (${issue.message})`).join(", ")}`,
|
|
275
|
-
};
|
|
276
|
-
}
|
|
277
|
-
const coalesceKey = Object.keys(request.patch).length === 1
|
|
278
|
-
? `catalog:${request.catalogId}:${request.entryId}:${Object.keys(request.patch)[0]}`
|
|
279
|
-
: `catalog:${request.catalogId}:${request.entryId}`;
|
|
280
|
-
const before = session.getState();
|
|
281
|
-
const state = session.dispatch({
|
|
282
|
-
type: "setCatalogEntry",
|
|
283
|
-
catalogId: request.catalogId,
|
|
284
|
-
entryId: request.entryId,
|
|
285
|
-
patch: {
|
|
286
|
-
meta: merged,
|
|
287
|
-
...(request.label === undefined ? {} : { label: request.label }),
|
|
288
|
-
},
|
|
289
|
-
}, { coalesce: coalesceKey });
|
|
290
|
-
if (state === before) {
|
|
291
|
-
return { ok: false, error: `set_catalog_entry rejected: ${request.catalogId}/${request.entryId}` };
|
|
292
|
-
}
|
|
293
|
-
const entry = findEditorCatalogEntry(state.document, request.catalogId, request.entryId);
|
|
294
|
-
return { ok: true, result: { catalogId: request.catalogId, entry } };
|
|
295
|
-
}
|
|
296
|
-
case "list_selection":
|
|
297
|
-
return { ok: true, result: { selection: session.getState().selection } };
|
|
298
|
-
case "get_marker": {
|
|
299
|
-
const marker = session.getState().document.markers.find((m) => m.id === request.id);
|
|
300
|
-
return marker === undefined
|
|
301
|
-
? { ok: false, error: `marker not found: ${request.id}` }
|
|
302
|
-
: { ok: true, result: marker };
|
|
303
|
-
}
|
|
304
|
-
case "get_volume": {
|
|
305
|
-
const volume = session.getState().document.volumes.find((v) => v.id === request.id);
|
|
306
|
-
return volume === undefined
|
|
307
|
-
? { ok: false, error: `volume not found: ${request.id}` }
|
|
308
|
-
: { ok: true, result: volume };
|
|
309
|
-
}
|
|
310
|
-
case "set_transform": {
|
|
311
|
-
const marker = session.getState().document.markers.find((m) => m.id === request.id);
|
|
312
|
-
const volume = session.getState().document.volumes.find((v) => v.id === request.id);
|
|
313
|
-
const base = marker?.position ?? volume?.center;
|
|
314
|
-
if (base === undefined)
|
|
315
|
-
return { ok: false, error: `id not found: ${request.id}` };
|
|
316
|
-
const { applied, state } = dispatchGuarded({
|
|
317
|
-
type: "setTransform",
|
|
318
|
-
id: request.id,
|
|
319
|
-
position: {
|
|
320
|
-
x: request.x ?? base.x,
|
|
321
|
-
y: request.y ?? base.y,
|
|
322
|
-
z: request.z ?? base.z,
|
|
323
|
-
},
|
|
324
|
-
...(request.rotationY === undefined ? {} : { rotationY: request.rotationY }),
|
|
325
|
-
});
|
|
326
|
-
if (!applied)
|
|
327
|
-
return { ok: false, error: `set_transform rejected: ${request.id} is locked` };
|
|
328
|
-
return { ok: true, result: summarizeEditorSession(state) };
|
|
329
|
-
}
|
|
330
|
-
case "set_volume": {
|
|
331
|
-
const volume = session.getState().document.volumes.find((v) => v.id === request.id);
|
|
332
|
-
if (volume === undefined)
|
|
333
|
-
return { ok: false, error: `volume not found: ${request.id}` };
|
|
334
|
-
session.dispatch({
|
|
335
|
-
type: "setVolume",
|
|
336
|
-
id: request.id,
|
|
337
|
-
patch: {
|
|
338
|
-
...(request.radius === undefined ? {} : { radius: request.radius }),
|
|
339
|
-
...(request.height === undefined ? {} : { height: request.height }),
|
|
340
|
-
...(request.x === undefined && request.y === undefined && request.z === undefined
|
|
341
|
-
? {}
|
|
342
|
-
: {
|
|
343
|
-
center: {
|
|
344
|
-
x: request.x ?? volume.center.x,
|
|
345
|
-
y: request.y ?? volume.center.y,
|
|
346
|
-
z: request.z ?? volume.center.z,
|
|
347
|
-
},
|
|
348
|
-
}),
|
|
349
|
-
},
|
|
350
|
-
});
|
|
351
|
-
return { ok: true, result: session.getState().document.volumes.find((v) => v.id === request.id) };
|
|
352
|
-
}
|
|
353
|
-
case "set_path": {
|
|
354
|
-
const doc = session.getState().document;
|
|
355
|
-
const path = doc.paths.find((p) => p.id === request.id);
|
|
356
|
-
if (path === undefined)
|
|
357
|
-
return { ok: false, error: `path not found: ${request.id}` };
|
|
358
|
-
const merged = request.meta === undefined ? undefined : { ...path.meta, ...request.meta };
|
|
359
|
-
const kind = request.kind ?? path.kind;
|
|
360
|
-
const invalid = validateMetaForKind(kind, merged);
|
|
361
|
-
if (invalid !== null)
|
|
362
|
-
return { ok: false, error: invalid };
|
|
363
|
-
session.dispatch({
|
|
364
|
-
type: "setPath",
|
|
365
|
-
id: request.id,
|
|
366
|
-
patch: {
|
|
367
|
-
...(request.kind === undefined ? {} : { kind: request.kind }),
|
|
368
|
-
...(request.width === undefined ? {} : { width: request.width }),
|
|
369
|
-
...(request.color === undefined ? {} : { color: request.color }),
|
|
370
|
-
...(request.label === undefined ? {} : { label: request.label }),
|
|
371
|
-
...(merged === undefined ? {} : { meta: merged }),
|
|
372
|
-
},
|
|
373
|
-
});
|
|
374
|
-
return { ok: true, result: session.getState().document.paths.find((p) => p.id === request.id) };
|
|
375
|
-
}
|
|
376
|
-
case "set_marker": {
|
|
377
|
-
const doc = session.getState().document;
|
|
378
|
-
const marker = doc.markers.find((m) => m.id === request.id);
|
|
379
|
-
if (marker === undefined)
|
|
380
|
-
return { ok: false, error: `marker not found: ${request.id}` };
|
|
381
|
-
const merged = request.meta === undefined ? undefined : { ...marker.meta, ...request.meta };
|
|
382
|
-
const kind = request.kind ?? marker.kind;
|
|
383
|
-
const invalid = validateMetaForKind(kind, merged);
|
|
384
|
-
if (invalid !== null)
|
|
385
|
-
return { ok: false, error: invalid };
|
|
386
|
-
session.dispatch({
|
|
387
|
-
type: "setMarker",
|
|
388
|
-
id: request.id,
|
|
389
|
-
patch: {
|
|
390
|
-
...(request.kind === undefined ? {} : { kind: request.kind }),
|
|
391
|
-
...(request.color === undefined ? {} : { color: request.color }),
|
|
392
|
-
...(request.label === undefined ? {} : { label: request.label }),
|
|
393
|
-
...(request.rotationY === undefined ? {} : { rotationY: request.rotationY }),
|
|
394
|
-
...(merged === undefined ? {} : { meta: merged }),
|
|
395
|
-
},
|
|
396
|
-
});
|
|
397
|
-
return { ok: true, result: session.getState().document.markers.find((m) => m.id === request.id) };
|
|
398
|
-
}
|
|
399
|
-
case "set_note": {
|
|
400
|
-
const doc = session.getState().document;
|
|
401
|
-
const note = doc.annotations.find((n) => n.id === request.id);
|
|
402
|
-
if (note === undefined)
|
|
403
|
-
return { ok: false, error: `note not found: ${request.id}` };
|
|
404
|
-
const merged = request.meta === undefined ? undefined : { ...note.meta, ...request.meta };
|
|
405
|
-
session.dispatch({
|
|
406
|
-
type: "setNote",
|
|
407
|
-
id: request.id,
|
|
408
|
-
patch: {
|
|
409
|
-
...(request.text === undefined ? {} : { text: request.text }),
|
|
410
|
-
...(merged === undefined ? {} : { meta: merged }),
|
|
411
|
-
},
|
|
412
|
-
});
|
|
413
|
-
return { ok: true, result: session.getState().document.annotations.find((n) => n.id === request.id) };
|
|
414
|
-
}
|
|
415
|
-
case "set_meta": {
|
|
416
|
-
const target = findMetaTarget(session.getState().document, request.id);
|
|
417
|
-
if (target === null)
|
|
418
|
-
return { ok: false, error: `object not found: ${request.id}` };
|
|
419
|
-
const merged = { ...target.meta, ...request.patch };
|
|
420
|
-
const invalid = validateMetaForKind(target.kind, merged);
|
|
421
|
-
if (invalid !== null)
|
|
422
|
-
return { ok: false, error: invalid };
|
|
423
|
-
session.dispatch(target.target === "marker"
|
|
424
|
-
? { type: "setMarker", id: request.id, patch: { meta: merged } }
|
|
425
|
-
: target.target === "volume"
|
|
426
|
-
? { type: "setVolume", id: request.id, patch: { meta: merged } }
|
|
427
|
-
: target.target === "path"
|
|
428
|
-
? { type: "setPath", id: request.id, patch: { meta: merged } }
|
|
429
|
-
: { type: "setNote", id: request.id, patch: { meta: merged } });
|
|
430
|
-
return { ok: true, result: { id: request.id, kind: target.kind, meta: merged } };
|
|
431
|
-
}
|
|
432
|
-
case "select":
|
|
433
|
-
session.dispatch({ type: "select", ids: request.ids });
|
|
434
|
-
return { ok: true, result: { selection: session.getState().selection } };
|
|
435
|
-
case "clear_selection":
|
|
436
|
-
session.dispatch({ type: "clearSelection" });
|
|
437
|
-
return { ok: true, result: { selection: [] } };
|
|
438
|
-
case "camera_goto": {
|
|
439
|
-
if (request.id !== undefined) {
|
|
440
|
-
const state = session.getState();
|
|
441
|
-
const marker = state.document.markers.find((m) => m.id === request.id);
|
|
442
|
-
const volume = state.document.volumes.find((v) => v.id === request.id);
|
|
443
|
-
const point = marker?.position ?? volume?.center;
|
|
444
|
-
if (point === undefined)
|
|
445
|
-
return { ok: false, error: `id not found: ${request.id}` };
|
|
446
|
-
api.setFocusTarget({ ...point });
|
|
447
|
-
session.dispatch({ type: "select", ids: [request.id] });
|
|
448
|
-
return { ok: true, result: { target: point } };
|
|
449
|
-
}
|
|
450
|
-
if (request.x === undefined || request.z === undefined) {
|
|
451
|
-
return { ok: false, error: "camera_goto requires id or x/z" };
|
|
452
|
-
}
|
|
453
|
-
const target = { x: request.x, y: request.y ?? 0, z: request.z };
|
|
454
|
-
api.setFocusTarget(target);
|
|
455
|
-
return { ok: true, result: { target } };
|
|
456
|
-
}
|
|
457
|
-
case "camera_frame": {
|
|
458
|
-
const bounds = editorDocumentBounds(session.getState().document);
|
|
459
|
-
if (bounds === null)
|
|
460
|
-
return { ok: false, error: "document is empty" };
|
|
461
|
-
const target = {
|
|
462
|
-
x: (bounds.min.x + bounds.max.x) / 2,
|
|
463
|
-
y: (bounds.min.y + bounds.max.y) / 2,
|
|
464
|
-
z: (bounds.min.z + bounds.max.z) / 2,
|
|
465
|
-
};
|
|
466
|
-
api.setFocusTarget(target);
|
|
467
|
-
return { ok: true, result: { target, bounds } };
|
|
468
|
-
}
|
|
469
|
-
case "scene_summary":
|
|
470
|
-
return {
|
|
471
|
-
ok: true,
|
|
472
|
-
result: {
|
|
473
|
-
gameId: options.gameId,
|
|
474
|
-
kinds: listEditorKinds(session.getState().document),
|
|
475
|
-
...summarizeEditorSession(session.getState()),
|
|
476
|
-
bounds: editorDocumentBounds(session.getState().document),
|
|
477
|
-
},
|
|
478
|
-
};
|
|
479
|
-
case "export_document":
|
|
480
|
-
return { ok: true, result: { json: session.exportJson(true) } };
|
|
481
|
-
case "import_document":
|
|
482
|
-
session.dispatch({ type: "importJson", json: request.json });
|
|
483
|
-
return { ok: true, result: summarizeEditorSession(session.getState()) };
|
|
484
|
-
case "push_document_patch": {
|
|
485
|
-
const force = request.force === true;
|
|
486
|
-
const patch = request.patch;
|
|
487
|
-
if (!force && patch.baseRevision !== liveSync.getRevision()) {
|
|
488
|
-
return {
|
|
489
|
-
ok: false,
|
|
490
|
-
error: `baseRevision mismatch: patch=${patch.baseRevision} current=${liveSync.getRevision()}`,
|
|
491
|
-
};
|
|
492
|
-
}
|
|
493
|
-
if (patch.type === "snapshot") {
|
|
494
|
-
session.dispatch({ type: "replaceDocument", document: patch.document });
|
|
495
|
-
return {
|
|
496
|
-
ok: true,
|
|
497
|
-
result: {
|
|
498
|
-
revision: liveSync.getRevision(),
|
|
499
|
-
...summarizeEditorSession(session.getState()),
|
|
500
|
-
},
|
|
501
|
-
};
|
|
502
|
-
}
|
|
503
|
-
if (patch.commands.length === 0)
|
|
504
|
-
return { ok: false, error: "commands patch is empty" };
|
|
505
|
-
for (const command of patch.commands) {
|
|
506
|
-
const { applied } = dispatchGuarded(command);
|
|
507
|
-
if (!applied && command.type !== "select" && command.type !== "clearSelection") {
|
|
508
|
-
return { ok: false, error: `${command.type} rejected while applying patch` };
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
return {
|
|
512
|
-
ok: true,
|
|
513
|
-
result: {
|
|
514
|
-
revision: liveSync.getRevision(),
|
|
515
|
-
...summarizeEditorSession(session.getState()),
|
|
516
|
-
},
|
|
517
|
-
};
|
|
518
|
-
}
|
|
519
|
-
case "pull_document_patches":
|
|
520
|
-
return {
|
|
521
|
-
ok: true,
|
|
522
|
-
result: {
|
|
523
|
-
revision: liveSync.getRevision(),
|
|
524
|
-
patches: liveSync.pullPatches(request.sinceRevision ?? 0),
|
|
525
|
-
},
|
|
526
|
-
};
|
|
527
|
-
case "document_revision":
|
|
528
|
-
return {
|
|
529
|
-
ok: true,
|
|
530
|
-
result: {
|
|
531
|
-
revision: liveSync.getRevision(),
|
|
532
|
-
...(request.includeDocument === true ? { document: liveSync.getDocument() } : {}),
|
|
533
|
-
},
|
|
534
|
-
};
|
|
535
|
-
case "push_runtime_delta": {
|
|
536
|
-
const delta = liveSync.pushRuntimeDelta({
|
|
537
|
-
at: request.at ?? Date.now(),
|
|
538
|
-
...(request.entities === undefined ? {} : { entities: request.entities }),
|
|
539
|
-
...(request.removeIds === undefined ? {} : { removeIds: request.removeIds }),
|
|
540
|
-
...(request.tunables === undefined ? {} : { tunables: request.tunables }),
|
|
541
|
-
});
|
|
542
|
-
return { ok: true, result: delta };
|
|
543
|
-
}
|
|
544
|
-
case "pull_runtime_deltas":
|
|
545
|
-
return {
|
|
546
|
-
ok: true,
|
|
547
|
-
result: {
|
|
548
|
-
seq: liveSync.getRuntimeState().seq,
|
|
549
|
-
deltas: liveSync.pullRuntimeDeltas(request.sinceSeq ?? 0),
|
|
550
|
-
...(request.includeSnapshot === true ? { snapshot: liveSync.getRuntimeState() } : {}),
|
|
551
|
-
},
|
|
552
|
-
};
|
|
553
|
-
case "runtime_snapshot":
|
|
554
|
-
return { ok: true, result: liveSync.getRuntimeState() };
|
|
555
|
-
case "runtime_summary":
|
|
556
|
-
return {
|
|
557
|
-
ok: true,
|
|
558
|
-
result: summarizeRuntimeInspector(liveSync.getRuntimeState(), liveSync.getRuntimeOverrides(), playControl),
|
|
559
|
-
};
|
|
560
|
-
case "runtime_get": {
|
|
561
|
-
if (request.id.length === 0)
|
|
562
|
-
return { ok: false, error: "runtime_get requires id" };
|
|
563
|
-
const got = getRuntimeInspectorValue(liveSync.getRuntimeState(), liveSync.getRuntimeOverrides(), request.id, request.path);
|
|
564
|
-
if (got.kind === "missing") {
|
|
565
|
-
return { ok: false, error: `runtime entity/tunable not found: ${request.id}` };
|
|
566
|
-
}
|
|
567
|
-
return { ok: true, result: got };
|
|
568
|
-
}
|
|
569
|
-
case "runtime_set": {
|
|
570
|
-
if (request.id.length === 0)
|
|
571
|
-
return { ok: false, error: "runtime_set requires id" };
|
|
572
|
-
const plan = planRuntimeInspectorSet(session.getState().document, {
|
|
573
|
-
id: request.id,
|
|
574
|
-
...(request.path === undefined ? {} : { path: request.path }),
|
|
575
|
-
...(request.value === undefined ? {} : { value: request.value }),
|
|
576
|
-
...(request.position === undefined ? {} : { position: request.position }),
|
|
577
|
-
...(request.rotationY === undefined ? {} : { rotationY: request.rotationY }),
|
|
578
|
-
...(request.values === undefined ? {} : { values: request.values }),
|
|
579
|
-
...(request.writeBack === undefined ? {} : { writeBack: request.writeBack }),
|
|
580
|
-
});
|
|
581
|
-
if (plan.error !== undefined)
|
|
582
|
-
return { ok: false, error: plan.error };
|
|
583
|
-
if (plan.tunable !== undefined) {
|
|
584
|
-
liveSync.pushRuntimeDelta({
|
|
585
|
-
at: Date.now(),
|
|
586
|
-
tunables: { [plan.tunable.key]: plan.tunable.value },
|
|
587
|
-
});
|
|
588
|
-
return {
|
|
589
|
-
ok: true,
|
|
590
|
-
result: {
|
|
591
|
-
kind: "tunable",
|
|
592
|
-
key: plan.tunable.key,
|
|
593
|
-
value: plan.tunable.value,
|
|
594
|
-
writeBack: false,
|
|
595
|
-
...summarizeRuntimeInspector(liveSync.getRuntimeState(), liveSync.getRuntimeOverrides(), playControl),
|
|
596
|
-
},
|
|
597
|
-
};
|
|
598
|
-
}
|
|
599
|
-
if (plan.entity === undefined)
|
|
600
|
-
return { ok: false, error: "runtime_set produced no entity" };
|
|
601
|
-
liveSync.setRuntimeOverride(plan.entity);
|
|
602
|
-
liveSync.pushRuntimeDelta({ at: Date.now(), entities: [plan.entity] });
|
|
603
|
-
let wroteBack = false;
|
|
604
|
-
let lastState = session.getState();
|
|
605
|
-
for (const command of plan.writeBackCommands) {
|
|
606
|
-
const { applied, state } = dispatchGuarded(command);
|
|
607
|
-
if (!applied) {
|
|
608
|
-
return { ok: false, error: `runtime_set write-back rejected: ${command.type}` };
|
|
609
|
-
}
|
|
610
|
-
lastState = state;
|
|
611
|
-
wroteBack = true;
|
|
612
|
-
}
|
|
613
|
-
if (wroteBack)
|
|
614
|
-
liveSync.clearRuntimeOverride(plan.entity.id);
|
|
615
|
-
return {
|
|
616
|
-
ok: true,
|
|
617
|
-
result: {
|
|
618
|
-
kind: "entity",
|
|
619
|
-
entity: plan.entity,
|
|
620
|
-
writeBack: wroteBack,
|
|
621
|
-
revision: liveSync.getRevision(),
|
|
622
|
-
...summarizeEditorSession(lastState),
|
|
623
|
-
},
|
|
624
|
-
};
|
|
625
|
-
}
|
|
626
|
-
case "runtime_pause": {
|
|
627
|
-
api.setPlayControl({ paused: true, pendingSteps: 0 });
|
|
628
|
-
liveSync.pushRuntimeDelta({ at: Date.now(), tunables: { paused: true } });
|
|
629
|
-
return { ok: true, result: { play: playControl } };
|
|
630
|
-
}
|
|
631
|
-
case "runtime_resume": {
|
|
632
|
-
api.setPlayControl({ paused: false, pendingSteps: 0 });
|
|
633
|
-
liveSync.pushRuntimeDelta({ at: Date.now(), tunables: { paused: false } });
|
|
634
|
-
return { ok: true, result: { play: playControl } };
|
|
635
|
-
}
|
|
636
|
-
case "runtime_step": {
|
|
637
|
-
const frames = request.frames === undefined ? 1 : Math.max(1, Math.floor(request.frames));
|
|
638
|
-
api.setPlayControl({ paused: true, pendingSteps: playControl.pendingSteps + frames });
|
|
639
|
-
liveSync.pushRuntimeDelta({ at: Date.now(), tunables: { paused: true, pendingSteps: playControl.pendingSteps } });
|
|
640
|
-
return { ok: true, result: { play: playControl } };
|
|
641
|
-
}
|
|
642
|
-
case "set_runtime_override": {
|
|
643
|
-
liveSync.setRuntimeOverride(request.entity);
|
|
644
|
-
return { ok: true, result: { overrides: liveSync.getRuntimeOverrides() } };
|
|
645
|
-
}
|
|
646
|
-
case "clear_runtime_override": {
|
|
647
|
-
liveSync.clearRuntimeOverride(request.id);
|
|
648
|
-
return { ok: true, result: { overrides: liveSync.getRuntimeOverrides() } };
|
|
649
|
-
}
|
|
650
|
-
case "write_back_override": {
|
|
651
|
-
const entity = liveSync.getRuntimeOverrides()[request.id];
|
|
652
|
-
if (entity === undefined) {
|
|
653
|
-
return { ok: false, error: `no runtime override for "${request.id}"` };
|
|
654
|
-
}
|
|
655
|
-
const transform = runtimeEntityWriteBackCommand(session.getState().document, entity);
|
|
656
|
-
const meta = runtimeEntityMetaWriteBackCommand(session.getState().document, entity);
|
|
657
|
-
if (transform === null && meta === null) {
|
|
658
|
-
return {
|
|
659
|
-
ok: false,
|
|
660
|
-
error: `override "${request.id}" has nothing to write back into the document`,
|
|
661
|
-
};
|
|
662
|
-
}
|
|
663
|
-
let lastState = session.getState();
|
|
664
|
-
for (const command of [transform, meta]) {
|
|
665
|
-
if (command === null)
|
|
666
|
-
continue;
|
|
667
|
-
const { applied, state } = dispatchGuarded(command);
|
|
668
|
-
if (!applied)
|
|
669
|
-
return { ok: false, error: `write_back_override rejected for "${request.id}"` };
|
|
670
|
-
lastState = state;
|
|
671
|
-
}
|
|
672
|
-
liveSync.clearRuntimeOverride(request.id);
|
|
673
|
-
return {
|
|
674
|
-
ok: true,
|
|
675
|
-
result: {
|
|
676
|
-
revision: liveSync.getRevision(),
|
|
677
|
-
...summarizeEditorSession(lastState),
|
|
678
|
-
},
|
|
679
|
-
};
|
|
680
|
-
}
|
|
681
|
-
case "dispatch": {
|
|
682
|
-
const { applied, state } = dispatchGuarded(request.command);
|
|
683
|
-
if (!applied)
|
|
684
|
-
return { ok: false, error: `${request.command.type} rejected: no effect` };
|
|
685
|
-
return { ok: true, result: summarizeEditorSession(state) };
|
|
686
|
-
}
|
|
687
|
-
case "undo": {
|
|
688
|
-
const { applied, state } = dispatchGuarded({ type: "undo" });
|
|
689
|
-
if (!applied)
|
|
690
|
-
return { ok: false, error: "nothing to undo" };
|
|
691
|
-
return { ok: true, result: summarizeEditorSession(state) };
|
|
692
|
-
}
|
|
693
|
-
case "redo": {
|
|
694
|
-
const { applied, state } = dispatchGuarded({ type: "redo" });
|
|
695
|
-
if (!applied)
|
|
696
|
-
return { ok: false, error: "nothing to redo" };
|
|
697
|
-
return { ok: true, result: summarizeEditorSession(state) };
|
|
698
|
-
}
|
|
699
|
-
case "list_assets":
|
|
700
|
-
return { ok: true, result: { assets } };
|
|
701
|
-
case "place_asset": {
|
|
702
|
-
const focus = focusTarget ?? { x: 0, y: 0, z: 0 };
|
|
703
|
-
const asset = assets.find((entry) => entry.id === request.id);
|
|
704
|
-
const placed = resolvePlaceAsset({
|
|
705
|
-
assetId: request.id,
|
|
706
|
-
position: {
|
|
707
|
-
x: request.x ?? focus.x,
|
|
708
|
-
y: request.y ?? focus.y,
|
|
709
|
-
z: request.z ?? focus.z,
|
|
710
|
-
},
|
|
711
|
-
kind: request.kind,
|
|
712
|
-
knownKind: asset?.kind,
|
|
713
|
-
knownLabel: asset?.label,
|
|
714
|
-
knownUrl: asset?.url,
|
|
715
|
-
});
|
|
716
|
-
const marker = toEditorMarker(placed);
|
|
717
|
-
session.dispatch({ type: "addMarker", marker });
|
|
718
|
-
return {
|
|
719
|
-
ok: true,
|
|
720
|
-
result: {
|
|
721
|
-
id: marker.id,
|
|
722
|
-
position: marker.position,
|
|
723
|
-
marker: session.getState().document.markers.find((entry) => entry.id === marker.id),
|
|
724
|
-
},
|
|
725
|
-
};
|
|
726
|
-
}
|
|
727
|
-
case "create_terrain": {
|
|
728
|
-
const width = request.width ?? 200;
|
|
729
|
-
const depth = request.depth ?? 200;
|
|
730
|
-
const cx = request.centerX ?? 0;
|
|
731
|
-
const cz = request.centerZ ?? 0;
|
|
732
|
-
const terrain = createTerrainSnapshot({
|
|
733
|
-
bounds: { minX: cx - width / 2, minZ: cz - depth / 2, maxX: cx + width / 2, maxZ: cz + depth / 2 },
|
|
734
|
-
cellSize: request.cellSize ?? 2,
|
|
735
|
-
});
|
|
736
|
-
session.dispatch({ type: "setTerrain", terrain });
|
|
737
|
-
return { ok: true, result: { cols: terrain.cols, rows: terrain.rows, cellSize: terrain.cellSize } };
|
|
738
|
-
}
|
|
739
|
-
case "sculpt_terrain": {
|
|
740
|
-
const terrain = session.getState().document.terrain;
|
|
741
|
-
if (terrain === undefined)
|
|
742
|
-
return { ok: false, error: "no terrain ΓÇö call create_terrain first" };
|
|
743
|
-
const live = editableTerrainFromSnapshot(terrain);
|
|
744
|
-
const edit = {
|
|
745
|
-
mode: request.mode,
|
|
746
|
-
center: [request.x, request.z],
|
|
747
|
-
radius: request.radius ?? 8,
|
|
748
|
-
strength: request.strength ?? 1,
|
|
749
|
-
...(request.target === undefined ? {} : { target: request.target }),
|
|
750
|
-
...(request.toX === undefined || request.toZ === undefined ? {} : { to: [request.toX, request.toZ] }),
|
|
751
|
-
...(request.seed === undefined ? {} : { seed: request.seed }),
|
|
752
|
-
...(request.shape === undefined ? {} : { shape: request.shape }),
|
|
753
|
-
};
|
|
754
|
-
const delta = live.editDelta(edit);
|
|
755
|
-
if (delta.indices.length === 0)
|
|
756
|
-
return { ok: false, error: "brush touched no vertices (check radius/position)" };
|
|
757
|
-
session.dispatch({ type: "sculptTerrain", delta });
|
|
758
|
-
return { ok: true, result: { changed: delta.indices.length, canUndo: session.canUndo() } };
|
|
759
|
-
}
|
|
760
|
-
case "terrain_summary": {
|
|
761
|
-
const terrain = session.getState().document.terrain;
|
|
762
|
-
if (terrain === undefined)
|
|
763
|
-
return { ok: true, result: { terrain: null } };
|
|
764
|
-
let min = Infinity;
|
|
765
|
-
let max = -Infinity;
|
|
766
|
-
let nonZero = 0;
|
|
767
|
-
for (const value of terrain.offsets) {
|
|
768
|
-
if (value < min)
|
|
769
|
-
min = value;
|
|
770
|
-
if (value > max)
|
|
771
|
-
max = value;
|
|
772
|
-
if (value !== 0)
|
|
773
|
-
nonZero += 1;
|
|
774
|
-
}
|
|
775
|
-
return {
|
|
776
|
-
ok: true,
|
|
777
|
-
result: {
|
|
778
|
-
cols: terrain.cols,
|
|
779
|
-
rows: terrain.rows,
|
|
780
|
-
cellSize: terrain.cellSize,
|
|
781
|
-
bounds: terrain.bounds,
|
|
782
|
-
minOffset: terrain.offsets.length === 0 ? 0 : min,
|
|
783
|
-
maxOffset: terrain.offsets.length === 0 ? 0 : max,
|
|
784
|
-
editedVertices: nonZero,
|
|
785
|
-
paintedCells: terrain.surfaces.filter((surface) => surface !== null).length,
|
|
786
|
-
},
|
|
787
|
-
};
|
|
788
|
-
}
|
|
789
|
-
case "paint_terrain": {
|
|
790
|
-
const terrain = session.getState().document.terrain;
|
|
791
|
-
if (terrain === undefined)
|
|
792
|
-
return { ok: false, error: "no terrain ΓÇö call create_terrain first" };
|
|
793
|
-
const live = editableTerrainFromSnapshot(terrain);
|
|
794
|
-
const delta = live.paintDelta({
|
|
795
|
-
mode: "paint",
|
|
796
|
-
center: [request.x, request.z],
|
|
797
|
-
radius: request.radius ?? 8,
|
|
798
|
-
surface: request.surface,
|
|
799
|
-
...(request.shape === undefined ? {} : { shape: request.shape }),
|
|
800
|
-
});
|
|
801
|
-
if (delta.indices.length === 0)
|
|
802
|
-
return { ok: false, error: "paint touched no cells (check radius/position)" };
|
|
803
|
-
session.dispatch({ type: "paintTerrain", delta });
|
|
804
|
-
return { ok: true, result: { changed: delta.indices.length, canUndo: session.canUndo() } };
|
|
805
|
-
}
|
|
806
|
-
case "fill_terrain": {
|
|
807
|
-
const terrain = session.getState().document.terrain;
|
|
808
|
-
if (terrain === undefined)
|
|
809
|
-
return { ok: false, error: "no terrain ΓÇö call create_terrain first" };
|
|
810
|
-
const live = editableTerrainFromSnapshot(terrain);
|
|
811
|
-
const delta = live.fillSurfaceDelta(request.surface);
|
|
812
|
-
if (delta.indices.length === 0)
|
|
813
|
-
return { ok: true, result: { changed: 0 } };
|
|
814
|
-
session.dispatch({ type: "paintTerrain", delta });
|
|
815
|
-
return { ok: true, result: { changed: delta.indices.length } };
|
|
816
|
-
}
|
|
817
|
-
case "auto_paint": {
|
|
818
|
-
const terrain = session.getState().document.terrain;
|
|
819
|
-
if (terrain === undefined)
|
|
820
|
-
return { ok: false, error: "no terrain ΓÇö call create_terrain first" };
|
|
821
|
-
const live = editableTerrainFromSnapshot(terrain);
|
|
822
|
-
const rule = {
|
|
823
|
-
surface: request.surface,
|
|
824
|
-
...(request.minSlope === undefined ? {} : { minSlope: request.minSlope }),
|
|
825
|
-
...(request.maxSlope === undefined ? {} : { maxSlope: request.maxSlope }),
|
|
826
|
-
...(request.minHeight === undefined ? {} : { minHeight: request.minHeight }),
|
|
827
|
-
...(request.maxHeight === undefined ? {} : { maxHeight: request.maxHeight }),
|
|
828
|
-
};
|
|
829
|
-
const delta = live.autoPaintDelta(rule);
|
|
830
|
-
if (delta.indices.length === 0)
|
|
831
|
-
return { ok: true, result: { changed: 0 } };
|
|
832
|
-
session.dispatch({ type: "paintTerrain", delta });
|
|
833
|
-
return { ok: true, result: { changed: delta.indices.length } };
|
|
834
|
-
}
|
|
835
|
-
case "terrain_materials":
|
|
836
|
-
return { ok: true, result: { materials: TERRAIN_MATERIALS } };
|
|
837
|
-
case "terrain_layers": {
|
|
838
|
-
const terrain = session.getState().document.terrain;
|
|
839
|
-
return { ok: true, result: { layers: terrain?.layers ?? [] } };
|
|
840
|
-
}
|
|
841
|
-
case "set_terrain_layers": {
|
|
842
|
-
if (session.getState().document.terrain === undefined) {
|
|
843
|
-
return { ok: false, error: "no terrain ΓÇö call create_terrain first" };
|
|
844
|
-
}
|
|
845
|
-
session.dispatch({ type: "setTerrainLayers", layers: request.layers });
|
|
846
|
-
return { ok: true, result: { layers: session.getState().document.terrain?.layers ?? [] } };
|
|
847
|
-
}
|
|
848
|
-
case "blend_terrain": {
|
|
849
|
-
const terrain = session.getState().document.terrain;
|
|
850
|
-
if (terrain === undefined)
|
|
851
|
-
return { ok: false, error: "no terrain ΓÇö call create_terrain first" };
|
|
852
|
-
// Auto-add the surface as a layer if the stack does not carry it yet.
|
|
853
|
-
const layers = terrain.layers ?? [];
|
|
854
|
-
if (!layers.some((layer) => layer.surface === request.surface)) {
|
|
855
|
-
const next = [...layers, { id: request.surface, surface: request.surface }];
|
|
856
|
-
session.dispatch({ type: "setTerrainLayers", layers: next });
|
|
857
|
-
}
|
|
858
|
-
const live = editableTerrainFromSnapshot(session.getState().document.terrain);
|
|
859
|
-
const delta = live.blendPaintDelta({
|
|
860
|
-
mode: "paint",
|
|
861
|
-
center: [request.x, request.z],
|
|
862
|
-
radius: request.radius ?? 8,
|
|
863
|
-
surface: request.surface,
|
|
864
|
-
strength: request.strength ?? 1,
|
|
865
|
-
...(request.shape === undefined ? {} : { shape: request.shape }),
|
|
866
|
-
});
|
|
867
|
-
if (delta.indices.length === 0)
|
|
868
|
-
return { ok: false, error: "blend touched no cells (check radius/position)" };
|
|
869
|
-
session.dispatch({ type: "blendTerrain", delta });
|
|
870
|
-
return { ok: true, result: { changed: delta.indices.length, layers: session.getState().document.terrain?.layers ?? [] } };
|
|
871
|
-
}
|
|
872
|
-
case "convert_scatter": {
|
|
873
|
-
const doc = session.getState().document;
|
|
874
|
-
const path = doc.paths.find((entry) => entry.id === request.pathId);
|
|
875
|
-
if (path === undefined)
|
|
876
|
-
return { ok: false, error: `path not found: ${request.pathId}` };
|
|
877
|
-
const region = scatterRegionFromPath(path);
|
|
878
|
-
if (region === null)
|
|
879
|
-
return { ok: false, error: `not a scatter region: ${request.pathId}` };
|
|
880
|
-
const terrainSnapshot = doc.terrain;
|
|
881
|
-
const terrain = terrainSnapshot === undefined ? undefined : editableTerrainFromSnapshot(terrainSnapshot);
|
|
882
|
-
const instances = resolveScatterRegion(region, terrain);
|
|
883
|
-
const markers = instances.map((instance) => ({
|
|
884
|
-
id: instance.id.replace(/[^a-zA-Z0-9_]/g, "_"),
|
|
885
|
-
kind: "prop",
|
|
886
|
-
position: { x: instance.x, y: instance.y, z: instance.z },
|
|
887
|
-
rotationY: instance.rotationY,
|
|
888
|
-
meta: { item: instance.item, scale: instance.scale, fromScatter: request.pathId },
|
|
889
|
-
}));
|
|
890
|
-
session.dispatch({ type: "convertScatterToObjects", pathId: request.pathId, markers });
|
|
891
|
-
return { ok: true, result: { created: markers.length, removedPath: request.pathId } };
|
|
892
|
-
}
|
|
893
|
-
case "add_foliage": {
|
|
894
|
-
if (request.points.length < 3)
|
|
895
|
-
return { ok: false, error: "add_foliage needs at least 3 polygon points" };
|
|
896
|
-
const id = `foliage_${Date.now().toString(36)}`;
|
|
897
|
-
session.dispatch({
|
|
898
|
-
type: "addPath",
|
|
899
|
-
path: {
|
|
900
|
-
id,
|
|
901
|
-
kind: SCATTER_PATH_KIND,
|
|
902
|
-
points: request.points.map((point) => ({ x: point.x, y: 0, z: point.z })),
|
|
903
|
-
label: "foliage",
|
|
904
|
-
meta: {
|
|
905
|
-
density: request.density ?? 0.15,
|
|
906
|
-
...(request.item === undefined ? {} : { item: request.item }),
|
|
907
|
-
...(request.seed === undefined ? {} : { seed: request.seed }),
|
|
908
|
-
...(request.minSpacing === undefined ? {} : { minSpacing: request.minSpacing }),
|
|
909
|
-
},
|
|
910
|
-
},
|
|
911
|
-
});
|
|
912
|
-
const path = session.getState().document.paths.find((p) => p.id === id);
|
|
913
|
-
return { ok: true, result: { id, estimate: scatterRegionEstimate(path) } };
|
|
914
|
-
}
|
|
915
|
-
case "scatter_summary": {
|
|
916
|
-
const doc = session.getState().document;
|
|
917
|
-
const regions = doc.paths.filter((path) => path.kind === SCATTER_PATH_KIND).length;
|
|
918
|
-
const instances = resolveScatter(doc).length;
|
|
919
|
-
return { ok: true, result: { regions, instances } };
|
|
920
|
-
}
|
|
921
|
-
case "set_parent": {
|
|
922
|
-
const { applied, state } = dispatchGuarded({
|
|
923
|
-
type: "setParent",
|
|
924
|
-
ids: request.ids,
|
|
925
|
-
parentId: request.parentId,
|
|
926
|
-
});
|
|
927
|
-
if (!applied)
|
|
928
|
-
return { ok: false, error: "set_parent rejected: cyclic parent or empty selection" };
|
|
929
|
-
const doc = state.document;
|
|
930
|
-
return {
|
|
931
|
-
ok: true,
|
|
932
|
-
result: { roots: editorRoots(doc), parents: request.ids.map((id) => ({ id, parentId: editorParentOf(doc, id) ?? null })) },
|
|
933
|
-
};
|
|
934
|
-
}
|
|
935
|
-
case "hierarchy": {
|
|
936
|
-
const doc = session.getState().document;
|
|
937
|
-
const roots = editorRoots(doc);
|
|
938
|
-
return {
|
|
939
|
-
ok: true,
|
|
940
|
-
result: { roots, tree: roots.map((id) => ({ id, children: editorChildren(doc, id) })) },
|
|
941
|
-
};
|
|
942
|
-
}
|
|
943
|
-
case "list_prefabs":
|
|
944
|
-
return { ok: true, result: { prefabs: session.getState().document.prefabs } };
|
|
945
|
-
case "create_prefab": {
|
|
946
|
-
if (request.ids.length === 0)
|
|
947
|
-
return { ok: false, error: "create_prefab needs at least one id" };
|
|
948
|
-
session.dispatch({ type: "createPrefab", id: request.id, name: request.name, ids: request.ids });
|
|
949
|
-
const prefab = findEditorPrefab(session.getState().document, request.id);
|
|
950
|
-
return prefab === undefined
|
|
951
|
-
? { ok: false, error: `prefab not created: ${request.id}` }
|
|
952
|
-
: { ok: true, result: prefab };
|
|
953
|
-
}
|
|
954
|
-
case "insert_prefab": {
|
|
955
|
-
const prefab = findEditorPrefab(session.getState().document, request.prefabId);
|
|
956
|
-
if (prefab === undefined)
|
|
957
|
-
return { ok: false, error: `prefab not found: ${request.prefabId}` };
|
|
958
|
-
const at = focusTarget ?? { x: 0, y: 0, z: 0 };
|
|
959
|
-
session.dispatch({
|
|
960
|
-
type: "insertPrefab",
|
|
961
|
-
prefabId: request.prefabId,
|
|
962
|
-
at: { x: request.x ?? at.x, y: request.y ?? at.y, z: request.z ?? at.z },
|
|
963
|
-
});
|
|
964
|
-
return { ok: true, result: summarizeEditorSession(session.getState()) };
|
|
965
|
-
}
|
|
966
|
-
case "detach_prefab_instance":
|
|
967
|
-
session.dispatch({ type: "detachPrefabInstance", instanceId: request.instanceId });
|
|
968
|
-
return { ok: true, result: summarizeEditorSession(session.getState()) };
|
|
969
|
-
case "delete_prefab":
|
|
970
|
-
session.dispatch({ type: "deletePrefab", prefabId: request.prefabId });
|
|
971
|
-
return { ok: true, result: { prefabs: session.getState().document.prefabs } };
|
|
972
|
-
case "list_collections":
|
|
973
|
-
return { ok: true, result: { collections: session.getState().document.collections } };
|
|
974
|
-
case "create_collection":
|
|
975
|
-
session.dispatch({
|
|
976
|
-
type: "createCollection",
|
|
977
|
-
id: request.id,
|
|
978
|
-
name: request.name,
|
|
979
|
-
...(request.memberIds === undefined ? {} : { memberIds: request.memberIds }),
|
|
980
|
-
});
|
|
981
|
-
return { ok: true, result: findEditorCollection(session.getState().document, request.id) };
|
|
982
|
-
case "rename_collection":
|
|
983
|
-
session.dispatch({ type: "renameCollection", id: request.id, name: request.name });
|
|
984
|
-
return { ok: true, result: findEditorCollection(session.getState().document, request.id) };
|
|
985
|
-
case "delete_collection":
|
|
986
|
-
session.dispatch({ type: "deleteCollection", id: request.id });
|
|
987
|
-
return { ok: true, result: { collections: session.getState().document.collections } };
|
|
988
|
-
case "set_collection_members":
|
|
989
|
-
session.dispatch({ type: "setCollectionMembers", id: request.id, memberIds: request.memberIds });
|
|
990
|
-
return { ok: true, result: findEditorCollection(session.getState().document, request.id) };
|
|
991
|
-
case "add_to_collection":
|
|
992
|
-
session.dispatch({ type: "addToCollection", id: request.id, ids: request.ids });
|
|
993
|
-
return { ok: true, result: findEditorCollection(session.getState().document, request.id) };
|
|
994
|
-
case "remove_from_collection":
|
|
995
|
-
session.dispatch({ type: "removeFromCollection", id: request.id, ids: request.ids });
|
|
996
|
-
return { ok: true, result: findEditorCollection(session.getState().document, request.id) };
|
|
997
|
-
case "set_collection_flags":
|
|
998
|
-
session.dispatch({
|
|
999
|
-
type: "setCollectionFlags",
|
|
1000
|
-
id: request.id,
|
|
1001
|
-
patch: {
|
|
1002
|
-
...(request.color === undefined ? {} : { color: request.color }),
|
|
1003
|
-
...(request.locked === undefined ? {} : { locked: request.locked }),
|
|
1004
|
-
...(request.visible === undefined ? {} : { visible: request.visible }),
|
|
1005
|
-
},
|
|
1006
|
-
});
|
|
1007
|
-
return { ok: true, result: findEditorCollection(session.getState().document, request.id) };
|
|
1008
|
-
case "select_collection": {
|
|
1009
|
-
const collection = findEditorCollection(session.getState().document, request.id);
|
|
1010
|
-
if (collection === undefined)
|
|
1011
|
-
return { ok: false, error: `collection not found: ${request.id}` };
|
|
1012
|
-
session.dispatch({ type: "selectCollection", id: request.id });
|
|
1013
|
-
return { ok: true, result: { selection: session.getState().selection } };
|
|
1014
|
-
}
|
|
1015
|
-
case "batch_set_properties":
|
|
1016
|
-
session.dispatch({
|
|
1017
|
-
type: "batchSetProperties",
|
|
1018
|
-
ids: request.ids,
|
|
1019
|
-
patch: {
|
|
1020
|
-
...(request.color === undefined ? {} : { color: request.color }),
|
|
1021
|
-
...(request.label === undefined ? {} : { label: request.label }),
|
|
1022
|
-
...(request.meta === undefined ? {} : { meta: request.meta }),
|
|
1023
|
-
},
|
|
1024
|
-
});
|
|
1025
|
-
return { ok: true, result: summarizeEditorSession(session.getState()) };
|
|
1026
|
-
case "assign_material":
|
|
1027
|
-
session.dispatch({ type: "assignMaterial", ids: request.ids, materialId: request.materialId });
|
|
1028
|
-
return { ok: true, result: summarizeEditorSession(session.getState()) };
|
|
1029
|
-
case "list_grids": {
|
|
1030
|
-
const grids = session.getState().document.grids ?? [];
|
|
1031
|
-
return { ok: true, result: { grids: grids.map(gridLayerSummary) } };
|
|
1032
|
-
}
|
|
1033
|
-
case "get_grid_cell": {
|
|
1034
|
-
const layer = (session.getState().document.grids ?? []).find((entry) => entry.id === request.id);
|
|
1035
|
-
if (layer === undefined)
|
|
1036
|
-
return { ok: false, error: `grid layer not found: ${request.id}` };
|
|
1037
|
-
return { ok: true, result: { id: request.id, col: request.col, row: request.row, value: eyedropGridCell(layer, request.col, request.row) } };
|
|
1038
|
-
}
|
|
1039
|
-
case "add_grid_layer": {
|
|
1040
|
-
const layer = createGridLayer({
|
|
1041
|
-
id: request.id,
|
|
1042
|
-
kind: request.kind,
|
|
1043
|
-
cols: request.cols,
|
|
1044
|
-
rows: request.rows,
|
|
1045
|
-
...(request.label === undefined ? {} : { label: request.label }),
|
|
1046
|
-
...(request.cellSize === undefined ? {} : { cellSize: request.cellSize }),
|
|
1047
|
-
...(request.origin === undefined ? {} : { origin: request.origin }),
|
|
1048
|
-
...(request.axes === undefined ? {} : { axes: request.axes }),
|
|
1049
|
-
...(request.empty === undefined ? {} : { empty: request.empty }),
|
|
1050
|
-
...(request.palette === undefined ? {} : { palette: request.palette }),
|
|
1051
|
-
});
|
|
1052
|
-
const { applied } = dispatchGuarded({ type: "addGridLayer", layer });
|
|
1053
|
-
if (!applied)
|
|
1054
|
-
return { ok: false, error: "add_grid_layer rejected: no effect" };
|
|
1055
|
-
return { ok: true, result: gridLayerSummary(layer) };
|
|
1056
|
-
}
|
|
1057
|
-
case "remove_grid_layer": {
|
|
1058
|
-
const { applied } = dispatchGuarded({ type: "removeGridLayer", id: request.id });
|
|
1059
|
-
if (!applied)
|
|
1060
|
-
return { ok: false, error: `grid layer not found: ${request.id}` };
|
|
1061
|
-
return { ok: true, result: { removed: request.id } };
|
|
1062
|
-
}
|
|
1063
|
-
case "set_grid_layer": {
|
|
1064
|
-
const patch = {};
|
|
1065
|
-
if (request.label !== undefined)
|
|
1066
|
-
patch.label = request.label;
|
|
1067
|
-
if (request.kind !== undefined)
|
|
1068
|
-
patch.kind = request.kind;
|
|
1069
|
-
if (request.visible !== undefined)
|
|
1070
|
-
patch.visible = request.visible;
|
|
1071
|
-
if (request.empty !== undefined)
|
|
1072
|
-
patch.empty = request.empty;
|
|
1073
|
-
if (request.cellSize !== undefined)
|
|
1074
|
-
patch.cellSize = request.cellSize;
|
|
1075
|
-
if (request.origin !== undefined)
|
|
1076
|
-
patch.origin = request.origin;
|
|
1077
|
-
if (request.axes !== undefined)
|
|
1078
|
-
patch.axes = request.axes;
|
|
1079
|
-
if (request.palette !== undefined)
|
|
1080
|
-
patch.palette = request.palette;
|
|
1081
|
-
const { applied, state } = dispatchGuarded({ type: "setGridLayer", id: request.id, patch });
|
|
1082
|
-
if (!applied)
|
|
1083
|
-
return { ok: false, error: `set_grid_layer rejected: no effect (${request.id})` };
|
|
1084
|
-
const layer = (state.document.grids ?? []).find((entry) => entry.id === request.id);
|
|
1085
|
-
return { ok: true, result: gridLayerSummary(layer) };
|
|
1086
|
-
}
|
|
1087
|
-
case "paint_grid_cells": {
|
|
1088
|
-
const { applied, state } = dispatchGuarded({ type: "paintGridCells", id: request.id, cells: request.cells });
|
|
1089
|
-
if (!applied)
|
|
1090
|
-
return { ok: false, error: `paint_grid_cells rejected: no effect (${request.id})` };
|
|
1091
|
-
const layer = (state.document.grids ?? []).find((entry) => entry.id === request.id);
|
|
1092
|
-
return { ok: true, result: gridLayerSummary(layer) };
|
|
1093
|
-
}
|
|
1094
|
-
case "fill_grid_rect": {
|
|
1095
|
-
const { applied, state } = dispatchGuarded({
|
|
1096
|
-
type: "fillGridRect",
|
|
1097
|
-
id: request.id,
|
|
1098
|
-
col0: request.col0,
|
|
1099
|
-
row0: request.row0,
|
|
1100
|
-
col1: request.col1,
|
|
1101
|
-
row1: request.row1,
|
|
1102
|
-
value: request.value,
|
|
1103
|
-
});
|
|
1104
|
-
if (!applied)
|
|
1105
|
-
return { ok: false, error: `fill_grid_rect rejected: no effect (${request.id})` };
|
|
1106
|
-
const layer = (state.document.grids ?? []).find((entry) => entry.id === request.id);
|
|
1107
|
-
return { ok: true, result: gridLayerSummary(layer) };
|
|
1108
|
-
}
|
|
1109
|
-
case "flood_fill_grid": {
|
|
1110
|
-
const { applied, state } = dispatchGuarded({
|
|
1111
|
-
type: "floodFillGrid",
|
|
1112
|
-
id: request.id,
|
|
1113
|
-
col: request.col,
|
|
1114
|
-
row: request.row,
|
|
1115
|
-
value: request.value,
|
|
1116
|
-
});
|
|
1117
|
-
if (!applied)
|
|
1118
|
-
return { ok: false, error: `flood_fill_grid rejected: no effect (${request.id})` };
|
|
1119
|
-
const layer = (state.document.grids ?? []).find((entry) => entry.id === request.id);
|
|
1120
|
-
return { ok: true, result: gridLayerSummary(layer) };
|
|
1121
|
-
}
|
|
1122
|
-
case "resize_grid_layer": {
|
|
1123
|
-
const { applied, state } = dispatchGuarded({ type: "resizeGridLayer", id: request.id, cols: request.cols, rows: request.rows });
|
|
1124
|
-
if (!applied)
|
|
1125
|
-
return { ok: false, error: `resize_grid_layer rejected: no effect (${request.id})` };
|
|
1126
|
-
const layer = (state.document.grids ?? []).find((entry) => entry.id === request.id);
|
|
1127
|
-
return { ok: true, result: gridLayerSummary(layer) };
|
|
1128
|
-
}
|
|
1129
|
-
case "import_grid": {
|
|
1130
|
-
const layer = request.format === "csv"
|
|
1131
|
-
? importCsvGrid(request.text, {
|
|
1132
|
-
id: request.id,
|
|
1133
|
-
kind: request.kind,
|
|
1134
|
-
...(request.empty === undefined ? {} : { empty: request.empty }),
|
|
1135
|
-
...(request.cellSize === undefined ? {} : { cellSize: request.cellSize }),
|
|
1136
|
-
...(request.origin === undefined ? {} : { origin: request.origin }),
|
|
1137
|
-
...(request.palette === undefined ? {} : { palette: request.palette }),
|
|
1138
|
-
})
|
|
1139
|
-
: importAsciiGrid(request.text, {
|
|
1140
|
-
id: request.id,
|
|
1141
|
-
kind: request.kind,
|
|
1142
|
-
...(request.empty === undefined ? {} : { empty: request.empty }),
|
|
1143
|
-
...(request.cellSize === undefined ? {} : { cellSize: request.cellSize }),
|
|
1144
|
-
...(request.origin === undefined ? {} : { origin: request.origin }),
|
|
1145
|
-
...(request.glyphMap === undefined ? {} : { glyphMap: request.glyphMap }),
|
|
1146
|
-
...(request.palette === undefined ? {} : { palette: request.palette }),
|
|
1147
|
-
});
|
|
1148
|
-
const { applied } = dispatchGuarded({ type: "addGridLayer", layer });
|
|
1149
|
-
if (!applied)
|
|
1150
|
-
return { ok: false, error: "import_grid rejected: no effect" };
|
|
1151
|
-
return { ok: true, result: gridLayerSummary(layer) };
|
|
1152
|
-
}
|
|
166
|
+
const response = EDITOR_RPC_HANDLERS[request.method](ctx, request);
|
|
167
|
+
if (!response.ok) {
|
|
168
|
+
emitEditorConsole("error", "rpc", `${request.method}: ${response.error ?? "failed"}`);
|
|
1153
169
|
}
|
|
170
|
+
return response;
|
|
1154
171
|
}
|
|
1155
172
|
catch (error) {
|
|
173
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
174
|
+
emitEditorConsole("error", "rpc", `${request.method} threw: ${message}`);
|
|
1156
175
|
return {
|
|
1157
176
|
ok: false,
|
|
1158
|
-
error:
|
|
177
|
+
error: message,
|
|
1159
178
|
};
|
|
1160
179
|
}
|
|
1161
180
|
},
|