@jgengine/editor 0.10.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 +325 -0
- package/dist/AssetBrowser.d.ts +20 -0
- package/dist/AssetBrowser.js +49 -0
- package/dist/CatalogsPanel.d.ts +10 -0
- package/dist/CatalogsPanel.js +41 -0
- package/dist/CollectionsPanel.d.ts +10 -0
- package/dist/CollectionsPanel.js +35 -0
- package/dist/DebugDraw.d.ts +17 -0
- package/dist/DebugDraw.js +233 -0
- package/dist/EditorApp.d.ts +26 -0
- package/dist/EditorApp.js +333 -0
- package/dist/EditorCameraDriver.d.ts +5 -0
- package/dist/EditorCameraDriver.js +18 -0
- package/dist/EditorChrome.d.ts +22 -0
- package/dist/EditorChrome.js +559 -0
- package/dist/EditorContextMenu.d.ts +11 -0
- package/dist/EditorContextMenu.js +12 -0
- package/dist/InspectorPanel.d.ts +14 -0
- package/dist/InspectorPanel.js +110 -0
- package/dist/MaterialDropZone.d.ts +10 -0
- package/dist/MaterialDropZone.js +77 -0
- package/dist/OutlinerPanel.d.ts +14 -0
- package/dist/OutlinerPanel.js +84 -0
- package/dist/PerfProbe.d.ts +5 -0
- package/dist/PerfProbe.js +34 -0
- package/dist/PrefabsPanel.d.ts +13 -0
- package/dist/PrefabsPanel.js +42 -0
- package/dist/RuntimePlayBridge.d.ts +18 -0
- package/dist/RuntimePlayBridge.js +140 -0
- package/dist/ScatterPreview.d.ts +11 -0
- package/dist/ScatterPreview.js +30 -0
- package/dist/SchemaInspector.d.ts +19 -0
- package/dist/SchemaInspector.js +78 -0
- package/dist/SelectionGizmo.d.ts +25 -0
- package/dist/SelectionGizmo.js +455 -0
- package/dist/StandaloneEditor.d.ts +44 -0
- package/dist/StandaloneEditor.js +92 -0
- package/dist/TerrainPanel.d.ts +7 -0
- package/dist/TerrainPanel.js +59 -0
- package/dist/TerrainSculpt.d.ts +14 -0
- package/dist/TerrainSculpt.js +415 -0
- package/dist/TriggerInspector.d.ts +13 -0
- package/dist/TriggerInspector.js +60 -0
- package/dist/agent/AgentPanel.d.ts +13 -0
- package/dist/agent/AgentPanel.js +156 -0
- package/dist/agent/context.d.ts +27 -0
- package/dist/agent/context.js +39 -0
- package/dist/agent/endpoint.d.ts +68 -0
- package/dist/agent/endpoint.js +186 -0
- package/dist/agent/toolBridge.d.ts +35 -0
- package/dist/agent/toolBridge.js +93 -0
- package/dist/agent/turn.d.ts +71 -0
- package/dist/agent/turn.js +113 -0
- package/dist/chromeFields.d.ts +15 -0
- package/dist/chromeFields.js +13 -0
- package/dist/chromeStyles.d.ts +3 -0
- package/dist/chromeStyles.js +3 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +19 -0
- package/dist/mcp/bridgeServer.d.ts +13 -0
- package/dist/mcp/bridgeServer.js +1 -0
- package/dist/mcp/loadGameCatalogs.d.ts +24 -0
- package/dist/mcp/loadGameCatalogs.js +87 -0
- package/dist/mcp/rpcRequest.d.ts +22 -0
- package/dist/mcp/rpcRequest.js +97 -0
- package/dist/mcp/tools.d.ts +8 -0
- package/dist/mcp/tools.js +764 -0
- package/dist/outlinerModel.d.ts +76 -0
- package/dist/outlinerModel.js +95 -0
- package/dist/perfMarks.d.ts +25 -0
- package/dist/perfMarks.js +36 -0
- package/dist/session.d.ts +400 -0
- package/dist/session.js +1032 -0
- package/dist/uiStore.d.ts +108 -0
- package/dist/uiStore.js +128 -0
- package/dist/useF2Chord.d.ts +2 -0
- package/dist/useF2Chord.js +32 -0
- package/dist/useStoreSelector.d.ts +31 -0
- package/dist/useStoreSelector.js +51 -0
- package/dist/viewportContextMenu.d.ts +24 -0
- package/dist/viewportContextMenu.js +32 -0
- package/package.json +58 -0
- package/skills/jgengine/SKILL.md +823 -0
- package/skills/jgengine/api.md +416 -0
- package/skills/jgengine/capabilities.md +21 -0
- package/skills/jgengine/reference-cartridge.md +72 -0
- package/skills/jgengine/reference-orphans.md +9 -0
- package/skills/jgengine/reference-systems.md +60 -0
- package/skills/jgengine-editor/SKILL.md +382 -0
- package/skills/jgengine-editor/api.md +416 -0
- package/skills/jgengine-editor/capabilities.md +29 -0
- package/skills/jgengine-editor/reference.md +64 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { useFrame } from "@react-three/fiber";
|
|
4
|
+
import { useGameContext } from "@jgengine/react/provider";
|
|
5
|
+
import { MICRO } from "./chromeStyles.js";
|
|
6
|
+
const PUBLISH_MS = 100;
|
|
7
|
+
function metaValues(meta) {
|
|
8
|
+
if (meta === null || typeof meta !== "object" || Array.isArray(meta))
|
|
9
|
+
return undefined;
|
|
10
|
+
const out = {};
|
|
11
|
+
for (const [key, value] of Object.entries(meta)) {
|
|
12
|
+
if (typeof value === "number" ||
|
|
13
|
+
typeof value === "string" ||
|
|
14
|
+
typeof value === "boolean" ||
|
|
15
|
+
value === null) {
|
|
16
|
+
out[key] = value;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return Object.keys(out).length === 0 ? undefined : out;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Publishes live scene entities onto the editor reverse channel and gates `ctx.time` against the
|
|
23
|
+
* host play-control (pause / step). Mount only in editor play mode.
|
|
24
|
+
* @internal
|
|
25
|
+
*/
|
|
26
|
+
export function RuntimePlayPublisher({ api }) {
|
|
27
|
+
const ctx = useGameContext();
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const publish = () => {
|
|
30
|
+
const entities = ctx.scene.entity.list().map((entity) => {
|
|
31
|
+
const values = metaValues(entity.meta);
|
|
32
|
+
return {
|
|
33
|
+
id: entity.id,
|
|
34
|
+
position: { x: entity.position[0], y: entity.position[1], z: entity.position[2] },
|
|
35
|
+
rotationY: entity.rotationY,
|
|
36
|
+
...(values === undefined ? {} : { values }),
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
const play = api.getPlayControl();
|
|
40
|
+
api.getLiveSync().pushRuntimeDelta({
|
|
41
|
+
at: Date.now(),
|
|
42
|
+
entities,
|
|
43
|
+
tunables: {
|
|
44
|
+
paused: play.paused,
|
|
45
|
+
pendingSteps: play.pendingSteps,
|
|
46
|
+
now: ctx.time.now(),
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
publish();
|
|
51
|
+
const timer = setInterval(publish, PUBLISH_MS);
|
|
52
|
+
return () => clearInterval(timer);
|
|
53
|
+
}, [api, ctx]);
|
|
54
|
+
useFrame(() => {
|
|
55
|
+
const play = api.getPlayControl();
|
|
56
|
+
if (!play.paused) {
|
|
57
|
+
if (ctx.time.isPaused())
|
|
58
|
+
ctx.time.play();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (play.pendingSteps > 0) {
|
|
62
|
+
if (ctx.time.isPaused())
|
|
63
|
+
ctx.time.play();
|
|
64
|
+
api.setPlayControl({ paused: true, pendingSteps: play.pendingSteps - 1 });
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (!ctx.time.isPaused())
|
|
68
|
+
ctx.time.pause();
|
|
69
|
+
}, -1);
|
|
70
|
+
useFrame(() => {
|
|
71
|
+
const play = api.getPlayControl();
|
|
72
|
+
if (play.paused && play.pendingSteps === 0 && !ctx.time.isPaused()) {
|
|
73
|
+
ctx.time.pause();
|
|
74
|
+
}
|
|
75
|
+
}, 1);
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Play-mode chrome: pause/step controls plus a compact reverse-channel entity inspector.
|
|
80
|
+
* Transform nudges go through `runtime_set` (document write-back by default).
|
|
81
|
+
* @internal
|
|
82
|
+
*/
|
|
83
|
+
export function RuntimePlayInspectorChrome({ api, onExit, }) {
|
|
84
|
+
const [play, setPlay] = useState(() => api.getPlayControl());
|
|
85
|
+
const [summary, setSummary] = useState(null);
|
|
86
|
+
const [selectedId, setSelectedId] = useState(null);
|
|
87
|
+
const [detail, setDetail] = useState("");
|
|
88
|
+
useEffect(() => api.subscribePlayControl(setPlay), [api]);
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
const tick = () => {
|
|
91
|
+
const result = api.handle({ method: "runtime_summary" });
|
|
92
|
+
if (result.ok) {
|
|
93
|
+
setSummary(result.result);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
tick();
|
|
97
|
+
const timer = setInterval(tick, PUBLISH_MS);
|
|
98
|
+
return () => clearInterval(timer);
|
|
99
|
+
}, [api]);
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
if (selectedId === null) {
|
|
102
|
+
setDetail("");
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const got = api.handle({ method: "runtime_get", id: selectedId });
|
|
106
|
+
if (!got.ok) {
|
|
107
|
+
setDetail(got.error ?? "missing");
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
setDetail(JSON.stringify(got.result, null, 2));
|
|
111
|
+
}, [api, selectedId, summary?.entityCount, play.pendingSteps, play.paused]);
|
|
112
|
+
const pause = () => {
|
|
113
|
+
api.handle({ method: "runtime_pause" });
|
|
114
|
+
};
|
|
115
|
+
const resume = () => {
|
|
116
|
+
api.handle({ method: "runtime_resume" });
|
|
117
|
+
};
|
|
118
|
+
const step = () => {
|
|
119
|
+
api.handle({ method: "runtime_step", frames: 1 });
|
|
120
|
+
};
|
|
121
|
+
const nudgeSelected = (axis, delta) => {
|
|
122
|
+
if (selectedId === null)
|
|
123
|
+
return;
|
|
124
|
+
const got = api.handle({ method: "runtime_get", id: selectedId });
|
|
125
|
+
if (!got.ok)
|
|
126
|
+
return;
|
|
127
|
+
const entity = got.result.entity;
|
|
128
|
+
const position = entity?.position ?? { x: 0, y: 0, z: 0 };
|
|
129
|
+
api.handle({
|
|
130
|
+
method: "runtime_set",
|
|
131
|
+
id: selectedId,
|
|
132
|
+
position: {
|
|
133
|
+
x: position.x + (axis === "x" ? delta : 0),
|
|
134
|
+
y: position.y,
|
|
135
|
+
z: position.z + (axis === "z" ? delta : 0),
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
};
|
|
139
|
+
return (_jsxs("div", { className: "pointer-events-none absolute inset-0 z-50 flex flex-col", children: [_jsx("div", { className: "pointer-events-none flex justify-center pt-2", children: _jsxs("div", { className: "pointer-events-auto flex items-center gap-2 rounded-full border border-white/10 bg-black/75 px-3 py-1.5 text-[11px] text-neutral-100 shadow-lg shadow-black/40 backdrop-blur-md", children: [_jsx("span", { className: "text-neutral-300", children: play.paused ? "❚❚ Paused" : "▶ Playing" }), _jsx("button", { type: "button", className: "rounded-md bg-white/[0.08] px-2 py-0.5 hover:bg-white/15", onClick: () => (play.paused ? resume() : pause()), children: play.paused ? "Resume" : "Pause" }), _jsxs("button", { type: "button", className: "rounded-md bg-white/[0.08] px-2 py-0.5 hover:bg-white/15 disabled:opacity-40", onClick: step, disabled: !play.paused, children: ["Step", play.pendingSteps > 0 ? ` (${play.pendingSteps})` : ""] }), _jsx("button", { type: "button", className: "rounded-md bg-white/[0.08] px-2 py-0.5 hover:bg-white/15", onClick: onExit, children: "Editor (F2+E)" })] }) }), _jsxs("aside", { className: "pointer-events-auto ml-auto mt-2 mr-2 flex w-72 max-h-[70vh] flex-col overflow-hidden rounded-xl border border-white/[0.08] bg-[#0d0f13]/95 text-[11px] text-neutral-200 shadow-2xl shadow-black/50 backdrop-blur-md", children: [_jsxs("div", { className: "flex items-center border-b border-white/[0.06] px-3 py-2", children: [_jsx("div", { className: MICRO, children: "Runtime" }), _jsxs("div", { className: "ml-auto text-neutral-500", children: [summary?.entityCount ?? 0, " entities"] })] }), _jsxs("div", { className: "min-h-0 flex-1 overflow-auto p-2", children: [(summary?.entities ?? []).map((entity) => (_jsxs("button", { type: "button", className: `mb-1 block w-full truncate rounded-md px-2 py-1 text-left ${selectedId === entity.id ? "bg-cyan-500/20 text-cyan-100" : "hover:bg-white/[0.06]"}`, onClick: () => setSelectedId(entity.id), children: [entity.id, entity.valueKeys.length > 0 ? (_jsx("span", { className: "ml-1 text-neutral-500", children: entity.valueKeys.join(",") })) : null] }, entity.id))), (summary?.entities.length ?? 0) === 0 ? (_jsx("div", { className: "px-2 py-3 text-neutral-500", children: "No live entities yet \u2014 play or push_runtime_delta." })) : null] }), selectedId !== null ? (_jsxs("div", { className: "border-t border-white/[0.06] p-2", children: [_jsxs("div", { className: "mb-1 flex gap-1", children: [_jsx("button", { type: "button", className: "rounded-md bg-white/[0.07] px-2 py-1 hover:bg-white/15", onClick: () => nudgeSelected("x", -1), children: "\u2212X" }), _jsx("button", { type: "button", className: "rounded-md bg-white/[0.07] px-2 py-1 hover:bg-white/15", onClick: () => nudgeSelected("x", 1), children: "+X" }), _jsx("button", { type: "button", className: "rounded-md bg-white/[0.07] px-2 py-1 hover:bg-white/15", onClick: () => nudgeSelected("z", -1), children: "\u2212Z" }), _jsx("button", { type: "button", className: "rounded-md bg-white/[0.07] px-2 py-1 hover:bg-white/15", onClick: () => nudgeSelected("z", 1), children: "+Z" })] }), _jsx("pre", { className: "max-h-40 overflow-auto rounded-md border border-white/[0.06] bg-black/40 p-2 text-[10px] text-neutral-400", children: detail }), _jsx("div", { className: "mt-1 text-[10px] text-neutral-500", children: "Tweaks write back to the scene document (undoable)." })] })) : null] })] }));
|
|
140
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { EditorHostApi } from "./session.js";
|
|
2
|
+
/**
|
|
3
|
+
* Deterministic instanced preview of every foliage/scatter region in the document, grounded on the
|
|
4
|
+
* sculpt terrain (or the game's ground) and rebuilt whenever a region or its density changes. Renders
|
|
5
|
+
* real per-species proxy models through the shared `InstancedScatter` primitive — the same renderer a
|
|
6
|
+
* game uses to consume `resolveScatter` at runtime.
|
|
7
|
+
* @internal — mounted by `EditorApp`; not a game-author entry point.
|
|
8
|
+
*/
|
|
9
|
+
export declare const ScatterPreview: import("react").MemoExoticComponent<({ api }: {
|
|
10
|
+
api: EditorHostApi;
|
|
11
|
+
}) => import("react").JSX.Element>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { memo, useEffect, useMemo, useState } from "react";
|
|
3
|
+
import { editableTerrainFromSnapshot } from "@jgengine/core/world/terraform";
|
|
4
|
+
import { resolveScatter } from "@jgengine/core/world/scatterRegion";
|
|
5
|
+
import { useGameContext } from "@jgengine/react/provider";
|
|
6
|
+
import { InstancedScatter } from "@jgengine/shell/scatter";
|
|
7
|
+
const MAX_INSTANCES = 20000;
|
|
8
|
+
/**
|
|
9
|
+
* Deterministic instanced preview of every foliage/scatter region in the document, grounded on the
|
|
10
|
+
* sculpt terrain (or the game's ground) and rebuilt whenever a region or its density changes. Renders
|
|
11
|
+
* real per-species proxy models through the shared `InstancedScatter` primitive — the same renderer a
|
|
12
|
+
* game uses to consume `resolveScatter` at runtime.
|
|
13
|
+
* @internal — mounted by `EditorApp`; not a game-author entry point.
|
|
14
|
+
*/
|
|
15
|
+
export const ScatterPreview = memo(function ScatterPreview({ api }) {
|
|
16
|
+
const ctx = useGameContext();
|
|
17
|
+
const session = api.getSession();
|
|
18
|
+
const [, setTick] = useState(0);
|
|
19
|
+
useEffect(() => session.subscribe(() => setTick((value) => value + 1)), [session]);
|
|
20
|
+
const state = session.getState();
|
|
21
|
+
const snapshot = state.document.terrain ?? null;
|
|
22
|
+
const baseGround = ctx.world.ground;
|
|
23
|
+
const terrain = useMemo(() => {
|
|
24
|
+
if (snapshot !== null)
|
|
25
|
+
return editableTerrainFromSnapshot(snapshot, baseGround);
|
|
26
|
+
return { sampleHeight: baseGround.sampleHeight, sampleNormal: baseGround.sampleNormal };
|
|
27
|
+
}, [snapshot, baseGround]);
|
|
28
|
+
const instances = useMemo(() => resolveScatter(state.document, terrain), [state.document, terrain]);
|
|
29
|
+
return _jsx(InstancedScatter, { instances: instances, maxInstances: MAX_INSTANCES });
|
|
30
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type ParamSchema } from "@jgengine/core/scene/sceneKinds";
|
|
2
|
+
/** A patch to a single meta key, coalesced under a stable undo key. */
|
|
3
|
+
export type MetaPatch = (patch: Record<string, unknown>, coalesce: string) => void;
|
|
4
|
+
/**
|
|
5
|
+
* Renders any parameter schema as an inspector panel — one row per field
|
|
6
|
+
* (range/number/bool/select/color/text/seed/weighted-list) plus an optional `note` readout. Fully
|
|
7
|
+
* schema-driven: a new studio or asset generator needs no new JSX here. Drives both registered scene
|
|
8
|
+
* kinds (scatter, water, poles) and generator assets (building, bookcase) — the proof that the #809
|
|
9
|
+
* seam owns the inspector. Replaces the hand-written per-kind field blocks (`ScatterFields`).
|
|
10
|
+
* @internal — mounted by `EditorChrome`'s inspector; not a game-author entry point.
|
|
11
|
+
*/
|
|
12
|
+
export declare function SchemaInspector({ schema, label, accent, note, meta, onMeta, }: {
|
|
13
|
+
schema: ParamSchema;
|
|
14
|
+
label: string;
|
|
15
|
+
accent?: string;
|
|
16
|
+
note?: string;
|
|
17
|
+
meta: Record<string, unknown> | undefined;
|
|
18
|
+
onMeta: MetaPatch;
|
|
19
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { parseParams, randomizeGroupParams, resetGroupParams, } from "@jgengine/core/scene/sceneKinds";
|
|
4
|
+
const INPUT = "rounded-md border border-white/10 bg-black/40 px-2 py-1 outline-none transition-colors placeholder:text-neutral-600 focus:border-cyan-400/60 focus:bg-black/60";
|
|
5
|
+
const MICRO = "text-[9px] font-semibold uppercase tracking-wider text-neutral-500";
|
|
6
|
+
function labelOf(field) {
|
|
7
|
+
return field.label ?? field.key;
|
|
8
|
+
}
|
|
9
|
+
function FieldRow({ field, meta, onMeta }) {
|
|
10
|
+
const params = parseParams({ fields: [field] }, meta);
|
|
11
|
+
const coalesce = `kind:${field.key}`;
|
|
12
|
+
switch (field.type) {
|
|
13
|
+
case "range": {
|
|
14
|
+
const value = params[field.key];
|
|
15
|
+
return (_jsxs("label", { className: "block space-y-1", children: [_jsxs("span", { className: "flex items-center justify-between", children: [_jsx("span", { className: MICRO, children: labelOf(field) }), _jsxs("span", { className: "text-cyan-200", children: [value.toFixed(field.step !== undefined && field.step >= 1 ? 0 : 2), field.unit ?? ""] })] }), _jsx("input", { type: "range", min: field.min, max: field.max, step: field.step ?? (field.max - field.min) / 100, value: value, className: "w-full accent-emerald-400", onChange: (event) => onMeta({ [field.key]: Number(event.target.value) }, coalesce) })] }));
|
|
16
|
+
}
|
|
17
|
+
case "number": {
|
|
18
|
+
const value = params[field.key];
|
|
19
|
+
return (_jsxs("label", { className: "flex items-center justify-between gap-2", children: [_jsx("span", { className: MICRO, children: labelOf(field) }), _jsx("input", { type: "number", step: field.step ?? 1, className: `w-32 ${INPUT}`, value: value, onChange: (event) => {
|
|
20
|
+
const next = Number(event.target.value);
|
|
21
|
+
if (Number.isFinite(next))
|
|
22
|
+
onMeta({ [field.key]: next }, coalesce);
|
|
23
|
+
} })] }));
|
|
24
|
+
}
|
|
25
|
+
case "bool": {
|
|
26
|
+
const value = params[field.key];
|
|
27
|
+
return (_jsxs("label", { className: "flex items-center gap-1.5 text-[10px] text-neutral-400", children: [_jsx("input", { type: "checkbox", className: "accent-emerald-400", checked: value, onChange: (event) => onMeta({ [field.key]: event.target.checked }, coalesce) }), labelOf(field)] }));
|
|
28
|
+
}
|
|
29
|
+
case "select": {
|
|
30
|
+
const value = params[field.key];
|
|
31
|
+
return (_jsxs("label", { className: "flex items-center justify-between gap-2", children: [_jsx("span", { className: MICRO, children: labelOf(field) }), _jsx("select", { className: `w-32 ${INPUT}`, value: value, onChange: (event) => onMeta({ [field.key]: event.target.value }, coalesce), children: field.options.map((option) => (_jsx("option", { value: option.value, children: option.label ?? option.value }, option.value))) })] }));
|
|
32
|
+
}
|
|
33
|
+
case "color": {
|
|
34
|
+
const value = params[field.key];
|
|
35
|
+
return (_jsxs("label", { className: "flex items-center justify-between gap-2", children: [_jsx("span", { className: MICRO, children: labelOf(field) }), _jsx("input", { type: "color", className: "h-7 w-14 cursor-pointer rounded-md border border-white/10 bg-black/40", value: value, onChange: (event) => onMeta({ [field.key]: event.target.value }, coalesce) })] }));
|
|
36
|
+
}
|
|
37
|
+
case "text": {
|
|
38
|
+
const value = params[field.key];
|
|
39
|
+
return (_jsxs("label", { className: "flex items-center justify-between gap-2", children: [_jsx("span", { className: MICRO, children: labelOf(field) }), _jsx("input", { className: `w-32 min-w-0 ${INPUT}`, value: value, onChange: (event) => onMeta({ [field.key]: event.target.value }, coalesce) })] }));
|
|
40
|
+
}
|
|
41
|
+
case "seed": {
|
|
42
|
+
const value = params[field.key];
|
|
43
|
+
return (_jsxs("div", { className: "flex items-center gap-2", children: [_jsxs("label", { className: "flex min-w-0 flex-1 items-center gap-2", children: [_jsx("span", { className: MICRO, children: labelOf(field) }), _jsx("input", { className: `w-full min-w-0 ${INPUT}`, value: value, placeholder: "reroll\u2026", onChange: (event) => onMeta({ [field.key]: event.target.value }, coalesce) })] }), _jsx("button", { type: "button", className: "shrink-0 rounded-md bg-white/[0.04] px-2 py-1 text-neutral-300 ring-1 ring-inset ring-white/[0.06] transition-colors hover:bg-white/10", title: "Reroll seed", onClick: () => onMeta({ [field.key]: `r${Math.abs(Math.round((value.length + 1) * 2654435761)).toString(36).slice(0, 6)}${value.length}` }, coalesce), children: "\u27F3" })] }));
|
|
44
|
+
}
|
|
45
|
+
case "weightedList": {
|
|
46
|
+
const list = params[field.key];
|
|
47
|
+
const set = (next) => onMeta({ [field.key]: next }, coalesce);
|
|
48
|
+
return (_jsxs("div", { className: "space-y-1", children: [_jsx("div", { className: MICRO, children: labelOf(field) }), list.map((entry, index) => (_jsxs("div", { className: "flex items-center gap-1", children: [_jsx("input", { className: `min-w-0 flex-1 ${INPUT}`, value: entry.item, placeholder: field.itemLabel ?? "id", onChange: (event) => set(list.map((e, i) => (i === index ? { ...e, item: event.target.value } : e))) }), _jsx("input", { type: "number", step: 1, min: 0, className: `w-14 ${INPUT}`, value: entry.weight, onChange: (event) => set(list.map((e, i) => (i === index ? { ...e, weight: Math.max(0, Number(event.target.value)) } : e))) }), _jsx("button", { type: "button", className: "rounded-md bg-white/[0.04] px-1.5 py-1 text-neutral-400 ring-1 ring-inset ring-white/[0.06] transition-colors hover:bg-rose-500/20 hover:text-rose-200", disabled: list.length <= 1, onClick: () => set(list.filter((_, i) => i !== index)), children: "\u00D7" })] }, index))), _jsx("button", { type: "button", className: "w-full rounded-md bg-white/[0.04] px-2 py-1 text-[10px] text-neutral-300 ring-1 ring-inset ring-white/[0.06] transition-colors hover:bg-white/10", onClick: () => set([...list, { item: "", weight: 1 }]), children: "+ row" })] }));
|
|
49
|
+
}
|
|
50
|
+
case "action":
|
|
51
|
+
return null; // Action buttons are rendered by the group section, which owns the whole schema.
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** A schema action button (randomize / reset a group) — computed patches from the core helpers. */
|
|
55
|
+
function ActionButton({ field, schema, onMeta }) {
|
|
56
|
+
const run = () => onMeta(field.action === "randomize" ? randomizeGroupParams(schema, field.group, Math.random) : resetGroupParams(schema, field.group), `${field.action}:${field.group ?? "all"}`);
|
|
57
|
+
return (_jsxs("button", { type: "button", className: "w-full rounded-md bg-white/[0.06] px-2 py-1 text-[10px] font-medium text-neutral-200 ring-1 ring-inset ring-white/[0.08] transition-colors hover:bg-white/12", onClick: run, children: [field.action === "randomize" ? "🎲 " : "↺ ", labelOf(field)] }));
|
|
58
|
+
}
|
|
59
|
+
/** A collapsible group section: header (click to toggle) + its fields + any action buttons. */
|
|
60
|
+
function GroupSection({ title, accent, fields, schema, meta, onMeta, defaultCollapsed, }) {
|
|
61
|
+
const [collapsed, setCollapsed] = useState(defaultCollapsed);
|
|
62
|
+
return (_jsxs("div", { className: "rounded-md border border-white/[0.06]", children: [_jsxs("button", { type: "button", className: "flex w-full items-center gap-1 px-2 py-1 text-left text-[9px] font-semibold uppercase tracking-[0.12em] text-neutral-300 transition-colors hover:bg-white/[0.04]", onClick: () => setCollapsed((value) => !value), children: [_jsx("span", { className: "text-neutral-500", children: collapsed ? "▸" : "▾" }), _jsx("span", { style: { color: accent }, children: title })] }), collapsed ? null : (_jsx("div", { className: "space-y-2 px-2 pb-2", children: fields.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))) }))] }));
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Renders any parameter schema as an inspector panel — one row per field
|
|
66
|
+
* (range/number/bool/select/color/text/seed/weighted-list) plus an optional `note` readout. Fully
|
|
67
|
+
* schema-driven: a new studio or asset generator needs no new JSX here. Drives both registered scene
|
|
68
|
+
* kinds (scatter, water, poles) and generator assets (building, bookcase) — the proof that the #809
|
|
69
|
+
* seam owns the inspector. Replaces the hand-written per-kind field blocks (`ScatterFields`).
|
|
70
|
+
* @internal — mounted by `EditorChrome`'s inspector; not a game-author entry point.
|
|
71
|
+
*/
|
|
72
|
+
export function SchemaInspector({ schema, label, accent = "#34d399", note, meta, onMeta, }) {
|
|
73
|
+
const groups = schema.groups ?? [];
|
|
74
|
+
const grouped = new Set(groups.map((group) => group.id));
|
|
75
|
+
// Fields with no group (or a group not declared in schema.groups) render first, headerless.
|
|
76
|
+
const ungrouped = schema.fields.filter((field) => field.group === undefined || !grouped.has(field.group));
|
|
77
|
+
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 }), 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.map((group) => (_jsx(GroupSection, { title: group.label, accent: accent, fields: schema.fields.filter((field) => field.group === group.id), 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] }));
|
|
78
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { EditorSession } from "@jgengine/core/editor/index";
|
|
2
|
+
import type { EditorHostApi } from "./session.js";
|
|
3
|
+
import { type EditorUiStore } from "./uiStore.js";
|
|
4
|
+
export type { GizmoMode } from "./uiStore.js";
|
|
5
|
+
/**
|
|
6
|
+
* Canvas click-to-select and click-to-place. Document objects pick by screen proximity
|
|
7
|
+
* (registration always matches what you see) with click-cycling through stacked candidates
|
|
8
|
+
* and shift/ctrl additive selection; everything else picks by occlusion-ordered raycast
|
|
9
|
+
* against the tagged scene graph. When a placement tool is armed, clicks author new
|
|
10
|
+
* markers, volumes, notes, or path points at the ground hit instead of selecting.
|
|
11
|
+
*/
|
|
12
|
+
export declare const ViewportSelect: import("react").MemoExoticComponent<({ api, ui }: {
|
|
13
|
+
api: EditorHostApi;
|
|
14
|
+
ui: EditorUiStore;
|
|
15
|
+
}) => null>;
|
|
16
|
+
/**
|
|
17
|
+
* Drag-to-transform gizmo bound to the current selection, dispatching editor commands on
|
|
18
|
+
* release. Mounts the shared shell `TransformGizmo` and maps release poses onto editor
|
|
19
|
+
* commands (multi-select translate, volume scale, path vertex move).
|
|
20
|
+
*/
|
|
21
|
+
export declare const SelectionGizmo: import("react").MemoExoticComponent<({ session, ui, groundSnap, }: {
|
|
22
|
+
session: EditorSession;
|
|
23
|
+
ui: EditorUiStore;
|
|
24
|
+
groundSnap?: (x: number, z: number) => number;
|
|
25
|
+
}) => import("react").JSX.Element | null>;
|