@jgengine/editor 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +325 -0
- package/dist/AssetBrowser.d.ts +20 -0
- package/dist/AssetBrowser.js +49 -0
- package/dist/CatalogsPanel.d.ts +10 -0
- package/dist/CatalogsPanel.js +41 -0
- package/dist/CollectionsPanel.d.ts +10 -0
- package/dist/CollectionsPanel.js +35 -0
- package/dist/DebugDraw.d.ts +17 -0
- package/dist/DebugDraw.js +233 -0
- package/dist/EditorApp.d.ts +26 -0
- package/dist/EditorApp.js +333 -0
- package/dist/EditorCameraDriver.d.ts +5 -0
- package/dist/EditorCameraDriver.js +18 -0
- package/dist/EditorChrome.d.ts +22 -0
- package/dist/EditorChrome.js +559 -0
- package/dist/EditorContextMenu.d.ts +11 -0
- package/dist/EditorContextMenu.js +12 -0
- package/dist/InspectorPanel.d.ts +14 -0
- package/dist/InspectorPanel.js +110 -0
- package/dist/MaterialDropZone.d.ts +10 -0
- package/dist/MaterialDropZone.js +77 -0
- package/dist/OutlinerPanel.d.ts +14 -0
- package/dist/OutlinerPanel.js +84 -0
- package/dist/PerfProbe.d.ts +5 -0
- package/dist/PerfProbe.js +34 -0
- package/dist/PrefabsPanel.d.ts +13 -0
- package/dist/PrefabsPanel.js +42 -0
- package/dist/RuntimePlayBridge.d.ts +18 -0
- package/dist/RuntimePlayBridge.js +140 -0
- package/dist/ScatterPreview.d.ts +11 -0
- package/dist/ScatterPreview.js +30 -0
- package/dist/SchemaInspector.d.ts +19 -0
- package/dist/SchemaInspector.js +78 -0
- package/dist/SelectionGizmo.d.ts +25 -0
- package/dist/SelectionGizmo.js +455 -0
- package/dist/StandaloneEditor.d.ts +44 -0
- package/dist/StandaloneEditor.js +92 -0
- package/dist/TerrainPanel.d.ts +7 -0
- package/dist/TerrainPanel.js +59 -0
- package/dist/TerrainSculpt.d.ts +14 -0
- package/dist/TerrainSculpt.js +415 -0
- package/dist/TriggerInspector.d.ts +13 -0
- package/dist/TriggerInspector.js +60 -0
- package/dist/agent/AgentPanel.d.ts +13 -0
- package/dist/agent/AgentPanel.js +156 -0
- package/dist/agent/context.d.ts +27 -0
- package/dist/agent/context.js +39 -0
- package/dist/agent/endpoint.d.ts +68 -0
- package/dist/agent/endpoint.js +186 -0
- package/dist/agent/toolBridge.d.ts +35 -0
- package/dist/agent/toolBridge.js +93 -0
- package/dist/agent/turn.d.ts +71 -0
- package/dist/agent/turn.js +113 -0
- package/dist/chromeFields.d.ts +15 -0
- package/dist/chromeFields.js +13 -0
- package/dist/chromeStyles.d.ts +3 -0
- package/dist/chromeStyles.js +3 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +19 -0
- package/dist/mcp/bridgeServer.d.ts +13 -0
- package/dist/mcp/bridgeServer.js +1 -0
- package/dist/mcp/loadGameCatalogs.d.ts +24 -0
- package/dist/mcp/loadGameCatalogs.js +87 -0
- package/dist/mcp/rpcRequest.d.ts +22 -0
- package/dist/mcp/rpcRequest.js +97 -0
- package/dist/mcp/tools.d.ts +8 -0
- package/dist/mcp/tools.js +764 -0
- package/dist/outlinerModel.d.ts +76 -0
- package/dist/outlinerModel.js +95 -0
- package/dist/perfMarks.d.ts +25 -0
- package/dist/perfMarks.js +36 -0
- package/dist/session.d.ts +400 -0
- package/dist/session.js +1032 -0
- package/dist/uiStore.d.ts +108 -0
- package/dist/uiStore.js +128 -0
- package/dist/useF2Chord.d.ts +2 -0
- package/dist/useF2Chord.js +32 -0
- package/dist/useStoreSelector.d.ts +31 -0
- package/dist/useStoreSelector.js +51 -0
- package/dist/viewportContextMenu.d.ts +24 -0
- package/dist/viewportContextMenu.js +32 -0
- package/package.json +58 -0
- package/skills/jgengine/SKILL.md +823 -0
- package/skills/jgengine/api.md +416 -0
- package/skills/jgengine/capabilities.md +21 -0
- package/skills/jgengine/reference-cartridge.md +72 -0
- package/skills/jgengine/reference-orphans.md +9 -0
- package/skills/jgengine/reference-systems.md +60 -0
- package/skills/jgengine-editor/SKILL.md +382 -0
- package/skills/jgengine-editor/api.md +416 -0
- package/skills/jgengine-editor/capabilities.md +29 -0
- package/skills/jgengine-editor/reference.md +64 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { EditorSession, EditorVec3, EditorVolumeShape } from "@jgengine/core/editor/index";
|
|
2
|
+
import type { TerraformFalloff, TerraformShape } from "@jgengine/core/world/terraform";
|
|
3
|
+
/** Which top-level editor tool is active: object placement/selection, or terrain sculpting. */
|
|
4
|
+
export type EditorTool = "select" | "terrain";
|
|
5
|
+
/** A heightfield sculpt brush the terrain tool can apply. */
|
|
6
|
+
export type TerrainBrushKind = "raise" | "lower" | "smooth" | "flatten" | "noise" | "ramp";
|
|
7
|
+
/** The terrain tool's active sub-mode: reshape the heightfield, or paint material layers onto it. */
|
|
8
|
+
export type TerrainMode = "sculpt" | "paint";
|
|
9
|
+
/** A paintable terrain material layer — a surface id plus the color it renders as. */
|
|
10
|
+
export interface TerrainMaterial {
|
|
11
|
+
id: string;
|
|
12
|
+
label: string;
|
|
13
|
+
color: string;
|
|
14
|
+
}
|
|
15
|
+
/** The default terrain paint palette (surface id → color) shared by the panel and the mesh. */
|
|
16
|
+
export declare const TERRAIN_MATERIALS: readonly TerrainMaterial[];
|
|
17
|
+
/** Maps every default material id to its render color, for the sculpt mesh's per-cell surface tint. */
|
|
18
|
+
export declare const TERRAIN_MATERIAL_COLORS: Record<string, string>;
|
|
19
|
+
/** Live terrain material-paint controls driven by the terrain tool panel. */
|
|
20
|
+
export interface PaintSettings {
|
|
21
|
+
material: string;
|
|
22
|
+
radius: number;
|
|
23
|
+
shape: TerraformShape;
|
|
24
|
+
}
|
|
25
|
+
/** The terrain tool's default paint controls. */
|
|
26
|
+
export declare const DEFAULT_PAINT_SETTINGS: PaintSettings;
|
|
27
|
+
/** Live terrain-brush controls driven by the terrain tool panel. */
|
|
28
|
+
export interface SculptSettings {
|
|
29
|
+
brush: TerrainBrushKind;
|
|
30
|
+
radius: number;
|
|
31
|
+
strength: number;
|
|
32
|
+
falloff: TerraformFalloff;
|
|
33
|
+
shape: TerraformShape;
|
|
34
|
+
/** Stamp spacing (world units) along a drag — smaller = denser strokes. */
|
|
35
|
+
spacing: number;
|
|
36
|
+
/** Flatten target; `null` samples the ground height under the first click. */
|
|
37
|
+
flattenHeight: number | null;
|
|
38
|
+
noiseSeed: number;
|
|
39
|
+
/** Invert modifier — flips raise↔lower for the active brush. */
|
|
40
|
+
invert: boolean;
|
|
41
|
+
heightLimit: {
|
|
42
|
+
min: number | null;
|
|
43
|
+
max: number | null;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** The terrain tool's default brush controls. */
|
|
47
|
+
export declare const DEFAULT_SCULPT_SETTINGS: SculptSettings;
|
|
48
|
+
/** Which transform gizmo is active for the current selection. */
|
|
49
|
+
export type GizmoMode = "translate" | "rotate" | "scale";
|
|
50
|
+
/** How gizmo drags land: stick to terrain height, quantize to a grid, or free. */
|
|
51
|
+
export type SnapMode = "ground" | "grid" | "off";
|
|
52
|
+
/** The active creation tool — what a viewport click places next. */
|
|
53
|
+
export type PlacementTool = {
|
|
54
|
+
tool: "marker";
|
|
55
|
+
kind: string;
|
|
56
|
+
} | {
|
|
57
|
+
tool: "volume";
|
|
58
|
+
kind: string;
|
|
59
|
+
shape: EditorVolumeShape;
|
|
60
|
+
} | {
|
|
61
|
+
tool: "note";
|
|
62
|
+
} | {
|
|
63
|
+
tool: "path";
|
|
64
|
+
kind: string;
|
|
65
|
+
};
|
|
66
|
+
/** @internal Open viewport context menu state (#866); null when closed. */
|
|
67
|
+
export interface EditorContextMenuState {
|
|
68
|
+
clientX: number;
|
|
69
|
+
clientY: number;
|
|
70
|
+
hitId: string | null;
|
|
71
|
+
ground: EditorVec3 | null;
|
|
72
|
+
}
|
|
73
|
+
/** Transient editor UI state shared between chrome, viewport, and gizmos. */
|
|
74
|
+
export interface EditorUiState {
|
|
75
|
+
gizmoMode: GizmoMode;
|
|
76
|
+
snapMode: SnapMode;
|
|
77
|
+
gridSize: number;
|
|
78
|
+
showGrid: boolean;
|
|
79
|
+
placement: PlacementTool | null;
|
|
80
|
+
pathDraft: readonly EditorVec3[];
|
|
81
|
+
pathPoint: {
|
|
82
|
+
pathId: string;
|
|
83
|
+
index: number;
|
|
84
|
+
} | null;
|
|
85
|
+
tool: EditorTool;
|
|
86
|
+
terrainMode: TerrainMode;
|
|
87
|
+
sculpt: SculptSettings;
|
|
88
|
+
paint: PaintSettings;
|
|
89
|
+
contextMenu: EditorContextMenuState | null;
|
|
90
|
+
}
|
|
91
|
+
/** Subscribable store for the editor's transient UI state (gizmo, snapping, placement). */
|
|
92
|
+
export interface EditorUiStore {
|
|
93
|
+
getState(): EditorUiState;
|
|
94
|
+
patch(partial: Partial<EditorUiState>): void;
|
|
95
|
+
subscribe(listener: () => void): () => void;
|
|
96
|
+
startPlacement(tool: PlacementTool): void;
|
|
97
|
+
cancelPlacement(): void;
|
|
98
|
+
pushDraftPoint(point: EditorVec3): void;
|
|
99
|
+
commitPathDraft(session: EditorSession): void;
|
|
100
|
+
setTool(tool: EditorTool): void;
|
|
101
|
+
setTerrainMode(mode: TerrainMode): void;
|
|
102
|
+
patchSculpt(partial: Partial<SculptSettings>): void;
|
|
103
|
+
patchPaint(partial: Partial<PaintSettings>): void;
|
|
104
|
+
}
|
|
105
|
+
/** Creates the shared UI store the editor chrome and viewport both drive. */
|
|
106
|
+
export declare function createEditorUiStore(): EditorUiStore;
|
|
107
|
+
/** Generates a fresh scene-object id for a placement tool click. */
|
|
108
|
+
export declare function newPlacementId(prefix: string): string;
|
package/dist/uiStore.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/** The default terrain paint palette (surface id → color) shared by the panel and the mesh. */
|
|
2
|
+
export const TERRAIN_MATERIALS = [
|
|
3
|
+
{ id: "grass", label: "Grass", color: "#4b7f3f" },
|
|
4
|
+
{ id: "dirt", label: "Dirt", color: "#6b4f34" },
|
|
5
|
+
{ id: "rock", label: "Rock", color: "#7c7f86" },
|
|
6
|
+
{ id: "sand", label: "Sand", color: "#c2b283" },
|
|
7
|
+
{ id: "mud", label: "Mud", color: "#574433" },
|
|
8
|
+
{ id: "snow", label: "Snow", color: "#eef3f7" },
|
|
9
|
+
{ id: "road", label: "Road", color: "#3a3a3d" },
|
|
10
|
+
{ id: "gravel", label: "Gravel", color: "#8d8d84" },
|
|
11
|
+
];
|
|
12
|
+
/** Maps every default material id to its render color, for the sculpt mesh's per-cell surface tint. */
|
|
13
|
+
export const TERRAIN_MATERIAL_COLORS = Object.fromEntries(TERRAIN_MATERIALS.map((material) => [material.id, material.color]));
|
|
14
|
+
/** The terrain tool's default paint controls. */
|
|
15
|
+
export const DEFAULT_PAINT_SETTINGS = {
|
|
16
|
+
material: "grass",
|
|
17
|
+
radius: 12,
|
|
18
|
+
shape: "circle",
|
|
19
|
+
};
|
|
20
|
+
/** The terrain tool's default brush controls. */
|
|
21
|
+
export const DEFAULT_SCULPT_SETTINGS = {
|
|
22
|
+
brush: "raise",
|
|
23
|
+
radius: 12,
|
|
24
|
+
strength: 1,
|
|
25
|
+
falloff: "smooth",
|
|
26
|
+
shape: "circle",
|
|
27
|
+
spacing: 2,
|
|
28
|
+
flattenHeight: null,
|
|
29
|
+
noiseSeed: 1337,
|
|
30
|
+
invert: false,
|
|
31
|
+
heightLimit: { min: null, max: null },
|
|
32
|
+
};
|
|
33
|
+
function placementId(prefix) {
|
|
34
|
+
return `${prefix}_${Date.now().toString(36)}_${Math.floor(Math.random() * 1296).toString(36)}`;
|
|
35
|
+
}
|
|
36
|
+
/** Creates the shared UI store the editor chrome and viewport both drive. */
|
|
37
|
+
export function createEditorUiStore() {
|
|
38
|
+
let state = {
|
|
39
|
+
gizmoMode: "translate",
|
|
40
|
+
snapMode: "ground",
|
|
41
|
+
gridSize: 1,
|
|
42
|
+
showGrid: true,
|
|
43
|
+
placement: null,
|
|
44
|
+
pathDraft: [],
|
|
45
|
+
pathPoint: null,
|
|
46
|
+
tool: "select",
|
|
47
|
+
terrainMode: "sculpt",
|
|
48
|
+
sculpt: { ...DEFAULT_SCULPT_SETTINGS },
|
|
49
|
+
paint: { ...DEFAULT_PAINT_SETTINGS },
|
|
50
|
+
contextMenu: null,
|
|
51
|
+
};
|
|
52
|
+
const listeners = new Set();
|
|
53
|
+
const emit = () => {
|
|
54
|
+
for (const listener of listeners)
|
|
55
|
+
listener();
|
|
56
|
+
};
|
|
57
|
+
const store = {
|
|
58
|
+
getState: () => state,
|
|
59
|
+
patch(partial) {
|
|
60
|
+
state = { ...state, ...partial };
|
|
61
|
+
emit();
|
|
62
|
+
},
|
|
63
|
+
subscribe(listener) {
|
|
64
|
+
listeners.add(listener);
|
|
65
|
+
return () => {
|
|
66
|
+
listeners.delete(listener);
|
|
67
|
+
};
|
|
68
|
+
},
|
|
69
|
+
startPlacement(tool) {
|
|
70
|
+
state = { ...state, placement: tool, pathDraft: [], pathPoint: null };
|
|
71
|
+
emit();
|
|
72
|
+
},
|
|
73
|
+
cancelPlacement() {
|
|
74
|
+
if (state.placement === null && state.pathDraft.length === 0)
|
|
75
|
+
return;
|
|
76
|
+
state = { ...state, placement: null, pathDraft: [] };
|
|
77
|
+
emit();
|
|
78
|
+
},
|
|
79
|
+
pushDraftPoint(point) {
|
|
80
|
+
state = { ...state, pathDraft: [...state.pathDraft, point] };
|
|
81
|
+
emit();
|
|
82
|
+
},
|
|
83
|
+
commitPathDraft(session) {
|
|
84
|
+
const placement = state.placement;
|
|
85
|
+
if (placement === null || placement.tool !== "path" || state.pathDraft.length < 2) {
|
|
86
|
+
store.cancelPlacement();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
session.dispatch({
|
|
90
|
+
type: "addPath",
|
|
91
|
+
path: {
|
|
92
|
+
id: placementId(placement.kind),
|
|
93
|
+
kind: placement.kind,
|
|
94
|
+
points: state.pathDraft.map((point) => ({ ...point })),
|
|
95
|
+
width: 4,
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
state = { ...state, placement: null, pathDraft: [] };
|
|
99
|
+
emit();
|
|
100
|
+
},
|
|
101
|
+
setTool(tool) {
|
|
102
|
+
if (state.tool === tool)
|
|
103
|
+
return;
|
|
104
|
+
// Leaving/entering the terrain tool clears any half-finished placement.
|
|
105
|
+
state = { ...state, tool, placement: null, pathDraft: [] };
|
|
106
|
+
emit();
|
|
107
|
+
},
|
|
108
|
+
patchSculpt(partial) {
|
|
109
|
+
state = { ...state, sculpt: { ...state.sculpt, ...partial } };
|
|
110
|
+
emit();
|
|
111
|
+
},
|
|
112
|
+
setTerrainMode(mode) {
|
|
113
|
+
if (state.terrainMode === mode)
|
|
114
|
+
return;
|
|
115
|
+
state = { ...state, terrainMode: mode };
|
|
116
|
+
emit();
|
|
117
|
+
},
|
|
118
|
+
patchPaint(partial) {
|
|
119
|
+
state = { ...state, paint: { ...state.paint, ...partial } };
|
|
120
|
+
emit();
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
return store;
|
|
124
|
+
}
|
|
125
|
+
/** Generates a fresh scene-object id for a placement tool click. */
|
|
126
|
+
export function newPlacementId(prefix) {
|
|
127
|
+
return placementId(prefix);
|
|
128
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
/** Listens for the engine's F2+<key> chord family and fires on the given code (e.g. "KeyE"). */
|
|
3
|
+
export function useF2Chord(code, onChord) {
|
|
4
|
+
useEffect(() => {
|
|
5
|
+
let f2Held = false;
|
|
6
|
+
const onKeyDown = (event) => {
|
|
7
|
+
if (event.code === "F2") {
|
|
8
|
+
f2Held = true;
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
if (event.code === code && f2Held) {
|
|
12
|
+
event.preventDefault();
|
|
13
|
+
onChord();
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
const onKeyUp = (event) => {
|
|
17
|
+
if (event.code === "F2")
|
|
18
|
+
f2Held = false;
|
|
19
|
+
};
|
|
20
|
+
const onBlur = () => {
|
|
21
|
+
f2Held = false;
|
|
22
|
+
};
|
|
23
|
+
window.addEventListener("keydown", onKeyDown);
|
|
24
|
+
window.addEventListener("keyup", onKeyUp);
|
|
25
|
+
window.addEventListener("blur", onBlur);
|
|
26
|
+
return () => {
|
|
27
|
+
window.removeEventListener("keydown", onKeyDown);
|
|
28
|
+
window.removeEventListener("keyup", onKeyUp);
|
|
29
|
+
window.removeEventListener("blur", onBlur);
|
|
30
|
+
};
|
|
31
|
+
}, [code, onChord]);
|
|
32
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** The minimal external-store shape both the editor session and UI store satisfy. */
|
|
2
|
+
export interface SubscribableStore<S> {
|
|
3
|
+
getState(): S;
|
|
4
|
+
subscribe(listener: () => void): () => void;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Subscribe a component to a **selected slice** of an external store (editor session or UI store)
|
|
8
|
+
* via `useSyncExternalStore`. The component re-renders only when `selector`'s output changes by
|
|
9
|
+
* `isEqual` (default `Object.is`) — a gizmo drag no longer rerenders panels that read an unrelated
|
|
10
|
+
* slice. The selected value is memoized so an equal slice keeps its reference (no render churn).
|
|
11
|
+
*/
|
|
12
|
+
export declare function useStoreSelector<S, T>(store: SubscribableStore<S>, selector: (state: S) => T, isEqual?: (a: T, b: T) => boolean): T;
|
|
13
|
+
/** Shallow array equality — for selectors that return id lists (`selection`) or small tuples. */
|
|
14
|
+
export declare function shallowArrayEqual<T>(a: readonly T[], b: readonly T[]): boolean;
|
|
15
|
+
/** The visible slice of a fixed-row-height list: which rows to mount and the spacer geometry. */
|
|
16
|
+
export interface VirtualWindow {
|
|
17
|
+
/** First row index to render (inclusive). */
|
|
18
|
+
start: number;
|
|
19
|
+
/** Last row index to render (exclusive). */
|
|
20
|
+
end: number;
|
|
21
|
+
/** Pixel height of the spacer above the first rendered row. */
|
|
22
|
+
offsetTop: number;
|
|
23
|
+
/** Total scroll height of the full list. */
|
|
24
|
+
totalHeight: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Pure windowing math for a fixed-row-height virtual list: given the scroll offset and viewport,
|
|
28
|
+
* returns the `[start, end)` row range to mount (padded by `overscan`) plus the spacer heights, so
|
|
29
|
+
* a 10,000-row outliner only ever mounts the visible handful. No DOM, unit-testable.
|
|
30
|
+
*/
|
|
31
|
+
export declare function virtualWindow(scrollTop: number, viewportHeight: number, rowHeight: number, rowCount: number, overscan?: number): VirtualWindow;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { useCallback, useRef, useSyncExternalStore } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Subscribe a component to a **selected slice** of an external store (editor session or UI store)
|
|
4
|
+
* via `useSyncExternalStore`. The component re-renders only when `selector`'s output changes by
|
|
5
|
+
* `isEqual` (default `Object.is`) — a gizmo drag no longer rerenders panels that read an unrelated
|
|
6
|
+
* slice. The selected value is memoized so an equal slice keeps its reference (no render churn).
|
|
7
|
+
*/
|
|
8
|
+
export function useStoreSelector(store, selector, isEqual = Object.is) {
|
|
9
|
+
const cache = useRef(null);
|
|
10
|
+
const selectorRef = useRef(selector);
|
|
11
|
+
selectorRef.current = selector;
|
|
12
|
+
const equalRef = useRef(isEqual);
|
|
13
|
+
equalRef.current = isEqual;
|
|
14
|
+
const subscribe = useCallback((onChange) => store.subscribe(onChange), [store]);
|
|
15
|
+
const getSnapshot = useCallback(() => {
|
|
16
|
+
const next = selectorRef.current(store.getState());
|
|
17
|
+
if (cache.current !== null && equalRef.current(cache.current.value, next))
|
|
18
|
+
return cache.current.value;
|
|
19
|
+
cache.current = { value: next };
|
|
20
|
+
return next;
|
|
21
|
+
}, [store]);
|
|
22
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
23
|
+
}
|
|
24
|
+
/** Shallow array equality — for selectors that return id lists (`selection`) or small tuples. */
|
|
25
|
+
export function shallowArrayEqual(a, b) {
|
|
26
|
+
if (a === b)
|
|
27
|
+
return true;
|
|
28
|
+
if (a.length !== b.length)
|
|
29
|
+
return false;
|
|
30
|
+
for (let i = 0; i < a.length; i += 1)
|
|
31
|
+
if (a[i] !== b[i])
|
|
32
|
+
return false;
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Pure windowing math for a fixed-row-height virtual list: given the scroll offset and viewport,
|
|
37
|
+
* returns the `[start, end)` row range to mount (padded by `overscan`) plus the spacer heights, so
|
|
38
|
+
* a 10,000-row outliner only ever mounts the visible handful. No DOM, unit-testable.
|
|
39
|
+
*/
|
|
40
|
+
export function virtualWindow(scrollTop, viewportHeight, rowHeight, rowCount, overscan = 6) {
|
|
41
|
+
const totalHeight = rowCount * rowHeight;
|
|
42
|
+
if (rowCount <= 0 || rowHeight <= 0 || viewportHeight <= 0) {
|
|
43
|
+
return { start: 0, end: rowCount, offsetTop: 0, totalHeight };
|
|
44
|
+
}
|
|
45
|
+
const clampedScroll = Math.max(0, Math.min(scrollTop, Math.max(0, totalHeight - viewportHeight)));
|
|
46
|
+
const first = Math.floor(clampedScroll / rowHeight);
|
|
47
|
+
const visible = Math.ceil(viewportHeight / rowHeight);
|
|
48
|
+
const start = Math.max(0, first - overscan);
|
|
49
|
+
const end = Math.min(rowCount, first + visible + overscan);
|
|
50
|
+
return { start, end, offsetTop: start * rowHeight, totalHeight };
|
|
51
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** @internal Movement below this (px) counts as a click; above is a camera drag (#866). */
|
|
2
|
+
export declare const RIGHT_CLICK_SLOP_PX = 4;
|
|
3
|
+
/** @internal True when the pointer released within the drag-vs-click threshold of its down position. */
|
|
4
|
+
export declare function isPointerClick(startX: number, startY: number, endX: number, endY: number, thresholdPx?: number): boolean;
|
|
5
|
+
/** @internal One entry in the editor viewport context menu. */
|
|
6
|
+
export type EditorContextActionId = "frame" | "frameAll" | "duplicate" | "delete" | "createPrefab" | "copy" | "paste" | "addMarker" | "addVolume" | "addPath" | "addNote" | "openAssets";
|
|
7
|
+
/** @internal Label + id for one editor context-menu verb. */
|
|
8
|
+
export interface EditorContextAction {
|
|
9
|
+
id: EditorContextActionId;
|
|
10
|
+
label: string;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
separatorBefore?: boolean;
|
|
13
|
+
}
|
|
14
|
+
/** @internal Inputs for {@link buildEditorContextMenu}. */
|
|
15
|
+
export interface BuildEditorContextMenuInput {
|
|
16
|
+
hitId: string | null;
|
|
17
|
+
selection: readonly string[];
|
|
18
|
+
canPaste: boolean;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* @internal Selection-aware editor context verbs (#866). Hit/selection → frame/duplicate/delete/copy/prefab;
|
|
22
|
+
* empty ground → place tools + paste when clipboard has content.
|
|
23
|
+
*/
|
|
24
|
+
export declare function buildEditorContextMenu(input: BuildEditorContextMenuInput): readonly EditorContextAction[];
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** @internal Movement below this (px) counts as a click; above is a camera drag (#866). */
|
|
2
|
+
export const RIGHT_CLICK_SLOP_PX = 4;
|
|
3
|
+
/** @internal True when the pointer released within the drag-vs-click threshold of its down position. */
|
|
4
|
+
export function isPointerClick(startX, startY, endX, endY, thresholdPx = RIGHT_CLICK_SLOP_PX) {
|
|
5
|
+
return Math.hypot(endX - startX, endY - startY) <= thresholdPx;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* @internal Selection-aware editor context verbs (#866). Hit/selection → frame/duplicate/delete/copy/prefab;
|
|
9
|
+
* empty ground → place tools + paste when clipboard has content.
|
|
10
|
+
*/
|
|
11
|
+
export function buildEditorContextMenu(input) {
|
|
12
|
+
const hasSelection = input.selection.length > 0 || input.hitId !== null;
|
|
13
|
+
if (hasSelection) {
|
|
14
|
+
return [
|
|
15
|
+
{ id: "frame", label: "Frame" },
|
|
16
|
+
{ id: "duplicate", label: "Duplicate" },
|
|
17
|
+
{ id: "copy", label: "Copy" },
|
|
18
|
+
{ id: "delete", label: "Delete", separatorBefore: true },
|
|
19
|
+
{ id: "createPrefab", label: "Create prefab…", separatorBefore: true },
|
|
20
|
+
{ id: "paste", label: "Paste", disabled: !input.canPaste, separatorBefore: true },
|
|
21
|
+
];
|
|
22
|
+
}
|
|
23
|
+
return [
|
|
24
|
+
{ id: "paste", label: "Paste", disabled: !input.canPaste },
|
|
25
|
+
{ id: "addMarker", label: "Add marker", separatorBefore: true },
|
|
26
|
+
{ id: "addVolume", label: "Add volume (zone)" },
|
|
27
|
+
{ id: "addPath", label: "Draw path" },
|
|
28
|
+
{ id: "addNote", label: "Add note" },
|
|
29
|
+
{ id: "openAssets", label: "Place asset…", separatorBefore: true },
|
|
30
|
+
{ id: "frameAll", label: "Frame all", separatorBefore: true },
|
|
31
|
+
];
|
|
32
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jgengine/editor",
|
|
3
|
+
"version": "0.10.0",
|
|
4
|
+
"description": "Scene/world/asset editor for JGengine — loaded lazily by the runner; never import from game entrypoints.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/Noisemaker111/jgengine.git",
|
|
11
|
+
"directory": "packages/editor"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"CHANGELOG.md",
|
|
16
|
+
"skills"
|
|
17
|
+
],
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"default": "./dist/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./*": {
|
|
24
|
+
"types": "./dist/*.d.ts",
|
|
25
|
+
"default": "./dist/*.js"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsgo -p tsconfig.build.json && bun ../../scripts/fix-extensions.ts dist",
|
|
30
|
+
"check-types": "tsgo --noEmit -p tsconfig.json",
|
|
31
|
+
"test": "bun test src"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@jgengine/core": "^0.10.0",
|
|
35
|
+
"@jgengine/react": "^0.10.0",
|
|
36
|
+
"@jgengine/shell": "^0.10.0"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"@react-three/drei": "^10.0.0",
|
|
40
|
+
"@react-three/fiber": "^9.0.0",
|
|
41
|
+
"react": "^19.0.0",
|
|
42
|
+
"react-dom": "^19.0.0",
|
|
43
|
+
"three": ">=0.170.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@react-three/drei": "^10.7.7",
|
|
47
|
+
"@react-three/fiber": "^9.5.0",
|
|
48
|
+
"@types/react": "^19",
|
|
49
|
+
"@types/react-dom": "^19",
|
|
50
|
+
"@types/three": "^0.182.0",
|
|
51
|
+
"react": "19.2.3",
|
|
52
|
+
"react-dom": "19.2.3",
|
|
53
|
+
"three": "^0.182.0"
|
|
54
|
+
},
|
|
55
|
+
"publishConfig": {
|
|
56
|
+
"access": "public"
|
|
57
|
+
}
|
|
58
|
+
}
|