C++ owns those documents and workspace descriptors; examples compose panels. UI ColorPalette and theme scope support the theme editor inspector. Co-authored-by: Cursor <cursoragent@cursor.com>
282 lines
12 KiB
Plaintext
282 lines
12 KiB
Plaintext
// Project-composed procgen script editor. Native code owns Params, undo and
|
|
// preview copies. This presenter loads generators/forest.nut and runs generate.
|
|
|
|
persist pcgUi = {
|
|
workspace = null, editor = null,
|
|
status = "Loading forest generator...",
|
|
undoWas = false, redoWas = false,
|
|
frame = 0, screenshotSaved = false,
|
|
};
|
|
|
|
function requireResult(result, context) {
|
|
if (!result.ok) throw context + ": " + result.status.summary;
|
|
return result.value;
|
|
}
|
|
|
|
function loadForestModule() {
|
|
dofile("generators/forest.nut");
|
|
requireResult(pcgUi.editor.loadModule("game:/generators/forest.nut", forestModule.id,
|
|
forestModule.displayName, forestModule.kind, forestModule.schema),
|
|
"Load forest module");
|
|
}
|
|
|
|
function fillParams(params) {
|
|
for (local i = 0; i < pcgUi.editor.getParamCount(); ++i) {
|
|
local key = pcgUi.editor.getParamKey(i);
|
|
local kind = pcgUi.editor.getParamKind(i);
|
|
if (kind == "int") params.setInt(key, pcgUi.editor.getInt(key));
|
|
else if (kind == "float") params.setFloat(key, pcgUi.editor.getFloat(key));
|
|
else if (kind == "bool") params.setBool(key, pcgUi.editor.getBool(key));
|
|
else params.setString(key, pcgUi.editor.getString(key));
|
|
}
|
|
params.setSeed(pcgUi.editor.getInt("seed"));
|
|
}
|
|
|
|
function rebuild() {
|
|
if (!pcgUi.editor.isDirty()) return;
|
|
local expected = pcgUi.editor.getRevision();
|
|
local paramsResult = procgen.newParams();
|
|
if (!paramsResult.ok) {
|
|
pcgUi.editor.failPreview(paramsResult.status.summary, expected);
|
|
pcgUi.status = paramsResult.status.summary;
|
|
return;
|
|
}
|
|
local params = paramsResult.value;
|
|
fillParams(params);
|
|
local contextResult = procgen.beginSystem(pcgUi.editor.getModuleId(), params.getSeed());
|
|
if (!contextResult.ok) {
|
|
pcgUi.editor.failPreview(contextResult.status.summary, expected);
|
|
pcgUi.status = contextResult.status.summary;
|
|
return;
|
|
}
|
|
local ctx = contextResult.value;
|
|
try {
|
|
forestModule.generate(params, ctx);
|
|
local commitResult = procgen.commitSystem(ctx);
|
|
if (!commitResult.ok) throw commitResult.status.summary;
|
|
local outputResult = procgen.getSystemOutput(pcgUi.editor.getModuleId(), "trees");
|
|
if (!outputResult.ok) throw outputResult.status.summary;
|
|
local stageCount = procgen.getSystemDebugStageCount(pcgUi.editor.getModuleId());
|
|
for (local i = 0; i < stageCount; ++i) {
|
|
local name = procgen.getSystemDebugStageName(pcgUi.editor.getModuleId(), i);
|
|
local stage = procgen.getSystemDebugStage(pcgUi.editor.getModuleId(), name);
|
|
if (stage.ok) pcgUi.editor.publishStage(stage.value, name);
|
|
}
|
|
requireResult(pcgUi.editor.publishPreview(outputResult.value, "trees", expected), "Publish preview");
|
|
pcgUi.status = "Generated " + pcgUi.editor.getPointCount() + " points";
|
|
} catch (error) {
|
|
ctx.fail(error.tostring());
|
|
procgen.commitSystem(ctx);
|
|
pcgUi.editor.failPreview(error.tostring(), expected);
|
|
pcgUi.status = "rebuild failed, previous preview kept: " + error.tostring();
|
|
}
|
|
}
|
|
|
|
function buildWorkspace() {
|
|
pcgUi.workspace = editor.newWorkspace("preview.procgen", "Forest Generator");
|
|
pcgUi.editor = requireResult(eve.ProcgenEditorModule().create("asset.preview.forest"),
|
|
"Create procgen script editor");
|
|
requireResult(pcgUi.editor.configureWorkspace(pcgUi.workspace), "Compose workspace");
|
|
loadForestModule();
|
|
pcgUi.workspace.setRegionSize("left", 220.0);
|
|
pcgUi.workspace.setRegionSize("right", 280.0);
|
|
pcgUi.workspace.setRegionSize("bottom", 140.0);
|
|
pcgUi.workspace.layout(config.width.tofloat(), config.height.tofloat());
|
|
}
|
|
|
|
function panelModules() {
|
|
ui.text("Modules", "modules-title");
|
|
ui.listItem(pcgUi.editor.getDisplayName(), "module-forest");
|
|
ui.text("", "module-selection");
|
|
}
|
|
|
|
function panelPreview() {
|
|
ui.text("Commit sliders to rebuild. Gold dots are the selected stage.", "preview-help");
|
|
ui.text("", "preview-status");
|
|
ui.viewport("procgen-world", pcgUi.workspace.getRegionW("center") - 20.0,
|
|
pcgUi.workspace.getRegionH("center") - 72.0);
|
|
}
|
|
|
|
function panelInspector() {
|
|
ui.text("Generator Inspector", "inspector-title");
|
|
ui.text("", "revision");
|
|
ui.separator("inspector-sep-1");
|
|
for (local i = 0; i < pcgUi.editor.getParamCount(); ++i) {
|
|
local key = pcgUi.editor.getParamKey(i);
|
|
local kind = pcgUi.editor.getParamKind(i);
|
|
local label = pcgUi.editor.getParamLabel(i);
|
|
if (kind == "bool") ui.checkbox(label, pcgUi.editor.getBool(key), key);
|
|
else if (kind == "choice") {
|
|
local choices = "";
|
|
for (local c = 0; c < pcgUi.editor.getParamChoiceCount(i); ++c) {
|
|
if (c > 0) choices += ",";
|
|
choices += pcgUi.editor.getParamChoice(i, c);
|
|
}
|
|
ui.combo(label, choices, 0, key);
|
|
} else if (kind == "string") ui.inputText(label, pcgUi.editor.getString(key), key);
|
|
else ui.slider(label, kind == "int" ? pcgUi.editor.getInt(key).tofloat() : pcgUi.editor.getFloat(key),
|
|
pcgUi.editor.getParamMinimum(i), pcgUi.editor.getParamMaximum(i), key);
|
|
}
|
|
ui.separator("inspector-sep-2");
|
|
ui.checkbox("Live", pcgUi.editor.isLive(), "live");
|
|
ui.beginRow("inspector-history", 8.0); ui.button("Undo", "undo"); ui.button("Redo", "redo"); ui.end();
|
|
}
|
|
|
|
function panelStages() {
|
|
ui.text("Debug Stages", "stages-title");
|
|
for (local i = 0; i < pcgUi.editor.getStageCount(); ++i)
|
|
ui.listItem(pcgUi.editor.getStageName(i), "stage-" + pcgUi.editor.getStageName(i));
|
|
ui.text("", "stage-selection");
|
|
}
|
|
|
|
panelBuilders <- {
|
|
["procgen.modules"]=panelModules,
|
|
["procgen.preview"]=panelPreview,
|
|
["procgen.inspector"]=panelInspector,
|
|
["procgen.stages"]=panelStages
|
|
};
|
|
|
|
function mountPanels() {
|
|
for (local i = 0; i < pcgUi.workspace.getPanelCount(); ++i) {
|
|
local id = pcgUi.workspace.getPanelId(i);
|
|
if (!(id in panelBuilders)) continue;
|
|
ui.beginBuild(); ui.beginWindow(pcgUi.workspace.getPanelTitle(i), "root");
|
|
panelBuilders[id](); ui.end(); ui.mountBuildAs(id); ui.select(id);
|
|
local region = pcgUi.workspace.getPanelRegion(i);
|
|
ui.setHostPos(pcgUi.workspace.getRegionX(region), pcgUi.workspace.getRegionY(region), 0.0, 0.0);
|
|
ui.setHostSize(pcgUi.workspace.getRegionW(region), pcgUi.workspace.getRegionH(region));
|
|
ui.setHostOverlay(false);
|
|
}
|
|
}
|
|
|
|
function applyHistory(command) {
|
|
local result = command == "undo" ? pcgUi.editor.undo() : pcgUi.editor.redo();
|
|
pcgUi.status = result.ok ? command + " applied" : command + ": " + result.status.summary;
|
|
}
|
|
|
|
function eventParts(path) {
|
|
local slash = path.find("/");
|
|
return slash == null ? ["", path] : [path.slice(0, slash), path.slice(slash + 1)];
|
|
}
|
|
|
|
function handleUiEvents() {
|
|
local click = ui.consumeClick();
|
|
while (click != "") {
|
|
local event = eventParts(click); local host = event[0]; local id = event[1];
|
|
if (id == "undo" || id == "redo") applyHistory(id);
|
|
else if (host == "procgen.stages" && id.find("stage-") == 0) {
|
|
local result = pcgUi.editor.selectStage(id.slice(6));
|
|
pcgUi.status = result.ok ? "Stage " + pcgUi.editor.getSelectedStage() : result.status.summary;
|
|
}
|
|
click = ui.consumeClick();
|
|
}
|
|
|
|
local changed = ui.consumeChange();
|
|
while (changed != "") {
|
|
local event = eventParts(changed); local host = event[0]; local id = event[1];
|
|
if (host == "procgen.inspector") {
|
|
if (id == "live") {
|
|
pcgUi.editor.setLive(ui.getValue("live") != 0);
|
|
} else {
|
|
local kind = "";
|
|
for (local i = 0; i < pcgUi.editor.getParamCount(); ++i)
|
|
if (pcgUi.editor.getParamKey(i) == id) kind = pcgUi.editor.getParamKind(i);
|
|
local result = { ok = true, status = { summary = "" } };
|
|
if (kind == "int") result = pcgUi.editor.setInt(id, ui.getValue(id).tointeger());
|
|
else if (kind == "float") result = pcgUi.editor.setFloat(id, ui.getValue(id));
|
|
else if (kind == "bool") result = pcgUi.editor.setBool(id, ui.getValue(id) != 0);
|
|
else if (kind == "choice" || kind == "string") result = pcgUi.editor.setString(id, ui.getText(id));
|
|
pcgUi.status = result.ok ? id + " committed · revision " + pcgUi.editor.getRevision()
|
|
: result.status.summary;
|
|
}
|
|
}
|
|
changed = ui.consumeChange();
|
|
}
|
|
}
|
|
|
|
function updateKeyboardShortcuts() {
|
|
local control = keyboard.isDown("lctrl") || keyboard.isDown("rctrl") || keyboard.isDown("ctrl");
|
|
local undo = control && (keyboard.isDown("z") || keyboard.isDown("Z"));
|
|
local redo = control && (keyboard.isDown("y") || keyboard.isDown("Y"));
|
|
if (undo && !pcgUi.undoWas) applyHistory("undo");
|
|
if (redo && !pcgUi.redoWas) applyHistory("redo");
|
|
pcgUi.undoWas = undo; pcgUi.redoWas = redo;
|
|
}
|
|
|
|
function updateLabels() {
|
|
ui.select("procgen.preview");
|
|
ui.setText("preview-status", format("%d points stage %s rev %d",
|
|
pcgUi.editor.getPointCount(), pcgUi.editor.getSelectedStage(), pcgUi.editor.getRevision()));
|
|
ui.select("procgen.inspector");
|
|
ui.setText("revision", "Revision " + pcgUi.editor.getRevision() +
|
|
(pcgUi.editor.getPreviewFailureSummary() == "" ? "" : " " + pcgUi.editor.getPreviewFailureSummary()));
|
|
for (local i = 0; i < pcgUi.editor.getParamCount(); ++i) {
|
|
local key = pcgUi.editor.getParamKey(i);
|
|
local kind = pcgUi.editor.getParamKind(i);
|
|
if (kind == "int") ui.setValue(key, pcgUi.editor.getInt(key).tofloat());
|
|
else if (kind == "float") ui.setValue(key, pcgUi.editor.getFloat(key));
|
|
else if (kind == "bool") ui.setValue(key, pcgUi.editor.getBool(key) ? 1.0 : 0.0);
|
|
}
|
|
ui.setValue("live", pcgUi.editor.isLive() ? 1.0 : 0.0);
|
|
ui.setEnabled("undo", pcgUi.editor.canUndo());
|
|
ui.setEnabled("redo", pcgUi.editor.canRedo());
|
|
ui.select("procgen.modules");
|
|
ui.setText("module-selection", "Active: " + pcgUi.editor.getModuleId());
|
|
ui.select("procgen.stages");
|
|
ui.setText("stage-selection", "Active: " + pcgUi.editor.getSelectedStage());
|
|
}
|
|
|
|
function drawPreview() {
|
|
ui.select("procgen.preview");
|
|
local canvas = ui.viewportCanvas("procgen-world");
|
|
if (canvas == null) return;
|
|
gfx.setCanvas(canvas); gfx.clear();
|
|
local width = pcgUi.workspace.getRegionW("center") - 20.0;
|
|
local height = pcgUi.workspace.getRegionH("center") - 72.0;
|
|
gfx.drawSolidRect(0.0, 0.0, width, height, 0.055, 0.075, 0.07, 1.0);
|
|
local count = pcgUi.editor.getPointCount();
|
|
local maxX = 1.0;
|
|
local maxZ = 1.0;
|
|
for (local i = 0; i < count; ++i) {
|
|
if (pcgUi.editor.getPointX(i) > maxX) maxX = pcgUi.editor.getPointX(i);
|
|
if (pcgUi.editor.getPointZ(i) > maxZ) maxZ = pcgUi.editor.getPointZ(i);
|
|
}
|
|
local scale = (width - 48.0) / maxX;
|
|
local scaleZ = (height - 48.0) / maxZ;
|
|
if (scaleZ < scale) scale = scaleZ;
|
|
for (local i = 0; i < count; ++i) {
|
|
local x = 24.0 + pcgUi.editor.getPointX(i) * scale;
|
|
local y = height - 24.0 - pcgUi.editor.getPointZ(i) * scale;
|
|
local size = 4.0 + (pcgUi.editor.getPointSeed(i) % 4);
|
|
gfx.drawSolidRect(x - size * 0.5, y - size * 0.5, size, size, 0.85, 0.64, 0.18, 1.0);
|
|
}
|
|
gfx.setCanvas(null);
|
|
}
|
|
|
|
eve_init = function() {
|
|
gfx.setBackgroundColor(0.09, 0.11, 0.14, 1.0);
|
|
buildWorkspace(); ui.setTheme("dark"); ui.setNavKeyboard(true); mountPanels(); rebuild();
|
|
print("procgen-script-editor: points=" + pcgUi.editor.getPointCount() + "\n");
|
|
};
|
|
|
|
eve_reload <- function() {
|
|
loadForestModule();
|
|
mountPanels();
|
|
rebuild();
|
|
};
|
|
|
|
eve_update = function(dt) {
|
|
handleUiEvents(); updateKeyboardShortcuts(); rebuild(); updateLabels();
|
|
};
|
|
|
|
eve_render = function() {
|
|
gfx.clear();
|
|
drawPreview();
|
|
ui.beginFrameAndRender();
|
|
pcgUi.frame += 1;
|
|
if (!pcgUi.screenshotSaved && pcgUi.frame > 90 && gfx.saveFramePng("procgen-script-editor.png")) {
|
|
pcgUi.screenshotSaved = true;
|
|
print("procgen-script-editor: saved procgen-script-editor.png\n");
|
|
}
|
|
};
|