@jgengine/editor 0.12.0 → 0.14.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 +73 -1
- package/dist/AssetBrowser.d.ts +7 -0
- package/dist/AssetBrowser.js +10 -1
- package/dist/ClipPreviewLayer.d.ts +15 -0
- package/dist/ClipPreviewLayer.js +60 -0
- package/dist/EditorApp.d.ts +7 -1
- package/dist/EditorApp.js +5 -4
- package/dist/EditorChrome.d.ts +3 -1
- package/dist/EditorChrome.js +8 -4
- package/dist/InspectorPanel.d.ts +4 -1
- package/dist/InspectorPanel.js +35 -4
- package/dist/SchemaInspector.d.ts +3 -1
- package/dist/SchemaInspector.js +8 -3
- package/dist/handlers/index.js +2 -0
- package/dist/handlers/streets.d.ts +3 -0
- package/dist/handlers/streets.js +159 -0
- package/dist/mcp/fieldSpec.js +1 -0
- package/dist/mcp/tools.js +1 -0
- package/dist/modelAnimationAuthoring.d.ts +75 -0
- package/dist/modelAnimationAuthoring.js +164 -0
- package/dist/session.d.ts +14 -0
- package/dist/shell/AnimationPanel.d.ts +10 -8
- package/dist/shell/AnimationPanel.js +112 -12
- package/dist/shell/BottomDock.d.ts +3 -1
- package/dist/shell/BottomDock.js +2 -2
- package/dist/shell/TopAppBar.d.ts +3 -1
- package/dist/shell/TopAppBar.js +2 -2
- package/dist/shell/clipPreview.d.ts +85 -0
- package/dist/shell/clipPreview.js +82 -0
- package/dist/shell/scatterCoverage.d.ts +22 -0
- package/dist/shell/scatterCoverage.js +31 -0
- package/dist/uiStore.d.ts +6 -0
- package/dist/uiStore.js +1 -0
- package/package.json +4 -4
- package/skills/jgengine/SKILL.md +5 -2
- package/skills/jgengine/api.md +13 -5
- package/skills/jgengine-editor/api.md +51 -8
package/CHANGELOG.md
CHANGED
|
@@ -10,7 +10,79 @@ let consumers pick up the better stuff, not just to list what moved.
|
|
|
10
10
|
Agents building on the published SDK can also read this programmatically:
|
|
11
11
|
`import { VERSION, CHANGELOG } from "@jgengine/core/meta/changelog"` gives the
|
|
12
12
|
same data as typed values, so an updater can diff its installed version against
|
|
13
|
-
the latest and surface the migration steps.
|
|
13
|
+
the latest and surface the migration steps. Easier still: `npx jgengine upgrade`
|
|
14
|
+
run inside a game project diffs the installed `@jgengine/*` versions against the
|
|
15
|
+
latest release and prints every Migrate step and Adopt-worthy addition in
|
|
16
|
+
between (`--json` for structured output).
|
|
17
|
+
|
|
18
|
+
## [Unreleased]
|
|
19
|
+
|
|
20
|
+
<!--
|
|
21
|
+
Every PR that changes `packages/*/src` records its consumer-facing change here, so
|
|
22
|
+
the next release's notes are complete by construction. Add a bullet under the right
|
|
23
|
+
subhead below. `bun run check-changelog` fails a source PR whose `[Unreleased]` block
|
|
24
|
+
is untouched; pure refactors/tests bypass with `[skip changelog]` in a commit message.
|
|
25
|
+
At publish, rename this heading to the new version and mirror the entries into
|
|
26
|
+
`packages/core/src/meta/changelog.ts` (the typed `CHANGELOG` export).
|
|
27
|
+
-->
|
|
28
|
+
|
|
29
|
+
### Migrate
|
|
30
|
+
|
|
31
|
+
- _Nothing yet._
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
### Removed
|
|
38
|
+
|
|
39
|
+
## 0.14.0
|
|
40
|
+
|
|
41
|
+
### Migrate
|
|
42
|
+
|
|
43
|
+
- **Nothing required** — this release is additive drop-in primitives, editor authoring, CLI discovery, and fixes. Bump `@jgengine/*` pins to `^0.14.0` (CLI `jgengine` is 0.11.0; `@jgengine/github` unchanged at 0.1.0) and rebuild.
|
|
44
|
+
- One behavior fix to know about: in a `rig: "chase"` game, on-foot movement and aim are now camera-relative (the chase rig reports its yaw to the shell like every other rig). If a game somehow relied on world-yaw-0 movement under a chase camera, that was the bug this fixes.
|
|
45
|
+
|
|
46
|
+
### Added
|
|
47
|
+
|
|
48
|
+
- One authoring gesture for scatterable coverage: `@jgengine/core/world/scatterCoverage` owns shared density/budget semantics (per-kind unit, requested→count→capped, one clamp-and-warn phrasing) for `grass_field`/`scatter`/`city`; the editor inspector leads each kind with the same Area → Assets → Density coverage section, scatter truncates to the shared 250k instance budget, and city single-sources its 2,600-lot cap.
|
|
49
|
+
- **Created games ship a drive/playtest script** (#1248) — `npx jgengine create` scaffolds now include `scripts/drive.mjs` (`bun run drive`): dependency-free headless play/testing of the running game — ordered `--click`/`--key`/`--wait`/`--shot`/`--rpc` steps against the agent bridge, plus `--playtest --strict` progress/softlock verdicts off `capture.probe`. Shared Chrome/CDP machinery lives in `scripts/browser.mjs`; `scripts/shoot.mjs` is unchanged in behavior, now a thin CLI over it.
|
|
50
|
+
- Editor: viewport clip preview for rigged assets (Animation dock "Clips" mode — pick a rig or placed instance, play/scrub/loop/speed any catalog clip) and an Inspector "Animation" section that authors a placement's `ModelConfig.animation` (role→clip dropdowns, auto/none, walk/run/fade, one-shot event bindings) as undoable `marker.meta.animation` edits. New subpaths: `@jgengine/editor/shell/clipPreview`, `@jgengine/editor/modelAnimationAuthoring`, `@jgengine/editor/ClipPreviewLayer`.
|
|
51
|
+
- **`npx jgengine find <intent>`** — active capability discovery: searches every shipped domain's `capabilities.md` (staged inside the CLI tarball, so it works regardless of which `@jgengine/*` packages a project installed) and prints the drop-in primitive + its import for an intent like `"toggleable window"`, `inventory`, or `minimap`. Scaffolded games are now briefed (in `AGENTS.md` and the `jgengine` intake skill) to reach for it before hand-rolling a HUD/inventory/window/rig.
|
|
52
|
+
- **`EntityPreview`** (`@jgengine/shell/render/EntityPreview`) — drop-in live 3D entity portrait for character screens, unit inspectors, and loadout viewers. Owns the nested `<Canvas>`, the `GameContextBridge`, and a `StudioStage` lighting rig with optional turntable / face-camera; the game passes its own `renderEntity` as children (or a native `model` + `instanceId`) and, bound to a live entity, the portrait walks/flinches/topples in sync with the world.
|
|
53
|
+
- **`GameContextBridge`** (`@jgengine/react`) — re-provide the running `GameContext` across a nested React reconciler boundary (the R3F `<Canvas>`), so a game building its own preview canvas no longer re-derives the bridge by hand.
|
|
54
|
+
|
|
55
|
+
### Changed
|
|
56
|
+
|
|
57
|
+
- **Chase rig reports its yaw to the shell** — `ChaseRig` now writes the camera yaw back to the shared yaw ref like every other player-facing rig, so on-foot movement and aim in a `rig: "chase"` game are camera-relative instead of frozen to world yaw 0 (WASD no longer fights the camera the moment it swings behind the player). Interior views (cockpit/hood/rear) report the vehicle heading.
|
|
58
|
+
- **Combat VFX no longer render as black squares under AO/DOF post-processing** (#1247) — GTAO/Bokeh scene prepasses skip overlay effects. Games with custom additive overlay effects opt out the same way: spread `POSTFX_OVERLAY_USERDATA` (`@jgengine/shell/postfx/postfxOverlay`) onto the overlay group's `userData`.
|
|
59
|
+
- **`PanelHost` windows stack above the HUD by default** (`@jgengine/react`) — the host establishes its own stacking context at `zIndexBase` (default 40), so open windows always paint over stat bars / nameplates / frames instead of bleeding through. Overridable per instance.
|
|
60
|
+
|
|
61
|
+
## 0.13.0
|
|
62
|
+
|
|
63
|
+
### Migrate
|
|
64
|
+
|
|
65
|
+
- **Nothing required** — this release is additive fixes, defaults, and new drop-in primitives. Bump `@jgengine/*` pins to `^0.13.0` (CLI `jgengine` is 0.10.0; `@jgengine/github` unchanged at 0.1.0) and rebuild.
|
|
66
|
+
- Two defaults changed in the player's favor, both overridable: **offline games now persist to localStorage automatically** (#1207) and **rigged catalog assets animate by default** via clip metadata + semantic clip roles (#1209). If a game relied on rigged models staying frozen or on saves being memory-only, opt out explicitly; otherwise you get better behavior for free.
|
|
67
|
+
|
|
68
|
+
### Added
|
|
69
|
+
|
|
70
|
+
- **Drop-in inventory grid, window manager, and character sheet** (#1220) — `@jgengine/react` ships `InventoryGrid` (real drag/stack/split), a toggleable-window system (`panels`: bag, character sheet, spellbook-style windows with `@jgengine/core/ui/panelModel` state), and a `CharacterSheet` paperdoll, all HudTheme-skinnable — wire in a line or two instead of re-deriving from raw divs.
|
|
71
|
+
- **Serializable runtime snapshots** (#1179) — capture/restore of live runtime state, including magazine state (`@jgengine/core/combat/magazine`) and stat modifiers, for saves and replication.
|
|
72
|
+
- **Portable weapon plumbing** (#1211) — `@jgengine/core/combat/weaponFire` composes trigger → magazine → spread → projectile/hitscan as data, no game-local glue.
|
|
73
|
+
- **Portable leveling** (#1206) and **portable damage-pool access** (#1208) — progression and damage pools exposed as genre-agnostic seams on the game context.
|
|
74
|
+
- **Named combat VFX presets** (#1204) — `@jgengine/core/combat/vfxPresets`: a visible attack is one line.
|
|
75
|
+
- **Runtime-measured render-bounds hitboxes** (#1227) — colliders wrap what the shell actually mounts, not the raw asset bounds.
|
|
76
|
+
- **Character motion feel** — procedural part motion for rig-less part-composed characters (#1222) plus squash & stretch and death splat (#1229).
|
|
77
|
+
- **City fabric** (#1191) — `streetGenerator` + `cityGenerator`, editor bake verb, street-aware lots, road junctions, grounding, and a web playground.
|
|
78
|
+
- **`jgengine recipe` CLI** (#1214, #1231) — vetted wired compositions, compile-pinned against the SDK, listed and copied from the CLI.
|
|
79
|
+
- **Editor/debug modes mirror to the URL** with a proper editor exit (#1203); game template ships a dependency-free WebGL screenshot script (#1202).
|
|
80
|
+
|
|
81
|
+
### Changed
|
|
82
|
+
|
|
83
|
+
- Offline games persist to localStorage by default (#1207); rigged assets animate by default (#1209).
|
|
84
|
+
- RTS camera pan inversion fixed (#1228); sprite raycast warning silenced and duplicate nameplate health bars removed (#1194); scaffolded F2+E editor Tailwind `@source` renders its theme (#1195).
|
|
85
|
+
- Outside-game DX: stat-pool no-op stability, camera-transparent decor, cleaner install/version output, headless UI-intent seam (#1223); new-game briefs direct engine bugs/gaps upstream (#1200).
|
|
14
86
|
|
|
15
87
|
## 0.12.0
|
|
16
88
|
|
package/dist/AssetBrowser.d.ts
CHANGED
|
@@ -5,7 +5,13 @@ export interface EditorAssetEntry {
|
|
|
5
5
|
label: string;
|
|
6
6
|
kind: "model" | "catalog" | "marker";
|
|
7
7
|
url?: string;
|
|
8
|
+
/** Animation clip names from the asset index — present on rigged catalog assets. */
|
|
9
|
+
clips?: readonly string[];
|
|
8
10
|
}
|
|
11
|
+
/** "idle · walk · run" summary of a rigged asset's detected animation roles for the browser row badge.
|
|
12
|
+
* @internal
|
|
13
|
+
*/
|
|
14
|
+
export declare function describeClipRoles(clips: readonly string[]): string;
|
|
9
15
|
/** Custom drag mime carrying a material id — read by hierarchy rows and the viewport drop zone. */
|
|
10
16
|
export declare const MATERIAL_DRAG_MIME = "application/x-jgengine-material";
|
|
11
17
|
/**
|
|
@@ -26,6 +32,7 @@ export declare function AssetBrowser({ assets, session, onPlace, }: {
|
|
|
26
32
|
/** Turns a game's asset catalog ids into editor asset entries for the browser panel. */
|
|
27
33
|
export declare function assetsFromCatalog(ids: readonly string[], resolve?: (id: string) => {
|
|
28
34
|
url?: string;
|
|
35
|
+
clips?: readonly string[];
|
|
29
36
|
} | null): EditorAssetEntry[];
|
|
30
37
|
/**
|
|
31
38
|
* Converts a durable/ephemeral standalone import into a Content Browser entry.
|
package/dist/AssetBrowser.js
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useMemo, useState } from "react";
|
|
3
|
+
import { rolesFromClips } from "@jgengine/core/game/clipRoles";
|
|
3
4
|
import { TERRAIN_MATERIALS } from "./uiStore.js";
|
|
5
|
+
/** "idle · walk · run" summary of a rigged asset's detected animation roles for the browser row badge.
|
|
6
|
+
* @internal
|
|
7
|
+
*/
|
|
8
|
+
export function describeClipRoles(clips) {
|
|
9
|
+
const roles = Object.keys(rolesFromClips(clips));
|
|
10
|
+
return roles.length === 0 ? "no roles detected" : roles.join(" · ");
|
|
11
|
+
}
|
|
4
12
|
/** Custom drag mime carrying a material id — read by hierarchy rows and the viewport drop zone. */
|
|
5
13
|
export const MATERIAL_DRAG_MIME = "application/x-jgengine-material";
|
|
6
14
|
/**
|
|
@@ -46,7 +54,7 @@ export function AssetBrowser({ assets, session, onPlace, }) {
|
|
|
46
54
|
return assets;
|
|
47
55
|
return assets.filter((entry) => entry.id.toLowerCase().includes(needle) || entry.label.toLowerCase().includes(needle));
|
|
48
56
|
}, [assets, query]);
|
|
49
|
-
return (_jsxs("div", { className: "flex min-h-0 flex-1 flex-col gap-1", children: [_jsx(MaterialsPalette, {}), _jsx("input", { type: "search", placeholder: "Search assets\u2026", value: query, onChange: (event) => setQuery(event.target.value), className: "rounded-md border border-white/10 bg-black/40 px-2.5 py-1.5 text-neutral-100 outline-none transition-colors placeholder:text-neutral-600 focus:border-cyan-400/60 focus:bg-black/60" }), _jsxs("div", { className: "min-h-0 flex-1 space-y-0.5 overflow-auto pr-1", children: [filtered.map((entry) => (_jsxs("div", { className: "group flex items-center gap-2 rounded-md px-1.5 py-1 transition-colors hover:bg-white/[0.06]", children: [_jsxs("div", { className: "min-w-0 flex-1 truncate", children: [_jsx("div", { className: "truncate text-neutral-100", children: entry.label }), _jsxs("div", { className: "truncate text-[10px] text-neutral-500", children: [entry.kind, " \u00B7 ", entry.id] })] }), _jsx("button", { type: "button", className: "shrink-0 rounded-md bg-cyan-500/20 px-2 py-0.5 text-cyan-200 opacity-0 ring-1 ring-inset ring-cyan-400/30 transition-all hover:bg-cyan-500/30 focus-visible:opacity-100 group-hover:opacity-100", onClick: () => onPlace(entry), children: "Place" })] }, entry.id))), filtered.length === 0 ? _jsx("div", { className: "text-neutral-500", children: "No assets match." }) : null] }), _jsx("button", { type: "button", className: "rounded-md bg-white/[0.05] px-2 py-1 text-neutral-300 ring-1 ring-inset ring-white/[0.06] transition-colors hover:bg-white/10 hover:text-neutral-100", onClick: () => {
|
|
57
|
+
return (_jsxs("div", { className: "flex min-h-0 flex-1 flex-col gap-1", children: [_jsx(MaterialsPalette, {}), _jsx("input", { type: "search", placeholder: "Search assets\u2026", value: query, onChange: (event) => setQuery(event.target.value), className: "rounded-md border border-white/10 bg-black/40 px-2.5 py-1.5 text-neutral-100 outline-none transition-colors placeholder:text-neutral-600 focus:border-cyan-400/60 focus:bg-black/60" }), _jsxs("div", { className: "min-h-0 flex-1 space-y-0.5 overflow-auto pr-1", children: [filtered.map((entry) => (_jsxs("div", { className: "group flex items-center gap-2 rounded-md px-1.5 py-1 transition-colors hover:bg-white/[0.06]", children: [_jsxs("div", { className: "min-w-0 flex-1 truncate", children: [_jsx("div", { className: "truncate text-neutral-100", children: entry.label }), _jsxs("div", { className: "truncate text-[10px] text-neutral-500", children: [entry.kind, " \u00B7 ", entry.id] }), entry.clips !== undefined && entry.clips.length > 0 ? (_jsxs("div", { className: "truncate text-[10px] text-emerald-400/80", title: `${entry.clips.length} animation clips (auto-animates when placed):\n${entry.clips.join("\n")}`, children: ["\u25B6 ", entry.clips.length, " clips \u00B7 ", describeClipRoles(entry.clips)] })) : null] }), _jsx("button", { type: "button", className: "shrink-0 rounded-md bg-cyan-500/20 px-2 py-0.5 text-cyan-200 opacity-0 ring-1 ring-inset ring-cyan-400/30 transition-all hover:bg-cyan-500/30 focus-visible:opacity-100 group-hover:opacity-100", onClick: () => onPlace(entry), children: "Place" })] }, entry.id))), filtered.length === 0 ? _jsx("div", { className: "text-neutral-500", children: "No assets match." }) : null] }), _jsx("button", { type: "button", className: "rounded-md bg-white/[0.05] px-2 py-1 text-neutral-300 ring-1 ring-inset ring-white/[0.06] transition-colors hover:bg-white/10 hover:text-neutral-100", onClick: () => {
|
|
50
58
|
const id = `marker_${Date.now().toString(36)}`;
|
|
51
59
|
const focus = null;
|
|
52
60
|
void focus;
|
|
@@ -71,6 +79,7 @@ export function assetsFromCatalog(ids, resolve) {
|
|
|
71
79
|
label: id,
|
|
72
80
|
kind: "model",
|
|
73
81
|
...(resolved?.url === undefined ? {} : { url: resolved.url }),
|
|
82
|
+
...(resolved?.clips === undefined ? {} : { clips: resolved.clips }),
|
|
74
83
|
};
|
|
75
84
|
});
|
|
76
85
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { EditorHostApi } from "./session.js";
|
|
2
|
+
import type { EditorUiStore } from "./uiStore.js";
|
|
3
|
+
/**
|
|
4
|
+
* Viewport layer that renders the active clip-preview asset playing the selected clip, mounted by
|
|
5
|
+
* `EditorApp` inside the editor's R3F scene + GameContext. Reuses the shell's shared GLTF loader,
|
|
6
|
+
* scene clone, and `useModelAnimation` mixer — the same driver `EntityModel` runs at play time — so
|
|
7
|
+
* the preview IS the runtime playback. Reads the live session from the editor UI store and publishes
|
|
8
|
+
* the selected clip's measured duration back so the dock scrubber can normalize.
|
|
9
|
+
*
|
|
10
|
+
* @internal — not a game-author entry point.
|
|
11
|
+
*/
|
|
12
|
+
export declare function ClipPreviewLayer({ api, ui }: {
|
|
13
|
+
api: EditorHostApi;
|
|
14
|
+
ui: EditorUiStore;
|
|
15
|
+
}): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Suspense, useEffect, useMemo } from "react";
|
|
3
|
+
import { useLoader } from "@react-three/fiber";
|
|
4
|
+
import * as THREE from "three";
|
|
5
|
+
import { sharedGltfLoader } from "@jgengine/shell/render/modelLoad";
|
|
6
|
+
import { cloneModelScene, disposeClonedMaterials } from "@jgengine/shell/render/modelRender";
|
|
7
|
+
import { useModelAnimation } from "@jgengine/shell/render/useModelAnimation";
|
|
8
|
+
import { previewAnimationConfig } from "./shell/clipPreview.js";
|
|
9
|
+
import { useStoreSelector } from "./useStoreSelector.js";
|
|
10
|
+
/** World-unit height the previewed model is normalized to, so tiny/huge rigs are both framed. */
|
|
11
|
+
const PREVIEW_TARGET_HEIGHT = 2;
|
|
12
|
+
/**
|
|
13
|
+
* Viewport layer that renders the active clip-preview asset playing the selected clip, mounted by
|
|
14
|
+
* `EditorApp` inside the editor's R3F scene + GameContext. Reuses the shell's shared GLTF loader,
|
|
15
|
+
* scene clone, and `useModelAnimation` mixer — the same driver `EntityModel` runs at play time — so
|
|
16
|
+
* the preview IS the runtime playback. Reads the live session from the editor UI store and publishes
|
|
17
|
+
* the selected clip's measured duration back so the dock scrubber can normalize.
|
|
18
|
+
*
|
|
19
|
+
* @internal — not a game-author entry point.
|
|
20
|
+
*/
|
|
21
|
+
export function ClipPreviewLayer({ api, ui }) {
|
|
22
|
+
const session = useStoreSelector(ui, (state) => state.clipPreview);
|
|
23
|
+
if (session === null)
|
|
24
|
+
return null;
|
|
25
|
+
return (_jsx(Suspense, { fallback: null, children: _jsx(ClipPreviewModel, { session: session, api: api, ui: ui }, session.source.assetId) }));
|
|
26
|
+
}
|
|
27
|
+
function ClipPreviewModel({ session, api, ui, }) {
|
|
28
|
+
const { source, driver } = session;
|
|
29
|
+
const gltf = useLoader(sharedGltfLoader, source.url);
|
|
30
|
+
const scene = useMemo(() => cloneModelScene(gltf.scene), [gltf]);
|
|
31
|
+
useEffect(() => () => disposeClonedMaterials(scene), [scene]);
|
|
32
|
+
// Normalize height and center/ground so any rig frames nicely at the camera focus point.
|
|
33
|
+
const placement = useMemo(() => {
|
|
34
|
+
const box = new THREE.Box3().setFromObject(scene);
|
|
35
|
+
const height = box.max.y - box.min.y;
|
|
36
|
+
if (!Number.isFinite(height) || height <= 0)
|
|
37
|
+
return { scale: 1, minY: 0, centerX: 0, centerZ: 0 };
|
|
38
|
+
return {
|
|
39
|
+
scale: PREVIEW_TARGET_HEIGHT / height,
|
|
40
|
+
minY: box.min.y,
|
|
41
|
+
centerX: (box.min.x + box.max.x) / 2,
|
|
42
|
+
centerZ: (box.min.z + box.max.z) / 2,
|
|
43
|
+
};
|
|
44
|
+
}, [scene]);
|
|
45
|
+
const config = useMemo(() => previewAnimationConfig(driver), [driver]);
|
|
46
|
+
useModelAnimation(scene, gltf.animations, config, undefined);
|
|
47
|
+
// Publish the selected clip's duration once loaded so the dock scrubber can span it (0 = unknown).
|
|
48
|
+
const clipName = driver.clipName;
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
const clip = clipName === null ? undefined : gltf.animations.find((entry) => entry.name === clipName);
|
|
51
|
+
const duration = clip?.duration ?? 0;
|
|
52
|
+
const current = ui.getState().clipPreview;
|
|
53
|
+
if (current !== null && current.source.assetId === source.assetId && current.duration !== duration) {
|
|
54
|
+
ui.patch({ clipPreview: { ...current, duration } });
|
|
55
|
+
}
|
|
56
|
+
}, [gltf, clipName, ui, source.assetId]);
|
|
57
|
+
const focus = api.getFocusTarget() ?? { x: 0, y: 0, z: 0 };
|
|
58
|
+
const s = placement.scale;
|
|
59
|
+
return (_jsx("group", { position: [focus.x, focus.y, focus.z], children: _jsx("primitive", { object: scene, scale: s, position: [-s * placement.centerX, -s * placement.minY, -s * placement.centerZ] }) }));
|
|
60
|
+
}
|
package/dist/EditorApp.d.ts
CHANGED
|
@@ -23,6 +23,12 @@ export interface EditorAppProps {
|
|
|
23
23
|
* session identity and presence rows. Omit when the host has nothing live — the panel stays honest.
|
|
24
24
|
*/
|
|
25
25
|
networkPresence?: EditorNetworkPresenceInput;
|
|
26
|
+
/**
|
|
27
|
+
* Called to close the editor and return to the game. Passed by `GameHost` when the editor was
|
|
28
|
+
* summoned over a running game; when set, the chrome shows an "Exit to game" control. Omit for the
|
|
29
|
+
* standalone editor, which has no game to return to.
|
|
30
|
+
*/
|
|
31
|
+
onExitEditor?: () => void;
|
|
26
32
|
}
|
|
27
33
|
/** Persists an exported document JSON; resolves with where it landed or why it failed. */
|
|
28
34
|
export type EditorSaveFn = (json: string) => Promise<{
|
|
@@ -31,4 +37,4 @@ export type EditorSaveFn = (json: string) => Promise<{
|
|
|
31
37
|
error?: string;
|
|
32
38
|
}>;
|
|
33
39
|
/** Top-level scene editor: author spawns/zones/paths/notes visually over edit, walk, or play modes. */
|
|
34
|
-
export declare function EditorApp({ gameId, playable, layers, catalogs, save, modeChip, networkPresence }: EditorAppProps): import("react").JSX.Element;
|
|
40
|
+
export declare function EditorApp({ gameId, playable, layers, catalogs, save, modeChip, networkPresence, onExitEditor }: EditorAppProps): import("react").JSX.Element;
|
package/dist/EditorApp.js
CHANGED
|
@@ -15,6 +15,7 @@ import { MaterialDropZone } from "./MaterialDropZone.js";
|
|
|
15
15
|
import { PerfProbe } from "./PerfProbe.js";
|
|
16
16
|
import { ScatterPreview } from "./ScatterPreview.js";
|
|
17
17
|
import { CityPreview } from "./CityPreview.js";
|
|
18
|
+
import { ClipPreviewLayer } from "./ClipPreviewLayer.js";
|
|
18
19
|
import { SelectionGizmo, ViewportSelect } from "./SelectionGizmo.js";
|
|
19
20
|
import { TerrainReadout } from "./TerrainReadout.js";
|
|
20
21
|
import { TerrainReadoutHud } from "./TerrainReadoutHud.js";
|
|
@@ -153,7 +154,7 @@ function EditorWorldOverlay({ api, ui, world, readout, }) {
|
|
|
153
154
|
unsubscribe();
|
|
154
155
|
};
|
|
155
156
|
}, [session, readoutActive]);
|
|
156
|
-
return (_jsxs(_Fragment, { children: [_jsx(PerfProbe, { api: api }), _jsx(EditorCameraDriver, { api: api }), _jsx(ViewportSelect, { api: api, ui: ui }), _jsx(MaterialDropZone, { api: api }), _jsx(AssetDropZone, { api: api }), _jsx(TerrainSculpt, { api: api, ui: ui, world: world }), _jsx(ScatterPreview, { api: api }), _jsx(CityPreview, { api: api }), showGrid ? _jsx("gridHelper", { args: [400, 80, "#3b4252", "#20242e"], position: [0, 0.05, 0] }) : null, readoutActive ? (_jsx(TerrainReadout, { groundHeightAt: groundHeightAt, region: readoutRegion, showContours: showContours, showSurfaceGrid: showSurfaceGrid, version: terrainVersion, readout: readout })) : null, _jsx(EditorLayerOverlays, { document: document, visibility: visibility, selection: selection, hoverId: hoverId, onSelect: (id) => session.dispatch({ type: "select", ids: [id] }), activePathPoint: pathPoint, groundHeightAt: groundHeightAt }), pathDraft.length > 0 ? _jsx(PathDraftPreview, { points: pathDraft }) : null, _jsx(SelectionGizmo, { session: session, ui: ui, groundSnap: groundHeightAt })] }));
|
|
157
|
+
return (_jsxs(_Fragment, { children: [_jsx(PerfProbe, { api: api }), _jsx(EditorCameraDriver, { api: api }), _jsx(ViewportSelect, { api: api, ui: ui }), _jsx(MaterialDropZone, { api: api }), _jsx(AssetDropZone, { api: api }), _jsx(TerrainSculpt, { api: api, ui: ui, world: world }), _jsx(ScatterPreview, { api: api }), _jsx(CityPreview, { api: api }), _jsx(ClipPreviewLayer, { api: api, ui: ui }), showGrid ? _jsx("gridHelper", { args: [400, 80, "#3b4252", "#20242e"], position: [0, 0.05, 0] }) : null, readoutActive ? (_jsx(TerrainReadout, { groundHeightAt: groundHeightAt, region: readoutRegion, showContours: showContours, showSurfaceGrid: showSurfaceGrid, version: terrainVersion, readout: readout })) : null, _jsx(EditorLayerOverlays, { document: document, visibility: visibility, selection: selection, hoverId: hoverId, onSelect: (id) => session.dispatch({ type: "select", ids: [id] }), activePathPoint: pathPoint, groundHeightAt: groundHeightAt }), pathDraft.length > 0 ? _jsx(PathDraftPreview, { points: pathDraft }) : null, _jsx(SelectionGizmo, { session: session, ui: ui, groundSnap: groundHeightAt })] }));
|
|
157
158
|
}
|
|
158
159
|
/** Default skin for the play/walk escape chip; swap via `EditorAppProps.modeChip`, or pass `null` to hide it (F2+E still exits). */
|
|
159
160
|
function EditorModeChip({ mode, onExit }) {
|
|
@@ -244,7 +245,7 @@ function resolveEditorCamera(document) {
|
|
|
244
245
|
};
|
|
245
246
|
}
|
|
246
247
|
/** Top-level scene editor: author spawns/zones/paths/notes visually over edit, walk, or play modes. */
|
|
247
|
-
export function EditorApp({ gameId, playable, layers, catalogs, save, modeChip, networkPresence }) {
|
|
248
|
+
export function EditorApp({ gameId, playable, layers, catalogs, save, modeChip, networkPresence, onExitEditor }) {
|
|
248
249
|
const resolvedModeChip = modeChip === undefined ? EditorModeChip : modeChip;
|
|
249
250
|
const saveFn = useMemo(() => save ?? endpointSaver(gameId), [save, gameId]);
|
|
250
251
|
const networkSnapshot = useMemo(() => buildEditorNetworkSnapshot({
|
|
@@ -483,7 +484,7 @@ export function EditorApp({ gameId, playable, layers, catalogs, save, modeChip,
|
|
|
483
484
|
}
|
|
484
485
|
};
|
|
485
486
|
const GameUI = function EditorUi() {
|
|
486
|
-
return (_jsx(EditorChrome, { gameId: gameId, session: host.api.getSession(), api: host.api, assets: catalogAssets, ui: ui, baselineDocument: host.baselineDocument, save: saveFn, networkSnapshot: networkSnapshot, onRegisterAsset: registerImportedAsset }));
|
|
487
|
+
return (_jsx(EditorChrome, { gameId: gameId, session: host.api.getSession(), api: host.api, assets: catalogAssets, ui: ui, baselineDocument: host.baselineDocument, save: saveFn, networkSnapshot: networkSnapshot, onRegisterAsset: registerImportedAsset, onExitEditor: onExitEditor }));
|
|
487
488
|
};
|
|
488
489
|
return {
|
|
489
490
|
...playable,
|
|
@@ -495,7 +496,7 @@ export function EditorApp({ gameId, playable, layers, catalogs, save, modeChip,
|
|
|
495
496
|
// WorldOverlay above is the editor layers, not PandoraViewmodel.
|
|
496
497
|
camera: inspectionCamera,
|
|
497
498
|
};
|
|
498
|
-
}, [playable, host, gameId, initialCamera, cameraProjection, catalogAssets, ui, readout, mode, saveFn, resolvedModeChip, networkSnapshot]);
|
|
499
|
+
}, [playable, host, gameId, initialCamera, cameraProjection, catalogAssets, ui, readout, mode, saveFn, resolvedModeChip, networkSnapshot, onExitEditor]);
|
|
499
500
|
const showElevation = useStoreSelector(ui, (s) => s.showElevation);
|
|
500
501
|
const onPanelCommit = useCallback((id, panel) => {
|
|
501
502
|
host.session.dispatch({ type: "setUiPanel", id, patch: panel });
|
package/dist/EditorChrome.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ import type { EditorNetworkSnapshot } from "./networkSnapshot.js";
|
|
|
10
10
|
* assistant), viewport overlays, and status bar — wired to the session, UI store, layout store,
|
|
11
11
|
* and host RPC. Mounted by `EditorApp`; not a game-author entry point.
|
|
12
12
|
*/
|
|
13
|
-
export declare function EditorChrome({ gameId, session, api, assets, ui, baselineDocument, save, networkSnapshot, importAsset, onRegisterAsset, }: {
|
|
13
|
+
export declare function EditorChrome({ gameId, session, api, assets, ui, baselineDocument, save, networkSnapshot, importAsset, onRegisterAsset, onExitEditor, }: {
|
|
14
14
|
gameId: string;
|
|
15
15
|
session: EditorSession;
|
|
16
16
|
api: EditorHostApi;
|
|
@@ -23,6 +23,8 @@ export declare function EditorChrome({ gameId, session, api, assets, ui, baselin
|
|
|
23
23
|
path?: string;
|
|
24
24
|
error?: string;
|
|
25
25
|
}>;
|
|
26
|
+
/** Closes the editor and returns to the game; set only when summoned over a game. Shows "Exit to game". */
|
|
27
|
+
onExitEditor?: () => void;
|
|
26
28
|
/**
|
|
27
29
|
* Network workspace inspection payload (adapter config + optional host presence).
|
|
28
30
|
* Built by `EditorApp` from the game definition; live presence rows only when the host injects them.
|
package/dist/EditorChrome.js
CHANGED
|
@@ -111,7 +111,7 @@ const LEFT_PAGES = [
|
|
|
111
111
|
* assistant), viewport overlays, and status bar — wired to the session, UI store, layout store,
|
|
112
112
|
* and host RPC. Mounted by `EditorApp`; not a game-author entry point.
|
|
113
113
|
*/
|
|
114
|
-
export function EditorChrome({ gameId, session, api, assets, ui, baselineDocument, save, networkSnapshot, importAsset = importAssetToHost, onRegisterAsset, }) {
|
|
114
|
+
export function EditorChrome({ gameId, session, api, assets, ui, baselineDocument, save, networkSnapshot, importAsset = importAssetToHost, onRegisterAsset, onExitEditor, }) {
|
|
115
115
|
const [, setTick] = useState(0);
|
|
116
116
|
const layoutRef = useRef(null);
|
|
117
117
|
layoutRef.current ??= createShellLayoutStore(gameId);
|
|
@@ -169,6 +169,8 @@ export function EditorChrome({ gameId, session, api, assets, ui, baselineDocumen
|
|
|
169
169
|
return { objects, foliage };
|
|
170
170
|
}, [state.document]);
|
|
171
171
|
useF2Chord("KeyE", () => api.setMode("play"));
|
|
172
|
+
// F2+Q leaves the editor entirely (back to the game) — only meaningful when summoned over one.
|
|
173
|
+
useF2Chord("KeyQ", useCallback(() => onExitEditor?.(), [onExitEditor]));
|
|
172
174
|
useEffect(() => session.subscribe(() => setTick((value) => value + 1)), [session]);
|
|
173
175
|
useEffect(() => ui.subscribe(() => setTick((value) => value + 1)), [ui]);
|
|
174
176
|
useEffect(() => layout.subscribe(() => setTick((value) => value + 1)), [layout]);
|
|
@@ -672,7 +674,9 @@ export function EditorChrome({ gameId, session, api, assets, ui, baselineDocumen
|
|
|
672
674
|
? `Drawing ${placement.kind}: click to add points (${uiState.pathDraft.length}) · Enter finish · Esc cancel`
|
|
673
675
|
: `Placing ${placement.tool === "note" ? "note" : placement.kind}: click the world · Shift-click places more · Esc cancel`;
|
|
674
676
|
const railActive = uiState.tool === "terrain" ? "terrain" : layoutState.workspace;
|
|
675
|
-
|
|
677
|
+
// Only advertise the exit chord when there is a game to return to.
|
|
678
|
+
const shortcuts = onExitEditor !== undefined ? [...SHORTCUTS, { keys: "F2+Q", action: "Exit editor to game" }] : SHORTCUTS;
|
|
679
|
+
return (_jsxs("div", { className: "pointer-events-none absolute inset-0 z-50 flex flex-col text-xs text-neutral-100", children: [_jsx(TopAppBar, { gameId: gameId, onExitToGame: onExitEditor, dirty: dirty, saveState: docSave.saveState, lastSavedAt: docSave.lastSavedAt, saveAvailable: docSave.available, saveError: docSave.saveError, canUndo: session.canUndo(), canRedo: session.canRedo(), onUndo: () => session.dispatch({ type: "undo" }), onRedo: () => session.dispatch({ type: "redo" }), onSave: docSave.doSave, onPlay: () => api.setMode("play"), onWalk: () => api.setMode("walk"), onHud: () => api.setMode("hud"), onImport: () => importInputRef.current?.click(), onExport: exportJson, onCopyJson: copyExportJson, onOpenPalette: () => setPaletteQuery(""), onToggleHelp: () => setHelpOpen((value) => !value), onResetLayout: () => layout.reset() }), _jsx("input", { ref: importInputRef, type: "file", accept: "application/json,.json", className: "hidden", onChange: (event) => {
|
|
676
680
|
const file = event.target.files?.[0];
|
|
677
681
|
if (file !== undefined)
|
|
678
682
|
importFile(file);
|
|
@@ -741,11 +745,11 @@ export function EditorChrome({ gameId, session, api, assets, ui, baselineDocumen
|
|
|
741
745
|
? `Parented to ${parentId}`
|
|
742
746
|
: `Parented ${ids.length} objects to ${parentId}`);
|
|
743
747
|
}
|
|
744
|
-
}, onClose: () => setParentPicker(null) })) : null] }), layoutState.bottomOpen ? (_jsxs(_Fragment, { children: [_jsx(PanelResizer, { orientation: "horizontal", label: "Resize bottom dock", sign: -1, onResize: (delta) => layout.resize("bottomHeight", delta) }), _jsx("div", { className: "flex shrink-0 flex-col", style: { height: layoutState.bottomHeight }, children: _jsx(BottomDock, { tab: layoutState.bottomTab, onSelectTab: (tab) => layout.patch({ bottomTab: tab }), onClose: () => layout.patch({ bottomOpen: false }), assets: liveAssets, session: session, api: api, consoleStore: consoleStore, perfHistory: perfHistory, browserView: layoutState.browserView, onSetBrowserView: (view) => layout.patch({ browserView: view }), onPlaceAsset: placeAsset, onImportModels: importModels, importBusy: importBusy }) })] })) : (_jsxs("div", { className: `pointer-events-auto flex h-7 shrink-0 items-center gap-1 border-t ${BORDER} bg-[#0e1014] px-1.5`, children: [[
|
|
748
|
+
}, onClose: () => setParentPicker(null) })) : null] }), layoutState.bottomOpen ? (_jsxs(_Fragment, { children: [_jsx(PanelResizer, { orientation: "horizontal", label: "Resize bottom dock", sign: -1, onResize: (delta) => layout.resize("bottomHeight", delta) }), _jsx("div", { className: "flex shrink-0 flex-col", style: { height: layoutState.bottomHeight }, children: _jsx(BottomDock, { tab: layoutState.bottomTab, onSelectTab: (tab) => layout.patch({ bottomTab: tab }), onClose: () => layout.patch({ bottomOpen: false }), assets: liveAssets, session: session, api: api, ui: ui, consoleStore: consoleStore, perfHistory: perfHistory, browserView: layoutState.browserView, onSetBrowserView: (view) => layout.patch({ browserView: view }), onPlaceAsset: placeAsset, onImportModels: importModels, importBusy: importBusy }) })] })) : (_jsxs("div", { className: `pointer-events-auto flex h-7 shrink-0 items-center gap-1 border-t ${BORDER} bg-[#0e1014] px-1.5`, children: [[
|
|
745
749
|
["content", "Content Browser"],
|
|
746
750
|
["console", "Console"],
|
|
747
751
|
["profiler", "Profiler"],
|
|
748
752
|
["animation", "Animation"],
|
|
749
753
|
["assistant", "AI Assistant"],
|
|
750
|
-
].map(([id, label]) => (_jsx("button", { type: "button", onClick: () => openBottomTab(id), className: `rounded-[4px] px-2 py-0.5 text-[10px] text-neutral-500 transition-colors hover:bg-white/[0.05] hover:text-neutral-300 ${FOCUS_RING}`, children: label }, id))), _jsx("span", { className: "ml-auto text-[9px] text-neutral-600", children: "Ctrl+B opens the content browser" })] }))] }), layoutState.rightOpen ? (_jsxs(_Fragment, { children: [_jsx(PanelResizer, { orientation: "vertical", label: "Resize inspector panel", sign: -1, onResize: (delta) => layout.resize("rightWidth", delta) }), _jsx("aside", { className: `pointer-events-auto hidden min-h-0 flex-col overflow-hidden border-l ${BORDER} bg-[#111318] md:flex`, style: { width: layoutState.rightWidth }, "aria-label": "Inspector dock", children: _jsx(InspectorPanel, { session: session, ui: ui, api: api, tab: layoutState.inspectorTab, onSelectTab: (tab) => layout.patch({ inspectorTab: tab }), collapsed: layoutState.collapsed, onToggleSection: (id) => layout.toggleSection(id), onClose: () => layout.patch({ rightOpen: false }) }) })] })) : null] }), _jsx(StatusBar, { api: api, history: perfHistory, objects: sceneStats.objects, foliage: sceneStats.foliage, selectionCount: state.selection.length, autosave: true }), paletteQuery !== null ? (_jsx(CommandPalette, { commands: paletteCommands, initialQuery: paletteQuery, onClose: () => setPaletteQuery(null) })) : null, helpOpen ? (_jsx("div", { className: "pointer-events-auto absolute inset-0 z-[70] flex items-center justify-center bg-black/50 backdrop-blur-[2px]", onClick: () => setHelpOpen(false), children: _jsxs("div", { className: "w-[28rem] max-w-[90vw] rounded-[10px] border border-white/10 bg-[#14171d] p-5 shadow-2xl shadow-black/60", onClick: (event) => event.stopPropagation(), role: "dialog", "aria-label": "Keyboard shortcuts", children: [_jsxs("div", { className: "mb-3 flex items-center", children: [_jsxs("div", { className: "flex items-center gap-2 text-sm font-semibold tracking-wide text-neutral-100", children: [_jsx(Icon, { name: "command", size: 15, className: "text-cyan-300" }), "Keyboard shortcuts"] }), _jsx(IconButton, { icon: "close", label: "Close shortcuts", tone: "ghost", className: "ml-auto", onClick: () => setHelpOpen(false) })] }), _jsx("div", { className: "grid max-h-[60vh] grid-cols-[auto_1fr] items-center gap-x-4 gap-y-1.5 overflow-auto", children:
|
|
754
|
+
].map(([id, label]) => (_jsx("button", { type: "button", onClick: () => openBottomTab(id), className: `rounded-[4px] px-2 py-0.5 text-[10px] text-neutral-500 transition-colors hover:bg-white/[0.05] hover:text-neutral-300 ${FOCUS_RING}`, children: label }, id))), _jsx("span", { className: "ml-auto text-[9px] text-neutral-600", children: "Ctrl+B opens the content browser" })] }))] }), layoutState.rightOpen ? (_jsxs(_Fragment, { children: [_jsx(PanelResizer, { orientation: "vertical", label: "Resize inspector panel", sign: -1, onResize: (delta) => layout.resize("rightWidth", delta) }), _jsx("aside", { className: `pointer-events-auto hidden min-h-0 flex-col overflow-hidden border-l ${BORDER} bg-[#111318] md:flex`, style: { width: layoutState.rightWidth }, "aria-label": "Inspector dock", children: _jsx(InspectorPanel, { session: session, ui: ui, api: api, assets: liveAssets, tab: layoutState.inspectorTab, onSelectTab: (tab) => layout.patch({ inspectorTab: tab }), collapsed: layoutState.collapsed, onToggleSection: (id) => layout.toggleSection(id), onClose: () => layout.patch({ rightOpen: false }) }) })] })) : null] }), _jsx(StatusBar, { api: api, history: perfHistory, objects: sceneStats.objects, foliage: sceneStats.foliage, selectionCount: state.selection.length, autosave: true }), paletteQuery !== null ? (_jsx(CommandPalette, { commands: paletteCommands, initialQuery: paletteQuery, onClose: () => setPaletteQuery(null) })) : null, helpOpen ? (_jsx("div", { className: "pointer-events-auto absolute inset-0 z-[70] flex items-center justify-center bg-black/50 backdrop-blur-[2px]", onClick: () => setHelpOpen(false), children: _jsxs("div", { className: "w-[28rem] max-w-[90vw] rounded-[10px] border border-white/10 bg-[#14171d] p-5 shadow-2xl shadow-black/60", onClick: (event) => event.stopPropagation(), role: "dialog", "aria-label": "Keyboard shortcuts", children: [_jsxs("div", { className: "mb-3 flex items-center", children: [_jsxs("div", { className: "flex items-center gap-2 text-sm font-semibold tracking-wide text-neutral-100", children: [_jsx(Icon, { name: "command", size: 15, className: "text-cyan-300" }), "Keyboard shortcuts"] }), _jsx(IconButton, { icon: "close", label: "Close shortcuts", tone: "ghost", className: "ml-auto", onClick: () => setHelpOpen(false) })] }), _jsx("div", { className: "grid max-h-[60vh] grid-cols-[auto_1fr] items-center gap-x-4 gap-y-1.5 overflow-auto", children: shortcuts.map((entry) => (_jsxs("div", { className: "contents", children: [_jsx("span", { className: "justify-self-start", children: _jsx(Kbd, { children: entry.keys }) }), _jsx("span", { className: "text-neutral-300", children: entry.action })] }, entry.keys))) })] }) })) : null] }));
|
|
751
755
|
}
|
package/dist/InspectorPanel.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type EditorSession } from "@jgengine/core/editor/index";
|
|
2
|
+
import { type EditorAssetEntry } from "./AssetBrowser.js";
|
|
2
3
|
import type { EditorHostApi } from "./session.js";
|
|
3
4
|
import type { EditorUiStore } from "./uiStore.js";
|
|
4
5
|
import type { InspectorTab } from "./shell/layoutStore.js";
|
|
@@ -9,12 +10,14 @@ import type { InspectorTab } from "./shell/layoutStore.js";
|
|
|
9
10
|
* `pathPoint` slice, so UI-only churn never rerenders it on `EditorChrome`'s render tick.
|
|
10
11
|
* @internal — mounted by `EditorChrome` inside the right dock.
|
|
11
12
|
*/
|
|
12
|
-
export declare function InspectorPanel({ session, ui, onClose, api, tab, onSelectTab, collapsed, onToggleSection, }: {
|
|
13
|
+
export declare function InspectorPanel({ session, ui, onClose, api, assets, tab, onSelectTab, collapsed, onToggleSection, }: {
|
|
13
14
|
session: EditorSession;
|
|
14
15
|
ui: EditorUiStore;
|
|
15
16
|
onClose?: () => void;
|
|
16
17
|
/** Host API for RPC-backed actions (material assignment, camera). */
|
|
17
18
|
api?: EditorHostApi;
|
|
19
|
+
/** Live browser asset list, used to resolve a placed marker's rig clips for animation authoring. */
|
|
20
|
+
assets?: readonly EditorAssetEntry[];
|
|
18
21
|
/** Controlled active tab; omit for local state. */
|
|
19
22
|
tab?: InspectorTab;
|
|
20
23
|
onSelectTab?: (tab: InspectorTab) => void;
|
package/dist/InspectorPanel.js
CHANGED
|
@@ -4,10 +4,13 @@ import { editorParentOf, findEditorNote, findEditorPath, isEditorObjectLocked, }
|
|
|
4
4
|
import { readVegetationSettings, vegetationFootprint } from "@jgengine/core/world/vegetation";
|
|
5
5
|
import { getSceneKind, isSceneKind, parseParams, } from "@jgengine/core/scene/sceneKinds";
|
|
6
6
|
import { getAssetGenerator } from "@jgengine/core/scene/assetGenerator";
|
|
7
|
+
import { markerCatalogId } from "@jgengine/core/world/authoredObjects";
|
|
7
8
|
import { useGameContext } from "@jgengine/react/provider";
|
|
8
9
|
import { canAuthorTrigger, clearMaterialAssignmentPatch, clearTriggerInstallPatch, defaultTriggerInstallPatch, hasAuthoredTrigger, hasMaterialAssignment, } from "./authoredComponentMeta.js";
|
|
9
10
|
import { SchemaInspector } from "./SchemaInspector.js";
|
|
10
11
|
import { TriggerInspector } from "./TriggerInspector.js";
|
|
12
|
+
import { describeClipRoles } from "./AssetBrowser.js";
|
|
13
|
+
import { animationMode, animationMetaPatch, LOCOMOTION_ROLES, ONE_SHOT_EVENTS, readAnimationSetting, setAnimationMode, setLocomotionClip, setLocomotionNumber, setOneShotClip, } from "./modelAnimationAuthoring.js";
|
|
11
14
|
import { listParentCandidates } from "./parentCandidates.js";
|
|
12
15
|
import { TERRAIN_MATERIALS } from "./uiStore.js";
|
|
13
16
|
import { shallowArrayEqual, useStoreSelector } from "./useStoreSelector.js";
|
|
@@ -15,6 +18,7 @@ import { INPUT } from "./chromeStyles.js";
|
|
|
15
18
|
import { NumberField } from "./chromeFields.js";
|
|
16
19
|
import { AxisNumberField, FieldRow, SectionAction } from "./shell/fields.js";
|
|
17
20
|
import { Icon, kindIcon } from "./shell/icons.js";
|
|
21
|
+
import { CoverageSection } from "./shell/scatterCoverage.js";
|
|
18
22
|
import { FOCUS_RING, INPUT_CLS, NUMERIC } from "./shell/theme.js";
|
|
19
23
|
import { CollapsibleSection, EmptyState, IconButton, PanelTabs } from "./shell/ui.js";
|
|
20
24
|
function VegetationFields({ volume, onMeta, }) {
|
|
@@ -49,8 +53,11 @@ function KindInspector({ object, meta, onMeta }) {
|
|
|
49
53
|
const definition = getSceneKind(object.kind);
|
|
50
54
|
if (definition === undefined)
|
|
51
55
|
return null;
|
|
52
|
-
const
|
|
53
|
-
|
|
56
|
+
const params = parseParams(definition.schema, object.meta);
|
|
57
|
+
const note = definition.note?.(object, params);
|
|
58
|
+
const coverage = definition.coverage;
|
|
59
|
+
const accent = definition.accent ?? "#34d399";
|
|
60
|
+
return (_jsxs("div", { className: "space-y-2", children: [coverage !== undefined ? (_jsx(CoverageSection, { schema: definition.schema, coverage: coverage, object: object, params: params, accent: accent, onMeta: onMeta, ...(note === undefined ? {} : { note }) })) : null, _jsx(SchemaInspector, { schema: definition.schema, label: definition.label, meta: meta, onMeta: onMeta, ...(definition.accent === undefined ? {} : { accent: definition.accent }), ...(coverage === undefined && note !== undefined ? { note } : {}), ...(coverage === undefined ? {} : { hideKeys: [coverage.densityKey] }) })] }));
|
|
54
61
|
}
|
|
55
62
|
/** Auto-generated inspector for a placed generator asset's params (building/bookcase/…). */
|
|
56
63
|
function GeneratorInspector({ meta, onMeta }) {
|
|
@@ -107,6 +114,26 @@ function ComponentAddBar({ target, meta, onMeta, api, selectionIds, }) {
|
|
|
107
114
|
event.currentTarget.value = "";
|
|
108
115
|
}, children: [_jsx("option", { value: "", children: "+ Material\u2026" }), TERRAIN_MATERIALS.map((material) => (_jsx("option", { value: material.id, children: material.label }, material.id)))] })] })) : null] })] }));
|
|
109
116
|
}
|
|
117
|
+
/** A labeled clip dropdown (never free-text) for a role/event; "— none —" clears the binding. */
|
|
118
|
+
function ClipSelectRow({ label, clips, value, onChange, }) {
|
|
119
|
+
return (_jsx(FieldRow, { label: label, children: _jsxs("select", { className: `h-6.5 w-full min-w-0 px-1.5 ${INPUT_CLS}`, value: value ?? "", "aria-label": `${label} clip`, onChange: (event) => onChange(event.target.value === "" ? null : event.target.value), children: [_jsx("option", { value: "", children: "\u2014 none \u2014" }), clips.map((clip) => (_jsx("option", { value: clip, children: clip }, clip)))] }) }));
|
|
120
|
+
}
|
|
121
|
+
const ANIMATION_MODE_HINT = {
|
|
122
|
+
default: "Inherits the catalog default: a rigged asset auto-animates from its clip roles.",
|
|
123
|
+
auto: "Forces derivation of idle/walk/run + hit/death/attack from this rig's clip names.",
|
|
124
|
+
none: "Renders the bind pose — no clip playback.",
|
|
125
|
+
custom: "Author explicit role→clip bindings below; unset roles fall back to the rig's defaults.",
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* Inspector section authoring a placement's `ModelConfig.animation` for a rigged catalog asset.
|
|
129
|
+
* Persists to `marker.meta.animation` through the passed `onChange` (an undoable `setMarker` dispatch).
|
|
130
|
+
* All clip picks are dropdowns over the asset's real clip list — never free-text.
|
|
131
|
+
*/
|
|
132
|
+
function ModelAnimationSection({ clips, setting, onChange, }) {
|
|
133
|
+
const mode = animationMode(setting);
|
|
134
|
+
const config = mode === "custom" && typeof setting === "object" ? setting : null;
|
|
135
|
+
return (_jsxs("div", { className: "space-y-2.5", children: [_jsxs("div", { className: "text-[10px] text-neutral-500", children: [clips.length, " clips \u00B7 ", describeClipRoles(clips)] }), _jsx(FieldRow, { label: "Mode", children: _jsxs("select", { className: `h-6.5 w-full min-w-0 px-1.5 ${INPUT_CLS}`, value: mode, "aria-label": "Animation mode", onChange: (event) => onChange(setAnimationMode(setting, event.target.value, clips)), children: [_jsx("option", { value: "default", children: "Default (catalog)" }), _jsx("option", { value: "auto", children: "Auto (derive)" }), _jsx("option", { value: "none", children: "None (bind pose)" }), _jsx("option", { value: "custom", children: "Custom" })] }) }), config === null ? (_jsx("div", { className: "text-[10px] leading-relaxed text-neutral-600", children: ANIMATION_MODE_HINT[mode] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "space-y-1.5 rounded-[6px] border border-white/[0.06] bg-white/[0.02] p-2", children: [_jsx("div", { className: "text-[9px] font-semibold uppercase tracking-wider text-neutral-500", children: "Locomotion states" }), LOCOMOTION_ROLES.map((role) => (_jsx(ClipSelectRow, { label: role, clips: clips, value: config.states?.[role] ?? null, onChange: (clip) => onChange(setLocomotionClip(setting, role, clip)) }, role))), _jsx(FieldRow, { label: "walkSpeed", title: "Speed (u/s) above which the entity counts as moving (walk).", children: _jsx(AxisNumberField, { label: "u/s", step: 0.1, value: config.states?.walkSpeed ?? 0.5, onCommit: (value) => onChange(setLocomotionNumber(setting, "walkSpeed", Math.max(0, value))) }) }), _jsx(FieldRow, { label: "runSpeed", title: "Speed above which the run clip plays (when a run clip is set).", children: _jsx(AxisNumberField, { label: "u/s", step: 0.5, value: config.states?.runSpeed ?? 6, onCommit: (value) => onChange(setLocomotionNumber(setting, "runSpeed", Math.max(0, value))) }) }), _jsx(FieldRow, { label: "fadeSec", title: "Crossfade duration (s) when the locomotion state changes.", children: _jsx(AxisNumberField, { label: "s", step: 0.05, value: config.states?.fadeSec ?? 0.2, onCommit: (value) => onChange(setLocomotionNumber(setting, "fadeSec", Math.max(0, value))) }) })] }), _jsxs("div", { className: "space-y-1.5 rounded-[6px] border border-white/[0.06] bg-white/[0.02] p-2", children: [_jsx("div", { className: "text-[9px] font-semibold uppercase tracking-wider text-neutral-500", children: "One-shot events" }), _jsx("div", { className: "text-[10px] text-neutral-600", children: "hit / death auto-fire on damage/death; others via playEntityAnimation." }), ONE_SHOT_EVENTS.map((event) => (_jsx(ClipSelectRow, { label: event, clips: clips, value: config.oneShots?.[event] ?? null, onChange: (clip) => onChange(setOneShotClip(setting, event, clip)) }, event)))] })] }))] }));
|
|
136
|
+
}
|
|
110
137
|
function Section({ id, title, icon, sections, children, trailing, }) {
|
|
111
138
|
return (_jsx(CollapsibleSection, { title: title, ...(icon === undefined ? {} : { icon }), open: sections.collapsed[id] !== true, onToggle: () => sections.toggle(id), ...(trailing === undefined ? {} : { trailing }), children: children }));
|
|
112
139
|
}
|
|
@@ -127,7 +154,7 @@ function MaterialsTab({ api, selection, meta, }) {
|
|
|
127
154
|
* `pathPoint` slice, so UI-only churn never rerenders it on `EditorChrome`'s render tick.
|
|
128
155
|
* @internal — mounted by `EditorChrome` inside the right dock.
|
|
129
156
|
*/
|
|
130
|
-
export function InspectorPanel({ session, ui, onClose, api, tab, onSelectTab, collapsed, onToggleSection, }) {
|
|
157
|
+
export function InspectorPanel({ session, ui, onClose, api, assets, tab, onSelectTab, collapsed, onToggleSection, }) {
|
|
131
158
|
const document = useStoreSelector(session, (state) => state.document);
|
|
132
159
|
const selection = useStoreSelector(session, (state) => state.selection, shallowArrayEqual);
|
|
133
160
|
const pathPoint = useStoreSelector(ui, (state) => state.pathPoint);
|
|
@@ -198,12 +225,16 @@ export function InspectorPanel({ session, ui, onClose, api, tab, onSelectTab, co
|
|
|
198
225
|
else if (selectedMarker !== undefined) {
|
|
199
226
|
const marker = selectedMarker;
|
|
200
227
|
const onMeta = (patch, coalesce) => session.dispatch({ type: "setMarker", id: marker.id, patch: { meta: { ...marker.meta, ...patch } } }, { coalesce: `${coalesce}:${marker.id}` });
|
|
228
|
+
// Rig clips for animation authoring: this placement's catalog asset must carry animation clips.
|
|
229
|
+
const markerCatalog = markerCatalogId(marker);
|
|
230
|
+
const markerRigAsset = markerCatalog === null ? undefined : assets?.find((entry) => entry.id === markerCatalog);
|
|
231
|
+
const markerRigClips = markerRigAsset?.clips !== undefined && markerRigAsset.clips.length > 0 ? markerRigAsset.clips : null;
|
|
201
232
|
const hasTrigger = hasAuthoredTrigger(marker.meta);
|
|
202
233
|
const hasMaterial = hasMaterialAssignment(marker.meta);
|
|
203
234
|
const hasGenerator = typeof marker.meta?.["assetId"] === "string" && getAssetGenerator(marker.meta["assetId"]) !== undefined;
|
|
204
235
|
const hasKindParams = isSceneKind(marker.kind);
|
|
205
236
|
const componentCards = (_jsxs(_Fragment, { children: [_jsx(ComponentAddBar, { target: "marker", meta: marker.meta, onMeta: onMeta, api: api, selectionIds: [marker.id] }), hasKindParams ? (_jsx(Section, { id: "kindParams", title: "Kind parameters", icon: "settings", sections: sections, children: _jsx(KindInspector, { object: markerObject(marker), meta: marker.meta, onMeta: onMeta }) })) : null, hasTrigger ? (_jsx(Section, { id: "trigger", title: "Trigger", icon: "target", sections: sections, trailing: _jsx(SectionAction, { label: "Remove trigger component", onClick: () => onMeta(clearTriggerInstallPatch(), "component:remove-trigger"), children: "remove" }), children: _jsx(TriggerInspector, { target: "marker", meta: marker.meta, onMeta: onMeta }) })) : null, hasMaterial ? (_jsx(Section, { id: "material", title: "Material", icon: "sphere", sections: sections, trailing: _jsx(SectionAction, { label: "Remove material assignment", onClick: () => onMeta(clearMaterialAssignmentPatch(), "component:remove-material"), children: "remove" }), children: _jsxs("div", { className: "text-[11px] text-neutral-300", children: ["materialId: ", _jsx("span", { className: NUMERIC, children: String(marker.meta?.["materialId"]) })] }) })) : null, hasGenerator ? (_jsx(Section, { id: "generator", title: "Generator", icon: "cube", sections: sections, children: _jsx(GeneratorInspector, { meta: marker.meta, onMeta: onMeta }) })) : null, !hasKindParams && !hasTrigger && !hasMaterial && !hasGenerator && !canAuthorTrigger("marker") ? (_jsx(EmptyState, { icon: "settings", title: "No components", description: "This marker has no optional components. Games that register trigger actions enable Add trigger here." })) : null] }));
|
|
206
|
-
body = (_jsxs("div", { className: "space-y-2 p-2.5", children: [_jsx(ObjectHeader, { kind: marker.kind, id: marker.id, label: marker.label ?? "", placeholder: marker.id, locked: isEditorObjectLocked(document, marker.id), onLabel: (value) => session.dispatch({ type: "setMarker", id: marker.id, patch: { label: value } }, { coalesce: `label:${marker.id}` }) }), activeTab === "inspector" ? (_jsxs(_Fragment, { children: [_jsx(Section, { id: "transform", title: "Transform", icon: "move", sections: sections, trailing: _jsx(SectionAction, { label: "Reset position to origin", onClick: () => session.dispatch({ type: "setTransform", id: marker.id, position: { x: 0, y: 0, z: 0 } }), children: "reset" }), children: _jsxs("div", { className: "space-y-1.5", children: [_jsx(FieldRow, { label: "Position", children: ["x", "y", "z"].map((axis) => (_jsx(AxisNumberField, { axis: axis, label: axis, step: 0.5, value: marker.position[axis], onCommit: (value) => session.dispatch({ type: "setTransform", id: marker.id, position: { ...marker.position, [axis]: value } }, { coalesce: `pos:${axis}:${marker.id}` }) }, axis))) }), _jsx(FieldRow, { label: "Rotation", children: _jsx(AxisNumberField, { axis: "y", label: "y\u00B0", step: 5, precision: 1, value: Math.round((((marker.rotationY ?? 0) * 180) / Math.PI) * 10) / 10, onCommit: (value) => session.dispatch({ type: "setTransform", id: marker.id, rotationY: (value * Math.PI) / 180 }, { coalesce: `rot:${marker.id}` }) }) })] }) }), _jsx(Section, { id: "appearance", title: "Appearance", icon: "sphere", sections: sections, children: _jsx(KindColorFields, { kind: marker.kind, color: marker.color, onKind: (kind) => session.dispatch({ type: "setMarker", id: marker.id, patch: { kind } }, { coalesce: `kind:${marker.id}` }), onColor: (color) => session.dispatch({ type: "setMarker", id: marker.id, patch: { color } }, { coalesce: `color:${marker.id}` }) }) }), _jsx(Section, { id: "placement", title: "Placement", icon: "pin", sections: sections, children: _jsxs("div", { className: "space-y-2", children: [_jsx(ClearanceField, { meta: marker.meta, onMeta: onMeta }), _jsx(ParentField, { session: session, id: marker.id })] }) }), marker.meta !== undefined ? (_jsx(Section, { id: "meta", title: "Raw metadata", icon: "script", sections: sections, children: _jsx("pre", { className: "max-h-48 overflow-auto rounded-[5px] border border-white/[0.06] bg-black/40 p-2 text-[10px] text-neutral-400", children: JSON.stringify(marker.meta, null, 2) }) })) : null] })) : (componentCards)] }));
|
|
237
|
+
body = (_jsxs("div", { className: "space-y-2 p-2.5", children: [_jsx(ObjectHeader, { kind: marker.kind, id: marker.id, label: marker.label ?? "", placeholder: marker.id, locked: isEditorObjectLocked(document, marker.id), onLabel: (value) => session.dispatch({ type: "setMarker", id: marker.id, patch: { label: value } }, { coalesce: `label:${marker.id}` }) }), activeTab === "inspector" ? (_jsxs(_Fragment, { children: [_jsx(Section, { id: "transform", title: "Transform", icon: "move", sections: sections, trailing: _jsx(SectionAction, { label: "Reset position to origin", onClick: () => session.dispatch({ type: "setTransform", id: marker.id, position: { x: 0, y: 0, z: 0 } }), children: "reset" }), children: _jsxs("div", { className: "space-y-1.5", children: [_jsx(FieldRow, { label: "Position", children: ["x", "y", "z"].map((axis) => (_jsx(AxisNumberField, { axis: axis, label: axis, step: 0.5, value: marker.position[axis], onCommit: (value) => session.dispatch({ type: "setTransform", id: marker.id, position: { ...marker.position, [axis]: value } }, { coalesce: `pos:${axis}:${marker.id}` }) }, axis))) }), _jsx(FieldRow, { label: "Rotation", children: _jsx(AxisNumberField, { axis: "y", label: "y\u00B0", step: 5, precision: 1, value: Math.round((((marker.rotationY ?? 0) * 180) / Math.PI) * 10) / 10, onCommit: (value) => session.dispatch({ type: "setTransform", id: marker.id, rotationY: (value * Math.PI) / 180 }, { coalesce: `rot:${marker.id}` }) }) })] }) }), _jsx(Section, { id: "appearance", title: "Appearance", icon: "sphere", sections: sections, children: _jsx(KindColorFields, { kind: marker.kind, color: marker.color, onKind: (kind) => session.dispatch({ type: "setMarker", id: marker.id, patch: { kind } }, { coalesce: `kind:${marker.id}` }), onColor: (color) => session.dispatch({ type: "setMarker", id: marker.id, patch: { color } }, { coalesce: `color:${marker.id}` }) }) }), _jsx(Section, { id: "placement", title: "Placement", icon: "pin", sections: sections, children: _jsxs("div", { className: "space-y-2", children: [_jsx(ClearanceField, { meta: marker.meta, onMeta: onMeta }), _jsx(ParentField, { session: session, id: marker.id })] }) }), markerRigClips !== null ? (_jsx(Section, { id: "modelAnimation", title: "Animation", icon: "film", sections: sections, children: _jsx(ModelAnimationSection, { clips: markerRigClips, setting: readAnimationSetting(marker.meta), onChange: (next) => onMeta(animationMetaPatch(next), "animation") }) })) : null, marker.meta !== undefined ? (_jsx(Section, { id: "meta", title: "Raw metadata", icon: "script", sections: sections, children: _jsx("pre", { className: "max-h-48 overflow-auto rounded-[5px] border border-white/[0.06] bg-black/40 p-2 text-[10px] text-neutral-400", children: JSON.stringify(marker.meta, null, 2) }) })) : null] })) : (componentCards)] }));
|
|
207
238
|
}
|
|
208
239
|
else if (selectedVolume !== undefined) {
|
|
209
240
|
const volume = selectedVolume;
|
|
@@ -9,11 +9,13 @@ export type MetaPatch = (patch: Record<string, unknown>, coalesce: string) => vo
|
|
|
9
9
|
* seam owns the inspector. Replaces the hand-written per-kind field blocks (`ScatterFields`).
|
|
10
10
|
* @internal — mounted by `EditorChrome`'s inspector; not a game-author entry point.
|
|
11
11
|
*/
|
|
12
|
-
export declare function SchemaInspector({ schema, label, accent, note, meta, onMeta, }: {
|
|
12
|
+
export declare function SchemaInspector({ schema, label, accent, note, meta, onMeta, hideKeys, }: {
|
|
13
13
|
schema: ParamSchema;
|
|
14
14
|
label: string;
|
|
15
15
|
accent?: string;
|
|
16
16
|
note?: string;
|
|
17
17
|
meta: Record<string, unknown> | undefined;
|
|
18
18
|
onMeta: MetaPatch;
|
|
19
|
+
/** Field keys to omit from this body — hoisted elsewhere (e.g. the density slider in `CoverageSection`). */
|
|
20
|
+
hideKeys?: readonly string[];
|
|
19
21
|
}): import("react").JSX.Element;
|
package/dist/SchemaInspector.js
CHANGED
|
@@ -82,10 +82,15 @@ function GroupSection({ title, accent, fields, schema, meta, onMeta, defaultColl
|
|
|
82
82
|
* seam owns the inspector. Replaces the hand-written per-kind field blocks (`ScatterFields`).
|
|
83
83
|
* @internal — mounted by `EditorChrome`'s inspector; not a game-author entry point.
|
|
84
84
|
*/
|
|
85
|
-
export function SchemaInspector({ schema, label, accent = "#34d399", note, meta, onMeta, }) {
|
|
85
|
+
export function SchemaInspector({ schema, label, accent = "#34d399", note, meta, onMeta, hideKeys, }) {
|
|
86
86
|
const groups = schema.groups ?? [];
|
|
87
87
|
const grouped = new Set(groups.map((group) => group.id));
|
|
88
|
+
const hidden = hideKeys === undefined || hideKeys.length === 0 ? null : new Set(hideKeys);
|
|
89
|
+
const visible = hidden === null ? schema.fields : schema.fields.filter((field) => !hidden.has(field.key));
|
|
88
90
|
// Fields with no group (or a group not declared in schema.groups) render first, headerless.
|
|
89
|
-
const ungrouped =
|
|
90
|
-
return (_jsxs("div", { className: "space-y-2 rounded-lg border p-2.5", style: { borderColor: `${accent}26`, backgroundColor: `${accent}10` }, children: [_jsx("div", { className: "text-[9px] font-semibold uppercase tracking-[0.14em]", style: { color: accent }, children: label }), _jsx(PresetRow, { schema: schema, onMeta: onMeta }), ungrouped.map((field) => field.type === "action" ? (_jsx(ActionButton, { field: field, schema: schema, onMeta: onMeta }, field.key)) : (_jsx(FieldRow, { field: field, meta: meta, onMeta: onMeta }, field.key))), groups
|
|
91
|
+
const ungrouped = visible.filter((field) => field.group === undefined || !grouped.has(field.group));
|
|
92
|
+
return (_jsxs("div", { className: "space-y-2 rounded-lg border p-2.5", style: { borderColor: `${accent}26`, backgroundColor: `${accent}10` }, children: [_jsx("div", { className: "text-[9px] font-semibold uppercase tracking-[0.14em]", style: { color: accent }, children: label }), _jsx(PresetRow, { schema: schema, onMeta: onMeta }), ungrouped.map((field) => field.type === "action" ? (_jsx(ActionButton, { field: field, schema: schema, onMeta: onMeta }, field.key)) : (_jsx(FieldRow, { field: field, meta: meta, onMeta: onMeta }, field.key))), groups
|
|
93
|
+
.map((group) => ({ group, fields: visible.filter((field) => field.group === group.id) }))
|
|
94
|
+
.filter((entry) => entry.fields.length > 0)
|
|
95
|
+
.map(({ group, fields }) => (_jsx(GroupSection, { title: group.label, accent: accent, fields: fields, schema: schema, meta: meta, onMeta: onMeta, defaultCollapsed: group.collapsed ?? false }, group.id))), note !== undefined && note.length > 0 ? _jsx("div", { className: "text-[10px] text-neutral-500", children: note }) : null] }));
|
|
91
96
|
}
|
package/dist/handlers/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { documentHandlers } from "./document.js";
|
|
|
4
4
|
import { gridHandlers } from "./grids.js";
|
|
5
5
|
import { hierarchyHandlers } from "./hierarchy.js";
|
|
6
6
|
import { minimapHandlers } from "./minimap.js";
|
|
7
|
+
import { streetsHandlers } from "./streets.js";
|
|
7
8
|
import { runtimeHandlers } from "./runtime.js";
|
|
8
9
|
import { terrainHandlers } from "./terrain.js";
|
|
9
10
|
/**
|
|
@@ -16,6 +17,7 @@ export const EDITOR_RPC_HANDLERS = {
|
|
|
16
17
|
...catalogHandlers,
|
|
17
18
|
...terrainHandlers,
|
|
18
19
|
...minimapHandlers,
|
|
20
|
+
...streetsHandlers,
|
|
19
21
|
...hierarchyHandlers,
|
|
20
22
|
...collectionHandlers,
|
|
21
23
|
...gridHandlers,
|