@jgengine/editor 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -0
- package/README.md +12 -0
- package/dist/AssetBrowser.d.ts +33 -1
- package/dist/AssetBrowser.js +64 -2
- package/dist/AssetDropZone.d.ts +9 -0
- package/dist/AssetDropZone.js +65 -0
- package/dist/CatalogsPanel.d.ts +4 -2
- package/dist/CatalogsPanel.js +162 -9
- package/dist/CityPreview.d.ts +11 -0
- package/dist/CityPreview.js +45 -0
- package/dist/CollectionsPanel.js +5 -5
- package/dist/DebugDraw.d.ts +3 -1
- package/dist/DebugDraw.js +37 -19
- package/dist/EditorApp.d.ts +15 -1
- package/dist/EditorApp.js +147 -35
- package/dist/EditorChrome.d.ts +27 -6
- package/dist/EditorChrome.js +314 -130
- package/dist/InspectorPanel.d.ts +17 -7
- package/dist/InspectorPanel.js +173 -27
- package/dist/LightingPanel.d.ts +11 -0
- package/dist/LightingPanel.js +99 -0
- package/dist/MaterialsWorkspacePanel.d.ts +12 -0
- package/dist/MaterialsWorkspacePanel.js +86 -0
- package/dist/ParentPickerMenu.d.ts +13 -0
- package/dist/ParentPickerMenu.js +33 -0
- package/dist/PerfProbe.js +33 -0
- package/dist/PrefabsPanel.js +5 -5
- package/dist/RuntimePlayBridge.d.ts +5 -3
- package/dist/RuntimePlayBridge.js +6 -13
- package/dist/ScatterPreview.d.ts +3 -1
- package/dist/ScatterPreview.js +10 -7
- package/dist/SchemaInspector.js +21 -8
- package/dist/ScriptingPanel.d.ts +15 -0
- package/dist/ScriptingPanel.js +46 -0
- package/dist/SelectionGizmo.js +114 -30
- package/dist/StandaloneEditor.d.ts +16 -9
- package/dist/StandaloneEditor.js +19 -20
- package/dist/TerrainPanel.d.ts +3 -2
- package/dist/TerrainPanel.js +16 -5
- package/dist/TerrainSculpt.js +98 -59
- package/dist/agent/AgentPanel.d.ts +5 -2
- package/dist/agent/AgentPanel.js +11 -6
- package/dist/agent/toolBridge.js +5 -0
- package/dist/assetImport.d.ts +36 -0
- package/dist/assetImport.js +66 -0
- package/dist/authoredComponentMeta.d.ts +25 -0
- package/dist/authoredComponentMeta.js +65 -0
- package/dist/gizmoPivot.d.ts +4 -0
- package/dist/gizmoPivot.js +34 -0
- package/dist/handlers/catalogs.d.ts +3 -0
- package/dist/handlers/catalogs.js +137 -0
- package/dist/handlers/collections.d.ts +3 -0
- package/dist/handlers/collections.js +57 -0
- package/dist/handlers/context.d.ts +36 -0
- package/dist/handlers/context.js +1 -0
- package/dist/handlers/document.d.ts +3 -0
- package/dist/handlers/document.js +350 -0
- package/dist/handlers/grids.d.ts +3 -0
- package/dist/handlers/grids.js +142 -0
- package/dist/handlers/hierarchy.d.ts +3 -0
- package/dist/handlers/hierarchy.js +55 -0
- package/dist/handlers/index.d.ts +8 -0
- package/dist/handlers/index.js +25 -0
- package/dist/handlers/minimap.d.ts +9 -0
- package/dist/handlers/minimap.js +32 -0
- package/dist/handlers/runtime.d.ts +3 -0
- package/dist/handlers/runtime.js +191 -0
- package/dist/handlers/streets.d.ts +3 -0
- package/dist/handlers/streets.js +159 -0
- package/dist/handlers/terrain.d.ts +3 -0
- package/dist/handlers/terrain.js +204 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/listDocumentTriggers.d.ts +42 -0
- package/dist/listDocumentTriggers.js +49 -0
- package/dist/materialAssignments.d.ts +63 -0
- package/dist/materialAssignments.js +84 -0
- package/dist/mcp/fieldSpec.d.ts +32 -0
- package/dist/mcp/fieldSpec.js +174 -0
- package/dist/mcp/rpcRequest.d.ts +8 -5
- package/dist/mcp/rpcRequest.js +72 -93
- package/dist/mcp/tools.js +105 -912
- package/dist/networkSnapshot.d.ts +82 -0
- package/dist/networkSnapshot.js +73 -0
- package/dist/parentCandidates.d.ts +12 -0
- package/dist/parentCandidates.js +25 -0
- package/dist/session.d.ts +91 -4
- package/dist/session.js +36 -1017
- package/dist/shell/AnimationPanel.d.ts +17 -0
- package/dist/shell/AnimationPanel.js +134 -0
- package/dist/shell/AssetThumbnail.d.ts +12 -0
- package/dist/shell/AssetThumbnail.js +23 -0
- package/dist/shell/BottomDock.d.ts +25 -0
- package/dist/shell/BottomDock.js +40 -0
- package/dist/shell/CommandPalette.d.ts +12 -0
- package/dist/shell/CommandPalette.js +56 -0
- package/dist/shell/ConsolePanel.d.ts +8 -0
- package/dist/shell/ConsolePanel.js +45 -0
- package/dist/shell/ContentBrowser.d.ts +21 -0
- package/dist/shell/ContentBrowser.js +130 -0
- package/dist/shell/HierarchyPanel.d.ts +26 -0
- package/dist/shell/HierarchyPanel.js +387 -0
- package/dist/shell/NetworkWorkspacePanel.d.ts +11 -0
- package/dist/shell/NetworkWorkspacePanel.js +49 -0
- package/dist/shell/PlayModeBar.d.ts +12 -0
- package/dist/shell/PlayModeBar.js +27 -0
- package/dist/shell/ProfilerPanel.d.ts +10 -0
- package/dist/shell/ProfilerPanel.js +41 -0
- package/dist/shell/SceneToolbar.d.ts +43 -0
- package/dist/shell/SceneToolbar.js +122 -0
- package/dist/shell/StatusBar.d.ts +18 -0
- package/dist/shell/StatusBar.js +41 -0
- package/dist/shell/TopAppBar.d.ts +35 -0
- package/dist/shell/TopAppBar.js +64 -0
- package/dist/shell/ViewportOverlays.d.ts +23 -0
- package/dist/shell/ViewportOverlays.js +136 -0
- package/dist/shell/WorkspaceRail.d.ts +20 -0
- package/dist/shell/WorkspaceRail.js +31 -0
- package/dist/shell/cameraTelemetry.d.ts +27 -0
- package/dist/shell/cameraTelemetry.js +24 -0
- package/dist/shell/commandRegistry.d.ts +58 -0
- package/dist/shell/commandRegistry.js +133 -0
- package/dist/shell/consoleSink.d.ts +11 -0
- package/dist/shell/consoleSink.js +17 -0
- package/dist/shell/consoleStore.d.ts +27 -0
- package/dist/shell/consoleStore.js +43 -0
- package/dist/shell/fields.d.ts +39 -0
- package/dist/shell/fields.js +104 -0
- package/dist/shell/formatSavedRelative.d.ts +2 -0
- package/dist/shell/formatSavedRelative.js +14 -0
- package/dist/shell/glbThumbnailCache.d.ts +45 -0
- package/dist/shell/glbThumbnailCache.js +241 -0
- package/dist/shell/hierarchyA11y.d.ts +21 -0
- package/dist/shell/hierarchyA11y.js +35 -0
- package/dist/shell/icons.d.ts +13 -0
- package/dist/shell/icons.js +85 -0
- package/dist/shell/layoutStore.d.ts +68 -0
- package/dist/shell/layoutStore.js +156 -0
- package/dist/shell/pathFlythrough.d.ts +37 -0
- package/dist/shell/pathFlythrough.js +54 -0
- package/dist/shell/perfHistory.d.ts +70 -0
- package/dist/shell/perfHistory.js +130 -0
- package/dist/shell/renameObject.d.ts +3 -0
- package/dist/shell/renameObject.js +25 -0
- package/dist/shell/theme.d.ts +36 -0
- package/dist/shell/theme.js +37 -0
- package/dist/shell/ui.d.ts +84 -0
- package/dist/shell/ui.js +90 -0
- package/dist/shell/useGlbThumbnail.d.ts +8 -0
- package/dist/shell/useGlbThumbnail.js +19 -0
- package/dist/uiStore.d.ts +27 -0
- package/dist/uiStore.js +10 -0
- package/dist/viewportContextMenu.d.ts +4 -2
- package/dist/viewportContextMenu.js +5 -1
- package/package.json +4 -4
- package/skills/jgengine/SKILL.md +8 -4
- package/skills/jgengine/api.md +182 -81
- package/skills/jgengine/capabilities.md +104 -2
- package/skills/jgengine/recipes/minimal-game.md +88 -0
- package/skills/jgengine-editor/SKILL.md +18 -3
- package/skills/jgengine-editor/api.md +403 -20
- package/skills/jgengine-editor/capabilities.md +46 -0
- package/skills/jgengine-editor/reference.md +87 -0
- package/skills/level-design/SKILL.md +54 -0
- package/skills/level-design/agents/openai.yaml +4 -0
- package/skills/level-design/references/application-playbook.md +192 -0
- package/skills/level-design/references/field-guide.md +337 -0
- package/skills/level-design/references/live-editor-loop.md +179 -0
- package/dist/OutlinerPanel.d.ts +0 -14
- package/dist/OutlinerPanel.js +0 -84
- package/skills/jgengine/reference-cartridge.md +0 -72
package/dist/SelectionGizmo.js
CHANGED
|
@@ -5,7 +5,8 @@ import * as THREE from "three";
|
|
|
5
5
|
import { findEditorMarker, findEditorNote, findEditorPath, findEditorVolume, } from "@jgengine/core/editor/index";
|
|
6
6
|
import { TransformGizmo, } from "@jgengine/shell/structures/TransformGizmo";
|
|
7
7
|
import { isPointerClick } from "./viewportContextMenu.js";
|
|
8
|
-
import {
|
|
8
|
+
import { resolvePivotPosition } from "./gizmoPivot.js";
|
|
9
|
+
import { newPlacementId, } from "./uiStore.js";
|
|
9
10
|
import { useStoreSelector } from "./useStoreSelector.js";
|
|
10
11
|
const CLICK_SLOP_PX = 5;
|
|
11
12
|
const SCREEN_PICK_RADIUS_PX = 22;
|
|
@@ -15,10 +16,26 @@ const NOTE_LIFT = 1;
|
|
|
15
16
|
const SELECT_TAGS = ["jgEditorId", "jgEntityId", "jgObjectId"];
|
|
16
17
|
const GROUND_PLANE = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
|
|
17
18
|
function selectGizmoUi(state) {
|
|
18
|
-
return {
|
|
19
|
+
return {
|
|
20
|
+
gizmoMode: state.gizmoMode,
|
|
21
|
+
gizmoSpace: state.gizmoSpace,
|
|
22
|
+
gizmoPivot: state.gizmoPivot,
|
|
23
|
+
snapMode: state.snapMode,
|
|
24
|
+
gridSize: state.gridSize,
|
|
25
|
+
rotationSnapDeg: state.rotationSnapDeg,
|
|
26
|
+
scaleSnap: state.scaleSnap,
|
|
27
|
+
pathPoint: state.pathPoint,
|
|
28
|
+
};
|
|
19
29
|
}
|
|
20
30
|
function gizmoUiEqual(a, b) {
|
|
21
|
-
return a.gizmoMode === b.gizmoMode &&
|
|
31
|
+
return (a.gizmoMode === b.gizmoMode &&
|
|
32
|
+
a.gizmoSpace === b.gizmoSpace &&
|
|
33
|
+
a.gizmoPivot === b.gizmoPivot &&
|
|
34
|
+
a.snapMode === b.snapMode &&
|
|
35
|
+
a.gridSize === b.gridSize &&
|
|
36
|
+
a.rotationSnapDeg === b.rotationSnapDeg &&
|
|
37
|
+
a.scaleSnap === b.scaleSnap &&
|
|
38
|
+
a.pathPoint === b.pathPoint);
|
|
22
39
|
}
|
|
23
40
|
/** Reads only the gizmo-relevant UI slice (mode, snap, path-point anchor) — skips re-render on
|
|
24
41
|
* unrelated UI churn (sculpt/paint drags, tool switches, placement) that the old blanket
|
|
@@ -41,6 +58,24 @@ function pathCentroid(points) {
|
|
|
41
58
|
const n = Math.max(1, points.length);
|
|
42
59
|
return { x: x / n, y: y / n, z: z / n };
|
|
43
60
|
}
|
|
61
|
+
function objectPosition(session, id) {
|
|
62
|
+
const { document } = session.getState();
|
|
63
|
+
const marker = findEditorMarker(document, id);
|
|
64
|
+
if (marker !== undefined) {
|
|
65
|
+
return { position: marker.position, lift: MARKER_LIFT, rotationY: marker.rotationY ?? 0, kind: "marker" };
|
|
66
|
+
}
|
|
67
|
+
const volume = findEditorVolume(document, id);
|
|
68
|
+
if (volume !== undefined)
|
|
69
|
+
return { position: volume.center, lift: 0, rotationY: 0, kind: "volume" };
|
|
70
|
+
const path = findEditorPath(document, id);
|
|
71
|
+
if (path !== undefined && path.points.length > 0) {
|
|
72
|
+
return { position: pathCentroid(path.points), lift: 0.8, rotationY: 0, kind: "path" };
|
|
73
|
+
}
|
|
74
|
+
const note = findEditorNote(document, id);
|
|
75
|
+
if (note !== undefined)
|
|
76
|
+
return { position: note.position, lift: NOTE_LIFT, rotationY: 0, kind: "note" };
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
44
79
|
function isEditorOverlayNode(node) {
|
|
45
80
|
let current = node;
|
|
46
81
|
while (current !== null) {
|
|
@@ -146,21 +181,24 @@ export const ViewportSelect = memo(function ViewportSelect({ api, ui }) {
|
|
|
146
181
|
const state = session().getState();
|
|
147
182
|
const visibility = api.getVisibility();
|
|
148
183
|
const candidates = [];
|
|
149
|
-
const consider = (id, kind, point, lift) => {
|
|
150
|
-
if (visibility[kind] === false)
|
|
184
|
+
const consider = (id, kind, point, lift, hidden) => {
|
|
185
|
+
if (visibility[kind] === false || hidden === true)
|
|
151
186
|
return;
|
|
152
187
|
const distance = screenDistance(point, lift, clickX, clickY, rect);
|
|
153
188
|
if (distance !== null && distance < SCREEN_PICK_RADIUS_PX)
|
|
154
189
|
candidates.push({ id, distance });
|
|
155
190
|
};
|
|
156
|
-
for (const marker of state.document.markers)
|
|
157
|
-
consider(marker.id, marker.kind, marker.position, MARKER_LIFT);
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
191
|
+
for (const marker of state.document.markers) {
|
|
192
|
+
consider(marker.id, marker.kind, marker.position, MARKER_LIFT, marker.hidden);
|
|
193
|
+
}
|
|
194
|
+
for (const volume of state.document.volumes) {
|
|
195
|
+
consider(volume.id, volume.kind, volume.center, 0, volume.hidden);
|
|
196
|
+
}
|
|
197
|
+
for (const note of state.document.annotations) {
|
|
198
|
+
consider(note.id, "note", note.position, NOTE_LIFT, note.hidden);
|
|
199
|
+
}
|
|
162
200
|
for (const path of state.document.paths) {
|
|
163
|
-
if (visibility[path.kind] === false)
|
|
201
|
+
if (visibility[path.kind] === false || path.hidden === true)
|
|
164
202
|
continue;
|
|
165
203
|
let best = null;
|
|
166
204
|
for (const point of path.points) {
|
|
@@ -211,6 +249,44 @@ export const ViewportSelect = memo(function ViewportSelect({ api, ui }) {
|
|
|
211
249
|
}
|
|
212
250
|
return null;
|
|
213
251
|
};
|
|
252
|
+
// Throttled hover pick for pre-selection affordance — reuses the same document pick path as click.
|
|
253
|
+
let hoverRaf = 0;
|
|
254
|
+
let lastHoverClientX = 0;
|
|
255
|
+
let lastHoverClientY = 0;
|
|
256
|
+
const flushHover = () => {
|
|
257
|
+
hoverRaf = 0;
|
|
258
|
+
if (ui.getState().tool === "terrain" || ui.getState().placement !== null) {
|
|
259
|
+
if (ui.getState().hoverId !== null)
|
|
260
|
+
ui.patch({ hoverId: null });
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const rect = canvas.getBoundingClientRect();
|
|
264
|
+
const clickX = lastHoverClientX - rect.left;
|
|
265
|
+
const clickY = lastHoverClientY - rect.top;
|
|
266
|
+
if (clickX < 0 || clickY < 0 || clickX > rect.width || clickY > rect.height) {
|
|
267
|
+
if (ui.getState().hoverId !== null)
|
|
268
|
+
ui.patch({ hoverId: null });
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const id = pickDocumentId(clickX, clickY, rect) ?? pickTaggedId(clickX, clickY, rect);
|
|
272
|
+
if (ui.getState().hoverId !== id)
|
|
273
|
+
ui.patch({ hoverId: id });
|
|
274
|
+
};
|
|
275
|
+
const onMove = (event) => {
|
|
276
|
+
lastHoverClientX = event.clientX;
|
|
277
|
+
lastHoverClientY = event.clientY;
|
|
278
|
+
if (hoverRaf !== 0)
|
|
279
|
+
return;
|
|
280
|
+
hoverRaf = requestAnimationFrame(flushHover);
|
|
281
|
+
};
|
|
282
|
+
const onLeave = () => {
|
|
283
|
+
if (hoverRaf !== 0) {
|
|
284
|
+
cancelAnimationFrame(hoverRaf);
|
|
285
|
+
hoverRaf = 0;
|
|
286
|
+
}
|
|
287
|
+
if (ui.getState().hoverId !== null)
|
|
288
|
+
ui.patch({ hoverId: null });
|
|
289
|
+
};
|
|
214
290
|
const onDown = (event) => {
|
|
215
291
|
if (ui.getState().tool === "terrain")
|
|
216
292
|
return;
|
|
@@ -317,16 +393,22 @@ export const ViewportSelect = memo(function ViewportSelect({ api, ui }) {
|
|
|
317
393
|
};
|
|
318
394
|
canvas.addEventListener("pointerdown", onDown);
|
|
319
395
|
canvas.addEventListener("pointerup", onUp);
|
|
396
|
+
canvas.addEventListener("pointermove", onMove);
|
|
397
|
+
canvas.addEventListener("pointerleave", onLeave);
|
|
320
398
|
canvas.addEventListener("contextmenu", onContextMenu);
|
|
321
399
|
return () => {
|
|
400
|
+
if (hoverRaf !== 0)
|
|
401
|
+
cancelAnimationFrame(hoverRaf);
|
|
322
402
|
canvas.removeEventListener("pointerdown", onDown);
|
|
323
403
|
canvas.removeEventListener("pointerup", onUp);
|
|
404
|
+
canvas.removeEventListener("pointermove", onMove);
|
|
405
|
+
canvas.removeEventListener("pointerleave", onLeave);
|
|
324
406
|
canvas.removeEventListener("contextmenu", onContextMenu);
|
|
325
407
|
};
|
|
326
408
|
}, [gl, camera, scene, api, ui]);
|
|
327
409
|
return null;
|
|
328
410
|
});
|
|
329
|
-
function resolveTarget(session,
|
|
411
|
+
function resolveTarget(session, selection, pathPoint, pivot) {
|
|
330
412
|
const { document } = session.getState();
|
|
331
413
|
if (pathPoint !== null) {
|
|
332
414
|
const path = findEditorPath(document, pathPoint.pathId);
|
|
@@ -334,23 +416,25 @@ function resolveTarget(session, selectedId, pathPoint) {
|
|
|
334
416
|
if (point !== undefined)
|
|
335
417
|
return { position: point, lift: 0.8, rotationY: 0, kind: "pathPoint" };
|
|
336
418
|
}
|
|
337
|
-
if (
|
|
419
|
+
if (selection.length === 0)
|
|
338
420
|
return null;
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
421
|
+
const primaryId = selection[0];
|
|
422
|
+
const primary = objectPosition(session, primaryId);
|
|
423
|
+
if (primary === null)
|
|
424
|
+
return null;
|
|
425
|
+
if (selection.length === 1)
|
|
426
|
+
return primary;
|
|
427
|
+
const positions = [];
|
|
428
|
+
for (const id of selection) {
|
|
429
|
+
const resolved = objectPosition(session, id);
|
|
430
|
+
if (resolved !== null)
|
|
431
|
+
positions.push(resolved.position);
|
|
349
432
|
}
|
|
350
|
-
const
|
|
351
|
-
if (
|
|
352
|
-
return
|
|
353
|
-
|
|
433
|
+
const pivotPos = resolvePivotPosition(positions, pivot, primary.position);
|
|
434
|
+
if (pivotPos === null)
|
|
435
|
+
return primary;
|
|
436
|
+
// Multi-select always translates as a group from the pivot; rotation/scale stay primary-kind limited.
|
|
437
|
+
return { position: pivotPos, lift: primary.lift, rotationY: primary.rotationY, kind: primary.kind };
|
|
354
438
|
}
|
|
355
439
|
function effectiveMode(mode, kind) {
|
|
356
440
|
if (kind === "marker")
|
|
@@ -368,7 +452,7 @@ export const SelectionGizmo = memo(function SelectionGizmo({ session, ui, ground
|
|
|
368
452
|
const uiState = useGizmoUiState(ui);
|
|
369
453
|
const state = session.getState();
|
|
370
454
|
const selectedId = state.selection[0];
|
|
371
|
-
const target = useMemo(() => resolveTarget(session,
|
|
455
|
+
const target = useMemo(() => resolveTarget(session, state.selection, uiState.pathPoint, uiState.gizmoPivot), [state, uiState.pathPoint, uiState.gizmoPivot, session]);
|
|
372
456
|
const mode = target === null ? "translate" : effectiveMode(uiState.gizmoMode, target.kind);
|
|
373
457
|
if (target === null)
|
|
374
458
|
return null;
|
|
@@ -451,5 +535,5 @@ export const SelectionGizmo = memo(function SelectionGizmo({ session, ui, ground
|
|
|
451
535
|
session.dispatch({ type: "setTransform", id: selectedId, position: dropped });
|
|
452
536
|
};
|
|
453
537
|
const snapMode = uiState.snapMode === "grid" ? "grid" : uiState.snapMode === "ground" ? "ground" : "free";
|
|
454
|
-
return (_jsx(TransformGizmo, { position: target.position, rotationY: target.rotationY, mode: mode, snapMode: snapMode, gridSize: uiState.gridSize, lift: target.lift, groundSnap: mode === "translate" && target.kind !== "path" ? groundSnap : undefined, onRelease: onRelease }));
|
|
538
|
+
return (_jsx(TransformGizmo, { position: target.position, rotationY: target.rotationY, mode: mode, snapMode: snapMode, gridSize: uiState.gridSize, space: uiState.gizmoSpace, rotationSnap: uiState.rotationSnapDeg === null ? null : (uiState.rotationSnapDeg * Math.PI) / 180, scaleSnap: uiState.scaleSnap, lift: target.lift, groundSnap: mode === "translate" && target.kind !== "path" ? groundSnap : undefined, onRelease: onRelease }));
|
|
455
539
|
});
|
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
import type { EditorLayersInput } from "@jgengine/core/editor/index";
|
|
2
2
|
import { type EnvironmentWorldFeature } from "@jgengine/core/world/features";
|
|
3
3
|
import type { PlayableGame } from "@jgengine/shell/registry";
|
|
4
|
+
import { type AssetImporter, type StandaloneAsset } from "./assetImport.js";
|
|
4
5
|
import { type EditorSaveFn } from "./EditorApp.js";
|
|
5
|
-
|
|
6
|
-
export
|
|
7
|
-
id: string;
|
|
8
|
-
url: string;
|
|
9
|
-
label?: string;
|
|
10
|
-
}
|
|
6
|
+
export type { AssetImporter, StandaloneAsset } from "./assetImport.js";
|
|
7
|
+
export { importAssetToHost, loadDroppedAssets, mergeStandaloneAssets } from "./assetImport.js";
|
|
11
8
|
/** Options for the blank, gameless world the standalone editor authors over. */
|
|
12
9
|
export interface BlankPlayableOptions {
|
|
13
10
|
name?: string;
|
|
14
11
|
assets?: readonly StandaloneAsset[];
|
|
15
12
|
world?: EnvironmentWorldFeature;
|
|
16
13
|
}
|
|
17
|
-
/**
|
|
18
|
-
|
|
14
|
+
/**
|
|
15
|
+
* The default flat-ground authoring canvas the standalone editor opens on when the host supplies
|
|
16
|
+
* none. Deliberately thin — bare sculptable ground under a clear default sky. Dressing (sky look,
|
|
17
|
+
* foliage scatter, props) is what the author adds here, written into the scene document; it is
|
|
18
|
+
* never pre-baked into the canvas. The `seed` parameter is legacy and unused.
|
|
19
|
+
*/
|
|
20
|
+
export declare function blankWorld(_seed?: string): EnvironmentWorldFeature;
|
|
19
21
|
/** Builds a minimal gameless `PlayableGame` — a flat world plus an asset catalog — for the editor to mount over. */
|
|
20
22
|
export declare function createBlankPlayable(options?: BlankPlayableOptions): PlayableGame;
|
|
21
23
|
/** A save fn that hands the scene JSON back to the browser as a downloaded file — the exit path when no dev server is listening. */
|
|
@@ -32,6 +34,11 @@ export interface StandaloneEditorProps {
|
|
|
32
34
|
world?: EnvironmentWorldFeature;
|
|
33
35
|
/** Where Save writes. Default: the installed dev-server endpoint, else a browser download. */
|
|
34
36
|
save?: EditorSaveFn;
|
|
37
|
+
/**
|
|
38
|
+
* How a dropped model is made durable. Default: upload to the `@jgengine/node` editor host plugin so it
|
|
39
|
+
* persists to disk and survives reload; when no host answers the import degrades to an ephemeral blob URL.
|
|
40
|
+
*/
|
|
41
|
+
importAsset?: AssetImporter;
|
|
35
42
|
/** Hide the built-in open-scene / add-assets strip (host supplies its own loading UI). */
|
|
36
43
|
hidePickers?: boolean;
|
|
37
44
|
}
|
|
@@ -41,4 +48,4 @@ export interface StandaloneEditorProps {
|
|
|
41
48
|
* desktop app, or any React host). Ships a slim strip to open a world file and pull in an asset
|
|
42
49
|
* folder; both are also settable up front through props.
|
|
43
50
|
*/
|
|
44
|
-
export declare function StandaloneEditor({ sceneId, scene, assets, world, save, hidePickers, }: StandaloneEditorProps): import("react").JSX.Element;
|
|
51
|
+
export declare function StandaloneEditor({ sceneId, scene, assets, world, save, importAsset, hidePickers, }: StandaloneEditorProps): import("react").JSX.Element;
|
package/dist/StandaloneEditor.js
CHANGED
|
@@ -2,15 +2,23 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useCallback, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { getSaveEndpoint } from "@jgengine/core/devtools/saveEndpoint";
|
|
4
4
|
import { createAssetCatalog } from "@jgengine/core/scene/assetCatalog";
|
|
5
|
-
import { environment,
|
|
5
|
+
import { environment, sky, terrain, } from "@jgengine/core/world/features";
|
|
6
6
|
import { defineGame } from "@jgengine/shell/defineGame";
|
|
7
|
+
import { importAssetToHost, loadDroppedAssets, mergeStandaloneAssets, } from "./assetImport.js";
|
|
7
8
|
import { EditorApp } from "./EditorApp.js";
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
export { importAssetToHost, loadDroppedAssets, mergeStandaloneAssets } from "./assetImport.js";
|
|
10
|
+
/**
|
|
11
|
+
* The default flat-ground authoring canvas the standalone editor opens on when the host supplies
|
|
12
|
+
* none. Deliberately thin — bare sculptable ground under a clear default sky. Dressing (sky look,
|
|
13
|
+
* foliage scatter, props) is what the author adds here, written into the scene document; it is
|
|
14
|
+
* never pre-baked into the canvas. The `seed` parameter is legacy and unused.
|
|
15
|
+
*/
|
|
16
|
+
export function blankWorld(_seed = "standalone") {
|
|
10
17
|
return environment({
|
|
11
18
|
terrain: terrain({ bounds: { w: 128, d: 128 }, height: 0, material: "grass" }),
|
|
12
|
-
|
|
13
|
-
|
|
19
|
+
// Fog pushed past the world edge: the engine's default (near 70 / far 260) soaks most of a
|
|
20
|
+
// 128 m authoring canvas in haze — the editor should open on a clear world.
|
|
21
|
+
sky: sky({ preset: "day", fog: { near: 220, far: 700 } }),
|
|
14
22
|
});
|
|
15
23
|
}
|
|
16
24
|
/** Builds a minimal gameless `PlayableGame` — a flat world plus an asset catalog — for the editor to mount over. */
|
|
@@ -44,17 +52,13 @@ export function downloadSaver(filename = "editor.scene.json") {
|
|
|
44
52
|
}
|
|
45
53
|
};
|
|
46
54
|
}
|
|
47
|
-
const MODEL_FILE = /\.(glb|gltf)$/i;
|
|
48
|
-
function assetId(name) {
|
|
49
|
-
return name.replace(MODEL_FILE, "").replace(/[^a-z0-9]+/gi, "-").replace(/^-+|-+$/g, "").toLowerCase() || "asset";
|
|
50
|
-
}
|
|
51
55
|
/**
|
|
52
56
|
* The scene editor, mounted over a blank gameless world instead of a game — the same `EditorApp`
|
|
53
57
|
* every jgengine game ships, usable standalone on the user's own project (CLI `jgengine editor`,
|
|
54
58
|
* desktop app, or any React host). Ships a slim strip to open a world file and pull in an asset
|
|
55
59
|
* folder; both are also settable up front through props.
|
|
56
60
|
*/
|
|
57
|
-
export function StandaloneEditor({ sceneId = "standalone", scene, assets, world, save, hidePickers = false, }) {
|
|
61
|
+
export function StandaloneEditor({ sceneId = "standalone", scene, assets, world, save, importAsset = importAssetToHost, hidePickers = false, }) {
|
|
58
62
|
const [loadedScene, setLoadedScene] = useState(scene);
|
|
59
63
|
const [loadedAssets, setLoadedAssets] = useState(assets ?? []);
|
|
60
64
|
const [rev, setRev] = useState(0);
|
|
@@ -67,18 +71,13 @@ export function StandaloneEditor({ sceneId = "standalone", scene, assets, world,
|
|
|
67
71
|
setLoadedScene(JSON.parse(text));
|
|
68
72
|
setRev((value) => value + 1);
|
|
69
73
|
}, []);
|
|
70
|
-
const addAssets = useCallback((files) => {
|
|
71
|
-
const next =
|
|
72
|
-
for (const file of Array.from(files)) {
|
|
73
|
-
if (!MODEL_FILE.test(file.name))
|
|
74
|
-
continue;
|
|
75
|
-
next.push({ id: assetId(file.name), url: URL.createObjectURL(file), label: file.name });
|
|
76
|
-
}
|
|
74
|
+
const addAssets = useCallback(async (files) => {
|
|
75
|
+
const next = await loadDroppedAssets(Array.from(files), importAsset);
|
|
77
76
|
if (next.length === 0)
|
|
78
77
|
return;
|
|
79
|
-
setLoadedAssets((current) =>
|
|
78
|
+
setLoadedAssets((current) => mergeStandaloneAssets(current, next));
|
|
80
79
|
setRev((value) => value + 1);
|
|
81
|
-
}, []);
|
|
80
|
+
}, [importAsset]);
|
|
82
81
|
return (_jsxs("div", { className: "relative h-full w-full", children: [_jsx(EditorApp, { gameId: sceneId, playable: playable, layers: loadedScene, save: saveFn }, rev), hidePickers ? null : (_jsxs("div", { className: "pointer-events-none absolute bottom-3 left-1/2 z-[70] flex -translate-x-1/2 items-center gap-2 rounded-full border border-white/10 bg-black/70 px-2 py-1.5 text-[11px] text-neutral-200 shadow-lg shadow-black/40 backdrop-blur-md", children: [_jsx("button", { type: "button", className: "pointer-events-auto rounded-full px-3 py-1 transition-colors hover:bg-white/10", onClick: () => sceneInput.current?.click(), children: "\uD83D\uDCC2 Open scene" }), _jsx("span", { className: "h-4 w-px bg-white/15" }), _jsx("button", { type: "button", className: "pointer-events-auto rounded-full px-3 py-1 transition-colors hover:bg-white/10", onClick: () => assetInput.current?.click(), children: "\uD83E\uDDCA Add assets" }), loadedAssets.length > 0 ? (_jsx("span", { className: "rounded-full bg-white/10 px-2 py-0.5 text-[10px] text-neutral-300", children: loadedAssets.length })) : null, _jsx("input", { ref: sceneInput, type: "file", accept: ".json,application/json", className: "hidden", onChange: (event) => {
|
|
83
82
|
const file = event.target.files?.[0];
|
|
84
83
|
if (file !== undefined)
|
|
@@ -86,7 +85,7 @@ export function StandaloneEditor({ sceneId = "standalone", scene, assets, world,
|
|
|
86
85
|
event.target.value = "";
|
|
87
86
|
} }), _jsx("input", { ref: assetInput, type: "file", accept: ".glb,.gltf,model/gltf-binary", multiple: true, className: "hidden", onChange: (event) => {
|
|
88
87
|
if (event.target.files !== null)
|
|
89
|
-
addAssets(event.target.files);
|
|
88
|
+
void addAssets(event.target.files);
|
|
90
89
|
event.target.value = "";
|
|
91
90
|
} })] }))] }));
|
|
92
91
|
}
|
package/dist/TerrainPanel.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type { EditorSession } from "./session.js";
|
|
1
|
+
import type { EditorHostApi, EditorSession } from "./session.js";
|
|
2
2
|
import { type EditorUiStore } from "./uiStore.js";
|
|
3
3
|
/** The terrain-tool panel: create/clear the heightfield and drive the sculpt/paint controls. */
|
|
4
|
-
export declare function TerrainPanel({ session, ui }: {
|
|
4
|
+
export declare function TerrainPanel({ session, ui, api }: {
|
|
5
5
|
session: EditorSession;
|
|
6
6
|
ui: EditorUiStore;
|
|
7
|
+
api: EditorHostApi;
|
|
7
8
|
}): import("react").JSX.Element;
|
package/dist/TerrainPanel.js
CHANGED
|
@@ -3,8 +3,11 @@ import { useEffect, useState } from "react";
|
|
|
3
3
|
import { editorDocumentBounds } from "@jgengine/core/editor/index";
|
|
4
4
|
import { createTerrainSnapshot, editableTerrainFromSnapshot, } from "@jgengine/core/world/terraform";
|
|
5
5
|
import { TERRAIN_MATERIALS } from "./uiStore.js";
|
|
6
|
-
import { INPUT } from "./chromeStyles.js";
|
|
7
6
|
import { SliderRow } from "./chromeFields.js";
|
|
7
|
+
import { BORDER, CONTROL, CONTROL_ACTIVE, FOCUS_RING, INPUT_CLS, MICRO_LABEL } from "./shell/theme.js";
|
|
8
|
+
const BTN = `${CONTROL} px-2 py-1 text-[11px] disabled:opacity-40`;
|
|
9
|
+
const BTN_SM = `${CONTROL} px-2 py-0.5 text-[11px]`;
|
|
10
|
+
const BTN_ACTIVE = `${CONTROL_ACTIVE} rounded-[5px] px-1.5 py-1 text-[11px] ${FOCUS_RING}`;
|
|
8
11
|
const TERRAIN_BRUSHES = [
|
|
9
12
|
{ kind: "raise", label: "Raise", hint: "Push terrain up" },
|
|
10
13
|
{ kind: "lower", label: "Lower", hint: "Dig terrain down" },
|
|
@@ -30,7 +33,7 @@ function defaultTerrainBounds(document) {
|
|
|
30
33
|
}
|
|
31
34
|
function SculptControls({ ui }) {
|
|
32
35
|
const sculpt = ui.getState().sculpt;
|
|
33
|
-
return (_jsxs(_Fragment, { children: [_jsx("div", { className: "grid grid-cols-3 gap-1", children: TERRAIN_BRUSHES.map((brush) => (_jsx("button", { type: "button", title: brush.hint, className:
|
|
36
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { className: "grid grid-cols-3 gap-1", children: TERRAIN_BRUSHES.map((brush) => (_jsx("button", { type: "button", title: brush.hint, className: sculpt.brush === brush.kind ? BTN_ACTIVE : `${BTN} px-1.5`, onClick: () => ui.patchSculpt({ brush: brush.kind }), children: brush.label }, brush.kind))) }), sculpt.brush === "ramp" ? _jsx("div", { className: "text-[10px] text-neutral-500", children: "Drag from the low end to the high end to grade a slope." }) : null, _jsx(SliderRow, { label: "radius", value: sculpt.radius, min: 1, max: 60, step: 0.5, onChange: (value) => ui.patchSculpt({ radius: value }), format: (v) => `${v.toFixed(1)}m` }), _jsx(SliderRow, { label: "strength", value: sculpt.strength, min: 0.05, max: 5, step: 0.05, onChange: (value) => ui.patchSculpt({ strength: value }) }), _jsx(SliderRow, { label: "spacing", value: sculpt.spacing, min: 0.25, max: 12, step: 0.25, onChange: (value) => ui.patchSculpt({ spacing: value }), format: (v) => `${v.toFixed(2)}m` }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx("span", { className: MICRO_LABEL, children: "falloff" }), _jsx("div", { className: `ml-auto flex gap-0.5 rounded-[5px] border ${BORDER} bg-black/40 p-0.5`, children: ["smooth", "linear", "none"].map((mode) => (_jsx("button", { type: "button", className: `rounded-[4px] px-1.5 py-0.5 text-[10px] capitalize transition-colors ${FOCUS_RING} ${sculpt.falloff === mode ? CONTROL_ACTIVE : "text-neutral-400 hover:text-neutral-200"}`, onClick: () => ui.patchSculpt({ falloff: mode }), children: mode }, mode))) })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: MICRO_LABEL, children: "shape" }), _jsx("button", { type: "button", className: `ml-auto capitalize ${BTN_SM}`, onClick: () => ui.patchSculpt({ shape: sculpt.shape === "circle" ? "square" : "circle" }), children: sculpt.shape }), _jsxs("label", { className: "flex items-center gap-1 text-[10px] text-neutral-400", children: [_jsx("input", { type: "checkbox", className: "accent-amber-400", checked: sculpt.invert, onChange: (event) => ui.patchSculpt({ invert: event.target.checked }) }), "invert"] })] }), sculpt.brush === "flatten" ? (_jsxs("label", { className: "flex items-center justify-between gap-2", children: [_jsx("span", { className: MICRO_LABEL, children: "flatten to" }), _jsx("input", { type: "number", step: 0.5, placeholder: "sample", className: `h-7 w-24 px-1.5 ${INPUT_CLS}`, value: sculpt.flattenHeight ?? "", onChange: (event) => ui.patchSculpt({ flattenHeight: event.target.value === "" ? null : Number(event.target.value) }) })] })) : null, sculpt.brush === "noise" ? (_jsxs("label", { className: "flex items-center justify-between gap-2", children: [_jsx("span", { className: MICRO_LABEL, children: "seed" }), _jsx("input", { type: "number", step: 1, className: `h-7 w-24 px-1.5 ${INPUT_CLS}`, value: sculpt.noiseSeed, onChange: (event) => ui.patchSculpt({ noiseSeed: Math.round(Number(event.target.value)) || 0 }) })] })) : null] }));
|
|
34
37
|
}
|
|
35
38
|
function PaintControls({ session, ui }) {
|
|
36
39
|
const paint = ui.getState().paint;
|
|
@@ -42,11 +45,12 @@ function PaintControls({ session, ui }) {
|
|
|
42
45
|
if (delta.indices.length > 0)
|
|
43
46
|
session.dispatch({ type: "paintTerrain", delta });
|
|
44
47
|
};
|
|
45
|
-
return (_jsxs(_Fragment, { children: [_jsx("div", { className: "grid grid-cols-2 gap-1", children: TERRAIN_MATERIALS.map((material) => (_jsxs("button", { type: "button", className: `flex items-center gap-1.5
|
|
48
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { className: "grid grid-cols-2 gap-1", children: TERRAIN_MATERIALS.map((material) => (_jsxs("button", { type: "button", className: `flex items-center gap-1.5 ${paint.material === material.id ? BTN_ACTIVE : `${BTN} px-1.5`}`, onClick: () => ui.patchPaint({ material: material.id }), children: [_jsx("span", { className: "h-3 w-3 shrink-0 rounded-sm ring-1 ring-inset ring-black/40", style: { backgroundColor: material.color } }), material.label] }, material.id))) }), _jsx("div", { className: "text-[10px] text-neutral-500", children: "Click/drag to paint \u00B7 Alt-click to sample." }), _jsx(SliderRow, { label: "radius", value: paint.radius, min: 1, max: 60, step: 0.5, onChange: (value) => ui.patchPaint({ radius: value }), format: (v) => `${v.toFixed(1)}m` }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: MICRO_LABEL, children: "shape" }), _jsx("button", { type: "button", className: `ml-auto capitalize ${BTN_SM}`, onClick: () => ui.patchPaint({ shape: paint.shape === "circle" ? "square" : "circle" }), children: paint.shape })] }), _jsxs("div", { className: "grid grid-cols-2 gap-1", children: [_jsx("button", { type: "button", className: BTN, onClick: () => paintDelta((terrain) => terrain.fillSurfaceDelta(paint.material)), children: "Fill all" }), _jsx("button", { type: "button", className: BTN, onClick: () => paintDelta((terrain) => terrain.fillSurfaceDelta(null)), children: "Clear paint" })] }), _jsxs("div", { className: `space-y-1 rounded-[6px] border ${BORDER} p-2`, children: [_jsx("div", { className: MICRO_LABEL, children: "Auto rules" }), _jsxs("div", { className: "grid grid-cols-2 gap-1", children: [_jsx("button", { type: "button", className: `${BTN} text-[10px]`, title: "Paint the selected material onto slopes steeper than ~30\u00B0", onClick: () => paintDelta((terrain) => terrain.autoPaintDelta({ surface: paint.material, minSlope: 0.6 })), children: "On steep slopes" }), _jsx("button", { type: "button", className: `${BTN} text-[10px]`, title: "Paint the selected material above height 15", onClick: () => paintDelta((terrain) => terrain.autoPaintDelta({ surface: paint.material, minHeight: 15 })), children: "On high ground" })] })] })] }));
|
|
46
49
|
}
|
|
47
50
|
/** The terrain-tool panel: create/clear the heightfield and drive the sculpt/paint controls. */
|
|
48
|
-
export function TerrainPanel({ session, ui }) {
|
|
51
|
+
export function TerrainPanel({ session, ui, api }) {
|
|
49
52
|
const [, setTick] = useState(0);
|
|
53
|
+
const [bakeStatus, setBakeStatus] = useState(null);
|
|
50
54
|
useEffect(() => ui.subscribe(() => setTick((value) => value + 1)), [ui]);
|
|
51
55
|
useEffect(() => session.subscribe(() => setTick((value) => value + 1)), [session]);
|
|
52
56
|
const uiState = ui.getState();
|
|
@@ -55,5 +59,12 @@ export function TerrainPanel({ session, ui }) {
|
|
|
55
59
|
const createTerrain = () => {
|
|
56
60
|
session.dispatch({ type: "setTerrain", terrain: createTerrainSnapshot({ bounds: defaultTerrainBounds(document), cellSize: 2 }) });
|
|
57
61
|
};
|
|
58
|
-
|
|
62
|
+
const bakeMinimap = () => {
|
|
63
|
+
const response = api.handle({ method: "bake_minimap" });
|
|
64
|
+
if (response.ok)
|
|
65
|
+
setBakeStatus({ tone: "ok", text: "Minimap baked onto the scene." });
|
|
66
|
+
else
|
|
67
|
+
setBakeStatus({ tone: "error", text: response.error ?? "Bake failed." });
|
|
68
|
+
};
|
|
69
|
+
return (_jsxs("div", { className: `pointer-events-auto absolute right-3 top-3 z-40 max-h-[calc(100%-1.5rem)] w-64 space-y-2.5 overflow-auto rounded-xl border ${BORDER} bg-[#111318]/96 p-3 shadow-2xl shadow-black/60 backdrop-blur-md`, children: [_jsxs("div", { className: "flex items-center", children: [_jsx("div", { className: MICRO_LABEL, children: "Terrain" }), _jsx("button", { type: "button", className: `ml-auto rounded-[5px] px-2 py-0.5 text-neutral-500 transition-colors hover:bg-white/10 hover:text-neutral-200 ${FOCUS_RING}`, onClick: () => ui.setTool("select"), title: "Back to select tool", children: "\u00D7" })] }), _jsxs("div", { className: `space-y-1 border-b ${BORDER} pb-2`, children: [_jsx("button", { type: "button", className: `w-full ${BTN}`, onClick: bakeMinimap, title: "Rasterize the authored terrain into a stored minimap PNG the game reads at runtime", children: "Bake minimap" }), bakeStatus !== null ? (_jsx("div", { className: `text-[10px] leading-snug ${bakeStatus.tone === "error" ? "text-rose-300" : "text-emerald-300"}`, children: bakeStatus.text })) : null] }), !hasTerrain ? (_jsxs("div", { className: "space-y-2", children: [_jsx("p", { className: "text-[11px] leading-relaxed text-neutral-400", children: "No terrain yet. Create an editable heightfield over the scene, then sculpt its shape and paint material layers on it." }), _jsx("button", { type: "button", className: `w-full ${CONTROL_ACTIVE} rounded-[5px] px-2.5 py-1.5 text-[12px] font-semibold ${FOCUS_RING}`, onClick: createTerrain, children: "Create terrain" })] })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: `flex gap-0.5 rounded-[6px] border ${BORDER} bg-black/40 p-0.5`, children: ["sculpt", "paint"].map((mode) => (_jsx("button", { type: "button", className: `flex-1 rounded-[5px] px-2 py-1 text-[11px] capitalize transition-colors ${FOCUS_RING} ${uiState.terrainMode === mode ? CONTROL_ACTIVE : "text-neutral-400 hover:bg-white/[0.06] hover:text-neutral-200"}`, onClick: () => ui.setTerrainMode(mode), children: mode }, mode))) }), uiState.terrainMode === "paint" ? _jsx(PaintControls, { session: session, ui: ui }) : _jsx(SculptControls, { ui: ui }), _jsxs("div", { className: `flex items-center justify-between gap-2 border-t ${BORDER} pt-2`, children: [_jsx("button", { type: "button", className: BTN, onClick: () => session.dispatch({ type: "undo" }), disabled: !session.canUndo(), children: "Undo" }), _jsx("button", { type: "button", className: `rounded-[5px] border border-rose-400/25 bg-rose-500/15 px-2 py-1 text-[11px] text-rose-200 transition-colors hover:bg-rose-500/25 ${FOCUS_RING}`, onClick: () => session.dispatch({ type: "clearTerrain" }), children: "Clear terrain" })] })] }))] }));
|
|
59
70
|
}
|