hmm?
This commit is contained in:
parent
ea0067dc0a
commit
112e6bf86b
11 changed files with 1586 additions and 1253 deletions
414
mapPatch.js
414
mapPatch.js
|
|
@ -1,4 +1,4 @@
|
|||
import { MAP_H, MAP_W, SIZE, MinHeap, clamp, hash2, lerp, smoothstep } from "./mapUtils.js";
|
||||
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";
|
||||
|
||||
|
|
@ -39,6 +39,11 @@ const DISCRETE_FIELD_NAMES = new Set([
|
|||
"adminId", "municipalityId", "prefectureRegionId", "regionId", "naturalCompartmentId", "watershedId",
|
||||
]);
|
||||
|
||||
|
||||
const ADMIN_CONTINUITY_FIELD_NAMES = new Set(["adminId", "municipalityId", "prefectureRegionId"]);
|
||||
const NATURAL_CONTINUITY_FIELD_NAMES = new Set(["regionId", "naturalCompartmentId", "watershedId"]);
|
||||
const CONTINUITY_FIELD_NAMES = new Set([...ADMIN_CONTINUITY_FIELD_NAMES, ...NATURAL_CONTINUITY_FIELD_NAMES]);
|
||||
|
||||
const SKIP_CELL_FIELDS = new Set(["flowTo"]);
|
||||
|
||||
function worldIndex(world, x, y) {
|
||||
|
|
@ -168,16 +173,20 @@ 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 + 96, Math.min(260, repairMargin + Math.max(MAP_W, MAP_H)));
|
||||
const transportReachRect = expandRect(coreRect, transportReachMargin, world);
|
||||
return {
|
||||
coreRect,
|
||||
writeRect,
|
||||
repairRect,
|
||||
contextRect: repairRect,
|
||||
transportReachRect,
|
||||
blendRect: coreRect,
|
||||
userRect: writeRect,
|
||||
selectedRect: coreRect,
|
||||
writeMargin,
|
||||
repairMargin,
|
||||
transportReachMargin,
|
||||
outerMargin: writeMargin,
|
||||
innerMargin: 0,
|
||||
};
|
||||
|
|
@ -188,8 +197,8 @@ function patchAlpha(x, y, rects, seed = 0) {
|
|||
if (!insideRect(x, y, writeRect)) return 0;
|
||||
const edge = distanceToRectEdge(x, y, writeRect);
|
||||
const margin = Math.max(1, rects.writeMargin || 1);
|
||||
const low = hash2(Math.floor(x / 18), Math.floor(y / 18), seed ^ 0x7153a9d1) - 0.5;
|
||||
const mid = hash2(Math.floor(x / 7), Math.floor(y / 7), seed ^ 0x9e3779b9) - 0.5;
|
||||
const low = valueNoise(x, y, seed ^ 0x7153a9d1, 18) - 0.5;
|
||||
const mid = valueNoise(x, y, seed ^ 0x9e3779b9, 7) - 0.5;
|
||||
const noisyEdge = edge + low * margin * 0.42 + mid * margin * 0.16;
|
||||
const base = smoothstep(clamp(noisyEdge / margin));
|
||||
// Keep the expanded repair band as the actual seam. The user's selected core
|
||||
|
|
@ -198,6 +207,19 @@ function patchAlpha(x, y, rects, seed = 0) {
|
|||
return clamp(base);
|
||||
}
|
||||
|
||||
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);
|
||||
if (NATURAL_CONTINUITY_FIELD_NAMES.has(name)) return clamp(0.68 + n * 0.16, 0.54, 0.82);
|
||||
return clamp(0.46 + n * 0.20, 0.28, 0.68);
|
||||
}
|
||||
|
||||
function continuitySegmentAllowed(x, y, nx, ny, rects, seed, minAlpha = 0.42) {
|
||||
if (!rects) return true;
|
||||
return Math.min(patchAlpha(x, y, rects, seed), patchAlpha(nx, ny, rects, seed)) >= minAlpha;
|
||||
}
|
||||
|
||||
|
||||
function sourceWindowForRects(rects) {
|
||||
const cx = (rects.coreRect.x0 + rects.coreRect.x1 - 1) / 2;
|
||||
const cy = (rects.coreRect.y0 + rects.coreRect.y1 - 1) / 2;
|
||||
|
|
@ -229,9 +251,188 @@ function fieldIdOffset(name, seed) {
|
|||
return base + ((seed >>> 0) % 997) * 10000;
|
||||
}
|
||||
|
||||
function offsetFieldValue(name, raw, seed) {
|
||||
if (!Number.isFinite(raw) || raw < 0) return raw;
|
||||
const offset = fieldIdOffset(name, seed);
|
||||
return offset ? raw + offset : raw;
|
||||
}
|
||||
|
||||
function isContinuityTransitionCell(x, y, rects, seed = 0, mode = "normal") {
|
||||
if (!insideRect(x, y, rects.writeRect)) return false;
|
||||
const edge = distanceToRectEdge(x, y, rects.writeRect);
|
||||
const margin = Math.max(2, rects.writeMargin || 1);
|
||||
const alpha = patchAlpha(x, y, rects, seed);
|
||||
const edgeLimit = mode === "prefecture" ? margin * 2.15 : mode === "admin" ? margin * 1.75 : margin * 1.45;
|
||||
const alphaLimit = mode === "prefecture" ? 0.995 : mode === "admin" ? 0.985 : 0.96;
|
||||
return edge <= edgeLimit || alpha < alphaLimit;
|
||||
}
|
||||
|
||||
function addCount(bucket, key, amount = 1) {
|
||||
if (!Number.isFinite(key) || key < 0) return;
|
||||
bucket.set(key, (bucket.get(key) || 0) + amount);
|
||||
}
|
||||
|
||||
function buildContinuityIdMappings(world, candidate, rects, window, oldFields, seed = 0) {
|
||||
const out = new Map();
|
||||
const debug = { continuityIdMappings: 0, continuityIdMappedCells: 0 };
|
||||
const dirs = [[0,0],[1,0],[-1,0],[0,1],[0,-1],[1,1],[1,-1],[-1,1],[-1,-1]];
|
||||
|
||||
for (const name of CONTINUITY_FIELD_NAMES) {
|
||||
const source = candidate?.[name];
|
||||
const old = oldFields?.get(name) || world.fields?.[name];
|
||||
if (!source || !old || !isCellField(source)) continue;
|
||||
const mode = name === "prefectureRegionId" ? "prefecture" : (name === "adminId" || name === "municipalityId") ? "admin" : "natural";
|
||||
const contacts = new Map();
|
||||
|
||||
for (let y = rects.writeRect.y0; y < rects.writeRect.y1; y++) {
|
||||
for (let x = rects.writeRect.x0; x < rects.writeRect.x1; x++) {
|
||||
if (!isContinuityTransitionCell(x, y, rects, seed, mode)) continue;
|
||||
const s = sourceCoordForWorld(window, x, y);
|
||||
const si = sourceIndex(s.x, s.y);
|
||||
if (si < 0) continue;
|
||||
const from = offsetFieldValue(name, source[si], seed);
|
||||
if (!Number.isFinite(from) || from < 0) continue;
|
||||
const bucket = contacts.get(from) || new Map();
|
||||
for (const [dx, dy] of dirs) {
|
||||
const nx = x + dx, ny = y + dy;
|
||||
const wi = worldIndex(world, nx, ny);
|
||||
if (wi < 0 || old[wi] < 0) continue;
|
||||
const outsideWrite = !insideRect(nx, ny, rects.writeRect);
|
||||
const weakPatch = insideRect(nx, ny, rects.writeRect) && patchAlpha(nx, ny, rects, seed) < (mode === "prefecture" ? 0.96 : 0.88);
|
||||
const edgeWeight = outsideWrite ? 6 : weakPatch ? 3 : (dx || dy ? 1 : 2);
|
||||
addCount(bucket, old[wi], edgeWeight);
|
||||
}
|
||||
contacts.set(from, bucket);
|
||||
}
|
||||
}
|
||||
|
||||
const mapping = new Map();
|
||||
for (const [from, bucket] of contacts) {
|
||||
let total = 0;
|
||||
let best = -1;
|
||||
let bestCount = 0;
|
||||
for (const [to, count] of bucket) {
|
||||
total += count;
|
||||
if (count > bestCount) { best = to; bestCount = count; }
|
||||
}
|
||||
const minCount = mode === "prefecture" ? 10 : mode === "admin" ? 8 : 5;
|
||||
const minShare = mode === "prefecture" ? 0.42 : mode === "admin" ? 0.48 : 0.36;
|
||||
if (best >= 0 && bestCount >= minCount && bestCount / Math.max(1, total) >= minShare) {
|
||||
mapping.set(from, best);
|
||||
}
|
||||
}
|
||||
if (mapping.size) {
|
||||
out.set(name, mapping);
|
||||
debug.continuityIdMappings += mapping.size;
|
||||
}
|
||||
}
|
||||
out.debug = debug;
|
||||
return out;
|
||||
}
|
||||
|
||||
function applyContinuityMapping(idMappings, name, value) {
|
||||
const map = idMappings?.get?.(name);
|
||||
return map && map.has(value) ? map.get(value) : value;
|
||||
}
|
||||
|
||||
function pointCandidateContinuityIds(p, key, seed = 0) {
|
||||
const ids = [];
|
||||
if (!p) return ids;
|
||||
if (key === "adminCenters") {
|
||||
for (const raw of [p.id, p.adminId, p.adminNumericId, p.municipalityId]) {
|
||||
if (Number.isFinite(raw) && raw >= 0) ids.push(offsetFieldValue("adminId", raw, seed));
|
||||
}
|
||||
} else if (key === "prefectureRegions") {
|
||||
for (const raw of [p.id, p.prefectureRegionId]) {
|
||||
if (Number.isFinite(raw) && raw >= 0) ids.push(offsetFieldValue("prefectureRegionId", raw, seed));
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
function cloneContinuityFields(world) {
|
||||
const out = new Map();
|
||||
for (const name of CONTINUITY_FIELD_NAMES) {
|
||||
const field = world?.fields?.[name];
|
||||
if (ArrayBuffer.isView(field)) out.set(name, new field.constructor(field));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function stabilizeContinuitySeam(world, rects, oldFields, seed = 0) {
|
||||
let restored = 0;
|
||||
let remapped = 0;
|
||||
const margin = Math.max(2, rects.writeMargin || 1);
|
||||
for (const name of CONTINUITY_FIELD_NAMES) {
|
||||
const field = world.fields?.[name];
|
||||
const old = oldFields?.get(name);
|
||||
if (!field || !old) continue;
|
||||
const isPrefecture = name === "prefectureRegionId";
|
||||
const isAdmin = name === "adminId" || name === "municipalityId";
|
||||
const preserveAlpha = isPrefecture ? 0.94 : isAdmin ? 0.90 : 0.74;
|
||||
const preserveEdge = isPrefecture ? margin * 1.25 : isAdmin ? margin : margin * 0.72;
|
||||
|
||||
// First preserve the old IDs in the transition band. This prevents the
|
||||
// writeRect edge from becoming a prefecture/municipal border.
|
||||
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 || old[i] < 0) continue;
|
||||
const edge = distanceToRectEdge(x, y, rects.writeRect);
|
||||
const a = patchAlpha(x, y, rects, seed);
|
||||
if (edge <= preserveEdge || a < preserveAlpha) {
|
||||
if (field[i] !== old[i]) { field[i] = old[i]; restored++; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Then map candidate IDs that contact an outside ID back to that outside ID.
|
||||
// This lets prefectures/municipalities cross the generated-area seam instead
|
||||
// of creating a new border exactly on the seam.
|
||||
const contacts = new Map();
|
||||
const dirs = [[1,0],[-1,0],[0,1],[0,-1]];
|
||||
for (let y = rects.writeRect.y0 + 1; y < rects.writeRect.y1 - 1; y++) {
|
||||
for (let x = rects.writeRect.x0 + 1; x < rects.writeRect.x1 - 1; x++) {
|
||||
const i = worldIndex(world, x, y);
|
||||
if (i < 0 || field[i] < 0 || old[i] === field[i]) continue;
|
||||
const a = patchAlpha(x, y, rects, seed);
|
||||
if (a < 0.98 && !isPrefecture) continue;
|
||||
for (const [dx, dy] of dirs) {
|
||||
const ni = worldIndex(world, x + dx, y + dy);
|
||||
if (ni < 0 || old[ni] < 0 || old[ni] === field[i]) continue;
|
||||
if (field[ni] === old[ni] || patchAlpha(x + dx, y + dy, rects, seed) < preserveAlpha) {
|
||||
const key = field[i];
|
||||
const bucket = contacts.get(key) || new Map();
|
||||
bucket.set(old[ni], (bucket.get(old[ni]) || 0) + 1);
|
||||
contacts.set(key, bucket);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const mapping = new Map();
|
||||
for (const [from, bucket] of contacts) {
|
||||
let best = -1, bestCount = 0;
|
||||
for (const [to, count] of bucket) if (count > bestCount) { best = to; bestCount = count; }
|
||||
if (best >= 0 && bestCount >= (isPrefecture ? 2 : 3)) mapping.set(from, best);
|
||||
}
|
||||
if (mapping.size) {
|
||||
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 && mapping.has(field[i])) { field[i] = mapping.get(field[i]); remapped++; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return { continuityCellsRestored: restored, continuityCellsRemapped: remapped };
|
||||
}
|
||||
|
||||
function copyFullPipelineFields(world, candidate, rects, seed) {
|
||||
const window = sourceWindowForRects(rects);
|
||||
const oldSea = world.fields.sea ? new Uint8Array(world.fields.sea) : null;
|
||||
const oldContinuityFields = cloneContinuityFields(world);
|
||||
const continuityIdMappings = buildContinuityIdMappings(world, candidate, rects, window, oldContinuityFields, seed);
|
||||
let continuityIdMappedCells = 0;
|
||||
let updatedCells = 0;
|
||||
let coastCellsChanged = 0;
|
||||
let terrainCellsFullyReplaced = 0;
|
||||
|
|
@ -258,11 +459,15 @@ function copyFullPipelineFields(world, candidate, rects, seed) {
|
|||
if (alpha <= 0.005) continue;
|
||||
|
||||
if (isDiscrete) {
|
||||
const thresholdNoise = hash2(Math.floor(x / 6), Math.floor(y / 6), seed ^ 0x21f0aaad) - 0.5;
|
||||
const threshold = clamp(0.46 + thresholdNoise * 0.20, 0.28, 0.68);
|
||||
const threshold = continuityReplaceThreshold(name, x, y, rects, seed);
|
||||
if (alpha >= threshold) {
|
||||
const raw = source[si];
|
||||
const value = idOffset && raw >= 0 ? raw + idOffset : raw;
|
||||
let value = idOffset && raw >= 0 ? raw + idOffset : raw;
|
||||
if (CONTINUITY_FIELD_NAMES.has(name)) {
|
||||
const mapped = applyContinuityMapping(continuityIdMappings, name, value);
|
||||
if (mapped !== value) continuityIdMappedCells++;
|
||||
value = mapped;
|
||||
}
|
||||
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++;
|
||||
|
|
@ -282,6 +487,27 @@ function copyFullPipelineFields(world, candidate, rects, seed) {
|
|||
}
|
||||
}
|
||||
|
||||
// The legacy full pipeline uses `adminId` as the municipality raster and
|
||||
// assigns municipality metadata on `adminCenters`; it does not expose a
|
||||
// separate municipalityId cell field. If an old experimental field exists,
|
||||
// keep it synchronized with the canonical legacy adminId instead of leaving
|
||||
// stale numeric/one-municipality data in regenerated patches.
|
||||
if (world.fields.adminId && !candidate?.municipalityId) {
|
||||
const expected = world.width * world.height;
|
||||
if (!world.fields.municipalityId || world.fields.municipalityId.length !== expected) {
|
||||
world.fields.municipalityId = new Int32Array(expected);
|
||||
world.fields.municipalityId.fill(-1);
|
||||
}
|
||||
const municipalityId = world.fields.municipalityId;
|
||||
const adminId = world.fields.adminId;
|
||||
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);
|
||||
if (wi >= 0) municipalityId[wi] = adminId[wi];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Keep water fields coherent after all continuous fields have been blended.
|
||||
const sea = world.fields.sea;
|
||||
const ocean = world.fields.ocean;
|
||||
|
|
@ -304,7 +530,21 @@ function copyFullPipelineFields(world, candidate, rects, seed) {
|
|||
}
|
||||
}
|
||||
|
||||
return { window, updatedCells, terrainCellsFullyReplaced, coastCellsChanged, naturalRegionsUpdated, adminCellsReassigned, landUseCellsUpdated };
|
||||
const continuityDebug = stabilizeContinuitySeam(world, rects, oldContinuityFields, seed);
|
||||
|
||||
return {
|
||||
window,
|
||||
idMappings: continuityIdMappings,
|
||||
updatedCells,
|
||||
terrainCellsFullyReplaced,
|
||||
coastCellsChanged,
|
||||
naturalRegionsUpdated,
|
||||
adminCellsReassigned,
|
||||
landUseCellsUpdated,
|
||||
continuityIdMappedCells,
|
||||
continuityIdMappings: continuityIdMappings.debug?.continuityIdMappings || 0,
|
||||
...continuityDebug,
|
||||
};
|
||||
}
|
||||
|
||||
function smoothWaterTopology(world, rect, seaLevel = 0.30) {
|
||||
|
|
@ -454,6 +694,8 @@ function transformCandidatePoint(world, window, p, key, seed = 0) {
|
|||
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 offset = fieldIdOffset("prefectureRegionId", seed);
|
||||
|
|
@ -619,40 +861,58 @@ function simplifyPath(path, keepEvery = 2) {
|
|||
return out;
|
||||
}
|
||||
|
||||
function collectInternalNetworkPoints(world, sourceMap, keys, rect) {
|
||||
function collectInternalNetworkPoints(world, sourceMap, keys, rect, mode = "road") {
|
||||
const points = [];
|
||||
const seen = new Set();
|
||||
const add = (x, y, key, weight = 1) => {
|
||||
x = Math.round(x); y = Math.round(y);
|
||||
if (!insideRect(x, y, rect) || !isLand(world, x, y)) return;
|
||||
const sig = `${x},${y},${key}`;
|
||||
if (seen.has(sig)) return;
|
||||
seen.add(sig);
|
||||
points.push({ x, y, key, weight });
|
||||
};
|
||||
for (const key of keys) {
|
||||
for (const path of sourceMap[key] || []) {
|
||||
for (let i = 0; i < path.length; i += 6) {
|
||||
const x = Math.round(tupleWorldX(world, path[i]));
|
||||
const y = Math.round(tupleWorldY(world, path[i]));
|
||||
if (insideRect(x, y, rect) && isLand(world, x, y)) points.push({ x, y, key });
|
||||
for (let i = 0; i < path.length; i += 4) {
|
||||
add(tupleWorldX(world, path[i]), tupleWorldY(world, path[i]), key, 1.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
const featureKeys = mode === "rail"
|
||||
? ["modernCities", "stations", "ports", "adminCenters", "industrialZones", "newTowns"]
|
||||
: ["modernCities", "ports", "markets", "villages", "adminCenters", "industrialZones", "logisticsParks", "newTowns"];
|
||||
for (const key of featureKeys) {
|
||||
for (const p of sourceMap[key] || []) {
|
||||
add(pointWorldX(world, p), pointWorldY(world, p), key, key === "adminCenters" || key === "modernCities" ? 1.8 : 1.25);
|
||||
}
|
||||
}
|
||||
return points;
|
||||
}
|
||||
|
||||
function connectAnchors(world, sourceMap, anchors, mode, rect) {
|
||||
const keys = mode === "rail" ? ["railways", "branchRailways"] : ["nationalRoads", "minorRoads", "premodernRoads"];
|
||||
const targets = collectInternalNetworkPoints(world, sourceMap, keys, rect);
|
||||
const targets = collectInternalNetworkPoints(world, sourceMap, keys, rect, mode);
|
||||
if (!targets.length) return { connectors: 0, disconnected: anchors.length };
|
||||
let connectors = 0;
|
||||
let disconnected = 0;
|
||||
const layer = mode === "rail" ? "branchRailways" : "minorRoads";
|
||||
sourceMap[layer] ||= [];
|
||||
const seen = new Set();
|
||||
const maxRange = mode === "rail" ? 260 : 300;
|
||||
for (const raw of anchors) {
|
||||
const anchorLand = nearestLand(world, raw.x, raw.y, rect, 12);
|
||||
const anchorLand = nearestLand(world, raw.x, raw.y, rect, 18);
|
||||
if (!anchorLand) { disconnected++; continue; }
|
||||
const target = targets
|
||||
.filter((p) => Math.hypot(p.x - anchorLand.x, p.y - anchorLand.y) <= (mode === "rail" ? 80 : 64))
|
||||
.sort((a, b) => Math.hypot(a.x - anchorLand.x, a.y - anchorLand.y) - Math.hypot(b.x - anchorLand.x, b.y - anchorLand.y))[0];
|
||||
.map((p) => ({ ...p, d: Math.hypot(p.x - anchorLand.x, p.y - anchorLand.y) }))
|
||||
.filter((p) => p.d <= maxRange)
|
||||
.sort((a, b) => (a.d / Math.max(0.6, a.weight || 1)) - (b.d / Math.max(0.6, b.weight || 1)))[0];
|
||||
if (!target) { disconnected++; continue; }
|
||||
const sig = `${anchorLand.x},${anchorLand.y}:${target.x},${target.y}:${mode}`;
|
||||
if (seen.has(sig)) continue;
|
||||
seen.add(sig);
|
||||
const path = localPathfind(world, anchorLand, target, rect, mode);
|
||||
const searchRect = expandRect(rect, 8, world);
|
||||
const path = localPathfind(world, anchorLand, target, searchRect, mode, 42000);
|
||||
if (!path || path.length < 2) { disconnected++; continue; }
|
||||
sourceMap[layer].push(sourcePathFromWorld(world, simplifyPath(path, mode === "rail" ? 3 : 2)));
|
||||
connectors++;
|
||||
|
|
@ -660,7 +920,48 @@ function connectAnchors(world, sourceMap, anchors, mode, rect) {
|
|||
return { connectors, disconnected };
|
||||
}
|
||||
|
||||
function mergePointLayers(world, sourceMap, candidate, rects, window, seed) {
|
||||
function nearestNetworkPoint(world, sourceMap, keys, point, rect, maxDistance = 80) {
|
||||
let best = null;
|
||||
let bestD = maxDistance;
|
||||
for (const key of keys) {
|
||||
for (const path of sourceMap[key] || []) {
|
||||
for (let i = 0; i < path.length; i += 5) {
|
||||
const x = Math.round(tupleWorldX(world, path[i]));
|
||||
const y = Math.round(tupleWorldY(world, path[i]));
|
||||
if (!insideRect(x, y, rect) || !isLand(world, x, y)) continue;
|
||||
const d = Math.hypot(point.x - x, point.y - y);
|
||||
if (d < bestD) { bestD = d; best = { x, y, key }; }
|
||||
}
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
function ensureSettlementRoadCoverage(world, sourceMap, rect) {
|
||||
const keys = ["nationalRoads", "minorRoads", "premodernRoads"];
|
||||
const featureKeys = ["modernCities", "ports", "markets", "adminCenters", "villages"];
|
||||
sourceMap.minorRoads ||= [];
|
||||
let connectors = 0;
|
||||
const seen = new Set();
|
||||
for (const key of featureKeys) {
|
||||
for (const p of sourceMap[key] || []) {
|
||||
const start = nearestLand(world, pointWorldX(world, p), pointWorldY(world, p), rect, 10);
|
||||
if (!start || !insideRect(start.x, start.y, rect)) continue;
|
||||
const target = nearestNetworkPoint(world, sourceMap, keys, start, rect, key === "villages" ? 36 : 58);
|
||||
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", 28000);
|
||||
if (!path || path.length < 2) continue;
|
||||
sourceMap.minorRoads.push(sourcePathFromWorld(world, simplifyPath(path, 2)));
|
||||
connectors++;
|
||||
}
|
||||
}
|
||||
return connectors;
|
||||
}
|
||||
|
||||
function mergePointLayers(world, sourceMap, candidate, rects, window, seed, idMappings = null) {
|
||||
let preservedExternalEntities = 0;
|
||||
let regeneratedInternalEntities = 0;
|
||||
let invalidPortsRemoved = 0;
|
||||
|
|
@ -681,6 +982,13 @@ function mergePointLayers(world, sourceMap, candidate, rects, window, seed) {
|
|||
}
|
||||
const generated = [];
|
||||
for (const p of candidate[key] || []) {
|
||||
const candidateIds = pointCandidateContinuityIds(p, key, seed);
|
||||
if (key === "adminCenters" && candidateIds.some((id) => applyContinuityMapping(idMappings, "adminId", id) !== id || applyContinuityMapping(idMappings, "municipalityId", id) !== id)) {
|
||||
continue;
|
||||
}
|
||||
if (key === "prefectureRegions" && candidateIds.some((id) => applyContinuityMapping(idMappings, "prefectureRegionId", id) !== id)) {
|
||||
continue;
|
||||
}
|
||||
const q = transformCandidatePoint(world, window, p, key, seed);
|
||||
if (!q) continue;
|
||||
const wx = Math.round(pointWorldX(world, q));
|
||||
|
|
@ -722,13 +1030,15 @@ function mergePathLayers(world, sourceMap, candidate, rects, window, seed) {
|
|||
}
|
||||
sourceMap[key] = next;
|
||||
}
|
||||
const roadConn = connectAnchors(world, sourceMap, roadAnchors, "road", rects.writeRect);
|
||||
const railConn = connectAnchors(world, sourceMap, railAnchors, "rail", rects.writeRect);
|
||||
const transportRect = rects.transportReachRect || rects.repairRect || rects.writeRect;
|
||||
const roadConn = connectAnchors(world, sourceMap, roadAnchors, "road", transportRect);
|
||||
const railConn = connectAnchors(world, sourceMap, railAnchors, "rail", transportRect);
|
||||
const settlementRoadConnectors = ensureSettlementRoadCoverage(world, sourceMap, transportRect);
|
||||
return {
|
||||
roadsClipped,
|
||||
railsClipped,
|
||||
regeneratedPaths,
|
||||
roadConnectorsCreated: roadConn.connectors,
|
||||
roadConnectorsCreated: roadConn.connectors + settlementRoadConnectors,
|
||||
railwayConnectorsCreated: railConn.connectors,
|
||||
disconnectedRoadComponents: roadConn.disconnected,
|
||||
disconnectedRailComponents: railConn.disconnected,
|
||||
|
|
@ -741,9 +1051,12 @@ function segmentTouchesRect(world, seg, rect) {
|
|||
|| insideRect(tupleWorldX(world, seg[1]), tupleWorldY(world, seg[1]), rect);
|
||||
}
|
||||
|
||||
function buildBoundarySegmentsFromField(world, fieldName, rect) {
|
||||
function buildBoundarySegmentsFromField(world, fieldName, rect, options = {}) {
|
||||
const field = world.fields[fieldName];
|
||||
const sea = world.fields.sea;
|
||||
const rects = options.rects || null;
|
||||
const seed = options.seed || 0;
|
||||
const minAlpha = Number.isFinite(options.minAlpha) ? options.minAlpha : 0;
|
||||
if (!field) return [];
|
||||
const out = [];
|
||||
for (let y = rect.y0; y < rect.y1; y++) {
|
||||
|
|
@ -753,11 +1066,11 @@ function buildBoundarySegmentsFromField(world, fieldName, rect) {
|
|||
const id = field[i];
|
||||
if (id < 0) continue;
|
||||
const right = worldIndex(world, x + 1, y);
|
||||
if (x + 1 < rect.x1 && right >= 0 && !sea?.[right] && field[right] >= 0 && field[right] !== id) {
|
||||
if (x + 1 < rect.x1 && right >= 0 && !sea?.[right] && field[right] >= 0 && field[right] !== id && continuitySegmentAllowed(x, y, x + 1, y, rects, seed, minAlpha)) {
|
||||
out.push([[x + 0.5 - world.originX, y - world.originY], [x + 0.5 - world.originX, y + 1 - world.originY]]);
|
||||
}
|
||||
const down = worldIndex(world, x, y + 1);
|
||||
if (y + 1 < rect.y1 && down >= 0 && !sea?.[down] && field[down] >= 0 && field[down] !== id) {
|
||||
if (y + 1 < rect.y1 && down >= 0 && !sea?.[down] && field[down] >= 0 && field[down] !== id && continuitySegmentAllowed(x, y, x, y + 1, rects, seed, minAlpha)) {
|
||||
out.push([[x - world.originX, y + 0.5 - world.originY], [x + 1 - world.originX, y + 0.5 - world.originY]]);
|
||||
}
|
||||
}
|
||||
|
|
@ -765,15 +1078,44 @@ function buildBoundarySegmentsFromField(world, fieldName, rect) {
|
|||
return out;
|
||||
}
|
||||
|
||||
function mergeSegmentLayers(world, sourceMap, rects) {
|
||||
function mergeCandidateCompartmentDebugSegments(world, sourceMap, candidate, rects, window, seed = 0) {
|
||||
const debug = sourceMap.adminDebug || {};
|
||||
debug.compartmentBorders ||= [];
|
||||
let added = 0;
|
||||
for (const seg of candidate?.adminDebug?.compartmentBorders || []) {
|
||||
if (!Array.isArray(seg) || seg.length < 2) continue;
|
||||
const a = worldCoordForSource(window, seg[0]?.[0], seg[0]?.[1]);
|
||||
const b = worldCoordForSource(window, seg[1]?.[0], seg[1]?.[1]);
|
||||
const mx = (a.x + b.x) * 0.5;
|
||||
const my = (a.y + b.y) * 0.5;
|
||||
if (!insideRect(mx, my, rects.writeRect) || patchAlpha(mx, my, rects, seed) < 0.38) continue;
|
||||
debug.compartmentBorders.push(sourcePathFromWorld(world, [[a.x, a.y], [b.x, b.y]]));
|
||||
added++;
|
||||
}
|
||||
sourceMap.adminDebug = debug;
|
||||
return added;
|
||||
}
|
||||
|
||||
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.adminBorders ||= [];
|
||||
sourceMap.adminBorders.push(...buildBoundarySegmentsFromField(world, "adminId", rects.writeRect));
|
||||
sourceMap.adminBorders.push(...buildBoundarySegmentsFromField(world, "adminId", rects.writeRect, { rects, seed, minAlpha: 0.58 }));
|
||||
sourceMap.regionalPrefectureBorders ||= [];
|
||||
sourceMap.regionalPrefectureBorders.push(...buildBoundarySegmentsFromField(world, "prefectureRegionId", rects.writeRect));
|
||||
sourceMap.regionalPrefectureBorders.push(...buildBoundarySegmentsFromField(world, "prefectureRegionId", rects.writeRect, { rects, seed, minAlpha: 0.72 }));
|
||||
sourceMap.prefectureBorder ||= [];
|
||||
|
||||
const debug = sourceMap.adminDebug || {};
|
||||
debug.compartmentBorders = (debug.compartmentBorders || []).filter((seg) => !segmentTouchesRect(world, seg, rects.writeRect));
|
||||
debug.compartmentBorders.push(...buildBoundarySegmentsFromField(world, "naturalCompartmentId", rects.writeRect, { rects, seed, minAlpha: 0.40 }));
|
||||
sourceMap.adminDebug = debug;
|
||||
return {
|
||||
adminBordersRebuilt: sourceMap.adminBorders.length,
|
||||
prefectureBordersRebuilt: sourceMap.regionalPrefectureBorders.length,
|
||||
compartmentBordersRebuilt: debug.compartmentBorders.length,
|
||||
};
|
||||
}
|
||||
|
||||
function repairLanduseAndPopulation(world, rects) {
|
||||
|
|
@ -829,16 +1171,18 @@ export function generatePatch(world, userRectInput, options = {}) {
|
|||
const rects = buildPatchRects(validation.rect, world);
|
||||
const terrainType = options.terrainType || "auto";
|
||||
const seed = Number.isFinite(options.seed) ? options.seed >>> 0 : ((world?.seed || 0) + 1013904223) >>> 0;
|
||||
const candidate = generateMap(seed, { terrainType, onProgress: () => {} });
|
||||
const variant = Number.isFinite(options.variant) ? Math.max(0, Math.floor(options.variant)) >>> 0 : 0;
|
||||
const candidate = generateMap(seed, { terrainType, legacyTerrain: true, onProgress: () => {} });
|
||||
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);
|
||||
recomputeSlopeAndWaterDependentFields(world, rects.repairRect, candidate.seaLevel || world.sourceMap?.seaLevel || 0.30);
|
||||
const pointDebug = mergePointLayers(world, sourceMap, candidate, rects, fieldDebug.window, seed);
|
||||
const pointDebug = mergePointLayers(world, sourceMap, candidate, rects, fieldDebug.window, seed, fieldDebug.idMappings);
|
||||
const pathDebug = mergePathLayers(world, sourceMap, candidate, rects, fieldDebug.window, seed);
|
||||
mergeSegmentLayers(world, sourceMap, rects);
|
||||
const segmentDebug = mergeSegmentLayers(world, sourceMap, rects, seed);
|
||||
const candidateCompartmentSegmentsAdded = mergeCandidateCompartmentDebugSegments(world, sourceMap, candidate, rects, fieldDebug.window, seed);
|
||||
const landDebug = repairLanduseAndPopulation(world, rects);
|
||||
sanitizeExistingLogistics(sourceMap);
|
||||
|
||||
|
|
@ -853,8 +1197,14 @@ export function generatePatch(world, userRectInput, options = {}) {
|
|||
...pointDebug,
|
||||
...pathDebug,
|
||||
adminCellsReassigned: fieldDebug.adminCellsReassigned,
|
||||
continuityCellsRestored: fieldDebug.continuityCellsRestored || 0,
|
||||
continuityCellsRemapped: fieldDebug.continuityCellsRemapped || 0,
|
||||
continuityIdMappings: fieldDebug.continuityIdMappings || 0,
|
||||
continuityIdMappedCells: fieldDebug.continuityIdMappedCells || 0,
|
||||
landUseCellsUpdated: fieldDebug.landUseCellsUpdated + landDebug.landUseCellsUpdated,
|
||||
logisticsLabelsMigrated,
|
||||
...segmentDebug,
|
||||
candidateCompartmentSegmentsAdded,
|
||||
};
|
||||
|
||||
const record = {
|
||||
|
|
@ -867,6 +1217,8 @@ export function generatePatch(world, userRectInput, options = {}) {
|
|||
terrainType: id,
|
||||
label,
|
||||
seed,
|
||||
variant,
|
||||
patchGenerationMode: "legacy-full-pipeline",
|
||||
updatedCells: fieldDebug.updatedCells,
|
||||
terrainCellsFullyReplaced: fieldDebug.terrainCellsFullyReplaced,
|
||||
coastCellsChanged: fieldDebug.coastCellsChanged + waterDebug.coastCellsChanged,
|
||||
|
|
@ -887,6 +1239,8 @@ export function generatePatch(world, userRectInput, options = {}) {
|
|||
terrainType: id,
|
||||
label,
|
||||
seed,
|
||||
variant,
|
||||
patchGenerationMode: "legacy-full-pipeline",
|
||||
updatedCells: record.updatedCells,
|
||||
terrainCellsFullyReplaced: record.terrainCellsFullyReplaced,
|
||||
coastCellsChanged: record.coastCellsChanged,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue