@jgengine/editor 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +12 -0
- package/dist/AssetBrowser.d.ts +26 -1
- package/dist/AssetBrowser.js +54 -1
- package/dist/AssetDropZone.d.ts +9 -0
- package/dist/AssetDropZone.js +65 -0
- package/dist/CatalogsPanel.d.ts +4 -2
- package/dist/CatalogsPanel.js +162 -9
- package/dist/CityPreview.d.ts +11 -0
- package/dist/CityPreview.js +45 -0
- package/dist/CollectionsPanel.js +5 -5
- package/dist/DebugDraw.d.ts +3 -1
- package/dist/DebugDraw.js +37 -19
- package/dist/EditorApp.d.ts +9 -1
- package/dist/EditorApp.js +147 -35
- package/dist/EditorChrome.d.ts +25 -6
- package/dist/EditorChrome.js +310 -130
- package/dist/InspectorPanel.d.ts +17 -7
- package/dist/InspectorPanel.js +173 -27
- package/dist/LightingPanel.d.ts +11 -0
- package/dist/LightingPanel.js +99 -0
- package/dist/MaterialsWorkspacePanel.d.ts +12 -0
- package/dist/MaterialsWorkspacePanel.js +86 -0
- package/dist/ParentPickerMenu.d.ts +13 -0
- package/dist/ParentPickerMenu.js +33 -0
- package/dist/PerfProbe.js +33 -0
- package/dist/PrefabsPanel.js +5 -5
- package/dist/RuntimePlayBridge.d.ts +5 -3
- package/dist/RuntimePlayBridge.js +6 -13
- package/dist/ScatterPreview.d.ts +3 -1
- package/dist/ScatterPreview.js +10 -7
- package/dist/SchemaInspector.js +21 -8
- package/dist/ScriptingPanel.d.ts +15 -0
- package/dist/ScriptingPanel.js +46 -0
- package/dist/SelectionGizmo.js +114 -30
- package/dist/StandaloneEditor.d.ts +16 -9
- package/dist/StandaloneEditor.js +19 -20
- package/dist/TerrainPanel.d.ts +3 -2
- package/dist/TerrainPanel.js +16 -5
- package/dist/TerrainSculpt.js +98 -59
- package/dist/agent/AgentPanel.d.ts +5 -2
- package/dist/agent/AgentPanel.js +11 -6
- package/dist/agent/toolBridge.js +5 -0
- package/dist/assetImport.d.ts +36 -0
- package/dist/assetImport.js +66 -0
- package/dist/authoredComponentMeta.d.ts +25 -0
- package/dist/authoredComponentMeta.js +65 -0
- package/dist/gizmoPivot.d.ts +4 -0
- package/dist/gizmoPivot.js +34 -0
- package/dist/handlers/catalogs.d.ts +3 -0
- package/dist/handlers/catalogs.js +137 -0
- package/dist/handlers/collections.d.ts +3 -0
- package/dist/handlers/collections.js +57 -0
- package/dist/handlers/context.d.ts +36 -0
- package/dist/handlers/context.js +1 -0
- package/dist/handlers/document.d.ts +3 -0
- package/dist/handlers/document.js +350 -0
- package/dist/handlers/grids.d.ts +3 -0
- package/dist/handlers/grids.js +142 -0
- package/dist/handlers/hierarchy.d.ts +3 -0
- package/dist/handlers/hierarchy.js +55 -0
- package/dist/handlers/index.d.ts +8 -0
- package/dist/handlers/index.js +23 -0
- package/dist/handlers/minimap.d.ts +9 -0
- package/dist/handlers/minimap.js +32 -0
- package/dist/handlers/runtime.d.ts +3 -0
- package/dist/handlers/runtime.js +191 -0
- package/dist/handlers/terrain.d.ts +3 -0
- package/dist/handlers/terrain.js +204 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/listDocumentTriggers.d.ts +42 -0
- package/dist/listDocumentTriggers.js +49 -0
- package/dist/materialAssignments.d.ts +63 -0
- package/dist/materialAssignments.js +84 -0
- package/dist/mcp/fieldSpec.d.ts +32 -0
- package/dist/mcp/fieldSpec.js +173 -0
- package/dist/mcp/rpcRequest.d.ts +8 -5
- package/dist/mcp/rpcRequest.js +72 -93
- package/dist/mcp/tools.js +104 -912
- package/dist/networkSnapshot.d.ts +82 -0
- package/dist/networkSnapshot.js +73 -0
- package/dist/parentCandidates.d.ts +12 -0
- package/dist/parentCandidates.js +25 -0
- package/dist/session.d.ts +77 -4
- package/dist/session.js +36 -1017
- package/dist/shell/AnimationPanel.d.ts +17 -0
- package/dist/shell/AnimationPanel.js +134 -0
- package/dist/shell/AssetThumbnail.d.ts +12 -0
- package/dist/shell/AssetThumbnail.js +23 -0
- package/dist/shell/BottomDock.d.ts +25 -0
- package/dist/shell/BottomDock.js +40 -0
- package/dist/shell/CommandPalette.d.ts +12 -0
- package/dist/shell/CommandPalette.js +56 -0
- package/dist/shell/ConsolePanel.d.ts +8 -0
- package/dist/shell/ConsolePanel.js +45 -0
- package/dist/shell/ContentBrowser.d.ts +21 -0
- package/dist/shell/ContentBrowser.js +130 -0
- package/dist/shell/HierarchyPanel.d.ts +26 -0
- package/dist/shell/HierarchyPanel.js +387 -0
- package/dist/shell/NetworkWorkspacePanel.d.ts +11 -0
- package/dist/shell/NetworkWorkspacePanel.js +49 -0
- package/dist/shell/PlayModeBar.d.ts +12 -0
- package/dist/shell/PlayModeBar.js +27 -0
- package/dist/shell/ProfilerPanel.d.ts +10 -0
- package/dist/shell/ProfilerPanel.js +41 -0
- package/dist/shell/SceneToolbar.d.ts +43 -0
- package/dist/shell/SceneToolbar.js +122 -0
- package/dist/shell/StatusBar.d.ts +18 -0
- package/dist/shell/StatusBar.js +41 -0
- package/dist/shell/TopAppBar.d.ts +33 -0
- package/dist/shell/TopAppBar.js +64 -0
- package/dist/shell/ViewportOverlays.d.ts +23 -0
- package/dist/shell/ViewportOverlays.js +136 -0
- package/dist/shell/WorkspaceRail.d.ts +20 -0
- package/dist/shell/WorkspaceRail.js +31 -0
- package/dist/shell/cameraTelemetry.d.ts +27 -0
- package/dist/shell/cameraTelemetry.js +24 -0
- package/dist/shell/commandRegistry.d.ts +58 -0
- package/dist/shell/commandRegistry.js +133 -0
- package/dist/shell/consoleSink.d.ts +11 -0
- package/dist/shell/consoleSink.js +17 -0
- package/dist/shell/consoleStore.d.ts +27 -0
- package/dist/shell/consoleStore.js +43 -0
- package/dist/shell/fields.d.ts +39 -0
- package/dist/shell/fields.js +104 -0
- package/dist/shell/formatSavedRelative.d.ts +2 -0
- package/dist/shell/formatSavedRelative.js +14 -0
- package/dist/shell/glbThumbnailCache.d.ts +45 -0
- package/dist/shell/glbThumbnailCache.js +241 -0
- package/dist/shell/hierarchyA11y.d.ts +21 -0
- package/dist/shell/hierarchyA11y.js +35 -0
- package/dist/shell/icons.d.ts +13 -0
- package/dist/shell/icons.js +85 -0
- package/dist/shell/layoutStore.d.ts +68 -0
- package/dist/shell/layoutStore.js +156 -0
- package/dist/shell/pathFlythrough.d.ts +37 -0
- package/dist/shell/pathFlythrough.js +54 -0
- package/dist/shell/perfHistory.d.ts +70 -0
- package/dist/shell/perfHistory.js +130 -0
- package/dist/shell/renameObject.d.ts +3 -0
- package/dist/shell/renameObject.js +25 -0
- package/dist/shell/theme.d.ts +36 -0
- package/dist/shell/theme.js +37 -0
- package/dist/shell/ui.d.ts +84 -0
- package/dist/shell/ui.js +90 -0
- package/dist/shell/useGlbThumbnail.d.ts +8 -0
- package/dist/shell/useGlbThumbnail.js +19 -0
- package/dist/uiStore.d.ts +27 -0
- package/dist/uiStore.js +10 -0
- package/dist/viewportContextMenu.d.ts +4 -2
- package/dist/viewportContextMenu.js +5 -1
- package/package.json +4 -4
- package/skills/jgengine/SKILL.md +7 -4
- package/skills/jgengine/api.md +172 -79
- package/skills/jgengine/capabilities.md +104 -2
- package/skills/jgengine/recipes/minimal-game.md +88 -0
- package/skills/jgengine-editor/SKILL.md +18 -3
- package/skills/jgengine-editor/api.md +397 -18
- package/skills/jgengine-editor/capabilities.md +46 -0
- package/skills/jgengine-editor/reference.md +87 -0
- package/skills/level-design/SKILL.md +54 -0
- package/skills/level-design/agents/openai.yaml +4 -0
- package/skills/level-design/references/application-playbook.md +192 -0
- package/skills/level-design/references/field-guide.md +337 -0
- package/skills/level-design/references/live-editor-loop.md +179 -0
- package/dist/OutlinerPanel.d.ts +0 -14
- package/dist/OutlinerPanel.js +0 -84
- package/skills/jgengine/reference-cartridge.md +0 -72
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,32 @@ Agents building on the published SDK can also read this programmatically:
|
|
|
12
12
|
same data as typed values, so an updater can diff its installed version against
|
|
13
13
|
the latest and surface the migration steps.
|
|
14
14
|
|
|
15
|
+
## 0.12.0
|
|
16
|
+
|
|
17
|
+
### Migrate
|
|
18
|
+
|
|
19
|
+
- **Author games through `defineGame` from `@jgengine/shell`** — it is the single public authoring entry (and carries the define-game capability tag). Core's constructor was renamed `defineGame` → `defineGameDefinition` (`@jgengine/core/game/defineGame`); only hosts and tooling call it. If you imported `defineGame` from core, switch the import to `@jgengine/shell`. `GamePlayer`/`GamePlayerShell` are `@internal` now — `GameHost` is the documented mount, and it owns the editor summon (`?mode=editor`, F2+E, dev save endpoint), so delete any per-game editor bootstrap.
|
|
20
|
+
- **Declare your world with `world()`** (`@jgengine/core/world/place`): `world({ id, ground: { mode, size, surface? }, physics? })` with `flat` / `round` / `voxel` / `board` ground. New scaffolds emit a thin infinite flat place instead of the old 96 m meadow. `environment()` still works but is demoted to the legacy consumption path for editor-written scene data — **nothing required** for existing games; new worlds should use `world()` and author all dressing in the editor.
|
|
21
|
+
- Everything else is additive — stat pools, minimap marker sources, auction house, city/path generation, editor workspaces require no changes to existing games.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- **World = place: `world()` API** (#1178) — a world is substrate + laws, not a coded diorama. `@jgengine/core/world/place` ships `world({ id, ground: { mode, size, surface?, generator? }, physics? })`; size is discriminated by mode (flat `{x,z}` with `Infinity` axes, round `{radius}`, voxel generator domain, board `{x,y}` cells — TS and runtime both reject mismatches). `ground.surface` carries matter/feel laws (metal vs slime vs felt) read by the same rule systems; per-place `physics` resolves over the game default; multiple worlds per game are first-class. The shell renders place substrates (`PlaceScene`) under the engine default sky; seeds derive from world id + save via `seedForPlace`.
|
|
26
|
+
- **`@jgengine/shell/gameKit`** — happy-path authoring surface: authoring, mount, stores, systems, authored-scene helpers, HUD primitives, seeded rng in one import.
|
|
27
|
+
- **Portable generic stat pools** — `@jgengine/core/stats/statPool`: bounded named pools (health, energy, heat, ammo…) with the entity-stats integration rebuilt on them; recipe in skill `jgengine-combat`.
|
|
28
|
+
- **Auction-house economy primitives** (#1086) — `@jgengine/core/economy/auctionBook` (timed bid auctions, escrowed bids, outbid refunds, optional buyout, anti-snipe close extension, seller settlement minus house cut) plus market price history.
|
|
29
|
+
- **Portable minimap marker sources** — core world seam feeding game minimaps; recipe in skill `jgengine-ui`.
|
|
30
|
+
- **Unified seed-driven path network + city generation v2** (#1103, #1106) — streets/blocks/buildings with grid-ness, curviness, branching and block-size dials; zone bands, weighted building-class mixes, massing pieces, balance presets, junction-centered clustering, road-derived block polygons and parcels. Shell renders realistic roads: real intersections, curbs, lane lines, tunnels, and roads riding bridge decks.
|
|
31
|
+
- **Physical fidelity** — mesh-accurate hitboxes (shots pass through holes in concave models, #1075), auto-fit entity/object colliders from measured model bounds (#1072), movement obstruction reading fitted boxes + compound-box openings (#1077).
|
|
32
|
+
- **Editor overhaul** (#1110) — materials / lighting / scripting / animation-timeline / multiplayer workspaces, command palette, minimap bake wired end-to-end (#1036), real offscreen GLB thumbnails in the content browser, hierarchy drag-drop + keyboard + context menus + per-object visibility/lock, multi-select inspector, console RPC, asset drag-place, ortho projection, pivot modes, play chrome.
|
|
33
|
+
- **Editor authoring pipeline** — author entity definitions and starter catalogs (#1012), author catalogs/schema fields in the Data tab (#1043), promote an authored scene folder into a runnable game (#1011), authored triggers promoted to a shared primitive with goal/win rules (#1013), asset import rewrites the game's `game/assets.ts` (#1042).
|
|
34
|
+
- **Skills** — research-backed `game-design` and `level-design` skills (#962); every-game-owns-custom-UI invariant codified (#1148).
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- `npx jgengine create` scaffolds a thin infinite flat `world()` place; the editor `blankWorld` no longer pre-bakes seeded grass. Dressing belongs in `editor.scene.json`.
|
|
39
|
+
- Environment studio visual-quality pass — turf tufts, depth-aware water, relief soil, weathered poles; grass fields no longer rebuild geometry per frame (#1090).
|
|
40
|
+
|
|
15
41
|
## 0.11.0
|
|
16
42
|
|
|
17
43
|
### Migrate
|
package/README.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# @jgengine/editor
|
|
2
|
+
|
|
3
|
+
Browser-based 3D scene editor for [JGengine](https://github.com/Noisemaker111/jgengine) — author `editor.scene.json` (placement, terrain, paths, zones, foliage, triggers, catalogs) through a Blender/Unity-style GUI or the agent RPC bridge.
|
|
4
|
+
|
|
5
|
+
Usually reached through the CLI rather than imported directly:
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npx jgengine editor # open the editor on the current folder
|
|
9
|
+
npx jgengine editor ./world --assets ./models
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Inside a scaffolded game the editor is engine-summoned (`?mode=editor`, F2+E); the game consumes the authored document at runtime via `@jgengine/core` / `@jgengine/shell`. Agents drive it headlessly with `npx jgengine editor-mcp` (document RPC over localhost).
|
package/dist/AssetBrowser.d.ts
CHANGED
|
@@ -6,8 +6,17 @@ export interface EditorAssetEntry {
|
|
|
6
6
|
kind: "model" | "catalog" | "marker";
|
|
7
7
|
url?: string;
|
|
8
8
|
}
|
|
9
|
-
/** Custom drag mime carrying a material id — read by
|
|
9
|
+
/** Custom drag mime carrying a material id — read by hierarchy rows and the viewport drop zone. */
|
|
10
10
|
export declare const MATERIAL_DRAG_MIME = "application/x-jgengine-material";
|
|
11
|
+
/**
|
|
12
|
+
* Custom drag mime for placeable catalog assets. Payload is JSON
|
|
13
|
+
* `{ id, label, kind }` so the viewport can call `place_asset` without a registry round-trip.
|
|
14
|
+
*/
|
|
15
|
+
export declare const ASSET_DRAG_MIME = "application/x-jgengine-editor-asset";
|
|
16
|
+
/** Serializes a placeable asset for HTML5 drag into the viewport. @internal */
|
|
17
|
+
export declare function encodeAssetDragPayload(entry: EditorAssetEntry): string;
|
|
18
|
+
/** Parses an asset drag payload; returns null when the data is missing or malformed. @internal */
|
|
19
|
+
export declare function decodeAssetDragPayload(raw: string): EditorAssetEntry | null;
|
|
11
20
|
/** Searchable panel for placing catalog assets or an empty marker into the scene. */
|
|
12
21
|
export declare function AssetBrowser({ assets, session, onPlace, }: {
|
|
13
22
|
assets: readonly EditorAssetEntry[];
|
|
@@ -18,3 +27,19 @@ export declare function AssetBrowser({ assets, session, onPlace, }: {
|
|
|
18
27
|
export declare function assetsFromCatalog(ids: readonly string[], resolve?: (id: string) => {
|
|
19
28
|
url?: string;
|
|
20
29
|
} | null): EditorAssetEntry[];
|
|
30
|
+
/**
|
|
31
|
+
* Converts a durable/ephemeral standalone import into a Content Browser entry.
|
|
32
|
+
* Imported models are always kind `"model"` so place_asset stamps a catalogId for mesh resolution.
|
|
33
|
+
* @internal
|
|
34
|
+
*/
|
|
35
|
+
export declare function editorAssetFromImport(asset: {
|
|
36
|
+
id: string;
|
|
37
|
+
url: string;
|
|
38
|
+
label?: string;
|
|
39
|
+
}): EditorAssetEntry;
|
|
40
|
+
/**
|
|
41
|
+
* Merges imported model entries into the live browser list, replacing any prior entry that shares
|
|
42
|
+
* an id (re-import stays a single catalog row).
|
|
43
|
+
* @internal
|
|
44
|
+
*/
|
|
45
|
+
export declare function mergeEditorAssets(current: readonly EditorAssetEntry[], next: readonly EditorAssetEntry[]): EditorAssetEntry[];
|
package/dist/AssetBrowser.js
CHANGED
|
@@ -1,8 +1,35 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useMemo, useState } from "react";
|
|
3
3
|
import { TERRAIN_MATERIALS } from "./uiStore.js";
|
|
4
|
-
/** Custom drag mime carrying a material id — read by
|
|
4
|
+
/** Custom drag mime carrying a material id — read by hierarchy rows and the viewport drop zone. */
|
|
5
5
|
export const MATERIAL_DRAG_MIME = "application/x-jgengine-material";
|
|
6
|
+
/**
|
|
7
|
+
* Custom drag mime for placeable catalog assets. Payload is JSON
|
|
8
|
+
* `{ id, label, kind }` so the viewport can call `place_asset` without a registry round-trip.
|
|
9
|
+
*/
|
|
10
|
+
export const ASSET_DRAG_MIME = "application/x-jgengine-editor-asset";
|
|
11
|
+
/** Serializes a placeable asset for HTML5 drag into the viewport. @internal */
|
|
12
|
+
export function encodeAssetDragPayload(entry) {
|
|
13
|
+
return JSON.stringify({ id: entry.id, label: entry.label, kind: entry.kind });
|
|
14
|
+
}
|
|
15
|
+
/** Parses an asset drag payload; returns null when the data is missing or malformed. @internal */
|
|
16
|
+
export function decodeAssetDragPayload(raw) {
|
|
17
|
+
if (raw.length === 0)
|
|
18
|
+
return null;
|
|
19
|
+
try {
|
|
20
|
+
const parsed = JSON.parse(raw);
|
|
21
|
+
if (typeof parsed.id !== "string" || parsed.id.length === 0)
|
|
22
|
+
return null;
|
|
23
|
+
if (typeof parsed.label !== "string")
|
|
24
|
+
return null;
|
|
25
|
+
if (parsed.kind !== "model" && parsed.kind !== "catalog" && parsed.kind !== "marker")
|
|
26
|
+
return null;
|
|
27
|
+
return { id: parsed.id, label: parsed.label, kind: parsed.kind };
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
6
33
|
/** Materials palette: drag a chip onto an outliner row (assign to that object) or the viewport (paint terrain / assign to the object under the cursor). */
|
|
7
34
|
function MaterialsPalette() {
|
|
8
35
|
return (_jsx("div", { className: "flex flex-wrap gap-1 border-b border-white/[0.08] pb-2", children: TERRAIN_MATERIALS.map((material) => (_jsxs("div", { draggable: true, onDragStart: (event) => {
|
|
@@ -47,3 +74,29 @@ export function assetsFromCatalog(ids, resolve) {
|
|
|
47
74
|
};
|
|
48
75
|
});
|
|
49
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Converts a durable/ephemeral standalone import into a Content Browser entry.
|
|
79
|
+
* Imported models are always kind `"model"` so place_asset stamps a catalogId for mesh resolution.
|
|
80
|
+
* @internal
|
|
81
|
+
*/
|
|
82
|
+
export function editorAssetFromImport(asset) {
|
|
83
|
+
return {
|
|
84
|
+
id: asset.id,
|
|
85
|
+
label: asset.label ?? asset.id,
|
|
86
|
+
kind: "model",
|
|
87
|
+
url: asset.url,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Merges imported model entries into the live browser list, replacing any prior entry that shares
|
|
92
|
+
* an id (re-import stays a single catalog row).
|
|
93
|
+
* @internal
|
|
94
|
+
*/
|
|
95
|
+
export function mergeEditorAssets(current, next) {
|
|
96
|
+
if (next.length === 0)
|
|
97
|
+
return [...current];
|
|
98
|
+
const byId = new Map(current.map((asset) => [asset.id, asset]));
|
|
99
|
+
for (const asset of next)
|
|
100
|
+
byId.set(asset.id, asset);
|
|
101
|
+
return Array.from(byId.values());
|
|
102
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { EditorHostApi } from "./session.js";
|
|
2
|
+
/**
|
|
3
|
+
* Viewport drop target for content-browser assets. Drops call `place_asset` at the ground hit
|
|
4
|
+
* (or the host focus target when the ray misses). No-op when the payload is not an asset drag.
|
|
5
|
+
* @internal — mounted by `EditorApp`'s world overlay.
|
|
6
|
+
*/
|
|
7
|
+
export declare const AssetDropZone: import("react").MemoExoticComponent<({ api }: {
|
|
8
|
+
api: EditorHostApi;
|
|
9
|
+
}) => null>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { useThree } from "@react-three/fiber";
|
|
2
|
+
import { memo, useEffect } from "react";
|
|
3
|
+
import * as THREE from "three";
|
|
4
|
+
import { ASSET_DRAG_MIME, decodeAssetDragPayload } from "./AssetBrowser.js";
|
|
5
|
+
const GROUND_PLANE = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
|
|
6
|
+
/**
|
|
7
|
+
* Viewport drop target for content-browser assets. Drops call `place_asset` at the ground hit
|
|
8
|
+
* (or the host focus target when the ray misses). No-op when the payload is not an asset drag.
|
|
9
|
+
* @internal — mounted by `EditorApp`'s world overlay.
|
|
10
|
+
*/
|
|
11
|
+
export const AssetDropZone = memo(function AssetDropZone({ api }) {
|
|
12
|
+
const gl = useThree((state) => state.gl);
|
|
13
|
+
const camera = useThree((state) => state.camera);
|
|
14
|
+
const scene = useThree((state) => state.scene);
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const canvas = gl.domElement;
|
|
17
|
+
const raycaster = new THREE.Raycaster();
|
|
18
|
+
const ndc = new THREE.Vector2();
|
|
19
|
+
const planeHit = new THREE.Vector3();
|
|
20
|
+
const onDragOver = (event) => {
|
|
21
|
+
if (event.dataTransfer === null || !event.dataTransfer.types.includes(ASSET_DRAG_MIME))
|
|
22
|
+
return;
|
|
23
|
+
event.preventDefault();
|
|
24
|
+
event.dataTransfer.dropEffect = "copy";
|
|
25
|
+
};
|
|
26
|
+
const groundAt = (clientX, clientY) => {
|
|
27
|
+
const rect = canvas.getBoundingClientRect();
|
|
28
|
+
ndc.set(((clientX - rect.left) / rect.width) * 2 - 1, -((clientY - rect.top) / rect.height) * 2 + 1);
|
|
29
|
+
raycaster.setFromCamera(ndc, camera);
|
|
30
|
+
for (const hit of raycaster.intersectObjects(scene.children, true)) {
|
|
31
|
+
if (!hit.object.isMesh || hit.object.visible === false)
|
|
32
|
+
continue;
|
|
33
|
+
if (hit.object.type.startsWith("TransformControls"))
|
|
34
|
+
continue;
|
|
35
|
+
return { x: hit.point.x, y: hit.point.y, z: hit.point.z };
|
|
36
|
+
}
|
|
37
|
+
if (raycaster.ray.intersectPlane(GROUND_PLANE, planeHit) !== null) {
|
|
38
|
+
return { x: planeHit.x, y: planeHit.y, z: planeHit.z };
|
|
39
|
+
}
|
|
40
|
+
return api.getFocusTarget() ?? { x: 0, y: 0, z: 0 };
|
|
41
|
+
};
|
|
42
|
+
const onDrop = (event) => {
|
|
43
|
+
const entry = decodeAssetDragPayload(event.dataTransfer?.getData(ASSET_DRAG_MIME) ?? "");
|
|
44
|
+
if (entry === null)
|
|
45
|
+
return;
|
|
46
|
+
event.preventDefault();
|
|
47
|
+
const position = groundAt(event.clientX, event.clientY);
|
|
48
|
+
api.handle({
|
|
49
|
+
method: "place_asset",
|
|
50
|
+
id: entry.id,
|
|
51
|
+
kind: entry.kind === "model" ? "prop" : entry.kind,
|
|
52
|
+
x: position.x,
|
|
53
|
+
y: position.y,
|
|
54
|
+
z: position.z,
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
canvas.addEventListener("dragover", onDragOver);
|
|
58
|
+
canvas.addEventListener("drop", onDrop);
|
|
59
|
+
return () => {
|
|
60
|
+
canvas.removeEventListener("dragover", onDragOver);
|
|
61
|
+
canvas.removeEventListener("drop", onDrop);
|
|
62
|
+
};
|
|
63
|
+
}, [gl, camera, scene, api]);
|
|
64
|
+
return null;
|
|
65
|
+
});
|
package/dist/CatalogsPanel.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { EditorCatalogDefinition, EditorSession } from "@jgengine/core/editor/index";
|
|
2
2
|
/**
|
|
3
|
-
* Gameplay data catalogs panel — lists game-exported
|
|
4
|
-
*
|
|
3
|
+
* Gameplay data catalogs panel — lists merged (game-exported + document-authored) catalogs, lets an
|
|
4
|
+
* author create a brand-new document catalog, author its schema fields, and edit rows via
|
|
5
|
+
* SchemaInspector. Row values and (for document catalogs) the schema itself persist on
|
|
6
|
+
* `document.catalogs` and undo with the same coalesce keys as meta patches.
|
|
5
7
|
* @internal — mounted by `EditorChrome` as a left-aside tab.
|
|
6
8
|
*/
|
|
7
9
|
export declare function CatalogsPanel({ session, definitions, }: {
|
package/dist/CatalogsPanel.js
CHANGED
|
@@ -1,36 +1,189 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useMemo, useState } from "react";
|
|
3
3
|
import { findEditorCatalog, findEditorCatalogEntry } from "@jgengine/core/editor/index";
|
|
4
|
+
import { parseParams } from "@jgengine/core/scene/sceneKinds";
|
|
4
5
|
import { SchemaInspector } from "./SchemaInspector.js";
|
|
6
|
+
import { BORDER, CONTROL, FOCUS_RING, INPUT_CLS, MICRO_LABEL } from "./shell/theme.js";
|
|
5
7
|
import { useStoreSelector } from "./useStoreSelector.js";
|
|
8
|
+
function slugifyEntryId(value) {
|
|
9
|
+
return value
|
|
10
|
+
.trim()
|
|
11
|
+
.toLowerCase()
|
|
12
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
13
|
+
.replace(/^_+|_+$/g, "");
|
|
14
|
+
}
|
|
15
|
+
/** The field types the document schema editor can author. */
|
|
16
|
+
const FIELD_TYPES = [
|
|
17
|
+
"range",
|
|
18
|
+
"number",
|
|
19
|
+
"bool",
|
|
20
|
+
"select",
|
|
21
|
+
"color",
|
|
22
|
+
"text",
|
|
23
|
+
"seed",
|
|
24
|
+
"weightedList",
|
|
25
|
+
"action",
|
|
26
|
+
];
|
|
27
|
+
/**
|
|
28
|
+
* Rebuilds a valid {@link ParamField} for a new `type`, preserving `key`/`label` and reusing prior
|
|
29
|
+
* numeric bounds/default where the target type still carries them. Every type gets a schema-valid
|
|
30
|
+
* default so `parseParams` (which drives the reclamp on `setCatalogSchema`) always has a value to read.
|
|
31
|
+
*/
|
|
32
|
+
function coerceField(prev, type) {
|
|
33
|
+
const key = prev.key;
|
|
34
|
+
const label = prev.label;
|
|
35
|
+
const prevNumber = "default" in prev && typeof prev.default === "number" ? prev.default : 0;
|
|
36
|
+
const prevMin = "min" in prev && typeof prev.min === "number" ? prev.min : undefined;
|
|
37
|
+
const prevMax = "max" in prev && typeof prev.max === "number" ? prev.max : undefined;
|
|
38
|
+
const prevString = "default" in prev && typeof prev.default === "string" ? prev.default : "";
|
|
39
|
+
switch (type) {
|
|
40
|
+
case "range":
|
|
41
|
+
return { key, label, type: "range", min: prevMin ?? 0, max: prevMax ?? 10, default: prevNumber };
|
|
42
|
+
case "number":
|
|
43
|
+
return { key, label, type: "number", default: prevNumber, ...(prevMin === undefined ? {} : { min: prevMin }), ...(prevMax === undefined ? {} : { max: prevMax }) };
|
|
44
|
+
case "bool":
|
|
45
|
+
return { key, label, type: "bool", default: false };
|
|
46
|
+
case "select":
|
|
47
|
+
return { key, label, type: "select", options: [], default: prevString };
|
|
48
|
+
case "color":
|
|
49
|
+
return { key, label, type: "color", default: prevString || "#888888" };
|
|
50
|
+
case "text":
|
|
51
|
+
return { key, label, type: "text", default: prevString };
|
|
52
|
+
case "seed":
|
|
53
|
+
return { key, label, type: "seed", default: prevString };
|
|
54
|
+
case "weightedList":
|
|
55
|
+
return { key, label, type: "weightedList", default: [] };
|
|
56
|
+
case "action":
|
|
57
|
+
return { key, label, type: "action", action: "randomize" };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const INPUT = `min-w-0 px-2 py-1 text-[11px] ${INPUT_CLS}`;
|
|
61
|
+
const BTN = `${CONTROL} px-2 py-1 text-[11px] font-medium disabled:opacity-40`;
|
|
6
62
|
/**
|
|
7
|
-
* Gameplay data catalogs panel — lists game-exported
|
|
8
|
-
*
|
|
63
|
+
* Gameplay data catalogs panel — lists merged (game-exported + document-authored) catalogs, lets an
|
|
64
|
+
* author create a brand-new document catalog, author its schema fields, and edit rows via
|
|
65
|
+
* SchemaInspector. Row values and (for document catalogs) the schema itself persist on
|
|
66
|
+
* `document.catalogs` and undo with the same coalesce keys as meta patches.
|
|
9
67
|
* @internal — mounted by `EditorChrome` as a left-aside tab.
|
|
10
68
|
*/
|
|
11
69
|
export function CatalogsPanel({ session, definitions, }) {
|
|
12
70
|
const document = useStoreSelector(session, (state) => state.document);
|
|
13
71
|
const [catalogId, setCatalogId] = useState(definitions[0]?.id ?? "");
|
|
14
72
|
const [entryId, setEntryId] = useState(definitions[0]?.entries[0]?.id ?? "");
|
|
73
|
+
const [newRowId, setNewRowId] = useState("");
|
|
74
|
+
const [newCatalogId, setNewCatalogId] = useState("");
|
|
75
|
+
const [newCatalogLabel, setNewCatalogLabel] = useState("");
|
|
15
76
|
const definition = useMemo(() => definitions.find((catalog) => catalog.id === catalogId) ?? definitions[0], [definitions, catalogId]);
|
|
77
|
+
const addCatalog = () => {
|
|
78
|
+
const id = slugifyEntryId(newCatalogId);
|
|
79
|
+
if (id.length === 0)
|
|
80
|
+
return;
|
|
81
|
+
const label = newCatalogLabel.trim();
|
|
82
|
+
session.dispatch({
|
|
83
|
+
type: "addCatalog",
|
|
84
|
+
id,
|
|
85
|
+
...(label.length === 0 ? {} : { label }),
|
|
86
|
+
schema: { fields: [] },
|
|
87
|
+
});
|
|
88
|
+
setNewCatalogId("");
|
|
89
|
+
setNewCatalogLabel("");
|
|
90
|
+
setCatalogId(id);
|
|
91
|
+
setEntryId("");
|
|
92
|
+
};
|
|
93
|
+
const newCatalogForm = (_jsxs("form", { className: `flex flex-col gap-1 rounded-[6px] border ${BORDER} bg-white/[0.02] p-2`, onSubmit: (event) => {
|
|
94
|
+
event.preventDefault();
|
|
95
|
+
addCatalog();
|
|
96
|
+
}, children: [_jsx("span", { className: MICRO_LABEL, children: "New catalog" }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx("input", { className: `flex-1 ${INPUT}`, value: newCatalogId, placeholder: "catalog id (e.g. towers)", onChange: (event) => setNewCatalogId(event.target.value) }), _jsx("input", { className: `flex-1 ${INPUT}`, value: newCatalogLabel, placeholder: "label (optional)", onChange: (event) => setNewCatalogLabel(event.target.value) }), _jsx("button", { type: "submit", disabled: slugifyEntryId(newCatalogId).length === 0, className: BTN, children: "\uFF0B New catalog" })] })] }));
|
|
16
97
|
if (definitions.length === 0) {
|
|
17
|
-
return (
|
|
98
|
+
return (_jsxs("div", { className: "flex min-h-0 flex-1 flex-col gap-2 p-2", children: [_jsx("div", { className: "text-[10px] text-neutral-500", children: "No gameplay catalogs yet. Create one below, or export `editorCatalogs` from the game (ParamSchema + default entries)." }), newCatalogForm] }));
|
|
18
99
|
}
|
|
19
100
|
const activeDefinition = definition ?? definitions[0];
|
|
20
101
|
const data = findEditorCatalog(document, activeDefinition.id);
|
|
21
102
|
const entries = data?.entries ?? activeDefinition.entries;
|
|
22
103
|
const activeEntryId = entries.some((entry) => entry.id === entryId) ? entryId : entries[0]?.id ?? "";
|
|
23
104
|
const entry = findEditorCatalogEntry(document, activeDefinition.id, activeEntryId);
|
|
24
|
-
|
|
105
|
+
// A document-authored catalog carries its own schema — only those are schema-editable here.
|
|
106
|
+
// Game-exported catalogs keep their schema in code and stay read-only.
|
|
107
|
+
const documentSchema = data?.schema;
|
|
108
|
+
const addRow = () => {
|
|
109
|
+
const id = slugifyEntryId(newRowId);
|
|
110
|
+
if (id.length === 0)
|
|
111
|
+
return;
|
|
112
|
+
// Seed the new row from the schema defaults so it is immediately editable and schema-valid.
|
|
113
|
+
const meta = parseParams(activeDefinition.schema, undefined);
|
|
114
|
+
session.dispatch({ type: "addCatalogEntry", catalogId: activeDefinition.id, entry: { id, meta } });
|
|
115
|
+
setNewRowId("");
|
|
116
|
+
setEntryId(id);
|
|
117
|
+
};
|
|
118
|
+
const removeRow = (id) => {
|
|
119
|
+
session.dispatch({ type: "removeCatalogEntry", catalogId: activeDefinition.id, entryId: id });
|
|
120
|
+
if (entryId === id)
|
|
121
|
+
setEntryId("");
|
|
122
|
+
};
|
|
123
|
+
// Rebuilds the whole `fields` array and dispatches ONE setCatalogSchema; the reducer reclamps every
|
|
124
|
+
// row so the row list + SchemaInspector re-render against clamped values.
|
|
125
|
+
const commitFields = (fields) => {
|
|
126
|
+
if (documentSchema === undefined)
|
|
127
|
+
return;
|
|
128
|
+
const schema = { fields, ...(documentSchema.groups === undefined ? {} : { groups: documentSchema.groups }) };
|
|
129
|
+
session.dispatch({ type: "setCatalogSchema", id: activeDefinition.id, schema });
|
|
130
|
+
};
|
|
131
|
+
const schemaFields = documentSchema?.fields ?? [];
|
|
132
|
+
const patchField = (index, next) => {
|
|
133
|
+
commitFields(schemaFields.map((field, i) => (i === index ? next : field)));
|
|
134
|
+
};
|
|
135
|
+
const addField = () => {
|
|
136
|
+
let key = "field";
|
|
137
|
+
let n = 2;
|
|
138
|
+
const taken = new Set(schemaFields.map((field) => field.key));
|
|
139
|
+
while (taken.has(key)) {
|
|
140
|
+
key = `field_${n}`;
|
|
141
|
+
n += 1;
|
|
142
|
+
}
|
|
143
|
+
commitFields([...schemaFields, { key, type: "number", default: 0 }]);
|
|
144
|
+
};
|
|
145
|
+
const removeField = (index) => {
|
|
146
|
+
commitFields(schemaFields.filter((_, i) => i !== index));
|
|
147
|
+
};
|
|
148
|
+
return (_jsxs("div", { className: "flex min-h-0 flex-1 flex-col gap-2 p-2", children: [newCatalogForm, _jsxs("label", { className: "flex items-center gap-2 text-[10px] text-neutral-400", children: [_jsx("span", { className: "shrink-0 uppercase tracking-wider text-neutral-500", children: "Catalog" }), _jsx("select", { className: `min-w-0 flex-1 px-2 py-1 ${INPUT_CLS}`, value: activeDefinition.id, onChange: (event) => {
|
|
25
149
|
const next = definitions.find((catalog) => catalog.id === event.target.value);
|
|
26
150
|
setCatalogId(event.target.value);
|
|
27
151
|
setEntryId(next?.entries[0]?.id ?? "");
|
|
28
|
-
}, children: definitions.map((catalog) => (_jsx("option", { value: catalog.id, children: catalog.label }, catalog.id))) })
|
|
152
|
+
}, children: definitions.map((catalog) => (_jsx("option", { value: catalog.id, children: catalog.label }, catalog.id))) }), documentSchema !== undefined ? (_jsx("button", { type: "button", className: `shrink-0 rounded-[5px] px-1.5 py-1 text-[11px] text-neutral-500 transition-colors hover:bg-rose-500/20 hover:text-rose-200 ${FOCUS_RING}`, title: `Remove catalog ${activeDefinition.id}`, "aria-label": `Remove catalog ${activeDefinition.id}`, onClick: () => {
|
|
153
|
+
session.dispatch({ type: "removeCatalog", id: activeDefinition.id });
|
|
154
|
+
setCatalogId("");
|
|
155
|
+
setEntryId("");
|
|
156
|
+
}, children: "\u00D7" })) : null] }), documentSchema !== undefined ? (_jsxs("div", { className: `flex flex-col gap-1 rounded-[6px] border ${BORDER} bg-white/[0.02] p-2`, children: [_jsx("span", { className: "text-[9px] font-semibold uppercase tracking-wider text-neutral-500", children: "Schema fields" }), schemaFields.map((field, index) => {
|
|
157
|
+
const hasBounds = field.type === "range" || field.type === "number";
|
|
158
|
+
const numberDefault = "default" in field && typeof field.default === "number" ? field.default : undefined;
|
|
159
|
+
const min = "min" in field && typeof field.min === "number" ? field.min : undefined;
|
|
160
|
+
const max = "max" in field && typeof field.max === "number" ? field.max : undefined;
|
|
161
|
+
return (_jsxs("div", { className: "flex flex-wrap items-center gap-1", children: [_jsx("input", { className: `w-24 ${INPUT}`, value: field.key, placeholder: "key", onChange: (event) => patchField(index, { ...field, key: event.target.value }) }), _jsx("select", { className: `w-24 ${INPUT}`, value: field.type, onChange: (event) => patchField(index, coerceField(field, event.target.value)), children: FIELD_TYPES.map((type) => (_jsx("option", { value: type, children: type }, type))) }), numberDefault !== undefined ? (_jsx("input", { type: "number", className: `w-16 ${INPUT}`, value: numberDefault, title: "default", onChange: (event) => patchField(index, { ...field, default: Number(event.target.value) }) })) : field.type === "bool" ? (_jsx("input", { type: "checkbox", className: "accent-emerald-400", checked: field.default, title: "default", onChange: (event) => patchField(index, { ...field, default: event.target.checked }) })) : "default" in field && typeof field.default === "string" ? (_jsx("input", { className: `w-16 ${INPUT}`, value: field.default, title: "default", onChange: (event) => patchField(index, { ...field, default: event.target.value }) })) : null, hasBounds ? (_jsxs(_Fragment, { children: [_jsx("input", { type: "number", className: `w-14 ${INPUT}`, value: min ?? "", placeholder: "min", title: "min", onChange: (event) => {
|
|
162
|
+
const value = event.target.value;
|
|
163
|
+
const next = { ...field };
|
|
164
|
+
if (value === "")
|
|
165
|
+
delete next.min;
|
|
166
|
+
else
|
|
167
|
+
next.min = Number(value);
|
|
168
|
+
patchField(index, next);
|
|
169
|
+
} }), _jsx("input", { type: "number", className: `w-14 ${INPUT}`, value: max ?? "", placeholder: "max", title: "max", onChange: (event) => {
|
|
170
|
+
const value = event.target.value;
|
|
171
|
+
const next = { ...field };
|
|
172
|
+
if (value === "")
|
|
173
|
+
delete next.max;
|
|
174
|
+
else
|
|
175
|
+
next.max = Number(value);
|
|
176
|
+
patchField(index, next);
|
|
177
|
+
} })] })) : null, _jsx("button", { type: "button", className: `shrink-0 rounded-[5px] px-1.5 py-1 text-[11px] text-neutral-500 transition-colors hover:bg-rose-500/20 hover:text-rose-200 ${FOCUS_RING}`, title: `Remove field ${field.key}`, "aria-label": `Remove field ${field.key}`, onClick: () => removeField(index), children: "\u00D7" })] }, index));
|
|
178
|
+
}), _jsx("button", { type: "button", className: BTN, onClick: addField, children: "+ Field" })] })) : null, _jsx("div", { className: "min-h-0 flex-1 space-y-1 overflow-auto pr-1", children: entries.map((row) => {
|
|
29
179
|
const selected = row.id === activeEntryId;
|
|
30
|
-
return (_jsxs("
|
|
180
|
+
return (_jsxs("div", { className: `flex items-center gap-1 rounded-md ring-1 ring-inset transition-colors ${selected
|
|
31
181
|
? "bg-amber-500/15 text-amber-100 ring-amber-400/30"
|
|
32
|
-
: "bg-white/[0.03] text-neutral-300 ring-white/[0.06] hover:bg-white/[0.06]"}`, onClick: () => setEntryId(row.id), children: [_jsx("span", { className: "truncate font-medium", children: row.label ?? row.id }), _jsx("span", { className: "ml-2 shrink-0 text-[9px] text-neutral-500", children: row.id })] }, row.id));
|
|
33
|
-
}) }),
|
|
182
|
+
: "bg-white/[0.03] text-neutral-300 ring-white/[0.06] hover:bg-white/[0.06]"}`, children: [_jsxs("button", { type: "button", className: "flex min-w-0 flex-1 items-center justify-between px-2 py-1.5 text-left text-[11px]", onClick: () => setEntryId(row.id), children: [_jsx("span", { className: "truncate font-medium", children: row.label ?? row.id }), _jsx("span", { className: "ml-2 shrink-0 text-[9px] text-neutral-500", children: row.id })] }), _jsx("button", { type: "button", className: `shrink-0 rounded-[5px] px-1.5 py-1 text-[11px] text-neutral-500 transition-colors hover:bg-rose-500/20 hover:text-rose-200 ${FOCUS_RING}`, title: `Remove ${row.id}`, "aria-label": `Remove ${row.id}`, onClick: () => removeRow(row.id), children: "\u00D7" })] }, row.id));
|
|
183
|
+
}) }), _jsxs("form", { className: "flex items-center gap-1", onSubmit: (event) => {
|
|
184
|
+
event.preventDefault();
|
|
185
|
+
addRow();
|
|
186
|
+
}, children: [_jsx("input", { className: `min-w-0 flex-1 px-2 py-1 text-[11px] ${INPUT_CLS}`, value: newRowId, placeholder: "new row id (e.g. goblin)", onChange: (event) => setNewRowId(event.target.value) }), _jsx("button", { type: "submit", disabled: slugifyEntryId(newRowId).length === 0, className: BTN, children: "+ Row" })] }), entry !== undefined ? (_jsx(SchemaInspector, { schema: activeDefinition.schema, label: entry.label ?? entry.id, accent: "#fbbf24", meta: entry.meta, onMeta: (patch, coalesce) => {
|
|
34
187
|
session.dispatch({
|
|
35
188
|
type: "setCatalogEntry",
|
|
36
189
|
catalogId: activeDefinition.id,
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { EditorHostApi } from "./session.js";
|
|
2
|
+
/**
|
|
3
|
+
* Live edit-mode preview of every `city` district volume, so slider drags (grid-ness, curviness,
|
|
4
|
+
* branching, block size, …) re-synthesize streets/buildings/parks in the viewport without entering
|
|
5
|
+
* play mode. Mounts the exact runtime renderer the shell registers for the kind — the preview IS the
|
|
6
|
+
* game's rendering, grounded on the sculpt terrain (or the game's ground) like {@link ScatterPreview}.
|
|
7
|
+
* @internal — mounted by `EditorApp`; not a game-author entry point.
|
|
8
|
+
*/
|
|
9
|
+
export declare const CityPreview: import("react").MemoExoticComponent<({ api }: {
|
|
10
|
+
api: EditorHostApi;
|
|
11
|
+
}) => import("react").JSX.Element | null>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { memo, useMemo } from "react";
|
|
3
|
+
import { CITY_KIND } from "@jgengine/core/world/cityKind";
|
|
4
|
+
import { editableTerrainFromSnapshot } from "@jgengine/core/world/terraform";
|
|
5
|
+
import { useGameContext } from "@jgengine/react/provider";
|
|
6
|
+
import { registerBuiltinSceneKindRenderers } from "@jgengine/shell/scene/builtinSceneKindRenderers";
|
|
7
|
+
import { getSceneKindRenderer } from "@jgengine/shell/scene/sceneKindRenderers";
|
|
8
|
+
import { useStoreSelector } from "./useStoreSelector.js";
|
|
9
|
+
/**
|
|
10
|
+
* Live edit-mode preview of every `city` district volume, so slider drags (grid-ness, curviness,
|
|
11
|
+
* branching, block size, …) re-synthesize streets/buildings/parks in the viewport without entering
|
|
12
|
+
* play mode. Mounts the exact runtime renderer the shell registers for the kind — the preview IS the
|
|
13
|
+
* game's rendering, grounded on the sculpt terrain (or the game's ground) like {@link ScatterPreview}.
|
|
14
|
+
* @internal — mounted by `EditorApp`; not a game-author entry point.
|
|
15
|
+
*/
|
|
16
|
+
export const CityPreview = memo(function CityPreview({ api }) {
|
|
17
|
+
registerBuiltinSceneKindRenderers();
|
|
18
|
+
const ctx = useGameContext();
|
|
19
|
+
const session = api.getSession();
|
|
20
|
+
const volumes = useStoreSelector(session, (state) => state.document.volumes);
|
|
21
|
+
const snapshot = useStoreSelector(session, (state) => state.document.terrain ?? null);
|
|
22
|
+
const baseGround = ctx.world.ground;
|
|
23
|
+
const field = useMemo(() => {
|
|
24
|
+
if (snapshot !== null) {
|
|
25
|
+
const editable = editableTerrainFromSnapshot(snapshot, baseGround);
|
|
26
|
+
return { ...baseGround, sampleHeight: editable.sampleHeight, sampleNormal: editable.sampleNormal };
|
|
27
|
+
}
|
|
28
|
+
return baseGround;
|
|
29
|
+
}, [snapshot, baseGround]);
|
|
30
|
+
const objects = useMemo(() => volumes
|
|
31
|
+
.filter((volume) => volume.kind === CITY_KIND)
|
|
32
|
+
.map((volume) => ({
|
|
33
|
+
id: volume.id,
|
|
34
|
+
kind: volume.kind,
|
|
35
|
+
center: volume.center,
|
|
36
|
+
...(volume.halfExtents === undefined ? {} : { halfExtents: volume.halfExtents }),
|
|
37
|
+
...(volume.radius === undefined ? {} : { radius: volume.radius }),
|
|
38
|
+
...(volume.meta === undefined ? {} : { meta: volume.meta }),
|
|
39
|
+
})), [volumes]);
|
|
40
|
+
const renderer = getSceneKindRenderer(CITY_KIND);
|
|
41
|
+
const document = useStoreSelector(session, (state) => state.document);
|
|
42
|
+
if (renderer === undefined || objects.length === 0)
|
|
43
|
+
return null;
|
|
44
|
+
return _jsx(_Fragment, { children: renderer({ objects, context: { document, field } }) });
|
|
45
|
+
});
|
package/dist/CollectionsPanel.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useState } from "react";
|
|
3
|
+
import { CONTROL, FOCUS_RING, INPUT_CLS, MICRO_LABEL } from "./shell/theme.js";
|
|
3
4
|
import { shallowArrayEqual, useStoreSelector } from "./useStoreSelector.js";
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
const CHIP_BTN = "rounded-md px-1.5 py-0.5 text-[10px] ring-1 ring-inset transition-colors";
|
|
5
|
+
const BTN = `${CONTROL} px-2 py-1 text-[11px] disabled:opacity-40`;
|
|
6
|
+
const CHIP_BTN = `rounded-[4px] px-1.5 py-0.5 text-[10px] ring-1 ring-inset transition-colors ${FOCUS_RING}`;
|
|
7
7
|
function newCollectionId() {
|
|
8
8
|
return `col_${Date.now().toString(36)}_${Math.floor(Math.random() * 1296).toString(36)}`;
|
|
9
9
|
}
|
|
@@ -25,11 +25,11 @@ export function CollectionsPanel({ session }) {
|
|
|
25
25
|
session.dispatch({ type: "createCollection", id: newCollectionId(), name: trimmed, memberIds: selection });
|
|
26
26
|
setName("");
|
|
27
27
|
};
|
|
28
|
-
return (_jsxs("div", { className: "flex min-h-0 flex-1 flex-col gap-2 p-2", children: [_jsxs("div", { className: "flex gap-1", children: [_jsx("input", { className: `min-w-0 flex-1 ${
|
|
28
|
+
return (_jsxs("div", { className: "flex min-h-0 flex-1 flex-col gap-2 p-2", children: [_jsx("div", { className: MICRO_LABEL, children: "Collections" }), _jsxs("div", { className: "flex gap-1", children: [_jsx("input", { className: `h-7 min-w-0 flex-1 px-2 ${INPUT_CLS}`, placeholder: "New collection name\u2026", value: name, onChange: (event) => setName(event.target.value), onKeyDown: (event) => {
|
|
29
29
|
if (event.key === "Enter")
|
|
30
30
|
createFromSelection();
|
|
31
31
|
} }), _jsx("button", { type: "button", className: BTN, disabled: selection.length === 0 || name.trim().length === 0, onClick: createFromSelection, children: "From selection" })] }), selection.length === 0 ? (_jsx("div", { className: "text-[10px] text-neutral-500", children: "Select objects first, then name and save a set." })) : null, _jsx("div", { className: "min-h-0 flex-1 space-y-1 overflow-auto pr-1", children: collections.length === 0 ? (_jsx("div", { className: "text-neutral-600", children: "No collections yet." })) : (collections.map((collection) => (_jsx(CollectionRow, { session: session, collection: collection, selection: selection }, collection.id)))) })] }));
|
|
32
32
|
}
|
|
33
33
|
function CollectionRow({ session, collection, selection, }) {
|
|
34
|
-
return (_jsxs("div", { className: "space-y-1 rounded-
|
|
34
|
+
return (_jsxs("div", { className: "space-y-1.5 rounded-[6px] border border-white/[0.07] bg-white/[0.02] p-2", children: [_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("span", { className: "h-2.5 w-2.5 shrink-0 rounded-full ring-1 ring-inset ring-white/20", style: { backgroundColor: collection.color ?? "#64748b" } }), _jsx("button", { type: "button", className: `min-w-0 flex-1 truncate text-left text-[12px] text-neutral-200 hover:text-cyan-200 ${FOCUS_RING}`, title: "Restore selection", onClick: () => session.dispatch({ type: "selectCollection", id: collection.id }), children: collection.name }), _jsx("span", { className: "text-[10px] tabular-nums text-neutral-500", children: collection.memberIds.length })] }), _jsxs("div", { className: "flex flex-wrap gap-1", children: [_jsx("button", { type: "button", className: `${CHIP_BTN} ${collection.locked === true ? "bg-amber-500/20 text-amber-200 ring-amber-400/30" : "bg-black/30 text-neutral-400 ring-white/[0.06] hover:text-neutral-200"}`, onClick: () => session.dispatch({ type: "setCollectionFlags", id: collection.id, patch: { locked: collection.locked !== true } }), children: collection.locked === true ? "🔒 locked" : "🔓 lock" }), _jsx("button", { type: "button", className: `${CHIP_BTN} ${collection.visible === false ? "bg-black/30 text-neutral-500 ring-white/[0.06]" : "bg-cyan-500/15 text-cyan-200 ring-cyan-400/25"}`, onClick: () => session.dispatch({ type: "setCollectionFlags", id: collection.id, patch: { visible: collection.visible === false } }), children: collection.visible === false ? "hidden" : "visible" }), _jsx("label", { className: "flex items-center gap-1 rounded-md bg-black/30 px-1.5 py-0.5 ring-1 ring-inset ring-white/[0.06]", children: _jsx("input", { type: "color", className: "h-3 w-4 cursor-pointer border-none bg-transparent p-0", value: collection.color ?? "#64748b", onChange: (event) => session.dispatch({ type: "setCollectionFlags", id: collection.id, patch: { color: event.target.value } }) }) }), _jsx("button", { type: "button", className: `${CHIP_BTN} bg-black/30 text-neutral-400 ring-white/[0.06] hover:text-neutral-200 disabled:opacity-30`, disabled: selection.length === 0, onClick: () => session.dispatch({ type: "addToCollection", id: collection.id, ids: selection }), children: "+ add selected" }), _jsx("button", { type: "button", className: `${CHIP_BTN} bg-black/30 text-neutral-400 ring-white/[0.06] hover:text-neutral-200 disabled:opacity-30`, disabled: selection.length === 0, onClick: () => session.dispatch({ type: "removeFromCollection", id: collection.id, ids: selection }), children: "\u2212 remove selected" }), _jsx("button", { type: "button", className: `${CHIP_BTN} ml-auto bg-rose-500/15 text-rose-200 ring-rose-400/25 hover:bg-rose-500/25`, onClick: () => session.dispatch({ type: "deleteCollection", id: collection.id }), children: "Delete" })] })] }));
|
|
35
35
|
}
|
package/dist/DebugDraw.d.ts
CHANGED
|
@@ -4,10 +4,12 @@ export declare function PathDraftPreview({ points }: {
|
|
|
4
4
|
points: readonly EditorVec3[];
|
|
5
5
|
}): import("react").JSX.Element;
|
|
6
6
|
/** Renders every visible marker, volume, path, and note from a document as in-scene 3D gizmos. */
|
|
7
|
-
export declare function EditorLayerOverlays({ document, visibility, selection, onSelect, activePathPoint, groundHeightAt, }: {
|
|
7
|
+
export declare function EditorLayerOverlays({ document, visibility, selection, hoverId, onSelect, activePathPoint, groundHeightAt, }: {
|
|
8
8
|
document: EditorDocument;
|
|
9
9
|
visibility: EditorKindVisibility;
|
|
10
10
|
selection: readonly string[];
|
|
11
|
+
/** Pre-selection hover id from viewport pointer pick; ignored when already selected. */
|
|
12
|
+
hoverId?: string | null;
|
|
11
13
|
onSelect: (id: string) => void;
|
|
12
14
|
activePathPoint?: {
|
|
13
15
|
pathId: string;
|