more porting work

This commit is contained in:
barrulus 2025-08-05 13:12:07 -04:00
parent 37391c8e8b
commit 7f31969f50
38 changed files with 3673 additions and 463 deletions

View file

@ -1,73 +1,73 @@
"use strict";
export async function generate(graph, config, utils) {
const { aleaPRNG, heightmapTemplates } = utils;
const { TIME } = config.debug;
const { templateId, seed } = config;
TIME && console.time("defineHeightmap");
const isTemplate = templateId in heightmapTemplates;
const heights = isTemplate
? fromTemplate(graph, templateId, config, utils)
: await fromPrecreated(graph, templateId, config, utils);
TIME && console.timeEnd("defineHeightmap");
return heights;
}
// Placeholder function for processing precreated heightmaps
// This will need further refactoring to work headlessly (see heightmap-generator_render.md)
export async function fromPrecreated(graph, id, config, utils) {
// TODO: Implement headless image processing
// This function currently requires DOM/Canvas which was removed
// Future implementation will need:
// - utils.loadImage() function to load PNG files headlessly
// - Image processing library (e.g., canvas package for Node.js)
// - getHeightsFromImageData() refactored for headless operation
throw new Error(`fromPrecreated not yet implemented for headless operation. Template ID: ${id}`);
}
export function fromTemplate(graph, id, config, utils) {
const { heightmapTemplates } = utils;
const templateString = heightmapTemplates[id]?.template || "";
export function fromTemplate(grid, templateId, config, utils) {
const { heightmapTemplates, aleaPRNG } = utils;
const templateString = heightmapTemplates[templateId]?.template || "";
const steps = templateString.split("\n");
if (!steps.length) throw new Error(`Heightmap template: no steps. Template: ${id}. Steps: ${steps}`);
if (!steps.length) throw new Error(`Heightmap template: no steps. Template: ${templateId}. Steps: ${steps}`);
let { heights, blobPower, linePower } = setGraph(graph, utils);
const { cellsDesired, cells, points } = grid;
let heights = cells.h ? Uint8Array.from(cells.h) : createTypedArray({ maxValue: 100, length: points.length });
const blobPower = getBlobPower(cellsDesired);
const linePower = getLinePower(cellsDesired);
// Set up PRNG if seed is provided
if (config.seed !== undefined) {
Math.random = aleaPRNG(config.seed);
}
for (const step of steps) {
const elements = step.trim().split(" ");
if (elements.length < 2) throw new Error(`Heightmap template: steps < 2. Template: ${id}. Step: ${elements}`);
heights = addStep(heights, graph, blobPower, linePower, config, utils, ...elements);
if (elements.length < 2) throw new Error(`Heightmap template: steps < 2. Template: ${templateId}. Step: ${elements}`);
heights = addStep(heights, grid, blobPower, linePower, utils, ...elements);
}
return heights;
}
function setGraph(graph, utils) {
const { createTypedArray } = utils;
const { cellsDesired, cells, points } = graph;
const heights = cells.h ? Uint8Array.from(cells.h) : createTypedArray({ maxValue: 100, length: points.length });
const blobPower = getBlobPower(cellsDesired);
const linePower = getLinePower(cellsDesired);
return { heights, blobPower, linePower };
export async function fromPrecreated(grid, imageId, config, utils) {
// This function requires browser-specific Canvas API and Image loading
// It should be handled by the viewer layer, not the headless engine
throw new Error("fromPrecreated requires browser environment - should be handled by viewer layer");
}
function addStep(heights, graph, blobPower, linePower, config, utils, tool, a2, a3, a4, a5) {
if (tool === "Hill") return addHill(heights, graph, blobPower, config, utils, a2, a3, a4, a5);
if (tool === "Pit") return addPit(heights, graph, blobPower, config, utils, a2, a3, a4, a5);
if (tool === "Range") return addRange(heights, graph, linePower, config, utils, a2, a3, a4, a5);
if (tool === "Trough") return addTrough(heights, graph, linePower, config, utils, a2, a3, a4, a5);
if (tool === "Strait") return addStrait(heights, graph, config, utils, a2, a3);
if (tool === "Mask") return mask(heights, graph, config, utils, a2);
if (tool === "Invert") return invert(heights, graph, config, utils, a2, a3);
if (tool === "Add") return modify(heights, a3, +a2, 1, utils);
if (tool === "Multiply") return modify(heights, a3, 0, +a2, utils);
if (tool === "Smooth") return smooth(heights, graph, utils, a2);
export async function generate(grid, config, utils) {
const { TIME, aleaPRNG } = utils;
TIME && console.time("defineHeightmap");
const templateId = config.heightmap.templateId;
// Set up PRNG if seed is provided
if (config.seed !== undefined) {
Math.random = aleaPRNG(config.seed);
}
const { heightmapTemplates } = utils;
const isTemplate = templateId in heightmapTemplates;
if (!isTemplate) {
throw new Error(`Template "${templateId}" not found. Available templates: ${Object.keys(heightmapTemplates).join(', ')}`);
}
const heights = fromTemplate(grid, templateId, config, utils);
TIME && console.timeEnd("defineHeightmap");
return heights;
}
function addStep(heights, grid, blobPower, linePower, utils, tool, a2, a3, a4, a5) {
if (tool === "Hill") return addHill(heights, grid, blobPower, utils, a2, a3, a4, a5);
if (tool === "Pit") return addPit(heights, grid, blobPower, utils, a2, a3, a4, a5);
if (tool === "Range") return addRange(heights, grid, linePower, utils, a2, a3, a4, a5);
if (tool === "Trough") return addTrough(heights, grid, linePower, utils, a2, a3, a4, a5);
if (tool === "Strait") return addStrait(heights, grid, utils, a2, a3);
if (tool === "Mask") return mask(heights, grid, a2);
if (tool === "Invert") return invert(heights, grid, a2, a3);
if (tool === "Add") return modify(heights, a3, +a2, 1);
if (tool === "Multiply") return modify(heights, a3, 0, +a2);
if (tool === "Smooth") return smooth(heights, grid, utils, a2);
return heights;
}
@ -110,12 +110,13 @@ function getLinePower(cells) {
return linePowerMap[cells] || 0.81;
}
export function addHill(heights, graph, blobPower, config, utils, count, height, rangeX, rangeY) {
const { getNumberInRange, lim, findGridCell } = utils;
const { graphWidth, graphHeight } = config;
function addHill(heights, grid, blobPower, utils, count, height, rangeX, rangeY) {
const { getNumberInRange, findGridCell, lim } = utils;
const graphWidth = grid.cellsX;
const graphHeight = grid.cellsY;
heights = new Uint8Array(heights);
count = getNumberInRange(count);
let newHeights = new Uint8Array(heights);
while (count > 0) {
addOneHill();
@ -131,34 +132,35 @@ export function addHill(heights, graph, blobPower, config, utils, count, height,
do {
const x = getPointInRange(rangeX, graphWidth, utils);
const y = getPointInRange(rangeY, graphHeight, utils);
start = findGridCell(x, y, graph);
start = findGridCell(x, y, grid);
limit++;
} while (heights[start] + h > 90 && limit < 50);
} while (newHeights[start] + h > 90 && limit < 50);
change[start] = h;
const queue = [start];
while (queue.length) {
const q = queue.shift();
for (const c of graph.cells.c[q]) {
for (const c of grid.cells.c[q]) {
if (change[c]) continue;
change[c] = change[q] ** blobPower * (Math.random() * 0.2 + 0.9);
if (change[c] > 1) queue.push(c);
}
}
heights = heights.map((h, i) => lim(h + change[i]));
newHeights = newHeights.map((h, i) => lim(h + change[i]));
}
return heights;
return newHeights;
}
export function addPit(heights, graph, blobPower, config, utils, count, height, rangeX, rangeY) {
const { getNumberInRange, lim, findGridCell } = utils;
const { graphWidth, graphHeight } = config;
function addPit(heights, grid, blobPower, utils, count, height, rangeX, rangeY) {
const { getNumberInRange, findGridCell, lim } = utils;
const graphWidth = grid.cellsX;
const graphHeight = grid.cellsY;
heights = new Uint8Array(heights);
count = getNumberInRange(count);
let newHeights = new Uint8Array(heights);
while (count > 0) {
addOnePit();
@ -167,16 +169,15 @@ export function addPit(heights, graph, blobPower, config, utils, count, height,
function addOnePit() {
const used = new Uint8Array(heights.length);
let limit = 0,
start;
let limit = 0, start;
let h = lim(getNumberInRange(height));
do {
const x = getPointInRange(rangeX, graphWidth, utils);
const y = getPointInRange(rangeY, graphHeight, utils);
start = findGridCell(x, y, graph);
start = findGridCell(x, y, grid);
limit++;
} while (heights[start] < 20 && limit < 50);
} while (newHeights[start] < 20 && limit < 50);
const queue = [start];
while (queue.length) {
@ -184,24 +185,26 @@ export function addPit(heights, graph, blobPower, config, utils, count, height,
h = h ** blobPower * (Math.random() * 0.2 + 0.9);
if (h < 1) return;
graph.cells.c[q].forEach(function (c, i) {
grid.cells.c[q].forEach(function (c, i) {
if (used[c]) return;
heights[c] = lim(heights[c] - h * (Math.random() * 0.2 + 0.9));
newHeights[c] = lim(newHeights[c] - h * (Math.random() * 0.2 + 0.9));
used[c] = 1;
queue.push(c);
});
}
}
return heights;
return newHeights;
}
export function addRange(heights, graph, linePower, config, utils, count, height, rangeX, rangeY, startCell, endCell) {
const { getNumberInRange, lim, findGridCell, d3 } = utils;
const { graphWidth, graphHeight } = config;
// fromCell, toCell are options cell ids
function addRange(heights, grid, linePower, utils, count, height, rangeX, rangeY, startCell, endCell) {
const { getNumberInRange, findGridCell, lim, d3 } = utils;
const graphWidth = grid.cellsX;
const graphHeight = grid.cellsY;
heights = new Uint8Array(heights);
count = getNumberInRange(count);
let newHeights = new Uint8Array(heights);
while (count > 0) {
addOneRange();
@ -217,10 +220,7 @@ export function addRange(heights, graph, linePower, config, utils, count, height
const startX = getPointInRange(rangeX, graphWidth, utils);
const startY = getPointInRange(rangeY, graphHeight, utils);
let dist = 0,
limit = 0,
endX,
endY;
let dist = 0, limit = 0, endX, endY;
do {
endX = Math.random() * graphWidth * 0.8 + graphWidth * 0.1;
@ -229,8 +229,8 @@ export function addRange(heights, graph, linePower, config, utils, count, height
limit++;
} while ((dist < graphWidth / 8 || dist > graphWidth / 3) && limit < 50);
startCell = findGridCell(startX, startY, graph);
endCell = findGridCell(endX, endY, graph);
startCell = findGridCell(startX, startY, grid);
endCell = findGridCell(endX, endY, grid);
}
let range = getRange(startCell, endCell);
@ -238,12 +238,12 @@ export function addRange(heights, graph, linePower, config, utils, count, height
// get main ridge
function getRange(cur, end) {
const range = [cur];
const p = graph.points;
const p = grid.points;
used[cur] = 1;
while (cur !== end) {
let min = Infinity;
graph.cells.c[cur].forEach(function (e) {
grid.cells.c[cur].forEach(function (e) {
if (used[e]) return;
let diff = (p[end][0] - p[e][0]) ** 2 + (p[end][1] - p[e][1]) ** 2;
if (Math.random() > 0.85) diff = diff / 2;
@ -261,18 +261,17 @@ export function addRange(heights, graph, linePower, config, utils, count, height
}
// add height to ridge and cells around
let queue = range.slice(),
i = 0;
let queue = range.slice(), i = 0;
while (queue.length) {
const frontier = queue.slice();
(queue = []), i++;
frontier.forEach(i => {
heights[i] = lim(heights[i] + h * (Math.random() * 0.3 + 0.85));
newHeights[i] = lim(newHeights[i] + h * (Math.random() * 0.3 + 0.85));
});
h = h ** linePower - 1;
if (h < 2) break;
frontier.forEach(f => {
graph.cells.c[f].forEach(i => {
grid.cells.c[f].forEach(i => {
if (!used[i]) {
queue.push(i);
used[i] = 1;
@ -285,22 +284,23 @@ export function addRange(heights, graph, linePower, config, utils, count, height
range.forEach((cur, d) => {
if (d % 6 !== 0) return;
for (const l of d3.range(i)) {
const min = graph.cells.c[cur][d3.scan(graph.cells.c[cur], (a, b) => heights[a] - heights[b])]; // downhill cell
heights[min] = (heights[cur] * 2 + heights[min]) / 3;
const min = grid.cells.c[cur][d3.scan(grid.cells.c[cur], (a, b) => newHeights[a] - newHeights[b])]; // downhill cell
newHeights[min] = (newHeights[cur] * 2 + newHeights[min]) / 3;
cur = min;
}
});
}
return heights;
return newHeights;
}
export function addTrough(heights, graph, linePower, config, utils, count, height, rangeX, rangeY, startCell, endCell) {
const { getNumberInRange, lim, findGridCell, d3 } = utils;
const { graphWidth, graphHeight } = config;
function addTrough(heights, grid, linePower, utils, count, height, rangeX, rangeY, startCell, endCell) {
const { getNumberInRange, findGridCell, lim, d3 } = utils;
const graphWidth = grid.cellsX;
const graphHeight = grid.cellsY;
heights = new Uint8Array(heights);
count = getNumberInRange(count);
let newHeights = new Uint8Array(heights);
while (count > 0) {
addOneTrough();
@ -313,18 +313,13 @@ export function addTrough(heights, graph, linePower, config, utils, count, heigh
if (rangeX && rangeY) {
// find start and end points
let limit = 0,
startX,
startY,
dist = 0,
endX,
endY;
let limit = 0, startX, startY, dist = 0, endX, endY;
do {
startX = getPointInRange(rangeX, graphWidth, utils);
startY = getPointInRange(rangeY, graphHeight, utils);
startCell = findGridCell(startX, startY, graph);
startCell = findGridCell(startX, startY, grid);
limit++;
} while (heights[startCell] < 20 && limit < 50);
} while (newHeights[startCell] < 20 && limit < 50);
limit = 0;
do {
@ -334,7 +329,7 @@ export function addTrough(heights, graph, linePower, config, utils, count, heigh
limit++;
} while ((dist < graphWidth / 8 || dist > graphWidth / 2) && limit < 50);
endCell = findGridCell(endX, endY, graph);
endCell = findGridCell(endX, endY, grid);
}
let range = getRange(startCell, endCell);
@ -342,12 +337,12 @@ export function addTrough(heights, graph, linePower, config, utils, count, heigh
// get main ridge
function getRange(cur, end) {
const range = [cur];
const p = graph.points;
const p = grid.points;
used[cur] = 1;
while (cur !== end) {
let min = Infinity;
graph.cells.c[cur].forEach(function (e) {
grid.cells.c[cur].forEach(function (e) {
if (used[e]) return;
let diff = (p[end][0] - p[e][0]) ** 2 + (p[end][1] - p[e][1]) ** 2;
if (Math.random() > 0.8) diff = diff / 2;
@ -365,18 +360,17 @@ export function addTrough(heights, graph, linePower, config, utils, count, heigh
}
// add height to ridge and cells around
let queue = range.slice(),
i = 0;
let queue = range.slice(), i = 0;
while (queue.length) {
const frontier = queue.slice();
(queue = []), i++;
frontier.forEach(i => {
heights[i] = lim(heights[i] - h * (Math.random() * 0.3 + 0.85));
newHeights[i] = lim(newHeights[i] - h * (Math.random() * 0.3 + 0.85));
});
h = h ** linePower - 1;
if (h < 2) break;
frontier.forEach(f => {
graph.cells.c[f].forEach(i => {
grid.cells.c[f].forEach(i => {
if (!used[i]) {
queue.push(i);
used[i] = 1;
@ -389,25 +383,25 @@ export function addTrough(heights, graph, linePower, config, utils, count, heigh
range.forEach((cur, d) => {
if (d % 6 !== 0) return;
for (const l of d3.range(i)) {
const min = graph.cells.c[cur][d3.scan(graph.cells.c[cur], (a, b) => heights[a] - heights[b])]; // downhill cell
//debug.append("circle").attr("cx", p[min][0]).attr("cy", p[min][1]).attr("r", 1);
heights[min] = (heights[cur] * 2 + heights[min]) / 3;
const min = grid.cells.c[cur][d3.scan(grid.cells.c[cur], (a, b) => newHeights[a] - newHeights[b])]; // downhill cell
newHeights[min] = (newHeights[cur] * 2 + newHeights[min]) / 3;
cur = min;
}
});
}
return heights;
return newHeights;
}
export function addStrait(heights, graph, config, utils, width, direction = "vertical") {
function addStrait(heights, grid, utils, width, direction = "vertical") {
const { getNumberInRange, findGridCell, P } = utils;
const { graphWidth, graphHeight } = config;
const graphWidth = grid.cellsX;
const graphHeight = grid.cellsY;
heights = new Uint8Array(heights);
width = Math.min(getNumberInRange(width), graph.cellsX / 3);
width = Math.min(getNumberInRange(width), grid.cellsX / 3);
if (width < 1 && P(width)) return heights;
let newHeights = new Uint8Array(heights);
const used = new Uint8Array(heights.length);
const vert = direction === "vertical";
const startX = vert ? Math.floor(Math.random() * graphWidth * 0.4 + graphWidth * 0.3) : 5;
@ -419,18 +413,18 @@ export function addStrait(heights, graph, config, utils, width, direction = "ver
? graphHeight - 5
: Math.floor(graphHeight - startY - graphHeight * 0.1 + Math.random() * graphHeight * 0.2);
const start = findGridCell(startX, startY, graph);
const end = findGridCell(endX, endY, graph);
const start = findGridCell(startX, startY, grid);
const end = findGridCell(endX, endY, grid);
let range = getRange(start, end);
const query = [];
function getRange(cur, end) {
const range = [];
const p = graph.points;
const p = grid.points;
while (cur !== end) {
let min = Infinity;
graph.cells.c[cur].forEach(function (e) {
grid.cells.c[cur].forEach(function (e) {
let diff = (p[end][0] - p[e][0]) ** 2 + (p[end][1] - p[e][1]) ** 2;
if (Math.random() > 0.8) diff = diff / 2;
if (diff < min) {
@ -449,12 +443,12 @@ export function addStrait(heights, graph, config, utils, width, direction = "ver
while (width > 0) {
const exp = 0.9 - step * width;
range.forEach(function (r) {
graph.cells.c[r].forEach(function (e) {
grid.cells.c[r].forEach(function (e) {
if (used[e]) return;
used[e] = 1;
query.push(e);
heights[e] **= exp;
if (heights[e] > 100) heights[e] = 5;
newHeights[e] **= exp;
if (newHeights[e] > 100) newHeights[e] = 5;
});
});
range = query.slice();
@ -462,18 +456,16 @@ export function addStrait(heights, graph, config, utils, width, direction = "ver
width--;
}
return heights;
return newHeights;
}
export function modify(heights, range, add, mult, power, utils) {
const { lim } = utils;
heights = new Uint8Array(heights);
function modify(heights, range, add, mult, power) {
const { lim } = { lim: val => Math.max(0, Math.min(100, val)) };
const min = range === "land" ? 20 : range === "all" ? 0 : +range.split("-")[0];
const max = range === "land" || range === "all" ? 100 : +range.split("-")[1];
const isLand = min === 20;
heights = heights.map(h => {
return heights.map(h => {
if (h < min || h > max) return h;
if (add) h = isLand ? Math.max(h + add, 20) : h + add;
@ -481,33 +473,27 @@ export function modify(heights, range, add, mult, power, utils) {
if (power) h = isLand ? (h - 20) ** power + 20 : h ** power;
return lim(h);
});
return heights;
}
export function smooth(heights, graph, utils, fr = 2, add = 0) {
const { lim, d3 } = utils;
function smooth(heights, grid, utils, fr = 2, add = 0) {
const { d3, lim } = utils;
heights = new Uint8Array(heights);
heights = heights.map((h, i) => {
return heights.map((h, i) => {
const a = [h];
graph.cells.c[i].forEach(c => a.push(heights[c]));
grid.cells.c[i].forEach(c => a.push(heights[c]));
if (fr === 1) return d3.mean(a) + add;
return lim((h * (fr - 1) + d3.mean(a) + add) / fr);
});
return heights;
}
export function mask(heights, graph, config, utils, power = 1) {
const { lim } = utils;
const { graphWidth, graphHeight } = config;
heights = new Uint8Array(heights);
function mask(heights, grid, power = 1) {
const { lim } = { lim: val => Math.max(0, Math.min(100, val)) };
const graphWidth = grid.cellsX;
const graphHeight = grid.cellsY;
const fr = power ? Math.abs(power) : 1;
heights = heights.map((h, i) => {
const [x, y] = graph.points[i];
return heights.map((h, i) => {
const [x, y] = grid.points[i];
const nx = (2 * x) / graphWidth - 1; // [-1, 1], 0 is center
const ny = (2 * y) / graphHeight - 1; // [-1, 1], 0 is center
let distance = (1 - nx ** 2) * (1 - ny ** 2); // 1 is center, 0 is edge
@ -515,19 +501,15 @@ export function mask(heights, graph, config, utils, power = 1) {
const masked = h * distance;
return lim((h * (fr - 1) + masked) / fr);
});
return heights;
}
export function invert(heights, graph, config, utils, count, axes) {
const { P } = utils;
function invert(heights, grid, count, axes) {
const { P } = { P: probability => Math.random() < probability };
if (!P(count)) return heights;
heights = new Uint8Array(heights);
const invertX = axes !== "y";
const invertY = axes !== "x";
const { cellsX, cellsY } = graph;
const { cellsX, cellsY } = grid;
const inverted = heights.map((h, i) => {
const x = i % cellsX;
@ -543,10 +525,9 @@ export function invert(heights, graph, config, utils, count, axes) {
}
function getPointInRange(range, length, utils) {
const { rand } = utils;
const { ERROR, rand } = utils;
if (typeof range !== "string") {
console.error("Range should be a string");
ERROR && console.error("Range should be a string");
return;
}
@ -554,3 +535,20 @@ function getPointInRange(range, length, utils) {
const max = range.split("-")[1] / 100 || min;
return rand(min * length, max * length);
}
function createTypedArray({ maxValue, length }) {
return new Uint8Array(length);
}
// Export utility functions for standalone use
export {
addHill,
addPit,
addRange,
addTrough,
addStrait,
smooth,
modify,
mask,
invert
};