@jgengine/editor 0.19.0 → 0.22.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 +364 -0
- package/dist/ClipPreviewLayer.d.ts +2 -3
- package/dist/ClipPreviewLayer.js +10 -28
- package/dist/CreatorApp.d.ts +12 -0
- package/dist/CreatorApp.js +107 -0
- package/dist/DebugDraw.js +3 -1
- package/dist/EditorApp.d.ts +14 -2
- package/dist/EditorApp.js +29 -14
- package/dist/EditorChrome.d.ts +9 -1
- package/dist/EditorChrome.js +54 -35
- package/dist/InspectorPanel.d.ts +3 -1
- package/dist/InspectorPanel.js +12 -8
- package/dist/MaterialAssetEditor.d.ts +13 -0
- package/dist/MaterialAssetEditor.js +109 -0
- package/dist/MaterialPreview.d.ts +16 -0
- package/dist/MaterialPreview.js +41 -0
- package/dist/MaterialsWorkspacePanel.d.ts +14 -2
- package/dist/MaterialsWorkspacePanel.js +78 -4
- package/dist/RuntimePlayBridge.js +1 -6
- package/dist/SelectionGizmo.js +23 -14
- package/dist/SimulationInspector.d.ts +9 -0
- package/dist/SimulationInspector.js +47 -0
- package/dist/TerrainSculpt.js +12 -59
- package/dist/agent/toolBridge.js +2 -0
- package/dist/authoringPlacement.d.ts +21 -0
- package/dist/authoringPlacement.js +34 -0
- package/dist/handlers/context.d.ts +1 -0
- package/dist/handlers/document.d.ts +1 -1
- package/dist/handlers/document.js +29 -1
- package/dist/handlers/hierarchy.d.ts +1 -1
- package/dist/handlers/hierarchy.js +4 -0
- package/dist/handlers/runtime.js +20 -17
- package/dist/handlers/terrain.js +52 -12
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/materialControls.d.ts +32 -0
- package/dist/materialControls.js +79 -0
- package/dist/mcp/fieldSpec.d.ts +1 -1
- package/dist/mcp/fieldSpec.js +11 -2
- package/dist/mcp/importOptionalGameModule.d.ts +2 -0
- package/dist/mcp/importOptionalGameModule.js +13 -0
- package/dist/mcp/loadGameCatalogs.js +5 -12
- package/dist/mcp/rpcRequest.d.ts +3 -3
- package/dist/mcp/rpcRequest.js +10 -4
- package/dist/mcp/tools.js +10 -1
- package/dist/modelAnimationAuthoring.d.ts +27 -7
- package/dist/modelAnimationAuthoring.js +69 -72
- package/dist/session.d.ts +62 -0
- package/dist/session.js +56 -7
- package/dist/shell/AnimationGraphPanel.js +18 -10
- package/dist/shell/PlayModeBar.d.ts +2 -1
- package/dist/shell/PlayModeBar.js +2 -2
- package/dist/shell/SceneToolbar.d.ts +2 -1
- package/dist/shell/SceneToolbar.js +5 -4
- package/dist/shell/TopAppBar.d.ts +2 -1
- package/dist/shell/TopAppBar.js +2 -2
- package/dist/shell/animGraphPreview.d.ts +2 -0
- package/dist/shell/animGraphPreview.js +5 -2
- package/dist/shell/clipPreview.d.ts +1 -0
- package/dist/uiStore.d.ts +1 -10
- package/dist/uiStore.js +1 -13
- package/dist/useDocumentSave.d.ts +20 -0
- package/dist/useDocumentSave.js +33 -0
- package/package.json +6 -5
- package/skills/ce-handoff/SKILL.md +1 -1
- package/skills/jgengine/SKILL.md +23 -2
- package/skills/jgengine/api.md +28 -17
- package/skills/jgengine/recipes/minimal-game.md +7 -2
- package/skills/jgengine-editor/SKILL.md +9 -1
- package/skills/jgengine-editor/api.md +111 -16
- package/skills/jgengine-editor/capabilities.md +40 -0
- package/skills/jgengine-editor/reference.md +149 -1
- package/skills/jgengine-editor/references/live-sync.md +15 -0
package/dist/SelectionGizmo.js
CHANGED
|
@@ -4,6 +4,7 @@ import { memo, useEffect, useMemo } from "react";
|
|
|
4
4
|
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
|
+
import { placementGround, pathSegmentDistance, selectionGizmoMode, snapPlacement } from "./authoringPlacement.js";
|
|
7
8
|
import { isPointerClick } from "./viewportContextMenu.js";
|
|
8
9
|
import { resolvePivotPosition } from "./gizmoPivot.js";
|
|
9
10
|
import { newPlacementId, } from "./uiStore.js";
|
|
@@ -108,13 +109,17 @@ export const ViewportSelect = memo(function ViewportSelect({ api, ui }) {
|
|
|
108
109
|
const planeHit = new THREE.Vector3();
|
|
109
110
|
let down = null;
|
|
110
111
|
const session = () => api.getSession();
|
|
111
|
-
const
|
|
112
|
+
const screenPoint = (point, lift, rect) => {
|
|
112
113
|
projected.set(point.x, point.y + lift, point.z).project(camera);
|
|
113
114
|
if (projected.z < -1 || projected.z > 1)
|
|
114
115
|
return null;
|
|
115
116
|
const px = (projected.x * 0.5 + 0.5) * rect.width;
|
|
116
117
|
const py = (-projected.y * 0.5 + 0.5) * rect.height;
|
|
117
|
-
return
|
|
118
|
+
return { x: px, y: py };
|
|
119
|
+
};
|
|
120
|
+
const screenDistance = (point, lift, clickX, clickY, rect) => {
|
|
121
|
+
const projectedPoint = screenPoint(point, lift, rect);
|
|
122
|
+
return projectedPoint === null ? null : Math.hypot(projectedPoint.x - clickX, projectedPoint.y - clickY);
|
|
118
123
|
};
|
|
119
124
|
const groundPoint = (clickX, clickY, rect) => {
|
|
120
125
|
ndc.set((clickX / rect.width) * 2 - 1, -(clickY / rect.height) * 2 + 1);
|
|
@@ -135,6 +140,11 @@ export const ViewportSelect = memo(function ViewportSelect({ api, ui }) {
|
|
|
135
140
|
const placement = ui.getState().placement;
|
|
136
141
|
if (placement === null)
|
|
137
142
|
return;
|
|
143
|
+
const ground = placementGround(session().getState().document.terrain, api.getTerrainSampler());
|
|
144
|
+
const snapped = snapPlacement(point, ui.getState().snapMode, ui.getState().gridSize, (x, z) => ground?.sampleHeight(x, z) ?? 0);
|
|
145
|
+
if (snapped === null)
|
|
146
|
+
return;
|
|
147
|
+
point = snapped;
|
|
138
148
|
if (placement.tool === "path") {
|
|
139
149
|
ui.pushDraftPoint(point);
|
|
140
150
|
return;
|
|
@@ -201,11 +211,17 @@ export const ViewportSelect = memo(function ViewportSelect({ api, ui }) {
|
|
|
201
211
|
if (visibility[path.kind] === false || path.hidden === true)
|
|
202
212
|
continue;
|
|
203
213
|
let best = null;
|
|
214
|
+
let previous = null;
|
|
204
215
|
for (const point of path.points) {
|
|
205
|
-
const
|
|
206
|
-
if (
|
|
207
|
-
|
|
216
|
+
const projectedPoint = screenPoint(point, 0.8, rect);
|
|
217
|
+
if (projectedPoint !== null) {
|
|
218
|
+
const distance = previous === null
|
|
219
|
+
? Math.hypot(projectedPoint.x - clickX, projectedPoint.y - clickY)
|
|
220
|
+
: pathSegmentDistance({ x: clickX, y: clickY }, previous, projectedPoint);
|
|
221
|
+
if (distance < SCREEN_PICK_RADIUS_PX && (best === null || distance < best))
|
|
222
|
+
best = distance;
|
|
208
223
|
}
|
|
224
|
+
previous = projectedPoint;
|
|
209
225
|
}
|
|
210
226
|
if (best !== null)
|
|
211
227
|
candidates.push({ id: path.id, distance: best });
|
|
@@ -410,7 +426,7 @@ export const ViewportSelect = memo(function ViewportSelect({ api, ui }) {
|
|
|
410
426
|
});
|
|
411
427
|
function resolveTarget(session, selection, pathPoint, pivot) {
|
|
412
428
|
const { document } = session.getState();
|
|
413
|
-
if (pathPoint !== null) {
|
|
429
|
+
if (pathPoint !== null && selection.length === 1 && selection[0] === pathPoint.pathId) {
|
|
414
430
|
const path = findEditorPath(document, pathPoint.pathId);
|
|
415
431
|
const point = path?.points[pathPoint.index];
|
|
416
432
|
if (point !== undefined)
|
|
@@ -436,13 +452,6 @@ function resolveTarget(session, selection, pathPoint, pivot) {
|
|
|
436
452
|
// Multi-select always translates as a group from the pivot; rotation/scale stay primary-kind limited.
|
|
437
453
|
return { position: pivotPos, lift: primary.lift, rotationY: primary.rotationY, kind: primary.kind };
|
|
438
454
|
}
|
|
439
|
-
function effectiveMode(mode, kind) {
|
|
440
|
-
if (kind === "marker")
|
|
441
|
-
return mode === "scale" ? "translate" : mode;
|
|
442
|
-
if (kind === "volume")
|
|
443
|
-
return mode === "rotate" ? "translate" : mode;
|
|
444
|
-
return "translate";
|
|
445
|
-
}
|
|
446
455
|
/**
|
|
447
456
|
* Drag-to-transform gizmo bound to the current selection, dispatching editor commands on
|
|
448
457
|
* release. Mounts the shared shell `TransformGizmo` and maps release poses onto editor
|
|
@@ -453,7 +462,7 @@ export const SelectionGizmo = memo(function SelectionGizmo({ session, ui, ground
|
|
|
453
462
|
const state = session.getState();
|
|
454
463
|
const selectedId = state.selection[0];
|
|
455
464
|
const target = useMemo(() => resolveTarget(session, state.selection, uiState.pathPoint, uiState.gizmoPivot), [state, uiState.pathPoint, uiState.gizmoPivot, session]);
|
|
456
|
-
const mode = target === null ? "translate" :
|
|
465
|
+
const mode = target === null ? "translate" : selectionGizmoMode(uiState.gizmoMode, target.kind, target.kind === "pathPoint" ? 1 : state.selection.length);
|
|
457
466
|
if (target === null)
|
|
458
467
|
return null;
|
|
459
468
|
const onRelease = (pose) => {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CreatorPolicy } from "@jgengine/core/editor/creatorStorage";
|
|
2
|
+
import type { EditorSession } from "@jgengine/core/editor/commands";
|
|
3
|
+
/** Production authoring controls share the same validated command and undo history as RPC.
|
|
4
|
+
* @capability simulation-inspector author weather schedules, effects, fire, wind zones, forces and habitats with validated undoable controls
|
|
5
|
+
*/
|
|
6
|
+
export declare function SimulationInspector({ session, creatorPolicy }: {
|
|
7
|
+
session: EditorSession;
|
|
8
|
+
creatorPolicy?: CreatorPolicy;
|
|
9
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { useStoreSelector } from "./useStoreSelector.js";
|
|
4
|
+
const modes = ["clear", "rain", "snow", "mixed", "dust"];
|
|
5
|
+
function NumberInput({ label, value, onChange, min = 0, step = 0.1 }) {
|
|
6
|
+
return _jsxs("label", { className: "flex items-center justify-between gap-2", children: [_jsx("span", { children: label }), _jsx("input", { "aria-label": label, className: "w-24 rounded border border-neutral-700 bg-neutral-900 p-1", type: "number", min: min, step: step, value: value, onChange: (event) => onChange(Number(event.target.value)) })] });
|
|
7
|
+
}
|
|
8
|
+
/** Production authoring controls share the same validated command and undo history as RPC.
|
|
9
|
+
* @capability simulation-inspector author weather schedules, effects, fire, wind zones, forces and habitats with validated undoable controls
|
|
10
|
+
*/
|
|
11
|
+
export function SimulationInspector({ session, creatorPolicy }) {
|
|
12
|
+
const simulation = useStoreSelector(session, (state) => state.document.simulation);
|
|
13
|
+
const [error, setError] = useState(null);
|
|
14
|
+
const [advanced, setAdvanced] = useState(null);
|
|
15
|
+
const [species, setSpecies] = useState("");
|
|
16
|
+
const weather = simulation?.weather ?? {};
|
|
17
|
+
const allowWeather = creatorPolicy === undefined || (creatorPolicy.maxSimulationParticles ?? 0) >= 5556;
|
|
18
|
+
const allowEmitters = creatorPolicy === undefined || (creatorPolicy.maxSimulationParticles ?? 0) > 0;
|
|
19
|
+
const allowFire = creatorPolicy === undefined || (creatorPolicy.maxFireCells ?? 0) > 0;
|
|
20
|
+
const allowFlocks = creatorPolicy === undefined || (creatorPolicy.maxPopulation ?? 0) > 0;
|
|
21
|
+
const allowForces = creatorPolicy === undefined || (creatorPolicy.maxForceFields ?? 0) > 0;
|
|
22
|
+
function update(next) {
|
|
23
|
+
try {
|
|
24
|
+
session.dispatch({ type: "setSimulation", simulation: next });
|
|
25
|
+
setError(null);
|
|
26
|
+
}
|
|
27
|
+
catch (reason) {
|
|
28
|
+
setError(reason instanceof Error ? reason.message : String(reason));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function setWeather(next) { update({ ...simulation, weather: next }); }
|
|
32
|
+
function id(prefix, rows) {
|
|
33
|
+
let index = 1;
|
|
34
|
+
while (rows.some((row) => row.id === `${prefix}-${index}`))
|
|
35
|
+
index += 1;
|
|
36
|
+
return `${prefix}-${index}`;
|
|
37
|
+
}
|
|
38
|
+
return _jsxs("section", { "aria-label": "Simulation authoring", className: "space-y-4 p-3 text-xs text-neutral-300", children: [_jsx("h3", { className: "text-sm font-semibold text-neutral-100", children: "Weather & simulation" }), _jsx("p", { children: "Metres \u00B7 game seconds \u00B7 wind on world X/Z. Save stores authored inputs; play state saves separately." }), error !== null ? _jsx("p", { role: "alert", className: "text-red-300", children: error }) : null, _jsxs("fieldset", { hidden: !allowWeather, className: "space-y-2", children: [_jsx("legend", { children: "Ambient weather" }), _jsxs("label", { className: "flex justify-between", children: ["Mode", _jsx("select", { "aria-label": "Weather mode", value: weather.ambient?.mode ?? "clear", onChange: (event) => setWeather({ ...weather, ambient: { ...weather.ambient, mode: event.target.value, intensity: weather.ambient?.intensity ?? 1 } }), children: modes.map((mode) => _jsx("option", { children: mode }, mode)) })] }), _jsx(NumberInput, { label: "Weather intensity", value: weather.ambient?.intensity ?? 0, onChange: (intensity) => setWeather({ ...weather, ambient: { mode: "clear", ...weather.ambient, intensity } }) }), _jsx(NumberInput, { label: "Temperature offset \u00B0C", min: -100, value: weather.ambient?.temperatureOffset ?? 0, onChange: (temperatureOffset) => setWeather({ ...weather, ambient: { mode: "clear", intensity: 0, ...weather.ambient, temperatureOffset } }) }), _jsx(NumberInput, { label: "Wind speed m/s", value: weather.wind?.speed ?? 0, onChange: (speed) => setWeather({ ...weather, wind: { ...weather.wind, speed } }) }), _jsx(NumberInput, { label: "Wind X direction", min: -1, value: weather.wind?.direction?.[0] ?? 1, onChange: (x) => setWeather({ ...weather, wind: { ...weather.wind, direction: [x, weather.wind?.direction?.[1] ?? 0] } }) }), _jsx(NumberInput, { label: "Wind Z direction", min: -1, value: weather.wind?.direction?.[1] ?? 0, onChange: (z) => setWeather({ ...weather, wind: { ...weather.wind, direction: [weather.wind?.direction?.[0] ?? 1, z] } }) })] }), _jsxs("fieldset", { hidden: !allowWeather, className: "space-y-2", children: [_jsx("legend", { children: "Named profiles" }), (weather.profiles ?? []).map((profile, index) => _jsxs("div", { className: "space-y-1 border-l border-neutral-700 pl-2", children: [_jsxs("label", { children: ["Profile id", _jsx("input", { "aria-label": `Profile ${index + 1} id`, value: profile.id, onChange: (event) => {
|
|
39
|
+
const nextId = event.target.value;
|
|
40
|
+
setWeather({ ...weather, initialProfileId: weather.initialProfileId === profile.id ? nextId : weather.initialProfileId, profiles: weather.profiles.map((row, i) => i === index ? { ...row, id: nextId } : row), schedule: weather.schedule?.map((row) => row.profileId === profile.id ? { ...row, profileId: nextId } : row) });
|
|
41
|
+
} })] }), _jsx("select", { "aria-label": `Profile ${profile.id} mode`, value: profile.mode, onChange: (event) => setWeather({ ...weather, profiles: weather.profiles.map((row, i) => i === index ? { ...row, mode: event.target.value } : row) }), children: modes.map((mode) => _jsx("option", { children: mode }, mode)) }), _jsx(NumberInput, { label: `Profile ${profile.id} intensity`, value: profile.intensity, onChange: (intensity) => setWeather({ ...weather, profiles: weather.profiles.map((row, i) => i === index ? { ...row, intensity } : row) }) }), _jsx("button", { type: "button", onClick: () => setWeather({ ...weather, profiles: weather.profiles.filter((_, i) => i !== index), initialProfileId: weather.initialProfileId === profile.id ? undefined : weather.initialProfileId, schedule: weather.schedule?.filter((row) => row.profileId !== profile.id) }), children: "Remove profile" })] }, profile.id)), _jsx("button", { type: "button", onClick: () => setWeather({ ...weather, profiles: [...(weather.profiles ?? []), { id: id("profile", weather.profiles ?? []), mode: "rain", intensity: 0.5 }] }), children: "Add weather profile" })] }), _jsxs("fieldset", { hidden: !allowWeather, className: "space-y-2", children: [_jsx("legend", { children: "Schedule" }), (weather.schedule ?? []).map((transition, index) => _jsxs("div", { className: "space-y-1 border-l border-neutral-700 pl-2", children: [_jsx("select", { "aria-label": `Transition ${index + 1} profile`, value: transition.profileId, onChange: (event) => setWeather({ ...weather, schedule: weather.schedule.map((row, i) => i === index ? { ...row, profileId: event.target.value } : row) }), children: weather.profiles?.map((profile) => _jsx("option", { children: profile.id }, profile.id)) }), _jsx(NumberInput, { label: `Transition ${index + 1} at seconds`, value: transition.atSeconds, onChange: (atSeconds) => setWeather({ ...weather, schedule: weather.schedule.map((row, i) => i === index ? { ...row, atSeconds } : row) }) }), _jsx(NumberInput, { label: `Transition ${index + 1} duration seconds`, value: transition.transitionSeconds ?? 0, onChange: (transitionSeconds) => setWeather({ ...weather, schedule: weather.schedule.map((row, i) => i === index ? { ...row, transitionSeconds } : row) }) }), _jsx("button", { type: "button", onClick: () => setWeather({ ...weather, schedule: weather.schedule.filter((_, i) => i !== index) }), children: "Remove transition" })] }, index)), _jsx("button", { type: "button", disabled: !weather.profiles?.length, onClick: () => setWeather({ ...weather, schedule: [...(weather.schedule ?? []), { atSeconds: (weather.schedule?.at(-1)?.atSeconds ?? 0) + 10, profileId: weather.profiles[0].id, transitionSeconds: 2 }] }), children: "Add transition" })] }), _jsxs("fieldset", { hidden: !allowWeather, className: "space-y-2", children: [_jsx("legend", { children: "Local wind zones" }), (weather.zones ?? []).map((zone, index) => _jsxs("div", { className: "space-y-1 border-l border-neutral-700 pl-2", children: [_jsx("span", { children: zone.id }), _jsx(NumberInput, { label: `${zone.id} X metres`, min: -1e6, value: zone.center[0], onChange: (x) => setWeather({ ...weather, zones: weather.zones.map((row, i) => i === index ? { ...row, center: [x, row.center[1]] } : row) }) }), _jsx(NumberInput, { label: `${zone.id} Z metres`, min: -1e6, value: zone.center[1], onChange: (z) => setWeather({ ...weather, zones: weather.zones.map((row, i) => i === index ? { ...row, center: [row.center[0], z] } : row) }) }), _jsx(NumberInput, { label: `${zone.id} radius metres`, value: zone.radius, onChange: (radius) => setWeather({ ...weather, zones: weather.zones.map((row, i) => i === index ? { ...row, radius } : row) }) }), _jsx(NumberInput, { label: `${zone.id} speed m/s`, value: zone.wind?.speed ?? 0, onChange: (speed) => setWeather({ ...weather, zones: weather.zones.map((row, i) => i === index ? { ...row, wind: { ...row.wind, speed } } : row) }) }), _jsxs("label", { children: [_jsx("input", { type: "checkbox", checked: zone.radial ?? false, onChange: (event) => setWeather({ ...weather, zones: weather.zones.map((row, i) => i === index ? { ...row, radial: event.target.checked } : row) }) }), " Radial wind"] }), _jsx("button", { type: "button", onClick: () => setWeather({ ...weather, zones: weather.zones.filter((_, i) => i !== index) }), children: "Remove wind zone" })] }, zone.id)), _jsx("button", { type: "button", onClick: () => setWeather({ ...weather, zones: [...(weather.zones ?? []), { id: id("wind", weather.zones ?? []), center: [0, 0], radius: 10, falloff: 5, wind: { direction: [1, 0], speed: 3 } }] }), children: "Add wind zone" })] }), _jsxs("fieldset", { hidden: !allowForces, className: "space-y-2", children: [_jsx("legend", { children: "Spatial forces" }), (simulation?.forces ?? []).map((field, index) => _jsxs("div", { className: "space-y-1 border-l border-neutral-700 pl-2", children: [[0, 1, 2].map((axis) => _jsx(NumberInput, { label: `Force ${index + 1} ${["X", "Y", "Z"][axis]} metres`, min: -1e6, value: field.center[axis], onChange: (value) => update({ ...simulation, forces: simulation.forces.map((row, i) => i === index ? { ...row, center: row.center.map((entry, j) => j === axis ? value : entry) } : row) }) }, axis)), field.shape.kind === "sphere" ? _jsx(NumberInput, { label: `Force ${index + 1} radius metres`, value: field.shape.radius, onChange: (radius) => update({ ...simulation, forces: simulation.forces.map((row, i) => i === index ? { ...row, shape: { kind: "sphere", radius } } : row) }) }) : _jsx("span", { children: "Box bounds are available in advanced editing." }), _jsx(NumberInput, { label: `Force ${index + 1} attraction m/s²`, min: -1e6, value: field.strength, onChange: (strength) => update({ ...simulation, forces: simulation.forces.map((row, i) => i === index ? { ...row, strength } : row) }) }), _jsx(NumberInput, { label: `Force ${index + 1} vortex m/s²`, min: -1e6, value: field.vortex?.strength ?? 0, onChange: (strength) => update({ ...simulation, forces: simulation.forces.map((row, i) => i === index ? { ...row, vortex: { axis: [0, 1, 0], ...row.vortex, strength } } : row) }) }), _jsx(NumberInput, { label: `Force ${index + 1} lift m/s²`, min: -1e6, value: field.vortex?.lift ?? 0, onChange: (lift) => update({ ...simulation, forces: simulation.forces.map((row, i) => i === index ? { ...row, vortex: { axis: [0, 1, 0], strength: 0, ...row.vortex, lift } } : row) }) }), _jsx(NumberInput, { label: `Force ${index + 1} falloff`, value: field.attenuation ?? 1, onChange: (attenuation) => update({ ...simulation, forces: simulation.forces.map((row, i) => i === index ? { ...row, attenuation } : row) }) }), _jsx("button", { type: "button", onClick: () => update({ ...simulation, forces: simulation.forces.filter((_, i) => i !== index) }), children: "Remove force" })] }, index)), _jsx("button", { type: "button", onClick: () => update({ ...simulation, forces: [...(simulation?.forces ?? []), { center: [0, 0, 0], shape: { kind: "sphere", radius: 10 }, strength: 0, attenuation: 1, vortex: { axis: [0, 1, 0], strength: 2, lift: 1 }, mask: 1 }] }), children: "Add spatial force" })] }), _jsxs("fieldset", { hidden: !allowEmitters, className: "space-y-2", children: [_jsx("legend", { children: "Named effects" }), (simulation?.emitters ?? []).map((emitter, index) => _jsxs("div", { className: "space-y-1 border-l border-neutral-700 pl-2", children: [_jsx("span", { children: emitter.id }), _jsx("select", { "aria-label": `${emitter.id} appearance`, value: emitter.options?.render?.shape ?? "point", onChange: (event) => update({ ...simulation, emitters: simulation.emitters.map((row, i) => i === index ? { ...row, options: { ...row.options, render: { shape: event.target.value } } } : row) }), children: ["point", "streak", "flake", "flame", "smoke", "ribbon", "ripple"].map((shape) => _jsx("option", { children: shape }, shape)) }), _jsx(NumberInput, { label: `${emitter.id} particles/second`, value: emitter.config.rate ?? 0, onChange: (rate) => update({ ...simulation, emitters: simulation.emitters.map((row, i) => i === index ? { ...row, config: { ...row.config, rate } } : row) }) }), _jsx(NumberInput, { label: `${emitter.id} particle capacity`, step: 1, value: emitter.config.max ?? 512, onChange: (max) => update({ ...simulation, emitters: simulation.emitters.map((row, i) => i === index ? { ...row, config: { ...row.config, max } } : row) }) }), ["x", "y", "z"].map((axis) => _jsx(NumberInput, { label: `${emitter.id} ${axis.toUpperCase()} metres`, min: -1e6, value: emitter.position[axis], onChange: (value) => update({ ...simulation, emitters: simulation.emitters.map((row, i) => i === index ? { ...row, position: { ...row.position, [axis]: value } } : row) }) }, axis)), _jsxs("label", { children: [_jsx("input", { type: "checkbox", checked: emitter.options?.active !== false, onChange: (event) => update({ ...simulation, emitters: simulation.emitters.map((row, i) => i === index ? { ...row, options: { ...row.options, active: event.target.checked } } : row) }) }), " Spawning"] }), _jsxs("label", { children: ["Fire binding", _jsxs("select", { "aria-label": `${emitter.id} fire area`, value: emitter.fireAreaId ?? "", onChange: (event) => update({ ...simulation, emitters: simulation.emitters.map((row, i) => i === index ? { ...row, fireAreaId: event.target.value || undefined } : row) }), children: [_jsx("option", { value: "", children: "Independent" }), simulation?.fires?.map((area) => _jsx("option", { children: area.id }, area.id))] })] }), _jsx("button", { type: "button", onClick: () => update({ ...simulation, emitters: simulation.emitters.filter((_, i) => i !== index) }), children: "Remove emitter" })] }, emitter.id)), _jsx("button", { type: "button", onClick: () => update({ ...simulation, emitters: [...(simulation?.emitters ?? []), { id: id("emitter", simulation?.emitters ?? []), position: { x: 0, y: 1, z: 0 }, config: { rate: 8, max: 64, lifetime: { min: 1, max: 2 }, seed: "authored" }, options: { render: { shape: "smoke" } } }] }), children: "Add named emitter" })] }), _jsxs("fieldset", { hidden: !allowFire, className: "space-y-2", children: [_jsx("legend", { children: "Fire areas" }), (simulation?.fires ?? []).map((area, index) => _jsxs("div", { className: "space-y-1 border-l border-neutral-700 pl-2", children: [_jsx("span", { children: area.id }), ["x", "y", "z"].map((axis) => _jsx(NumberInput, { label: `${area.id} ${axis.toUpperCase()} metres`, min: -1e6, value: area.position[axis], onChange: (value) => update({ ...simulation, fires: simulation.fires.map((row, i) => i === index ? { ...row, position: { ...row.position, [axis]: value } } : row) }) }, axis)), _jsx(NumberInput, { label: `${area.id} cell size metres`, value: area.config.cellSize, onChange: (cellSize) => update({ ...simulation, fires: simulation.fires.map((row, i) => i === index ? { ...row, config: { ...row.config, cellSize } } : row) }) }), _jsx(NumberInput, { label: `${area.id} fuel consumed/second`, value: area.config.burnRate ?? 0.25, onChange: (burnRate) => update({ ...simulation, fires: simulation.fires.map((row, i) => i === index ? { ...row, config: { ...row.config, burnRate } } : row) }) }), _jsx(NumberInput, { label: `${area.id} rain cooling heat/second`, value: area.extinguishRate ?? 0, onChange: (extinguishRate) => update({ ...simulation, fires: simulation.fires.map((row, i) => i === index ? { ...row, extinguishRate } : row) }) }), _jsxs("label", { children: [_jsx("input", { type: "checkbox", checked: (area.ignitions?.length ?? 0) > 0, onChange: (event) => update({ ...simulation, fires: simulation.fires.map((row, i) => i === index ? { ...row, ignitions: event.target.checked ? [{ col: 0, row: 0 }] : [] } : row) }) }), " Ignite first cell on play"] }), _jsx("button", { type: "button", onClick: () => update({ ...simulation, fires: simulation.fires.filter((_, i) => i !== index), emitters: simulation?.emitters?.map((emitter) => emitter.fireAreaId === area.id ? { ...emitter, fireAreaId: undefined } : emitter) }), children: "Remove fire area" })] }, area.id)), _jsx("button", { type: "button", onClick: () => update({ ...simulation, fires: [...(simulation?.fires ?? []), { id: id("fire", simulation?.fires ?? []), position: { x: 0, y: 0, z: 0 }, config: { cols: 4, rows: 4, cellSize: 1 }, ignitions: [] }] }), children: "Add fire area" })] }), _jsxs("fieldset", { hidden: !allowFlocks, className: "space-y-2", children: [_jsx("legend", { children: "Flock habitats" }), (simulation?.habitats ?? []).map((habitat, index) => _jsxs("div", { className: "space-y-1 border-l border-neutral-700 pl-2", children: [_jsxs("span", { children: [habitat.id, " \u00B7 ", habitat.species] }), _jsxs("select", { "aria-label": `${habitat.id} role`, value: habitat.role, onChange: (event) => update({ ...simulation, habitats: simulation.habitats.map((row, i) => i === index ? { ...row, role: event.target.value } : row) }), children: [_jsx("option", { children: "cosmetic" }), _jsx("option", { children: "gameplay" })] }), _jsx(NumberInput, { label: `${habitat.id} bird count`, step: 1, value: habitat.count, onChange: (count) => update({ ...simulation, habitats: simulation.habitats.map((row, i) => i === index ? { ...row, count } : row) }) }), _jsx(NumberInput, { label: `${habitat.id} radius metres`, value: habitat.radius, onChange: (radius) => update({ ...simulation, habitats: simulation.habitats.map((row, i) => i === index ? { ...row, radius } : row) }) }), ["x", "y", "z"].map((axis) => _jsx(NumberInput, { label: `${habitat.id} ${axis.toUpperCase()} metres`, min: -1e6, value: habitat.position[axis], onChange: (value) => update({ ...simulation, habitats: simulation.habitats.map((row, i) => i === index ? { ...row, position: { ...row.position, [axis]: value } } : row) }) }, axis)), _jsxs("select", { "aria-label": `${habitat.id} route`, value: habitat.routeId ?? "", onChange: (event) => update({ ...simulation, habitats: simulation.habitats.map((row, i) => i === index ? { ...row, routeId: event.target.value || undefined } : row) }), children: [_jsx("option", { value: "", children: "Stay in habitat" }), session.getState().document.paths.filter((path) => path.points.length > 1).map((path) => _jsx("option", { value: path.id, children: path.label ?? path.id }, path.id))] }), _jsx("button", { type: "button", onClick: () => update({ ...simulation, habitats: simulation.habitats.filter((_, i) => i !== index) }), children: "Remove habitat" })] }, habitat.id)), _jsxs("label", { children: ["Game species", _jsx("input", { "aria-label": "Game species", value: species, onChange: (event) => setSpecies(event.target.value) })] }), _jsx("button", { type: "button", disabled: !species.trim(), onClick: () => update({ ...simulation, habitats: [...(simulation?.habitats ?? []), { id: id("habitat", simulation?.habitats ?? []), species: species.trim(), position: { x: 0, y: 4, z: 0 }, radius: 8, count: 8, seed: "authored", role: "cosmetic", steering: { maxSpeed: 4, separationRadius: 1, neighborRadius: 5 } }] }), children: "Add flock habitat" })] }), _jsx("button", { type: "button", onClick: () => setAdvanced(JSON.stringify(simulation ?? {}, null, 2)), children: "Edit complete simulation data" }), advanced !== null ? _jsxs("div", { className: "space-y-2", children: [_jsx("textarea", { "aria-label": "Simulation JSON", className: "h-60 w-full bg-neutral-900 p-2 font-mono", value: advanced, onChange: (event) => setAdvanced(event.target.value) }), _jsx("button", { type: "button", onClick: () => { try {
|
|
42
|
+
update(JSON.parse(advanced));
|
|
43
|
+
}
|
|
44
|
+
catch (reason) {
|
|
45
|
+
setError(String(reason));
|
|
46
|
+
} }, children: "Apply simulation data" }), _jsx("button", { type: "button", onClick: () => setAdvanced(null), children: "Close data editor" })] }) : null] });
|
|
47
|
+
}
|
package/dist/TerrainSculpt.js
CHANGED
|
@@ -5,18 +5,14 @@ import * as THREE from "three";
|
|
|
5
5
|
import { createTerrainPaletteSampler, raycastHeightField, } from "@jgengine/core/world/terrain";
|
|
6
6
|
import { beginSurfaceStroke, beginTerraformStroke, editableTerrainFromSnapshot, } from "@jgengine/core/world/terraform";
|
|
7
7
|
import { useGameContext } from "@jgengine/react/provider";
|
|
8
|
-
import { displaceHeightfieldGeometry, normalizeHeightBlend, TerraformBrushCursor, } from "@jgengine/shell/terrain";
|
|
8
|
+
import { displaceHeightfieldGeometry, normalizeHeightBlend, createTerrainPaintColorSampler, TerraformBrushCursor, } from "@jgengine/shell/terrain";
|
|
9
9
|
import { editorPerfMarks } from "./perfMarks.js";
|
|
10
|
-
import {
|
|
10
|
+
import {} from "./uiStore.js";
|
|
11
11
|
import { useStoreSelector } from "./useStoreSelector.js";
|
|
12
12
|
const GROUND_PLANE = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
|
|
13
13
|
const LOW = new THREE.Color("#3f5d34");
|
|
14
14
|
const HIGH = new THREE.Color("#8f9c66");
|
|
15
15
|
const COLOR_SPAN = 24;
|
|
16
|
-
/** Resolves a material layer to its render color (its tint, else its palette surface color). */
|
|
17
|
-
function layerColor(layer) {
|
|
18
|
-
return layer.tint ?? TERRAIN_MATERIAL_COLORS[layer.surface] ?? "#7c7f86";
|
|
19
|
-
}
|
|
20
16
|
/** The game's `environment({ terrain })` descriptor, when the world it declares carries one — the same descriptor `TerrainGround` samples at runtime via `createTerrainPaletteSampler`. */
|
|
21
17
|
function terrainDescriptorOf(world) {
|
|
22
18
|
return world?.kind === "environment" ? world.terrain : undefined;
|
|
@@ -65,28 +61,11 @@ function editFromSettings(settings, center, clickHeight) {
|
|
|
65
61
|
...(heightLimit === undefined ? {} : { heightLimit }),
|
|
66
62
|
};
|
|
67
63
|
}
|
|
68
|
-
const BLEND_A = new THREE.Color();
|
|
69
|
-
const BLEND_B = new THREE.Color();
|
|
70
|
-
/** Parsed once — `toneAt` runs per vertex, so hex→Color parsing must not. */
|
|
71
|
-
const SURFACE_COLOR_CACHE = new Map();
|
|
72
|
-
function surfaceColor(hex) {
|
|
73
|
-
let color = SURFACE_COLOR_CACHE.get(hex);
|
|
74
|
-
if (color === undefined) {
|
|
75
|
-
color = new THREE.Color(hex);
|
|
76
|
-
SURFACE_COLOR_CACHE.set(hex, color);
|
|
77
|
-
}
|
|
78
|
-
return color;
|
|
79
|
-
}
|
|
80
64
|
/** World-unit spacing between topographic relief bands drawn on the sculpt preview. */
|
|
81
65
|
const CONTOUR_INTERVAL = 1.5;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
* darken slopes toward each band trough (a contour-map look) and higher ground brightens slightly —
|
|
86
|
-
* a `multiply`, so painted grass/dirt/rock keep their hue. Flat ground at a band height (the common
|
|
87
|
-
* unsculpted case at y=0) is left untouched: only varying height shows the bands. `strength` 0 is a
|
|
88
|
-
* no-op.
|
|
89
|
-
*/
|
|
66
|
+
const BLEND_A = new THREE.Color();
|
|
67
|
+
const BLEND_B = new THREE.Color();
|
|
68
|
+
/** Multiplicative contour bands preserve painted hue while making elevation readable. */
|
|
90
69
|
function applyRelief(out, height, strength) {
|
|
91
70
|
if (strength <= 0)
|
|
92
71
|
return;
|
|
@@ -102,37 +81,9 @@ function applyRelief(out, height, strength) {
|
|
|
102
81
|
* default — so `materialRegions`/base `colors`/`material` (worn paths, rock outcrops, biome tint)
|
|
103
82
|
* show through in the editor the same way `TerrainGround` renders them at runtime.
|
|
104
83
|
*/
|
|
105
|
-
function toneAt(
|
|
106
|
-
|
|
107
|
-
if (layers.length > 0) {
|
|
108
|
-
const weights = terrain.weightsAt(x, z);
|
|
109
|
-
if (weights.length === layers.length) {
|
|
110
|
-
let total = 0;
|
|
111
|
-
let r = 0;
|
|
112
|
-
let g = 0;
|
|
113
|
-
let b = 0;
|
|
114
|
-
for (let l = 0; l < layers.length; l += 1) {
|
|
115
|
-
const w = weights[l] * (layers[l].opacity ?? 1);
|
|
116
|
-
if (w <= 0)
|
|
117
|
-
continue;
|
|
118
|
-
const color = layerColors[l];
|
|
119
|
-
r += color.r * w;
|
|
120
|
-
g += color.g * w;
|
|
121
|
-
b += color.b * w;
|
|
122
|
-
total += w;
|
|
123
|
-
}
|
|
124
|
-
if (total > 0) {
|
|
125
|
-
out.setRGB(r / total, g / total, b / total);
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
const surface = terrain.surfaceAt(x, z);
|
|
131
|
-
const painted = surface === null ? undefined : TERRAIN_MATERIAL_COLORS[surface];
|
|
132
|
-
if (painted !== undefined) {
|
|
133
|
-
out.copy(surfaceColor(painted));
|
|
84
|
+
function toneAt(x, z, height, out, paletteAt, heightRange, paintColorAt) {
|
|
85
|
+
if (paintColorAt(x, z, out))
|
|
134
86
|
return;
|
|
135
|
-
}
|
|
136
87
|
if (paletteAt !== null) {
|
|
137
88
|
const palette = paletteAt(x, z);
|
|
138
89
|
const blend = normalizeHeightBlend(height, heightRange[0], heightRange[1]);
|
|
@@ -151,12 +102,12 @@ function toneAt(terrain, x, z, height, out, paletteAt, heightRange, layerColors)
|
|
|
151
102
|
*/
|
|
152
103
|
function displace(geometry, terrain, bounds, region, paletteAt, heightRange, contourStrength) {
|
|
153
104
|
const started = performance.now();
|
|
154
|
-
const
|
|
105
|
+
const paintColorAt = createTerrainPaintColorSampler(terrain);
|
|
155
106
|
displaceHeightfieldGeometry(geometry, terrain.sampleHeight, {
|
|
156
107
|
bounds,
|
|
157
108
|
region,
|
|
158
109
|
color: (x, z, height, out) => {
|
|
159
|
-
toneAt(
|
|
110
|
+
toneAt(x, z, height, out, paletteAt, heightRange, paintColorAt);
|
|
160
111
|
applyRelief(out, height, contourStrength);
|
|
161
112
|
},
|
|
162
113
|
});
|
|
@@ -172,6 +123,7 @@ function unionRect(a, b) {
|
|
|
172
123
|
}
|
|
173
124
|
/** Memoized so cursor-hover state updates in the parent never reconcile the mesh subtree. */
|
|
174
125
|
const SculptMesh = memo(function SculptMesh({ terrain, bounds, segments, revision, pendingRef, paletteAt, heightRange, contourStrength, }) {
|
|
126
|
+
const invalidate = useThree((state) => state.invalidate);
|
|
175
127
|
const geometry = useMemo(() => {
|
|
176
128
|
const width = Math.max(1, bounds.maxX - bounds.minX);
|
|
177
129
|
const depth = Math.max(1, bounds.maxZ - bounds.minZ);
|
|
@@ -188,8 +140,9 @@ const SculptMesh = memo(function SculptMesh({ terrain, bounds, segments, revisio
|
|
|
188
140
|
pendingRef.current = null;
|
|
189
141
|
const region = pending !== null && pending.kind === "region" ? pending.region : null;
|
|
190
142
|
displace(geometry, terrain, bounds, region, paletteAt, heightRange, contourStrength);
|
|
143
|
+
invalidate();
|
|
191
144
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
192
|
-
}, [geometry, terrain, revision, paletteAt, heightRange, contourStrength]);
|
|
145
|
+
}, [geometry, terrain, revision, paletteAt, heightRange, contourStrength, invalidate]);
|
|
193
146
|
return (_jsx("mesh", { geometry: geometry, receiveShadow: true, children: _jsx("meshStandardMaterial", { vertexColors: true, roughness: 0.95, metalness: 0, polygonOffset: true, polygonOffsetFactor: -1, polygonOffsetUnits: -1 }) }));
|
|
194
147
|
});
|
|
195
148
|
function selectSculptUi(state) {
|
package/dist/agent/toolBridge.js
CHANGED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { EditorVec3 } from "@jgengine/core/editor/index";
|
|
2
|
+
import type { TerrainField } from "@jgengine/core/world/terrain";
|
|
3
|
+
import { type TerraformSnapshot } from "@jgengine/core/world/terraform";
|
|
4
|
+
import type { GizmoMode, SnapMode } from "./uiStore.js";
|
|
5
|
+
/** Compose authored sculpt offsets over the same base field as the terrain preview. @internal */
|
|
6
|
+
export declare function placementGround(terrain: TerraformSnapshot | undefined, base: TerrainField | null): TerrainField | null;
|
|
7
|
+
/** Snap XZ and resample the shared ground field; reject a nonfinite surface height. @internal */
|
|
8
|
+
export declare function snapPlacement(point: EditorVec3, mode: SnapMode, gridSize: number, groundHeightAt: (x: number, z: number) => number): EditorVec3 | null;
|
|
9
|
+
/** Distance from a pointer to a projected path segment, including either endpoint. @internal */
|
|
10
|
+
export declare function pathSegmentDistance(pointer: {
|
|
11
|
+
x: number;
|
|
12
|
+
y: number;
|
|
13
|
+
}, a: {
|
|
14
|
+
x: number;
|
|
15
|
+
y: number;
|
|
16
|
+
}, b: {
|
|
17
|
+
x: number;
|
|
18
|
+
y: number;
|
|
19
|
+
}): number;
|
|
20
|
+
/** Group transforms use the translation command for every selected object. @internal */
|
|
21
|
+
export declare function selectionGizmoMode(mode: GizmoMode, kind: string, count: number): GizmoMode;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { editableTerrainFromSnapshot } from "@jgengine/core/world/terraform";
|
|
2
|
+
/** Compose authored sculpt offsets over the same base field as the terrain preview. @internal */
|
|
3
|
+
export function placementGround(terrain, base) {
|
|
4
|
+
return terrain === undefined ? base : editableTerrainFromSnapshot(terrain, base ?? undefined);
|
|
5
|
+
}
|
|
6
|
+
/** Snap XZ and resample the shared ground field; reject a nonfinite surface height. @internal */
|
|
7
|
+
export function snapPlacement(point, mode, gridSize, groundHeightAt) {
|
|
8
|
+
if (mode !== "grid" || !Number.isFinite(gridSize) || gridSize <= 0)
|
|
9
|
+
return point;
|
|
10
|
+
const x = Math.round(point.x / gridSize) * gridSize;
|
|
11
|
+
const z = Math.round(point.z / gridSize) * gridSize;
|
|
12
|
+
if (!Number.isFinite(x) || !Number.isFinite(z))
|
|
13
|
+
return null;
|
|
14
|
+
const y = groundHeightAt(x, z);
|
|
15
|
+
return Number.isFinite(y) ? { x, y, z } : null;
|
|
16
|
+
}
|
|
17
|
+
/** Distance from a pointer to a projected path segment, including either endpoint. @internal */
|
|
18
|
+
export function pathSegmentDistance(pointer, a, b) {
|
|
19
|
+
const dx = b.x - a.x;
|
|
20
|
+
const dy = b.y - a.y;
|
|
21
|
+
const lengthSquared = dx * dx + dy * dy;
|
|
22
|
+
const t = lengthSquared === 0 ? 0 : Math.max(0, Math.min(1, ((pointer.x - a.x) * dx + (pointer.y - a.y) * dy) / lengthSquared));
|
|
23
|
+
return Math.hypot(pointer.x - a.x - t * dx, pointer.y - a.y - t * dy);
|
|
24
|
+
}
|
|
25
|
+
/** Group transforms use the translation command for every selected object. @internal */
|
|
26
|
+
export function selectionGizmoMode(mode, kind, count) {
|
|
27
|
+
if (count > 1)
|
|
28
|
+
return "translate";
|
|
29
|
+
if (kind === "marker")
|
|
30
|
+
return mode === "scale" ? "translate" : mode;
|
|
31
|
+
if (kind === "volume")
|
|
32
|
+
return mode === "rotate" ? "translate" : mode;
|
|
33
|
+
return "translate";
|
|
34
|
+
}
|
|
@@ -16,6 +16,7 @@ export interface HandlerContext {
|
|
|
16
16
|
api: EditorHostApi;
|
|
17
17
|
session: EditorSession;
|
|
18
18
|
liveSync: DocumentLiveSync;
|
|
19
|
+
persistAssetUrls?: boolean;
|
|
19
20
|
gameId: string;
|
|
20
21
|
catalogDefinitions: readonly EditorCatalogDefinition[];
|
|
21
22
|
catalogById: ReadonlyMap<string, EditorCatalogDefinition>;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { HandlerTable } from "./context.js";
|
|
2
2
|
/** Document, selection, camera, mode, asset placement, and status verbs. */
|
|
3
|
-
export declare const documentHandlers: Pick<HandlerTable, "editor_status" | "set_mode" | "perf_report" | "list_layers" | "list_selection" | "get_marker" | "get_volume" | "set_transform" | "set_volume" | "set_path" | "add_path" | "add_marker" | "set_marker" | "set_note" | "set_meta" | "apply_preset" | "select" | "clear_selection" | "camera_goto" | "camera_frame" | "scene_summary" | "export_document" | "import_document" | "dispatch" | "undo" | "redo" | "list_assets" | "place_asset" | "batch_set_properties" | "assign_material" | "set_object_flags">;
|
|
3
|
+
export declare const documentHandlers: Pick<HandlerTable, "editor_status" | "set_mode" | "perf_report" | "list_layers" | "list_selection" | "get_marker" | "get_volume" | "set_transform" | "set_volume" | "set_path" | "add_path" | "add_marker" | "set_marker" | "set_note" | "set_meta" | "apply_preset" | "select" | "clear_selection" | "camera_goto" | "camera_frame" | "scene_summary" | "get_simulation" | "set_simulation" | "export_document" | "import_document" | "dispatch" | "undo" | "redo" | "list_assets" | "place_asset" | "batch_set_properties" | "list_material_assets" | "list_material_slots" | "upsert_material_asset" | "remove_material_asset" | "assign_material_asset" | "clear_material_assets" | "assign_material" | "set_object_flags">;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseEditorMaterialAsset } from "@jgengine/core/editor/materialAuthoring";
|
|
1
2
|
import { editorDocumentBounds, listEditorKinds, summarizeEditorSession, } from "@jgengine/core/editor/index";
|
|
2
3
|
import { resolvePlaceAsset, toEditorMarker } from "@jgengine/core/world/placeAsset";
|
|
3
4
|
import { findSchemaPreset, getSceneKind, validateParams } from "@jgengine/core/scene/sceneKinds";
|
|
@@ -314,6 +315,11 @@ export const documentHandlers = {
|
|
|
314
315
|
ctx.api.setFocusTarget(target);
|
|
315
316
|
return { ok: true, result: { target, bounds } };
|
|
316
317
|
},
|
|
318
|
+
get_simulation: (ctx) => ({ ok: true, result: { simulation: structuredClone(ctx.session.getState().document.simulation ?? {}) } }),
|
|
319
|
+
set_simulation: (ctx, request) => {
|
|
320
|
+
ctx.session.dispatch({ type: "setSimulation", simulation: request.simulation === null ? undefined : request.simulation });
|
|
321
|
+
return { ok: true, result: { simulation: structuredClone(ctx.session.getState().document.simulation ?? {}) } };
|
|
322
|
+
},
|
|
317
323
|
scene_summary: (ctx) => ({
|
|
318
324
|
ok: true,
|
|
319
325
|
result: {
|
|
@@ -361,7 +367,7 @@ export const documentHandlers = {
|
|
|
361
367
|
kind: request.kind,
|
|
362
368
|
knownKind: asset?.kind,
|
|
363
369
|
knownLabel: asset?.label,
|
|
364
|
-
knownUrl: asset?.url,
|
|
370
|
+
knownUrl: ctx.persistAssetUrls === false ? undefined : asset?.url,
|
|
365
371
|
});
|
|
366
372
|
const placedMarker = toEditorMarker(placed);
|
|
367
373
|
// URL-backed catalog models need a first-class catalogId so AuthoredObjects places the mesh;
|
|
@@ -411,6 +417,28 @@ export const documentHandlers = {
|
|
|
411
417
|
});
|
|
412
418
|
return { ok: true, result: summarizeEditorSession(ctx.session.getState()) };
|
|
413
419
|
},
|
|
420
|
+
list_material_assets: (ctx) => ({ ok: true, result: { assets: ctx.session.getState().document.materialAssets ?? [], assignments: ctx.session.getState().document.markers.filter((marker) => marker.meta?.materialAssignments !== undefined).map((marker) => ({ id: marker.id, assignments: marker.meta.materialAssignments })) } }),
|
|
421
|
+
list_material_slots: (ctx, request) => {
|
|
422
|
+
const inventory = ctx.api.getMaterialSlots(request.id);
|
|
423
|
+
return inventory.status === "ready" ? { ok: true, result: inventory } : { ok: false, result: inventory, error: inventory.status === "loading" ? "Material slots are still loading." : inventory.reason };
|
|
424
|
+
},
|
|
425
|
+
upsert_material_asset: (ctx, request) => {
|
|
426
|
+
const asset = parseEditorMaterialAsset(request.asset);
|
|
427
|
+
ctx.session.dispatch({ type: "upsertMaterialAsset", asset }, request.coalesce === undefined ? undefined : { coalesce: request.coalesce });
|
|
428
|
+
return { ok: true, result: { asset } };
|
|
429
|
+
},
|
|
430
|
+
remove_material_asset: (ctx, request) => {
|
|
431
|
+
const result = ctx.session.transaction([{ type: "removeMaterialAsset", id: request.id }]);
|
|
432
|
+
return result.ok ? { ok: true, result: summarizeEditorSession(result.state) } : { ok: false, error: result.error };
|
|
433
|
+
},
|
|
434
|
+
assign_material_asset: (ctx, request) => {
|
|
435
|
+
const result = ctx.session.transaction([{ type: "assignMaterialAsset", ids: request.ids, materialId: request.materialId, selector: request.selector }]);
|
|
436
|
+
return result.ok ? { ok: true, result: summarizeEditorSession(result.state) } : { ok: false, error: result.error };
|
|
437
|
+
},
|
|
438
|
+
clear_material_assets: (ctx, request) => {
|
|
439
|
+
const result = ctx.session.transaction([{ type: "clearMaterialAssets", ids: request.ids }]);
|
|
440
|
+
return result.ok ? { ok: true, result: summarizeEditorSession(result.state) } : { ok: false, error: result.error };
|
|
441
|
+
},
|
|
414
442
|
assign_material: (ctx, request) => {
|
|
415
443
|
if (!documentHasAnyId(ctx.session.getState().document, request.ids)) {
|
|
416
444
|
return { ok: false, error: "assign_material matched no objects" };
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { HandlerTable } from "./context.js";
|
|
2
2
|
/** Parent/child hierarchy and prefab library verbs. */
|
|
3
|
-
export declare const hierarchyHandlers: Pick<HandlerTable, "set_parent" | "hierarchy" | "list_prefabs" | "create_prefab" | "insert_prefab" | "detach_prefab_instance" | "delete_prefab">;
|
|
3
|
+
export declare const hierarchyHandlers: Pick<HandlerTable, "set_parent" | "hierarchy" | "list_prefabs" | "create_prefab" | "insert_prefab" | "detach_prefab_instance" | "delete_prefab" | "set_prefab_static_bake">;
|
|
@@ -45,6 +45,10 @@ export const hierarchyHandlers = {
|
|
|
45
45
|
ctx.session.dispatch({ type: "detachPrefabInstance", instanceId: request.instanceId });
|
|
46
46
|
return { ok: true, result: summarizeEditorSession(ctx.session.getState()) };
|
|
47
47
|
},
|
|
48
|
+
set_prefab_static_bake: (ctx, request) => {
|
|
49
|
+
const result = ctx.session.transaction([{ type: "setPrefabStaticBake", prefabId: request.prefabId, bake: request.bake }]);
|
|
50
|
+
return result.ok ? { ok: true, result: findEditorPrefab(ctx.session.getState().document, request.prefabId) } : { ok: false, error: result.error };
|
|
51
|
+
},
|
|
48
52
|
delete_prefab: (ctx, request) => {
|
|
49
53
|
if (findEditorPrefab(ctx.session.getState().document, request.prefabId) === undefined) {
|
|
50
54
|
return { ok: false, error: `prefab not found: ${request.prefabId}` };
|
package/dist/handlers/runtime.js
CHANGED
|
@@ -11,8 +11,8 @@ export const runtimeHandlers = {
|
|
|
11
11
|
if (patch.type !== "snapshot" && patch.type !== "commands") {
|
|
12
12
|
return { ok: false, error: `unknown document patch type: ${String(patch.type)} (snapshot | commands)` };
|
|
13
13
|
}
|
|
14
|
-
if (
|
|
15
|
-
return { ok: false, error: "push_document_patch requires a
|
|
14
|
+
if (!Number.isSafeInteger(patch.baseRevision) || patch.baseRevision < 0) {
|
|
15
|
+
return { ok: false, error: "push_document_patch requires a nonnegative safe-integer baseRevision" };
|
|
16
16
|
}
|
|
17
17
|
if (!force && patch.baseRevision !== ctx.liveSync.getRevision()) {
|
|
18
18
|
return { ok: false, error: `baseRevision mismatch: patch=${patch.baseRevision} current=${ctx.liveSync.getRevision()}` };
|
|
@@ -32,13 +32,10 @@ export const runtimeHandlers = {
|
|
|
32
32
|
return { ok: false, error: "commands patch requires a commands array" };
|
|
33
33
|
if (patch.commands.length === 0)
|
|
34
34
|
return { ok: false, error: "commands patch is empty" };
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
return { ok: true, result: { revision: ctx.liveSync.getRevision(), ...summarizeEditorSession(ctx.session.getState()) } };
|
|
35
|
+
const transaction = ctx.session.transaction(patch.commands);
|
|
36
|
+
if (!transaction.ok)
|
|
37
|
+
return { ok: false, error: transaction.error, result: { commandIndex: transaction.commandIndex, revision: ctx.liveSync.getRevision() } };
|
|
38
|
+
return { ok: true, result: { revision: ctx.liveSync.getRevision(), changed: transaction.changed, ...summarizeEditorSession(transaction.state) } };
|
|
42
39
|
},
|
|
43
40
|
pull_document_patches: (ctx, request) => ({
|
|
44
41
|
ok: true,
|
|
@@ -60,14 +57,20 @@ export const runtimeHandlers = {
|
|
|
60
57
|
});
|
|
61
58
|
return { ok: true, result: delta };
|
|
62
59
|
},
|
|
63
|
-
pull_runtime_deltas: (ctx, request) =>
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
60
|
+
pull_runtime_deltas: (ctx, request) => {
|
|
61
|
+
const sinceSeq = request.sinceSeq ?? 0;
|
|
62
|
+
const snapshot = ctx.liveSync.getRuntimeState();
|
|
63
|
+
const deltas = ctx.liveSync.pullRuntimeDeltas(sinceSeq);
|
|
64
|
+
const historyGap = deltas.length > 0 && deltas[0].seq > sinceSeq + 1;
|
|
65
|
+
return {
|
|
66
|
+
ok: true,
|
|
67
|
+
result: {
|
|
68
|
+
seq: snapshot.seq,
|
|
69
|
+
deltas: historyGap ? [] : deltas,
|
|
70
|
+
...(request.includeSnapshot === true || historyGap ? { snapshot } : {}),
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
},
|
|
71
74
|
runtime_snapshot: (ctx) => ({ ok: true, result: ctx.liveSync.getRuntimeState() }),
|
|
72
75
|
runtime_summary: (ctx) => ({
|
|
73
76
|
ok: true,
|