Files
西风逍遥游andCursor 905bd8c813 feat(editor): add procgen script, UI theme, and voxel catalog workspaces
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>
2026-09-05 19:08:50 +08:00

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");
}
};