@jgengine/editor 0.10.0 → 0.11.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 +1 -1
- package/dist/EditorApp.js +64 -5
- package/dist/EditorChrome.js +14 -2
- package/dist/PerfProbe.js +36 -2
- package/dist/TerrainReadout.d.ts +17 -0
- package/dist/TerrainReadout.js +163 -0
- package/dist/TerrainReadoutHud.d.ts +9 -0
- package/dist/TerrainReadoutHud.js +20 -0
- package/dist/mcp/rpcRequest.js +10 -0
- package/dist/mcp/tools.js +149 -0
- package/dist/perfPill.d.ts +21 -0
- package/dist/perfPill.js +20 -0
- package/dist/session.d.ts +90 -1
- package/dist/session.js +139 -1
- package/dist/terrainReadoutStore.d.ts +19 -0
- package/dist/terrainReadoutStore.js +36 -0
- package/dist/uiStore.d.ts +6 -0
- package/dist/uiStore.js +3 -0
- package/package.json +4 -4
- package/skills/jgengine/SKILL.md +32 -804
- package/skills/jgengine/api.md +20 -2
- package/skills/jgengine/capabilities.md +8 -0
- package/skills/jgengine-editor/SKILL.md +20 -368
- package/skills/jgengine-editor/api.md +122 -0
- package/skills/jgengine-editor/capabilities.md +45 -0
- package/skills/jgengine-editor/reference.md +26 -0
- package/skills/jgengine/reference-orphans.md +0 -9
- package/skills/jgengine/reference-systems.md +0 -60
package/dist/mcp/tools.js
CHANGED
|
@@ -761,4 +761,153 @@ export const EDITOR_MCP_TOOLS = [
|
|
|
761
761
|
additionalProperties: false,
|
|
762
762
|
},
|
|
763
763
|
},
|
|
764
|
+
{
|
|
765
|
+
name: "list_grids",
|
|
766
|
+
description: "List the scene document's grid/tile layers (id, kind, bounds, cell count, palette ids).",
|
|
767
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
768
|
+
},
|
|
769
|
+
{
|
|
770
|
+
name: "get_grid_cell",
|
|
771
|
+
description: "Eyedrop: read the value id at col,row of a grid layer (empty value for unset/out-of-bounds cells).",
|
|
772
|
+
inputSchema: {
|
|
773
|
+
type: "object",
|
|
774
|
+
properties: { id: { type: "string" }, col: { type: "number" }, row: { type: "number" } },
|
|
775
|
+
required: ["id", "col", "row"],
|
|
776
|
+
additionalProperties: false,
|
|
777
|
+
},
|
|
778
|
+
},
|
|
779
|
+
{
|
|
780
|
+
name: "add_grid_layer",
|
|
781
|
+
description: "Create an empty grid/tile layer on the scene document with the given bounds, cell size, and palette.",
|
|
782
|
+
inputSchema: {
|
|
783
|
+
type: "object",
|
|
784
|
+
properties: {
|
|
785
|
+
id: { type: "string" },
|
|
786
|
+
kind: { type: "string" },
|
|
787
|
+
cols: { type: "number" },
|
|
788
|
+
rows: { type: "number" },
|
|
789
|
+
label: { type: "string" },
|
|
790
|
+
cellSize: { type: "number" },
|
|
791
|
+
origin: { type: "object", properties: { x: { type: "number" }, y: { type: "number" }, z: { type: "number" } }, required: ["x", "y", "z"], additionalProperties: false },
|
|
792
|
+
axes: { type: "string", enum: ["xz", "xy"] },
|
|
793
|
+
empty: { type: "string" },
|
|
794
|
+
palette: { type: "array", items: { type: "object" } },
|
|
795
|
+
},
|
|
796
|
+
required: ["id", "kind", "cols", "rows"],
|
|
797
|
+
additionalProperties: false,
|
|
798
|
+
},
|
|
799
|
+
},
|
|
800
|
+
{
|
|
801
|
+
name: "remove_grid_layer",
|
|
802
|
+
description: "Delete a grid/tile layer from the scene document by id.",
|
|
803
|
+
inputSchema: {
|
|
804
|
+
type: "object",
|
|
805
|
+
properties: { id: { type: "string" } },
|
|
806
|
+
required: ["id"],
|
|
807
|
+
additionalProperties: false,
|
|
808
|
+
},
|
|
809
|
+
},
|
|
810
|
+
{
|
|
811
|
+
name: "set_grid_layer",
|
|
812
|
+
description: "Patch a grid layer's metadata — label, kind, visibility, empty value, cell size, origin, axes, or palette.",
|
|
813
|
+
inputSchema: {
|
|
814
|
+
type: "object",
|
|
815
|
+
properties: {
|
|
816
|
+
id: { type: "string" },
|
|
817
|
+
label: { type: "string" },
|
|
818
|
+
kind: { type: "string" },
|
|
819
|
+
visible: { type: "boolean" },
|
|
820
|
+
empty: { type: "string" },
|
|
821
|
+
cellSize: { type: "number" },
|
|
822
|
+
origin: { type: "object", properties: { x: { type: "number" }, y: { type: "number" }, z: { type: "number" } }, required: ["x", "y", "z"], additionalProperties: false },
|
|
823
|
+
axes: { type: "string", enum: ["xz", "xy"] },
|
|
824
|
+
palette: { type: "array", items: { type: "object" } },
|
|
825
|
+
},
|
|
826
|
+
required: ["id"],
|
|
827
|
+
additionalProperties: false,
|
|
828
|
+
},
|
|
829
|
+
},
|
|
830
|
+
{
|
|
831
|
+
name: "paint_grid_cells",
|
|
832
|
+
description: "Paint or erase grid cells (value equal to the layer's empty value erases). One undo step for the whole batch.",
|
|
833
|
+
inputSchema: {
|
|
834
|
+
type: "object",
|
|
835
|
+
properties: {
|
|
836
|
+
id: { type: "string" },
|
|
837
|
+
cells: {
|
|
838
|
+
type: "array",
|
|
839
|
+
items: {
|
|
840
|
+
type: "object",
|
|
841
|
+
properties: { col: { type: "number" }, row: { type: "number" }, value: { type: "string" } },
|
|
842
|
+
required: ["col", "row", "value"],
|
|
843
|
+
additionalProperties: false,
|
|
844
|
+
},
|
|
845
|
+
},
|
|
846
|
+
},
|
|
847
|
+
required: ["id", "cells"],
|
|
848
|
+
additionalProperties: false,
|
|
849
|
+
},
|
|
850
|
+
},
|
|
851
|
+
{
|
|
852
|
+
name: "fill_grid_rect",
|
|
853
|
+
description: "Rectangle tool: fill the inclusive col0,row0..col1,row1 rectangle of a grid layer with one value.",
|
|
854
|
+
inputSchema: {
|
|
855
|
+
type: "object",
|
|
856
|
+
properties: {
|
|
857
|
+
id: { type: "string" },
|
|
858
|
+
col0: { type: "number" },
|
|
859
|
+
row0: { type: "number" },
|
|
860
|
+
col1: { type: "number" },
|
|
861
|
+
row1: { type: "number" },
|
|
862
|
+
value: { type: "string" },
|
|
863
|
+
},
|
|
864
|
+
required: ["id", "col0", "row0", "col1", "row1", "value"],
|
|
865
|
+
additionalProperties: false,
|
|
866
|
+
},
|
|
867
|
+
},
|
|
868
|
+
{
|
|
869
|
+
name: "flood_fill_grid",
|
|
870
|
+
description: "Bucket tool: flood-fill the contiguous region sharing the seed cell's value with a new value.",
|
|
871
|
+
inputSchema: {
|
|
872
|
+
type: "object",
|
|
873
|
+
properties: {
|
|
874
|
+
id: { type: "string" },
|
|
875
|
+
col: { type: "number" },
|
|
876
|
+
row: { type: "number" },
|
|
877
|
+
value: { type: "string" },
|
|
878
|
+
},
|
|
879
|
+
required: ["id", "col", "row", "value"],
|
|
880
|
+
additionalProperties: false,
|
|
881
|
+
},
|
|
882
|
+
},
|
|
883
|
+
{
|
|
884
|
+
name: "resize_grid_layer",
|
|
885
|
+
description: "Resize a grid layer's bounds to cols x rows, trimming any cells outside the new extent.",
|
|
886
|
+
inputSchema: {
|
|
887
|
+
type: "object",
|
|
888
|
+
properties: { id: { type: "string" }, cols: { type: "number" }, rows: { type: "number" } },
|
|
889
|
+
required: ["id", "cols", "rows"],
|
|
890
|
+
additionalProperties: false,
|
|
891
|
+
},
|
|
892
|
+
},
|
|
893
|
+
{
|
|
894
|
+
name: "import_grid",
|
|
895
|
+
description: "Import an ASCII/glyph map or CSV into a new grid layer (the ASCII/CSV is an import adapter, not the stored form).",
|
|
896
|
+
inputSchema: {
|
|
897
|
+
type: "object",
|
|
898
|
+
properties: {
|
|
899
|
+
id: { type: "string" },
|
|
900
|
+
kind: { type: "string" },
|
|
901
|
+
format: { type: "string", enum: ["ascii", "csv"] },
|
|
902
|
+
text: { type: "string" },
|
|
903
|
+
empty: { type: "string" },
|
|
904
|
+
cellSize: { type: "number" },
|
|
905
|
+
origin: { type: "object", properties: { x: { type: "number" }, y: { type: "number" }, z: { type: "number" } }, required: ["x", "y", "z"], additionalProperties: false },
|
|
906
|
+
glyphMap: { type: "object" },
|
|
907
|
+
palette: { type: "array", items: { type: "object" } },
|
|
908
|
+
},
|
|
909
|
+
required: ["id", "kind", "format", "text"],
|
|
910
|
+
additionalProperties: false,
|
|
911
|
+
},
|
|
912
|
+
},
|
|
764
913
|
];
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Toolbar perf-pill health, separating an idle render-on-demand loop from a genuinely slow frame. */
|
|
2
|
+
export type EditorPerfTone = "idle" | "healthy" | "busy";
|
|
3
|
+
/**
|
|
4
|
+
* FPS at or below which an *actively rendering* editor frame is treated as struggling.
|
|
5
|
+
* Only applied when the loop is active — an idle / browser-throttled loop never trips it.
|
|
6
|
+
*/
|
|
7
|
+
export declare const EDITOR_PERF_LOW_FPS = 30;
|
|
8
|
+
/**
|
|
9
|
+
* Classify a perf sample for the toolbar pill. The editor renders `frameloop="always"`, so a
|
|
10
|
+
* healthy visible loop sits at ~60fps; a low wall-clock fps only appears when the browser throttles
|
|
11
|
+
* rAF (backgrounded tab / headless screenshot) while nothing is happening. `PerfProbe` measures
|
|
12
|
+
* that idleness (no camera move, no raycast/rebuild, no scene change) and reports it as `active:false`,
|
|
13
|
+
* which classifies here as `"idle"` instead of the red danger cue. The danger cue (`"busy"`) is
|
|
14
|
+
* reserved for sustained low fps while the loop is genuinely active (orbit / drag / sculpt) — the
|
|
15
|
+
* regression actually worth surfacing.
|
|
16
|
+
* @internal — drives the editor toolbar perf pill; not game-facing.
|
|
17
|
+
*/
|
|
18
|
+
export declare function classifyEditorPerf(sample: {
|
|
19
|
+
fps: number;
|
|
20
|
+
active: boolean;
|
|
21
|
+
}): EditorPerfTone;
|
package/dist/perfPill.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FPS at or below which an *actively rendering* editor frame is treated as struggling.
|
|
3
|
+
* Only applied when the loop is active — an idle / browser-throttled loop never trips it.
|
|
4
|
+
*/
|
|
5
|
+
export const EDITOR_PERF_LOW_FPS = 30;
|
|
6
|
+
/**
|
|
7
|
+
* Classify a perf sample for the toolbar pill. The editor renders `frameloop="always"`, so a
|
|
8
|
+
* healthy visible loop sits at ~60fps; a low wall-clock fps only appears when the browser throttles
|
|
9
|
+
* rAF (backgrounded tab / headless screenshot) while nothing is happening. `PerfProbe` measures
|
|
10
|
+
* that idleness (no camera move, no raycast/rebuild, no scene change) and reports it as `active:false`,
|
|
11
|
+
* which classifies here as `"idle"` instead of the red danger cue. The danger cue (`"busy"`) is
|
|
12
|
+
* reserved for sustained low fps while the loop is genuinely active (orbit / drag / sculpt) — the
|
|
13
|
+
* regression actually worth surfacing.
|
|
14
|
+
* @internal — drives the editor toolbar perf pill; not game-facing.
|
|
15
|
+
*/
|
|
16
|
+
export function classifyEditorPerf(sample) {
|
|
17
|
+
if (!sample.active)
|
|
18
|
+
return "idle";
|
|
19
|
+
return sample.fps < EDITOR_PERF_LOW_FPS ? "busy" : "healthy";
|
|
20
|
+
}
|
package/dist/session.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type DocumentLiveSync, type DocumentPatch, type EditorCatalogDefinition, type EditorCommand, type EditorDocument, type EditorKindVisibility, type EditorLayersInput, type EditorSession, type EditorSessionState, type RuntimeEntityState, type RuntimePlayControl } from "@jgengine/core/editor/index";
|
|
1
|
+
import { type DocumentLiveSync, type DocumentPatch, type EditorCatalogDefinition, type EditorCommand, type EditorDocument, type EditorGridPaletteEntry, type EditorKindVisibility, type EditorLayersInput, type EditorSession, type EditorSessionState, type RuntimeEntityState, type RuntimePlayControl } from "@jgengine/core/editor/index";
|
|
2
2
|
import { type TerraformMode, type TerrainMaterialLayer } from "@jgengine/core/world/terraform";
|
|
3
3
|
/** How the editor hosts the game: frozen placement view, roamable world, or the real game. */
|
|
4
4
|
export type EditorRunMode = "edit" | "walk" | "play";
|
|
@@ -246,6 +246,89 @@ export type EditorBridgeRequest = {
|
|
|
246
246
|
method: "assign_material";
|
|
247
247
|
ids: string[];
|
|
248
248
|
materialId: string;
|
|
249
|
+
} | {
|
|
250
|
+
method: "list_grids";
|
|
251
|
+
} | {
|
|
252
|
+
method: "get_grid_cell";
|
|
253
|
+
id: string;
|
|
254
|
+
col: number;
|
|
255
|
+
row: number;
|
|
256
|
+
} | {
|
|
257
|
+
method: "add_grid_layer";
|
|
258
|
+
id: string;
|
|
259
|
+
kind: string;
|
|
260
|
+
cols: number;
|
|
261
|
+
rows: number;
|
|
262
|
+
label?: string;
|
|
263
|
+
cellSize?: number;
|
|
264
|
+
origin?: {
|
|
265
|
+
x: number;
|
|
266
|
+
y: number;
|
|
267
|
+
z: number;
|
|
268
|
+
};
|
|
269
|
+
axes?: "xz" | "xy";
|
|
270
|
+
empty?: string;
|
|
271
|
+
palette?: EditorGridPaletteEntry[];
|
|
272
|
+
} | {
|
|
273
|
+
method: "remove_grid_layer";
|
|
274
|
+
id: string;
|
|
275
|
+
} | {
|
|
276
|
+
method: "set_grid_layer";
|
|
277
|
+
id: string;
|
|
278
|
+
label?: string;
|
|
279
|
+
kind?: string;
|
|
280
|
+
visible?: boolean;
|
|
281
|
+
empty?: string;
|
|
282
|
+
cellSize?: number;
|
|
283
|
+
origin?: {
|
|
284
|
+
x: number;
|
|
285
|
+
y: number;
|
|
286
|
+
z: number;
|
|
287
|
+
};
|
|
288
|
+
axes?: "xz" | "xy";
|
|
289
|
+
palette?: EditorGridPaletteEntry[];
|
|
290
|
+
} | {
|
|
291
|
+
method: "paint_grid_cells";
|
|
292
|
+
id: string;
|
|
293
|
+
cells: {
|
|
294
|
+
col: number;
|
|
295
|
+
row: number;
|
|
296
|
+
value: string;
|
|
297
|
+
}[];
|
|
298
|
+
} | {
|
|
299
|
+
method: "fill_grid_rect";
|
|
300
|
+
id: string;
|
|
301
|
+
col0: number;
|
|
302
|
+
row0: number;
|
|
303
|
+
col1: number;
|
|
304
|
+
row1: number;
|
|
305
|
+
value: string;
|
|
306
|
+
} | {
|
|
307
|
+
method: "flood_fill_grid";
|
|
308
|
+
id: string;
|
|
309
|
+
col: number;
|
|
310
|
+
row: number;
|
|
311
|
+
value: string;
|
|
312
|
+
} | {
|
|
313
|
+
method: "resize_grid_layer";
|
|
314
|
+
id: string;
|
|
315
|
+
cols: number;
|
|
316
|
+
rows: number;
|
|
317
|
+
} | {
|
|
318
|
+
method: "import_grid";
|
|
319
|
+
id: string;
|
|
320
|
+
kind: string;
|
|
321
|
+
format: "ascii" | "csv";
|
|
322
|
+
text: string;
|
|
323
|
+
empty?: string;
|
|
324
|
+
cellSize?: number;
|
|
325
|
+
origin?: {
|
|
326
|
+
x: number;
|
|
327
|
+
y: number;
|
|
328
|
+
z: number;
|
|
329
|
+
};
|
|
330
|
+
glyphMap?: Record<string, string>;
|
|
331
|
+
palette?: EditorGridPaletteEntry[];
|
|
249
332
|
} | {
|
|
250
333
|
method: "push_document_patch";
|
|
251
334
|
patch: DocumentPatch;
|
|
@@ -324,6 +407,12 @@ export interface EditorPerfSample {
|
|
|
324
407
|
drawCalls: number;
|
|
325
408
|
triangles: number;
|
|
326
409
|
sampledAt: number;
|
|
410
|
+
/**
|
|
411
|
+
* Whether the sampled window saw camera or edit activity. `false` means the render-on-demand /
|
|
412
|
+
* browser-throttled loop is idle, so a low `fps` is expected pacing rather than lag — the pill
|
|
413
|
+
* shows a neutral "idle" instead of the red danger cue.
|
|
414
|
+
*/
|
|
415
|
+
active: boolean;
|
|
327
416
|
/** Avg viewport raycast (pick) time this window — editor-authoring cost, not sim cost. */
|
|
328
417
|
raycastMs?: number;
|
|
329
418
|
/** Avg preview-mesh rebuild (displace) time this window — editor-authoring cost, not sim cost. */
|
package/dist/session.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createDocumentLiveSync, createEditorSession, createRuntimePlayControl, editorChildren, editorDocumentBounds, editorParentOf, editorRoots, findEditorCatalog, findEditorCatalogEntry, findEditorCollection, findEditorPrefab, getRuntimeInspectorValue, installDocumentLiveSync, listEditorKinds, normalizeEditorLayers, planRuntimeInspectorSet, runtimeEntityMetaWriteBackCommand, runtimeEntityWriteBackCommand, seedEditorCatalogs, summarizeEditorSession, summarizeRuntimeInspector, } from "@jgengine/core/editor/index";
|
|
1
|
+
import { createDocumentLiveSync, createEditorSession, createRuntimePlayControl, editorChildren, editorDocumentBounds, editorParentOf, editorRoots, findEditorCatalog, findEditorCatalogEntry, findEditorCollection, findEditorPrefab, getRuntimeInspectorValue, installDocumentLiveSync, listEditorKinds, normalizeEditorLayers, planRuntimeInspectorSet, runtimeEntityMetaWriteBackCommand, runtimeEntityWriteBackCommand, seedEditorCatalogs, summarizeEditorSession, summarizeRuntimeInspector, createGridLayer, eyedropGridCell, gridCellCount, importAsciiGrid, importCsvGrid, } from "@jgengine/core/editor/index";
|
|
2
2
|
import { createTerrainSnapshot, editableTerrainFromSnapshot, } from "@jgengine/core/world/terraform";
|
|
3
3
|
import { resolvePlaceAsset, toEditorMarker } from "@jgengine/core/world/placeAsset";
|
|
4
4
|
import { resolveScatter, resolveScatterRegion, scatterRegionEstimate, scatterRegionFromPath, SCATTER_PATH_KIND, } from "@jgengine/core/world/scatterRegion";
|
|
@@ -22,6 +22,20 @@ function findMetaTarget(doc, id) {
|
|
|
22
22
|
return { target: "note", kind: "note", meta: note.meta };
|
|
23
23
|
return null;
|
|
24
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
|
+
}
|
|
25
39
|
/** Validates a merged meta bag against a registered kind's schema; returns an error string or null. */
|
|
26
40
|
function validateMetaForKind(kind, meta) {
|
|
27
41
|
const definition = getSceneKind(kind);
|
|
@@ -1012,6 +1026,130 @@ export function createEditorHost(options) {
|
|
|
1012
1026
|
case "assign_material":
|
|
1013
1027
|
session.dispatch({ type: "assignMaterial", ids: request.ids, materialId: request.materialId });
|
|
1014
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
|
+
}
|
|
1015
1153
|
}
|
|
1016
1154
|
}
|
|
1017
1155
|
catch (error) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ElevationReadout, ElevationSummary } from "@jgengine/core/world/terrainGuides";
|
|
2
|
+
/** Live, transient terrain-readability feedback shared by the viewport overlay and the HUD legend. */
|
|
3
|
+
export interface TerrainReadoutState {
|
|
4
|
+
/** Elevation under the pointer, or `null` when the pointer is off the terrain. */
|
|
5
|
+
cursor: ElevationReadout | null;
|
|
6
|
+
/** Elevation stats for the readout region (whole terrain), or `null` before the first build. */
|
|
7
|
+
summary: ElevationSummary | null;
|
|
8
|
+
/** The contour interval the overlay chose for the current relief; `0` when flat. */
|
|
9
|
+
interval: number;
|
|
10
|
+
}
|
|
11
|
+
/** Subscribable store for transient terrain-readout feedback (not persisted — it is live measurement). */
|
|
12
|
+
export interface TerrainReadoutStore {
|
|
13
|
+
getState(): TerrainReadoutState;
|
|
14
|
+
setCursor(cursor: ElevationReadout | null): void;
|
|
15
|
+
setSummary(summary: ElevationSummary | null, interval: number): void;
|
|
16
|
+
subscribe(listener: () => void): () => void;
|
|
17
|
+
}
|
|
18
|
+
/** Creates the readout store the terrain overlay writes and the HUD reads. */
|
|
19
|
+
export declare function createTerrainReadoutStore(): TerrainReadoutStore;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Creates the readout store the terrain overlay writes and the HUD reads. */
|
|
2
|
+
export function createTerrainReadoutStore() {
|
|
3
|
+
let state = { cursor: null, summary: null, interval: 0 };
|
|
4
|
+
const listeners = new Set();
|
|
5
|
+
const emit = () => {
|
|
6
|
+
for (const listener of listeners)
|
|
7
|
+
listener();
|
|
8
|
+
};
|
|
9
|
+
return {
|
|
10
|
+
getState: () => state,
|
|
11
|
+
setCursor(cursor) {
|
|
12
|
+
const prev = state.cursor;
|
|
13
|
+
if (prev === cursor)
|
|
14
|
+
return;
|
|
15
|
+
if (prev !== null &&
|
|
16
|
+
cursor !== null &&
|
|
17
|
+
prev.height === cursor.height &&
|
|
18
|
+
prev.x === cursor.x &&
|
|
19
|
+
prev.z === cursor.z) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
state = { ...state, cursor };
|
|
23
|
+
emit();
|
|
24
|
+
},
|
|
25
|
+
setSummary(summary, interval) {
|
|
26
|
+
state = { ...state, summary, interval };
|
|
27
|
+
emit();
|
|
28
|
+
},
|
|
29
|
+
subscribe(listener) {
|
|
30
|
+
listeners.add(listener);
|
|
31
|
+
return () => {
|
|
32
|
+
listeners.delete(listener);
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
package/dist/uiStore.d.ts
CHANGED
|
@@ -76,6 +76,12 @@ export interface EditorUiState {
|
|
|
76
76
|
snapMode: SnapMode;
|
|
77
77
|
gridSize: number;
|
|
78
78
|
showGrid: boolean;
|
|
79
|
+
/** Surface-following iso-elevation contour overlay (terrain readability). */
|
|
80
|
+
showContours: boolean;
|
|
81
|
+
/** Terrain-draped reference grid overlay that climbs relief instead of being occluded by it. */
|
|
82
|
+
showSurfaceGrid: boolean;
|
|
83
|
+
/** Measurable elevation HUD: cursor height/delta, region min/max/mean, contour interval. */
|
|
84
|
+
showElevation: boolean;
|
|
79
85
|
placement: PlacementTool | null;
|
|
80
86
|
pathDraft: readonly EditorVec3[];
|
|
81
87
|
pathPoint: {
|
package/dist/uiStore.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jgengine/editor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Scene/world/asset editor for JGengine — loaded lazily by the runner; never import from game entrypoints.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -31,9 +31,9 @@
|
|
|
31
31
|
"test": "bun test src"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@jgengine/core": "^0.
|
|
35
|
-
"@jgengine/react": "^0.
|
|
36
|
-
"@jgengine/shell": "^0.
|
|
34
|
+
"@jgengine/core": "^0.11.0",
|
|
35
|
+
"@jgengine/react": "^0.11.0",
|
|
36
|
+
"@jgengine/shell": "^0.11.0"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"@react-three/drei": "^10.0.0",
|