@jgengine/editor 0.19.0 → 0.21.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 +346 -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 +3 -1
- 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/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 +22 -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 +102 -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
|
@@ -172,6 +172,7 @@ function unionRect(a, b) {
|
|
|
172
172
|
}
|
|
173
173
|
/** Memoized so cursor-hover state updates in the parent never reconcile the mesh subtree. */
|
|
174
174
|
const SculptMesh = memo(function SculptMesh({ terrain, bounds, segments, revision, pendingRef, paletteAt, heightRange, contourStrength, }) {
|
|
175
|
+
const invalidate = useThree((state) => state.invalidate);
|
|
175
176
|
const geometry = useMemo(() => {
|
|
176
177
|
const width = Math.max(1, bounds.maxX - bounds.minX);
|
|
177
178
|
const depth = Math.max(1, bounds.maxZ - bounds.minZ);
|
|
@@ -188,8 +189,9 @@ const SculptMesh = memo(function SculptMesh({ terrain, bounds, segments, revisio
|
|
|
188
189
|
pendingRef.current = null;
|
|
189
190
|
const region = pending !== null && pending.kind === "region" ? pending.region : null;
|
|
190
191
|
displace(geometry, terrain, bounds, region, paletteAt, heightRange, contourStrength);
|
|
192
|
+
invalidate();
|
|
191
193
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
192
|
-
}, [geometry, terrain, revision, paletteAt, heightRange, contourStrength]);
|
|
194
|
+
}, [geometry, terrain, revision, paletteAt, heightRange, contourStrength, invalidate]);
|
|
193
195
|
return (_jsx("mesh", { geometry: geometry, receiveShadow: true, children: _jsx("meshStandardMaterial", { vertexColors: true, roughness: 0.95, metalness: 0, polygonOffset: true, polygonOffsetFactor: -1, polygonOffsetUnits: -1 }) }));
|
|
194
196
|
});
|
|
195
197
|
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,
|
package/dist/handlers/terrain.js
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { createTerrainSnapshot, editableTerrainFromSnapshot, } from "@jgengine/core/world/terraform";
|
|
2
2
|
import { resolveScatter, resolveScatterRegion, scatterRegionEstimate, scatterRegionFromPath, SCATTER_PATH_KIND, } from "@jgengine/core/world/scatterRegion";
|
|
3
3
|
import { TERRAIN_MATERIALS } from "../uiStore.js";
|
|
4
|
+
function brushError(request) {
|
|
5
|
+
if (![request.x, request.z, request.radius ?? 8, request.strength ?? 1].every(Number.isFinite))
|
|
6
|
+
return "brush coordinates, radius and strength must be finite";
|
|
7
|
+
if ((request.radius ?? 8) <= 0)
|
|
8
|
+
return "brush radius must be positive";
|
|
9
|
+
if ((request.strength ?? 1) < 0)
|
|
10
|
+
return "brush strength must be nonnegative";
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
4
13
|
/** Sculpt heightfield, material painting, terrain layers, and foliage/scatter verbs. */
|
|
5
14
|
export const terrainHandlers = {
|
|
6
15
|
create_terrain: (ctx, request) => {
|
|
@@ -8,17 +17,35 @@ export const terrainHandlers = {
|
|
|
8
17
|
const depth = request.depth ?? 200;
|
|
9
18
|
const cx = request.centerX ?? 0;
|
|
10
19
|
const cz = request.centerZ ?? 0;
|
|
20
|
+
const cellSize = request.cellSize ?? 2;
|
|
21
|
+
if (![width, depth, cx, cz, cellSize].every(Number.isFinite) || width <= 0 || depth <= 0 || cellSize <= 0) {
|
|
22
|
+
return { ok: false, error: "terrain dimensions and cellSize must be finite and positive; center must be finite" };
|
|
23
|
+
}
|
|
24
|
+
if ((Math.max(1, Math.round(width / cellSize)) + 1) * (Math.max(1, Math.round(depth / cellSize)) + 1) > 1_000_000) {
|
|
25
|
+
return { ok: false, error: "terrain exceeds 1,000,000 vertices — increase cellSize or reduce width/depth" };
|
|
26
|
+
}
|
|
27
|
+
const bounds = { minX: cx - width / 2, minZ: cz - depth / 2, maxX: cx + width / 2, maxZ: cz + depth / 2 };
|
|
28
|
+
if (!Object.values(bounds).every(Number.isFinite) || bounds.maxX <= bounds.minX || bounds.maxZ <= bounds.minZ) {
|
|
29
|
+
return { ok: false, error: "terrain bounds must have a finite, nonzero extent" };
|
|
30
|
+
}
|
|
11
31
|
const terrain = createTerrainSnapshot({
|
|
12
|
-
bounds
|
|
13
|
-
cellSize
|
|
32
|
+
bounds,
|
|
33
|
+
cellSize,
|
|
14
34
|
});
|
|
15
35
|
ctx.session.dispatch({ type: "setTerrain", terrain });
|
|
16
36
|
return { ok: true, result: { cols: terrain.cols, rows: terrain.rows, cellSize: terrain.cellSize } };
|
|
17
37
|
},
|
|
18
38
|
sculpt_terrain: (ctx, request) => {
|
|
39
|
+
const invalid = brushError(request);
|
|
40
|
+
if (invalid !== null)
|
|
41
|
+
return { ok: false, error: invalid };
|
|
19
42
|
const terrain = ctx.session.getState().document.terrain;
|
|
20
43
|
if (terrain === undefined)
|
|
21
44
|
return { ok: false, error: "no terrain — call create_terrain first" };
|
|
45
|
+
if (request.mode === "ramp" && (request.toX === undefined || request.toZ === undefined))
|
|
46
|
+
return { ok: false, error: "ramp requires toX and toZ" };
|
|
47
|
+
if ([request.target, request.toX, request.toZ, request.seed].some((value) => value !== undefined && !Number.isFinite(value)))
|
|
48
|
+
return { ok: false, error: "sculpt target, ramp endpoint and seed must be finite" };
|
|
22
49
|
const live = editableTerrainFromSnapshot(terrain);
|
|
23
50
|
const edit = {
|
|
24
51
|
mode: request.mode,
|
|
@@ -66,6 +93,9 @@ export const terrainHandlers = {
|
|
|
66
93
|
};
|
|
67
94
|
},
|
|
68
95
|
paint_terrain: (ctx, request) => {
|
|
96
|
+
const invalid = brushError(request);
|
|
97
|
+
if (invalid !== null)
|
|
98
|
+
return { ok: false, error: invalid };
|
|
69
99
|
const terrain = ctx.session.getState().document.terrain;
|
|
70
100
|
if (terrain === undefined)
|
|
71
101
|
return { ok: false, error: "no terrain — call create_terrain first" };
|
|
@@ -120,23 +150,31 @@ export const terrainHandlers = {
|
|
|
120
150
|
if (ctx.session.getState().document.terrain === undefined) {
|
|
121
151
|
return { ok: false, error: "no terrain — call create_terrain first" };
|
|
122
152
|
}
|
|
153
|
+
if (new Set(request.layers.map((layer) => layer.id)).size !== request.layers.length || request.layers.some((layer) => layer.id.length === 0 || layer.surface.length === 0)) {
|
|
154
|
+
return { ok: false, error: "terrain layers require unique nonempty ids and nonempty surfaces" };
|
|
155
|
+
}
|
|
123
156
|
ctx.session.dispatch({ type: "setTerrainLayers", layers: request.layers });
|
|
124
157
|
return { ok: true, result: { layers: ctx.session.getState().document.terrain?.layers ?? [] } };
|
|
125
158
|
},
|
|
126
159
|
blend_terrain: (ctx, request) => {
|
|
160
|
+
const invalid = brushError(request);
|
|
161
|
+
if (invalid !== null)
|
|
162
|
+
return { ok: false, error: invalid };
|
|
127
163
|
const terrain = ctx.session.getState().document.terrain;
|
|
128
164
|
if (terrain === undefined)
|
|
129
165
|
return { ok: false, error: "no terrain — call create_terrain first" };
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
166
|
+
const live = editableTerrainFromSnapshot(terrain);
|
|
167
|
+
if ((request.strength ?? 1) > 1)
|
|
168
|
+
return { ok: false, error: "blend strength must be between 0 and 1" };
|
|
169
|
+
const addLayer = !live.layers.some((layer) => layer.surface === request.surface);
|
|
170
|
+
if (addLayer) {
|
|
171
|
+
let id = request.surface;
|
|
172
|
+
let suffix = 1;
|
|
173
|
+
while (live.layers.some((layer) => layer.id === id))
|
|
174
|
+
id = `${request.surface}_${suffix++}`;
|
|
175
|
+
const layers = [...live.layers, { id, surface: request.surface }];
|
|
176
|
+
live.setLayers(layers);
|
|
135
177
|
}
|
|
136
|
-
const snapshot = ctx.session.getState().document.terrain;
|
|
137
|
-
if (snapshot === undefined)
|
|
138
|
-
return { ok: false, error: "no terrain — call create_terrain first" };
|
|
139
|
-
const live = editableTerrainFromSnapshot(snapshot);
|
|
140
178
|
const delta = live.blendPaintDelta({
|
|
141
179
|
mode: "paint",
|
|
142
180
|
center: [request.x, request.z],
|
|
@@ -147,7 +185,9 @@ export const terrainHandlers = {
|
|
|
147
185
|
});
|
|
148
186
|
if (delta.indices.length === 0)
|
|
149
187
|
return { ok: false, error: "blend touched no cells (check radius/position)" };
|
|
150
|
-
ctx.session.dispatch(
|
|
188
|
+
ctx.session.dispatch(addLayer
|
|
189
|
+
? { type: "setTerrain", terrain: live.snapshot() }
|
|
190
|
+
: { type: "blendTerrain", delta });
|
|
151
191
|
return { ok: true, result: { changed: delta.indices.length, layers: ctx.session.getState().document.terrain?.layers ?? [] } };
|
|
152
192
|
},
|
|
153
193
|
convert_scatter: (ctx, request) => {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { EditorApp, type EditorAppProps, type EditorSaveFn } from "./EditorApp.js";
|
|
2
|
+
export { CreatorApp, type CreatorConfig } from "./CreatorApp.js";
|
|
2
3
|
export { buildEditorNetworkSnapshot, isNetworkMultiplayerConfigured, type EditorNetworkAdapterKind, type EditorNetworkPresenceActor, type EditorNetworkPresenceInput, type EditorNetworkSession, type EditorNetworkSnapshot, } from "./networkSnapshot.js";
|
|
3
4
|
export { StandaloneEditor, createBlankPlayable, blankWorld, downloadSaver, importAssetToHost, loadDroppedAssets, mergeStandaloneAssets, type StandaloneEditorProps, type StandaloneAsset, type AssetImporter, type BlankPlayableOptions, } from "./StandaloneEditor.js";
|
|
4
5
|
export { EditorLayerOverlays, PathDraftPreview } from "./DebugDraw.js";
|
|
@@ -9,7 +10,7 @@ export { TerrainSculpt } from "./TerrainSculpt.js";
|
|
|
9
10
|
export { ScatterPreview } from "./ScatterPreview.js";
|
|
10
11
|
export { EditorCameraDriver } from "./EditorCameraDriver.js";
|
|
11
12
|
export { AssetBrowser, assetsFromCatalog, editorAssetFromImport, mergeEditorAssets, type EditorAssetEntry, } from "./AssetBrowser.js";
|
|
12
|
-
export { createEditorHost, getEditorHost, installEditorHost, type EditorAssetInfo, type EditorBridgeRequest, type EditorBridgeResponse, type EditorHostApi, type EditorPerfSample, type EditorRunMode, } from "./session.js";
|
|
13
|
+
export { createEditorHost, getEditorHost, installEditorHost, type EditorAssetInfo, type EditorBridgeRequest, type EditorBridgeResponse, type EditorHostApi, type EditorMaterialSlotInfo, type EditorMaterialSlotReport, type EditorMaterialSlotInventory, type EditorPerfSample, type EditorRunMode, } from "./session.js";
|
|
13
14
|
export { PerfProbe } from "./PerfProbe.js";
|
|
14
15
|
export { useStoreSelector, shallowArrayEqual, virtualWindow, type SubscribableStore, type VirtualWindow, } from "./useStoreSelector.js";
|
|
15
16
|
export { useF2Chord } from "./useF2Chord.js";
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { EditorApp } from "./EditorApp.js";
|
|
2
|
+
export { CreatorApp } from "./CreatorApp.js";
|
|
2
3
|
export { buildEditorNetworkSnapshot, isNetworkMultiplayerConfigured, } from "./networkSnapshot.js";
|
|
3
4
|
export { StandaloneEditor, createBlankPlayable, blankWorld, downloadSaver, importAssetToHost, loadDroppedAssets, mergeStandaloneAssets, } from "./StandaloneEditor.js";
|
|
4
5
|
export { EditorLayerOverlays, PathDraftPreview } from "./DebugDraw.js";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type MaterialAsset, type MaterialFamily, type MaterialSurfaceParameters } from "@jgengine/core/material/materialAsset";
|
|
2
|
+
/** Prepare an explicit artist edit by retaining declarations and adding its shader requirements. @internal */
|
|
3
|
+
export declare function prepareMaterialAuthoringEdit(asset: MaterialAsset): MaterialAsset;
|
|
4
|
+
/** One physical surface field in the material workspace. */
|
|
5
|
+
export interface MaterialControl {
|
|
6
|
+
key: keyof MaterialSurfaceParameters;
|
|
7
|
+
label: string;
|
|
8
|
+
kind: "number" | "color";
|
|
9
|
+
fallback: number | string;
|
|
10
|
+
min?: number;
|
|
11
|
+
max?: number;
|
|
12
|
+
step?: number;
|
|
13
|
+
hint?: string;
|
|
14
|
+
}
|
|
15
|
+
/** A family-specific group of primary controls; maps and full physical fields remain available. */
|
|
16
|
+
export interface MaterialControlGroup {
|
|
17
|
+
label: string;
|
|
18
|
+
hint: string;
|
|
19
|
+
controls: readonly MaterialControl[];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Returns six primary groups suited to the material family without hiding the physical model.
|
|
23
|
+
* @capability editor-material-controls Group physical controls by surface family while retaining advanced fields.
|
|
24
|
+
*/
|
|
25
|
+
export declare function materialControlGroups(family: MaterialFamily): readonly MaterialControlGroup[];
|
|
26
|
+
/** Complete supported physical scalar/color fields, independent of the six primary groups. */
|
|
27
|
+
export declare const advancedMaterialControls: readonly MaterialControl[];
|
|
28
|
+
/**
|
|
29
|
+
* Reports geometry prerequisites and renderer approximations to authors.
|
|
30
|
+
* @capability editor-material-controls Explain geometry prerequisites and approximations for a material asset.
|
|
31
|
+
*/
|
|
32
|
+
export declare function materialAuthoringNotes(asset: MaterialAsset): readonly string[];
|