hm
This commit is contained in:
parent
f2d0306d96
commit
27ceb6568a
7 changed files with 940 additions and 132 deletions
652
mapPatch.js
652
mapPatch.js
|
|
@ -1,6 +1,7 @@
|
|||
import { MAP_H, MAP_W, SIZE, MinHeap, clamp, hash2, lerp, smoothstep, valueNoise } from "./mapUtils.js";
|
||||
import { generateMap } from "./mapPipeline.js";
|
||||
import { LANDUSE } from "./landuseCodes.js";
|
||||
import { reconcileMunicipalMetadata, refreshPrefectureRegionsMetadata } from "./mapMunicipalCoherence.js";
|
||||
|
||||
export const PATCH_MIN_WIDTH = 48;
|
||||
export const PATCH_MIN_HEIGHT = 48;
|
||||
|
|
@ -24,6 +25,7 @@ const RAIL_LAYER_KEYS = new Set(["railways", "branchRailways", "ringRailways", "
|
|||
const RIVER_LAYER_KEYS = new Set(["mainRivers", "tributaryRivers", "smallStreams", "riverPaths"]);
|
||||
|
||||
const SEGMENT_LAYER_KEYS = ["adminBorders", "regionalPrefectureBorders", "prefectureBorder"];
|
||||
const PATCH_CANDIDATE_CACHE_LIMIT = 3;
|
||||
|
||||
const ID_FIELD_OFFSETS = new Map([
|
||||
["adminId", 100000],
|
||||
|
|
@ -72,6 +74,23 @@ function rectArea(rect) {
|
|||
return rectWidth(rect) * rectHeight(rect);
|
||||
}
|
||||
|
||||
function nowMs() {
|
||||
return typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
||||
}
|
||||
|
||||
function createPatchTimer() {
|
||||
const timings = [];
|
||||
let mark = nowMs();
|
||||
return {
|
||||
timings,
|
||||
mark(key, label = key) {
|
||||
const t = nowMs();
|
||||
timings.push({ key, label, ms: Math.round((t - mark) * 10) / 10 });
|
||||
mark = t;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeRect(rect) {
|
||||
if (!rect) return null;
|
||||
const x0 = Math.floor(Math.min(rect.x0, rect.x1));
|
||||
|
|
@ -257,7 +276,10 @@ export function buildPatchRects(userRect, world = null) {
|
|||
const repairMargin = Math.max(writeMargin, Math.min(desiredRepair, maxBySource));
|
||||
const writeRect = expandRect(coreRect, writeMargin, world);
|
||||
const repairRect = expandRect(coreRect, repairMargin, world);
|
||||
const transportReachMargin = Math.max(repairMargin + 160, Math.min(420, Math.max(220, repairMargin + Math.floor(Math.max(MAP_W, MAP_H) * 1.35))));
|
||||
const transportReachMargin = Math.max(
|
||||
repairMargin + 48,
|
||||
Math.min(260, Math.max(96, repairMargin + Math.floor(shortSide * 1.15)))
|
||||
);
|
||||
const transportReachRect = expandRect(coreRect, transportReachMargin, world);
|
||||
return {
|
||||
coreRect,
|
||||
|
|
@ -277,7 +299,7 @@ export function buildPatchRects(userRect, world = null) {
|
|||
};
|
||||
}
|
||||
|
||||
function patchAlpha(x, y, rects, seed = 0) {
|
||||
function computePatchAlpha(x, y, rects, seed = 0) {
|
||||
const writeRect = rects.writeRect || rects.userRect;
|
||||
if (!insideRect(x, y, writeRect)) return 0;
|
||||
const margin = Math.max(1, rects.writeMargin || 1);
|
||||
|
|
@ -289,10 +311,9 @@ function patchAlpha(x, y, rects, seed = 0) {
|
|||
const py = y + 0.5;
|
||||
const inside = pointInPolygon(px, py, shape.polygon);
|
||||
const dist = distanceToPolygonEdge(px, py, shape.polygon);
|
||||
const noisyDist = dist + low * margin * 0.28 + mid * margin * 0.10;
|
||||
if (inside) return 1;
|
||||
if (noisyDist >= margin * 1.08) return 0;
|
||||
return clamp(smoothstep(1 - noisyDist / Math.max(1e-6, margin)));
|
||||
const signedDist = inside ? dist : -dist;
|
||||
const noisySigned = signedDist + low * margin * 0.28 + mid * margin * 0.10;
|
||||
return clamp(smoothstep((noisySigned + margin) / Math.max(1e-6, margin * 2)));
|
||||
}
|
||||
const edge = distanceToRectEdge(x, y, writeRect);
|
||||
const noisyEdge = edge + low * margin * 0.42 + mid * margin * 0.16;
|
||||
|
|
@ -303,6 +324,53 @@ function patchAlpha(x, y, rects, seed = 0) {
|
|||
return clamp(base);
|
||||
}
|
||||
|
||||
function getPatchAlphaCache(rects, seed = 0) {
|
||||
const writeRect = rects?.writeRect || rects?.userRect;
|
||||
if (!writeRect) return null;
|
||||
const width = rectWidth(writeRect);
|
||||
const height = rectHeight(writeRect);
|
||||
const existing = rects.patchAlphaCache;
|
||||
if (
|
||||
existing
|
||||
&& existing.seed === seed
|
||||
&& existing.width === width
|
||||
&& existing.height === height
|
||||
&& existing.x0 === writeRect.x0
|
||||
&& existing.y0 === writeRect.y0
|
||||
) return existing;
|
||||
|
||||
const data = new Float32Array(width * height);
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
data[y * width + x] = computePatchAlpha(writeRect.x0 + x, writeRect.y0 + y, rects, seed);
|
||||
}
|
||||
}
|
||||
rects.patchAlphaCache = { seed, width, height, x0: writeRect.x0, y0: writeRect.y0, data };
|
||||
return rects.patchAlphaCache;
|
||||
}
|
||||
|
||||
function patchAlpha(x, y, rects, seed = 0) {
|
||||
const writeRect = rects?.writeRect || rects?.userRect;
|
||||
if (!writeRect || !insideRect(x, y, writeRect)) return 0;
|
||||
const cache = rects.patchAlphaCache;
|
||||
if (
|
||||
cache
|
||||
&& cache.seed === seed
|
||||
&& x >= cache.x0
|
||||
&& y >= cache.y0
|
||||
&& x < cache.x0 + cache.width
|
||||
&& y < cache.y0 + cache.height
|
||||
) return cache.data[(y - cache.y0) * cache.width + (x - cache.x0)] || 0;
|
||||
return computePatchAlpha(x, y, rects, seed);
|
||||
}
|
||||
|
||||
function patchBand(x, y, rects, seed = 0) {
|
||||
const a = patchAlpha(x, y, rects, seed);
|
||||
if (a <= 0.18) return "preserve";
|
||||
if (a >= 0.82) return "core";
|
||||
return "feather";
|
||||
}
|
||||
|
||||
function continuityReplaceThreshold(name, x, y, rects, seed = 0) {
|
||||
const n = valueNoise(x, y, seed ^ 0x4f1bbcdc, 11) - 0.5;
|
||||
if (ADMIN_CONTINUITY_FIELD_NAMES.has(name)) return clamp(0.82 + n * 0.12, 0.70, 0.92);
|
||||
|
|
@ -315,6 +383,21 @@ function continuitySegmentAllowed(x, y, nx, ny, rects, seed, minAlpha = 0.42) {
|
|||
return Math.min(patchAlpha(x, y, rects, seed), patchAlpha(nx, ny, rects, seed)) >= minAlpha;
|
||||
}
|
||||
|
||||
function patchAffected(x, y, rects, seed = 0, minAlpha = 0.34) {
|
||||
return insideRect(Math.round(x), Math.round(y), rects?.writeRect) && patchAlpha(Math.round(x), Math.round(y), rects, seed) >= minAlpha;
|
||||
}
|
||||
|
||||
function segmentTouchesPatch(world, seg, rects, seed = 0, minAlpha = 0.34) {
|
||||
if (!Array.isArray(seg) || seg.length < 2) return false;
|
||||
const ax = tupleWorldX(world, seg[0]);
|
||||
const ay = tupleWorldY(world, seg[0]);
|
||||
const bx = tupleWorldX(world, seg[1]);
|
||||
const by = tupleWorldY(world, seg[1]);
|
||||
const mx = (ax + bx) * 0.5;
|
||||
const my = (ay + by) * 0.5;
|
||||
return patchAffected(ax, ay, rects, seed, minAlpha) || patchAffected(bx, by, rects, seed, minAlpha) || patchAffected(mx, my, rects, seed, minAlpha);
|
||||
}
|
||||
|
||||
|
||||
function sourceWindowForRects(rects) {
|
||||
const cx = (rects.coreRect.x0 + rects.coreRect.x1 - 1) / 2;
|
||||
|
|
@ -334,6 +417,59 @@ function sourceCoordForWorld(window, x, y) {
|
|||
};
|
||||
}
|
||||
|
||||
function getPatchSourceIndexCache(rects, window) {
|
||||
const writeRect = rects?.writeRect;
|
||||
if (!writeRect || !window) return null;
|
||||
const width = rectWidth(writeRect);
|
||||
const height = rectHeight(writeRect);
|
||||
const existing = rects.patchSourceIndexCache;
|
||||
if (
|
||||
existing
|
||||
&& existing.width === width
|
||||
&& existing.height === height
|
||||
&& existing.x0 === writeRect.x0
|
||||
&& existing.y0 === writeRect.y0
|
||||
&& existing.worldCenterX === window.worldCenterX
|
||||
&& existing.worldCenterY === window.worldCenterY
|
||||
&& existing.sourceCenterX === window.sourceCenterX
|
||||
&& existing.sourceCenterY === window.sourceCenterY
|
||||
) return existing;
|
||||
|
||||
const data = new Int32Array(width * height);
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
const sx = Math.round(writeRect.x0 + x - window.worldCenterX + window.sourceCenterX);
|
||||
const sy = Math.round(writeRect.y0 + y - window.worldCenterY + window.sourceCenterY);
|
||||
data[y * width + x] = sourceIndex(sx, sy);
|
||||
}
|
||||
}
|
||||
rects.patchSourceIndexCache = {
|
||||
width,
|
||||
height,
|
||||
x0: writeRect.x0,
|
||||
y0: writeRect.y0,
|
||||
worldCenterX: window.worldCenterX,
|
||||
worldCenterY: window.worldCenterY,
|
||||
sourceCenterX: window.sourceCenterX,
|
||||
sourceCenterY: window.sourceCenterY,
|
||||
data,
|
||||
};
|
||||
return rects.patchSourceIndexCache;
|
||||
}
|
||||
|
||||
function sourceIndexForWorld(rects, window, x, y) {
|
||||
const cache = rects?.patchSourceIndexCache;
|
||||
if (
|
||||
cache
|
||||
&& x >= cache.x0
|
||||
&& y >= cache.y0
|
||||
&& x < cache.x0 + cache.width
|
||||
&& y < cache.y0 + cache.height
|
||||
) return cache.data[(y - cache.y0) * cache.width + (x - cache.x0)];
|
||||
const s = sourceCoordForWorld(window, x, y);
|
||||
return sourceIndex(s.x, s.y);
|
||||
}
|
||||
|
||||
function worldCoordForSource(window, sx, sy) {
|
||||
return {
|
||||
x: Math.round(sx - window.sourceCenterX + window.worldCenterX),
|
||||
|
|
@ -385,8 +521,7 @@ function collectCandidateIdsInRect(candidateField, rects, window, minAlpha = 0.2
|
|||
for (let y = rect.y0; y < rect.y1; y++) {
|
||||
for (let x = rect.x0; x < rect.x1; x++) {
|
||||
if (patchAlpha(x, y, rects, seed) < minAlpha) continue;
|
||||
const s = sourceCoordForWorld(window, x, y);
|
||||
const si = sourceIndex(s.x, s.y);
|
||||
const si = sourceIndexForWorld(rects, window, x, y);
|
||||
if (si < 0) continue;
|
||||
const id = candidateField[si];
|
||||
if (Number.isFinite(id) && id >= 0) ids.add(Math.floor(id));
|
||||
|
|
@ -671,6 +806,70 @@ function stabilizeContinuitySeam(world, rects, oldFields, seed = 0) {
|
|||
return { continuityCellsRestored: restored, continuityCellsRemapped: remapped };
|
||||
}
|
||||
|
||||
function chooseSeamOwnerValue(world, fieldName, oldField, candidateValue, x, y, rects, seed) {
|
||||
const a = patchAlpha(x, y, rects, seed);
|
||||
const i = worldIndex(world, x, y);
|
||||
const oldValue = oldField?.[i] ?? -1;
|
||||
if (oldValue < 0 || candidateValue < 0) return candidateValue >= 0 ? candidateValue : oldValue;
|
||||
if (a <= 0.24) return oldValue;
|
||||
if (a >= 0.82) return candidateValue;
|
||||
|
||||
const field = world.fields?.[fieldName];
|
||||
const pref = world.fields?.prefectureRegionId;
|
||||
const naturalBarrier = world.fields?.naturalBarrierScore || world.fields?.ridgeField;
|
||||
let oldScore = (1 - a) * 3.0;
|
||||
let candidateScore = a * 3.0;
|
||||
for (const [dx, dy] of [[1,0],[-1,0],[0,1],[0,-1]]) {
|
||||
const ni = worldIndex(world, x + dx, y + dy);
|
||||
if (ni < 0) continue;
|
||||
const neighbor = field?.[ni] ?? -1;
|
||||
if (neighbor === oldValue) oldScore += 1.2;
|
||||
if (neighbor === candidateValue) candidateScore += 1.2;
|
||||
if (fieldName !== "prefectureRegionId" && pref && pref[ni] >= 0) {
|
||||
if (pref[ni] === pref[i] && candidateValue !== oldValue) oldScore += 0.18;
|
||||
}
|
||||
}
|
||||
const barrierBonus = naturalBarrier?.[i] || 0;
|
||||
if (barrierBonus > 0.48 && Math.abs(a - 0.5) < 0.24) {
|
||||
if (a < 0.5) oldScore += barrierBonus * 0.9;
|
||||
else candidateScore += barrierBonus * 0.9;
|
||||
}
|
||||
return candidateScore > oldScore ? candidateValue : oldValue;
|
||||
}
|
||||
|
||||
function repairDiscreteSeamOwnership(world, rects, oldFields, seed = 0) {
|
||||
let adminSeamCellsResolved = 0;
|
||||
let prefectureSeamCellsResolved = 0;
|
||||
for (const name of ["prefectureRegionId", "adminId", "municipalityId"]) {
|
||||
const field = world.fields?.[name];
|
||||
const old = oldFields?.get(name);
|
||||
if (!field || !old) continue;
|
||||
for (let y = rects.writeRect.y0; y < rects.writeRect.y1; y++) {
|
||||
for (let x = rects.writeRect.x0; x < rects.writeRect.x1; x++) {
|
||||
const i = worldIndex(world, x, y);
|
||||
if (i < 0 || world.fields.sea?.[i]) continue;
|
||||
if (patchBand(x, y, rects, seed) !== "feather") continue;
|
||||
const before = field[i];
|
||||
const next = chooseSeamOwnerValue(world, name, old, before, x, y, rects, seed);
|
||||
if (next !== before) {
|
||||
field[i] = next;
|
||||
if (name === "prefectureRegionId") prefectureSeamCellsResolved++;
|
||||
else adminSeamCellsResolved++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (world.fields.adminId && world.fields.municipalityId) {
|
||||
for (let y = rects.writeRect.y0; y < rects.writeRect.y1; y++) {
|
||||
for (let x = rects.writeRect.x0; x < rects.writeRect.x1; x++) {
|
||||
const i = worldIndex(world, x, y);
|
||||
if (i >= 0 && !world.fields.sea?.[i]) world.fields.municipalityId[i] = world.fields.adminId[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
return { adminSeamCellsResolved, prefectureSeamCellsResolved };
|
||||
}
|
||||
|
||||
function copyFullPipelineFields(world, candidate, rects, seed) {
|
||||
const window = sourceWindowForRects(rects);
|
||||
const oldSea = world.fields.sea ? new Uint8Array(world.fields.sea) : null;
|
||||
|
|
@ -703,8 +902,7 @@ function copyFullPipelineFields(world, candidate, rects, seed) {
|
|||
for (let x = rects.writeRect.x0; x < rects.writeRect.x1; x++) {
|
||||
const wi = worldIndex(world, x, y);
|
||||
if (wi < 0) continue;
|
||||
const s = sourceCoordForWorld(window, x, y);
|
||||
const si = sourceIndex(s.x, s.y);
|
||||
const si = sourceIndexForWorld(rects, window, x, y);
|
||||
if (si < 0) continue;
|
||||
const alpha = patchAlpha(x, y, rects, seed);
|
||||
if (alpha <= 0.005) continue;
|
||||
|
|
@ -780,6 +978,7 @@ function copyFullPipelineFields(world, candidate, rects, seed) {
|
|||
}
|
||||
|
||||
const continuityDebug = stabilizeContinuitySeam(world, rects, oldContinuityFields, seed);
|
||||
const seamOwnershipDebug = repairDiscreteSeamOwnership(world, rects, oldContinuityFields, seed);
|
||||
if (world.fields.adminId && world.fields.municipalityId && !candidate?.municipalityId) {
|
||||
for (let y = rects.writeRect.y0; y < rects.writeRect.y1; y++) {
|
||||
for (let x = rects.writeRect.x0; x < rects.writeRect.x1; x++) {
|
||||
|
|
@ -800,6 +999,7 @@ function copyFullPipelineFields(world, candidate, rects, seed) {
|
|||
adminIdMapping,
|
||||
adminIdMappingDebug: summarizeIdMapping(adminIdMapping),
|
||||
...continuityDebug,
|
||||
...seamOwnershipDebug,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -830,6 +1030,47 @@ function repairDisplayMasks(world, rects, seed = 0) {
|
|||
return { displayMaskUpdated };
|
||||
}
|
||||
|
||||
function featherTerrainSeam(world, rects, seed = 0) {
|
||||
const fields = world.fields || {};
|
||||
const smoothKeys = [
|
||||
"elevation", "moisture", "ridgeField", "valleyField", "visibleRavineField",
|
||||
"basinField", "coastalLowland", "plain", "agriculture", "erosionField",
|
||||
"depositionField", "depositionalLowland", "alluvialFanField", "deltaField",
|
||||
"naturalBarrierScore", "settlementScore", "populationDensity",
|
||||
];
|
||||
let terrainFeatherCells = 0;
|
||||
let terrainFeatherValues = 0;
|
||||
for (const key of smoothKeys) {
|
||||
const field = fields[key];
|
||||
if (!field || !ArrayBuffer.isView(field)) continue;
|
||||
const old = new field.constructor(field);
|
||||
for (let y = rects.writeRect.y0; y < rects.writeRect.y1; y++) {
|
||||
for (let x = rects.writeRect.x0; x < rects.writeRect.x1; x++) {
|
||||
if (patchBand(x, y, rects, seed) !== "feather") continue;
|
||||
const i = worldIndex(world, x, y);
|
||||
if (i < 0 || fields.sea?.[i]) continue;
|
||||
let sum = 0;
|
||||
let count = 0;
|
||||
for (const [dx, dy] of [[1,0],[-1,0],[0,1],[0,-1]]) {
|
||||
const ni = worldIndex(world, x + dx, y + dy);
|
||||
if (ni >= 0 && !fields.sea?.[ni]) {
|
||||
sum += old[ni] || 0;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
if (!count) continue;
|
||||
const a = patchAlpha(x, y, rects, seed);
|
||||
const neighborMean = sum / count;
|
||||
const seamWeight = 0.34 * (1 - Math.abs(a - 0.5) * 1.2);
|
||||
field[i] = lerp(field[i] || 0, neighborMean, clamp(seamWeight, 0.08, 0.34));
|
||||
terrainFeatherValues++;
|
||||
if (key === "elevation") terrainFeatherCells++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return { terrainFeatherCells, terrainFeatherValues };
|
||||
}
|
||||
|
||||
|
||||
function nearestLandFieldValue(world, x, y, fieldName, rect, options = {}) {
|
||||
const field = world.fields?.[fieldName];
|
||||
|
|
@ -873,7 +1114,7 @@ function lookupPrefectureForAdmin(sourceMap, adminIdMapping, adminId) {
|
|||
return -1;
|
||||
}
|
||||
|
||||
function repairAdminCoverage(world, sourceMap, rects, adminIdMapping = null) {
|
||||
function repairAdminCoverage(world, sourceMap, rects, adminIdMapping = null, seed = 0) {
|
||||
const admin = world.fields?.adminId;
|
||||
if (!admin) return { seaAdminCellsCleared: 0, landAdminCellsFilled: 0, prefectureCellsFilled: 0, adminPrefectureCellsAligned: 0 };
|
||||
const expected = world.width * world.height;
|
||||
|
|
@ -905,7 +1146,7 @@ function repairAdminCoverage(world, sourceMap, rects, adminIdMapping = null) {
|
|||
prefecture[i] = -1;
|
||||
continue;
|
||||
}
|
||||
const generated = !coverage || coverage[i] || patchAlpha(x, y, rects, 0) > 0.08;
|
||||
const generated = patchAlpha(x, y, rects, seed) > 0.08 || (!coverage && insideRect(x, y, rects.writeRect));
|
||||
if (!generated) continue;
|
||||
|
||||
if (admin[i] < 0) {
|
||||
|
|
@ -958,7 +1199,7 @@ function repairAdminCoverage(world, sourceMap, rects, adminIdMapping = null) {
|
|||
return { seaAdminCellsCleared, landAdminCellsFilled, prefectureCellsFilled, adminPrefectureCellsAligned };
|
||||
}
|
||||
|
||||
function smoothWaterTopology(world, rect, seaLevel = 0.30) {
|
||||
function smoothWaterTopology(world, rect, seaLevel = 0.30, rects = null, seed = 0) {
|
||||
const sea = world.fields.sea;
|
||||
const ocean = world.fields.ocean;
|
||||
const lake = world.fields.lake;
|
||||
|
|
@ -981,8 +1222,11 @@ function smoothWaterTopology(world, rect, seaLevel = 0.30) {
|
|||
if (sea[ni]) seaN++; else landN++;
|
||||
}
|
||||
}
|
||||
if (sea[i] && seaN <= 1 && elevation[i] > seaLevel - 0.035) flips.push([i, 0]);
|
||||
else if (!sea[i] && seaN >= 7 && elevation[i] < seaLevel + 0.055) flips.push([i, 1]);
|
||||
const a = rects ? patchAlpha(x, y, rects, seed) : 1;
|
||||
if (a < 0.24) continue;
|
||||
const strongOnly = a < 0.42;
|
||||
if (sea[i] && seaN <= (strongOnly ? 0 : 1) && elevation[i] > seaLevel - 0.035) flips.push([i, 0]);
|
||||
else if (!sea[i] && seaN >= (strongOnly ? 8 : 7) && elevation[i] < seaLevel + 0.055) flips.push([i, 1]);
|
||||
}
|
||||
}
|
||||
for (const [i, nextSea] of flips) {
|
||||
|
|
@ -1086,6 +1330,45 @@ function sourcePathFromWorld(world, path) {
|
|||
return path.map(([x, y]) => [Math.round(x - world.originX), Math.round(y - world.originY)]);
|
||||
}
|
||||
|
||||
function offsetPointNumericFields(point, fields, offset) {
|
||||
for (const field of fields) if (Number.isFinite(point[field])) point[field] += offset;
|
||||
}
|
||||
|
||||
function normalizeGeneratedPointIds(point, key, seed = 0, adminIdMapping = null) {
|
||||
const rawAdminId = numericFeatureId(point, ["adminId", "municipalityId", "adminNumericId"]);
|
||||
if (rawAdminId >= 0) {
|
||||
const mappedAdminId = adminIdMapping?.admin?.get(rawAdminId) ?? adminIdMapping?.municipality?.get(rawAdminId);
|
||||
if (Number.isFinite(mappedAdminId)) {
|
||||
point.sourceAdminId = rawAdminId;
|
||||
point.adminId = mappedAdminId;
|
||||
point.adminNumericId = mappedAdminId;
|
||||
point.municipalityId = mappedAdminId;
|
||||
} else {
|
||||
offsetPointNumericFields(point, ["adminId", "adminNumericId", "municipalityId"], fieldIdOffset("adminId", seed));
|
||||
if (!Number.isFinite(point.adminId) && Number.isFinite(point.municipalityId)) point.adminId = point.municipalityId;
|
||||
if (!Number.isFinite(point.municipalityId) && Number.isFinite(point.adminId)) point.municipalityId = point.adminId;
|
||||
}
|
||||
}
|
||||
|
||||
const rawPrefectureId = numericFeatureId(point, key === "prefectureRegions" ? ["prefectureRegionId", "id"] : ["prefectureRegionId"]);
|
||||
if (rawPrefectureId >= 0) {
|
||||
const mappedPrefectureId = adminIdMapping?.prefecture?.get(rawPrefectureId);
|
||||
if (Number.isFinite(mappedPrefectureId)) {
|
||||
point.sourcePrefectureRegionId = rawPrefectureId;
|
||||
point.prefectureRegionId = mappedPrefectureId;
|
||||
if (key === "prefectureRegions") point.id = mappedPrefectureId;
|
||||
} else {
|
||||
const offset = fieldIdOffset("prefectureRegionId", seed);
|
||||
if (Number.isFinite(point.prefectureRegionId)) point.prefectureRegionId += offset;
|
||||
if (key === "prefectureRegions" && Number.isFinite(point.id)) point.id += offset;
|
||||
}
|
||||
}
|
||||
|
||||
if (Number.isFinite(point.adminId) && !Number.isFinite(point.municipalityId)) point.municipalityId = point.adminId;
|
||||
if (Number.isFinite(point.municipalityId) && !Number.isFinite(point.adminId)) point.adminId = point.municipalityId;
|
||||
return point;
|
||||
}
|
||||
|
||||
function transformCandidatePoint(world, window, p, key, seed = 0, adminIdMapping = null) {
|
||||
if (!p || !Number.isFinite(p.x) || !Number.isFinite(p.y)) return null;
|
||||
const w = worldCoordForSource(window, p.x, p.y);
|
||||
|
|
@ -1099,39 +1382,12 @@ function transformCandidatePoint(world, window, p, key, seed = 0, adminIdMapping
|
|||
w.x = land.x; w.y = land.y;
|
||||
}
|
||||
const out = sourcePointFromWorld(world, { ...p, x: w.x, y: w.y });
|
||||
normalizeGeneratedPointIds(out, key, seed, adminIdMapping);
|
||||
if (key === "adminCenters") {
|
||||
const rawAdminId = numericFeatureId(out, ["adminId", "municipalityId", "adminNumericId"]);
|
||||
const mappedAdminId = rawAdminId >= 0
|
||||
? adminIdMapping?.admin?.get(rawAdminId) ?? adminIdMapping?.municipality?.get(rawAdminId)
|
||||
: undefined;
|
||||
if (Number.isFinite(mappedAdminId)) {
|
||||
out.sourceAdminId = rawAdminId;
|
||||
out.adminId = mappedAdminId;
|
||||
out.adminNumericId = mappedAdminId;
|
||||
out.municipalityId = mappedAdminId;
|
||||
const candidatePrefId = adminIdMapping?.candidateAdminToPrefecture?.get(rawAdminId);
|
||||
if (Number.isFinite(out.sourceAdminId)) {
|
||||
const candidatePrefId = adminIdMapping?.candidateAdminToPrefecture?.get(out.sourceAdminId);
|
||||
const mappedPrefId = adminIdMapping?.prefecture?.get(candidatePrefId);
|
||||
if (Number.isFinite(mappedPrefId)) out.prefectureRegionId = mappedPrefId;
|
||||
} else {
|
||||
const offset = fieldIdOffset("adminId", seed);
|
||||
if (Number.isFinite(out.adminId)) out.adminId += offset;
|
||||
if (Number.isFinite(out.adminNumericId)) out.adminNumericId += offset;
|
||||
if (Number.isFinite(out.municipalityId)) out.municipalityId += offset;
|
||||
if (!Number.isFinite(out.adminId) && Number.isFinite(out.municipalityId)) out.adminId = out.municipalityId;
|
||||
if (!Number.isFinite(out.municipalityId) && Number.isFinite(out.adminId)) out.municipalityId = out.adminId;
|
||||
}
|
||||
}
|
||||
if (key === "prefectureRegions") {
|
||||
const rawPrefectureId = numericFeatureId(out, ["prefectureRegionId", "id"]);
|
||||
const mappedPrefectureId = rawPrefectureId >= 0 ? adminIdMapping?.prefecture?.get(rawPrefectureId) : undefined;
|
||||
if (Number.isFinite(mappedPrefectureId)) {
|
||||
out.sourcePrefectureRegionId = rawPrefectureId;
|
||||
out.id = mappedPrefectureId;
|
||||
out.prefectureRegionId = mappedPrefectureId;
|
||||
} else {
|
||||
const offset = fieldIdOffset("prefectureRegionId", seed);
|
||||
if (Number.isFinite(out.id)) out.id += offset;
|
||||
if (Number.isFinite(out.prefectureRegionId)) out.prefectureRegionId += offset;
|
||||
}
|
||||
}
|
||||
if (key === "logisticsParks") sanitizeLogisticsPark(out);
|
||||
|
|
@ -1181,12 +1437,12 @@ function transformCandidatePath(window, path) {
|
|||
return out;
|
||||
}
|
||||
|
||||
function splitWorldPathByRect(path, rect, keepInside) {
|
||||
function splitWorldPathByPredicate(path, predicate, keepWhenTrue) {
|
||||
const chunks = [];
|
||||
let current = [];
|
||||
for (const p of path || []) {
|
||||
const inside = insideRect(Math.round(p[0]), Math.round(p[1]), rect);
|
||||
if (inside === keepInside) current.push([Math.round(p[0]), Math.round(p[1])]);
|
||||
const matches = predicate(Math.round(p[0]), Math.round(p[1]));
|
||||
if (matches === keepWhenTrue) current.push([Math.round(p[0]), Math.round(p[1])]);
|
||||
else {
|
||||
if (current.length >= 2) chunks.push(current);
|
||||
current = [];
|
||||
|
|
@ -1196,13 +1452,21 @@ function splitWorldPathByRect(path, rect, keepInside) {
|
|||
return chunks;
|
||||
}
|
||||
|
||||
function pruneOldPathLayer(world, paths, rect, mode) {
|
||||
function splitWorldPathByRect(path, rect, keepInside) {
|
||||
return splitWorldPathByPredicate(path, (x, y) => insideRect(x, y, rect), keepInside);
|
||||
}
|
||||
|
||||
function splitWorldPathByPatch(path, rects, seed, keepAffected, minAlpha = 0.34) {
|
||||
return splitWorldPathByPredicate(path, (x, y) => patchAffected(x, y, rects, seed, minAlpha), keepAffected);
|
||||
}
|
||||
|
||||
function pruneOldPathLayer(world, paths, rects, seed, mode) {
|
||||
const kept = [];
|
||||
const anchors = [];
|
||||
let clipped = 0;
|
||||
for (const path of paths || []) {
|
||||
const worldPath = (path || []).map((tuple) => [Math.round(tupleWorldX(world, tuple)), Math.round(tupleWorldY(world, tuple))]);
|
||||
const touches = worldPath.some(([x, y]) => insideRect(x, y, rect));
|
||||
const touches = worldPath.some(([x, y]) => patchAffected(x, y, rects, seed, 0.34));
|
||||
if (!touches) {
|
||||
kept.push(path);
|
||||
continue;
|
||||
|
|
@ -1211,7 +1475,7 @@ function pruneOldPathLayer(world, paths, rect, mode) {
|
|||
let lastOutside = null;
|
||||
let wasInside = false;
|
||||
for (const [x, y] of worldPath) {
|
||||
const inside = insideRect(x, y, rect);
|
||||
const inside = patchAffected(x, y, rects, seed, 0.34);
|
||||
if (!inside) {
|
||||
if (wasInside) anchors.push({ x, y, mode });
|
||||
lastOutside = { x, y, mode };
|
||||
|
|
@ -1220,7 +1484,7 @@ function pruneOldPathLayer(world, paths, rect, mode) {
|
|||
}
|
||||
wasInside = inside;
|
||||
}
|
||||
for (const chunk of splitWorldPathByRect(worldPath, rect, false)) kept.push(sourcePathFromWorld(world, chunk));
|
||||
for (const chunk of splitWorldPathByPatch(worldPath, rects, seed, false, 0.34)) kept.push(sourcePathFromWorld(world, chunk));
|
||||
}
|
||||
return { kept, anchors, clipped };
|
||||
}
|
||||
|
|
@ -1357,28 +1621,38 @@ function connectAnchors(world, sourceMap, anchors, mode, rect, preferredTargetRe
|
|||
const allTargets = collectInternalNetworkPoints(world, sourceMap, keys, rect, mode);
|
||||
const preferredTargets = preferredTargetRect ? allTargets.filter((p) => insideRect(p.x, p.y, preferredTargetRect)) : [];
|
||||
const targets = preferredTargets.length ? preferredTargets : allTargets;
|
||||
if (!targets.length) return { connectors: 0, disconnected: anchors.length };
|
||||
if (!targets.length) return { connectors: 0, disconnected: anchors.length, skippedConnectorAnchors: 0, connectorAttempts: 0 };
|
||||
let connectors = 0;
|
||||
let disconnected = 0;
|
||||
let skippedConnectorAnchors = 0;
|
||||
let connectorAttempts = 0;
|
||||
const layer = mode === "rail" ? "branchRailways" : "minorRoads";
|
||||
sourceMap[layer] ||= [];
|
||||
const seen = new Set();
|
||||
const maxRange = mode === "rail" ? 320 : 360;
|
||||
for (const raw of anchors) {
|
||||
const maxRange = mode === "rail" ? 220 : 260;
|
||||
const maxAnchors = mode === "rail" ? 10 : 18;
|
||||
const maxTargets = mode === "rail" ? 3 : 3;
|
||||
const searchRect = expandRect(rect, 16, world);
|
||||
const orderedAnchors = (anchors || [])
|
||||
.map((p) => ({ ...p, patchDistance: rectDistance(p.x, p.y, preferredTargetRect || rect) }))
|
||||
.sort((a, b) => a.patchDistance - b.patchDistance)
|
||||
.slice(0, maxAnchors);
|
||||
skippedConnectorAnchors = Math.max(0, (anchors?.length || 0) - orderedAnchors.length);
|
||||
for (const raw of orderedAnchors) {
|
||||
const anchorLand = nearestLand(world, raw.x, raw.y, rect, 18);
|
||||
if (!anchorLand) { disconnected++; continue; }
|
||||
const targetList = targets
|
||||
.map((p) => ({ ...p, d: Math.hypot(p.x - anchorLand.x, p.y - anchorLand.y) }))
|
||||
.filter((p) => p.d <= maxRange && p.d >= 6)
|
||||
.sort((a, b) => (a.d / Math.max(0.6, a.weight || 1)) - (b.d / Math.max(0.6, b.weight || 1)))
|
||||
.slice(0, 5);
|
||||
.slice(0, maxTargets);
|
||||
if (!targetList.length) { disconnected++; continue; }
|
||||
let made = false;
|
||||
for (const target of targetList) {
|
||||
const sig = `${anchorLand.x},${anchorLand.y}:${target.x},${target.y}:${mode}`;
|
||||
if (seen.has(sig)) continue;
|
||||
const searchRect = expandRect(rect, 24, world);
|
||||
const path = localPathfind(world, anchorLand, target, searchRect, mode, mode === "rail" ? 62000 : 76000);
|
||||
connectorAttempts++;
|
||||
const path = localPathfind(world, anchorLand, target, searchRect, mode, mode === "rail" ? 36000 : 44000);
|
||||
if (!path || path.length < 2) continue;
|
||||
seen.add(sig);
|
||||
sourceMap[layer].push(sourcePathFromWorld(world, simplifyPath(path, mode === "rail" ? 3 : 2)));
|
||||
|
|
@ -1388,7 +1662,7 @@ function connectAnchors(world, sourceMap, anchors, mode, rect, preferredTargetRe
|
|||
}
|
||||
if (!made) disconnected++;
|
||||
}
|
||||
return { connectors, disconnected };
|
||||
return { connectors, disconnected, skippedConnectorAnchors, connectorAttempts };
|
||||
}
|
||||
|
||||
function nearestNetworkPoint(world, sourceMap, keys, point, rect, maxDistance = 80) {
|
||||
|
|
@ -1413,23 +1687,37 @@ function ensureSettlementRoadCoverage(world, sourceMap, rect) {
|
|||
const featureKeys = ["modernCities", "ports", "markets", "adminCenters", "villages"];
|
||||
sourceMap.minorRoads ||= [];
|
||||
let connectors = 0;
|
||||
let skippedServedSettlements = 0;
|
||||
let checked = 0;
|
||||
const seen = new Set();
|
||||
for (const key of featureKeys) {
|
||||
for (const p of sourceMap[key] || []) {
|
||||
const limit = key === "villages" ? 30 : 18;
|
||||
const items = (sourceMap[key] || [])
|
||||
.map((p) => ({ p, d: rectDistance(pointWorldX(world, p), pointWorldY(world, p), rect) }))
|
||||
.filter((row) => row.d <= (key === "villages" ? 80 : 150))
|
||||
.sort((a, b) => a.d - b.d)
|
||||
.slice(0, limit);
|
||||
for (const { p } of items) {
|
||||
checked++;
|
||||
const start = nearestLand(world, pointWorldX(world, p), pointWorldY(world, p), rect, 10);
|
||||
if (!start || !insideRect(start.x, start.y, rect)) continue;
|
||||
const si = worldIndex(world, start.x, start.y);
|
||||
if ((world.fields.roadInfluence?.[si] || 0) > (key === "villages" ? 0.18 : 0.12)) {
|
||||
skippedServedSettlements++;
|
||||
continue;
|
||||
}
|
||||
const target = nearestNetworkPoint(world, sourceMap, keys, start, rect, key === "villages" ? 72 : 132);
|
||||
if (!target || Math.hypot(target.x - start.x, target.y - start.y) < 5) continue;
|
||||
const sig = `${start.x},${start.y}:${target.x},${target.y}`;
|
||||
if (seen.has(sig)) continue;
|
||||
seen.add(sig);
|
||||
const path = localPathfind(world, start, target, rect, "road", 64000);
|
||||
const path = localPathfind(world, start, target, rect, "road", 36000);
|
||||
if (!path || path.length < 2) continue;
|
||||
sourceMap.minorRoads.push(sourcePathFromWorld(world, simplifyPath(path, 2)));
|
||||
connectors++;
|
||||
}
|
||||
}
|
||||
return connectors;
|
||||
return { connectors, skippedServedSettlements, checkedSettlementCoverage: checked };
|
||||
}
|
||||
|
||||
function dedupeAdminCentersByWorldId(kept, generated) {
|
||||
|
|
@ -1478,9 +1766,11 @@ function mergePointLayers(world, sourceMap, candidate, rects, window, seed, admi
|
|||
const inWrite = insideRect(wx, wy, rects.writeRect);
|
||||
const alpha = inWrite ? patchAlpha(wx, wy, rects, seed) : 0;
|
||||
if (!inWrite || alpha < 0.34) {
|
||||
if (key === "ports" && inWrite && (!isLand(world, wx, wy) || seaNeighbors(world, wx, wy, 2) < 2)) { invalidPortsRemoved++; continue; }
|
||||
kept.push(p);
|
||||
if (!inWrite) preservedExternalEntities++;
|
||||
} else if (key === "ports" && (!isLand(world, wx, wy) || seaNeighbors(world, wx, wy, 2) < 2)) {
|
||||
invalidPortsRemoved++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const generated = [];
|
||||
|
|
@ -1510,13 +1800,13 @@ function mergePathLayers(world, sourceMap, candidate, rects, window, seed) {
|
|||
for (const key of PATH_LAYER_KEYS) {
|
||||
const oldArr = Array.isArray(sourceMap[key]) ? sourceMap[key] : [];
|
||||
const mode = RAIL_LAYER_KEYS.has(key) ? "rail" : ROAD_LAYER_KEYS.has(key) ? "road" : RIVER_LAYER_KEYS.has(key) ? "river" : "path";
|
||||
const pruned = pruneOldPathLayer(world, oldArr, rects.writeRect, mode);
|
||||
const pruned = pruneOldPathLayer(world, oldArr, rects, seed, mode);
|
||||
if (mode === "rail") { railAnchors = railAnchors.concat(pruned.anchors); railsClipped += pruned.clipped; }
|
||||
else if (mode === "road") { roadAnchors = roadAnchors.concat(pruned.anchors); roadsClipped += pruned.clipped; }
|
||||
const next = [...pruned.kept];
|
||||
for (const path of candidate[key] || []) {
|
||||
const worldPath = transformCandidatePath(window, path);
|
||||
const chunks = splitWorldPathByRect(worldPath, rects.writeRect, true)
|
||||
const chunks = splitWorldPathByPatch(worldPath, rects, seed, true, 0.40)
|
||||
.map((chunk) => chunk.filter(([x, y]) => mode === "river" || isLand(world, x, y) || patchAlpha(x, y, rects, seed) > 0.90))
|
||||
.filter((chunk) => chunk.length >= 2);
|
||||
for (const chunk of chunks) {
|
||||
|
|
@ -1540,21 +1830,19 @@ function mergePathLayers(world, sourceMap, candidate, rects, window, seed) {
|
|||
roadsClipped,
|
||||
railsClipped,
|
||||
regeneratedPaths,
|
||||
roadConnectorsCreated: roadConn.connectors + settlementRoadConnectors,
|
||||
roadConnectorsCreated: roadConn.connectors + settlementRoadConnectors.connectors,
|
||||
railwayConnectorsCreated: railConn.connectors,
|
||||
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,
|
||||
externalRoadAnchors: externalRoadAnchors.length,
|
||||
externalRailAnchors: externalRailAnchors.length,
|
||||
};
|
||||
}
|
||||
|
||||
function segmentTouchesRect(world, seg, rect) {
|
||||
if (!Array.isArray(seg) || seg.length < 2) return false;
|
||||
return insideRect(tupleWorldX(world, seg[0]), tupleWorldY(world, seg[0]), rect)
|
||||
|| insideRect(tupleWorldX(world, seg[1]), tupleWorldY(world, seg[1]), rect);
|
||||
}
|
||||
|
||||
function buildBoundarySegmentsFromField(world, fieldName, rect, options = {}) {
|
||||
const field = world.fields[fieldName];
|
||||
const sea = world.fields.sea;
|
||||
|
|
@ -1603,7 +1891,7 @@ function mergeCandidateCompartmentDebugSegments(world, sourceMap, candidate, rec
|
|||
function mergeSegmentLayers(world, sourceMap, rects, seed = 0) {
|
||||
for (const key of SEGMENT_LAYER_KEYS) {
|
||||
const oldArr = Array.isArray(sourceMap[key]) ? sourceMap[key] : [];
|
||||
sourceMap[key] = oldArr.filter((seg) => !segmentTouchesRect(world, seg, rects.writeRect));
|
||||
sourceMap[key] = oldArr.filter((seg) => !segmentTouchesPatch(world, seg, rects, seed, 0.34));
|
||||
}
|
||||
sourceMap.adminBorders ||= [];
|
||||
sourceMap.adminBorders.push(...buildBoundarySegmentsFromField(world, "adminId", rects.writeRect, { rects, seed, minAlpha: 0.58 }));
|
||||
|
|
@ -1616,7 +1904,7 @@ function mergeSegmentLayers(world, sourceMap, rects, seed = 0) {
|
|||
// areas. Rebuilding directly from the raster field made patch compartments
|
||||
// look denser/smaller than the initial map. Candidate segments are merged
|
||||
// just after this function.
|
||||
debug.compartmentBorders = (debug.compartmentBorders || []).filter((seg) => !segmentTouchesRect(world, seg, rects.writeRect));
|
||||
debug.compartmentBorders = (debug.compartmentBorders || []).filter((seg) => !segmentTouchesPatch(world, seg, rects, seed, 0.34));
|
||||
sourceMap.adminDebug = debug;
|
||||
return {
|
||||
adminBordersRebuilt: sourceMap.adminBorders.length,
|
||||
|
|
@ -1649,6 +1937,89 @@ function repairLanduseAndPopulation(world, rects) {
|
|||
return { landUseCellsUpdated: updated };
|
||||
}
|
||||
|
||||
function ensureWorldFloatField(world, name) {
|
||||
const expected = world.width * world.height;
|
||||
if (!world.fields[name] || world.fields[name].length !== expected) world.fields[name] = new Float32Array(expected);
|
||||
return world.fields[name];
|
||||
}
|
||||
|
||||
function clearFieldRect(world, field, rect) {
|
||||
for (let y = rect.y0; y < rect.y1; y++) {
|
||||
for (let x = rect.x0; x < rect.x1; x++) {
|
||||
const i = worldIndex(world, x, y);
|
||||
if (i >= 0) field[i] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function paintInfluenceDisk(world, field, cx, cy, radius, strength, rect) {
|
||||
const sea = world.fields.sea;
|
||||
const r = Math.max(1, Math.ceil(radius));
|
||||
for (let dy = -r; dy <= r; dy++) {
|
||||
for (let dx = -r; dx <= r; dx++) {
|
||||
if (dx * dx + dy * dy > radius * radius) continue;
|
||||
const x = Math.round(cx + dx);
|
||||
const y = Math.round(cy + dy);
|
||||
if (!insideRect(x, y, rect)) continue;
|
||||
const i = worldIndex(world, x, y);
|
||||
if (i < 0 || sea?.[i]) continue;
|
||||
const d = Math.hypot(dx, dy);
|
||||
const value = strength * Math.pow(1 - d / Math.max(1, radius), 1.35);
|
||||
if (value > field[i]) field[i] = clamp(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function refreshPatchInfluenceFields(world, sourceMap, rects) {
|
||||
const rect = rects.repairRect || rects.writeRect;
|
||||
const roadInfluence = ensureWorldFloatField(world, "roadInfluence");
|
||||
const railInfluence2 = ensureWorldFloatField(world, "railInfluence2");
|
||||
const stationInfluence = ensureWorldFloatField(world, "stationInfluence");
|
||||
const villageInfluence = ensureWorldFloatField(world, "villageInfluence");
|
||||
for (const field of [roadInfluence, railInfluence2, stationInfluence, villageInfluence]) clearFieldRect(world, field, rect);
|
||||
|
||||
let roadCellsPainted = 0;
|
||||
let railCellsPainted = 0;
|
||||
let stationCellsPainted = 0;
|
||||
let villageCellsPainted = 0;
|
||||
const paintPathLayer = (keys, field, radius, strength, counterName) => {
|
||||
let painted = 0;
|
||||
for (const key of keys) {
|
||||
for (const path of sourceMap[key] || []) {
|
||||
for (const tuple of path || []) {
|
||||
const x = tupleWorldX(world, tuple);
|
||||
const y = tupleWorldY(world, tuple);
|
||||
if (rectDistance(x, y, rect) > radius + 1) continue;
|
||||
paintInfluenceDisk(world, field, x, y, radius, strength, rect);
|
||||
painted++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (counterName === "road") roadCellsPainted += painted;
|
||||
if (counterName === "rail") railCellsPainted += painted;
|
||||
};
|
||||
|
||||
paintPathLayer(["nationalRoads", "ringRoads", "externalRoads", "minorRoads", "premodernRoads", "icAccessRoads"], roadInfluence, 5, 1, "road");
|
||||
paintPathLayer(["railways", "branchRailways", "ringRailways", "externalRailways"], railInfluence2, 4, 1, "rail");
|
||||
|
||||
for (const p of sourceMap.stations || []) {
|
||||
const x = pointWorldX(world, p);
|
||||
const y = pointWorldY(world, p);
|
||||
if (rectDistance(x, y, rect) > 8) continue;
|
||||
paintInfluenceDisk(world, stationInfluence, x, y, 5, clamp(p.score || 1), rect);
|
||||
stationCellsPainted++;
|
||||
}
|
||||
for (const p of sourceMap.villages || []) {
|
||||
const x = pointWorldX(world, p);
|
||||
const y = pointWorldY(world, p);
|
||||
if (rectDistance(x, y, rect) > 10) continue;
|
||||
paintInfluenceDisk(world, villageInfluence, x, y, 6, clamp((p.population || 1800) / 4200, 0.35, 1.2), rect);
|
||||
villageCellsPainted++;
|
||||
}
|
||||
|
||||
return { roadCellsPainted, railCellsPainted, stationCellsPainted, villageCellsPainted };
|
||||
}
|
||||
|
||||
function countSea(world, rect) {
|
||||
let seaCount = 0;
|
||||
let total = 0;
|
||||
|
|
@ -1671,6 +2042,39 @@ function terrainId(candidate, fallback) {
|
|||
return candidate?.terrainTemplate?.terrainType || candidate?.terrainDebug?.terrainType || fallback;
|
||||
}
|
||||
|
||||
function rectKey(rect) {
|
||||
return rect ? `${rect.x0},${rect.y0},${rect.x1},${rect.y1}` : "-";
|
||||
}
|
||||
|
||||
function patchCandidateCacheKey({ seed, terrainType, variant, candidateOriginX, candidateOriginY, contextRect, serial = 0 }) {
|
||||
return [serial, seed >>> 0, terrainType || "auto", variant >>> 0, candidateOriginX | 0, candidateOriginY | 0, rectKey(contextRect)].join("|");
|
||||
}
|
||||
|
||||
function getPatchCandidateCache(world) {
|
||||
if (!world.patchCandidateCache) world.patchCandidateCache = new Map();
|
||||
return world.patchCandidateCache;
|
||||
}
|
||||
|
||||
function rememberPatchCandidate(world, key, candidate) {
|
||||
const cache = getPatchCandidateCache(world);
|
||||
if (cache.has(key)) cache.delete(key);
|
||||
cache.set(key, candidate);
|
||||
while (cache.size > PATCH_CANDIDATE_CACHE_LIMIT) cache.delete(cache.keys().next().value);
|
||||
}
|
||||
|
||||
function getOrGeneratePatchCandidate(world, key, create) {
|
||||
const cache = getPatchCandidateCache(world);
|
||||
if (cache.has(key)) {
|
||||
const candidate = cache.get(key);
|
||||
cache.delete(key);
|
||||
cache.set(key, candidate);
|
||||
return { candidate, cacheHit: true, cacheSize: cache.size };
|
||||
}
|
||||
const candidate = create();
|
||||
rememberPatchCandidate(world, key, candidate);
|
||||
return { candidate, cacheHit: false, cacheSize: getPatchCandidateCache(world).size };
|
||||
}
|
||||
|
||||
export function generatePatch(world, userRectInput, options = {}) {
|
||||
const validation = validatePatchRect(userRectInput, world);
|
||||
if (!validation.ok) return { ok: false, ...validation };
|
||||
|
|
@ -1682,35 +2086,90 @@ export function generatePatch(world, userRectInput, options = {}) {
|
|||
const candidateWindow = sourceWindowForRects(rects);
|
||||
const candidateOriginX = Math.round(candidateWindow.worldCenterX - candidateWindow.sourceCenterX);
|
||||
const candidateOriginY = Math.round(candidateWindow.worldCenterY - candidateWindow.sourceCenterY);
|
||||
const candidate = generateMap(seed, {
|
||||
const patchTimer = createPatchTimer();
|
||||
const patchGenerationMode = "legacy-full-pipeline";
|
||||
const cacheKey = patchCandidateCacheKey({
|
||||
seed,
|
||||
terrainType,
|
||||
legacyTerrain: true,
|
||||
worldNative: true,
|
||||
variant,
|
||||
originX: candidateOriginX,
|
||||
originY: candidateOriginY,
|
||||
width: MAP_W,
|
||||
height: MAP_H,
|
||||
candidateOriginX,
|
||||
candidateOriginY,
|
||||
contextRect: rects.contextRect,
|
||||
boundaryWorld: world,
|
||||
onProgress: () => {},
|
||||
serial: world.patchGenerationSerial || 0,
|
||||
});
|
||||
const { candidate, cacheHit, cacheSize } = getOrGeneratePatchCandidate(world, cacheKey, () => generateMap(seed, {
|
||||
terrainType,
|
||||
legacyTerrain: true,
|
||||
worldNative: true,
|
||||
variant,
|
||||
originX: candidateOriginX,
|
||||
originY: candidateOriginY,
|
||||
width: MAP_W,
|
||||
height: MAP_H,
|
||||
contextRect: rects.contextRect,
|
||||
boundaryWorld: world,
|
||||
onProgress: () => {},
|
||||
}));
|
||||
patchTimer.mark("candidate", cacheHit ? "Full candidate generation (cached)" : "Full candidate generation");
|
||||
getPatchAlphaCache(rects, seed);
|
||||
getPatchSourceIndexCache(rects, candidateWindow);
|
||||
const sourceMap = world.sourceMap || (world.sourceMap = {});
|
||||
const logisticsLabelsMigrated = sanitizeExistingLogistics(sourceMap);
|
||||
|
||||
const fieldDebug = copyFullPipelineFields(world, candidate, rects, seed);
|
||||
const waterDebug = smoothWaterTopology(world, rects.writeRect, candidate.seaLevel || world.sourceMap?.seaLevel || 0.30);
|
||||
patchTimer.mark("fields", "Field copy and alpha blend");
|
||||
const terrainSeamDebug = featherTerrainSeam(world, rects, seed);
|
||||
const waterDebug = smoothWaterTopology(world, rects.writeRect, candidate.seaLevel || world.sourceMap?.seaLevel || 0.30, rects, seed);
|
||||
const maskDebug = repairDisplayMasks(world, rects, seed);
|
||||
recomputeSlopeAndWaterDependentFields(world, rects.repairRect, candidate.seaLevel || world.sourceMap?.seaLevel || 0.30);
|
||||
const sourceAdminMetadataUpdated = updateSourceAdminMetadata(sourceMap, fieldDebug.adminIdMapping);
|
||||
const adminCoverageDebug = repairAdminCoverage(world, sourceMap, rects, fieldDebug.adminIdMapping);
|
||||
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);
|
||||
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 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 || [],
|
||||
fields: world.fields,
|
||||
width: world.width,
|
||||
height: world.height,
|
||||
pointOffsetX: world.originX || 0,
|
||||
pointOffsetY: world.originY || 0,
|
||||
});
|
||||
sourceMap.prefectureRegions = prefectureCoherence.prefectureRegions;
|
||||
sourceMap.adminDebug = {
|
||||
...(sourceMap.adminDebug || {}),
|
||||
municipalCoherence: municipalCoherence.debug,
|
||||
prefectureMetadataCoherence: prefectureCoherence.debug,
|
||||
};
|
||||
const segmentDebug = mergeSegmentLayers(world, sourceMap, rects, seed);
|
||||
const candidateCompartmentSegmentsAdded = mergeCandidateCompartmentDebugSegments(world, sourceMap, candidate, rects, fieldDebug.window, seed);
|
||||
const landDebug = repairLanduseAndPopulation(world, rects);
|
||||
const finalAdminCoverageDebug = repairAdminCoverage(world, sourceMap, rects, fieldDebug.adminIdMapping);
|
||||
patchTimer.mark("segments", "Boundary and debug segment merge");
|
||||
sanitizeExistingLogistics(sourceMap);
|
||||
patchTimer.mark("cleanup", "Land-use, admin, and label cleanup");
|
||||
const patchTimings = patchTimer.timings;
|
||||
|
||||
const seaStats = countSea(world, rects.coreRect);
|
||||
const label = terrainLabel(candidate, terrainType);
|
||||
|
|
@ -1722,19 +2181,27 @@ export function generatePatch(world, userRectInput, options = {}) {
|
|||
villages: (sourceMap.villages || []).filter((p) => insideRect(pointWorldX(world, p), pointWorldY(world, p), rects.writeRect)).length,
|
||||
...pointDebug,
|
||||
...pathDebug,
|
||||
...influenceDebug,
|
||||
adminCellsReassigned: fieldDebug.adminCellsReassigned,
|
||||
adminIdMapping: fieldDebug.adminIdMappingDebug,
|
||||
sourceAdminMetadataUpdated,
|
||||
...adminCoverageDebug,
|
||||
...finalAdminCoverageDebug,
|
||||
finalSeaAdminCellsCleared: finalAdminCoverageDebug.seaAdminCellsCleared || 0,
|
||||
finalLandAdminCellsFilled: finalAdminCoverageDebug.landAdminCellsFilled || 0,
|
||||
finalPrefectureCellsFilled: finalAdminCoverageDebug.prefectureCellsFilled || 0,
|
||||
finalAdminPrefectureCellsAligned: finalAdminCoverageDebug.adminPrefectureCellsAligned || 0,
|
||||
municipalCoherence: municipalCoherence.debug,
|
||||
prefectureMetadataCoherence: prefectureCoherence.debug,
|
||||
continuityCellsRestored: fieldDebug.continuityCellsRestored || 0,
|
||||
continuityCellsRemapped: fieldDebug.continuityCellsRemapped || 0,
|
||||
adminSeamCellsResolved: fieldDebug.adminSeamCellsResolved || 0,
|
||||
prefectureSeamCellsResolved: fieldDebug.prefectureSeamCellsResolved || 0,
|
||||
...terrainSeamDebug,
|
||||
landUseCellsUpdated: fieldDebug.landUseCellsUpdated + landDebug.landUseCellsUpdated,
|
||||
displayMaskUpdated: maskDebug.displayMaskUpdated || 0,
|
||||
logisticsLabelsMigrated,
|
||||
candidateCacheHit: cacheHit,
|
||||
candidateCacheSize: cacheSize,
|
||||
...segmentDebug,
|
||||
candidateCompartmentSegmentsAdded,
|
||||
};
|
||||
|
|
@ -1757,7 +2224,8 @@ export function generatePatch(world, userRectInput, options = {}) {
|
|||
variant,
|
||||
candidateOriginX,
|
||||
candidateOriginY,
|
||||
patchGenerationMode: "legacy-full-pipeline",
|
||||
patchGenerationMode,
|
||||
patchTimings,
|
||||
updatedCells: fieldDebug.updatedCells,
|
||||
terrainCellsFullyReplaced: fieldDebug.terrainCellsFullyReplaced,
|
||||
coastCellsChanged: fieldDebug.coastCellsChanged + waterDebug.coastCellsChanged,
|
||||
|
|
@ -1771,6 +2239,7 @@ export function generatePatch(world, userRectInput, options = {}) {
|
|||
world.generatedRects = [...(world.generatedRects || []), record];
|
||||
world.invalidatedRects = [...(world.invalidatedRects || []), { ...rects.writeRect }];
|
||||
world.lastPatchResult = record;
|
||||
world.patchGenerationSerial = (world.patchGenerationSerial || 0) + 1;
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
|
|
@ -1789,7 +2258,8 @@ export function generatePatch(world, userRectInput, options = {}) {
|
|||
variant,
|
||||
candidateOriginX,
|
||||
candidateOriginY,
|
||||
patchGenerationMode: "legacy-full-pipeline",
|
||||
patchGenerationMode,
|
||||
patchTimings,
|
||||
updatedCells: record.updatedCells,
|
||||
terrainCellsFullyReplaced: record.terrainCellsFullyReplaced,
|
||||
coastCellsChanged: record.coastCellsChanged,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue