improoooved

This commit is contained in:
33333-33333 2026-05-30 03:25:19 +09:00
commit 0990b98436
5 changed files with 358 additions and 2795 deletions

28
app.js
View file

@ -957,6 +957,7 @@ function updateRenderDiagnostics(options = {}, timings = {}) {
hoverMs: timings.hoverMs || 0,
drawMs: timings.drawMs || 0,
totalRenderMs: timings.totalRenderMs || 0,
renderBreakdown: timings.renderBreakdown || null,
};
state.diagnostics.lastFeatureCounts = collectFeatureCounts(map);
}
@ -981,6 +982,15 @@ function selectionWriteDiagnostics() {
function renderViewportDiagnostics() {
const viewport = state.diagnostics.lastViewport;
const renderRows = viewport?.renderBreakdown ? [
{ label: "Draw / base", value: formatMs(viewport.renderBreakdown.baseTerrain || 0), sub: "terrain raster" },
{ label: "Draw / urban", value: formatMs(viewport.renderBreakdown.urbanFill || 0), sub: "land-use overlay" },
{ label: "Draw / coast", value: formatMs(viewport.renderBreakdown.coastline || 0), sub: "coastline vectors" },
{ label: "Draw / rivers", value: formatMs(viewport.renderBreakdown.rivers || 0), sub: "river paths" },
{ label: "Draw / admin", value: formatMs(viewport.renderBreakdown.adminBorders || 0), sub: "fills and borders" },
{ label: "Draw / transport", value: formatMs(viewport.renderBreakdown.transport || 0), sub: "roads and rail" },
{ label: "Draw / labels", value: formatMs((viewport.renderBreakdown.icons || 0) + (viewport.renderBreakdown.labels || 0)), sub: "icons and text" },
] : [];
renderDiagnosticGrid(advancedViewportDiagnosticsEl, viewport ? [
{ label: "Viewport", value: `${viewport.viewWidth}×${viewport.viewHeight}`, sub: `${formatAreaCells(viewport.cells)} drawn` },
{ label: "Canvas CSS", value: `${Math.round(viewport.cssWidth)}×${Math.round(viewport.cssHeight)}`, sub: "display pixels" },
@ -990,6 +1000,7 @@ function renderViewportDiagnostics() {
{ label: "Canvas draw", value: formatMs(viewport.drawMs), sub: "drawMap" },
{ label: "Hover index", value: formatMs(viewport.hoverMs), sub: "labels / hit targets" },
{ label: "Render total", value: formatMs(viewport.totalRenderMs), sub: `${viewport.mode} mode` },
...renderRows,
] : [], "No viewport render recorded yet.");
const counts = state.diagnostics.lastFeatureCounts;
@ -1056,13 +1067,15 @@ function performanceMetricsForRuns(runs) {
const totals = summarizeValues(numericValues(runs, (run) => run.totalMs));
const perArea = summarizeValues(numericValues(runs, (run) => run.secondsPerThousand));
if (!totals) return [];
const hasSmallAreaRuns = (runs || []).some((run) => Number(run?.areaCells) > 0 && Number(run.areaCells) < 1000);
const perAreaSub = hasSmallAreaRuns ? "area-normalized; small runs include fixed overhead" : "seconds / 1k cells";
return [
{ label: "Samples", value: String(totals.count), sub: "last 10" },
{ label: "Avg total", value: formatMs(totals.avg), sub: "generation time" },
{ label: "Max total", value: formatMs(totals.max), sub: "slowest run" },
{ label: "P95 total", value: formatMs(totals.p95), sub: "tail latency" },
perArea ? { label: "Avg / 1k", value: formatSeconds(perArea.avg), sub: "seconds / 1k cells" } : null,
perArea ? { label: "P95 / 1k", value: formatSeconds(perArea.p95), sub: "area-normalized" } : null,
perArea ? { label: "Avg / 1k", value: formatSeconds(perArea.avg), sub: perAreaSub } : null,
perArea ? { label: "P95 / 1k", value: formatSeconds(perArea.p95), sub: hasSmallAreaRuns ? "fixed overhead dominated below 1k cells" : "area-normalized" } : null,
];
}
@ -1072,6 +1085,10 @@ function recordGenerationRun(map, terrainType) {
const totalMs = Number.isFinite(map.generationTotalMs)
? map.generationTotalMs
: (map.generationTimings || []).reduce((sum, row) => sum + (Number(row?.ms) || 0), 0);
const featureTimings = (map.transportDebug?.featureTimings || []).map((row) => ({
label: `Settlements / ${row.key || "substage"}`,
ms: Number(row.ms) || 0,
}));
pushCapped(state.generationRuns, {
id: Date.now(),
createdAt: new Date(),
@ -1082,7 +1099,7 @@ function recordGenerationRun(map, terrainType) {
timings: (map.generationTimings || []).map((row) => ({
label: row.label || row.key || "Stage",
ms: Number(row.ms) || 0,
})),
})).concat(featureTimings),
});
renderAdvancedData();
}
@ -1187,7 +1204,7 @@ function appendRunHistory(container, runs, options = {}) {
<span>${name}</span>
<strong>${formatMs(run.totalMs)}</strong>
<span>${formatAreaCells(run.areaCells)}${options.patch && run.writeAreaCells ? ` / write ${formatAreaCells(run.writeAreaCells)}` : ""}</span>
<span>${formatSeconds(run.secondsPerThousand)} / 1k cells</span>
<span>${formatSeconds(run.secondsPerThousand)} / 1k cells${run.areaCells && run.areaCells < 1000 ? " (fixed overhead)" : ""}</span>
`;
const body = document.createElement("div");
@ -2235,7 +2252,7 @@ function redraw(options = {}) {
state.hoverEntities = options.fastTerrain ? [] : buildHoverEntities(state.viewportMap);
const hoverMs = performance.now() - hoverStartedAt;
const drawStartedAt = performance.now();
drawMap(canvas, state.viewportMap, {
const renderBreakdown = drawMap(canvas, state.viewportMap, {
mode: state.mode,
showFeatures: state.showFeatures && !options.fastTerrain,
showLabels: state.showLabels && !options.fastTerrain,
@ -2251,6 +2268,7 @@ function redraw(options = {}) {
hoverMs,
drawMs,
totalRenderMs: performance.now() - redrawStartedAt,
renderBreakdown,
});
renderAdvancedData();
}

View file

@ -3,6 +3,7 @@ import { generateMap } from "./mapPipeline.js";
import { generateTerrainRect } from "./mapTerrain.js";
import { LANDUSE } from "./landuseCodes.js";
import { reconcileMunicipalMetadata, refreshPrefectureRegionsMetadata } from "./mapMunicipalCoherence.js";
import { createPatchContext } from "./mapPatchContext.js";
export const PATCH_MIN_WIDTH = 48;
export const PATCH_MIN_HEIGHT = 48;
@ -1358,8 +1359,8 @@ function repairDiscreteSeamOwnership(world, rects, oldFields, seed = 0) {
return { adminSeamCellsResolved, prefectureSeamCellsResolved };
}
function copyFullPipelineFields(world, candidate, rects, seed, sourceMap = null, seaLevel = 0.30) {
const window = sourceWindowForRects(rects);
function copyFullPipelineFields(world, candidate, rects, seed, sourceMap = null, seaLevel = 0.30, patchContext = null) {
const window = patchContext?.candidateWindow || sourceWindowForRects(rects);
const elevationAdjustment = computeElevationCandidateAdjustment(world, candidate, rects, window, seed);
const oldSea = world.fields.sea ? new Uint8Array(world.fields.sea) : null;
const oldLanduse = world.fields.landuse ? new world.fields.landuse.constructor(world.fields.landuse) : null;
@ -1381,6 +1382,7 @@ function copyFullPipelineFields(world, candidate, rects, seed, sourceMap = null,
let adminCellsReassigned = 0;
let landUseCellsUpdated = 0;
const fieldEntries = [];
for (const [name, source] of Object.entries(candidate || {})) {
if (SKIP_CELL_FIELDS.has(name) || !isCandidateCellField(candidate, source, window)) continue;
const dest = ensureWorldField(world, name, source);
@ -1388,7 +1390,11 @@ function copyFullPipelineFields(world, candidate, rects, seed, sourceMap = null,
const isFloat = source.constructor === Float32Array || source.constructor === Float64Array;
const isDiscrete = DISCRETE_FIELD_NAMES.has(name) || !isFloat;
const idOffset = fieldIdOffset(name, seed);
fieldEntries.push({ name, source, dest, isDiscrete, idOffset });
}
const cells = patchContext?.writeCells?.length ? patchContext.writeCells : (() => {
const out = [];
for (let y = rects.writeRect.y0; y < rects.writeRect.y1; y++) {
for (let x = rects.writeRect.x0; x < rects.writeRect.x1; x++) {
const wi = worldIndex(world, x, y);
@ -1396,33 +1402,39 @@ function copyFullPipelineFields(world, candidate, rects, seed, sourceMap = null,
const si = sourceIndexForWorld(rects, window, x, y);
if (si < 0) continue;
const alpha = patchAlpha(x, y, rects, seed);
if (alpha <= 0.005) continue;
if (alpha > 0.005) out.push({ x, y, wi, si, alpha });
}
}
return out;
})();
if (isDiscrete) {
const threshold = continuityReplaceThreshold(name, x, y, rects, seed);
if (alpha >= threshold) {
const raw = source[si];
const mapped = remapAdminCandidateValue(name, raw, adminIdMapping);
const value = (name === "adminId" || name === "municipalityId" || name === "prefectureRegionId")
? mapped
: idOffset && raw >= 0 ? raw + idOffset : raw;
if (name === "sea" && oldSea && dest[wi] !== value) coastCellsChanged++;
if ((name === "naturalCompartmentId" || name === "watershedId" || name === "regionId") && dest[wi] !== value) naturalRegionsUpdated++;
if ((name === "adminId" || name === "municipalityId") && dest[wi] !== value) adminCellsReassigned++;
if (name === "landuse" && dest[wi] !== value) landUseCellsUpdated++;
dest[wi] = value;
}
} else {
const before = dest[wi] || 0;
let candidateValue = source[si] || 0;
if (name === "elevation") candidateValue = adjustedCandidateElevation(candidateValue, x, y, elevationAdjustment);
dest[wi] = lerp(before, candidateValue, alpha);
}
for (const cell of cells) {
const { x, y, wi, si, alpha } = cell;
for (const entry of fieldEntries) {
const { name, source, dest, isDiscrete, idOffset } = entry;
if (isDiscrete) {
const threshold = continuityReplaceThreshold(name, x, y, rects, seed);
if (alpha < threshold) continue;
const raw = source[si];
const mapped = remapAdminCandidateValue(name, raw, adminIdMapping);
const value = (name === "adminId" || name === "municipalityId" || name === "prefectureRegionId")
? mapped
: idOffset && raw >= 0 ? raw + idOffset : raw;
if (name === "sea" && oldSea && dest[wi] !== value) coastCellsChanged++;
if ((name === "naturalCompartmentId" || name === "watershedId" || name === "regionId") && dest[wi] !== value) naturalRegionsUpdated++;
if ((name === "adminId" || name === "municipalityId") && dest[wi] !== value) adminCellsReassigned++;
if (name === "landuse" && dest[wi] !== value) landUseCellsUpdated++;
dest[wi] = value;
} else {
const before = dest[wi] || 0;
let candidateValue = source[si] || 0;
if (name === "elevation") candidateValue = adjustedCandidateElevation(candidateValue, x, y, elevationAdjustment);
dest[wi] = lerp(before, candidateValue, alpha);
}
if (name === "elevation") {
updatedCells++;
if (alpha > 0.94) terrainCellsFullyReplaced++;
}
if (name === "elevation") {
updatedCells++;
if (alpha > 0.94) terrainCellsFullyReplaced++;
}
}
}
@ -3212,7 +3224,6 @@ function reconnectTransportGraph(world, sourceMap, rects, seed, mode, graphRect,
const rejectedPairs = new Set();
let attemptsRemaining = options.maxAttempts ?? (mode === "rail" ? 4 : 8);
for (let pass = 0; pass < maxAdds && attemptsRemaining > 0; pass++) {
snapshot = buildTransportGraphSnapshot(world, sourceMap, mode, graphRect, rects, seed);
const dirtyCount = snapshot.comps.filter(eligible).length;
if (dirtyCount <= 1) break;
const comps = snapshot.comps.filter(contextual);
@ -3285,13 +3296,13 @@ function reconnectTransportGraph(world, sourceMap, rects, seed, mode, graphRect,
rejectedPairs.add(transportPairSignature(b, a, mode));
continue;
}
snapshot = checkSnapshot;
debug[`${mode}GraphConnectorsAdded`] += writeResult.wrote;
accepted = true;
break;
}
if (!accepted) break;
}
snapshot = buildTransportGraphSnapshot(world, sourceMap, mode, graphRect, rects, seed);
debug[`${mode}GraphAfterComponents`] = snapshot.comps.filter(eligible).length;
return debug;
}
@ -3438,7 +3449,6 @@ function mergePathLayers(world, sourceMap, candidate, rects, window, seed) {
// repair is intentionally allowed to operate in a much broader neighborhood,
// because clipping graph repairs to the selected polygon leaves implausible
// dangling regional networks just outside the patch.
const connectorPatchOptions = null;
const externalRoadAnchors = collectExternalNetworkAnchors(world, sourceMap, ["nationalRoads", "minorRoads", "premodernRoads", "externalRoads"], rects.writeRect, transportRect, "road");
const externalRailAnchors = collectExternalNetworkAnchors(world, sourceMap, ["railways", "branchRailways", "externalRailways"], rects.writeRect, transportRect, "rail");
roadAnchors = roadAnchors.concat(externalRoadAnchors);
@ -3447,9 +3457,6 @@ function mergePathLayers(world, sourceMap, candidate, rects, window, seed) {
// visible fragments that did not reduce disconnected components. Keep the
// anchor collection for diagnostics, but route all actual repair through the
// graph reconnection pass below, which rolls back failed candidates.
const roadConn = { connectors: 0, disconnected: roadAnchors.length, skippedConnectorAnchors: 0, connectorAttempts: 0 };
const railConn = { connectors: 0, disconnected: railAnchors.length, skippedConnectorAnchors: 0, connectorAttempts: 0 };
const settlementRoadConnectors = { connectors: 0, skippedServedSettlements: 0, checkedSettlementCoverage: 0 };
const graphRect = strictMask
? transportRect
: transportRect;
@ -3459,14 +3466,14 @@ function mergePathLayers(world, sourceMap, candidate, rects, window, seed) {
roadsClipped,
railsClipped,
regeneratedPaths,
roadConnectorsCreated: roadConn.connectors + settlementRoadConnectors.connectors + (roadGraph.roadGraphConnectorsAdded || 0),
railwayConnectorsCreated: railConn.connectors + (railGraph.railGraphConnectorsAdded || 0),
disconnectedRoadComponents: roadConn.disconnected,
disconnectedRailComponents: railConn.disconnected,
skippedConnectorAnchors: (roadConn.skippedConnectorAnchors || 0) + (railConn.skippedConnectorAnchors || 0),
connectorAttempts: (roadConn.connectorAttempts || 0) + (railConn.connectorAttempts || 0),
skippedServedSettlements: settlementRoadConnectors.skippedServedSettlements || 0,
checkedSettlementCoverage: settlementRoadConnectors.checkedSettlementCoverage || 0,
roadConnectorsCreated: roadGraph.roadGraphConnectorsAdded || 0,
railwayConnectorsCreated: railGraph.railGraphConnectorsAdded || 0,
disconnectedRoadComponents: roadAnchors.length,
disconnectedRailComponents: railAnchors.length,
skippedConnectorAnchors: 0,
connectorAttempts: (roadGraph.roadGraphCandidatesConsidered || 0) + (railGraph.railGraphCandidatesConsidered || 0),
skippedServedSettlements: 0,
checkedSettlementCoverage: 0,
externalRoadAnchors: externalRoadAnchors.length,
externalRailAnchors: externalRailAnchors.length,
...roadGraph,
@ -4208,6 +4215,82 @@ function addInvalidatedRect(world, rect) {
if (!list.some((r) => rectKey(r) === key)) list.push({ ...normalized });
}
function repairPatchTerrain(world, rects, seed, seaLevel) {
const terrainSeamDebug = featherTerrainSeam(world, rects, seed);
const elevationCliffDebug = smoothExtremeElevationSeams(world, rects, seed, seaLevel);
const waterDebug = smoothWaterTopology(world, rects.writeRect, seaLevel, rects, seed);
const waterComponentDebug = repairWaterComponentTopology(world, rects, seaLevel, seed);
const residualSeaDebug = fillTinyResidualSeas(world, rects, seaLevel, seed, { aggressive: true });
const waterElevationDebug = smoothPatchedWaterElevation(world, rects, seaLevel, seed);
const maskDebug = repairDisplayMasks(world, rects, seed);
recomputeSlopeAndWaterDependentFields(world, rects.repairRect, seaLevel);
return {
terrainSeamDebug,
elevationCliffDebug,
waterDebug,
waterComponentDebug,
residualSeaDebug,
waterElevationDebug,
maskDebug,
};
}
function repairPatchAdministration(world, sourceMap, rects, seed, adminIdMapping, strictFieldSnapshot) {
const landDebug = repairLanduseAndPopulation(world, rects);
const finalAdminCoverageDebug = repairAdminCoverage(world, sourceMap, rects, adminIdMapping, seed);
const adminTopologyDebug = repairPatchAdministrativeTopology(world, rects);
const strictMaskDebugPreCoherence = restoreOutsideStrictSelectionFields(world, rects, strictFieldSnapshot, seed);
const sourceAdminMetadataUpdated = updateSourceAdminMetadata(sourceMap, adminIdMapping);
const municipalCoherence = reconcileMunicipalMetadata({
adminId: world.fields.adminId,
municipalityId: world.fields.municipalityId,
prefectureRegionId: world.fields.prefectureRegionId,
sea: world.fields.sea,
adminCenters: sourceMap.adminCenters || [],
municipalityToPrefectureId: sourceMap.municipalityToPrefectureId,
fields: world.fields,
width: world.width,
height: world.height,
pointOffsetX: world.originX || 0,
pointOffsetY: world.originY || 0,
seed,
});
sourceMap.adminCenters = municipalCoherence.adminCenters;
sourceMap.municipalityToPrefectureId = municipalCoherence.municipalityToPrefectureId;
const prefectureCoherence = refreshPrefectureRegionsMetadata({
prefectureRegionId: world.fields.prefectureRegionId,
sea: world.fields.sea,
existing: sourceMap.prefectureRegions || [],
adminCenters: sourceMap.adminCenters || [],
fields: world.fields,
width: world.width,
height: world.height,
pointOffsetX: world.originX || 0,
pointOffsetY: world.originY || 0,
});
sourceMap.prefectureRegions = prefectureCoherence.prefectureRegions;
const strictMaskDebugPostCoherence = restoreOutsideStrictSelectionFields(world, rects, strictFieldSnapshot, seed);
sourceMap.adminDebug = {
...(sourceMap.adminDebug || {}),
municipalCoherence: municipalCoherence.debug,
prefectureMetadataCoherence: prefectureCoherence.debug,
};
return {
landDebug,
finalAdminCoverageDebug,
adminTopologyDebug,
strictMaskDebugPreCoherence,
strictMaskDebugPostCoherence,
sourceAdminMetadataUpdated,
municipalCoherence,
prefectureCoherence,
};
}
function repairPatchTransport(world, sourceMap, candidate, rects, window, seed) {
return mergePathLayers(world, sourceMap, candidate, rects, window, seed);
}
export function generatePatch(world, userRectInput, options = {}) {
const validation = validatePatchRect(userRectInput, world);
if (!validation.ok) return { ok: false, ...validation };
@ -4258,85 +4341,80 @@ export function generatePatch(world, userRectInput, options = {}) {
: (candidateWindow.variable ? "Variable candidate generation" : "Full candidate generation"));
getPatchAlphaCache(rects, seed);
getPatchSourceIndexCache(rects, candidateWindow);
const patchContext = createPatchContext({
world,
rects,
candidateWindow,
seed,
patchAlpha,
sourceIndexForWorld,
worldIndex,
});
const sourceMap = world.sourceMap || (world.sourceMap = {});
const strictMetadataSnapshot = captureStrictMetadataSnapshot(world, sourceMap, rects, seed);
const logisticsLabelsMigrated = sanitizeExistingLogistics(sourceMap);
const seaLevel = candidate.seaLevel || world.sourceMap?.seaLevel || 0.30;
const fieldDebug = copyFullPipelineFields(world, candidate, rects, seed, sourceMap, seaLevel);
const fieldDebug = copyFullPipelineFields(world, candidate, rects, seed, sourceMap, seaLevel, patchContext);
patchTimer.mark("fields", "Field copy and alpha blend");
const terrainSeamDebug = featherTerrainSeam(world, rects, seed);
const elevationCliffDebug = smoothExtremeElevationSeams(world, rects, seed, seaLevel);
const waterDebug = smoothWaterTopology(world, rects.writeRect, seaLevel, rects, seed);
const waterComponentDebug = repairWaterComponentTopology(world, rects, seaLevel, seed);
const residualSeaDebug = fillTinyResidualSeas(world, rects, seaLevel, seed, { aggressive: true });
const waterElevationDebug = smoothPatchedWaterElevation(world, rects, seaLevel, seed);
const maskDebug = repairDisplayMasks(world, rects, seed);
recomputeSlopeAndWaterDependentFields(world, rects.repairRect, seaLevel);
const terrainDebug = repairPatchTerrain(world, rects, seed, seaLevel);
patchTimer.mark("terrainRepair", "Water, masks, and terrain repair");
const pointDebug = mergePointLayers(world, sourceMap, candidate, rects, fieldDebug.window, seed, fieldDebug.adminIdMapping);
patchTimer.mark("points", "Point merge");
const pathDebug = mergePathLayers(world, sourceMap, candidate, rects, fieldDebug.window, seed);
const pathDebug = repairPatchTransport(world, sourceMap, candidate, rects, fieldDebug.window, seed);
patchTimer.mark("paths", "Path merge and connector repair");
const influenceDebug = refreshPatchInfluenceFields(world, sourceMap, rects);
patchTimer.mark("influence", "Influence refresh");
const landDebug = repairLanduseAndPopulation(world, rects);
const finalAdminCoverageDebug = repairAdminCoverage(world, sourceMap, rects, fieldDebug.adminIdMapping, seed);
const adminTopologyDebug = repairPatchAdministrativeTopology(world, rects);
const strictMaskDebugPreCoherence = restoreOutsideStrictSelectionFields(world, rects, strictFieldSnapshot, seed);
const sourceAdminMetadataUpdated = updateSourceAdminMetadata(sourceMap, fieldDebug.adminIdMapping);
const municipalCoherence = reconcileMunicipalMetadata({
adminId: world.fields.adminId,
municipalityId: world.fields.municipalityId,
prefectureRegionId: world.fields.prefectureRegionId,
sea: world.fields.sea,
adminCenters: sourceMap.adminCenters || [],
municipalityToPrefectureId: sourceMap.municipalityToPrefectureId,
fields: world.fields,
width: world.width,
height: world.height,
pointOffsetX: world.originX || 0,
pointOffsetY: world.originY || 0,
seed,
});
sourceMap.adminCenters = municipalCoherence.adminCenters;
sourceMap.municipalityToPrefectureId = municipalCoherence.municipalityToPrefectureId;
const prefectureCoherence = refreshPrefectureRegionsMetadata({
prefectureRegionId: world.fields.prefectureRegionId,
sea: world.fields.sea,
existing: sourceMap.prefectureRegions || [],
adminCenters: sourceMap.adminCenters || [],
fields: world.fields,
width: world.width,
height: world.height,
pointOffsetX: world.originX || 0,
pointOffsetY: world.originY || 0,
});
sourceMap.prefectureRegions = prefectureCoherence.prefectureRegions;
const strictMaskDebugPostCoherence = restoreOutsideStrictSelectionFields(world, rects, strictFieldSnapshot, seed);
const adminDebug = repairPatchAdministration(world, sourceMap, rects, seed, fieldDebug.adminIdMapping, strictFieldSnapshot);
const residualSeaStrictDebug = fillTinyResidualSeas(world, rects, seaLevel, seed, { aggressive: true, preservePockets: true });
if ((residualSeaStrictDebug.residualSeaCellsFilled || 0) > 0) {
repairDisplayMasks(world, rects, seed);
recomputeSlopeAndWaterDependentFields(world, rects.repairRect, seaLevel);
}
const strictMetadataDebug = restoreOutsideStrictMetadata(world, sourceMap, rects, strictMetadataSnapshot, seed);
sourceMap.adminDebug = {
...(sourceMap.adminDebug || {}),
municipalCoherence: municipalCoherence.debug,
prefectureMetadataCoherence: prefectureCoherence.debug,
};
const segmentDebug = mergeSegmentLayers(world, sourceMap, rects, seed, candidate, fieldDebug.window);
const candidateCompartmentSegmentsAdded = mergeCandidateCompartmentDebugSegments(world, sourceMap, candidate, rects, fieldDebug.window, seed);
patchTimer.mark("segments", "Boundary and debug segment merge");
sanitizeExistingLogistics(sourceMap);
patchTimer.mark("cleanup", "Land-use, admin, and label cleanup");
const patchTimings = patchTimer.timings;
const {
terrainSeamDebug,
elevationCliffDebug,
waterDebug,
waterComponentDebug,
residualSeaDebug,
waterElevationDebug,
maskDebug,
} = terrainDebug;
const {
landDebug,
finalAdminCoverageDebug,
adminTopologyDebug,
strictMaskDebugPreCoherence,
strictMaskDebugPostCoherence,
sourceAdminMetadataUpdated,
municipalCoherence,
prefectureCoherence,
} = adminDebug;
const seaStats = countSea(world, rects.coreRect, rects, seed);
const label = terrainLabel(candidate, terrainType);
const id = terrainId(candidate, terrainType);
const patchStageDebug = {
candidate: { cacheHit, cacheSize, patchGenerationMode, candidateAreaRatio: candidateWindow.areaRatio ?? 1 },
fields: fieldDebug,
terrain: terrainDebug,
points: pointDebug,
transport: pathDebug,
influence: influenceDebug,
admin: adminDebug,
segments: { ...segmentDebug, candidateCompartmentSegmentsAdded },
cleanup: { logisticsLabelsMigrated, strictMetadataDebug, residualSeaStrictDebug },
};
const humanGeography = {
ok: true,
patchStages: patchStageDebug,
modernCities: (sourceMap.modernCities || []).filter((p) => insideRect(pointWorldX(world, p), pointWorldY(world, p), rects.writeRect)).length,
ports: (sourceMap.ports || []).filter((p) => insideRect(pointWorldX(world, p), pointWorldY(world, p), rects.writeRect)).length,
villages: (sourceMap.villages || []).filter((p) => insideRect(pointWorldX(world, p), pointWorldY(world, p), rects.writeRect)).length,

File diff suppressed because it is too large Load diff

24
mapPatchContext.js Normal file
View file

@ -0,0 +1,24 @@
export function createPatchContext({ world, rects, candidateWindow, seed, patchAlpha, sourceIndexForWorld, worldIndex }) {
const writeRect = rects?.writeRect;
const writeCells = [];
if (world && writeRect && typeof patchAlpha === "function" && typeof sourceIndexForWorld === "function" && typeof worldIndex === "function") {
for (let y = writeRect.y0; y < writeRect.y1; y++) {
for (let x = writeRect.x0; x < writeRect.x1; x++) {
const wi = worldIndex(world, x, y);
if (wi < 0) continue;
const si = sourceIndexForWorld(rects, candidateWindow, x, y);
if (si < 0) continue;
const alpha = patchAlpha(x, y, rects, seed);
if (alpha <= 0.005) continue;
writeCells.push({ x, y, wi, si, alpha });
}
}
}
return {
world,
rects,
candidateWindow,
seed,
writeCells,
};
}

View file

@ -2,11 +2,74 @@ import { CELL_SIZE, MAP_H, MAP_W, clamp, valueNoise } from "./mapUtils.js";
const segmentVectorCache = new WeakMap();
const segmentVectorStableCache = new Map();
const pathVectorCache = new WeakMap();
const coastlineCache = new WeakMap();
const rasterBorderCache = new WeakMap();
const baseImageCache = new WeakMap();
const MAX_BASE_CACHE_IMAGES = 4;
const baseImageCache = new Map();
const urbanOverlayCache = new Map();
const prefectureFillCache = new Map();
const MAX_BASE_CACHE_IMAGES = 18;
const MAX_OVERLAY_CACHE_IMAGES = 18;
const MAX_SEGMENT_VECTOR_CACHE = 48;
const CONTINUOUS_BASE_MODES = ["terrain", "development", "all"];
function nowMs() {
return typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
}
function fieldRefSignature(map) {
const refs = [
map?.elevation, map?.slope, map?.sea, map?.prefectureMask, map?.humanRegionMask,
map?.landuse, map?.populationDensity, map?.prefectureRegionId, map?.adminId,
];
return refs.map((field) => ArrayBuffer.isView(field) ? `${field.constructor.name}:${field.length}` : "-").join("|");
}
function generatedRectSignature(map) {
const rects = Array.isArray(map?.generatedRects) ? map.generatedRects : [];
const last = rects[rects.length - 1] || null;
if (!last) return "0";
return `${rects.length}:${last.x0 || 0},${last.y0 || 0},${last.x1 || 0},${last.y1 || 0}:${last.terrainType || ""}:${last.variant || ""}`;
}
function stableViewportCachePrefix(map) {
const camera = map?.worldCamera || {};
const origin = map?.worldOrigin || {};
return [
map?.baseSeed ?? map?.seed ?? 0,
map?.effectiveSeed ?? "",
map?.generationContext?.variant ?? "",
mapWidth(map),
mapHeight(map),
Math.round(camera.x || 0),
Math.round(camera.y || 0),
Math.round(origin.x || 0),
Math.round(origin.y || 0),
generatedRectSignature(map),
fieldRefSignature(map),
].join(":");
}
function cappedSet(cache, key, value, maxSize) {
cache.set(key, value);
while (cache.size > maxSize) cache.delete(cache.keys().next().value);
return value;
}
function segmentContentSignature(segments) {
const count = segments?.length || 0;
if (!count) return "0";
const step = Math.max(1, Math.floor(count / 24));
const parts = [String(count)];
for (let i = 0; i < count; i += step) {
const seg = segments[i];
parts.push(`${seg?.[0]?.[0] || 0},${seg?.[0]?.[1] || 0},${seg?.[1]?.[0] || 0},${seg?.[1]?.[1] || 0}`);
}
const last = segments[count - 1];
parts.push(`${last?.[0]?.[0] || 0},${last?.[0]?.[1] || 0},${last?.[1]?.[0] || 0},${last?.[1]?.[1] || 0}`);
return parts.join("|");
}
function mapWidth(map) {
return Math.max(1, Math.floor(Number.isFinite(map?.width) ? map.width : MAP_W));
@ -189,6 +252,12 @@ function vectorizeSegments(segments, { iterations = 2, tolerance = 0.08 } = {})
segmentVectorCache.set(segments, cachedByOption);
}
if (cachedByOption.has(cacheKey)) return cachedByOption.get(cacheKey);
const stableKey = `${cacheKey}:${segmentContentSignature(segments)}`;
const stableCached = segmentVectorStableCache.get(stableKey);
if (stableCached) {
cachedByOption.set(cacheKey, stableCached);
return stableCached;
}
const polylines = segmentChains(segments).map((line) => {
const cleaned = removeCollinear(line);
@ -199,6 +268,7 @@ function vectorizeSegments(segments, { iterations = 2, tolerance = 0.08 } = {})
}).filter((line) => line.length >= 2);
cachedByOption.set(cacheKey, polylines);
cappedSet(segmentVectorStableCache, stableKey, polylines, MAX_SEGMENT_VECTOR_CACHE);
return polylines;
}
@ -527,35 +597,29 @@ function discreteColor(map, x, y, mode) {
return blendOutside(color, Boolean(map.prefectureMask[i]));
}
function baseCacheKey(mode, continuousTerrain, renderScale = 1) {
const continuousModes = ["terrain", "development", "all"];
const scaleKey = Math.round((renderScale || 1) * 20) / 20;
if (continuousTerrain && continuousModes.includes(mode)) {
return `continuous:${mode === "all" ? "terrain" : mode}:${scaleKey}`;
function baseCacheKey(map, mode, continuousTerrain, renderScale = 1) {
const prefix = stableViewportCachePrefix(map);
if (continuousTerrain && CONTINUOUS_BASE_MODES.includes(mode)) {
const scaleKey = Math.round((renderScale || 1) * 20) / 20;
return `${prefix}:continuous:${mode === "all" ? "terrain" : mode}:${scaleKey}`;
}
return `discrete:${mode}:${scaleKey}`;
return `${prefix}:discrete:${mode}:1`;
}
function getCachedBaseCanvas(ctx, map, mode, continuousTerrain, renderScale = 1) {
let cache = baseImageCache.get(map);
if (!cache) {
cache = new Map();
baseImageCache.set(map, cache);
}
const key = baseCacheKey(mode, continuousTerrain, renderScale);
let canvas = cache.get(key);
const key = baseCacheKey(map, mode, continuousTerrain, renderScale);
let canvas = baseImageCache.get(key);
if (canvas) return canvas;
const sourceWidth = mapPixelWidth(map);
const sourceHeight = mapPixelHeight(map);
const targetScale = Math.max(0.35, Math.min(1, renderScale || 1));
const continuous = continuousTerrain && CONTINUOUS_BASE_MODES.includes(mode);
const targetScale = continuous ? Math.max(0.35, Math.min(1, renderScale || 1)) : 1;
const width = Math.max(1, Math.round(sourceWidth * targetScale));
const height = Math.max(1, Math.round(sourceHeight * targetScale));
const img = ctx.createImageData(width, height);
const continuousModes = ["terrain", "development", "all"];
if (continuousTerrain && continuousModes.includes(mode)) {
if (continuous) {
for (let py = 0; py < height; py++) {
const fy = (py / Math.max(1, height)) * mapHeight(map);
for (let px = 0; px < width; px++) {
@ -594,9 +658,7 @@ function getCachedBaseCanvas(ctx, map, mode, continuousTerrain, renderScale = 1)
canvas.height = height;
const bctx = canvas.getContext("2d");
bctx.putImageData(img, 0, 0);
cache.set(key, canvas);
if (cache.size > MAX_BASE_CACHE_IMAGES) cache.delete(cache.keys().next().value);
return canvas;
return cappedSet(baseImageCache, key, canvas, MAX_BASE_CACHE_IMAGES);
}
function drawBase(ctx, map, mode, continuousTerrain, renderScale = 1) {
@ -842,6 +904,13 @@ function drawUrbanAreas(ctx, map, mode) {
const visibleModes = ["all", "modern", "development", "landuse", "admin"];
if (!visibleModes.includes(mode)) return;
const key = `${stableViewportCachePrefix(map)}:urban:${mode}`;
let canvas = urbanOverlayCache.get(key);
if (canvas) {
ctx.drawImage(canvas, 0, 0, mapPixelWidth(map), mapPixelHeight(map));
return;
}
const detailedColors = {
2: "rgba(223, 214, 206, 0.72)",
3: "rgba(215, 175, 172, 0.88)",
@ -854,7 +923,11 @@ function drawUrbanAreas(ctx, map, mode) {
const cityColor = "rgba(232, 222, 228, 0.60)";
const cbdColor = "rgba(215, 175, 172, 0.84)";
ctx.save();
canvas = document.createElement("canvas");
canvas.width = mapPixelWidth(map);
canvas.height = mapPixelHeight(map);
const bctx = canvas.getContext("2d");
if (!bctx) return;
const w = mapWidth(map);
const h = mapHeight(map);
for (let y = 0; y < h; y++) {
@ -871,11 +944,12 @@ function drawUrbanAreas(ctx, map, mode) {
const px = x * CELL_SIZE;
const py = y * CELL_SIZE;
ctx.fillStyle = fill;
ctx.fillRect(px, py, CELL_SIZE, CELL_SIZE);
bctx.fillStyle = fill;
bctx.fillRect(px, py, CELL_SIZE, CELL_SIZE);
}
}
ctx.restore();
cappedSet(urbanOverlayCache, key, canvas, MAX_OVERLAY_CACHE_IMAGES);
ctx.drawImage(canvas, 0, 0, mapPixelWidth(map), mapPixelHeight(map));
}
function drawDebugCells(ctx, map, field, color) {
@ -940,9 +1014,19 @@ function drawPrefectureRegionFill(ctx, map, mode) {
if (!["all", "admin", "borders-debug"].includes(mode)) return;
const ids = map.prefectureRegionId;
if (!ids) return;
const key = `${stableViewportCachePrefix(map)}:prefecture-fill:${mode}`;
let canvas = prefectureFillCache.get(key);
if (canvas) {
ctx.drawImage(canvas, 0, 0, mapPixelWidth(map), mapPixelHeight(map));
return;
}
const alpha = mode === "borders-debug" ? 0.34 : 0.18;
ctx.save();
ctx.globalAlpha = alpha;
canvas = document.createElement("canvas");
canvas.width = mapPixelWidth(map);
canvas.height = mapPixelHeight(map);
const bctx = canvas.getContext("2d");
if (!bctx) return;
bctx.globalAlpha = alpha;
const w = mapWidth(map);
const h = mapHeight(map);
for (let y = 0; y < h; y++) {
@ -951,11 +1035,12 @@ function drawPrefectureRegionFill(ctx, map, mode) {
const id = ids[i];
if (map.sea[i] || id < 0) continue;
const [r, g, b] = prefectureRegionColor(id);
ctx.fillStyle = `rgb(${r}, ${g}, ${b})`;
ctx.fillRect(x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, CELL_SIZE);
bctx.fillStyle = `rgb(${r}, ${g}, ${b})`;
bctx.fillRect(x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, CELL_SIZE);
}
}
ctx.restore();
cappedSet(prefectureFillCache, key, canvas, MAX_OVERLAY_CACHE_IMAGES);
ctx.drawImage(canvas, 0, 0, mapPixelWidth(map), mapPixelHeight(map));
}
function dot(ctx, p, radius, fill, stroke = "white") {
@ -1090,6 +1175,14 @@ export function drawMap(canvas, map, options) {
const ctx = canvas.getContext("2d");
if (!ctx) return;
const timings = {};
let timingMark = nowMs();
const markTiming = (key) => {
const t = nowMs();
timings[key] = Math.round((t - timingMark) * 10) / 10;
timingMark = t;
};
const mode = options.mode || "all";
const showFeatures = options.showFeatures !== false;
const showLabels = options.showLabels !== false;
@ -1102,7 +1195,8 @@ export function drawMap(canvas, map, options) {
const drawOffsetX = (outputWidth - sourceWidth * drawScale) * 0.5;
const drawOffsetY = (outputHeight - sourceHeight * drawScale) * 0.5;
const cellScreenSize = CELL_SIZE * drawScale;
const terrainRenderScale = continuousTerrain ? clamp(drawScale * (options.fastTerrain ? 0.48 : 1.35), 0.30, 1) : 1;
const terrainQualityScale = options.fastTerrain ? 0.48 : 0.62;
const terrainRenderScale = continuousTerrain ? clamp(drawScale * terrainQualityScale, 0.30, options.fastTerrain ? 0.55 : 0.78) : 1;
if (canvas.width !== outputWidth) canvas.width = outputWidth;
if (canvas.height !== outputHeight) canvas.height = outputHeight;
@ -1117,14 +1211,19 @@ export function drawMap(canvas, map, options) {
drawScaleBar(ctx, cellScreenSize);
delete ctx.__mapPixelWidth;
delete ctx.__mapPixelHeight;
timings.scale = Math.round((nowMs() - timingMark) * 10) / 10;
return timings;
};
// 1. Base Terrain & Urban
drawBase(ctx, map, mode, continuousTerrain, terrainRenderScale);
markTiming("baseTerrain");
drawUrbanAreas(ctx, map, mode);
markTiming("urbanFill");
const coastSegments = getCoastlineSegments(map);
drawVectorSegments(ctx, coastSegments, "rgba(120, 175, 210, 0.22)", 2.2, false, { iterations: 2, tolerance: 0.06, offsetX: -0.5, offsetY: -0.5 });
drawVectorSegments(ctx, coastSegments, "rgba(248, 250, 242, 0.68)", 1.1, false, { iterations: 2, tolerance: 0.06, offsetX: -0.5, offsetY: -0.5 });
markTiming("coastline");
// 2. Rivers
const waterBlue = "rgba(116, 165, 202, 0.92)";
@ -1175,6 +1274,7 @@ export function drawMap(canvas, map, options) {
return 1.55 * downstreamBoost;
}, 1.0);
}
markTiming("rivers");
const showHistory = mode === "history";
const showTransportDebug = mode === "transport-debug";
@ -1214,10 +1314,10 @@ export function drawMap(canvas, map, options) {
drawVectorSegments(ctx, finalPrefectureBorders, "rgba(255, 255, 255, 0.95)", 5.0, false, { iterations: 2, tolerance: 0.06, offsetX: -0.5, offsetY: -0.5 });
drawVectorSegments(ctx, finalPrefectureBorders, "rgba(110, 90, 110, 1)", 2.2, true, { iterations: 2, tolerance: 0.06, offsetX: -0.5, offsetY: -0.5 });
}
markTiming("adminBorders");
if (!showFeatures) {
finish();
return;
return finish();
}
// 4. Transport casings. Layer order: local roads, trunk roads, railways, expressways.
@ -1267,6 +1367,7 @@ export function drawMap(canvas, map, options) {
for (const path of map.expressways) drawPath(ctx, path, "rgba(135, 160, 135, 0.88)", 2.4);
for (const path of map.externalExpressways) drawPath(ctx, path, "rgba(135, 160, 135, 0.88)", 2.4);
}
markTiming("transport");
// 6. Icons & Labels
if (["admin", "borders-debug"].includes(mode)) {
@ -1307,6 +1408,7 @@ export function drawMap(canvas, map, options) {
}
}
}
markTiming("icons");
if (showLabels) {
const prefectureLabels = (map.prefectureRegions || []).map((p) => ({ ...p, isPrefectureLabel: true, forceLabel: true, labelPriorityBase: p.labelPriorityBase || 1900 }));
@ -1315,13 +1417,13 @@ export function drawMap(canvas, map, options) {
.filter((p) => p && p.name && Number.isFinite(p.x) && Number.isFinite(p.y) && p.x >= 0 && p.y >= 0 && p.x < map.width && p.y < map.height && map.adminId?.[Math.round(p.y) * map.width + Math.round(p.x)] === (p.adminId ?? p.municipalityId ?? p.adminNumericId))
.map((p) => ({ ...p, labelStyle: "municipality", forceLabel: true, labelPriorityBase: 1200 }));
drawLabels(ctx, [...prefectureLabels, ...municipalLabels], Infinity);
finish();
return;
markTiming("labels");
return finish();
}
if (mode === "borders-debug") {
drawLabels(ctx, prefectureLabels, Infinity);
finish();
return;
markTiming("labels");
return finish();
}
const important = [
...prefectureLabels,
@ -1332,5 +1434,6 @@ export function drawMap(canvas, map, options) {
].filter((p) => !p.suppressSettlementLabel && (p.isPrefectureLabel || p.forceAllLayerTownLabel || p.insidePrefecture || p.isRegionalCapital || p.kind === "External Gateway" || (p.population || 0) >= 5000));
drawLabels(ctx, important, mode === "all" || mode === "history" ? 78 : 60);
}
finish();
markTiming("labels");
return finish();
}