@jgengine/editor 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -1
- package/README.md +12 -0
- package/dist/AssetBrowser.d.ts +26 -1
- package/dist/AssetBrowser.js +54 -1
- package/dist/AssetDropZone.d.ts +9 -0
- package/dist/AssetDropZone.js +65 -0
- package/dist/CatalogsPanel.d.ts +4 -2
- package/dist/CatalogsPanel.js +162 -9
- package/dist/CityPreview.d.ts +11 -0
- package/dist/CityPreview.js +45 -0
- package/dist/CollectionsPanel.js +5 -5
- package/dist/DebugDraw.d.ts +3 -1
- package/dist/DebugDraw.js +37 -19
- package/dist/EditorApp.d.ts +9 -1
- package/dist/EditorApp.js +208 -37
- package/dist/EditorChrome.d.ts +25 -6
- package/dist/EditorChrome.js +310 -118
- 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 +69 -2
- 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/TerrainReadout.d.ts +17 -0
- package/dist/TerrainReadout.js +163 -0
- package/dist/TerrainReadoutHud.d.ts +9 -0
- package/dist/TerrainReadoutHud.js +20 -0
- package/dist/TerrainSculpt.js +98 -59
- package/dist/agent/AgentPanel.d.ts +5 -2
- package/dist/agent/AgentPanel.js +11 -6
- package/dist/agent/toolBridge.js +5 -0
- package/dist/assetImport.d.ts +36 -0
- package/dist/assetImport.js +66 -0
- package/dist/authoredComponentMeta.d.ts +25 -0
- package/dist/authoredComponentMeta.js +65 -0
- package/dist/gizmoPivot.d.ts +4 -0
- package/dist/gizmoPivot.js +34 -0
- package/dist/handlers/catalogs.d.ts +3 -0
- package/dist/handlers/catalogs.js +137 -0
- package/dist/handlers/collections.d.ts +3 -0
- package/dist/handlers/collections.js +57 -0
- package/dist/handlers/context.d.ts +36 -0
- package/dist/handlers/context.js +1 -0
- package/dist/handlers/document.d.ts +3 -0
- package/dist/handlers/document.js +350 -0
- package/dist/handlers/grids.d.ts +3 -0
- package/dist/handlers/grids.js +142 -0
- package/dist/handlers/hierarchy.d.ts +3 -0
- package/dist/handlers/hierarchy.js +55 -0
- package/dist/handlers/index.d.ts +8 -0
- package/dist/handlers/index.js +23 -0
- package/dist/handlers/minimap.d.ts +9 -0
- package/dist/handlers/minimap.js +32 -0
- package/dist/handlers/runtime.d.ts +3 -0
- package/dist/handlers/runtime.js +191 -0
- package/dist/handlers/terrain.d.ts +3 -0
- package/dist/handlers/terrain.js +204 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/listDocumentTriggers.d.ts +42 -0
- package/dist/listDocumentTriggers.js +49 -0
- package/dist/materialAssignments.d.ts +63 -0
- package/dist/materialAssignments.js +84 -0
- package/dist/mcp/fieldSpec.d.ts +32 -0
- package/dist/mcp/fieldSpec.js +173 -0
- package/dist/mcp/rpcRequest.d.ts +8 -5
- package/dist/mcp/rpcRequest.js +72 -83
- package/dist/mcp/tools.js +104 -763
- 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/perfPill.d.ts +21 -0
- package/dist/perfPill.js +20 -0
- package/dist/session.d.ts +167 -5
- package/dist/session.js +36 -879
- package/dist/shell/AnimationPanel.d.ts +17 -0
- package/dist/shell/AnimationPanel.js +134 -0
- package/dist/shell/AssetThumbnail.d.ts +12 -0
- package/dist/shell/AssetThumbnail.js +23 -0
- package/dist/shell/BottomDock.d.ts +25 -0
- package/dist/shell/BottomDock.js +40 -0
- package/dist/shell/CommandPalette.d.ts +12 -0
- package/dist/shell/CommandPalette.js +56 -0
- package/dist/shell/ConsolePanel.d.ts +8 -0
- package/dist/shell/ConsolePanel.js +45 -0
- package/dist/shell/ContentBrowser.d.ts +21 -0
- package/dist/shell/ContentBrowser.js +130 -0
- package/dist/shell/HierarchyPanel.d.ts +26 -0
- package/dist/shell/HierarchyPanel.js +387 -0
- package/dist/shell/NetworkWorkspacePanel.d.ts +11 -0
- package/dist/shell/NetworkWorkspacePanel.js +49 -0
- package/dist/shell/PlayModeBar.d.ts +12 -0
- package/dist/shell/PlayModeBar.js +27 -0
- package/dist/shell/ProfilerPanel.d.ts +10 -0
- package/dist/shell/ProfilerPanel.js +41 -0
- package/dist/shell/SceneToolbar.d.ts +43 -0
- package/dist/shell/SceneToolbar.js +122 -0
- package/dist/shell/StatusBar.d.ts +18 -0
- package/dist/shell/StatusBar.js +41 -0
- package/dist/shell/TopAppBar.d.ts +33 -0
- package/dist/shell/TopAppBar.js +64 -0
- package/dist/shell/ViewportOverlays.d.ts +23 -0
- package/dist/shell/ViewportOverlays.js +136 -0
- package/dist/shell/WorkspaceRail.d.ts +20 -0
- package/dist/shell/WorkspaceRail.js +31 -0
- package/dist/shell/cameraTelemetry.d.ts +27 -0
- package/dist/shell/cameraTelemetry.js +24 -0
- package/dist/shell/commandRegistry.d.ts +58 -0
- package/dist/shell/commandRegistry.js +133 -0
- package/dist/shell/consoleSink.d.ts +11 -0
- package/dist/shell/consoleSink.js +17 -0
- package/dist/shell/consoleStore.d.ts +27 -0
- package/dist/shell/consoleStore.js +43 -0
- package/dist/shell/fields.d.ts +39 -0
- package/dist/shell/fields.js +104 -0
- package/dist/shell/formatSavedRelative.d.ts +2 -0
- package/dist/shell/formatSavedRelative.js +14 -0
- package/dist/shell/glbThumbnailCache.d.ts +45 -0
- package/dist/shell/glbThumbnailCache.js +241 -0
- package/dist/shell/hierarchyA11y.d.ts +21 -0
- package/dist/shell/hierarchyA11y.js +35 -0
- package/dist/shell/icons.d.ts +13 -0
- package/dist/shell/icons.js +85 -0
- package/dist/shell/layoutStore.d.ts +68 -0
- package/dist/shell/layoutStore.js +156 -0
- package/dist/shell/pathFlythrough.d.ts +37 -0
- package/dist/shell/pathFlythrough.js +54 -0
- package/dist/shell/perfHistory.d.ts +70 -0
- package/dist/shell/perfHistory.js +130 -0
- package/dist/shell/renameObject.d.ts +3 -0
- package/dist/shell/renameObject.js +25 -0
- package/dist/shell/theme.d.ts +36 -0
- package/dist/shell/theme.js +37 -0
- package/dist/shell/ui.d.ts +84 -0
- package/dist/shell/ui.js +90 -0
- package/dist/shell/useGlbThumbnail.d.ts +8 -0
- package/dist/shell/useGlbThumbnail.js +19 -0
- package/dist/terrainReadoutStore.d.ts +19 -0
- package/dist/terrainReadoutStore.js +36 -0
- package/dist/uiStore.d.ts +33 -0
- package/dist/uiStore.js +13 -0
- package/dist/viewportContextMenu.d.ts +4 -2
- package/dist/viewportContextMenu.js +5 -1
- package/package.json +4 -4
- package/skills/jgengine/SKILL.md +35 -804
- package/skills/jgengine/api.md +190 -79
- package/skills/jgengine/capabilities.md +112 -2
- package/skills/jgengine/recipes/minimal-game.md +88 -0
- package/skills/jgengine-editor/SKILL.md +29 -362
- package/skills/jgengine-editor/api.md +519 -18
- package/skills/jgengine-editor/capabilities.md +91 -0
- package/skills/jgengine-editor/reference.md +113 -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/skills/jgengine/reference-orphans.md +0 -9
- package/skills/jgengine/reference-systems.md +0 -60
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { GizmoMode, PlacementTool, SnapMode } from "../uiStore.js";
|
|
2
|
+
import type { BottomDockTab } from "./layoutStore.js";
|
|
3
|
+
/** One executable command-palette entry. */
|
|
4
|
+
export interface PaletteCommand {
|
|
5
|
+
id: string;
|
|
6
|
+
title: string;
|
|
7
|
+
group: string;
|
|
8
|
+
kbd?: string;
|
|
9
|
+
keywords?: string;
|
|
10
|
+
run: () => void;
|
|
11
|
+
}
|
|
12
|
+
/** A scene object the palette can jump to by name/id. */
|
|
13
|
+
export interface PaletteObjectTarget {
|
|
14
|
+
id: string;
|
|
15
|
+
label: string;
|
|
16
|
+
kind: string;
|
|
17
|
+
}
|
|
18
|
+
/** Everything the palette can drive — thin callbacks over existing editor systems. */
|
|
19
|
+
export interface PaletteContext {
|
|
20
|
+
setGizmoMode(mode: GizmoMode): void;
|
|
21
|
+
setTool(tool: "select" | "terrain"): void;
|
|
22
|
+
toggleGrid(): void;
|
|
23
|
+
toggleContours(): void;
|
|
24
|
+
toggleSurfaceGrid(): void;
|
|
25
|
+
toggleElevation(): void;
|
|
26
|
+
setSnapMode(mode: SnapMode): void;
|
|
27
|
+
frameAll(): void;
|
|
28
|
+
frameSelection(): void;
|
|
29
|
+
undo(): void;
|
|
30
|
+
redo(): void;
|
|
31
|
+
save(): void;
|
|
32
|
+
exportJson(): void;
|
|
33
|
+
importJson(): void;
|
|
34
|
+
copyJson(): void;
|
|
35
|
+
setMode(mode: "play" | "walk" | "hud"): void;
|
|
36
|
+
startPlacement(tool: PlacementTool): void;
|
|
37
|
+
openBottomTab(tab: BottomDockTab): void;
|
|
38
|
+
toggleLeftDock(): void;
|
|
39
|
+
toggleRightDock(): void;
|
|
40
|
+
toggleHelp(): void;
|
|
41
|
+
resetLayout(): void;
|
|
42
|
+
/** Select + frame a document object by id (jump-to-object). */
|
|
43
|
+
gotoObject?(id: string): void;
|
|
44
|
+
/** Live scene objects for object-search rows. */
|
|
45
|
+
objects?: readonly PaletteObjectTarget[];
|
|
46
|
+
}
|
|
47
|
+
/** Builds the full palette command list from live editor capabilities — no dead entries. */
|
|
48
|
+
export declare function buildPaletteCommands(ctx: PaletteContext): PaletteCommand[];
|
|
49
|
+
/** Loads recent command ids from localStorage (session-shared across games). @internal */
|
|
50
|
+
export declare function loadRecentCommandIds(): string[];
|
|
51
|
+
/** Records a command id at the front of the recent list (deduped, capped). @internal */
|
|
52
|
+
export declare function pushRecentCommandId(id: string): string[];
|
|
53
|
+
/**
|
|
54
|
+
* Case-insensitive filter over title, group, and keywords.
|
|
55
|
+
* With an empty query, recent commands (that still exist) float to the top under a virtual order
|
|
56
|
+
* — the caller groups them visually.
|
|
57
|
+
*/
|
|
58
|
+
export declare function filterPaletteCommands(commands: readonly PaletteCommand[], query: string, recentIds?: readonly string[]): PaletteCommand[];
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { WELL_KNOWN_MARKER_KINDS } from "@jgengine/core/editor/index";
|
|
2
|
+
import { listSceneKinds } from "@jgengine/core/scene/sceneKinds";
|
|
3
|
+
/** Builds the full palette command list from live editor capabilities — no dead entries. */
|
|
4
|
+
export function buildPaletteCommands(ctx) {
|
|
5
|
+
const commands = [
|
|
6
|
+
{ id: "tool.select", title: "Select tool", group: "Tools", run: () => ctx.setTool("select") },
|
|
7
|
+
{ id: "tool.terrain", title: "Terrain sculpt/paint tool", group: "Tools", kbd: "T", run: () => ctx.setTool("terrain") },
|
|
8
|
+
{ id: "gizmo.translate", title: "Move gizmo", group: "Tools", kbd: "W", run: () => ctx.setGizmoMode("translate") },
|
|
9
|
+
{ id: "gizmo.rotate", title: "Rotate gizmo", group: "Tools", kbd: "E", run: () => ctx.setGizmoMode("rotate") },
|
|
10
|
+
{ id: "gizmo.scale", title: "Scale gizmo", group: "Tools", kbd: "R", run: () => ctx.setGizmoMode("scale") },
|
|
11
|
+
{ id: "snap.ground", title: "Snap to ground", group: "Snapping", run: () => ctx.setSnapMode("ground") },
|
|
12
|
+
{ id: "snap.grid", title: "Snap to grid", group: "Snapping", run: () => ctx.setSnapMode("grid") },
|
|
13
|
+
{ id: "snap.off", title: "Snapping off", group: "Snapping", run: () => ctx.setSnapMode("off") },
|
|
14
|
+
{ id: "view.grid", title: "Toggle reference grid", group: "View", kbd: "G", run: () => ctx.toggleGrid() },
|
|
15
|
+
{ id: "view.contours", title: "Toggle elevation contours", group: "View", run: () => ctx.toggleContours() },
|
|
16
|
+
{ id: "view.drape", title: "Toggle terrain-draped grid", group: "View", run: () => ctx.toggleSurfaceGrid() },
|
|
17
|
+
{ id: "view.elevation", title: "Toggle elevation readout", group: "View", run: () => ctx.toggleElevation() },
|
|
18
|
+
{ id: "view.frameAll", title: "Frame whole scene", group: "View", kbd: "F", run: () => ctx.frameAll() },
|
|
19
|
+
{ id: "view.frameSelection", title: "Frame selection", group: "View", kbd: "F", run: () => ctx.frameSelection() },
|
|
20
|
+
{ id: "edit.undo", title: "Undo", group: "Edit", kbd: "Ctrl+Z", run: () => ctx.undo() },
|
|
21
|
+
{ id: "edit.redo", title: "Redo", group: "Edit", kbd: "Ctrl+Y", run: () => ctx.redo() },
|
|
22
|
+
{ id: "doc.save", title: "Save scene", group: "Document", kbd: "Ctrl+S", run: () => ctx.save() },
|
|
23
|
+
{ id: "doc.export", title: "Export scene JSON", group: "Document", run: () => ctx.exportJson() },
|
|
24
|
+
{ id: "doc.import", title: "Import scene JSON…", group: "Document", run: () => ctx.importJson() },
|
|
25
|
+
{ id: "doc.copy", title: "Copy scene JSON to clipboard", group: "Document", run: () => ctx.copyJson() },
|
|
26
|
+
{ id: "mode.play", title: "Enter Play mode", group: "Modes", kbd: "F2+E", run: () => ctx.setMode("play") },
|
|
27
|
+
{ id: "mode.walk", title: "Enter Walk mode", group: "Modes", run: () => ctx.setMode("walk") },
|
|
28
|
+
{ id: "mode.hud", title: "Lay out HUD panels", group: "Modes", run: () => ctx.setMode("hud") },
|
|
29
|
+
{ id: "panel.content", title: "Open Content Browser", group: "Panels", kbd: "Ctrl+B", run: () => ctx.openBottomTab("content") },
|
|
30
|
+
{ id: "panel.console", title: "Open Console", group: "Panels", run: () => ctx.openBottomTab("console") },
|
|
31
|
+
{ id: "panel.profiler", title: "Open Profiler", group: "Panels", run: () => ctx.openBottomTab("profiler") },
|
|
32
|
+
{ id: "panel.animation", title: "Open Animation (path flythrough)", group: "Panels", keywords: "timeline path scrub", run: () => ctx.openBottomTab("animation") },
|
|
33
|
+
{ id: "panel.assistant", title: "Open AI Assistant", group: "Panels", run: () => ctx.openBottomTab("assistant") },
|
|
34
|
+
{ id: "panel.left", title: "Toggle hierarchy panel", group: "Panels", run: () => ctx.toggleLeftDock() },
|
|
35
|
+
{ id: "panel.right", title: "Toggle inspector panel", group: "Panels", run: () => ctx.toggleRightDock() },
|
|
36
|
+
{ id: "panel.resetLayout", title: "Reset layout to defaults", group: "Panels", run: () => ctx.resetLayout() },
|
|
37
|
+
{ id: "help.shortcuts", title: "Keyboard shortcuts", group: "Help", kbd: "?", run: () => ctx.toggleHelp() },
|
|
38
|
+
];
|
|
39
|
+
for (const kind of WELL_KNOWN_MARKER_KINDS) {
|
|
40
|
+
commands.push({
|
|
41
|
+
id: `add.marker.${kind}`,
|
|
42
|
+
title: `Add marker: ${kind}`,
|
|
43
|
+
group: "Add",
|
|
44
|
+
keywords: "place create marker",
|
|
45
|
+
run: () => ctx.startPlacement({ tool: "marker", kind }),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
for (const definition of listSceneKinds()) {
|
|
49
|
+
if (definition.addCategory === undefined)
|
|
50
|
+
continue;
|
|
51
|
+
commands.push({
|
|
52
|
+
id: `add.studio.${definition.kind}`,
|
|
53
|
+
title: `Add ${definition.label}`,
|
|
54
|
+
group: "Add",
|
|
55
|
+
keywords: "place create studio",
|
|
56
|
+
run: () => ctx.startPlacement(definition.target === "path"
|
|
57
|
+
? { tool: "path", kind: definition.kind }
|
|
58
|
+
: definition.target === "volume"
|
|
59
|
+
? { tool: "volume", kind: definition.kind, shape: "box" }
|
|
60
|
+
: { tool: "marker", kind: definition.kind }),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
commands.push({ id: "add.path", title: "Draw path (route)", group: "Add", keywords: "place create", run: () => ctx.startPlacement({ tool: "path", kind: "route" }) }, { id: "add.road", title: "Draw road", group: "Add", keywords: "place create", run: () => ctx.startPlacement({ tool: "path", kind: "road" }) }, { id: "add.zone", title: "Add zone volume", group: "Add", keywords: "place create volume", run: () => ctx.startPlacement({ tool: "volume", kind: "zone", shape: "sphere" }) }, { id: "add.note", title: "Add note", group: "Add", keywords: "place create annotation", run: () => ctx.startPlacement({ tool: "note" }) });
|
|
64
|
+
if (ctx.gotoObject !== undefined && ctx.objects !== undefined) {
|
|
65
|
+
for (const object of ctx.objects) {
|
|
66
|
+
commands.push({
|
|
67
|
+
id: `goto.object.${object.id}`,
|
|
68
|
+
title: object.label,
|
|
69
|
+
group: "Objects",
|
|
70
|
+
keywords: `jump goto select frame ${object.kind} ${object.id}`,
|
|
71
|
+
run: () => ctx.gotoObject(object.id),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return commands;
|
|
76
|
+
}
|
|
77
|
+
const RECENT_KEY = "jgeditor:palette-recent";
|
|
78
|
+
const RECENT_LIMIT = 8;
|
|
79
|
+
/** Loads recent command ids from localStorage (session-shared across games). @internal */
|
|
80
|
+
export function loadRecentCommandIds() {
|
|
81
|
+
try {
|
|
82
|
+
const raw = localStorage.getItem(RECENT_KEY);
|
|
83
|
+
if (raw === null)
|
|
84
|
+
return [];
|
|
85
|
+
const parsed = JSON.parse(raw);
|
|
86
|
+
if (!Array.isArray(parsed))
|
|
87
|
+
return [];
|
|
88
|
+
return parsed.filter((entry) => typeof entry === "string").slice(0, RECENT_LIMIT);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/** Records a command id at the front of the recent list (deduped, capped). @internal */
|
|
95
|
+
export function pushRecentCommandId(id) {
|
|
96
|
+
// Object jump rows are scene-specific and flood the recent list — skip them.
|
|
97
|
+
if (id.startsWith("goto.object."))
|
|
98
|
+
return loadRecentCommandIds();
|
|
99
|
+
const next = [id, ...loadRecentCommandIds().filter((entry) => entry !== id)].slice(0, RECENT_LIMIT);
|
|
100
|
+
try {
|
|
101
|
+
localStorage.setItem(RECENT_KEY, JSON.stringify(next));
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// ignore quota / private mode
|
|
105
|
+
}
|
|
106
|
+
return next;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Case-insensitive filter over title, group, and keywords.
|
|
110
|
+
* With an empty query, recent commands (that still exist) float to the top under a virtual order
|
|
111
|
+
* — the caller groups them visually.
|
|
112
|
+
*/
|
|
113
|
+
export function filterPaletteCommands(commands, query, recentIds = []) {
|
|
114
|
+
const needle = query.trim().toLowerCase();
|
|
115
|
+
if (needle.length === 0) {
|
|
116
|
+
if (recentIds.length === 0)
|
|
117
|
+
return [...commands];
|
|
118
|
+
const byId = new Map(commands.map((command) => [command.id, command]));
|
|
119
|
+
const recent = [];
|
|
120
|
+
for (const id of recentIds) {
|
|
121
|
+
const command = byId.get(id);
|
|
122
|
+
if (command !== undefined)
|
|
123
|
+
recent.push({ ...command, group: "Recent" });
|
|
124
|
+
}
|
|
125
|
+
const recentSet = new Set(recent.map((command) => command.id));
|
|
126
|
+
return [...recent, ...commands.filter((command) => !recentSet.has(command.id))];
|
|
127
|
+
}
|
|
128
|
+
const terms = needle.split(/\s+/);
|
|
129
|
+
return commands.filter((command) => {
|
|
130
|
+
const haystack = `${command.title} ${command.group} ${command.keywords ?? ""}`.toLowerCase();
|
|
131
|
+
return terms.every((term) => haystack.includes(term));
|
|
132
|
+
});
|
|
133
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ConsoleSeverity } from "./consoleStore.js";
|
|
2
|
+
type ConsoleSink = (severity: ConsoleSeverity, source: string, message: string) => void;
|
|
3
|
+
/**
|
|
4
|
+
* Installs the global console sink (typically the dock console store). Returns a disposer that
|
|
5
|
+
* clears the sink only if it still points at this callback.
|
|
6
|
+
* @internal
|
|
7
|
+
*/
|
|
8
|
+
export declare function installEditorConsoleSink(next: ConsoleSink): () => void;
|
|
9
|
+
/** Forwards a real editor event to the installed console sink, if any. @internal */
|
|
10
|
+
export declare function emitEditorConsole(severity: ConsoleSeverity, source: string, message: string): void;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
let sink = null;
|
|
2
|
+
/**
|
|
3
|
+
* Installs the global console sink (typically the dock console store). Returns a disposer that
|
|
4
|
+
* clears the sink only if it still points at this callback.
|
|
5
|
+
* @internal
|
|
6
|
+
*/
|
|
7
|
+
export function installEditorConsoleSink(next) {
|
|
8
|
+
sink = next;
|
|
9
|
+
return () => {
|
|
10
|
+
if (sink === next)
|
|
11
|
+
sink = null;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/** Forwards a real editor event to the installed console sink, if any. @internal */
|
|
15
|
+
export function emitEditorConsole(severity, source, message) {
|
|
16
|
+
sink?.(severity, source, message);
|
|
17
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded editor event log backing the Console dock tab. Only real editor events are recorded —
|
|
3
|
+
* notifications, save/import/export results, RPC and agent errors — never fabricated runtime logs.
|
|
4
|
+
*/
|
|
5
|
+
/** Console entry severity, driving the filter chips and row tinting. */
|
|
6
|
+
export type ConsoleSeverity = "info" | "warning" | "error";
|
|
7
|
+
/** One recorded editor event. */
|
|
8
|
+
export interface ConsoleEntry {
|
|
9
|
+
id: number;
|
|
10
|
+
at: number;
|
|
11
|
+
severity: ConsoleSeverity;
|
|
12
|
+
/** Which editor subsystem produced the event (ui, save, import, agent, rpc). */
|
|
13
|
+
source: string;
|
|
14
|
+
message: string;
|
|
15
|
+
}
|
|
16
|
+
/** Subscribable, bounded editor event log. */
|
|
17
|
+
export interface EditorConsoleStore {
|
|
18
|
+
getEntries(): readonly ConsoleEntry[];
|
|
19
|
+
log(severity: ConsoleSeverity, source: string, message: string): void;
|
|
20
|
+
clear(): void;
|
|
21
|
+
subscribe(listener: () => void): () => void;
|
|
22
|
+
counts(): Record<ConsoleSeverity, number>;
|
|
23
|
+
}
|
|
24
|
+
/** Entries kept before the oldest are dropped. */
|
|
25
|
+
export declare const CONSOLE_CAPACITY = 500;
|
|
26
|
+
/** Creates the editor console store. `now` is injectable for tests. */
|
|
27
|
+
export declare function createEditorConsoleStore(now?: () => number): EditorConsoleStore;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded editor event log backing the Console dock tab. Only real editor events are recorded —
|
|
3
|
+
* notifications, save/import/export results, RPC and agent errors — never fabricated runtime logs.
|
|
4
|
+
*/
|
|
5
|
+
/** Entries kept before the oldest are dropped. */
|
|
6
|
+
export const CONSOLE_CAPACITY = 500;
|
|
7
|
+
/** Creates the editor console store. `now` is injectable for tests. */
|
|
8
|
+
export function createEditorConsoleStore(now = () => Date.now()) {
|
|
9
|
+
let entries = [];
|
|
10
|
+
let nextId = 1;
|
|
11
|
+
const listeners = new Set();
|
|
12
|
+
const emit = () => {
|
|
13
|
+
for (const listener of listeners)
|
|
14
|
+
listener();
|
|
15
|
+
};
|
|
16
|
+
return {
|
|
17
|
+
getEntries: () => entries,
|
|
18
|
+
log(severity, source, message) {
|
|
19
|
+
entries = [...entries, { id: nextId++, at: now(), severity, source, message }];
|
|
20
|
+
if (entries.length > CONSOLE_CAPACITY)
|
|
21
|
+
entries = entries.slice(entries.length - CONSOLE_CAPACITY);
|
|
22
|
+
emit();
|
|
23
|
+
},
|
|
24
|
+
clear() {
|
|
25
|
+
if (entries.length === 0)
|
|
26
|
+
return;
|
|
27
|
+
entries = [];
|
|
28
|
+
emit();
|
|
29
|
+
},
|
|
30
|
+
subscribe(listener) {
|
|
31
|
+
listeners.add(listener);
|
|
32
|
+
return () => {
|
|
33
|
+
listeners.delete(listener);
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
counts() {
|
|
37
|
+
const counts = { info: 0, warning: 0, error: 0 };
|
|
38
|
+
for (const entry of entries)
|
|
39
|
+
counts[entry.severity] += 1;
|
|
40
|
+
return counts;
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Polished numeric field with an axis-colored label chip. The whole field is a drag-scrub
|
|
3
|
+
* surface (not just the chip): horizontal pointer-drag adjusts by `step` per few pixels while
|
|
4
|
+
* Alt/Option or double-click focuses the input for precise typing. The input commits finite
|
|
5
|
+
* values on change and re-normalizes its text on blur. Invalid text never commits.
|
|
6
|
+
*/
|
|
7
|
+
export declare function AxisNumberField({ axis, label, value, onCommit, step, precision, disabled, mixed, }: {
|
|
8
|
+
/** Axis coloring; omit for a neutral field. */
|
|
9
|
+
axis?: "x" | "y" | "z";
|
|
10
|
+
label: string;
|
|
11
|
+
value: number;
|
|
12
|
+
onCommit: (value: number) => void;
|
|
13
|
+
step?: number;
|
|
14
|
+
precision?: number;
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
/** Multi-select mixed values: show an em dash until the user commits a concrete number. */
|
|
17
|
+
mixed?: boolean;
|
|
18
|
+
}): import("react").JSX.Element;
|
|
19
|
+
/** Labeled row wrapper aligning a caption with one or more fields. */
|
|
20
|
+
export declare function FieldRow({ label, children, title }: {
|
|
21
|
+
label: string;
|
|
22
|
+
children: React.ReactNode;
|
|
23
|
+
title?: string;
|
|
24
|
+
}): import("react").JSX.Element;
|
|
25
|
+
/** Compact text input row used by the inspector header and component cards. */
|
|
26
|
+
export declare function TextField({ label, value, placeholder, onCommit, }: {
|
|
27
|
+
label: string;
|
|
28
|
+
value: string;
|
|
29
|
+
placeholder?: string;
|
|
30
|
+
onCommit: (value: string) => void;
|
|
31
|
+
}): import("react").JSX.Element;
|
|
32
|
+
/** Small ghost action used inside section headers (reset, link). */
|
|
33
|
+
export declare function SectionAction({ label, onClick, active, disabled, children, }: {
|
|
34
|
+
label: string;
|
|
35
|
+
onClick: () => void;
|
|
36
|
+
active?: boolean;
|
|
37
|
+
disabled?: boolean;
|
|
38
|
+
children: React.ReactNode;
|
|
39
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import { AXIS_COLORS, FOCUS_RING, INPUT_CLS, NUMERIC } from "./theme.js";
|
|
4
|
+
/** Pixels of pointer drag per `step` of value change when scrubbing an axis chip. */
|
|
5
|
+
const DRAG_PX_PER_STEP = 4;
|
|
6
|
+
function formatValue(value, precision) {
|
|
7
|
+
const fixed = value.toFixed(precision);
|
|
8
|
+
// Trim trailing zeros but keep at least one decimal digit ("1.50" -> "1.5", "2.00" -> "2").
|
|
9
|
+
return fixed.replace(/\.?0+$/, "") || "0";
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Polished numeric field with an axis-colored label chip. The whole field is a drag-scrub
|
|
13
|
+
* surface (not just the chip): horizontal pointer-drag adjusts by `step` per few pixels while
|
|
14
|
+
* Alt/Option or double-click focuses the input for precise typing. The input commits finite
|
|
15
|
+
* values on change and re-normalizes its text on blur. Invalid text never commits.
|
|
16
|
+
*/
|
|
17
|
+
export function AxisNumberField({ axis, label, value, onCommit, step = 0.1, precision = 3, disabled = false, mixed = false, }) {
|
|
18
|
+
const [text, setText] = useState(() => (mixed ? "—" : formatValue(value, precision)));
|
|
19
|
+
const [editing, setEditing] = useState(false);
|
|
20
|
+
const inputRef = useRef(null);
|
|
21
|
+
const dragRef = useRef(null);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (!editing)
|
|
24
|
+
setText(mixed ? "—" : formatValue(value, precision));
|
|
25
|
+
}, [value, precision, editing, mixed]);
|
|
26
|
+
const commitText = (raw) => {
|
|
27
|
+
if (raw === "—" || raw.trim().length === 0)
|
|
28
|
+
return;
|
|
29
|
+
const next = Number(raw);
|
|
30
|
+
if (Number.isFinite(next) && (mixed || next !== value))
|
|
31
|
+
onCommit(next);
|
|
32
|
+
};
|
|
33
|
+
const onScrubPointerDown = (event) => {
|
|
34
|
+
if (disabled || editing)
|
|
35
|
+
return;
|
|
36
|
+
// Alt/Option starts text edit instead of scrubbing.
|
|
37
|
+
if (event.altKey) {
|
|
38
|
+
inputRef.current?.focus();
|
|
39
|
+
inputRef.current?.select();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
event.preventDefault();
|
|
43
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
44
|
+
// Scrub from the first selected value when mixed — committing absolute values to all.
|
|
45
|
+
dragRef.current = { pointerId: event.pointerId, startX: event.clientX, startValue: value, moved: false };
|
|
46
|
+
};
|
|
47
|
+
const onScrubPointerMove = (event) => {
|
|
48
|
+
const drag = dragRef.current;
|
|
49
|
+
if (drag === null || drag.pointerId !== event.pointerId)
|
|
50
|
+
return;
|
|
51
|
+
const deltaSteps = Math.round((event.clientX - drag.startX) / DRAG_PX_PER_STEP);
|
|
52
|
+
if (deltaSteps === 0 && !drag.moved)
|
|
53
|
+
return;
|
|
54
|
+
drag.moved = true;
|
|
55
|
+
const next = drag.startValue + deltaSteps * step;
|
|
56
|
+
const rounded = Number(next.toFixed(precision));
|
|
57
|
+
if (rounded !== value)
|
|
58
|
+
onCommit(rounded);
|
|
59
|
+
};
|
|
60
|
+
const onScrubPointerUp = (event) => {
|
|
61
|
+
const drag = dragRef.current;
|
|
62
|
+
if (drag === null || drag.pointerId !== event.pointerId)
|
|
63
|
+
return;
|
|
64
|
+
dragRef.current = null;
|
|
65
|
+
// Click without drag focuses the input for precise typing (whole-field scrub UX).
|
|
66
|
+
if (!drag.moved) {
|
|
67
|
+
inputRef.current?.focus();
|
|
68
|
+
inputRef.current?.select();
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const colors = axis !== undefined ? AXIS_COLORS[axis] : null;
|
|
72
|
+
return (_jsxs("label", { title: `Drag to adjust ${label} · Alt-click or focus input to type`, onPointerDown: onScrubPointerDown, onPointerMove: onScrubPointerMove, onPointerUp: onScrubPointerUp, className: `flex min-w-0 flex-1 cursor-ew-resize items-center overflow-hidden rounded-[5px] border border-white/[0.08] bg-black/30 focus-within:border-cyan-400/50 ${editing ? "cursor-text" : ""}`, children: [_jsxs("span", { className: `relative flex h-6.5 w-6 shrink-0 select-none items-center justify-center border-r border-white/[0.06] bg-white/[0.03] text-[10px] font-semibold uppercase ${colors !== null ? colors.text : "text-neutral-500"}`, children: [colors !== null ? _jsx("span", { className: `absolute inset-y-0 left-0 w-0.5 ${colors.bar}` }) : null, label] }), _jsx("input", { ref: inputRef, type: "text", inputMode: "decimal", value: text, disabled: disabled, "aria-label": label, onFocus: () => setEditing(true), onDoubleClick: (event) => {
|
|
73
|
+
event.stopPropagation();
|
|
74
|
+
event.currentTarget.select();
|
|
75
|
+
}, onChange: (event) => {
|
|
76
|
+
setText(event.target.value);
|
|
77
|
+
commitText(event.target.value);
|
|
78
|
+
}, onBlur: (event) => {
|
|
79
|
+
setEditing(false);
|
|
80
|
+
commitText(event.target.value);
|
|
81
|
+
setText(formatValue(Number.isFinite(Number(event.target.value)) ? Number(event.target.value) : value, precision));
|
|
82
|
+
}, onKeyDown: (event) => {
|
|
83
|
+
if (event.key === "Enter")
|
|
84
|
+
event.currentTarget.blur();
|
|
85
|
+
if (event.key === "ArrowUp" || event.key === "ArrowDown") {
|
|
86
|
+
event.preventDefault();
|
|
87
|
+
const direction = event.key === "ArrowUp" ? 1 : -1;
|
|
88
|
+
const next = Number((value + direction * step).toFixed(precision));
|
|
89
|
+
onCommit(next);
|
|
90
|
+
}
|
|
91
|
+
}, className: `h-6.5 w-full min-w-0 cursor-text bg-transparent px-1.5 text-right text-[11px] text-neutral-200 outline-none ${NUMERIC} disabled:opacity-40` })] }));
|
|
92
|
+
}
|
|
93
|
+
/** Labeled row wrapper aligning a caption with one or more fields. */
|
|
94
|
+
export function FieldRow({ label, children, title }) {
|
|
95
|
+
return (_jsxs("div", { className: "flex items-center gap-2", title: title, children: [_jsx("span", { className: "w-16 shrink-0 text-[10px] text-neutral-500", children: label }), _jsx("div", { className: "flex min-w-0 flex-1 items-center gap-1", children: children })] }));
|
|
96
|
+
}
|
|
97
|
+
/** Compact text input row used by the inspector header and component cards. */
|
|
98
|
+
export function TextField({ label, value, placeholder, onCommit, }) {
|
|
99
|
+
return (_jsx(FieldRow, { label: label, children: _jsx("input", { className: `h-6.5 w-full min-w-0 px-2 ${INPUT_CLS}`, value: value, placeholder: placeholder, "aria-label": label, onChange: (event) => onCommit(event.target.value) }) }));
|
|
100
|
+
}
|
|
101
|
+
/** Small ghost action used inside section headers (reset, link). */
|
|
102
|
+
export function SectionAction({ label, onClick, active = false, disabled = false, children, }) {
|
|
103
|
+
return (_jsx("button", { type: "button", title: label, "aria-label": label, "aria-pressed": active, disabled: disabled, onClick: onClick, className: `flex h-5 items-center justify-center rounded-[4px] px-1 text-[10px] transition-colors ${FOCUS_RING} ${active ? "bg-cyan-500/20 text-cyan-200" : "text-neutral-500 hover:bg-white/[0.08] hover:text-neutral-200"} disabled:pointer-events-none disabled:opacity-35`, children: children }));
|
|
104
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Formats a past timestamp as a short relative phrase ("just now", "2m ago", "3h ago"). @internal */
|
|
2
|
+
export function formatSavedRelative(savedAtMs, nowMs = Date.now()) {
|
|
3
|
+
const elapsedSec = Math.max(0, Math.floor((nowMs - savedAtMs) / 1000));
|
|
4
|
+
if (elapsedSec < 45)
|
|
5
|
+
return "just now";
|
|
6
|
+
const elapsedMin = Math.floor(elapsedSec / 60);
|
|
7
|
+
if (elapsedMin < 60)
|
|
8
|
+
return `${elapsedMin}m ago`;
|
|
9
|
+
const elapsedHr = Math.floor(elapsedMin / 60);
|
|
10
|
+
if (elapsedHr < 48)
|
|
11
|
+
return `${elapsedHr}h ago`;
|
|
12
|
+
const elapsedDay = Math.floor(elapsedHr / 24);
|
|
13
|
+
return `${elapsedDay}d ago`;
|
|
14
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offscreen GLB → PNG thumbnail cache for the Content Browser.
|
|
3
|
+
* Renders real model bytes once per URL into a data URL; never invents screenshots.
|
|
4
|
+
* Failures stay honest (glyph fallback). Bounded concurrency + cache eviction keep
|
|
5
|
+
* the editor from thrashing WebGL or retaining unbounded GPU memory.
|
|
6
|
+
* @internal
|
|
7
|
+
*/
|
|
8
|
+
import * as THREE from "three";
|
|
9
|
+
/** Lifecycle of one GLB thumbnail render: idle → loading → ready, or error. */
|
|
10
|
+
export type ThumbnailStatus = "idle" | "loading" | "ready" | "error";
|
|
11
|
+
/** Cache entry for one asset's thumbnail: status plus the data URL when ready. */
|
|
12
|
+
export interface ThumbnailState {
|
|
13
|
+
status: ThumbnailStatus;
|
|
14
|
+
/** `data:image/png;base64,…` when ready; otherwise null. */
|
|
15
|
+
dataUrl: string | null;
|
|
16
|
+
}
|
|
17
|
+
/** @internal */
|
|
18
|
+
export declare function setGlbThumbnailLoader(fn: (url: string) => Promise<THREE.Object3D>): void;
|
|
19
|
+
/** @internal Restore production loader after tests. */
|
|
20
|
+
export declare function resetGlbThumbnailLoader(): void;
|
|
21
|
+
/** @internal Clear cache + dispose GPU resources (tests / hot dispose). */
|
|
22
|
+
export declare function clearGlbThumbnailCache(): void;
|
|
23
|
+
/**
|
|
24
|
+
* Place the camera so the object's bounding sphere fills most of the frame from a
|
|
25
|
+
* consistent three-quarter angle. Pure math — unit-tested without WebGL.
|
|
26
|
+
* @internal
|
|
27
|
+
*/
|
|
28
|
+
export declare function frameCameraForBounds(box: THREE.Box3, camera: THREE.PerspectiveCamera, size?: number): void;
|
|
29
|
+
/**
|
|
30
|
+
* Snapshot of cache state for a model URL. Does not start a render — call
|
|
31
|
+
* {@link requestGlbThumbnail} first (the React hook does both).
|
|
32
|
+
* @internal
|
|
33
|
+
*/
|
|
34
|
+
export declare function getGlbThumbnailState(url: string | undefined): ThumbnailState;
|
|
35
|
+
/**
|
|
36
|
+
* Ensures a thumbnail render is in flight (or already cached) for `url`.
|
|
37
|
+
* Safe to call repeatedly; concurrent callers share one load.
|
|
38
|
+
* @internal
|
|
39
|
+
*/
|
|
40
|
+
export declare function requestGlbThumbnail(url: string): void;
|
|
41
|
+
/**
|
|
42
|
+
* Subscribe to thumbnail state changes for a URL. Returns an unsubscribe.
|
|
43
|
+
* @internal
|
|
44
|
+
*/
|
|
45
|
+
export declare function subscribeGlbThumbnail(url: string | undefined, listener: () => void): () => void;
|