@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,54 @@
|
|
|
1
|
+
import { pathFollowSeek, pathLength, } from "@jgengine/core/nav/pathFollow";
|
|
2
|
+
/**
|
|
3
|
+
* Builds the path-follow config used by the flythrough scrubber. Pure; no document mutation.
|
|
4
|
+
* Returns null when the path cannot be sampled (fewer than two points).
|
|
5
|
+
*
|
|
6
|
+
* @internal
|
|
7
|
+
*/
|
|
8
|
+
export function pathFollowConfigFromEditorPath(path) {
|
|
9
|
+
if (path.points.length < 2)
|
|
10
|
+
return null;
|
|
11
|
+
const waypoints = path.points.map((point) => [point.x, point.y, point.z]);
|
|
12
|
+
return { waypoints, speed: 1 };
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Lists document paths that can be scrubbed, with length metadata for the dock UI.
|
|
16
|
+
*
|
|
17
|
+
* @internal
|
|
18
|
+
*/
|
|
19
|
+
export function listScrubbablePaths(paths) {
|
|
20
|
+
return paths
|
|
21
|
+
.map((path) => {
|
|
22
|
+
const config = pathFollowConfigFromEditorPath(path);
|
|
23
|
+
const length = config === null ? 0 : pathLength(config);
|
|
24
|
+
return {
|
|
25
|
+
id: path.id,
|
|
26
|
+
kind: path.kind,
|
|
27
|
+
label: path.label?.trim() || path.id,
|
|
28
|
+
pointCount: path.points.length,
|
|
29
|
+
length,
|
|
30
|
+
};
|
|
31
|
+
})
|
|
32
|
+
.filter((entry) => entry.pointCount >= 2);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Samples a world position along an authored path at normalized progress `t` in `[0, 1]`.
|
|
36
|
+
* Clamps `t`. Returns null when the path has fewer than two points.
|
|
37
|
+
*
|
|
38
|
+
* @internal
|
|
39
|
+
*/
|
|
40
|
+
export function samplePathAt(path, t) {
|
|
41
|
+
const config = pathFollowConfigFromEditorPath(path);
|
|
42
|
+
if (config === null)
|
|
43
|
+
return null;
|
|
44
|
+
const length = pathLength(config);
|
|
45
|
+
const clamped = Number.isFinite(t) ? Math.max(0, Math.min(1, t)) : 0;
|
|
46
|
+
const state = pathFollowSeek(config, { kind: "normalized", value: clamped });
|
|
47
|
+
return {
|
|
48
|
+
x: state.position[0],
|
|
49
|
+
y: state.position[1],
|
|
50
|
+
z: state.position[2],
|
|
51
|
+
distance: length * clamped,
|
|
52
|
+
length,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { EditorPerfSample } from "../session.js";
|
|
2
|
+
/**
|
|
3
|
+
* Rolling history of real {@link EditorPerfSample}s for the Profiler dock tab's frame-time graph.
|
|
4
|
+
* Fed by the same 500ms host poll the toolbar pill uses; never fabricates values. Kept outside
|
|
5
|
+
* React state so recording continues while the profiler tab is closed without rerendering anything.
|
|
6
|
+
*/
|
|
7
|
+
export interface PerfHistoryStore {
|
|
8
|
+
push(sample: EditorPerfSample): void;
|
|
9
|
+
getSamples(): readonly EditorPerfSample[];
|
|
10
|
+
clear(): void;
|
|
11
|
+
setPaused(paused: boolean): void;
|
|
12
|
+
isPaused(): boolean;
|
|
13
|
+
subscribe(listener: () => void): () => void;
|
|
14
|
+
}
|
|
15
|
+
/** Samples retained (~2 minutes at the 500ms poll). */
|
|
16
|
+
export declare const PERF_HISTORY_CAPACITY = 240;
|
|
17
|
+
/** Creates the rolling perf-sample history store. */
|
|
18
|
+
export declare function createPerfHistoryStore(): PerfHistoryStore;
|
|
19
|
+
/** Mean of a numeric series, 0 for empty input. */
|
|
20
|
+
export declare function seriesAverage(values: readonly number[]): number;
|
|
21
|
+
/**
|
|
22
|
+
* Mean of defined values only — used for optional series (`simMs` / `outsideMs` / `memoryMb`)
|
|
23
|
+
* so zeros from absent samples do not drag the average down.
|
|
24
|
+
*
|
|
25
|
+
* @internal
|
|
26
|
+
*/
|
|
27
|
+
export declare function seriesAverageDefined(values: readonly (number | undefined)[]): number;
|
|
28
|
+
/** Builds an SVG polyline `points` string for a series scaled into a width×height box. */
|
|
29
|
+
export declare function sparklinePoints(values: readonly number[], width: number, height: number, maxValue: number): string;
|
|
30
|
+
/** Minimal frame-stats shape consumed when folding core `devtools.frame.stats()` into a sample. */
|
|
31
|
+
export interface FrameBudgetStatsInput {
|
|
32
|
+
avgSimMs: number;
|
|
33
|
+
avgOutsideMs: number;
|
|
34
|
+
phases: readonly {
|
|
35
|
+
name: string;
|
|
36
|
+
avgMs: number;
|
|
37
|
+
}[];
|
|
38
|
+
}
|
|
39
|
+
/** Optional sim / outside / phase fields derived from real frame-tracker stats (never invented). */
|
|
40
|
+
export interface FrameBudgetFields {
|
|
41
|
+
simMs: number;
|
|
42
|
+
outsideMs: number;
|
|
43
|
+
phases?: readonly {
|
|
44
|
+
name: string;
|
|
45
|
+
avgMs: number;
|
|
46
|
+
}[];
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Map core `devtools.frame.stats()` into optional editor sample fields.
|
|
50
|
+
* Returns `null` when the frame tracker has no samples yet — callers must omit the series entirely.
|
|
51
|
+
*
|
|
52
|
+
* @internal
|
|
53
|
+
*/
|
|
54
|
+
export declare function frameBudgetFromStats(stats: FrameBudgetStatsInput | null): FrameBudgetFields | null;
|
|
55
|
+
/**
|
|
56
|
+
* True when any sample carries real sim/outside budget fields from the frame tracker.
|
|
57
|
+
*
|
|
58
|
+
* @internal
|
|
59
|
+
*/
|
|
60
|
+
export declare function samplesHaveFrameBudget(samples: readonly EditorPerfSample[]): boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Latest non-empty phase list from the history (newest sample that reported phases).
|
|
63
|
+
* Empty when no named `measure()` marks have been recorded.
|
|
64
|
+
*
|
|
65
|
+
* @internal
|
|
66
|
+
*/
|
|
67
|
+
export declare function latestPhases(samples: readonly EditorPerfSample[]): readonly {
|
|
68
|
+
name: string;
|
|
69
|
+
avgMs: number;
|
|
70
|
+
}[];
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/** Samples retained (~2 minutes at the 500ms poll). */
|
|
2
|
+
export const PERF_HISTORY_CAPACITY = 240;
|
|
3
|
+
/** Creates the rolling perf-sample history store. */
|
|
4
|
+
export function createPerfHistoryStore() {
|
|
5
|
+
let samples = [];
|
|
6
|
+
let paused = false;
|
|
7
|
+
let lastSampledAt = -1;
|
|
8
|
+
const listeners = new Set();
|
|
9
|
+
const emit = () => {
|
|
10
|
+
for (const listener of listeners)
|
|
11
|
+
listener();
|
|
12
|
+
};
|
|
13
|
+
return {
|
|
14
|
+
push(sample) {
|
|
15
|
+
if (paused)
|
|
16
|
+
return;
|
|
17
|
+
// The host poll re-reads the same sample between probe windows; only record fresh ones.
|
|
18
|
+
if (sample.sampledAt === lastSampledAt)
|
|
19
|
+
return;
|
|
20
|
+
lastSampledAt = sample.sampledAt;
|
|
21
|
+
samples = [...samples, sample];
|
|
22
|
+
if (samples.length > PERF_HISTORY_CAPACITY)
|
|
23
|
+
samples = samples.slice(samples.length - PERF_HISTORY_CAPACITY);
|
|
24
|
+
emit();
|
|
25
|
+
},
|
|
26
|
+
getSamples: () => samples,
|
|
27
|
+
clear() {
|
|
28
|
+
samples = [];
|
|
29
|
+
lastSampledAt = -1;
|
|
30
|
+
emit();
|
|
31
|
+
},
|
|
32
|
+
setPaused(next) {
|
|
33
|
+
if (paused === next)
|
|
34
|
+
return;
|
|
35
|
+
paused = next;
|
|
36
|
+
emit();
|
|
37
|
+
},
|
|
38
|
+
isPaused: () => paused,
|
|
39
|
+
subscribe(listener) {
|
|
40
|
+
listeners.add(listener);
|
|
41
|
+
return () => {
|
|
42
|
+
listeners.delete(listener);
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** Mean of a numeric series, 0 for empty input. */
|
|
48
|
+
export function seriesAverage(values) {
|
|
49
|
+
if (values.length === 0)
|
|
50
|
+
return 0;
|
|
51
|
+
let sum = 0;
|
|
52
|
+
for (const value of values)
|
|
53
|
+
sum += value;
|
|
54
|
+
return sum / values.length;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Mean of defined values only — used for optional series (`simMs` / `outsideMs` / `memoryMb`)
|
|
58
|
+
* so zeros from absent samples do not drag the average down.
|
|
59
|
+
*
|
|
60
|
+
* @internal
|
|
61
|
+
*/
|
|
62
|
+
export function seriesAverageDefined(values) {
|
|
63
|
+
let sum = 0;
|
|
64
|
+
let count = 0;
|
|
65
|
+
for (const value of values) {
|
|
66
|
+
if (value === undefined)
|
|
67
|
+
continue;
|
|
68
|
+
sum += value;
|
|
69
|
+
count += 1;
|
|
70
|
+
}
|
|
71
|
+
return count === 0 ? 0 : sum / count;
|
|
72
|
+
}
|
|
73
|
+
/** Builds an SVG polyline `points` string for a series scaled into a width×height box. */
|
|
74
|
+
export function sparklinePoints(values, width, height, maxValue) {
|
|
75
|
+
if (values.length === 0 || maxValue <= 0)
|
|
76
|
+
return "";
|
|
77
|
+
const step = values.length > 1 ? width / (values.length - 1) : 0;
|
|
78
|
+
return values
|
|
79
|
+
.map((value, index) => {
|
|
80
|
+
const x = values.length > 1 ? index * step : width / 2;
|
|
81
|
+
const y = height - Math.min(1, value / maxValue) * height;
|
|
82
|
+
return `${x.toFixed(1)},${y.toFixed(1)}`;
|
|
83
|
+
})
|
|
84
|
+
.join(" ");
|
|
85
|
+
}
|
|
86
|
+
const MAX_PHASES_IN_SAMPLE = 6;
|
|
87
|
+
function roundMs(value) {
|
|
88
|
+
return Math.round(value * 100) / 100;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Map core `devtools.frame.stats()` into optional editor sample fields.
|
|
92
|
+
* Returns `null` when the frame tracker has no samples yet — callers must omit the series entirely.
|
|
93
|
+
*
|
|
94
|
+
* @internal
|
|
95
|
+
*/
|
|
96
|
+
export function frameBudgetFromStats(stats) {
|
|
97
|
+
if (stats === null)
|
|
98
|
+
return null;
|
|
99
|
+
const phases = stats.phases
|
|
100
|
+
.filter((phase) => Number.isFinite(phase.avgMs) && phase.avgMs > 0 && phase.name.length > 0)
|
|
101
|
+
.slice(0, MAX_PHASES_IN_SAMPLE)
|
|
102
|
+
.map((phase) => ({ name: phase.name, avgMs: roundMs(phase.avgMs) }));
|
|
103
|
+
return {
|
|
104
|
+
simMs: roundMs(stats.avgSimMs),
|
|
105
|
+
outsideMs: roundMs(stats.avgOutsideMs),
|
|
106
|
+
...(phases.length > 0 ? { phases } : {}),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* True when any sample carries real sim/outside budget fields from the frame tracker.
|
|
111
|
+
*
|
|
112
|
+
* @internal
|
|
113
|
+
*/
|
|
114
|
+
export function samplesHaveFrameBudget(samples) {
|
|
115
|
+
return samples.some((sample) => sample.simMs !== undefined && sample.outsideMs !== undefined);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Latest non-empty phase list from the history (newest sample that reported phases).
|
|
119
|
+
* Empty when no named `measure()` marks have been recorded.
|
|
120
|
+
*
|
|
121
|
+
* @internal
|
|
122
|
+
*/
|
|
123
|
+
export function latestPhases(samples) {
|
|
124
|
+
for (let index = samples.length - 1; index >= 0; index -= 1) {
|
|
125
|
+
const phases = samples[index]?.phases;
|
|
126
|
+
if (phases !== undefined && phases.length > 0)
|
|
127
|
+
return phases;
|
|
128
|
+
}
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { type EditorSession } from "@jgengine/core/editor/index";
|
|
2
|
+
/** Renames a placeable object by writing its label (notes use `text`). Returns false if unknown. @internal */
|
|
3
|
+
export declare function renameEditorObject(session: EditorSession, id: string, nextLabel: string): boolean;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { findEditorMarker, findEditorNote, findEditorPath, findEditorVolume, } from "@jgengine/core/editor/index";
|
|
2
|
+
/** Renames a placeable object by writing its label (notes use `text`). Returns false if unknown. @internal */
|
|
3
|
+
export function renameEditorObject(session, id, nextLabel) {
|
|
4
|
+
const label = nextLabel.trim();
|
|
5
|
+
if (label.length === 0)
|
|
6
|
+
return false;
|
|
7
|
+
const doc = session.getState().document;
|
|
8
|
+
if (findEditorMarker(doc, id) !== undefined) {
|
|
9
|
+
session.dispatch({ type: "setMarker", id, patch: { label } });
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
if (findEditorVolume(doc, id) !== undefined) {
|
|
13
|
+
session.dispatch({ type: "setVolume", id, patch: { label } });
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
if (findEditorPath(doc, id) !== undefined) {
|
|
17
|
+
session.dispatch({ type: "setPath", id, patch: { label } });
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
if (findEditorNote(doc, id) !== undefined) {
|
|
21
|
+
session.dispatch({ type: "setNote", id, patch: { text: label } });
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Editor shell design tokens as Tailwind class constants. One place owns the layered dark theme
|
|
3
|
+
* (app < panel < control elevation), text scale, and interaction states so every shell component
|
|
4
|
+
* reads identically. Class strings are complete literals so the host app's Tailwind scan keeps them.
|
|
5
|
+
*/
|
|
6
|
+
/** Application background — the near-black base everything sits on. */
|
|
7
|
+
export declare const APP_BG = "bg-[#0b0d10]";
|
|
8
|
+
/** Panel surface — one elevation step above the app background. */
|
|
9
|
+
export declare const PANEL_BG = "bg-[#111318]";
|
|
10
|
+
/** Elevated control surface — inputs, chips, dropdowns. */
|
|
11
|
+
export declare const CONTROL_BG = "bg-[#191d24]";
|
|
12
|
+
/** Floating layer surface — menus, palettes, popovers. */
|
|
13
|
+
export declare const FLOAT_BG = "bg-[#14171d]";
|
|
14
|
+
/** Low-contrast neutral border used between shell regions. */
|
|
15
|
+
export declare const BORDER = "border-white/[0.07]";
|
|
16
|
+
/** Standard control text row. */
|
|
17
|
+
export declare const TEXT = "text-[12px] text-neutral-300";
|
|
18
|
+
/** Muted metadata text. */
|
|
19
|
+
export declare const TEXT_MUTED = "text-[11px] text-neutral-500";
|
|
20
|
+
/** Micro uppercase section label. */
|
|
21
|
+
export declare const MICRO_LABEL = "text-[10px] font-semibold uppercase tracking-[0.12em] text-neutral-500";
|
|
22
|
+
/** Shared focus ring for keyboard navigation on interactive controls. */
|
|
23
|
+
export declare const FOCUS_RING = "outline-none focus-visible:ring-2 focus-visible:ring-cyan-400/60 focus-visible:ring-offset-0";
|
|
24
|
+
/** Base interactive control: compact, subtle border, hover elevation. */
|
|
25
|
+
export declare const CONTROL = "rounded-[5px] border border-white/[0.07] bg-[#191d24] text-neutral-300 transition-colors hover:bg-[#1f242d] hover:text-neutral-100 outline-none focus-visible:ring-2 focus-visible:ring-cyan-400/60 focus-visible:ring-offset-0";
|
|
26
|
+
/** Active/selected control state — restrained cyan. */
|
|
27
|
+
export declare const CONTROL_ACTIVE = "border-cyan-400/40 bg-cyan-500/15 text-cyan-200 hover:bg-cyan-500/20";
|
|
28
|
+
/** Text input styling shared across shell panels. */
|
|
29
|
+
export declare const INPUT_CLS = "rounded-[5px] border border-white/[0.08] bg-black/40 text-[12px] text-neutral-200 outline-none transition-colors placeholder:text-neutral-600 focus:border-cyan-400/50 focus:bg-black/60 outline-none focus-visible:ring-2 focus-visible:ring-cyan-400/60 focus-visible:ring-offset-0";
|
|
30
|
+
/** Tabular numerals for transforms, metrics, and timing readouts. */
|
|
31
|
+
export declare const NUMERIC = "tabular-nums";
|
|
32
|
+
/** Per-axis accent colors for transform fields (X red, Y green, Z blue — gizmo-matched). */
|
|
33
|
+
export declare const AXIS_COLORS: Record<"x" | "y" | "z", {
|
|
34
|
+
text: string;
|
|
35
|
+
bar: string;
|
|
36
|
+
}>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Editor shell design tokens as Tailwind class constants. One place owns the layered dark theme
|
|
3
|
+
* (app < panel < control elevation), text scale, and interaction states so every shell component
|
|
4
|
+
* reads identically. Class strings are complete literals so the host app's Tailwind scan keeps them.
|
|
5
|
+
*/
|
|
6
|
+
/** Application background — the near-black base everything sits on. */
|
|
7
|
+
export const APP_BG = "bg-[#0b0d10]";
|
|
8
|
+
/** Panel surface — one elevation step above the app background. */
|
|
9
|
+
export const PANEL_BG = "bg-[#111318]";
|
|
10
|
+
/** Elevated control surface — inputs, chips, dropdowns. */
|
|
11
|
+
export const CONTROL_BG = "bg-[#191d24]";
|
|
12
|
+
/** Floating layer surface — menus, palettes, popovers. */
|
|
13
|
+
export const FLOAT_BG = "bg-[#14171d]";
|
|
14
|
+
/** Low-contrast neutral border used between shell regions. */
|
|
15
|
+
export const BORDER = "border-white/[0.07]";
|
|
16
|
+
/** Standard control text row. */
|
|
17
|
+
export const TEXT = "text-[12px] text-neutral-300";
|
|
18
|
+
/** Muted metadata text. */
|
|
19
|
+
export const TEXT_MUTED = "text-[11px] text-neutral-500";
|
|
20
|
+
/** Micro uppercase section label. */
|
|
21
|
+
export const MICRO_LABEL = "text-[10px] font-semibold uppercase tracking-[0.12em] text-neutral-500";
|
|
22
|
+
/** Shared focus ring for keyboard navigation on interactive controls. */
|
|
23
|
+
export const FOCUS_RING = "outline-none focus-visible:ring-2 focus-visible:ring-cyan-400/60 focus-visible:ring-offset-0";
|
|
24
|
+
/** Base interactive control: compact, subtle border, hover elevation. */
|
|
25
|
+
export const CONTROL = `rounded-[5px] border border-white/[0.07] bg-[#191d24] text-neutral-300 transition-colors hover:bg-[#1f242d] hover:text-neutral-100 ${FOCUS_RING}`;
|
|
26
|
+
/** Active/selected control state — restrained cyan. */
|
|
27
|
+
export const CONTROL_ACTIVE = "border-cyan-400/40 bg-cyan-500/15 text-cyan-200 hover:bg-cyan-500/20";
|
|
28
|
+
/** Text input styling shared across shell panels. */
|
|
29
|
+
export const INPUT_CLS = `rounded-[5px] border border-white/[0.08] bg-black/40 text-[12px] text-neutral-200 outline-none transition-colors placeholder:text-neutral-600 focus:border-cyan-400/50 focus:bg-black/60 ${FOCUS_RING}`;
|
|
30
|
+
/** Tabular numerals for transforms, metrics, and timing readouts. */
|
|
31
|
+
export const NUMERIC = "tabular-nums";
|
|
32
|
+
/** Per-axis accent colors for transform fields (X red, Y green, Z blue — gizmo-matched). */
|
|
33
|
+
export const AXIS_COLORS = {
|
|
34
|
+
x: { text: "text-[#f87171]", bar: "bg-[#f87171]" },
|
|
35
|
+
y: { text: "text-[#4ade80]", bar: "bg-[#4ade80]" },
|
|
36
|
+
z: { text: "text-[#60a5fa]", bar: "bg-[#60a5fa]" },
|
|
37
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { type IconName } from "./icons.js";
|
|
3
|
+
/** Icon-only control with a mandatory accessible name (tooltip + aria-label). */
|
|
4
|
+
export declare function IconButton({ icon, label, onClick, active, disabled, size, className, tone, }: {
|
|
5
|
+
icon: IconName;
|
|
6
|
+
label: string;
|
|
7
|
+
onClick?: () => void;
|
|
8
|
+
active?: boolean;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
size?: number;
|
|
11
|
+
className?: string;
|
|
12
|
+
tone?: "neutral" | "ghost";
|
|
13
|
+
}): import("react").JSX.Element;
|
|
14
|
+
/** Thin vertical divider between toolbar clusters. */
|
|
15
|
+
export declare function ToolbarDivider(): import("react").JSX.Element;
|
|
16
|
+
/** Keyboard-shortcut chip rendered inside buttons and menus. */
|
|
17
|
+
export declare function Kbd({ children }: {
|
|
18
|
+
children: ReactNode;
|
|
19
|
+
}): import("react").JSX.Element;
|
|
20
|
+
/** One choice within a {@link Segmented} control. */
|
|
21
|
+
export interface SegmentedOption<T extends string> {
|
|
22
|
+
value: T;
|
|
23
|
+
label: string;
|
|
24
|
+
icon?: IconName;
|
|
25
|
+
/** Keyboard hint shown after the label. */
|
|
26
|
+
kbd?: string;
|
|
27
|
+
/** Hide the text label and render icon-only (label becomes the tooltip). */
|
|
28
|
+
iconOnly?: boolean;
|
|
29
|
+
}
|
|
30
|
+
/** Compact segmented control on the elevated-control surface with a cyan active segment. */
|
|
31
|
+
export declare function Segmented<T extends string>({ options, value, onChange, ariaLabel, disabled, }: {
|
|
32
|
+
options: readonly SegmentedOption<T>[];
|
|
33
|
+
value: T;
|
|
34
|
+
onChange: (value: T) => void;
|
|
35
|
+
ariaLabel: string;
|
|
36
|
+
disabled?: boolean;
|
|
37
|
+
}): import("react").JSX.Element;
|
|
38
|
+
/** Horizontal tab strip for dock panels (bottom dock, inspector tabs). */
|
|
39
|
+
export declare function PanelTabs<T extends string>({ tabs, active, onSelect, trailing, ariaLabel, }: {
|
|
40
|
+
tabs: readonly {
|
|
41
|
+
id: T;
|
|
42
|
+
label: string;
|
|
43
|
+
icon?: IconName;
|
|
44
|
+
badge?: string | number;
|
|
45
|
+
}[];
|
|
46
|
+
active: T;
|
|
47
|
+
onSelect: (id: T) => void;
|
|
48
|
+
trailing?: ReactNode;
|
|
49
|
+
ariaLabel: string;
|
|
50
|
+
}): import("react").JSX.Element;
|
|
51
|
+
/** Collapsible component card for the inspector and side panels. */
|
|
52
|
+
export declare function CollapsibleSection({ title, icon, open, onToggle, children, trailing, tone, }: {
|
|
53
|
+
title: string;
|
|
54
|
+
icon?: IconName;
|
|
55
|
+
open: boolean;
|
|
56
|
+
onToggle: () => void;
|
|
57
|
+
children: ReactNode;
|
|
58
|
+
trailing?: ReactNode;
|
|
59
|
+
/** Optional accent color for the title icon. */
|
|
60
|
+
tone?: string;
|
|
61
|
+
}): import("react").JSX.Element;
|
|
62
|
+
/** Polished empty state used by unsupported workspaces and empty docks. */
|
|
63
|
+
export declare function EmptyState({ icon, title, description, badge, children, }: {
|
|
64
|
+
icon: IconName;
|
|
65
|
+
title: string;
|
|
66
|
+
description: string;
|
|
67
|
+
badge?: string;
|
|
68
|
+
children?: ReactNode;
|
|
69
|
+
}): import("react").JSX.Element;
|
|
70
|
+
/** Micro uppercase heading used inside panels. */
|
|
71
|
+
export declare function PanelHeading({ children, trailing }: {
|
|
72
|
+
children: ReactNode;
|
|
73
|
+
trailing?: ReactNode;
|
|
74
|
+
}): import("react").JSX.Element;
|
|
75
|
+
/**
|
|
76
|
+
* Drag handle between shell regions. Pointer-drag resizes continuously; arrow keys resize in
|
|
77
|
+
* 16px steps for keyboard access. `sign` maps a positive pointer delta to a size increase.
|
|
78
|
+
*/
|
|
79
|
+
export declare function PanelResizer({ orientation, label, onResize, sign, }: {
|
|
80
|
+
orientation: "vertical" | "horizontal";
|
|
81
|
+
label: string;
|
|
82
|
+
onResize: (delta: number) => void;
|
|
83
|
+
sign?: 1 | -1;
|
|
84
|
+
}): import("react").JSX.Element;
|
package/dist/shell/ui.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useRef } from "react";
|
|
3
|
+
import { Icon } from "./icons.js";
|
|
4
|
+
import { CONTROL, CONTROL_ACTIVE, FOCUS_RING, MICRO_LABEL } from "./theme.js";
|
|
5
|
+
/** Icon-only control with a mandatory accessible name (tooltip + aria-label). */
|
|
6
|
+
export function IconButton({ icon, label, onClick, active = false, disabled = false, size = 14, className = "", tone = "neutral", }) {
|
|
7
|
+
const base = tone === "ghost"
|
|
8
|
+
? `rounded-[5px] border border-transparent text-neutral-400 transition-colors hover:bg-white/[0.06] hover:text-neutral-100 ${FOCUS_RING}`
|
|
9
|
+
: CONTROL;
|
|
10
|
+
return (_jsx("button", { type: "button", title: label, "aria-label": label, "aria-pressed": active, disabled: disabled, onClick: onClick, className: `flex h-7 w-7 shrink-0 items-center justify-center ${base} ${active ? CONTROL_ACTIVE : ""} disabled:pointer-events-none disabled:opacity-35 ${className}`, children: _jsx(Icon, { name: icon, size: size }) }));
|
|
11
|
+
}
|
|
12
|
+
/** Thin vertical divider between toolbar clusters. */
|
|
13
|
+
export function ToolbarDivider() {
|
|
14
|
+
return _jsx("div", { className: "mx-1 h-5 w-px shrink-0 bg-white/[0.07]", "aria-hidden": "true" });
|
|
15
|
+
}
|
|
16
|
+
/** Keyboard-shortcut chip rendered inside buttons and menus. */
|
|
17
|
+
export function Kbd({ children }) {
|
|
18
|
+
return (_jsx("kbd", { className: "rounded-[3px] bg-white/[0.07] px-1 py-px font-sans text-[9px] leading-none text-neutral-400 ring-1 ring-inset ring-white/[0.08]", children: children }));
|
|
19
|
+
}
|
|
20
|
+
/** Compact segmented control on the elevated-control surface with a cyan active segment. */
|
|
21
|
+
export function Segmented({ options, value, onChange, ariaLabel, disabled = false, }) {
|
|
22
|
+
return (_jsx("div", { role: "group", "aria-label": ariaLabel, className: `flex shrink-0 items-center gap-0.5 rounded-[6px] border border-white/[0.07] bg-black/30 p-0.5 ${disabled ? "pointer-events-none opacity-40" : ""}`, children: options.map((option) => {
|
|
23
|
+
const active = option.value === value;
|
|
24
|
+
return (_jsxs("button", { type: "button", title: option.kbd !== undefined ? `${option.label} (${option.kbd})` : option.label, "aria-label": option.label, "aria-pressed": active, onClick: () => onChange(option.value), className: `flex h-6 items-center gap-1.5 rounded-[4px] px-2 text-[11px] transition-colors ${FOCUS_RING} ${active
|
|
25
|
+
? "bg-cyan-500/20 text-cyan-100 shadow-[inset_0_0_0_1px_rgba(34,211,238,0.25)]"
|
|
26
|
+
: "text-neutral-400 hover:bg-white/[0.06] hover:text-neutral-200"}`, children: [option.icon !== undefined ? _jsx(Icon, { name: option.icon, size: 13 }) : null, option.iconOnly === true ? null : _jsx("span", { children: option.label }), option.kbd !== undefined && option.iconOnly !== true ? (_jsx("kbd", { className: `font-sans text-[9px] ${active ? "text-cyan-200/70" : "text-neutral-600"}`, children: option.kbd })) : null] }, option.value));
|
|
27
|
+
}) }));
|
|
28
|
+
}
|
|
29
|
+
/** Horizontal tab strip for dock panels (bottom dock, inspector tabs). */
|
|
30
|
+
export function PanelTabs({ tabs, active, onSelect, trailing, ariaLabel, }) {
|
|
31
|
+
return (_jsxs("div", { role: "tablist", "aria-label": ariaLabel, className: "flex h-8 shrink-0 items-center gap-0.5 border-b border-white/[0.07] px-1.5", children: [tabs.map((tab) => {
|
|
32
|
+
const selected = tab.id === active;
|
|
33
|
+
return (_jsxs("button", { type: "button", role: "tab", "aria-selected": selected, onClick: () => onSelect(tab.id), className: `flex h-6.5 items-center gap-1.5 rounded-[5px] px-2.5 text-[11px] transition-colors ${FOCUS_RING} ${selected ? "bg-white/[0.08] text-neutral-100" : "text-neutral-500 hover:bg-white/[0.04] hover:text-neutral-300"}`, children: [tab.icon !== undefined ? _jsx(Icon, { name: tab.icon, size: 13 }) : null, _jsx("span", { children: tab.label }), tab.badge !== undefined ? (_jsx("span", { className: `rounded-full px-1.5 py-px text-[9px] tabular-nums ${selected ? "bg-cyan-500/20 text-cyan-200" : "bg-white/[0.06] text-neutral-500"}`, children: tab.badge })) : null] }, tab.id));
|
|
34
|
+
}), trailing !== undefined ? _jsx("div", { className: "ml-auto flex items-center gap-1", children: trailing }) : null] }));
|
|
35
|
+
}
|
|
36
|
+
/** Collapsible component card for the inspector and side panels. */
|
|
37
|
+
export function CollapsibleSection({ title, icon, open, onToggle, children, trailing, tone, }) {
|
|
38
|
+
return (_jsxs("section", { className: "overflow-hidden rounded-[6px] border border-white/[0.07] bg-white/[0.02]", children: [_jsxs("div", { className: "flex h-7.5 items-center gap-1.5 px-1.5", children: [_jsxs("button", { type: "button", "aria-expanded": open, onClick: onToggle, className: `flex min-w-0 flex-1 items-center gap-1.5 rounded-[4px] px-1 py-0.5 text-left ${FOCUS_RING}`, children: [_jsx(Icon, { name: open ? "chevronDown" : "chevronRight", size: 11, className: "shrink-0 text-neutral-500" }), icon !== undefined ? (_jsx(Icon, { name: icon, size: 13, className: "shrink-0 text-neutral-400", style: tone !== undefined ? { color: tone } : undefined })) : null, _jsx("span", { className: "truncate text-[12px] font-medium text-neutral-200", children: title })] }), trailing] }), open ? _jsx("div", { className: "border-t border-white/[0.05] p-2.5", children: children }) : null] }));
|
|
39
|
+
}
|
|
40
|
+
/** Polished empty state used by unsupported workspaces and empty docks. */
|
|
41
|
+
export function EmptyState({ icon, title, description, badge, children, }) {
|
|
42
|
+
return (_jsxs("div", { className: "flex h-full min-h-0 flex-col items-center justify-center gap-2 p-6 text-center", children: [_jsx("div", { className: "flex h-10 w-10 items-center justify-center rounded-[8px] border border-white/[0.07] bg-white/[0.03] text-neutral-500", children: _jsx(Icon, { name: icon, size: 18 }) }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "text-[12px] font-medium text-neutral-300", children: title }), badge !== undefined ? (_jsx("span", { className: "rounded-full bg-white/[0.06] px-2 py-0.5 text-[9px] font-semibold uppercase tracking-wider text-neutral-500 ring-1 ring-inset ring-white/[0.08]", children: badge })) : null] }), _jsx("p", { className: "max-w-[32ch] text-[11px] leading-relaxed text-neutral-500", children: description }), children] }));
|
|
43
|
+
}
|
|
44
|
+
/** Micro uppercase heading used inside panels. */
|
|
45
|
+
export function PanelHeading({ children, trailing }) {
|
|
46
|
+
return (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: MICRO_LABEL, children: children }), trailing !== undefined ? _jsx("div", { className: "ml-auto flex items-center gap-1", children: trailing }) : null] }));
|
|
47
|
+
}
|
|
48
|
+
const RESIZE_KEY_STEP = 16;
|
|
49
|
+
/**
|
|
50
|
+
* Drag handle between shell regions. Pointer-drag resizes continuously; arrow keys resize in
|
|
51
|
+
* 16px steps for keyboard access. `sign` maps a positive pointer delta to a size increase.
|
|
52
|
+
*/
|
|
53
|
+
export function PanelResizer({ orientation, label, onResize, sign = 1, }) {
|
|
54
|
+
const dragState = useRef(null);
|
|
55
|
+
const onPointerDown = useCallback((event) => {
|
|
56
|
+
event.preventDefault();
|
|
57
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
58
|
+
dragState.current = {
|
|
59
|
+
pointerId: event.pointerId,
|
|
60
|
+
last: orientation === "vertical" ? event.clientX : event.clientY,
|
|
61
|
+
};
|
|
62
|
+
}, [orientation]);
|
|
63
|
+
const onPointerMove = useCallback((event) => {
|
|
64
|
+
const drag = dragState.current;
|
|
65
|
+
if (drag === null || drag.pointerId !== event.pointerId)
|
|
66
|
+
return;
|
|
67
|
+
const position = orientation === "vertical" ? event.clientX : event.clientY;
|
|
68
|
+
const delta = (position - drag.last) * sign;
|
|
69
|
+
drag.last = position;
|
|
70
|
+
if (delta !== 0)
|
|
71
|
+
onResize(delta);
|
|
72
|
+
}, [onResize, orientation, sign]);
|
|
73
|
+
const onPointerUp = useCallback((event) => {
|
|
74
|
+
if (dragState.current?.pointerId === event.pointerId)
|
|
75
|
+
dragState.current = null;
|
|
76
|
+
}, []);
|
|
77
|
+
const onKeyDown = useCallback((event) => {
|
|
78
|
+
const grow = orientation === "vertical" ? "ArrowRight" : "ArrowDown";
|
|
79
|
+
const shrink = orientation === "vertical" ? "ArrowLeft" : "ArrowUp";
|
|
80
|
+
if (event.key === grow) {
|
|
81
|
+
event.preventDefault();
|
|
82
|
+
onResize(RESIZE_KEY_STEP * sign);
|
|
83
|
+
}
|
|
84
|
+
else if (event.key === shrink) {
|
|
85
|
+
event.preventDefault();
|
|
86
|
+
onResize(-RESIZE_KEY_STEP * sign);
|
|
87
|
+
}
|
|
88
|
+
}, [onResize, orientation, sign]);
|
|
89
|
+
return (_jsx("div", { role: "separator", "aria-label": label, "aria-orientation": orientation, tabIndex: 0, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: onPointerUp, onKeyDown: onKeyDown, className: `group relative z-10 shrink-0 ${FOCUS_RING} ${orientation === "vertical" ? "w-1 cursor-col-resize" : "h-1 cursor-row-resize"}`, children: _jsx("div", { className: `absolute bg-transparent transition-colors group-hover:bg-cyan-400/40 group-focus-visible:bg-cyan-400/50 ${orientation === "vertical" ? "inset-y-0 left-0 w-1" : "inset-x-0 top-0 h-1"}` }) }));
|
|
90
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type ThumbnailState } from "./glbThumbnailCache.js";
|
|
2
|
+
/**
|
|
3
|
+
* Subscribes to the offscreen GLB thumbnail cache for a model URL.
|
|
4
|
+
* Without a URL (catalog markers, generators with no mesh) stays idle so the
|
|
5
|
+
* caller can keep a typed glyph — never invents imagery.
|
|
6
|
+
* @internal
|
|
7
|
+
*/
|
|
8
|
+
export declare function useGlbThumbnail(url: string | undefined): ThumbnailState;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useSyncExternalStore } from "react";
|
|
2
|
+
import { getGlbThumbnailState, requestGlbThumbnail, subscribeGlbThumbnail, } from "./glbThumbnailCache.js";
|
|
3
|
+
const IDLE = { status: "idle", dataUrl: null };
|
|
4
|
+
/**
|
|
5
|
+
* Subscribes to the offscreen GLB thumbnail cache for a model URL.
|
|
6
|
+
* Without a URL (catalog markers, generators with no mesh) stays idle so the
|
|
7
|
+
* caller can keep a typed glyph — never invents imagery.
|
|
8
|
+
* @internal
|
|
9
|
+
*/
|
|
10
|
+
export function useGlbThumbnail(url) {
|
|
11
|
+
const subscribe = (onStoreChange) => {
|
|
12
|
+
if (url === undefined || url.length === 0)
|
|
13
|
+
return () => { };
|
|
14
|
+
requestGlbThumbnail(url);
|
|
15
|
+
return subscribeGlbThumbnail(url, onStoreChange);
|
|
16
|
+
};
|
|
17
|
+
const getSnapshot = () => getGlbThumbnailState(url);
|
|
18
|
+
return useSyncExternalStore(subscribe, getSnapshot, () => IDLE);
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ElevationReadout, ElevationSummary } from "@jgengine/core/world/terrainGuides";
|
|
2
|
+
/** Live, transient terrain-readability feedback shared by the viewport overlay and the HUD legend. */
|
|
3
|
+
export interface TerrainReadoutState {
|
|
4
|
+
/** Elevation under the pointer, or `null` when the pointer is off the terrain. */
|
|
5
|
+
cursor: ElevationReadout | null;
|
|
6
|
+
/** Elevation stats for the readout region (whole terrain), or `null` before the first build. */
|
|
7
|
+
summary: ElevationSummary | null;
|
|
8
|
+
/** The contour interval the overlay chose for the current relief; `0` when flat. */
|
|
9
|
+
interval: number;
|
|
10
|
+
}
|
|
11
|
+
/** Subscribable store for transient terrain-readout feedback (not persisted — it is live measurement). */
|
|
12
|
+
export interface TerrainReadoutStore {
|
|
13
|
+
getState(): TerrainReadoutState;
|
|
14
|
+
setCursor(cursor: ElevationReadout | null): void;
|
|
15
|
+
setSummary(summary: ElevationSummary | null, interval: number): void;
|
|
16
|
+
subscribe(listener: () => void): () => void;
|
|
17
|
+
}
|
|
18
|
+
/** Creates the readout store the terrain overlay writes and the HUD reads. */
|
|
19
|
+
export declare function createTerrainReadoutStore(): TerrainReadoutStore;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Creates the readout store the terrain overlay writes and the HUD reads. */
|
|
2
|
+
export function createTerrainReadoutStore() {
|
|
3
|
+
let state = { cursor: null, summary: null, interval: 0 };
|
|
4
|
+
const listeners = new Set();
|
|
5
|
+
const emit = () => {
|
|
6
|
+
for (const listener of listeners)
|
|
7
|
+
listener();
|
|
8
|
+
};
|
|
9
|
+
return {
|
|
10
|
+
getState: () => state,
|
|
11
|
+
setCursor(cursor) {
|
|
12
|
+
const prev = state.cursor;
|
|
13
|
+
if (prev === cursor)
|
|
14
|
+
return;
|
|
15
|
+
if (prev !== null &&
|
|
16
|
+
cursor !== null &&
|
|
17
|
+
prev.height === cursor.height &&
|
|
18
|
+
prev.x === cursor.x &&
|
|
19
|
+
prev.z === cursor.z) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
state = { ...state, cursor };
|
|
23
|
+
emit();
|
|
24
|
+
},
|
|
25
|
+
setSummary(summary, interval) {
|
|
26
|
+
state = { ...state, summary, interval };
|
|
27
|
+
emit();
|
|
28
|
+
},
|
|
29
|
+
subscribe(listener) {
|
|
30
|
+
listeners.add(listener);
|
|
31
|
+
return () => {
|
|
32
|
+
listeners.delete(listener);
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|