Files
EVEngine/examples/composable-editor/main.nut
T

661 lines
27 KiB
Plaintext

// Composable editor SDK example.
//
// The C++ editor module owns only semantic workspace/tool/selection/command
// models. This file is a project-specific presenter: replacing panelBuilders
// creates a different editor without modifying the engine.
dofile("dialogue_component.nut");
dofile("gameplay_components.nut");
const MAP_W = 48;
const MAP_H = 48;
const CELL = 0.55;
const HEIGHT_SCALE = 3.5;
class WorldPosition extends eve.Component {
x = 0.0
y = 0.0
z = 0.0
}
class WorldIdentity extends eve.Component {
id = ""
kind = "prop"
}
class RuntimeObject extends eve.Entity {
position = WorldPosition
identity = WorldIdentity
}
// ViewModel: reflected metadata drives the optional generated inspector; the
// custom panels below bind to the same live object.
class WorldEditorVM {
</ editor = "combo", options = "raise,lower" />
tool = "raise"
</ editor = "slider", min = 1, max = 12 />
brushRadius = 4.0
</ editor = "slider", min = 0.01, max = 0.20 />
strength = 0.06
</ editor = "combo", options = "grass,rock,sand" />
material = "grass"
</ editor = "combo", options = "edit,play,simulate" />
mode = "edit"
seed = 20260825
selected = "terrain"
status = "Workspace composed from project descriptors"
}
persist state = {
workspace = null
session = null
vm = null
heightmap = null
terrainMesh = null
terrainEntity = null
terrainTarget = null
brushFalloff = null
brushKernel = null
brushOperation = null
brushTool = null
procgenAlgorithm = "cave.cellular"
procgenParams = null
procgenSchema = null
procgenPreview = null
materialRecipe = "pbr.rock"
materialParams = null
materialSchema = null
materialResource = null
materialSet = null
dialogueComponent = null
gameplayComponents = null
camera = null
panelsMounted = false
objects = []
orbiting = false
sculpting = false
sculptX = 0.0
sculptY = 0.0
lastX = 0.0
lastY = 0.0
yaw = 0.72
pitch = 0.52
distance = 23.0
meshDirty = false
meshCooldown = 0.0
lastSelectionSequence = 0
lastTargetRevision = 0
frameCount = 0
screenshotSaved = false
}
materials <- {
grass = { r=0.24, g=0.46, b=0.22, metallic=0.0, roughness=0.92 },
rock = { r=0.42, g=0.44, b=0.48, metallic=0.08, roughness=0.76 },
sand = { r=0.70, g=0.58, b=0.32, metallic=0.0, roughness=0.98 },
};
heightmapTargets <- eve.HeightmapTargetModule()
function configureWorkspace() {
state.workspace = editor.newWorkspace("world-authoring", "Project World Authoring");
state.workspace.setRegionSize("top", 54.0);
state.workspace.setRegionSize("left", 230.0);
state.workspace.setRegionSize("right", 280.0);
state.workspace.setRegionSize("bottom", 180.0);
state.workspace.layout(config.width.tofloat(), config.height.tofloat());
// These are project descriptors, not engine-known panel types.
state.workspace.registerPanel("toolbar", "World Tools", "top", 0);
state.workspace.registerPanel("hierarchy", "Hierarchy", "left", 10);
state.workspace.registerPanel("scene", "Scene View", "center", 20);
state.workspace.registerPanel("inspector", "Terrain + Material", "right", 30);
state.workspace.registerPanel("palette", "Project Extensions", "bottom", 40);
state.workspace.setPanelCapability("scene", "scene.viewport.3d");
state.workspace.setPanelCapability("inspector", "property.terrain");
state.workspace.setPanelContext("scene", "scene");
state.workspace.setPanelContext("palette", "asset");
}
function panelToolbar() {
ui.beginRow("commands", 8.0);
ui.button("Edit", "mode-edit");
ui.button("Play", "mode-play");
ui.button("Simulate", "mode-simulate");
ui.separator("sep-mode");
ui.button("Raise", "tool-raise");
ui.button("Lower", "tool-lower");
ui.button("Undo", "edit-undo");
ui.button("Redo", "edit-redo");
ui.button("Regenerate", "terrain-regenerate");
ui.button("Reflected Inspector", "reflect-inspector");
ui.text("", "mode-label");
ui.end();
}
function panelHierarchy() {
ui.text("Scene", "heading");
ui.listItem("Terrain", "select-terrain");
ui.listItem("Directional Light", "select-light");
ui.listItem("Gameplay / RTS Spawn", "select-rts");
ui.listItem("Gameplay / Card Table", "select-card");
ui.listItem("Gameplay / Voxel Volume", "select-voxel");
ui.separator("sep-hierarchy");
ui.text("ECS runtime objects", "ecs-title");
ui.text("", "ecs-count");
ui.textWrapped("The hierarchy and game runtime share stable semantic IDs.", 190.0, "ecs-help");
}
function panelScene() {
ui.text("Live runtime viewport · LMB sculpt · RMB orbit · wheel zoom", "scene-help");
ui.viewport("scene-vp", state.workspace.getRegionW("center") - 20.0,
state.workspace.getRegionH("center") - 58.0);
ui.text("", "scene-status");
}
// The project decides layout and filtering; RecipeDescriptor only supplies
// portable metadata. This same builder handles algorithms, textures and PBR.
function renderRecipeFields(schema, params, prefix, horizontal, wanted) {
if (horizontal) ui.beginRow(prefix + "schema-row", 8.0);
for (local i = 0; i < schema.getParamCount(); ++i) {
if (schema.isParamAdvanced(i)) continue;
local key = schema.getParamKey(i);
if (wanted != null && !(key in wanted)) continue;
local label = schema.getParamLabel(i);
local kind = schema.getParamKind(i);
local id = prefix + key;
local hiddenLabel = "##" + prefix + key;
ui.beginColumn(prefix + "field-" + key, 2.0);
ui.text(label, prefix + "label-" + key);
if (kind == "float")
ui.slider(hiddenLabel, params.getFloat(key, 0.0), schema.getParamMinimum(i), schema.getParamMaximum(i), id);
else if (kind == "int")
ui.slider(hiddenLabel, params.getInt(key, 0).tofloat(), schema.getParamMinimum(i), schema.getParamMaximum(i), id);
else if (kind == "bool") ui.checkbox(hiddenLabel, params.getInt(key, 0) != 0, id);
else if (kind == "choice") {
local choices = "";
local selected = 0;
local current = params.getString(key, "");
for (local c = 0; c < schema.getParamChoiceCount(i); ++c) {
local value = schema.getParamChoice(i, c);
if (c > 0) choices += ",";
choices += value;
if (value == current) selected = c;
}
ui.combo(hiddenLabel, choices, selected, id);
} else ui.inputText(hiddenLabel, params.getString(key, ""), id);
ui.setItemSize(horizontal ? 145.0 : 235.0, 0.0);
ui.end();
if (horizontal) ui.setItemSize(165.0, 0.0);
}
if (horizontal) ui.end();
}
function panelInspector() {
ui.text("MVVM Terrain Tool", "inspector-title");
ui.text("", "selection");
ui.combo("Tool", "raise,lower", 0, "tool");
ui.slider("Brush radius", state.vm.brushRadius, 1.0, 12.0, "brush");
ui.slider("Strength", state.vm.strength, 0.01, 0.20, "strength");
ui.separator("sep-material");
ui.text("Material", "material-title");
ui.button("Grass", "material-grass");
ui.button("Rock", "material-rock");
ui.button("Sand", "material-sand");
ui.text("", "material-current");
ui.separator("sep-pbr");
ui.text(state.materialSchema.getDisplayName() + " · recipe schema", "pbr-title");
renderRecipeFields(state.materialSchema, state.materialParams, "pbr-", false, {
scale=true, roughnessLow=true, roughnessHigh=true, metallic=true,
normalStrength=true, heightStrength=true
});
ui.button("Generate + Apply PBR", "material-generate-pbr");
ui.textWrapped("All controls write the reflected ViewModel; the model then updates terrain/runtime data.",
235.0, "mvvm-help");
}
function panelPalette() {
ui.text("Game modules inject editor actions through the same command service", "palette-title");
ui.beginRow("extension-actions", 8.0);
ui.button("Spawn RTS Unit", "spawn-rts");
ui.button("Add Card Table", "spawn-card");
ui.button("Add Voxel Volume", "spawn-voxel");
ui.button("Dialogue Node", "spawn-dialogue");
ui.button("Avatar Preview", "spawn-avatar");
ui.end();
state.dialogueComponent.render("dialogue-");
ui.separator("sep-procgen");
ui.text(state.procgenSchema.getDisplayName() + " · reflected schema", "procgen-title");
renderRecipeFields(state.procgenSchema, state.procgenParams, "procgen-", true, null);
ui.button("Generate", "procgen-generate");
ui.text("", "palette-status");
}
panelBuilders <- {
toolbar = panelToolbar,
hierarchy = panelHierarchy,
scene = panelScene,
inspector = panelInspector,
palette = panelPalette,
};
function mountWorkspacePanels() {
for (local i = 0; i < state.workspace.getPanelCount(); ++i) {
if (!state.workspace.getPanelVisible(i)) continue;
local id = state.workspace.getPanelId(i);
if (!(id in panelBuilders)) continue;
print("composable-editor: mounting " + id + "\n");
ui.beginBuild();
ui.beginWindow(state.workspace.getPanelTitle(i), "root");
panelBuilders[id]();
ui.end();
ui.mountBuildAs(id);
ui.select(id);
local region = state.workspace.getPanelRegion(i);
ui.setHostPos(state.workspace.getRegionX(region), state.workspace.getRegionY(region), 0.0, 0.0);
ui.setHostSize(state.workspace.getRegionW(region), state.workspace.getRegionH(region));
ui.setHostOverlay(false);
print("composable-editor: mounted " + id + "\n");
}
state.panelsMounted = true;
}
function generateTerrain() {
local paramsResult = procgen.newParams();
if (!paramsResult.ok) {
state.vm.status = "Parameter creation failed: " + paramsResult.status.summary;
return;
}
local params = paramsResult.value;
params.setSize(MAP_W, MAP_H);
params.setSeed(state.vm.seed);
params.setFloat("frequency", 1.0 / 18.0);
params.setInt("octaves", 5);
local heightmapResult = procgen.generateHeightmap(params);
if (heightmapResult.ok) {
state.heightmap = heightmapResult.value;
} else if (state.heightmap == null) {
local fallbackResult = procgen.newHeightmap(MAP_W, MAP_H);
if (!fallbackResult.ok) {
state.vm.status = "Heightmap creation failed: " + fallbackResult.status.summary;
return;
}
state.heightmap = fallbackResult.value;
}
if (state.terrainMesh == null) {
state.terrainMesh = heightmapTargets.newSmoothMesh(state.heightmap, CELL, HEIGHT_SCALE);
state.terrainEntity = eve.Renderable3D();
state.terrainEntity.setMesh(state.terrainMesh);
state.terrainEntity.setPosition(0.0, 0.0, 0.0);
state.terrainEntity.setVisible(true);
} else {
heightmapTargets.updateSmoothMesh(state.terrainMesh, gfx, state.heightmap, CELL, HEIGHT_SCALE);
}
applyMaterial(state.vm.material);
if (state.brushTool != null) bindTerrainTarget();
state.vm.status = "Generated terrain seed " + state.vm.seed;
}
function bindTerrainTarget() {
state.terrainTarget = heightmapTargets.create("world.terrain", state.heightmap);
heightmapTargets.bind(state.session, state.terrainTarget);
state.lastTargetRevision = state.terrainTarget.getRevision();
}
function configureTerrainTool() {
state.brushFalloff = editor.newSmoothBrushFalloff();
state.brushKernel = editor.newCircleBrushKernel();
state.brushKernel.setSmoothFalloff(state.brushFalloff);
state.brushOperation = editor.newAddScalarFieldOperation();
state.brushTool = editor.newFieldBrushTool("terrain.sculpt", "Sculpt Terrain");
state.brushTool.setCircleKernel(state.brushKernel);
state.brushTool.setAddScalarOperation(state.brushOperation);
state.session.addFieldTool(state.brushTool);
bindTerrainTarget();
state.session.activateTool("terrain.sculpt");
}
function applyMaterial(name) {
if (!(name in materials) || state.terrainEntity == null) return;
state.vm.material = name;
local m = materials[name];
state.terrainEntity.setTint(m.r, m.g, m.b, 1.0);
state.terrainEntity.setMetallic(m.metallic);
state.terrainEntity.setRoughness(m.roughness);
}
function generateAndApplyPbrMaterial() {
if (state.terrainEntity == null) return;
local materialResult = procgen.generatePbrMaterial(state.materialRecipe, state.materialParams);
if (!materialResult.ok) {
state.vm.status = "Material generation failed: " + materialResult.status.summary;
return;
}
state.materialSet = materialResult.value;
local albedo = gfx.newTexture(state.materialSet.getAlbedo(), true, true);
local normal = gfx.newTexture(state.materialSet.getNormal(), true, true);
local height = gfx.newTexture(state.materialSet.getHeight(), true, true);
state.materialResource = gfx.newMaterial();
state.materialResource.setAlbedoTexture(albedo);
state.materialResource.setNormalTexture(normal);
state.materialResource.setHeightTexture(height);
state.materialResource.setMetallic(state.materialParams.getFloat("metallic", 0.0));
local roughness = (state.materialParams.getFloat("roughnessLow", 0.5) +
state.materialParams.getFloat("roughnessHigh", 0.8)) * 0.5;
state.materialResource.setRoughness(roughness);
state.materialResource.setParallax(state.materialParams.getFloat("heightStrength", 1.0) * 0.025, 8.0, 24.0);
state.terrainEntity.setMaterial(state.materialResource);
state.materialSet.destroy();
state.vm.material = state.materialRecipe;
state.vm.status = "Generated " + state.materialSchema.getDisplayName() + " from reflected recipe parameters";
}
function registerProjectCommands() {
editor.registerScriptCommand("world.spawn-archetype", "Spawn Project Archetype", "World/Create",
function(payload) {
if (!("kind" in payload) || !("id" in payload)) return false;
local entity = RuntimeObject.create();
entity.identity.id = payload.id;
entity.identity.kind = payload.kind;
entity.position.x = 4.0 + state.objects.len() * 2.0;
entity.position.z = 4.0 + state.objects.len() * 1.5;
state.objects.push(entity);
state.workspace.select("world", "scene", "demo-world", payload.id, payload.kind, false);
state.vm.selected = payload.id;
state.vm.status = "Created " + payload.kind + " through shared editor/game command";
return true;
});
state.session = editor.newSession();
}
function spawnArchetype(kind) {
local id = kind + "." + (state.objects.len() + 1);
local result = state.session.executeCommand("world.spawn-archetype", { kind=kind, id=id });
if (!result.accepted) state.vm.status = "Command rejected: " + kind;
return result.accepted ? state.objects.top() : null;
}
function spawnCardTable() {
local instance = state.gameplayComponents.createCardInstance();
if (instance == null) {
state.vm.status = "Card definition rejected";
return;
}
spawnArchetype("card.table");
state.vm.status = "Card runtime instance " + instance.getInstanceId() +
" from schema-validated definition";
}
function spawnRtsUnit() {
local entity = spawnArchetype("rts.unit");
if (entity == null) return;
if (state.gameplayComponents.createRtsUnit(entity.identity.id, entity, 2.0, 2.0))
state.vm.status = "RTS unit " + entity.identity.id + " uses stable Crowd identity + terrain costs";
}
function applyDialogueDocument() {
if (state.dialogueComponent.apply(dialogueFlow)) {
spawnArchetype("dialogue.document");
state.vm.status = "Applied project-composed Dialogue document";
} else {
state.vm.status = "Dialogue validation failed: " + state.dialogueComponent.document.getDiagnosticMessage(0);
}
}
function updateRecipeParam(schema, params, prefix, id) {
local key = id.slice(prefix.len());
for (local i = 0; i < schema.getParamCount(); ++i) {
if (schema.getParamKey(i) != key) continue;
local kind = schema.getParamKind(i);
if (kind == "float") params.setFloat(key, ui.getValue(id));
else if (kind == "int") params.setInt(key, ui.getValue(id).tointeger());
else if (kind == "bool") params.setInt(key, ui.getChecked(id) ? 1 : 0);
else params.setString(key, ui.getValueText(id));
state.vm.status = "Schema property changed: " + key;
return;
}
}
function generateProcgenPreview() {
local previewResult = procgen.generate(state.procgenAlgorithm, state.procgenParams);
if (!previewResult.ok) {
state.vm.status = "Procgen failed: " + previewResult.status.summary;
} else {
state.procgenPreview = previewResult.value;
state.vm.status = procgen.getAlgorithmDisplayName(state.procgenAlgorithm) + " preview " +
state.procgenPreview.getWidth() + "x" + state.procgenPreview.getHeight();
}
}
function updateCamera() {
local cx = (MAP_W - 1) * CELL * 0.5;
local cz = (MAP_H - 1) * CELL * 0.5;
state.camera.setEye(cx + state.distance * cos(state.pitch) * sin(state.yaw),
state.distance * sin(state.pitch) + 1.5,
cz + state.distance * cos(state.pitch) * cos(state.yaw));
state.camera.setTarget(cx, 0.8, cz);
}
function terrainHit(mx, my, viewportW, viewportH) {
state.camera.screenToRay(mx, my, viewportW, viewportH);
local oy = state.camera.getScreenRayOriginY();
local dy = state.camera.getScreenRayDirY();
if (dy >= -0.0001) return null;
local t = (HEIGHT_SCALE * 0.5 - oy) / dy;
if (t < 0.0) return null;
local wx = state.camera.getScreenRayOriginX() + state.camera.getScreenRayDirX() * t;
local wz = state.camera.getScreenRayOriginZ() + state.camera.getScreenRayDirZ() * t;
if (wx < 0.0 || wz < 0.0 || wx > (MAP_W - 1) * CELL || wz > (MAP_H - 1) * CELL) return null;
return [wx / CELL, wz / CELL];
}
function handlePanelEvents() {
foreach (host in ["toolbar", "hierarchy", "inspector", "palette"]) {
ui.select(host);
local click = ui.consumeClick();
while (click != "") {
local id = click.slice(host.len() + 1);
if (id == "mode-edit" || id == "mode-play" || id == "mode-simulate") {
state.vm.mode = id.slice(5);
state.workspace.setMode(state.vm.mode);
} else if (id == "tool-raise" || id == "tool-lower") {
state.vm.tool = id.slice(5);
} else if (id == "edit-undo") {
if (state.session.undo()) state.vm.status = "Undo terrain stroke";
} else if (id == "edit-redo") {
if (state.session.redo()) state.vm.status = "Redo terrain stroke";
} else if (id == "terrain-regenerate") {
state.vm.seed += 1;
generateTerrain();
} else if (id == "reflect-inspector") {
ui.inspectObject(state.vm);
} else if (id == "procgen-generate") {
generateProcgenPreview();
} else if (id == "material-generate-pbr") {
generateAndApplyPbrMaterial();
} else if (id == "material-grass") applyMaterial("grass");
else if (id == "material-rock") applyMaterial("rock");
else if (id == "material-sand") applyMaterial("sand");
else if (id == "spawn-rts") spawnRtsUnit();
else if (id == "spawn-card") spawnCardTable();
else if (id == "spawn-voxel") spawnArchetype("voxel.volume");
else if (id == "spawn-dialogue") applyDialogueDocument();
else if (id == "spawn-avatar") spawnArchetype("avatar.preview");
else if (id.find("select-") == 0) {
local selected = id.slice(7);
state.workspace.select("world", "scene", "demo-world", selected, "scene.object", false);
state.vm.selected = selected;
}
click = ui.consumeClick();
}
local change = ui.consumeChange();
while (change != "") {
local id = change.slice(host.len() + 1);
if (id == "brush") state.vm.brushRadius = ui.getValue("brush");
else if (id == "strength") state.vm.strength = ui.getValue("strength");
else if (id == "tool") state.vm.tool = ui.getValueText("tool");
else if (id.find("procgen-") == 0)
updateRecipeParam(state.procgenSchema, state.procgenParams, "procgen-", id);
else if (id.find("pbr-") == 0)
updateRecipeParam(state.materialSchema, state.materialParams, "pbr-", id);
else if (id.find("dialogue-") == 0) {
if (state.dialogueComponent.consumeChange("dialogue-", id))
state.vm.status = "Dialogue field changed: " + id.slice(9);
}
change = ui.consumeChange();
}
}
}
function updateSceneInteraction(dt) {
ui.select("scene");
local hovered = ui.viewportHovered("scene-vp");
local mx = ui.viewportMouseX("scene-vp");
local my = ui.viewportMouseY("scene-vp");
local canvas = ui.viewportCanvas("scene-vp");
if (hovered) {
state.distance = clampf(state.distance - ui.viewportWheel("scene-vp") * 1.4, 7.0, 42.0);
}
if (hovered && mouse.isDown(2)) {
if (state.orbiting) {
state.yaw -= (mx - state.lastX) * 0.008;
state.pitch = clampf(state.pitch + (my - state.lastY) * 0.008, 0.08, 1.42);
}
state.lastX = mx;
state.lastY = my;
state.orbiting = true;
} else state.orbiting = false;
state.brushTool.setRadius(state.vm.brushRadius);
local signedStrength = state.vm.tool == "lower" ? -state.vm.strength : state.vm.strength;
state.brushTool.setStrength(signedStrength * clampf(dt * 60.0, 0.0, 2.0));
if (state.vm.mode == "edit" && hovered && canvas != null && mouse.isDown(1)) {
local hit = terrainHit(mx, my, canvas.getWidth().tofloat(), canvas.getHeight().tofloat());
if (hit != null) {
local phase = state.sculpting ? 1 : 0;
state.session.dispatchPointer(phase, 1, 0, hit[0], hit[1],
hit[0] - state.sculptX, hit[1] - state.sculptY, 1.0);
state.sculpting = true;
state.sculptX = hit[0];
state.sculptY = hit[1];
}
} else if (state.sculpting) {
state.session.dispatchPointer(2, 1, 0, state.sculptX, state.sculptY, 0.0, 0.0, 1.0);
state.sculpting = false;
}
local revision = state.terrainTarget.getRevision();
if (revision != state.lastTargetRevision) {
state.lastTargetRevision = revision;
state.meshDirty = true;
state.vm.status = state.vm.tool + " terrain · revision " + revision;
}
}
function syncViews() {
ui.select("toolbar");
ui.setText("mode-label", "Mode: " + state.vm.mode + " · Tool: " + state.vm.tool);
ui.select("hierarchy");
ui.setText("ecs-count", "RuntimeObject view: " + eve.view(RuntimeObject).len());
ui.select("inspector");
ui.setText("selection", "Selected: " + state.vm.selected);
ui.setText("material-current", "Active: " + state.vm.material);
ui.setValue("brush", state.vm.brushRadius);
ui.setValue("strength", state.vm.strength);
ui.select("scene");
ui.setText("scene-status", state.vm.status);
ui.select("palette");
ui.setText("palette-status", "Commands create real ECS data · objects " + state.objects.len());
}
eve_init = function() {
state.vm = WorldEditorVM();
local procgenParamsResult = procgen.newParams();
if (!procgenParamsResult.ok) {
state.vm.status = "Procgen parameter creation failed: " + procgenParamsResult.status.summary;
return;
}
state.procgenParams = procgenParamsResult.value;
local procgenDefaultsResult = procgen.applyAlgorithmDefaults(state.procgenAlgorithm, state.procgenParams);
if (!procgenDefaultsResult.ok) {
state.vm.status = "Procgen defaults failed: " + procgenDefaultsResult.status.summary;
return;
}
local procgenSchemaResult = procgen.getAlgorithmSchema(state.procgenAlgorithm);
if (!procgenSchemaResult.ok) {
state.vm.status = "Procgen schema failed: " + procgenSchemaResult.status.summary;
return;
}
state.procgenSchema = procgenSchemaResult.value;
local materialParamsResult = procgen.newParams();
if (!materialParamsResult.ok) {
state.vm.status = "Material parameter creation failed: " + materialParamsResult.status.summary;
return;
}
state.materialParams = materialParamsResult.value;
state.materialParams.setSize(128, 128);
local materialDefaultsResult = procgen.applyPbrRecipeDefaults(state.materialRecipe, state.materialParams);
if (!materialDefaultsResult.ok) {
state.vm.status = "Material defaults failed: " + materialDefaultsResult.status.summary;
return;
}
local materialSchemaResult = procgen.getPbrRecipeSchema(state.materialRecipe);
if (!materialSchemaResult.ok) {
state.vm.status = "Material schema failed: " + materialSchemaResult.status.summary;
return;
}
state.materialSchema = materialSchemaResult.value;
state.dialogueComponent = DialogueEditorComponent(dialogueFlow, "quest.example");
state.gameplayComponents = GameplayEditorComponents();
configureWorkspace();
registerProjectCommands();
generateTerrain();
state.gameplayComponents.bindTerrain(state.heightmap, CELL);
generateAndApplyPbrMaterial();
configureTerrainTool();
state.camera = eve.Camera3D();
state.camera.setFov(50.0);
state.camera.setAmbient(0.14, 0.16, 0.20);
state.camera.setActive(true);
gfx.setBackgroundColor(0.055, 0.065, 0.085, 1.0);
gfx.setDirectionalLight(-0.45, 0.9, 0.35, 0.95, 0.98, 1.0);
ui.setTheme("dark");
ui.setNavKeyboard(true);
mountWorkspacePanels();
print("composable-editor: panels mounted\n");
ui.dbRegister(state.vm, "WorldEditorVM");
ui.dbClose();
print("composable-editor: vm registered\n");
state.workspace.select("world", "scene", "demo-world", "terrain", "terrain.heightmap", false);
updateCamera();
print("composable-editor: workspace panels=" + state.workspace.getPanelCount() + "\n");
};
eve_update = function(dt) {
handlePanelEvents();
updateSceneInteraction(dt);
updateCamera();
state.gameplayComponents.update(dt);
state.meshCooldown -= dt;
if (state.meshDirty && state.meshCooldown <= 0.0) {
heightmapTargets.updateSmoothMesh(state.terrainMesh, gfx, state.heightmap, CELL, HEIGHT_SCALE);
state.meshDirty = false;
state.meshCooldown = 1.0 / 30.0;
}
syncViews();
};
eve_render = function() {
gfx.clear();
// Scene presentation is project code: another editor can replace this
// builder/canvas with a card table, tilemap, voxel slice, or custom preview.
ui.select("scene");
local canvas = ui.viewportCanvas("scene-vp");
if (canvas != null) gfx.renderScene3DToCanvas(canvas, state.camera);
ui.beginFrameAndRender();
state.frameCount += 1;
if (!state.screenshotSaved && state.frameCount > 8 && gfx.saveFramePng("composable-editor.png")) {
state.screenshotSaved = true;
print("composable-editor: saved composable-editor.png\n");
}
};