map/worldViewport.js

257 lines
12 KiB
JavaScript
Raw Normal View History

2026-05-28 19:37:28 +09:00
import { MAP_H, MAP_W, SIZE } from "./mapUtils.js";
const EMPTY_ARRAY_KEYS = new Set([
"villages", "markets", "castles", "ports", "modernCities", "satelliteCities", "stations",
"interchanges", "industrialZones", "logisticsParks", "newTowns", "adminCenters",
"premodernRoads", "minorRoads", "nationalRoads", "ringRoads", "externalRoads",
"railways", "branchRailways", "ringRailways", "externalRailways", "expressways", "externalExpressways",
"mainRivers", "tributaryRivers", "smallStreams", "prefectureBorder", "regionalPrefectureBorders",
"adminBorders", "externalGateways", "prefectureRegions",
]);
const PATH_ARRAY_KEYS = new Set([
"premodernRoads", "minorRoads", "nationalRoads", "ringRoads", "externalRoads",
"railways", "branchRailways", "ringRailways", "externalRailways", "expressways", "externalExpressways",
"mainRivers", "tributaryRivers", "smallStreams",
]);
const SEGMENT_ARRAY_KEYS = new Set([
"prefectureBorder", "regionalPrefectureBorders", "adminBorders",
]);
const POINT_ARRAY_KEYS = new Set([
"villages", "markets", "castles", "ports", "modernCities", "satelliteCities", "stations",
"interchanges", "industrialZones", "logisticsParks", "newTowns", "adminCenters",
"externalGateways", "prefectureRegions",
]);
const NEGATIVE_ONE_FIELDS = new Set([
"adminId",
"prefectureRegionId",
"regionId",
"municipalityId",
2026-05-28 23:51:55 +09:00
"naturalCompartmentId",
"watershedId",
2026-05-28 19:37:28 +09:00
]);
function isCellField(value) {
return ArrayBuffer.isView(value) && typeof value.length === "number" && value.length === SIZE;
}
function defaultForField(name, Constructor) {
if (name === "sea") return 1;
if (name === "elevation") return 0.08;
if (NEGATIVE_ONE_FIELDS.has(name)) return -1;
if (Constructor === Float32Array || Constructor === Float64Array) return 0;
return 0;
}
function worldIndex(world, x, y) {
if (!world || x < 0 || y < 0 || x >= world.width || y >= world.height) return -1;
return y * world.width + x;
}
function copyViewportField(name, source, world, camera, viewWidth, viewHeight) {
const Constructor = source.constructor;
const out = new Constructor(viewWidth * viewHeight);
const fallback = defaultForField(name, Constructor);
if (fallback !== 0) out.fill(fallback);
const cx = Math.round(camera.x || 0);
const cy = Math.round(camera.y || 0);
for (let y = 0; y < viewHeight; y++) {
for (let x = 0; x < viewWidth; x++) {
const src = worldIndex(world, cx + x, cy + y);
if (src >= 0) out[y * viewWidth + x] = source[src];
}
}
return out;
}
2026-05-29 14:31:42 +09:00
function inViewportPoint(p, margin = 0, viewWidth = MAP_W, viewHeight = MAP_H) {
return p && p.x >= -margin && p.y >= -margin && p.x < viewWidth + margin && p.y < viewHeight + margin;
2026-05-28 19:37:28 +09:00
}
function transformPointObject(point, camera, originX, originY) {
if (!point || !Number.isFinite(point.x) || !Number.isFinite(point.y)) return null;
return {
...point,
x: point.x + originX - camera.x,
y: point.y + originY - camera.y,
worldX: point.x + originX,
worldY: point.y + originY,
};
}
2026-05-29 14:31:42 +09:00
function transformPointArray(items, camera, originX, originY, margin = 36, preserveIndexes = false, viewWidth = MAP_W, viewHeight = MAP_H) {
2026-05-28 19:37:28 +09:00
const mapped = (items || []).map((item) => transformPointObject(item, camera, originX, originY));
2026-05-29 14:31:42 +09:00
return preserveIndexes ? mapped : mapped.filter((item) => inViewportPoint(item, margin, viewWidth, viewHeight));
2026-05-28 19:37:28 +09:00
}
function transformTuple(tuple, camera, originX, originY) {
if (!Array.isArray(tuple) || tuple.length < 2) return null;
return [tuple[0] + originX - camera.x, tuple[1] + originY - camera.y];
}
2026-05-29 14:31:42 +09:00
function tupleInside(tuple, margin = 0, viewWidth = MAP_W, viewHeight = MAP_H) {
return tuple && tuple[0] >= -margin && tuple[1] >= -margin && tuple[0] < viewWidth + margin && tuple[1] < viewHeight + margin;
2026-05-28 19:37:28 +09:00
}
2026-05-29 14:31:42 +09:00
function splitTransformedPath(path, camera, originX, originY, margin = 0, viewWidth = MAP_W, viewHeight = MAP_H) {
2026-05-28 19:37:28 +09:00
const chunks = [];
let current = [];
for (const tuple of path || []) {
const p = transformTuple(tuple, camera, originX, originY);
2026-05-29 14:31:42 +09:00
const inside = tupleInside(p, margin, viewWidth, viewHeight);
2026-05-28 19:37:28 +09:00
if (inside) {
current.push([Math.round(p[0]), Math.round(p[1])]);
} else if (current.length >= 2) {
chunks.push(current);
current = [];
} else {
current = [];
}
}
if (current.length >= 2) chunks.push(current);
return chunks;
}
2026-05-29 14:31:42 +09:00
function transformPaths(paths, camera, originX, originY, viewWidth = MAP_W, viewHeight = MAP_H) {
2026-05-28 19:37:28 +09:00
const out = [];
2026-05-29 14:31:42 +09:00
for (const path of paths || []) out.push(...splitTransformedPath(path, camera, originX, originY, 0, viewWidth, viewHeight));
2026-05-28 19:37:28 +09:00
return out;
}
2026-05-29 14:31:42 +09:00
function segmentIntersectsViewport(seg, margin = 4, viewWidth = MAP_W, viewHeight = MAP_H) {
2026-05-28 19:37:28 +09:00
if (!seg || seg.length < 2) return false;
const xs = [seg[0][0], seg[1][0]];
const ys = [seg[0][1], seg[1][1]];
2026-05-29 14:31:42 +09:00
return Math.max(...xs) >= -margin && Math.min(...xs) <= viewWidth + margin && Math.max(...ys) >= -margin && Math.min(...ys) <= viewHeight + margin;
2026-05-28 19:37:28 +09:00
}
2026-05-29 14:31:42 +09:00
function transformSegments(segments, camera, originX, originY, viewWidth = MAP_W, viewHeight = MAP_H) {
2026-05-28 19:37:28 +09:00
return (segments || [])
.map((seg) => [transformTuple(seg?.[0], camera, originX, originY), transformTuple(seg?.[1], camera, originX, originY)])
2026-05-29 14:31:42 +09:00
.filter((seg) => segmentIntersectsViewport(seg, 4, viewWidth, viewHeight));
2026-05-28 19:37:28 +09:00
}
2026-05-28 23:51:55 +09:00
function copySourceMapViewportField(source, camera, originX, originY, viewWidth, viewHeight) {
if (!ArrayBuffer.isView(source) || typeof source.length !== "number" || source.length !== SIZE) return source;
const out = new source.constructor(viewWidth * viewHeight);
const cx = Math.round(camera.x || 0);
const cy = Math.round(camera.y || 0);
for (let y = 0; y < viewHeight; y++) {
for (let x = 0; x < viewWidth; x++) {
const sx = cx + x - originX;
const sy = cy + y - originY;
if (sx >= 0 && sy >= 0 && sx < MAP_W && sy < MAP_H) out[y * viewWidth + x] = source[sy * MAP_W + sx];
}
}
return out;
}
2026-05-29 14:31:42 +09:00
function transformTransportDebug(debug, camera, originX, originY, viewport = null, viewWidth = MAP_W, viewHeight = MAP_H) {
2026-05-28 19:37:28 +09:00
if (!debug?.layers) return debug;
const layers = { ...debug.layers };
2026-05-28 23:51:55 +09:00
for (const [key, value] of Object.entries(layers)) {
2026-05-29 14:31:42 +09:00
if (ArrayBuffer.isView(value) && value.length === SIZE) layers[key] = copySourceMapViewportField(value, camera, originX, originY, viewWidth, viewHeight);
2026-05-28 23:51:55 +09:00
}
// The original transport-debug potential layers are fixed-map arrays. Once the
// viewport pans into patched world cells, synthesize equivalent viewport-sized
// debug fields from the current world-backed fields so the color overlay moves
// with the terrain instead of staying tied to the initial source map.
if (viewport) {
2026-05-29 14:31:42 +09:00
const n = viewWidth * viewHeight;
2026-05-28 23:51:55 +09:00
const make = (fn) => {
const out = new Float32Array(n);
for (let i = 0; i < n; i++) out[i] = fn(i);
return out;
};
const sea = viewport.sea || new Uint8Array(n);
const slope = viewport.slope || new Float32Array(n);
const plain = viewport.plain || new Float32Array(n);
const pop = viewport.populationDensity || viewport.settlementScore || new Float32Array(n);
const road = viewport.roadInfluence || new Float32Array(n);
const rail = viewport.railInfluence2 || viewport.stationInfluence || new Float32Array(n);
layers.expresswayPotential = make((i) => sea[i] ? 0 : Math.max(0, Math.min(1, road[i] * 0.72 + pop[i] * 0.42 + plain[i] * 0.22 - slope[i] * 0.52)));
layers.nationalRoadPotential = make((i) => sea[i] ? 0 : Math.max(0, Math.min(1, road[i] * 0.58 + pop[i] * 0.55 + plain[i] * 0.18 - slope[i] * 0.38)));
layers.railPotential = make((i) => sea[i] ? 0 : Math.max(0, Math.min(1, rail[i] * 0.72 + pop[i] * 0.38 + plain[i] * 0.26 - slope[i] * 0.72)));
layers.slopeSeaPenalty = make((i) => sea[i] ? 1 : Math.max(0, Math.min(1, slope[i] * 1.35)));
}
2026-05-28 19:37:28 +09:00
if (Array.isArray(layers.components)) {
layers.components = layers.components.map((component) => ({
...component,
cells: (component.cells || [])
.map((cell) => transformTuple(cell, camera, originX, originY))
2026-05-29 14:31:42 +09:00
.filter((cell) => tupleInside(cell, 0, viewWidth, viewHeight))
2026-05-28 19:37:28 +09:00
.map(([x, y]) => [Math.round(x), Math.round(y)]),
})).filter((component) => component.cells.length);
}
if (Array.isArray(layers.repairedSegments)) {
2026-05-29 14:31:42 +09:00
layers.repairedSegments = layers.repairedSegments.flatMap((repair) => transformPaths([repair.path || []], camera, originX, originY, viewWidth, viewHeight).map((path) => ({ ...repair, path })));
2026-05-28 19:37:28 +09:00
}
2026-05-29 14:31:42 +09:00
if (Array.isArray(layers.unservedSettlements)) layers.unservedSettlements = transformPointArray(layers.unservedSettlements, camera, originX, originY, 36, false, viewWidth, viewHeight);
2026-05-28 19:37:28 +09:00
return { ...debug, layers };
}
function buildEmptyViewportFromSource(sourceMap, world, camera, viewWidth, viewHeight) {
const viewport = { ...sourceMap };
viewport.width = viewWidth;
viewport.height = viewHeight;
viewport.worldCamera = { x: camera.x, y: camera.y };
viewport.worldOrigin = { x: world.originX, y: world.originY };
viewport.generatedRects = world.generatedRects || [];
for (const key of EMPTY_ARRAY_KEYS) if (Array.isArray(viewport[key])) viewport[key] = [];
return viewport;
}
2026-05-29 14:31:42 +09:00
export function getViewportMap(world, camera, viewWidth = MAP_W, viewHeight = MAP_H, options = {}) {
2026-05-28 19:37:28 +09:00
const sourceMap = world?.sourceMap || {};
const normalizedCamera = {
x: Math.round(camera?.x || 0),
y: Math.round(camera?.y || 0),
};
const viewport = buildEmptyViewportFromSource(sourceMap, world, normalizedCamera, viewWidth, viewHeight);
for (const [name, value] of Object.entries(world?.fields || {})) {
viewport[name] = copyViewportField(name, value, world, normalizedCamera, viewWidth, viewHeight);
}
2026-05-29 22:00:42 +09:00
if (options.light) {
// Light/fast redraws are used while panning and zooming. Do not leave
// source-space debug vectors on the viewport; in borders-debug this made
// natural compartment lines appear fixed on screen while the map moved.
viewport.adminDebug = null;
viewport.transportDebug = null;
return viewport;
}
2026-05-29 14:31:42 +09:00
2026-05-28 19:37:28 +09:00
const originX = world?.originX || 0;
const originY = world?.originY || 0;
for (const key of POINT_ARRAY_KEYS) {
if (!Array.isArray(sourceMap[key])) continue;
2026-05-29 14:31:42 +09:00
viewport[key] = transformPointArray(sourceMap[key], normalizedCamera, originX, originY, 36, key === "adminCenters", viewWidth, viewHeight);
2026-05-28 19:37:28 +09:00
}
2026-05-29 14:31:42 +09:00
for (const key of PATH_ARRAY_KEYS) if (Array.isArray(sourceMap[key])) viewport[key] = transformPaths(sourceMap[key], normalizedCamera, originX, originY, viewWidth, viewHeight);
for (const key of SEGMENT_ARRAY_KEYS) if (Array.isArray(sourceMap[key])) viewport[key] = transformSegments(sourceMap[key], normalizedCamera, originX, originY, viewWidth, viewHeight);
2026-05-28 19:37:28 +09:00
if (sourceMap.adminDebug) {
viewport.adminDebug = {
...sourceMap.adminDebug,
2026-05-29 14:31:42 +09:00
compartmentBorders: transformSegments(sourceMap.adminDebug.compartmentBorders || [], normalizedCamera, originX, originY, viewWidth, viewHeight),
lowlandAdminSeeds: transformPointArray(sourceMap.adminDebug.lowlandAdminSeeds || [], normalizedCamera, originX, originY, 36, false, viewWidth, viewHeight),
2026-05-28 19:37:28 +09:00
};
}
2026-05-29 14:31:42 +09:00
if (sourceMap.transportDebug) viewport.transportDebug = transformTransportDebug(sourceMap.transportDebug, normalizedCamera, originX, originY, viewport, viewWidth, viewHeight);
2026-05-28 19:37:28 +09:00
if (sourceMap.neighborPrefectureDetails) {
viewport.neighborPrefectureDetails = {
...sourceMap.neighborPrefectureDetails,
2026-05-29 14:31:42 +09:00
cities: transformPointArray(sourceMap.neighborPrefectureDetails.cities || [], normalizedCamera, originX, originY, 36, false, viewWidth, viewHeight),
adminCenters: transformPointArray(sourceMap.neighborPrefectureDetails.adminCenters || [], normalizedCamera, originX, originY, 36, false, viewWidth, viewHeight),
roads: transformPaths(sourceMap.neighborPrefectureDetails.roads || [], normalizedCamera, originX, originY, viewWidth, viewHeight),
2026-05-28 19:37:28 +09:00
};
}
return viewport;
}