@jgengine/editor 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -0
- package/README.md +12 -0
- package/dist/AssetBrowser.d.ts +33 -1
- package/dist/AssetBrowser.js +64 -2
- package/dist/AssetDropZone.d.ts +9 -0
- package/dist/AssetDropZone.js +65 -0
- package/dist/CatalogsPanel.d.ts +4 -2
- package/dist/CatalogsPanel.js +162 -9
- package/dist/CityPreview.d.ts +11 -0
- package/dist/CityPreview.js +45 -0
- package/dist/CollectionsPanel.js +5 -5
- package/dist/DebugDraw.d.ts +3 -1
- package/dist/DebugDraw.js +37 -19
- package/dist/EditorApp.d.ts +15 -1
- package/dist/EditorApp.js +147 -35
- package/dist/EditorChrome.d.ts +27 -6
- package/dist/EditorChrome.js +314 -130
- package/dist/InspectorPanel.d.ts +17 -7
- package/dist/InspectorPanel.js +173 -27
- package/dist/LightingPanel.d.ts +11 -0
- package/dist/LightingPanel.js +99 -0
- package/dist/MaterialsWorkspacePanel.d.ts +12 -0
- package/dist/MaterialsWorkspacePanel.js +86 -0
- package/dist/ParentPickerMenu.d.ts +13 -0
- package/dist/ParentPickerMenu.js +33 -0
- package/dist/PerfProbe.js +33 -0
- package/dist/PrefabsPanel.js +5 -5
- package/dist/RuntimePlayBridge.d.ts +5 -3
- package/dist/RuntimePlayBridge.js +6 -13
- package/dist/ScatterPreview.d.ts +3 -1
- package/dist/ScatterPreview.js +10 -7
- package/dist/SchemaInspector.js +21 -8
- package/dist/ScriptingPanel.d.ts +15 -0
- package/dist/ScriptingPanel.js +46 -0
- package/dist/SelectionGizmo.js +114 -30
- package/dist/StandaloneEditor.d.ts +16 -9
- package/dist/StandaloneEditor.js +19 -20
- package/dist/TerrainPanel.d.ts +3 -2
- package/dist/TerrainPanel.js +16 -5
- package/dist/TerrainSculpt.js +98 -59
- package/dist/agent/AgentPanel.d.ts +5 -2
- package/dist/agent/AgentPanel.js +11 -6
- package/dist/agent/toolBridge.js +5 -0
- package/dist/assetImport.d.ts +36 -0
- package/dist/assetImport.js +66 -0
- package/dist/authoredComponentMeta.d.ts +25 -0
- package/dist/authoredComponentMeta.js +65 -0
- package/dist/gizmoPivot.d.ts +4 -0
- package/dist/gizmoPivot.js +34 -0
- package/dist/handlers/catalogs.d.ts +3 -0
- package/dist/handlers/catalogs.js +137 -0
- package/dist/handlers/collections.d.ts +3 -0
- package/dist/handlers/collections.js +57 -0
- package/dist/handlers/context.d.ts +36 -0
- package/dist/handlers/context.js +1 -0
- package/dist/handlers/document.d.ts +3 -0
- package/dist/handlers/document.js +350 -0
- package/dist/handlers/grids.d.ts +3 -0
- package/dist/handlers/grids.js +142 -0
- package/dist/handlers/hierarchy.d.ts +3 -0
- package/dist/handlers/hierarchy.js +55 -0
- package/dist/handlers/index.d.ts +8 -0
- package/dist/handlers/index.js +25 -0
- package/dist/handlers/minimap.d.ts +9 -0
- package/dist/handlers/minimap.js +32 -0
- package/dist/handlers/runtime.d.ts +3 -0
- package/dist/handlers/runtime.js +191 -0
- package/dist/handlers/streets.d.ts +3 -0
- package/dist/handlers/streets.js +159 -0
- package/dist/handlers/terrain.d.ts +3 -0
- package/dist/handlers/terrain.js +204 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/listDocumentTriggers.d.ts +42 -0
- package/dist/listDocumentTriggers.js +49 -0
- package/dist/materialAssignments.d.ts +63 -0
- package/dist/materialAssignments.js +84 -0
- package/dist/mcp/fieldSpec.d.ts +32 -0
- package/dist/mcp/fieldSpec.js +174 -0
- package/dist/mcp/rpcRequest.d.ts +8 -5
- package/dist/mcp/rpcRequest.js +72 -93
- package/dist/mcp/tools.js +105 -912
- package/dist/networkSnapshot.d.ts +82 -0
- package/dist/networkSnapshot.js +73 -0
- package/dist/parentCandidates.d.ts +12 -0
- package/dist/parentCandidates.js +25 -0
- package/dist/session.d.ts +91 -4
- package/dist/session.js +36 -1017
- package/dist/shell/AnimationPanel.d.ts +17 -0
- package/dist/shell/AnimationPanel.js +134 -0
- package/dist/shell/AssetThumbnail.d.ts +12 -0
- package/dist/shell/AssetThumbnail.js +23 -0
- package/dist/shell/BottomDock.d.ts +25 -0
- package/dist/shell/BottomDock.js +40 -0
- package/dist/shell/CommandPalette.d.ts +12 -0
- package/dist/shell/CommandPalette.js +56 -0
- package/dist/shell/ConsolePanel.d.ts +8 -0
- package/dist/shell/ConsolePanel.js +45 -0
- package/dist/shell/ContentBrowser.d.ts +21 -0
- package/dist/shell/ContentBrowser.js +130 -0
- package/dist/shell/HierarchyPanel.d.ts +26 -0
- package/dist/shell/HierarchyPanel.js +387 -0
- package/dist/shell/NetworkWorkspacePanel.d.ts +11 -0
- package/dist/shell/NetworkWorkspacePanel.js +49 -0
- package/dist/shell/PlayModeBar.d.ts +12 -0
- package/dist/shell/PlayModeBar.js +27 -0
- package/dist/shell/ProfilerPanel.d.ts +10 -0
- package/dist/shell/ProfilerPanel.js +41 -0
- package/dist/shell/SceneToolbar.d.ts +43 -0
- package/dist/shell/SceneToolbar.js +122 -0
- package/dist/shell/StatusBar.d.ts +18 -0
- package/dist/shell/StatusBar.js +41 -0
- package/dist/shell/TopAppBar.d.ts +35 -0
- package/dist/shell/TopAppBar.js +64 -0
- package/dist/shell/ViewportOverlays.d.ts +23 -0
- package/dist/shell/ViewportOverlays.js +136 -0
- package/dist/shell/WorkspaceRail.d.ts +20 -0
- package/dist/shell/WorkspaceRail.js +31 -0
- package/dist/shell/cameraTelemetry.d.ts +27 -0
- package/dist/shell/cameraTelemetry.js +24 -0
- package/dist/shell/commandRegistry.d.ts +58 -0
- package/dist/shell/commandRegistry.js +133 -0
- package/dist/shell/consoleSink.d.ts +11 -0
- package/dist/shell/consoleSink.js +17 -0
- package/dist/shell/consoleStore.d.ts +27 -0
- package/dist/shell/consoleStore.js +43 -0
- package/dist/shell/fields.d.ts +39 -0
- package/dist/shell/fields.js +104 -0
- package/dist/shell/formatSavedRelative.d.ts +2 -0
- package/dist/shell/formatSavedRelative.js +14 -0
- package/dist/shell/glbThumbnailCache.d.ts +45 -0
- package/dist/shell/glbThumbnailCache.js +241 -0
- package/dist/shell/hierarchyA11y.d.ts +21 -0
- package/dist/shell/hierarchyA11y.js +35 -0
- package/dist/shell/icons.d.ts +13 -0
- package/dist/shell/icons.js +85 -0
- package/dist/shell/layoutStore.d.ts +68 -0
- package/dist/shell/layoutStore.js +156 -0
- package/dist/shell/pathFlythrough.d.ts +37 -0
- package/dist/shell/pathFlythrough.js +54 -0
- package/dist/shell/perfHistory.d.ts +70 -0
- package/dist/shell/perfHistory.js +130 -0
- package/dist/shell/renameObject.d.ts +3 -0
- package/dist/shell/renameObject.js +25 -0
- package/dist/shell/theme.d.ts +36 -0
- package/dist/shell/theme.js +37 -0
- package/dist/shell/ui.d.ts +84 -0
- package/dist/shell/ui.js +90 -0
- package/dist/shell/useGlbThumbnail.d.ts +8 -0
- package/dist/shell/useGlbThumbnail.js +19 -0
- package/dist/uiStore.d.ts +27 -0
- package/dist/uiStore.js +10 -0
- package/dist/viewportContextMenu.d.ts +4 -2
- package/dist/viewportContextMenu.js +5 -1
- package/package.json +4 -4
- package/skills/jgengine/SKILL.md +8 -4
- package/skills/jgengine/api.md +182 -81
- package/skills/jgengine/capabilities.md +104 -2
- package/skills/jgengine/recipes/minimal-game.md +88 -0
- package/skills/jgengine-editor/SKILL.md +18 -3
- package/skills/jgengine-editor/api.md +403 -20
- package/skills/jgengine-editor/capabilities.md +46 -0
- package/skills/jgengine-editor/reference.md +87 -0
- package/skills/level-design/SKILL.md +54 -0
- package/skills/level-design/agents/openai.yaml +4 -0
- package/skills/level-design/references/application-playbook.md +192 -0
- package/skills/level-design/references/field-guide.md +337 -0
- package/skills/level-design/references/live-editor-loop.md +179 -0
- package/dist/OutlinerPanel.d.ts +0 -14
- package/dist/OutlinerPanel.js +0 -84
- package/skills/jgengine/reference-cartridge.md +0 -72
package/dist/InspectorPanel.d.ts
CHANGED
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
import { type EditorSession } from "@jgengine/core/editor/index";
|
|
2
|
+
import type { EditorHostApi } from "./session.js";
|
|
2
3
|
import type { EditorUiStore } from "./uiStore.js";
|
|
4
|
+
import type { InspectorTab } from "./shell/layoutStore.js";
|
|
3
5
|
/**
|
|
4
|
-
* The right-hand inspector as an isolated, selector-subscribed panel
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* @internal — mounted by `EditorChrome`
|
|
6
|
+
* The right-hand inspector as an isolated, selector-subscribed panel: tabbed (Inspector /
|
|
7
|
+
* Components / Materials), with collapsible component cards over the live selected object.
|
|
8
|
+
* Reads only the document + selection slices (via `useStoreSelector`) and the ui store's
|
|
9
|
+
* `pathPoint` slice, so UI-only churn never rerenders it on `EditorChrome`'s render tick.
|
|
10
|
+
* @internal — mounted by `EditorChrome` inside the right dock.
|
|
9
11
|
*/
|
|
10
|
-
export declare function InspectorPanel({ session, ui, onClose }: {
|
|
12
|
+
export declare function InspectorPanel({ session, ui, onClose, api, tab, onSelectTab, collapsed, onToggleSection, }: {
|
|
11
13
|
session: EditorSession;
|
|
12
14
|
ui: EditorUiStore;
|
|
13
|
-
onClose
|
|
15
|
+
onClose?: () => void;
|
|
16
|
+
/** Host API for RPC-backed actions (material assignment, camera). */
|
|
17
|
+
api?: EditorHostApi;
|
|
18
|
+
/** Controlled active tab; omit for local state. */
|
|
19
|
+
tab?: InspectorTab;
|
|
20
|
+
onSelectTab?: (tab: InspectorTab) => void;
|
|
21
|
+
/** Controlled section collapse state (persisted by the shell layout store). */
|
|
22
|
+
collapsed?: Record<string, boolean>;
|
|
23
|
+
onToggleSection?: (id: string) => void;
|
|
14
24
|
}): import("react").JSX.Element;
|
package/dist/InspectorPanel.js
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { editorParentOf, findEditorNote, findEditorPath, isEditorObjectLocked, } from "@jgengine/core/editor/index";
|
|
3
4
|
import { readVegetationSettings, vegetationFootprint } from "@jgengine/core/world/vegetation";
|
|
4
5
|
import { getSceneKind, isSceneKind, parseParams, } from "@jgengine/core/scene/sceneKinds";
|
|
5
6
|
import { getAssetGenerator } from "@jgengine/core/scene/assetGenerator";
|
|
6
7
|
import { useGameContext } from "@jgengine/react/provider";
|
|
8
|
+
import { canAuthorTrigger, clearMaterialAssignmentPatch, clearTriggerInstallPatch, defaultTriggerInstallPatch, hasAuthoredTrigger, hasMaterialAssignment, } from "./authoredComponentMeta.js";
|
|
7
9
|
import { SchemaInspector } from "./SchemaInspector.js";
|
|
8
10
|
import { TriggerInspector } from "./TriggerInspector.js";
|
|
11
|
+
import { listParentCandidates } from "./parentCandidates.js";
|
|
12
|
+
import { TERRAIN_MATERIALS } from "./uiStore.js";
|
|
9
13
|
import { shallowArrayEqual, useStoreSelector } from "./useStoreSelector.js";
|
|
10
|
-
import { INPUT
|
|
14
|
+
import { INPUT } from "./chromeStyles.js";
|
|
11
15
|
import { NumberField } from "./chromeFields.js";
|
|
16
|
+
import { AxisNumberField, FieldRow, SectionAction } from "./shell/fields.js";
|
|
17
|
+
import { Icon, kindIcon } from "./shell/icons.js";
|
|
18
|
+
import { FOCUS_RING, INPUT_CLS, NUMERIC } from "./shell/theme.js";
|
|
19
|
+
import { CollapsibleSection, EmptyState, IconButton, PanelTabs } from "./shell/ui.js";
|
|
12
20
|
function VegetationFields({ volume, onMeta, }) {
|
|
13
21
|
const settings = readVegetationSettings(volume);
|
|
14
22
|
if (settings === null)
|
|
@@ -17,7 +25,7 @@ function VegetationFields({ volume, onMeta, }) {
|
|
|
17
25
|
const areaM2 = (footprint.maxX - footprint.minX) * (footprint.maxZ - footprint.minZ);
|
|
18
26
|
const sliderMax = settings.item === "grass" ? 12 : 1;
|
|
19
27
|
const estimated = Math.floor(areaM2 * settings.density);
|
|
20
|
-
return (_jsxs("div", { className: "space-y-2
|
|
28
|
+
return (_jsxs("div", { className: "space-y-2", children: [_jsxs("label", { className: "flex items-center justify-between gap-2", children: [_jsx("span", { className: "text-[9px] font-semibold uppercase tracking-wider text-neutral-500", children: "item" }), _jsx("input", { className: `w-32 ${INPUT}`, value: settings.item, placeholder: "grass", onChange: (event) => onMeta({ item: event.target.value }, "veg:item") })] }), _jsxs("label", { className: "block space-y-1", children: [_jsxs("span", { className: "flex items-center justify-between", children: [_jsx("span", { className: "text-[9px] font-semibold uppercase tracking-wider text-neutral-500", children: "density /m\u00B2" }), _jsx("span", { className: "text-cyan-200", children: settings.density.toFixed(2) })] }), _jsx("input", { type: "range", min: 0, max: sliderMax, step: sliderMax / 200, className: "w-full accent-emerald-400", value: Math.min(settings.density, sliderMax), onChange: (event) => onMeta({ density: Number(event.target.value) }, "veg:density") })] }), _jsx(NumberField, { label: "density", step: 0.01, value: settings.density, onCommit: (value) => onMeta({ density: Math.max(0, value) }, "veg:density") }), _jsx(NumberField, { label: "min scale", step: 0.05, value: settings.minScale, onCommit: (value) => onMeta({ minScale: value }, "veg:minScale") }), _jsx(NumberField, { label: "max scale", step: 0.05, value: settings.maxScale, onCommit: (value) => onMeta({ maxScale: value }, "veg:maxScale") }), _jsx(NumberField, { label: "spacing", step: 0.25, value: settings.minDistance, onCommit: (value) => onMeta({ minDistance: Math.max(0, value) }, "veg:minDistance") }), _jsxs("label", { className: "flex items-center justify-between gap-2", children: [_jsx("span", { className: "text-[9px] font-semibold uppercase tracking-wider text-neutral-500", children: "seed" }), _jsx("input", { className: `w-32 ${INPUT}`, value: settings.seed, placeholder: "reroll\u2026", onChange: (event) => onMeta({ seed: event.target.value }, "veg:seed") })] }), _jsxs("div", { className: "text-[10px] text-neutral-500", children: ["\u2248 ", estimated.toLocaleString(), " ", settings.item === "grass" ? "blades" : "placements", " over ", Math.round(areaM2).toLocaleString(), " m\u00B2"] })] }));
|
|
21
29
|
}
|
|
22
30
|
/** Builds the resolver-facing view of a document object for a registered scene kind's inspector. */
|
|
23
31
|
function markerObject(marker) {
|
|
@@ -58,42 +66,82 @@ function GeneratorInspector({ meta, onMeta }) {
|
|
|
58
66
|
*/
|
|
59
67
|
function ClearanceField({ meta, onMeta, }) {
|
|
60
68
|
const value = typeof meta?.["clearance"] === "number" ? meta["clearance"] : 0;
|
|
61
|
-
return (_jsx(
|
|
69
|
+
return (_jsx(FieldRow, { label: "Clearance", title: "Radius (m) foliage stays clear of and terrain flattens under; 0 = no clearance", children: _jsx(AxisNumberField, { label: "m", step: 0.5, value: value, onCommit: (next) => onMeta({ clearance: Math.max(0, next) }, "clearance") }) }));
|
|
62
70
|
}
|
|
63
71
|
/** Inspector row to parent the selected object under another (excludes itself and its descendants). */
|
|
64
72
|
function ParentField({ session, id }) {
|
|
65
73
|
const document = session.getState().document;
|
|
66
74
|
const current = editorParentOf(document, id) ?? "";
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
const labelOf = (node) => node.label ?? node.id;
|
|
70
|
-
const candidates = [
|
|
71
|
-
...document.markers.map((m) => ({ id: m.id, label: labelOf(m) })),
|
|
72
|
-
...document.volumes.map((v) => ({ id: v.id, label: labelOf(v) })),
|
|
73
|
-
...document.paths.map((p) => ({ id: p.id, label: labelOf(p) })),
|
|
74
|
-
...document.annotations.map((n) => ({ id: n.id, label: n.text.slice(0, 30) || n.id })),
|
|
75
|
-
].filter((entry) => !banned.has(entry.id));
|
|
76
|
-
return (_jsxs("label", { className: "flex items-center justify-between gap-2", children: [_jsx("span", { className: "text-[9px] font-semibold uppercase tracking-wider text-neutral-500", children: "parent" }), _jsxs("select", { className: `w-40 ${INPUT}`, value: current, onChange: (event) => session.dispatch({ type: "setParent", ids: [id], parentId: event.target.value === "" ? null : event.target.value }), children: [_jsx("option", { value: "", children: "\u2014 none (root) \u2014" }), candidates.map((entry) => (_jsx("option", { value: entry.id, children: entry.label }, entry.id)))] })] }));
|
|
75
|
+
const candidates = listParentCandidates(document, [id]);
|
|
76
|
+
return (_jsx(FieldRow, { label: "Parent", children: _jsxs("select", { className: `h-6.5 w-full min-w-0 px-1.5 ${INPUT_CLS}`, value: current, "aria-label": "Parent object", onChange: (event) => session.dispatch({ type: "setParent", ids: [id], parentId: event.target.value === "" ? null : event.target.value }), children: [_jsx("option", { value: "", children: "\u2014 none (root) \u2014" }), candidates.map((entry) => (_jsx("option", { value: entry.id, children: entry.label }, entry.id)))] }) }));
|
|
77
77
|
}
|
|
78
78
|
function KindColorFields({ kind, color, onKind, onColor, }) {
|
|
79
|
-
return (_jsxs("div", { className: "
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
79
|
+
return (_jsxs("div", { className: "space-y-2", children: [_jsx(FieldRow, { label: "Kind", children: _jsx("input", { className: `h-6.5 w-full min-w-0 px-2 ${INPUT_CLS}`, value: kind, "aria-label": "Object kind", onChange: (event) => {
|
|
80
|
+
const next = event.target.value.trim();
|
|
81
|
+
if (next.length > 0)
|
|
82
|
+
onKind(next);
|
|
83
|
+
} }) }), _jsxs(FieldRow, { label: "Color", children: [_jsx("input", { type: "color", className: "h-6.5 w-9 shrink-0 cursor-pointer rounded-[5px] border border-white/10 bg-black/40", title: "Display color", "aria-label": "Display color", value: color ?? "#ffffff", onChange: (event) => onColor(event.target.value) }), color !== undefined ? (_jsx(SectionAction, { label: "Reset to kind default color", onClick: () => onColor(undefined), children: "reset" })) : (_jsx("span", { className: "text-[10px] text-neutral-600", children: "kind default" }))] })] }));
|
|
84
|
+
}
|
|
85
|
+
/** Header card identifying the selected object: type glyph, editable name, kind, id, lock state. */
|
|
86
|
+
function ObjectHeader({ kind, id, label, placeholder, locked, onLabel, }) {
|
|
87
|
+
return (_jsxs("div", { className: "flex items-center gap-2.5", children: [_jsx("div", { className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-[7px] border border-white/[0.08] bg-white/[0.03] text-cyan-300", children: _jsx(Icon, { name: kindIcon(kind), size: 17 }) }), _jsxs("div", { className: "min-w-0 flex-1", children: [onLabel !== undefined ? (_jsx("input", { className: `h-6.5 w-full px-2 font-medium text-cyan-100 ${INPUT_CLS}`, value: label, placeholder: placeholder, "aria-label": "Object name", onChange: (event) => onLabel(event.target.value) })) : (_jsx("div", { className: "truncate text-[12px] font-medium text-cyan-100", children: label.length > 0 ? label : placeholder })), _jsxs("div", { className: "mt-0.5 flex items-center gap-1.5 text-[10px] text-neutral-500", children: [_jsx("span", { children: kind }), _jsx("span", { className: "text-neutral-700", children: "\u00B7" }), _jsx("span", { className: "truncate", title: id, children: id }), locked ? _jsx(Icon, { name: "lock", size: 10, className: "shrink-0 text-amber-400/80", "aria-label": "Locked via collection" }) : null] })] })] }));
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Add/remove bar for optional authored components (trigger, material). Only offers real
|
|
91
|
+
* data-model seams — no invented component system.
|
|
92
|
+
*/
|
|
93
|
+
function ComponentAddBar({ target, meta, onMeta, api, selectionIds, }) {
|
|
94
|
+
const offerTrigger = canAuthorTrigger(target) && !hasAuthoredTrigger(meta);
|
|
95
|
+
const offerMaterial = api !== undefined && selectionIds.length > 0 && !hasMaterialAssignment(meta);
|
|
96
|
+
if (!offerTrigger && !offerMaterial)
|
|
97
|
+
return null;
|
|
98
|
+
return (_jsxs("div", { className: "space-y-1.5 rounded-[6px] border border-dashed border-white/[0.1] bg-white/[0.02] p-2", children: [_jsx("div", { className: "text-[10px] font-medium uppercase tracking-wider text-neutral-500", children: "Add component" }), _jsxs("div", { className: "flex flex-wrap gap-1.5", children: [offerTrigger ? (_jsx("button", { type: "button", className: `rounded-[5px] border border-amber-400/25 bg-amber-500/10 px-2 py-1 text-[11px] text-amber-100 transition-colors hover:bg-amber-500/20 ${FOCUS_RING}`, onClick: () => {
|
|
99
|
+
const patch = defaultTriggerInstallPatch(target, meta);
|
|
100
|
+
if (patch !== null)
|
|
101
|
+
onMeta(patch, "component:add-trigger");
|
|
102
|
+
}, children: "+ Trigger" })) : null, offerMaterial ? (_jsxs("label", { className: "flex items-center gap-1.5 text-[11px] text-neutral-400", children: [_jsx("span", { className: "sr-only", children: "Add material" }), _jsxs("select", { className: `h-7 max-w-[10rem] px-1.5 ${INPUT_CLS}`, defaultValue: "", "aria-label": "Add material component", onChange: (event) => {
|
|
103
|
+
const materialId = event.target.value;
|
|
104
|
+
if (materialId.length === 0)
|
|
105
|
+
return;
|
|
106
|
+
api.handle({ method: "assign_material", ids: [...selectionIds], materialId });
|
|
107
|
+
event.currentTarget.value = "";
|
|
108
|
+
}, children: [_jsx("option", { value: "", children: "+ Material\u2026" }), TERRAIN_MATERIALS.map((material) => (_jsx("option", { value: material.id, children: material.label }, material.id)))] })] })) : null] })] }));
|
|
109
|
+
}
|
|
110
|
+
function Section({ id, title, icon, sections, children, trailing, }) {
|
|
111
|
+
return (_jsx(CollapsibleSection, { title: title, ...(icon === undefined ? {} : { icon }), open: sections.collapsed[id] !== true, onToggle: () => sections.toggle(id), ...(trailing === undefined ? {} : { trailing }), children: children }));
|
|
112
|
+
}
|
|
113
|
+
/** Materials tab: assign the terrain material palette to the current selection over the real RPC. */
|
|
114
|
+
function MaterialsTab({ api, selection, meta, }) {
|
|
115
|
+
const current = typeof meta?.["materialId"] === "string" ? meta["materialId"] : null;
|
|
116
|
+
if (selection.length === 0) {
|
|
117
|
+
return (_jsx(EmptyState, { icon: "sphere", title: "No selection", description: "Select an object to view or assign its material. Chips can also be dragged onto hierarchy rows or the viewport." }));
|
|
118
|
+
}
|
|
119
|
+
return (_jsxs("div", { className: "space-y-2.5 p-2.5", children: [_jsxs("div", { className: "text-[10px] text-neutral-500", children: ["Assigned material", _jsx("div", { className: "mt-1 flex items-center gap-2 rounded-[6px] border border-white/[0.07] bg-white/[0.02] px-2.5 py-2 text-[12px] text-neutral-200", children: current !== null ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "h-3.5 w-3.5 rounded-full ring-1 ring-inset ring-white/20", style: { backgroundColor: TERRAIN_MATERIALS.find((material) => material.id === current)?.color ?? "#888" } }), current] })) : (_jsx("span", { className: "text-neutral-500", children: "none \u2014 kind default appearance" })) })] }), _jsx("div", { className: "text-[10px] text-neutral-500", children: "Palette" }), _jsx("div", { className: "flex flex-wrap gap-1.5", children: TERRAIN_MATERIALS.map((material) => (_jsxs("button", { type: "button", disabled: api === undefined, onClick: () => api?.handle({ method: "assign_material", ids: [...selection], materialId: material.id }), className: `flex items-center gap-1.5 rounded-[6px] border px-2 py-1 text-[11px] transition-colors ${FOCUS_RING} ${current === material.id
|
|
120
|
+
? "border-cyan-400/50 bg-cyan-500/15 text-cyan-100"
|
|
121
|
+
: "border-white/[0.08] bg-[#191d24] text-neutral-300 hover:bg-[#1f242d]"}`, title: `Assign ${material.label} to selection`, children: [_jsx("span", { className: "h-3 w-3 rounded-full ring-1 ring-inset ring-white/20", style: { backgroundColor: material.color } }), material.label] }, material.id))) })] }));
|
|
84
122
|
}
|
|
85
123
|
/**
|
|
86
|
-
* The right-hand inspector as an isolated, selector-subscribed panel
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
* @internal — mounted by `EditorChrome`
|
|
124
|
+
* The right-hand inspector as an isolated, selector-subscribed panel: tabbed (Inspector /
|
|
125
|
+
* Components / Materials), with collapsible component cards over the live selected object.
|
|
126
|
+
* Reads only the document + selection slices (via `useStoreSelector`) and the ui store's
|
|
127
|
+
* `pathPoint` slice, so UI-only churn never rerenders it on `EditorChrome`'s render tick.
|
|
128
|
+
* @internal — mounted by `EditorChrome` inside the right dock.
|
|
91
129
|
*/
|
|
92
|
-
export function InspectorPanel({ session, ui, onClose }) {
|
|
130
|
+
export function InspectorPanel({ session, ui, onClose, api, tab, onSelectTab, collapsed, onToggleSection, }) {
|
|
93
131
|
const document = useStoreSelector(session, (state) => state.document);
|
|
94
132
|
const selection = useStoreSelector(session, (state) => state.selection, shallowArrayEqual);
|
|
95
133
|
const pathPoint = useStoreSelector(ui, (state) => state.pathPoint);
|
|
96
134
|
const ctx = useGameContext();
|
|
135
|
+
const [localTab, setLocalTab] = useState("inspector");
|
|
136
|
+
const [localCollapsed, setLocalCollapsed] = useState({});
|
|
137
|
+
const [linkedExtents, setLinkedExtents] = useState(false);
|
|
138
|
+
const activeTab = tab ?? localTab;
|
|
139
|
+
const selectTab = onSelectTab ?? setLocalTab;
|
|
140
|
+
const sections = {
|
|
141
|
+
collapsed: collapsed ?? localCollapsed,
|
|
142
|
+
toggle: onToggleSection ??
|
|
143
|
+
((id) => setLocalCollapsed((previous) => ({ ...previous, [id]: !(previous[id] === true) }))),
|
|
144
|
+
};
|
|
97
145
|
const selectedId = selection[0];
|
|
98
146
|
const selectedMarker = document.markers.find((marker) => marker.id === selectedId);
|
|
99
147
|
const selectedVolume = document.volumes.find((volume) => volume.id === selectedId);
|
|
@@ -106,5 +154,103 @@ export function InspectorPanel({ session, ui, onClose }) {
|
|
|
106
154
|
selectedNote === undefined;
|
|
107
155
|
const liveEntity = documentMiss ? ctx.scene.entity.get(selectedId) : null;
|
|
108
156
|
const liveObject = documentMiss && liveEntity === null ? ctx.scene.object.get(selectedId) : null;
|
|
109
|
-
return (_jsx("aside", { className: "pointer-events-auto flex w-72 min-w-56 max-w-[42vw] resize-x flex-col overflow-auto border-l border-white/[0.08] bg-[#0d0f13]/95 p-3 backdrop-blur-md", style: { direction: "rtl" }, children: _jsxs("div", { className: "flex-1", style: { direction: "ltr" }, children: [_jsxs("div", { className: "flex items-center", children: [_jsx("div", { className: MICRO, children: "Inspector" }), _jsx("button", { type: "button", className: "ml-auto rounded-md px-2 py-1 text-neutral-500 transition-colors hover:bg-white/10 hover:text-neutral-200", onClick: onClose, "aria-label": "Close inspector panel", children: "\u00D7" })] }), selection.length > 1 ? (_jsxs("div", { className: "mt-3 space-y-2", children: [_jsxs("div", { className: "text-cyan-200", children: [selection.length, " objects selected"] }), _jsx("div", { className: "max-h-32 space-y-0.5 overflow-auto text-neutral-500", children: selection.map((id) => _jsx("div", { className: "truncate", children: id }, id)) }), _jsxs("div", { className: "flex gap-2", children: [_jsx("button", { type: "button", className: "rounded-md bg-white/[0.07] px-2 py-1 ring-1 ring-inset ring-white/[0.06] transition-colors hover:bg-white/15", onClick: () => session.dispatch({ type: "duplicate", ids: selection }), children: "Duplicate" }), _jsx("button", { type: "button", className: "rounded-md bg-rose-500/15 px-2 py-1 text-rose-200 ring-1 ring-inset ring-rose-400/25 transition-colors hover:bg-rose-500/25", onClick: () => session.dispatch({ type: "removeMany", ids: selection }), children: "Delete all" })] })] })) : null, selection.length <= 1 && selectedMarker !== undefined ? (_jsxs("div", { className: "mt-3 space-y-2", children: [_jsx("input", { className: `w-full ${INPUT} font-medium text-cyan-200`, value: selectedMarker.label ?? "", placeholder: selectedMarker.id, onChange: (event) => session.dispatch({ type: "setMarker", id: selectedMarker.id, patch: { label: event.target.value } }, { coalesce: `label:${selectedMarker.id}` }) }), _jsxs("div", { className: "text-neutral-500", children: [selectedMarker.kind, " \u00B7 ", selectedMarker.id] }), _jsx(KindColorFields, { kind: selectedMarker.kind, color: selectedMarker.color, onKind: (kind) => session.dispatch({ type: "setMarker", id: selectedMarker.id, patch: { kind } }, { coalesce: `kind:${selectedMarker.id}` }), onColor: (color) => session.dispatch({ type: "setMarker", id: selectedMarker.id, patch: { color } }, { coalesce: `color:${selectedMarker.id}` }) }), ["x", "y", "z"].map((axis) => (_jsx(NumberField, { label: axis, value: selectedMarker.position[axis], onCommit: (value) => session.dispatch({ type: "setTransform", id: selectedMarker.id, position: { ...selectedMarker.position, [axis]: value } }, { coalesce: `pos:${axis}:${selectedMarker.id}` }) }, axis))), _jsx(NumberField, { label: "rot\u00B0", step: 5, value: Math.round(((selectedMarker.rotationY ?? 0) * 180) / Math.PI), onCommit: (value) => session.dispatch({ type: "setTransform", id: selectedMarker.id, rotationY: (value * Math.PI) / 180 }, { coalesce: `rot:${selectedMarker.id}` }) }), _jsx(ClearanceField, { meta: selectedMarker.meta, onMeta: (patch, coalesce) => session.dispatch({ type: "setMarker", id: selectedMarker.id, patch: { meta: { ...selectedMarker.meta, ...patch } } }, { coalesce: `${coalesce}:${selectedMarker.id}` }) }), _jsx(TriggerInspector, { target: "marker", meta: selectedMarker.meta, onMeta: (patch, coalesce) => session.dispatch({ type: "setMarker", id: selectedMarker.id, patch: { meta: { ...selectedMarker.meta, ...patch } } }, { coalesce: `${coalesce}:${selectedMarker.id}` }) }), _jsx(ParentField, { session: session, id: selectedMarker.id }), isSceneKind(selectedMarker.kind) ? (_jsx(KindInspector, { object: markerObject(selectedMarker), meta: selectedMarker.meta, onMeta: (patch, coalesce) => session.dispatch({ type: "setMarker", id: selectedMarker.id, patch: { meta: { ...selectedMarker.meta, ...patch } } }, { coalesce: `${coalesce}:${selectedMarker.id}` }) })) : null, _jsx(GeneratorInspector, { meta: selectedMarker.meta, onMeta: (patch, coalesce) => session.dispatch({ type: "setMarker", id: selectedMarker.id, patch: { meta: { ...selectedMarker.meta, ...patch } } }, { coalesce: `${coalesce}:${selectedMarker.id}` }) }), selectedMarker.meta !== undefined ? _jsx("pre", { className: "max-h-48 overflow-auto rounded-md border border-white/[0.06] bg-black/40 p-2 text-[10px] text-neutral-400", children: JSON.stringify(selectedMarker.meta, null, 2) }) : null] })) : null, selection.length <= 1 && selectedVolume !== undefined ? (_jsxs("div", { className: "mt-3 space-y-2", children: [_jsx("input", { className: `w-full ${INPUT} font-medium text-cyan-200`, value: selectedVolume.label ?? "", placeholder: selectedVolume.id, onChange: (event) => session.dispatch({ type: "setVolume", id: selectedVolume.id, patch: { label: event.target.value } }, { coalesce: `label:${selectedVolume.id}` }) }), _jsxs("div", { className: "text-neutral-500", children: [selectedVolume.kind, " \u00B7 ", selectedVolume.shape] }), _jsx(KindColorFields, { kind: selectedVolume.kind, color: selectedVolume.color, onKind: (kind) => session.dispatch({ type: "setVolume", id: selectedVolume.id, patch: { kind } }, { coalesce: `kind:${selectedVolume.id}` }), onColor: (color) => session.dispatch({ type: "setVolume", id: selectedVolume.id, patch: { color } }, { coalesce: `color:${selectedVolume.id}` }) }), ["x", "y", "z"].map((axis) => (_jsx(NumberField, { label: axis, value: selectedVolume.center[axis], onCommit: (value) => session.dispatch({ type: "setVolume", id: selectedVolume.id, patch: { center: { ...selectedVolume.center, [axis]: value } } }, { coalesce: `center:${axis}:${selectedVolume.id}` }) }, axis))), selectedVolume.shape !== "box" ? (_jsx(NumberField, { label: "radius", value: selectedVolume.radius ?? 5, onCommit: (value) => session.dispatch({ type: "setVolume", id: selectedVolume.id, patch: { radius: Math.max(0.5, value) } }, { coalesce: `radius:${selectedVolume.id}` }) })) : null, selectedVolume.shape === "cylinder" ? (_jsx(NumberField, { label: "height", value: selectedVolume.height ?? 4, onCommit: (value) => session.dispatch({ type: "setVolume", id: selectedVolume.id, patch: { height: Math.max(0.5, value) } }, { coalesce: `height:${selectedVolume.id}` }) })) : null, selectedVolume.shape === "box" ? (["x", "y", "z"].map((axis) => (_jsx(NumberField, { label: `half ${axis}`, value: selectedVolume.halfExtents?.[axis] ?? 5, onCommit: (value) => session.dispatch({ type: "setVolume", id: selectedVolume.id, patch: { halfExtents: { x: selectedVolume.halfExtents?.x ?? 5, y: selectedVolume.halfExtents?.y ?? 5, z: selectedVolume.halfExtents?.z ?? 5, [axis]: Math.max(0.5, value) } } }, { coalesce: `he:${axis}:${selectedVolume.id}` }) }, axis)))) : null, _jsx(ClearanceField, { meta: selectedVolume.meta, onMeta: (patch, coalesce) => session.dispatch({ type: "setVolume", id: selectedVolume.id, patch: { meta: { ...selectedVolume.meta, ...patch } } }, { coalesce: `${coalesce}:${selectedVolume.id}` }) }), _jsx(TriggerInspector, { target: "volume", meta: selectedVolume.meta, onMeta: (patch, coalesce) => session.dispatch({ type: "setVolume", id: selectedVolume.id, patch: { meta: { ...selectedVolume.meta, ...patch } } }, { coalesce: `${coalesce}:${selectedVolume.id}` }) }), _jsx(ParentField, { session: session, id: selectedVolume.id }), _jsx(VegetationFields, { volume: selectedVolume, onMeta: (patch, coalesce) => session.dispatch({ type: "setVolume", id: selectedVolume.id, patch: { meta: { ...selectedVolume.meta, ...patch } } }, { coalesce: `${coalesce}:${selectedVolume.id}` }) }), isSceneKind(selectedVolume.kind) ? (_jsx(KindInspector, { object: volumeObject(selectedVolume), meta: selectedVolume.meta, onMeta: (patch, coalesce) => session.dispatch({ type: "setVolume", id: selectedVolume.id, patch: { meta: { ...selectedVolume.meta, ...patch } } }, { coalesce: `${coalesce}:${selectedVolume.id}` }) })) : null] })) : null, selection.length <= 1 && selectedPath !== undefined ? (_jsxs("div", { className: "mt-3 space-y-2", children: [_jsx("input", { className: `w-full ${INPUT} font-medium text-cyan-200`, value: selectedPath.label ?? "", placeholder: selectedPath.id, onChange: (event) => session.dispatch({ type: "setPath", id: selectedPath.id, patch: { label: event.target.value } }, { coalesce: `label:${selectedPath.id}` }) }), _jsxs("div", { className: "text-neutral-500", children: [selectedPath.kind, " \u00B7 ", selectedPath.points.length, " points"] }), _jsx(KindColorFields, { kind: selectedPath.kind, color: selectedPath.color, onKind: (kind) => session.dispatch({ type: "setPath", id: selectedPath.id, patch: { kind } }, { coalesce: `kind:${selectedPath.id}` }), onColor: (color) => session.dispatch({ type: "setPath", id: selectedPath.id, patch: { color } }, { coalesce: `color:${selectedPath.id}` }) }), _jsx(NumberField, { label: "width", value: selectedPath.width ?? 4, onCommit: (value) => session.dispatch({ type: "setPath", id: selectedPath.id, patch: { width: Math.max(0.5, value) } }, { coalesce: `width:${selectedPath.id}` }) }), pathPoint !== null && pathPoint.pathId === selectedPath.id ? (_jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "text-neutral-400", children: ["Point ", pathPoint.index + 1, "/", selectedPath.points.length] }), _jsxs("div", { className: "flex gap-2", children: [_jsx("button", { type: "button", className: "rounded-md bg-white/[0.07] px-2 py-1 ring-1 ring-inset ring-white/[0.06] transition-colors hover:bg-white/15", onClick: () => { const at = pathPoint.index; const points = [...selectedPath.points.slice(0, at + 1), { ...selectedPath.points[at] }, ...selectedPath.points.slice(at + 1)]; session.dispatch({ type: "setPath", id: selectedPath.id, patch: { points } }); }, children: "Insert point" }), _jsx("button", { type: "button", className: "rounded-md bg-rose-500/15 px-2 py-1 text-rose-200 ring-1 ring-inset ring-rose-400/25 transition-colors hover:bg-rose-500/25 disabled:opacity-40", disabled: selectedPath.points.length <= 2, onClick: () => { const points = selectedPath.points.filter((_, index) => index !== pathPoint.index); ui.patch({ pathPoint: null }); session.dispatch({ type: "setPath", id: selectedPath.id, patch: { points } }); }, children: "Delete point" })] })] })) : _jsx("div", { className: "text-[10px] text-neutral-500", children: "Click a vertex sphere to edit points." }), isSceneKind(selectedPath.kind) ? (_jsx(KindInspector, { object: pathObject(selectedPath), meta: selectedPath.meta, onMeta: (patch, coalesce) => session.dispatch({ type: "setPath", id: selectedPath.id, patch: { meta: { ...selectedPath.meta, ...patch } } }, { coalesce: `${coalesce}:${selectedPath.id}` }) })) : null, _jsx(ParentField, { session: session, id: selectedPath.id })] })) : null, selection.length <= 1 && selectedNote !== undefined ? (_jsxs("div", { className: "mt-3 space-y-2", children: [_jsxs("div", { className: "text-neutral-500", children: ["note \u00B7 ", selectedNote.id] }), _jsx("textarea", { className: `h-24 w-full ${INPUT} text-neutral-100`, value: selectedNote.text, onChange: (event) => session.dispatch({ type: "setNote", id: selectedNote.id, patch: { text: event.target.value } }, { coalesce: `text:${selectedNote.id}` }) }), ["x", "y", "z"].map((axis) => (_jsx(NumberField, { label: axis, value: selectedNote.position[axis], onCommit: (value) => session.dispatch({ type: "setNote", id: selectedNote.id, patch: { position: { ...selectedNote.position, [axis]: value } } }, { coalesce: `npos:${axis}:${selectedNote.id}` }) }, axis)))] })) : null, liveEntity !== null ? _jsxs("div", { className: "mt-3 space-y-1", children: [_jsx("div", { className: "text-cyan-200", children: liveEntity.name }), _jsxs("div", { className: "text-neutral-500", children: ["live entity \u00B7 ", liveEntity.role, " \u00B7 ", liveEntity.id] }), _jsxs("div", { className: "text-neutral-400", children: ["x ", liveEntity.position[0].toFixed(1), " \u00B7 y ", liveEntity.position[1].toFixed(1), " \u00B7 z ", liveEntity.position[2].toFixed(1)] }), _jsx("div", { className: "text-[10px] text-neutral-500", children: "Live world object \u2014 edit its source data to move it permanently." })] }) : null, liveObject !== null ? _jsxs("div", { className: "mt-3 space-y-1", children: [_jsx("div", { className: "text-cyan-200", children: liveObject.catalogId }), _jsxs("div", { className: "text-neutral-500", children: ["live object \u00B7 ", liveObject.instanceId] }), _jsxs("div", { className: "text-neutral-400", children: ["x ", liveObject.position[0].toFixed(1), " \u00B7 y ", liveObject.position[1].toFixed(1), " \u00B7 z ", liveObject.position[2].toFixed(1)] }), _jsx("div", { className: "text-[10px] text-neutral-500", children: "Live world object \u2014 edit its source data to move it permanently." })] }) : null, selection.length === 0 && liveEntity === null && liveObject === null ? _jsx("div", { className: "mt-3 text-neutral-500", children: "Select an authored or live world object, or + Add to place new ones." }) : null] }) }));
|
|
157
|
+
const selectedMeta = selectedMarker?.meta ?? selectedVolume?.meta ?? selectedPath?.meta;
|
|
158
|
+
let body = null;
|
|
159
|
+
if (activeTab === "materials") {
|
|
160
|
+
body = _jsx(MaterialsTab, { api: api, selection: selection, meta: selectedMeta });
|
|
161
|
+
}
|
|
162
|
+
else if (selection.length > 1) {
|
|
163
|
+
const positions = selection
|
|
164
|
+
.map((id) => {
|
|
165
|
+
const marker = document.markers.find((entry) => entry.id === id);
|
|
166
|
+
if (marker !== undefined)
|
|
167
|
+
return { id, position: marker.position };
|
|
168
|
+
const volume = document.volumes.find((entry) => entry.id === id);
|
|
169
|
+
if (volume !== undefined)
|
|
170
|
+
return { id, position: volume.center };
|
|
171
|
+
const note = document.annotations.find((entry) => entry.id === id);
|
|
172
|
+
if (note !== undefined)
|
|
173
|
+
return { id, position: note.position };
|
|
174
|
+
return null;
|
|
175
|
+
})
|
|
176
|
+
.filter((entry) => entry !== null);
|
|
177
|
+
const axisValues = (axis) => {
|
|
178
|
+
if (positions.length === 0)
|
|
179
|
+
return { value: 0, mixed: true };
|
|
180
|
+
const first = positions[0].position[axis];
|
|
181
|
+
const mixed = positions.some((entry) => entry.position[axis] !== first);
|
|
182
|
+
return { value: first, mixed };
|
|
183
|
+
};
|
|
184
|
+
const commitAxis = (axis, value) => {
|
|
185
|
+
for (const entry of positions) {
|
|
186
|
+
session.dispatch({
|
|
187
|
+
type: "setTransform",
|
|
188
|
+
id: entry.id,
|
|
189
|
+
position: { ...entry.position, [axis]: value },
|
|
190
|
+
}, { coalesce: `multi-pos:${axis}:${entry.id}` });
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
body = (_jsxs("div", { className: "space-y-2.5 p-2.5", children: [_jsxs("div", { className: "flex items-center gap-2.5", children: [_jsx("div", { className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-[7px] border border-white/[0.08] bg-white/[0.03] text-cyan-300", children: _jsx(Icon, { name: "layers", size: 17 }) }), _jsxs("div", { children: [_jsxs("div", { className: "text-[12px] font-medium text-neutral-100", children: [selection.length, " objects selected"] }), _jsx("div", { className: "text-[10px] text-neutral-500", children: "Shared fields show \u201C\u2014\u201D when mixed; writing applies to every selected object" })] })] }), _jsx("div", { className: "max-h-28 space-y-0.5 overflow-auto rounded-[6px] border border-white/[0.06] bg-black/20 p-1.5 text-[10px] text-neutral-500", children: selection.map((id) => (_jsx("div", { className: "truncate", children: id }, id))) }), positions.length > 0 ? (_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-[10px] font-medium uppercase tracking-wider text-neutral-500", children: "Position" }), _jsx(FieldRow, { label: "Position", children: ["x", "y", "z"].map((axis) => {
|
|
194
|
+
const { value, mixed } = axisValues(axis);
|
|
195
|
+
return (_jsx(AxisNumberField, { axis: axis, label: axis, step: 0.5, value: value, mixed: mixed, onCommit: (next) => commitAxis(axis, next) }, axis));
|
|
196
|
+
}) })] })) : null, _jsxs("div", { className: "flex gap-1.5", children: [_jsx("button", { type: "button", className: `flex-1 rounded-[5px] border border-white/[0.07] bg-[#191d24] px-2 py-1.5 text-[11px] text-neutral-200 transition-colors hover:bg-[#1f242d] ${FOCUS_RING}`, onClick: () => session.dispatch({ type: "duplicate", ids: selection }), children: "Duplicate" }), _jsx("button", { type: "button", className: `flex-1 rounded-[5px] border border-rose-400/25 bg-rose-500/15 px-2 py-1.5 text-[11px] text-rose-200 transition-colors hover:bg-rose-500/25 ${FOCUS_RING}`, onClick: () => session.dispatch({ type: "removeMany", ids: selection }), children: "Delete all" })] })] }));
|
|
197
|
+
}
|
|
198
|
+
else if (selectedMarker !== undefined) {
|
|
199
|
+
const marker = selectedMarker;
|
|
200
|
+
const onMeta = (patch, coalesce) => session.dispatch({ type: "setMarker", id: marker.id, patch: { meta: { ...marker.meta, ...patch } } }, { coalesce: `${coalesce}:${marker.id}` });
|
|
201
|
+
const hasTrigger = hasAuthoredTrigger(marker.meta);
|
|
202
|
+
const hasMaterial = hasMaterialAssignment(marker.meta);
|
|
203
|
+
const hasGenerator = typeof marker.meta?.["assetId"] === "string" && getAssetGenerator(marker.meta["assetId"]) !== undefined;
|
|
204
|
+
const hasKindParams = isSceneKind(marker.kind);
|
|
205
|
+
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)] }));
|
|
207
|
+
}
|
|
208
|
+
else if (selectedVolume !== undefined) {
|
|
209
|
+
const volume = selectedVolume;
|
|
210
|
+
const onMeta = (patch, coalesce) => session.dispatch({ type: "setVolume", id: volume.id, patch: { meta: { ...volume.meta, ...patch } } }, { coalesce: `${coalesce}:${volume.id}` });
|
|
211
|
+
const setExtent = (axis, value) => {
|
|
212
|
+
const current = { x: volume.halfExtents?.x ?? 5, y: volume.halfExtents?.y ?? 5, z: volume.halfExtents?.z ?? 5 };
|
|
213
|
+
const next = linkedExtents
|
|
214
|
+
? { x: Math.max(0.5, value), y: Math.max(0.5, value), z: Math.max(0.5, value) }
|
|
215
|
+
: { ...current, [axis]: Math.max(0.5, value) };
|
|
216
|
+
session.dispatch({ type: "setVolume", id: volume.id, patch: { halfExtents: next } }, { coalesce: `he:${linkedExtents ? "all" : axis}:${volume.id}` });
|
|
217
|
+
};
|
|
218
|
+
const hasTrigger = hasAuthoredTrigger(volume.meta);
|
|
219
|
+
const hasMaterial = hasMaterialAssignment(volume.meta);
|
|
220
|
+
const hasVegetation = readVegetationSettings(volume) !== null;
|
|
221
|
+
const hasKindParams = isSceneKind(volume.kind);
|
|
222
|
+
const componentCards = (_jsxs(_Fragment, { children: [_jsx(ComponentAddBar, { target: "volume", meta: volume.meta, onMeta: onMeta, api: api, selectionIds: [volume.id] }), hasKindParams ? (_jsx(Section, { id: "kindParams", title: "Kind parameters", icon: "settings", sections: sections, children: _jsx(KindInspector, { object: volumeObject(volume), meta: volume.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: "volume", meta: volume.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(volume.meta?.["materialId"]) })] }) })) : null, hasVegetation ? (_jsxs(Section, { id: "vegetation", title: "Vegetation", icon: "terrain", sections: sections, children: [_jsxs("div", { className: "mb-1.5 text-[10px] text-neutral-500", children: ["Owned by volume kind ", _jsx("span", { className: NUMERIC, children: "vegetation" }), " \u2014 change kind in Inspector to remove."] }), _jsx(VegetationFields, { volume: volume, onMeta: onMeta })] })) : null, !hasKindParams && !hasTrigger && !hasMaterial && !hasVegetation && !canAuthorTrigger("volume") ? (_jsx(EmptyState, { icon: "settings", title: "No components", description: "This volume has no optional components. Games that register trigger actions enable Add trigger here." })) : null] }));
|
|
223
|
+
body = (_jsxs("div", { className: "space-y-2 p-2.5", children: [_jsx(ObjectHeader, { kind: volume.kind, id: volume.id, label: volume.label ?? "", placeholder: volume.id, locked: isEditorObjectLocked(document, volume.id), onLabel: (value) => session.dispatch({ type: "setVolume", id: volume.id, patch: { label: value } }, { coalesce: `label:${volume.id}` }) }), activeTab === "inspector" ? (_jsxs(_Fragment, { children: [_jsx(Section, { id: "transform", title: "Transform", icon: "move", sections: sections, trailing: volume.shape === "box" ? (_jsx(SectionAction, { label: linkedExtents ? "Unlink extents" : "Link extents (uniform)", active: linkedExtents, onClick: () => setLinkedExtents((value) => !value), children: "link" })) : undefined, children: _jsxs("div", { className: "space-y-1.5", children: [_jsx(FieldRow, { label: "Center", children: ["x", "y", "z"].map((axis) => (_jsx(AxisNumberField, { axis: axis, label: axis, step: 0.5, value: volume.center[axis], onCommit: (value) => session.dispatch({ type: "setVolume", id: volume.id, patch: { center: { ...volume.center, [axis]: value } } }, { coalesce: `center:${axis}:${volume.id}` }) }, axis))) }), volume.shape !== "box" ? (_jsx(FieldRow, { label: "Radius", children: _jsx(AxisNumberField, { label: "r", step: 0.5, value: volume.radius ?? 5, onCommit: (value) => session.dispatch({ type: "setVolume", id: volume.id, patch: { radius: Math.max(0.5, value) } }, { coalesce: `radius:${volume.id}` }) }) })) : null, volume.shape === "cylinder" ? (_jsx(FieldRow, { label: "Height", children: _jsx(AxisNumberField, { label: "h", step: 0.5, value: volume.height ?? 4, onCommit: (value) => session.dispatch({ type: "setVolume", id: volume.id, patch: { height: Math.max(0.5, value) } }, { coalesce: `height:${volume.id}` }) }) })) : null, volume.shape === "box" ? (_jsx(FieldRow, { label: "Extents", children: ["x", "y", "z"].map((axis) => (_jsx(AxisNumberField, { axis: axis, label: axis, step: 0.5, value: volume.halfExtents?.[axis] ?? 5, onCommit: (value) => setExtent(axis, value) }, axis))) })) : null] }) }), _jsx(Section, { id: "appearance", title: "Appearance", icon: "sphere", sections: sections, children: _jsx(KindColorFields, { kind: volume.kind, color: volume.color, onKind: (kind) => session.dispatch({ type: "setVolume", id: volume.id, patch: { kind } }, { coalesce: `kind:${volume.id}` }), onColor: (color) => session.dispatch({ type: "setVolume", id: volume.id, patch: { color } }, { coalesce: `color:${volume.id}` }) }) }), _jsx(Section, { id: "placement", title: "Placement", icon: "pin", sections: sections, children: _jsxs("div", { className: "space-y-2", children: [_jsx(ClearanceField, { meta: volume.meta, onMeta: onMeta }), _jsx(ParentField, { session: session, id: volume.id })] }) })] })) : (componentCards)] }));
|
|
224
|
+
}
|
|
225
|
+
else if (selectedPath !== undefined) {
|
|
226
|
+
const path = selectedPath;
|
|
227
|
+
const onMeta = (patch, coalesce) => session.dispatch({ type: "setPath", id: path.id, patch: { meta: { ...path.meta, ...patch } } }, { coalesce: `${coalesce}:${path.id}` });
|
|
228
|
+
body = (_jsxs("div", { className: "space-y-2 p-2.5", children: [_jsx(ObjectHeader, { kind: path.kind, id: path.id, label: path.label ?? "", placeholder: path.id, locked: isEditorObjectLocked(document, path.id), onLabel: (value) => session.dispatch({ type: "setPath", id: path.id, patch: { label: value } }, { coalesce: `label:${path.id}` }) }), activeTab === "inspector" ? (_jsxs(_Fragment, { children: [_jsx(Section, { id: "path", title: "Path", icon: "spline", sections: sections, children: _jsxs("div", { className: "space-y-1.5", children: [_jsx(FieldRow, { label: "Width", children: _jsx(AxisNumberField, { label: "w", step: 0.5, value: path.width ?? 4, onCommit: (value) => session.dispatch({ type: "setPath", id: path.id, patch: { width: Math.max(0.5, value) } }, { coalesce: `width:${path.id}` }) }) }), _jsxs("div", { className: `text-[10px] text-neutral-500 ${NUMERIC}`, children: [path.points.length, " points"] }), pathPoint !== null && pathPoint.pathId === path.id ? (_jsxs("div", { className: "space-y-1.5 rounded-[5px] border border-white/[0.06] bg-black/20 p-2", children: [_jsxs("div", { className: `text-[10px] text-neutral-400 ${NUMERIC}`, children: ["Point ", pathPoint.index + 1, "/", path.points.length] }), _jsxs("div", { className: "flex gap-1.5", children: [_jsx("button", { type: "button", className: `flex-1 rounded-[5px] border border-white/[0.07] bg-[#191d24] px-2 py-1 text-[10px] text-neutral-200 transition-colors hover:bg-[#1f242d] ${FOCUS_RING}`, onClick: () => {
|
|
229
|
+
const at = pathPoint.index;
|
|
230
|
+
const points = [...path.points.slice(0, at + 1), { ...path.points[at] }, ...path.points.slice(at + 1)];
|
|
231
|
+
session.dispatch({ type: "setPath", id: path.id, patch: { points } });
|
|
232
|
+
}, children: "Insert point" }), _jsx("button", { type: "button", className: `flex-1 rounded-[5px] border border-rose-400/25 bg-rose-500/15 px-2 py-1 text-[10px] text-rose-200 transition-colors hover:bg-rose-500/25 disabled:opacity-40 ${FOCUS_RING}`, disabled: path.points.length <= 2, onClick: () => {
|
|
233
|
+
const points = path.points.filter((_, index) => index !== pathPoint.index);
|
|
234
|
+
ui.patch({ pathPoint: null });
|
|
235
|
+
session.dispatch({ type: "setPath", id: path.id, patch: { points } });
|
|
236
|
+
}, children: "Delete point" })] })] })) : (_jsx("div", { className: "text-[10px] text-neutral-600", children: "Click a vertex sphere in the viewport to edit points." }))] }) }), _jsx(Section, { id: "appearance", title: "Appearance", icon: "sphere", sections: sections, children: _jsx(KindColorFields, { kind: path.kind, color: path.color, onKind: (kind) => session.dispatch({ type: "setPath", id: path.id, patch: { kind } }, { coalesce: `kind:${path.id}` }), onColor: (color) => session.dispatch({ type: "setPath", id: path.id, patch: { color } }, { coalesce: `color:${path.id}` }) }) }), _jsx(Section, { id: "placement", title: "Placement", icon: "pin", sections: sections, children: _jsx(ParentField, { session: session, id: path.id }) })] })) : isSceneKind(path.kind) ? (_jsx(Section, { id: "kindParams", title: "Kind parameters", icon: "settings", sections: sections, children: _jsx(KindInspector, { object: pathObject(path), meta: path.meta, onMeta: onMeta }) })) : (_jsx(EmptyState, { icon: "settings", title: "No components", description: "This path's kind registers no parameter schema." }))] }));
|
|
237
|
+
}
|
|
238
|
+
else if (selectedNote !== undefined) {
|
|
239
|
+
const note = selectedNote;
|
|
240
|
+
body = (_jsxs("div", { className: "space-y-2 p-2.5", children: [_jsx(ObjectHeader, { kind: "note", id: note.id, label: note.text.slice(0, 32), placeholder: note.id, locked: false }), _jsx(Section, { id: "note", title: "Note", icon: "note", sections: sections, children: _jsx("textarea", { className: `h-24 w-full p-2 ${INPUT_CLS}`, value: note.text, "aria-label": "Note text", onChange: (event) => session.dispatch({ type: "setNote", id: note.id, patch: { text: event.target.value } }, { coalesce: `text:${note.id}` }) }) }), _jsx(Section, { id: "transform", title: "Transform", icon: "move", sections: sections, children: _jsx(FieldRow, { label: "Position", children: ["x", "y", "z"].map((axis) => (_jsx(AxisNumberField, { axis: axis, label: axis, step: 0.5, value: note.position[axis], onCommit: (value) => session.dispatch({ type: "setNote", id: note.id, patch: { position: { ...note.position, [axis]: value } } }, { coalesce: `npos:${axis}:${note.id}` }) }, axis))) }) })] }));
|
|
241
|
+
}
|
|
242
|
+
else if (liveEntity !== null) {
|
|
243
|
+
body = (_jsxs("div", { className: "space-y-2 p-2.5", children: [_jsx(ObjectHeader, { kind: liveEntity.role, id: liveEntity.id, label: liveEntity.name, placeholder: liveEntity.id, locked: false }), _jsx(Section, { id: "live", title: "Live entity", icon: "walk", sections: sections, children: _jsxs("div", { className: `space-y-1 text-[11px] text-neutral-400 ${NUMERIC}`, children: [_jsxs("div", { children: ["x ", liveEntity.position[0].toFixed(1), " \u00B7 y ", liveEntity.position[1].toFixed(1), " \u00B7 z ", liveEntity.position[2].toFixed(1)] }), _jsx("div", { className: "text-[10px] text-neutral-600", children: "Read-only live world object \u2014 edit its source data to move it permanently." })] }) })] }));
|
|
244
|
+
}
|
|
245
|
+
else if (liveObject !== null) {
|
|
246
|
+
body = (_jsxs("div", { className: "space-y-2 p-2.5", children: [_jsx(ObjectHeader, { kind: "prop", id: liveObject.instanceId, label: liveObject.catalogId, placeholder: liveObject.instanceId, locked: false }), _jsx(Section, { id: "live", title: "Live object", icon: "cube", sections: sections, children: _jsxs("div", { className: `space-y-1 text-[11px] text-neutral-400 ${NUMERIC}`, children: [_jsxs("div", { children: ["x ", liveObject.position[0].toFixed(1), " \u00B7 y ", liveObject.position[1].toFixed(1), " \u00B7 z ", liveObject.position[2].toFixed(1)] }), _jsx("div", { className: "text-[10px] text-neutral-600", children: "Read-only live world object \u2014 edit its source data to move it permanently." })] }) })] }));
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
body = (_jsx(EmptyState, { icon: "cursor", title: "Nothing selected", description: "Click an object in the viewport or hierarchy to inspect it, or use Add to place new content." }));
|
|
250
|
+
}
|
|
251
|
+
return (_jsxs("div", { className: "flex h-full min-h-0 flex-col", children: [_jsx(PanelTabs, { ariaLabel: "Inspector tabs", active: activeTab, onSelect: selectTab, tabs: [
|
|
252
|
+
{ id: "inspector", label: "Inspector" },
|
|
253
|
+
{ id: "components", label: "Components" },
|
|
254
|
+
{ id: "materials", label: "Materials" },
|
|
255
|
+
], trailing: onClose !== undefined ? _jsx(IconButton, { icon: "close", label: "Close inspector", size: 12, tone: "ghost", onClick: onClose }) : undefined }), _jsx("div", { className: "min-h-0 flex-1 overflow-auto", children: body })] }));
|
|
110
256
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { EditorSession } from "@jgengine/core/editor/index";
|
|
2
|
+
/**
|
|
3
|
+
* Lighting workspace panel — edits `document.environment` (sky preset, time-of-day, intensities,
|
|
4
|
+
* fog, optional horizon/zenith colors) through the session undo stack. No fabricated runtime data:
|
|
5
|
+
* empty document shows defaults as display placeholders; the first edit writes a real bag.
|
|
6
|
+
*
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
export declare function LightingPanel({ session }: {
|
|
10
|
+
session: EditorSession;
|
|
11
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useSyncExternalStore } from "react";
|
|
3
|
+
import { SliderRow } from "./chromeFields.js";
|
|
4
|
+
import { BORDER, CONTROL, CONTROL_ACTIVE, FOCUS_RING, INPUT_CLS, MICRO_LABEL, PANEL_BG } from "./shell/theme.js";
|
|
5
|
+
const PRESETS = [
|
|
6
|
+
{ id: "day", label: "Day", hint: "Clear daylight sky" },
|
|
7
|
+
{ id: "dusk", label: "Dusk", hint: "Warm horizon, cooler zenith" },
|
|
8
|
+
{ id: "night", label: "Night", hint: "Low sun, dark dome" },
|
|
9
|
+
];
|
|
10
|
+
/** Engine-aligned defaults shown when the document has no environment bag yet. */
|
|
11
|
+
const DISPLAY_DEFAULTS = {
|
|
12
|
+
preset: "day",
|
|
13
|
+
timeOfDay: false,
|
|
14
|
+
sunIntensity: 1,
|
|
15
|
+
ambientIntensity: 0.45,
|
|
16
|
+
fogNear: 80,
|
|
17
|
+
fogFar: 400,
|
|
18
|
+
fogColor: "",
|
|
19
|
+
horizonColor: "",
|
|
20
|
+
zenithColor: "",
|
|
21
|
+
};
|
|
22
|
+
function readDisplay(env) {
|
|
23
|
+
return {
|
|
24
|
+
preset: env?.preset ?? DISPLAY_DEFAULTS.preset,
|
|
25
|
+
timeOfDay: env?.timeOfDay ?? DISPLAY_DEFAULTS.timeOfDay,
|
|
26
|
+
sunIntensity: env?.sunIntensity ?? DISPLAY_DEFAULTS.sunIntensity,
|
|
27
|
+
ambientIntensity: env?.ambientIntensity ?? DISPLAY_DEFAULTS.ambientIntensity,
|
|
28
|
+
fogNear: env?.fog?.near ?? DISPLAY_DEFAULTS.fogNear,
|
|
29
|
+
fogFar: env?.fog?.far ?? DISPLAY_DEFAULTS.fogFar,
|
|
30
|
+
fogColor: env?.fog?.color ?? DISPLAY_DEFAULTS.fogColor,
|
|
31
|
+
horizonColor: env?.horizonColor ?? DISPLAY_DEFAULTS.horizonColor,
|
|
32
|
+
zenithColor: env?.zenithColor ?? DISPLAY_DEFAULTS.zenithColor,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Lighting workspace panel — edits `document.environment` (sky preset, time-of-day, intensities,
|
|
37
|
+
* fog, optional horizon/zenith colors) through the session undo stack. No fabricated runtime data:
|
|
38
|
+
* empty document shows defaults as display placeholders; the first edit writes a real bag.
|
|
39
|
+
*
|
|
40
|
+
* @internal
|
|
41
|
+
*/
|
|
42
|
+
export function LightingPanel({ session }) {
|
|
43
|
+
const state = useSyncExternalStore(session.subscribe, session.getState, session.getState);
|
|
44
|
+
const env = state.document.environment;
|
|
45
|
+
const display = readDisplay(env);
|
|
46
|
+
const authored = env !== undefined;
|
|
47
|
+
const commit = (next, coalesce) => {
|
|
48
|
+
session.dispatch({ type: "setEnvironment", environment: next }, coalesce === undefined ? undefined : { coalesce });
|
|
49
|
+
};
|
|
50
|
+
const patch = (partial, coalesce) => {
|
|
51
|
+
const base = {
|
|
52
|
+
...(env ?? {}),
|
|
53
|
+
...partial,
|
|
54
|
+
};
|
|
55
|
+
// Keep fog merged so scrubbing near/far doesn't drop color.
|
|
56
|
+
if (partial.fog !== undefined || env?.fog !== undefined) {
|
|
57
|
+
base.fog = { ...(env?.fog ?? {}), ...(partial.fog ?? {}) };
|
|
58
|
+
if (Object.keys(base.fog).length === 0)
|
|
59
|
+
delete base.fog;
|
|
60
|
+
}
|
|
61
|
+
commit(base, coalesce);
|
|
62
|
+
};
|
|
63
|
+
return (_jsxs("div", { className: `pointer-events-auto absolute left-2.5 top-2 z-30 flex w-72 flex-col gap-2 rounded-[8px] border ${BORDER} ${PANEL_BG} p-2.5 shadow-xl shadow-black/40`, children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsxs("div", { children: [_jsx("div", { className: "text-[12px] font-medium text-neutral-100", children: "Lighting" }), _jsx("div", { className: "text-[10px] text-neutral-500", children: authored ? "Scene document environment" : "Defaults shown — first edit authors the bag" })] }), authored ? (_jsx("button", { type: "button", className: `${CONTROL} px-2 py-1 text-[10px]`, title: "Clear authored environment so world.ts fallbacks apply", onClick: () => commit(undefined), children: "Clear" })) : null] }), _jsxs("section", { className: "space-y-1.5", children: [_jsx("span", { className: MICRO_LABEL, children: "Sky preset" }), _jsx("div", { className: "grid grid-cols-3 gap-1", children: PRESETS.map((entry) => (_jsx("button", { type: "button", title: entry.hint, className: `rounded-[5px] px-1.5 py-1 text-[11px] transition-colors ${FOCUS_RING} ${display.preset === entry.id && (authored || entry.id === DISPLAY_DEFAULTS.preset)
|
|
64
|
+
? CONTROL_ACTIVE
|
|
65
|
+
: CONTROL}`, onClick: () => patch({ preset: entry.id }, "env:preset"), children: entry.label }, entry.id))) }), _jsxs("label", { className: "flex items-center gap-1.5 text-[11px] text-neutral-400", children: [_jsx("input", { type: "checkbox", className: "accent-cyan-400", checked: display.timeOfDay, onChange: (event) => patch({ timeOfDay: event.target.checked }, "env:tod") }), "Drive from time of day"] })] }), _jsxs("section", { className: "space-y-1.5 border-t border-white/[0.05] pt-2", children: [_jsx("span", { className: MICRO_LABEL, children: "Sun & ambient" }), _jsx(SliderRow, { label: "sun", value: display.sunIntensity, min: 0, max: 3, step: 0.05, onChange: (value) => patch({ sunIntensity: value }, "env:sun"), format: (v) => v.toFixed(2) }), _jsx(SliderRow, { label: "ambient", value: display.ambientIntensity, min: 0, max: 2, step: 0.05, onChange: (value) => patch({ ambientIntensity: value }, "env:ambient"), format: (v) => v.toFixed(2) })] }), _jsxs("section", { className: "space-y-1.5 border-t border-white/[0.05] pt-2", children: [_jsx("span", { className: MICRO_LABEL, children: "Fog" }), _jsx(SliderRow, { label: "near", value: display.fogNear, min: 0, max: 500, step: 1, onChange: (value) => patch({ fog: { near: value } }, "env:fogNear"), format: (v) => `${Math.round(v)}m` }), _jsx(SliderRow, { label: "far", value: display.fogFar, min: 10, max: 2000, step: 5, onChange: (value) => patch({ fog: { far: value } }, "env:fogFar"), format: (v) => `${Math.round(v)}m` }), _jsxs("label", { className: "flex items-center justify-between gap-2", children: [_jsx("span", { className: "text-[9px] font-semibold uppercase tracking-wider text-neutral-500", children: "color" }), _jsx("input", { type: "text", className: `w-36 px-2 py-1 ${INPUT_CLS}`, placeholder: "#a8c4d8", value: display.fogColor, onChange: (event) => {
|
|
66
|
+
const color = event.target.value.trim();
|
|
67
|
+
const fog = { ...(env?.fog ?? {}) };
|
|
68
|
+
if (color.length === 0)
|
|
69
|
+
delete fog.color;
|
|
70
|
+
else
|
|
71
|
+
fog.color = color;
|
|
72
|
+
const next = { ...(env ?? {}) };
|
|
73
|
+
if (Object.keys(fog).length === 0)
|
|
74
|
+
delete next.fog;
|
|
75
|
+
else
|
|
76
|
+
next.fog = fog;
|
|
77
|
+
commit(Object.keys(next).length === 0 ? undefined : next, "env:fogColor");
|
|
78
|
+
} })] })] }), _jsxs("section", { className: "space-y-1.5 border-t border-white/[0.05] pt-2", children: [_jsx("span", { className: MICRO_LABEL, children: "Sky colors (optional)" }), _jsxs("label", { className: "flex items-center justify-between gap-2", children: [_jsx("span", { className: "text-[9px] font-semibold uppercase tracking-wider text-neutral-500", children: "horizon" }), _jsx("input", { type: "text", className: `w-36 px-2 py-1 ${INPUT_CLS}`, placeholder: "#87ceeb", value: display.horizonColor, onChange: (event) => {
|
|
79
|
+
const horizonColor = event.target.value.trim();
|
|
80
|
+
if (horizonColor.length === 0) {
|
|
81
|
+
const next = { ...(env ?? {}) };
|
|
82
|
+
delete next.horizonColor;
|
|
83
|
+
commit(Object.keys(next).length === 0 ? undefined : next, "env:horizon");
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
patch({ horizonColor }, "env:horizon");
|
|
87
|
+
}
|
|
88
|
+
} })] }), _jsxs("label", { className: "flex items-center justify-between gap-2", children: [_jsx("span", { className: "text-[9px] font-semibold uppercase tracking-wider text-neutral-500", children: "zenith" }), _jsx("input", { type: "text", className: `w-36 px-2 py-1 ${INPUT_CLS}`, placeholder: "#1e3a5f", value: display.zenithColor, onChange: (event) => {
|
|
89
|
+
const zenithColor = event.target.value.trim();
|
|
90
|
+
if (zenithColor.length === 0) {
|
|
91
|
+
const next = { ...(env ?? {}) };
|
|
92
|
+
delete next.zenithColor;
|
|
93
|
+
commit(Object.keys(next).length === 0 ? undefined : next, "env:zenith");
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
patch({ zenithColor }, "env:zenith");
|
|
97
|
+
}
|
|
98
|
+
} })] })] })] }));
|
|
99
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { EditorSession } from "@jgengine/core/editor/index";
|
|
2
|
+
import type { EditorHostApi } from "./session.js";
|
|
3
|
+
/**
|
|
4
|
+
* Materials workspace home panel: browse every placeable's real `meta.materialId` from the live
|
|
5
|
+
* document, filter by assignment/palette id/text, select into the hierarchy, and assign/clear via
|
|
6
|
+
* the existing `assign_material` / `batch_set_properties` RPC seams. No fake thumbnails.
|
|
7
|
+
* @internal — mounted by `EditorChrome` when the materials workspace is active.
|
|
8
|
+
*/
|
|
9
|
+
export declare function MaterialsWorkspacePanel({ session, api, }: {
|
|
10
|
+
session: EditorSession;
|
|
11
|
+
api: EditorHostApi;
|
|
12
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo, useState } from "react";
|
|
3
|
+
import { clearMaterialAssignmentPatch } from "./authoredComponentMeta.js";
|
|
4
|
+
import { filterMaterialAssignments, listMaterialAssignments, summarizeMaterialUsage, } from "./materialAssignments.js";
|
|
5
|
+
import { TERRAIN_MATERIALS } from "./uiStore.js";
|
|
6
|
+
import { FOCUS_RING, INPUT_CLS, MICRO_LABEL } from "./shell/theme.js";
|
|
7
|
+
import { EmptyState } from "./shell/ui.js";
|
|
8
|
+
import { shallowArrayEqual, useStoreSelector } from "./useStoreSelector.js";
|
|
9
|
+
const CHIP = `rounded-[5px] border px-1.5 py-0.5 text-[10px] transition-colors ${FOCUS_RING}`;
|
|
10
|
+
const CHIP_IDLE = "border-white/[0.08] bg-[#191d24] text-neutral-400 hover:bg-[#1f242d] hover:text-neutral-200";
|
|
11
|
+
const CHIP_ACTIVE = "border-cyan-400/40 bg-cyan-500/15 text-cyan-100";
|
|
12
|
+
function materialColor(materialId) {
|
|
13
|
+
if (materialId === null)
|
|
14
|
+
return "transparent";
|
|
15
|
+
return TERRAIN_MATERIALS.find((material) => material.id === materialId)?.color ?? "#64748b";
|
|
16
|
+
}
|
|
17
|
+
function materialLabel(materialId) {
|
|
18
|
+
return TERRAIN_MATERIALS.find((material) => material.id === materialId)?.label ?? materialId;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Materials workspace home panel: browse every placeable's real `meta.materialId` from the live
|
|
22
|
+
* document, filter by assignment/palette id/text, select into the hierarchy, and assign/clear via
|
|
23
|
+
* the existing `assign_material` / `batch_set_properties` RPC seams. No fake thumbnails.
|
|
24
|
+
* @internal — mounted by `EditorChrome` when the materials workspace is active.
|
|
25
|
+
*/
|
|
26
|
+
export function MaterialsWorkspacePanel({ session, api, }) {
|
|
27
|
+
const document = useStoreSelector(session, (state) => state.document);
|
|
28
|
+
const selection = useStoreSelector(session, (state) => state.selection, shallowArrayEqual);
|
|
29
|
+
const [query, setQuery] = useState("");
|
|
30
|
+
const [filter, setFilter] = useState("all");
|
|
31
|
+
const rows = useMemo(() => listMaterialAssignments(document), [document]);
|
|
32
|
+
const filtered = useMemo(() => filterMaterialAssignments(rows, query, filter), [rows, query, filter]);
|
|
33
|
+
const usage = useMemo(() => summarizeMaterialUsage(rows), [rows]);
|
|
34
|
+
const assignedCount = rows.filter((row) => row.materialId !== null).length;
|
|
35
|
+
const selectRow = (id, additive) => {
|
|
36
|
+
if (additive) {
|
|
37
|
+
const next = selection.includes(id) ? selection.filter((entry) => entry !== id) : [...selection, id];
|
|
38
|
+
api.handle({ method: "select", ids: next });
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
api.handle({ method: "select", ids: [id] });
|
|
42
|
+
};
|
|
43
|
+
const assignTo = (ids, materialId) => {
|
|
44
|
+
if (ids.length === 0)
|
|
45
|
+
return;
|
|
46
|
+
api.handle({ method: "assign_material", ids: [...ids], materialId });
|
|
47
|
+
};
|
|
48
|
+
const clearFrom = (ids) => {
|
|
49
|
+
if (ids.length === 0)
|
|
50
|
+
return;
|
|
51
|
+
api.handle({
|
|
52
|
+
method: "batch_set_properties",
|
|
53
|
+
ids: [...ids],
|
|
54
|
+
meta: clearMaterialAssignmentPatch(),
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
const filterActive = (candidate) => {
|
|
58
|
+
if (typeof filter === "object" && typeof candidate === "object")
|
|
59
|
+
return filter.materialId === candidate.materialId;
|
|
60
|
+
return filter === candidate;
|
|
61
|
+
};
|
|
62
|
+
return (_jsxs("div", { className: "flex min-h-0 flex-1 flex-col", children: [_jsxs("div", { className: "space-y-2 border-b border-white/[0.06] p-2", children: [_jsx("div", { className: MICRO_LABEL, children: "Materials" }), _jsx("p", { className: "text-[10px] leading-snug text-neutral-500", children: "Document material assignments \u2014 select a row to inspect, assign from the palette, or clear." }), _jsx("input", { type: "search", value: query, onChange: (event) => setQuery(event.target.value), placeholder: "Filter objects\u2026", "aria-label": "Filter material assignments", className: `h-7 w-full px-2 ${INPUT_CLS}` }), _jsx("div", { className: "flex flex-wrap gap-1", role: "group", "aria-label": "Assignment filters", children: [
|
|
63
|
+
{ id: "all", label: `All (${rows.length})` },
|
|
64
|
+
{ id: "assigned", label: `Assigned (${assignedCount})` },
|
|
65
|
+
{ id: "unassigned", label: `None (${rows.length - assignedCount})` },
|
|
66
|
+
].map((entry) => (_jsx("button", { type: "button", onClick: () => setFilter(entry.id), className: `${CHIP} ${filterActive(entry.id) ? CHIP_ACTIVE : CHIP_IDLE}`, children: entry.label }, entry.id))) }), usage.length > 0 ? (_jsx("div", { className: "flex flex-wrap gap-1", role: "group", "aria-label": "Materials in use", children: usage.map((entry) => (_jsxs("button", { type: "button", onClick: () => setFilter({ materialId: entry.materialId }), title: `Show objects using ${entry.materialId}`, className: `${CHIP} flex items-center gap-1 ${filterActive({ materialId: entry.materialId }) ? CHIP_ACTIVE : CHIP_IDLE}`, children: [_jsx("span", { className: "h-2.5 w-2.5 rounded-full ring-1 ring-inset ring-white/20", style: { backgroundColor: materialColor(entry.materialId) } }), materialLabel(entry.materialId), _jsx("span", { className: "tabular-nums text-neutral-600", children: entry.count })] }, entry.materialId))) })) : null, selection.length > 0 ? (_jsxs("div", { className: "space-y-1.5 rounded-[6px] border border-white/[0.07] bg-white/[0.02] p-1.5", children: [_jsxs("div", { className: "text-[10px] text-neutral-500", children: ["Selection (", selection.length, ") \u2014 assign or clear"] }), _jsxs("div", { className: "flex flex-wrap gap-1", children: [TERRAIN_MATERIALS.map((material) => (_jsxs("button", { type: "button", title: `Assign ${material.label} to selection`, onClick: () => assignTo(selection, material.id), className: `${CHIP} flex items-center gap-1 ${CHIP_IDLE}`, children: [_jsx("span", { className: "h-2.5 w-2.5 rounded-full ring-1 ring-inset ring-white/20", style: { backgroundColor: material.color } }), material.label] }, material.id))), _jsx("button", { type: "button", onClick: () => clearFrom(selection), className: `${CHIP} border-rose-400/25 bg-rose-500/10 text-rose-200 hover:bg-rose-500/20`, children: "Clear" })] })] })) : null] }), _jsx("div", { className: "min-h-0 flex-1 overflow-auto p-1.5", role: "list", "aria-label": "Material assignments", children: filtered.length === 0 ? (_jsx(EmptyState, { icon: "sphere", title: rows.length === 0 ? "No placeables" : "No matches", description: rows.length === 0
|
|
67
|
+
? "Add markers, volumes, or paths to the scene, then assign materials from the palette or content browser."
|
|
68
|
+
: "Nothing matches this filter. Try All, or clear the search." })) : (filtered.map((row) => (_jsx(MaterialRow, { row: row, selected: selection.includes(row.id), onSelect: selectRow, onAssign: (materialId) => assignTo([row.id], materialId), onClear: () => clearFrom([row.id]) }, row.id)))) }), _jsx("div", { className: "flex h-7 shrink-0 items-center gap-2 border-t border-white/[0.06] px-2 text-[10px] text-neutral-600", children: _jsxs("span", { className: "tabular-nums", children: [filtered.length, " of ", rows.length, " objects \u00B7 ", assignedCount, " assigned"] }) })] }));
|
|
69
|
+
}
|
|
70
|
+
function MaterialRow({ row, selected, onSelect, onAssign, onClear, }) {
|
|
71
|
+
return (_jsxs("div", { role: "listitem", className: `group mb-0.5 flex items-center gap-1.5 rounded-[5px] px-1.5 py-1 transition-colors ${selected ? "bg-cyan-500/10 ring-1 ring-inset ring-cyan-400/30" : "hover:bg-white/[0.04]"}`, children: [_jsx("span", { className: "h-3 w-3 shrink-0 rounded-full ring-1 ring-inset ring-white/20", style: {
|
|
72
|
+
backgroundColor: materialColor(row.materialId),
|
|
73
|
+
opacity: row.materialId === null ? 0.35 : 1,
|
|
74
|
+
backgroundImage: row.materialId === null
|
|
75
|
+
? "linear-gradient(45deg, #333 25%, transparent 25%, transparent 75%, #333 75%), linear-gradient(45deg, #333 25%, transparent 25%, transparent 75%, #333 75%)"
|
|
76
|
+
: undefined,
|
|
77
|
+
backgroundSize: row.materialId === null ? "4px 4px" : undefined,
|
|
78
|
+
backgroundPosition: row.materialId === null ? "0 0, 2px 2px" : undefined,
|
|
79
|
+
}, title: row.materialId ?? "no material" }), _jsxs("button", { type: "button", onClick: (event) => onSelect(row.id, event.shiftKey || event.metaKey || event.ctrlKey), className: `min-w-0 flex-1 truncate text-left ${FOCUS_RING}`, title: `${row.label} (${row.id}) — click to select`, children: [_jsx("span", { className: "block truncate text-[11px] text-neutral-200", children: row.label }), _jsxs("span", { className: "block truncate text-[9px] text-neutral-600", children: [row.kind, " \u00B7 ", row.objectKind, row.materialId !== null ? ` · ${row.materialId}` : " · none"] })] }), _jsxs("select", { "aria-label": `Assign material to ${row.label}`, className: `h-6 max-w-[5.5rem] shrink-0 px-1 text-[10px] opacity-0 transition-opacity focus:opacity-100 group-hover:opacity-100 ${INPUT_CLS}`, value: row.materialId ?? "", onChange: (event) => {
|
|
80
|
+
const value = event.target.value;
|
|
81
|
+
if (value.length === 0)
|
|
82
|
+
onClear();
|
|
83
|
+
else
|
|
84
|
+
onAssign(value);
|
|
85
|
+
}, onClick: (event) => event.stopPropagation(), children: [_jsx("option", { value: "", children: "none" }), TERRAIN_MATERIALS.map((material) => (_jsx("option", { value: material.id, children: material.label }, material.id)))] })] }));
|
|
86
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ParentCandidate } from "./parentCandidates.js";
|
|
2
|
+
/**
|
|
3
|
+
* Searchable parent-target menu for hierarchy / viewport "Parent to…".
|
|
4
|
+
* Parent owns candidate list (cycle-safe) and the set_parent write.
|
|
5
|
+
* @internal
|
|
6
|
+
*/
|
|
7
|
+
export declare function ParentPickerMenu({ x, y, candidates, onPick, onClose, }: {
|
|
8
|
+
x: number;
|
|
9
|
+
y: number;
|
|
10
|
+
candidates: readonly ParentCandidate[];
|
|
11
|
+
onPick: (parentId: string | null) => void;
|
|
12
|
+
onClose: () => void;
|
|
13
|
+
}): import("react").JSX.Element;
|