improoooved
This commit is contained in:
parent
440dba2f09
commit
0990b98436
5 changed files with 358 additions and 2795 deletions
28
app.js
28
app.js
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
258
mapPatch.js
258
mapPatch.js
|
|
@ -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,
|
||||
|
|
|
|||
2660
mapPatch.js.bak
2660
mapPatch.js.bak
File diff suppressed because it is too large
Load diff
24
mapPatchContext.js
Normal file
24
mapPatchContext.js
Normal 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,
|
||||
};
|
||||
}
|
||||
181
renderer.js
181
renderer.js
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue