feat: add direct edit preview APIs

This commit is contained in:
2026-08-27 12:48:29 +08:00
parent 735ce69d26
commit 41c314fc61
3 changed files with 162 additions and 3 deletions

View File

@@ -4,12 +4,20 @@
const fs = require('fs');
const http = require('http');
const path = require('path');
const { loadOverrides, validateOverrides, writeJsonAtomic } = require('../src/compile/native-road');
const {
compileRoadModel,
compileGeometry,
loadOverrides,
validateOverrides,
writeJsonAtomic,
} = require('../src/compile/native-road');
const { generate, validateDocument, runtime } = require('../src/native-traffic-signals');
const { convertGeoJson } = require('../src/reference/gaode');
const { exportNativeRoadPackage } = require('../src/export/native-road-package');
const { compileInput } = require('../src/compile/compiler');
const { ensureRevisionStore, setActiveAreaConfig } = require('../src/compile/road-revisions');
const { GEOMETRY_VERSION, resolveDirectEditConstraints } = require('../src/compile/direct-edit-solver');
const { loadEditDocument, validateEditDocument } = require('../src/compile/native-road-edits');
function startWorkbench({
area = null,
@@ -69,6 +77,19 @@ function handle(request, response, session) {
return area
? sendJson(response, 200, state(area, junctionReference, debug))
: sendJson(response, 200, { active: false });
if (request.method === 'GET' && url.pathname === '/api/edit-state')
return Promise.resolve()
.then(() => (area ? editState(area) : { active: false }))
.then((value) => sendJson(response, 200, value))
.catch((error) => sendJson(response, 400, { ok: false, error: error.message }));
if (request.method === 'POST' && url.pathname === '/api/edit-preview')
return readBody(request)
.then((body) => {
if (!area) throw new Error('请先导入 OSM 文件。');
return editPreview(area, body);
})
.then((value) => sendJson(response, 200, value))
.catch((error) => sendJson(response, 400, { ok: false, error: error.message }));
if (request.method === 'GET' && url.pathname === '/api/session')
return sendJson(response, 200, { active: Boolean(session.area), areaId: session.area?.id || null });
if (request.method === 'POST' && url.pathname === '/api/import')
@@ -210,6 +231,83 @@ function state(area, junctionReference = null, debug = false) {
},
};
}
function editState(area) {
const workspace = path.dirname(area.input);
const initialized = ensureRevisionStore(workspace, area.nativeRoad);
const directEdits = loadEditDocument(initialized.paths.activeEdits);
const model = compileRoadModel(fs.readFileSync(area.input, 'utf8'), loadOverrides(area.outputs.nativeRoadOverrides));
const resolution = resolveDirectEditConstraints(model, directEdits, {
revisionId: initialized.active.activeRevisionId,
compilerGeometryVersion: GEOMETRY_VERSION,
});
return {
active: true,
areaId: area.id,
activeRevisionId: initialized.active.activeRevisionId,
documentVersion: directEdits.documentVersion,
document: directEdits,
constraintStates: resolution.constraintStates,
diagnostics: resolution.diagnostics,
handles: resolution.handles,
};
}
function editPreview(area, body = {}) {
const startedAt = Date.now();
const workspace = path.dirname(area.input);
const activeEditsFile = path.join(workspace, 'active', 'native-road-edits.json');
const activeStateFile = path.join(workspace, 'active', 'state.json');
if (!fs.existsSync(activeEditsFile) || !fs.existsSync(activeStateFile))
throw new Error('编辑工作区尚未初始化,请先重新导入 OSM 文件。');
const activeEdits = loadEditDocument(activeEditsFile);
const draft = body.document
? validateEditDocument(body.document)
: validateEditDocument({
...activeEdits,
...(Array.isArray(body.constraints) ? { constraints: body.constraints } : {}),
...(Array.isArray(body.operations) ? { operations: body.operations } : {}),
});
const activeState = readJson(activeStateFile);
const model = compileRoadModel(fs.readFileSync(area.input, 'utf8'), loadOverrides(area.outputs.nativeRoadOverrides));
const previewSeq = Number.isInteger(body.previewSeq) ? body.previewSeq : 0;
const resolution = resolveDirectEditConstraints(model, draft, {
revisionId: activeState.activeRevisionId,
previewSeq,
compilerGeometryVersion: GEOMETRY_VERSION,
});
const compiled = compileGeometry(model, loadOverrides(area.outputs.nativeRoadOverrides), {
edgeLines: area.nativeRoad.edgeLines,
junctionTemplates: area.nativeRoad.junctionTemplates,
directEdit: resolution,
});
const diagnostics = [...compiled.diagnostics, ...resolution.diagnostics];
const degraded = Date.now() - startedAt > 300;
return {
ok: true,
previewSeq,
degraded,
revisionId: activeState.activeRevisionId,
documentVersion: activeEdits.documentVersion,
constraintStates: resolution.constraintStates,
diagnostics,
handles: resolution.handles,
layers: {
roadSurface: compiled.roadSurface,
edgeLines: compiled.edgeLines,
sidewalkSurface: compiled.sidewalkSurface,
intersectionSurface: compiled.intersectionSurface,
laneCenterlines: compiled.laneCenterlines,
laneSeparators: compiled.laneSeparators,
centerLines: compiled.centerLines,
directionArrows: compiled.directionArrows,
turnArrows: compiled.turnArrows,
crosswalks: compiled.crosswalks,
vehicleStopLines: compiled.vehicleStopLines,
connectors: compiled.connectors,
},
};
}
// The compiler reports candidates as advisory diagnostics. Lift them into their
// own payload with a stable index so the map can label them "#1, #2, ..." and
// the inspector can offer a ready-to-paste cluster配置.
@@ -427,4 +525,4 @@ function sendJson(response, status, value) {
response.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' });
response.end(`${JSON.stringify(value)}\n`);
}
module.exports = { startWorkbench, readMultipart };
module.exports = { startWorkbench, readMultipart, editState, editPreview };