not baad
This commit is contained in:
parent
4a8aff90b2
commit
6cef9a3abe
11 changed files with 1184 additions and 235 deletions
299
app.js
299
app.js
|
|
@ -35,6 +35,7 @@ const state = {
|
|||
patchVariant: 0,
|
||||
zoom: 1,
|
||||
lastPatchResult: null,
|
||||
pendingPatch: null,
|
||||
};
|
||||
|
||||
const canvas = document.getElementById("mapCanvas");
|
||||
|
|
@ -62,6 +63,9 @@ let generationCurrentStage = "";
|
|||
let generationTimer = null;
|
||||
let zoomRedrawRaf = null;
|
||||
let zoomSettledTimer = null;
|
||||
let zoomVisualState = null;
|
||||
let patchWorker = null;
|
||||
let patchJobSeq = 0;
|
||||
|
||||
const dragState = {
|
||||
mode: null,
|
||||
|
|
@ -81,8 +85,16 @@ const dragState = {
|
|||
panRaf: null,
|
||||
};
|
||||
|
||||
function displayWorld() {
|
||||
return state.pendingPatch?.world || state.world;
|
||||
}
|
||||
|
||||
function displaySourceMap() {
|
||||
return displayWorld()?.sourceMap || state.map;
|
||||
}
|
||||
|
||||
function activeMap() {
|
||||
return state.viewportMap || state.map;
|
||||
return state.viewportMap || displaySourceMap();
|
||||
}
|
||||
|
||||
function clampZoom(value) {
|
||||
|
|
@ -99,8 +111,8 @@ function viewportSizeForZoom(zoom = state.zoom) {
|
|||
};
|
||||
}
|
||||
|
||||
function clampCameraForView(camera, size = viewportSizeForZoom(state.zoom)) {
|
||||
return clampCameraToWorld(camera, state.world, size?.width || MAP_W, size?.height || MAP_H);
|
||||
function clampCameraForView(camera, size = viewportSizeForZoom(state.zoom), world = displayWorld()) {
|
||||
return clampCameraToWorld(camera, world, size?.width || MAP_W, size?.height || MAP_H);
|
||||
}
|
||||
|
||||
function syncViewportSize() {
|
||||
|
|
@ -110,8 +122,12 @@ function syncViewportSize() {
|
|||
return size;
|
||||
}
|
||||
|
||||
function canvasInteractionRect() {
|
||||
return zoomVisualState?.baseRect || canvas.getBoundingClientRect();
|
||||
}
|
||||
|
||||
function mapCellScreenSize(map = activeMap()) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const rect = canvasInteractionRect();
|
||||
const mapWidth = Math.max(1, map?.width || state.viewWidth || MAP_W);
|
||||
return rect.width ? rect.width / mapWidth : CELL_SIZE * clampZoom(state.zoom || 1);
|
||||
}
|
||||
|
|
@ -131,7 +147,7 @@ function displayedCellSize() {
|
|||
}
|
||||
|
||||
function screenPointToMapPixel(clientX, clientY, sizeOverride = null) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const rect = canvasInteractionRect();
|
||||
if (!rect.width || !rect.height) return null;
|
||||
const map = activeMap();
|
||||
const viewWidth = Math.max(1, sizeOverride?.width || map?.width || state.viewWidth || MAP_W);
|
||||
|
|
@ -144,7 +160,7 @@ function screenPointToMapPixel(clientX, clientY, sizeOverride = null) {
|
|||
}
|
||||
|
||||
function mapPixelToScreenPoint(px, py) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const rect = canvasInteractionRect();
|
||||
const map = activeMap();
|
||||
const viewWidth = Math.max(1, map?.width || state.viewWidth || MAP_W);
|
||||
const viewHeight = Math.max(1, map?.height || state.viewHeight || MAP_H);
|
||||
|
|
@ -176,7 +192,7 @@ function viewportCellToWorldCell(cell) {
|
|||
}
|
||||
|
||||
function clampCanvasPoint(event) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const rect = canvasInteractionRect();
|
||||
return {
|
||||
x: Math.min(Math.max(event.clientX - rect.left, 0), rect.width),
|
||||
y: Math.min(Math.max(event.clientY - rect.top, 0), rect.height),
|
||||
|
|
@ -186,7 +202,7 @@ function clampCanvasPoint(event) {
|
|||
function screenPointToWorldCell(point) {
|
||||
const map = activeMap();
|
||||
if (!map || !point) return null;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const rect = canvasInteractionRect();
|
||||
if (!rect.width || !rect.height) return null;
|
||||
const viewWidth = Math.max(1, map.width || state.viewWidth || MAP_W);
|
||||
const viewHeight = Math.max(1, map.height || state.viewHeight || MAP_H);
|
||||
|
|
@ -387,8 +403,9 @@ function updatePatchControls() {
|
|||
return;
|
||||
}
|
||||
const rects = buildPatchRects(validation.rect, state.world);
|
||||
const patchText = state.lastPatchResult
|
||||
? ` Last patch: ${state.lastPatchResult.label}, variant ${state.lastPatchResult.variant ?? "-"}, mode ${state.lastPatchResult.patchGenerationMode || "legacy-full-pipeline"}, terrain ${state.lastPatchResult.updatedCells.toLocaleString()} cells, coast ${state.lastPatchResult.coastCellsChanged || 0}, natural ${state.lastPatchResult.naturalRegionsUpdated || 0}${state.lastPatchResult.humanGeography?.ok ? `, connectors ${(state.lastPatchResult.humanGeography.roadConnectorsCreated || 0) + (state.lastPatchResult.humanGeography.railwayConnectorsCreated || 0)}, admin ${state.lastPatchResult.humanGeography.adminCellsReassigned || 0}, invalid ports ${state.lastPatchResult.humanGeography.invalidPortsRemoved || 0}` : ""}.`
|
||||
const shownPatch = state.pendingPatch?.result || state.lastPatchResult;
|
||||
const patchText = shownPatch
|
||||
? ` ${state.pendingPatch ? "Preview" : "Last applied"}: ${shownPatch.label}, variant ${shownPatch.variant ?? "-"}, mode ${shownPatch.patchGenerationMode || "legacy-full-pipeline"}, terrain ${shownPatch.updatedCells.toLocaleString()} cells, coast ${shownPatch.coastCellsChanged || 0}, natural ${shownPatch.naturalRegionsUpdated || 0}${shownPatch.humanGeography?.ok ? `, connectors ${(shownPatch.humanGeography.roadConnectorsCreated || 0) + (shownPatch.humanGeography.railwayConnectorsCreated || 0)}, admin ${shownPatch.humanGeography.adminCellsReassigned || 0}, invalid ports ${shownPatch.humanGeography.invalidPortsRemoved || 0}` : ""}.${state.pendingPatch ? " Click the map without dragging to apply; generate Alternative to replace the preview." : ""}`
|
||||
: "";
|
||||
patchStatusEl.textContent = `Variant: ${variant}. Core: ${formatRectSize(rects.coreRect)}. Write: ${formatRectSize(rects.writeRect)}. Context: ${formatRectSize(rects.contextRect)}. Repair: ${formatRectSize(rects.repairRect)}.${patchText}`;
|
||||
patchStatusEl.classList.toggle("invalid", false);
|
||||
|
|
@ -424,7 +441,36 @@ function schedulePanRedraw(camera) {
|
|||
});
|
||||
}
|
||||
|
||||
function hideSelectionOverlay() {
|
||||
function commitPendingPatch({ redrawAfter = true } = {}) {
|
||||
if (!state.pendingPatch?.world) return false;
|
||||
state.world = state.pendingPatch.world;
|
||||
state.map = state.world.sourceMap || state.map;
|
||||
state.lastPatchResult = state.pendingPatch.result || state.world.lastPatchResult || state.lastPatchResult;
|
||||
state.pendingPatch = null;
|
||||
state.viewportMap = null;
|
||||
if (redrawAfter) {
|
||||
renderStats(displaySourceMap());
|
||||
redraw({ fastTerrain: true, allowWorldExpand: false });
|
||||
window.setTimeout(() => redraw({ fastTerrain: false, allowWorldExpand: false }), 80);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function discardPendingPatch({ redrawAfter = true } = {}) {
|
||||
if (!state.pendingPatch) return false;
|
||||
state.pendingPatch = null;
|
||||
state.viewportMap = null;
|
||||
if (redrawAfter) {
|
||||
renderStats(displaySourceMap());
|
||||
redraw({ fastTerrain: false, allowWorldExpand: false });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function hideSelectionOverlay(options = {}) {
|
||||
const commitPreview = options.commitPreview === true;
|
||||
if (commitPreview) commitPendingPatch({ redrawAfter: false });
|
||||
else if (options.discardPreview === true) discardPendingPatch({ redrawAfter: false });
|
||||
dragState.selectStart = null;
|
||||
dragState.selectEnd = null;
|
||||
dragState.selectPath = null;
|
||||
|
|
@ -432,7 +478,10 @@ function hideSelectionOverlay() {
|
|||
resetPatchVariant({ update: false });
|
||||
hideSelectionSvg();
|
||||
if (selectionEl) selectionEl.style.display = "none";
|
||||
state.viewportMap = null;
|
||||
renderStats(displaySourceMap());
|
||||
updatePatchControls();
|
||||
if (commitPreview || options.discardPreview === true) redraw({ fastTerrain: false, allowWorldExpand: false });
|
||||
}
|
||||
|
||||
function selectionPixelsToCells(start, end) {
|
||||
|
|
@ -470,6 +519,7 @@ function handleMapPointerDown(event) {
|
|||
dragState.mode = "pan";
|
||||
canvasShell.classList.add("panning");
|
||||
} else {
|
||||
if (state.selectionRect) commitPendingPatch({ redrawAfter: false });
|
||||
dragState.mode = "select";
|
||||
dragState.selectStart = clampCanvasPoint(event);
|
||||
dragState.selectEnd = dragState.selectStart;
|
||||
|
|
@ -551,8 +601,11 @@ function handleMapPointerUp(event) {
|
|||
state.camera = dragState.pendingCamera;
|
||||
dragState.pendingCamera = null;
|
||||
}
|
||||
const clickDistance = Math.hypot(event.clientX - dragState.startClientX, event.clientY - dragState.startClientY);
|
||||
const shouldClearSelectionByClick = wasPanning && clickDistance <= 5 && !!state.selectionRect;
|
||||
clearDragMode();
|
||||
if (wasPanning) redraw({ fastTerrain: false });
|
||||
if (shouldClearSelectionByClick) hideSelectionOverlay({ commitPreview: true });
|
||||
else if (wasPanning) redraw({ fastTerrain: false });
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
|
|
@ -738,6 +791,11 @@ function hasNumericId(item, id, keys = ADMIN_ID_KEYS) {
|
|||
return keys.some((key) => Number.isFinite(item?.[key]) && Math.floor(item[key]) === Math.floor(id));
|
||||
}
|
||||
|
||||
function numericPrefectureId(item) {
|
||||
const value = numericIdOf(item, PREFECTURE_ID_KEYS);
|
||||
return value == null ? -1 : value;
|
||||
}
|
||||
|
||||
function firstUsableText(item, keys) {
|
||||
for (const key of keys) {
|
||||
const value = item?.[key];
|
||||
|
|
@ -768,7 +826,7 @@ function adminCenterForId(map, adminId) {
|
|||
function looksNumericName(name) {
|
||||
if (!name) return true;
|
||||
const text = String(name).trim();
|
||||
return !text || /^-?\d+(?:\s*[,,]\s*\d+)*$/u.test(text) || /^(Municipality|Prefecture|Admin|Region)\s*-?\d+/i.test(text);
|
||||
return !text || /^県域\d*$/u.test(text) || /^-?\d+(?:\s*[,,]\s*\d+)*$/u.test(text) || /^(Municipality|Prefecture|Admin|Region)\s*-?\d+/i.test(text);
|
||||
}
|
||||
|
||||
function nearestNamedAdminCenter(map, cellIndex, maxDistance = 36, adminId = null) {
|
||||
|
|
@ -810,13 +868,46 @@ function adminPopulation(map, adminId, cellIndex = -1) {
|
|||
return found ? sum : null;
|
||||
}
|
||||
|
||||
function prefectureNameForCell(map, i) {
|
||||
const id = map.prefectureRegionId?.[i] ?? -1;
|
||||
const region = (map.prefectureRegions || []).find((p) => hasNumericId(p, id, PREFECTURE_ID_KEYS));
|
||||
function worldSourceMap() {
|
||||
return displayWorld()?.sourceMap || null;
|
||||
}
|
||||
|
||||
function prefectureRegionById(id, maps = []) {
|
||||
if (!Number.isFinite(id) || id < 0) return null;
|
||||
for (const source of maps) {
|
||||
const region = (source?.prefectureRegions || []).find((p) => hasNumericId(p, id, PREFECTURE_ID_KEYS));
|
||||
if (region) return region;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function prefectureNameForId(id, adminId = -1) {
|
||||
if (!Number.isFinite(id) || id < 0) return "";
|
||||
const sources = [activeMap(), worldSourceMap()].filter(Boolean);
|
||||
const region = prefectureRegionById(id, sources);
|
||||
const regionName = firstUsableText(region, PREFECTURE_NAME_KEYS);
|
||||
if (regionName) return regionName;
|
||||
|
||||
for (const source of sources) {
|
||||
for (const center of source?.adminCenters || []) {
|
||||
if (adminId >= 0 && !hasNumericId(center, adminId)) continue;
|
||||
const centerPref = numericPrefectureId(center);
|
||||
if (centerPref >= 0 && centerPref !== id) continue;
|
||||
const name = firstUsableText(center, ["prefectureName", "prefectureRegionName", "regionName"]);
|
||||
if (name) return name;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function prefectureNameForCell(map, i) {
|
||||
const id = map.prefectureRegionId?.[i] ?? -1;
|
||||
const direct = prefectureNameForId(id);
|
||||
if (direct) return direct;
|
||||
const adminId = map.adminId?.[i] ?? -1;
|
||||
const mappedPref = adminId >= 0 ? map.municipalityToPrefectureId?.[adminId] ?? -1 : -1;
|
||||
const mappedPref = adminId >= 0 ? map.municipalityToPrefectureId?.[adminId] ?? state.world?.sourceMap?.municipalityToPrefectureId?.[adminId] ?? -1 : -1;
|
||||
const mapped = prefectureNameForId(mappedPref, adminId);
|
||||
if (mapped) return mapped;
|
||||
const center = mappedPref === id ? nearestNamedAdminCenter(map, i, 90, adminId) : null;
|
||||
const fromCenter = firstUsableText(center, ["prefectureName", "prefectureRegionName", "regionName"]);
|
||||
return fromCenter || (id >= 0 ? "Unnamed prefecture" : "-");
|
||||
|
|
@ -893,8 +984,9 @@ async function regenerate() {
|
|||
state.world = createWorldMap(state.map);
|
||||
state.camera = createInitialCamera(state.world);
|
||||
state.lastPatchResult = null;
|
||||
state.pendingPatch = null;
|
||||
resetPatchVariant({ update: false });
|
||||
hideSelectionOverlay();
|
||||
hideSelectionOverlay({ discardPreview: true });
|
||||
renderStats(state.map);
|
||||
redraw();
|
||||
if (progressStageEl) progressStageEl.textContent = `Done in ${formatMs(state.map.generationTotalMs || 0)}`;
|
||||
|
|
@ -919,6 +1011,47 @@ function derivePatchSeed(rect, terrainType, variant = 0) {
|
|||
}
|
||||
|
||||
|
||||
|
||||
function beginZoomVisual(oldZoom, event) {
|
||||
if (zoomVisualState) return;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
zoomVisualState = {
|
||||
baseRect: rect,
|
||||
startZoom: clampZoom(oldZoom || state.zoom || 1),
|
||||
originX: Math.min(Math.max(event.clientX - rect.left, 0), rect.width),
|
||||
originY: Math.min(Math.max(event.clientY - rect.top, 0), rect.height),
|
||||
};
|
||||
canvas.style.transformOrigin = `${zoomVisualState.originX}px ${zoomVisualState.originY}px`;
|
||||
canvas.style.willChange = "transform";
|
||||
canvas.classList.add("is-zooming");
|
||||
if (selectionSvgEl) selectionSvgEl.style.visibility = "hidden";
|
||||
}
|
||||
|
||||
function scheduleZoomVisualUpdate() {
|
||||
if (!zoomVisualState || zoomRedrawRaf != null) return;
|
||||
zoomRedrawRaf = requestAnimationFrame(() => {
|
||||
zoomRedrawRaf = null;
|
||||
if (!zoomVisualState) return;
|
||||
const scale = clampZoom(state.zoom || 1) / Math.max(1e-6, zoomVisualState.startZoom || 1);
|
||||
canvas.style.transform = `scale(${scale})`;
|
||||
});
|
||||
}
|
||||
|
||||
function finishZoomVisual() {
|
||||
if (zoomRedrawRaf != null) {
|
||||
cancelAnimationFrame(zoomRedrawRaf);
|
||||
zoomRedrawRaf = null;
|
||||
}
|
||||
if (zoomVisualState) {
|
||||
canvas.style.transform = "";
|
||||
canvas.style.transformOrigin = "";
|
||||
canvas.style.willChange = "";
|
||||
canvas.classList.remove("is-zooming");
|
||||
if (selectionSvgEl) selectionSvgEl.style.visibility = "";
|
||||
zoomVisualState = null;
|
||||
}
|
||||
}
|
||||
|
||||
function handleCanvasWheel(event) {
|
||||
if (!state.world || !activeMap()) return;
|
||||
event.preventDefault();
|
||||
|
|
@ -942,19 +1075,101 @@ function handleCanvasWheel(event) {
|
|||
}
|
||||
}
|
||||
|
||||
// Wheel events can fire dozens of times per second. Do one lightweight redraw
|
||||
// per frame, then a full labeled/continuous redraw once zooming settles.
|
||||
if (zoomRedrawRaf == null) {
|
||||
zoomRedrawRaf = requestAnimationFrame(() => {
|
||||
zoomRedrawRaf = null;
|
||||
redraw({ fastTerrain: true, allowWorldExpand: false });
|
||||
});
|
||||
}
|
||||
// Wheel events can fire dozens of times per second. During the gesture, keep
|
||||
// the last rendered bitmap and only transform it on the GPU; rebuild the
|
||||
// viewport and labels once the gesture settles.
|
||||
beginZoomVisual(oldZoom, event);
|
||||
scheduleZoomVisualUpdate();
|
||||
if (zoomSettledTimer != null) clearTimeout(zoomSettledTimer);
|
||||
zoomSettledTimer = window.setTimeout(() => {
|
||||
zoomSettledTimer = null;
|
||||
finishZoomVisual();
|
||||
redraw({ fastTerrain: false, allowWorldExpand: false });
|
||||
}, 140);
|
||||
}, 170);
|
||||
}
|
||||
|
||||
function cloneForPatchPreview(value, seen = new Map()) {
|
||||
if (value == null || typeof value !== "object") return value;
|
||||
if (ArrayBuffer.isView(value)) return new value.constructor(value);
|
||||
if (value instanceof ArrayBuffer) return value.slice(0);
|
||||
if (seen.has(value)) return seen.get(value);
|
||||
if (value instanceof Map) {
|
||||
const out = new Map();
|
||||
seen.set(value, out);
|
||||
for (const [k, v] of value.entries()) out.set(cloneForPatchPreview(k, seen), cloneForPatchPreview(v, seen));
|
||||
return out;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
const out = [];
|
||||
seen.set(value, out);
|
||||
for (const item of value) out.push(cloneForPatchPreview(item, seen));
|
||||
return out;
|
||||
}
|
||||
const out = {};
|
||||
seen.set(value, out);
|
||||
for (const [key, item] of Object.entries(value)) out[key] = cloneForPatchPreview(item, seen);
|
||||
return out;
|
||||
}
|
||||
|
||||
function createPatchWorker() {
|
||||
if (patchWorker || typeof Worker === "undefined") return patchWorker;
|
||||
try {
|
||||
patchWorker = new Worker(new URL("./mapPatchWorker.js", import.meta.url), { type: "module" });
|
||||
patchWorker.addEventListener("error", () => {
|
||||
patchWorker?.terminate?.();
|
||||
patchWorker = null;
|
||||
});
|
||||
} catch (_) {
|
||||
patchWorker = null;
|
||||
}
|
||||
return patchWorker;
|
||||
}
|
||||
|
||||
function runPatchInWorker(world, rect, options) {
|
||||
const worker = createPatchWorker();
|
||||
if (!worker) return null;
|
||||
const id = ++patchJobSeq;
|
||||
return new Promise((resolve, reject) => {
|
||||
const cleanup = () => {
|
||||
worker.removeEventListener("message", onMessage);
|
||||
worker.removeEventListener("error", onError);
|
||||
worker.removeEventListener("messageerror", onMessageError);
|
||||
};
|
||||
const onMessage = (event) => {
|
||||
if (event.data?.id !== id) return;
|
||||
cleanup();
|
||||
if (event.data.ok) resolve({ world: event.data.world, result: event.data.result, worker: true });
|
||||
else reject(new Error(event.data.error || "Patch worker failed"));
|
||||
};
|
||||
const onError = (event) => {
|
||||
cleanup();
|
||||
reject(new Error(event.message || "Patch worker error"));
|
||||
};
|
||||
const onMessageError = () => {
|
||||
cleanup();
|
||||
reject(new Error("Patch worker message clone failed"));
|
||||
};
|
||||
worker.addEventListener("message", onMessage);
|
||||
worker.addEventListener("error", onError);
|
||||
worker.addEventListener("messageerror", onMessageError);
|
||||
worker.postMessage({ id, world, rect, options });
|
||||
});
|
||||
}
|
||||
|
||||
async function generatePatchPreviewWorld(baseWorld, rect, options) {
|
||||
const workerPromise = runPatchInWorker(baseWorld, rect, options);
|
||||
if (workerPromise) {
|
||||
try {
|
||||
return await workerPromise;
|
||||
} catch (error) {
|
||||
console.warn("Patch worker unavailable; falling back to main-thread preview generation.", error);
|
||||
patchWorker?.terminate?.();
|
||||
patchWorker = null;
|
||||
}
|
||||
}
|
||||
const previewWorld = cloneForPatchPreview(baseWorld);
|
||||
const result = generatePatch(previewWorld, rect, options);
|
||||
return { world: previewWorld, result, worker: false };
|
||||
}
|
||||
|
||||
async function generateSelectedPatch() {
|
||||
|
|
@ -966,23 +1181,29 @@ async function generateSelectedPatch() {
|
|||
const terrainType = patchTerrainTypeInput?.value || generationTypeInput?.value || "auto";
|
||||
const variant = readPatchVariant();
|
||||
const seed = derivePatchSeed(validation.rect, terrainType, variant);
|
||||
setProgressVisible(true, "Generating selected patch...");
|
||||
setProgressVisible(true, "Generating preview patch...");
|
||||
await nextFrame();
|
||||
try {
|
||||
const result = generatePatch(state.world, validation.rect, { terrainType, seed, variant });
|
||||
const job = await generatePatchPreviewWorld(state.world, validation.rect, { terrainType, seed, variant });
|
||||
const result = job.result;
|
||||
if (!result.ok) {
|
||||
if (progressStageEl) progressStageEl.textContent = `Patch failed: ${result.reason || "invalid selection"}`;
|
||||
updatePatchControls();
|
||||
window.setTimeout(() => setProgressVisible(false), 1200);
|
||||
return;
|
||||
}
|
||||
state.lastPatchResult = result;
|
||||
redraw();
|
||||
renderStats(state.map);
|
||||
state.pendingPatch = { world: job.world, result, rect: validation.rect, terrainType, seed, variant, worker: job.worker };
|
||||
// Keep the committed world untouched. The preview world is rendered until
|
||||
// the user clicks once without dragging; Alternative replaces this preview
|
||||
// from the same committed base, so old candidate artifacts cannot accumulate.
|
||||
state.viewportMap = null;
|
||||
redraw({ fastTerrain: true, allowWorldExpand: false });
|
||||
window.setTimeout(() => redraw({ fastTerrain: false, allowWorldExpand: false }), 80);
|
||||
renderStats(displaySourceMap());
|
||||
updatePatchControls();
|
||||
const human = result.humanGeography;
|
||||
const humanText = human?.ok ? ` / human: ${human.modernCities || 0} cities, ${human.ports || 0} ports, ${human.villages || 0} villages, ${(human.roadConnectorsCreated || 0) + (human.railwayConnectorsCreated || 0)} connectors, admin ${human.adminCellsReassigned || 0}, invalid ports ${human.invalidPortsRemoved || 0}` : "";
|
||||
if (progressStageEl) progressStageEl.textContent = `Patch generated: ${result.label} / variant ${result.variant ?? variant} / mode ${result.patchGenerationMode || "legacy-full-pipeline"} / core ${formatRectSize(result.rects.coreRect)} / write ${formatRectSize(result.rects.writeRect)} / terrain ${result.updatedCells.toLocaleString()} cells / coast ${result.coastCellsChanged || 0} / natural ${result.naturalRegionsUpdated || 0}${humanText}`;
|
||||
if (progressStageEl) progressStageEl.textContent = `Preview generated${job.worker ? " in worker" : ""}: ${result.label} / variant ${result.variant ?? variant} / mode ${result.patchGenerationMode || "legacy-full-pipeline"} / core ${formatRectSize(result.rects.coreRect)} / write ${formatRectSize(result.rects.writeRect)} / terrain ${result.updatedCells.toLocaleString()} cells / coast ${result.coastCellsChanged || 0} / natural ${result.naturalRegionsUpdated || 0}${humanText}. Click the map without dragging to apply.`;
|
||||
renderTimingRows(result.patchTimings || []);
|
||||
window.setTimeout(() => setProgressVisible(false), 900);
|
||||
} catch (error) {
|
||||
|
|
@ -1002,9 +1223,11 @@ async function generateAlternativePatch() {
|
|||
}
|
||||
|
||||
function redraw(options = {}) {
|
||||
if (!state.world) return;
|
||||
const renderWorld = displayWorld();
|
||||
if (!renderWorld) return;
|
||||
const viewSize = syncViewportSize();
|
||||
const expansion = options.allowWorldExpand === false ? null : ensureWorldPaddingForCamera(state.world, state.camera, viewSize.width, viewSize.height);
|
||||
const mayExpand = !state.pendingPatch && options.allowWorldExpand !== false;
|
||||
const expansion = mayExpand ? ensureWorldPaddingForCamera(state.world, state.camera, viewSize.width, viewSize.height) : null;
|
||||
if (expansion?.expanded) {
|
||||
const ex = expansion.dx || 0;
|
||||
const ey = expansion.dy || 0;
|
||||
|
|
@ -1025,8 +1248,9 @@ function redraw(options = {}) {
|
|||
};
|
||||
}
|
||||
}
|
||||
state.camera = clampCameraForView(state.camera, viewSize);
|
||||
state.viewportMap = getViewportMap(state.world, state.camera, viewSize.width, viewSize.height, { light: !!options.fastTerrain });
|
||||
const activeWorldForRender = displayWorld();
|
||||
state.camera = clampCameraForView(state.camera, viewSize, activeWorldForRender);
|
||||
state.viewportMap = getViewportMap(activeWorldForRender, state.camera, viewSize.width, viewSize.height, { light: !!options.fastTerrain });
|
||||
state.hoverEntities = options.fastTerrain ? [] : buildHoverEntities(state.viewportMap);
|
||||
drawMap(canvas, state.viewportMap, {
|
||||
mode: state.mode,
|
||||
|
|
@ -1050,6 +1274,7 @@ function init() {
|
|||
|
||||
generationTypeInput?.addEventListener("change", regenerate);
|
||||
patchTerrainTypeInput?.addEventListener("change", () => {
|
||||
discardPendingPatch({ redrawAfter: true });
|
||||
state.lastPatchResult = null;
|
||||
resetPatchVariant({ update: false });
|
||||
updatePatchControls();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue