mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2026-03-25 00:27:24 +01:00
feat: relief three.js renderer
This commit is contained in:
parent
7a49098425
commit
7481a2843e
19 changed files with 828 additions and 120 deletions
|
|
@ -180,7 +180,10 @@ async function getMapURL(
|
|||
fullMap = false
|
||||
} = {}
|
||||
) {
|
||||
// Temporarily inject <use> elements so the clone includes relief icon data
|
||||
if (typeof prepareReliefForSave === "function") prepareReliefForSave();
|
||||
const cloneEl = byId("map").cloneNode(true); // clone svg
|
||||
if (typeof restoreReliefAfterSave === "function") restoreReliefAfterSave();
|
||||
cloneEl.id = "fantasyMap";
|
||||
document.body.appendChild(cloneEl);
|
||||
const clone = d3.select(cloneEl);
|
||||
|
|
@ -286,13 +289,13 @@ async function getMapURL(
|
|||
}
|
||||
}
|
||||
|
||||
// add relief icons
|
||||
// add relief icons (from <use> elements – canvas <image> is excluded)
|
||||
if (cloneEl.getElementById("terrain")) {
|
||||
const uniqueElements = new Set();
|
||||
const terrainNodes = cloneEl.getElementById("terrain").childNodes;
|
||||
for (let i = 0; i < terrainNodes.length; i++) {
|
||||
const href = terrainNodes[i].getAttribute("href") || terrainNodes[i].getAttribute("xlink:href");
|
||||
uniqueElements.add(href);
|
||||
const terrainUses = cloneEl.getElementById("terrain").querySelectorAll("use");
|
||||
for (let i = 0; i < terrainUses.length; i++) {
|
||||
const href = terrainUses[i].getAttribute("href") || terrainUses[i].getAttribute("xlink:href");
|
||||
if (href && href.startsWith("#")) uniqueElements.add(href);
|
||||
}
|
||||
|
||||
const defsRelief = svgDefs.getElementById("defs-relief");
|
||||
|
|
@ -424,7 +427,8 @@ async function getMapURL(
|
|||
|
||||
// remove hidden g elements and g elements without children to make downloaded svg smaller in size
|
||||
function removeUnusedElements(clone) {
|
||||
if (!terrain.selectAll("use").size()) clone.select("#defs-relief")?.remove();
|
||||
// Check the clone (not the live terrain) so canvas-mode maps export correctly
|
||||
if (!clone.select("#terrain use").size()) clone.select("#defs-relief")?.remove();
|
||||
|
||||
for (let empty = 1; empty; ) {
|
||||
empty = 0;
|
||||
|
|
@ -583,31 +587,31 @@ function saveGeoJsonZones() {
|
|||
// Handles multiple disconnected components and holes properly
|
||||
function getZonePolygonCoordinates(zoneCells) {
|
||||
const cellsInZone = new Set(zoneCells);
|
||||
const ofSameType = (cellId) => cellsInZone.has(cellId);
|
||||
const ofDifferentType = (cellId) => !cellsInZone.has(cellId);
|
||||
|
||||
const ofSameType = cellId => cellsInZone.has(cellId);
|
||||
const ofDifferentType = cellId => !cellsInZone.has(cellId);
|
||||
|
||||
const checkedCells = new Set();
|
||||
const rings = []; // Array of LinearRings (each ring is an array of coordinates)
|
||||
|
||||
|
||||
// Find all boundary components by tracing each connected region
|
||||
for (const cellId of zoneCells) {
|
||||
if (checkedCells.has(cellId)) continue;
|
||||
|
||||
|
||||
// Check if this cell is on the boundary (has a neighbor outside the zone)
|
||||
const neighbors = cells.c[cellId];
|
||||
const onBorder = neighbors.some(ofDifferentType);
|
||||
if (!onBorder) continue;
|
||||
|
||||
|
||||
// Check if this is an inner lake (hole) - skip if so
|
||||
const feature = pack.features[cells.f[cellId]];
|
||||
if (feature.type === "lake" && feature.shoreline) {
|
||||
if (feature.shoreline.every(ofSameType)) continue;
|
||||
}
|
||||
|
||||
|
||||
// Find a starting vertex that's on the boundary
|
||||
const cellVertices = cells.v[cellId];
|
||||
let startingVertex = null;
|
||||
|
||||
|
||||
for (const vertexId of cellVertices) {
|
||||
const vertexCells = vertices.c[vertexId];
|
||||
if (vertexCells.some(ofDifferentType)) {
|
||||
|
|
@ -615,38 +619,38 @@ function saveGeoJsonZones() {
|
|||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (startingVertex === null) continue;
|
||||
|
||||
|
||||
// Use connectVertices to trace the boundary (reusing existing logic)
|
||||
const vertexChain = connectVertices({
|
||||
vertices,
|
||||
startingVertex,
|
||||
ofSameType,
|
||||
addToChecked: (cellId) => checkedCells.add(cellId),
|
||||
closeRing: false, // We'll close it manually after converting to coordinates
|
||||
addToChecked: cellId => checkedCells.add(cellId),
|
||||
closeRing: false // We'll close it manually after converting to coordinates
|
||||
});
|
||||
|
||||
|
||||
if (vertexChain.length < 3) continue;
|
||||
|
||||
|
||||
// Convert vertex chain to coordinates
|
||||
const coordinates = [];
|
||||
for (const vertexId of vertexChain) {
|
||||
const [x, y] = vertices.p[vertexId];
|
||||
coordinates.push(getCoordinates(x, y, 4));
|
||||
}
|
||||
|
||||
|
||||
// Close the ring (first coordinate = last coordinate)
|
||||
if (coordinates.length > 0) {
|
||||
coordinates.push(coordinates[0]);
|
||||
}
|
||||
|
||||
|
||||
// Only add ring if it has at least 4 positions (minimum for valid LinearRing)
|
||||
if (coordinates.length >= 4) {
|
||||
rings.push(coordinates);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return rings;
|
||||
}
|
||||
|
||||
|
|
@ -656,10 +660,10 @@ function saveGeoJsonZones() {
|
|||
if (zone.hidden || !zone.cells || zone.cells.length === 0) return;
|
||||
|
||||
const rings = getZonePolygonCoordinates(zone.cells);
|
||||
|
||||
|
||||
// Skip if no valid rings were generated
|
||||
if (rings.length === 0) return;
|
||||
|
||||
|
||||
const properties = {
|
||||
id: zone.i,
|
||||
name: zone.name,
|
||||
|
|
@ -667,7 +671,7 @@ function saveGeoJsonZones() {
|
|||
color: zone.color,
|
||||
cells: zone.cells
|
||||
};
|
||||
|
||||
|
||||
// If there's only one ring, use Polygon geometry
|
||||
if (rings.length === 1) {
|
||||
const feature = {
|
||||
|
|
|
|||
|
|
@ -440,7 +440,12 @@ async function parseLoadedData(data, mapVersion) {
|
|||
if (hasChildren(coordinates)) turnOn("toggleCoordinates");
|
||||
if (isVisible(compass) && hasChild(compass, "use")) turnOn("toggleCompass");
|
||||
if (hasChildren(rivers)) turnOn("toggleRivers");
|
||||
if (isVisible(terrain) && hasChildren(terrain)) turnOn("toggleRelief");
|
||||
if (isVisible(terrain) && hasChildren(terrain)) {
|
||||
turnOn("toggleRelief");
|
||||
}
|
||||
// Migrate any legacy SVG <use> elements to canvas rendering
|
||||
// (runs regardless of visibility to handle maps loaded with relief layer off)
|
||||
if (typeof migrateReliefFromSvg === "function") migrateReliefFromSvg();
|
||||
if (hasChildren(relig)) turnOn("toggleReligions");
|
||||
if (hasChildren(cults)) turnOn("toggleCultures");
|
||||
if (hasChildren(statesBody)) turnOn("toggleStates");
|
||||
|
|
|
|||
|
|
@ -32,13 +32,12 @@ async function saveMap(method) {
|
|||
$(this).dialog("close");
|
||||
}
|
||||
},
|
||||
position: { my: "center", at: "center", of: "svg" }
|
||||
position: {my: "center", at: "center", of: "svg"}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function prepareMapData() {
|
||||
|
||||
const date = new Date();
|
||||
const dateString = date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate();
|
||||
const license = "File can be loaded in azgaar.github.io/Fantasy-Map-Generator";
|
||||
|
|
@ -79,7 +78,10 @@ function prepareMapData() {
|
|||
const fonts = JSON.stringify(getUsedFonts(svg.node()));
|
||||
|
||||
// save svg
|
||||
// Temporarily inject <use> elements so the SVG snapshot includes relief icon data
|
||||
if (typeof prepareReliefForSave === "function") prepareReliefForSave();
|
||||
const cloneEl = document.getElementById("map").cloneNode(true);
|
||||
if (typeof restoreReliefAfterSave === "function") restoreReliefAfterSave();
|
||||
|
||||
// reset transform values to default
|
||||
cloneEl.setAttribute("width", graphWidth);
|
||||
|
|
@ -90,8 +92,8 @@ function prepareMapData() {
|
|||
|
||||
const serializedSVG = new XMLSerializer().serializeToString(cloneEl);
|
||||
|
||||
const { spacing, cellsX, cellsY, boundary, points, features, cellsDesired } = grid;
|
||||
const gridGeneral = JSON.stringify({ spacing, cellsX, cellsY, boundary, points, features, cellsDesired });
|
||||
const {spacing, cellsX, cellsY, boundary, points, features, cellsDesired} = grid;
|
||||
const gridGeneral = JSON.stringify({spacing, cellsX, cellsY, boundary, points, features, cellsDesired});
|
||||
const packFeatures = JSON.stringify(pack.features);
|
||||
const cultures = JSON.stringify(pack.cultures);
|
||||
const states = JSON.stringify(pack.states);
|
||||
|
|
@ -165,14 +167,14 @@ function prepareMapData() {
|
|||
|
||||
// save map file to indexedDB
|
||||
async function saveToStorage(mapData, showTip = false) {
|
||||
const blob = new Blob([mapData], { type: "text/plain" });
|
||||
const blob = new Blob([mapData], {type: "text/plain"});
|
||||
await ldb.set("lastMap", blob);
|
||||
showTip && tip("Map is saved to the browser storage", false, "success");
|
||||
}
|
||||
|
||||
// download map file
|
||||
function saveToMachine(mapData, filename) {
|
||||
const blob = new Blob([mapData], { type: "text/plain" });
|
||||
const blob = new Blob([mapData], {type: "text/plain"});
|
||||
const URL = window.URL.createObjectURL(blob);
|
||||
|
||||
const link = document.createElement("a");
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ function clicked() {
|
|||
else if (grand.id === "burgLabels") editBurg();
|
||||
else if (grand.id === "burgIcons") editBurg();
|
||||
else if (parent.id === "ice") editIce(el);
|
||||
else if (parent.id === "terrain") editReliefIcon();
|
||||
else if (grand.id === "markers" || great.id === "markers") editMarker();
|
||||
else if (grand.id === "coastline") editCoastline();
|
||||
else if (grand.id === "lakes") editLake();
|
||||
|
|
@ -544,8 +543,8 @@ function changePickerSpace() {
|
|||
space === "hex"
|
||||
? d3.rgb(this.value)
|
||||
: space === "rgb"
|
||||
? d3.rgb(i[0], i[1], i[2])
|
||||
: d3.hsl(i[0], i[1] / 100, i[2] / 100);
|
||||
? d3.rgb(i[0], i[1], i[2])
|
||||
: d3.hsl(i[0], i[1] / 100, i[2] / 100);
|
||||
|
||||
const hsl = d3.hsl(fill);
|
||||
if (isNaN(hsl.l)) {
|
||||
|
|
|
|||
|
|
@ -129,8 +129,8 @@ function showMapTooltip(point, e, i, g) {
|
|||
parent.id === "burgEmblems"
|
||||
? [pack.burgs, "burg"]
|
||||
: parent.id === "provinceEmblems"
|
||||
? [pack.provinces, "province"]
|
||||
: [pack.states, "state"];
|
||||
? [pack.provinces, "province"]
|
||||
: [pack.states, "state"];
|
||||
const i = +e.target.dataset.i;
|
||||
if (event.shiftKey) highlightEmblemElement(type, g[i]);
|
||||
|
||||
|
|
@ -160,8 +160,6 @@ function showMapTooltip(point, e, i, g) {
|
|||
}
|
||||
}
|
||||
|
||||
if (group === "terrain") return tip("Click to edit the Relief Icon");
|
||||
|
||||
if (subgroup === "burgLabels" || subgroup === "burgIcons") {
|
||||
const burgId = +path[path.length - 10].dataset.id;
|
||||
if (burgId) {
|
||||
|
|
@ -346,7 +344,8 @@ function getFriendlyHeight([x, y]) {
|
|||
function getHeight(h, abs) {
|
||||
const unit = heightUnit.value;
|
||||
let unitRatio = 3.281; // default calculations are in feet
|
||||
if (unit === "m") unitRatio = 1; // if meter
|
||||
if (unit === "m")
|
||||
unitRatio = 1; // if meter
|
||||
else if (unit === "f") unitRatio = 0.5468; // if fathom
|
||||
|
||||
let height = -990;
|
||||
|
|
|
|||
|
|
@ -699,7 +699,16 @@ function toggleCompass(event) {
|
|||
function toggleRelief(event) {
|
||||
if (!layerIsOn("toggleRelief")) {
|
||||
turnButtonOn("toggleRelief");
|
||||
if (!terrain.selectAll("*").size()) drawReliefIcons();
|
||||
if (!terrain.selectAll("*").size()) {
|
||||
drawReliefIcons();
|
||||
} else if (
|
||||
terrain.selectAll("use").size() &&
|
||||
!terrain.select("#terrainCanvasImage").size() &&
|
||||
!terrain.select("#terrainGlFo").size()
|
||||
) {
|
||||
// Legacy SVG use elements present but no canvas/GL render yet – migrate now
|
||||
if (typeof migrateReliefFromSvg === "function") migrateReliefFromSvg();
|
||||
}
|
||||
$("#terrain").fadeIn();
|
||||
if (event && isCtrlClick(event)) editStyle("terrain");
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -4,8 +4,16 @@ function editReliefIcon() {
|
|||
closeDialogs(".stable");
|
||||
if (!layerIsOn("toggleRelief")) toggleRelief();
|
||||
|
||||
// Switch from canvas image to editable SVG <use> elements
|
||||
if (typeof enterReliefSvgEditMode === "function") enterReliefSvgEditMode();
|
||||
|
||||
terrain.selectAll("use").call(d3.drag().on("drag", dragReliefIcon)).classed("draggable", true);
|
||||
elSelected = d3.select(d3.event.target);
|
||||
|
||||
// When called from the Tools button there is no d3 click event; fall back to the first <use>.
|
||||
// When called from a map click, prefer the actual clicked element if it is a <use>.
|
||||
const clickTarget = d3.event && d3.event.target;
|
||||
const useTarget = clickTarget && clickTarget.tagName === "use" ? clickTarget : terrain.select("use").node();
|
||||
elSelected = d3.select(useTarget);
|
||||
|
||||
restoreEditMode();
|
||||
updateReliefIconSelected();
|
||||
|
|
@ -59,6 +67,7 @@ function editReliefIcon() {
|
|||
function updateReliefIconSelected() {
|
||||
const type = elSelected.attr("href") || elSelected.attr("data-type");
|
||||
const button = reliefIconsDiv.querySelector("svg[data-type='" + type + "']");
|
||||
if (!button) return;
|
||||
|
||||
reliefIconsDiv.querySelectorAll("svg.pressed").forEach(b => b.classList.remove("pressed"));
|
||||
button.classList.add("pressed");
|
||||
|
|
@ -260,7 +269,9 @@ function editReliefIcon() {
|
|||
const type = reliefIconsDiv.querySelector("svg.pressed")?.dataset.type;
|
||||
selection = type ? terrain.selectAll("use[href='" + type + "']") : terrain.selectAll("use");
|
||||
const size = selection.size();
|
||||
alertMessage.innerHTML = type ? `Are you sure you want to remove all ${type} icons (${size})?` : `Are you sure you want to remove all icons (${size})?`;
|
||||
alertMessage.innerHTML = type
|
||||
? `Are you sure you want to remove all ${type} icons (${size})?`
|
||||
: `Are you sure you want to remove all icons (${size})?`;
|
||||
}
|
||||
|
||||
$("#alert").dialog({
|
||||
|
|
@ -284,5 +295,7 @@ function editReliefIcon() {
|
|||
removeCircle();
|
||||
unselect();
|
||||
clearMainTip();
|
||||
// Read back edits and switch terrain to canvas rendering
|
||||
if (typeof exitReliefSvgEditMode === "function") exitReliefSvgEditMode();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue