improved tectonic edit UI

This commit is contained in:
Dobidop 2026-03-22 12:17:56 +01:00
parent ed9a0ba739
commit d2b9dd119b
2 changed files with 273 additions and 188 deletions

View file

@ -1,10 +1,11 @@
"use strict"; "use strict";
// Tectonic Plate Editor // Tectonic Plate Editor
// Visualizes tectonic plates and allows editing plate properties (type, velocity) // Click plates to select & edit, drag arrows to set velocity/direction
// then regenerates terrain from the modified plate configuration
let tectonicViewMode = "plates"; // "plates" or "heights" let tectonicViewMode = "plates"; // "plates" or "heights"
let tectonicPlateColors = [];
let tectonicSelectedPlate = -1;
function editTectonics() { function editTectonics() {
if (customization) return tip("Please exit the customization mode first", false, "error"); if (customization) return tip("Please exit the customization mode first", false, "error");
@ -15,18 +16,18 @@ function editTectonics() {
closeDialogs(".stable"); closeDialogs(".stable");
tectonicViewMode = "plates"; tectonicViewMode = "plates";
tectonicSelectedPlate = -1;
const plates = window.tectonicGenerator.getPlates(); const plates = window.tectonicGenerator.getPlates();
const plateIds = window.tectonicMetadata.plateIds; tectonicPlateColors = generatePlateColors(plates.length);
const plateColors = generatePlateColors(plates.length);
drawPlateOverlay(plateIds, plateColors, plates); drawPlateOverlay();
buildPlateList(plates, plateColors); closePlatePopup();
$("#tectonicEditor").dialog({ $("#tectonicEditor").dialog({
title: "Tectonic Plate Editor", title: "Tectonic Plate Editor",
resizable: false, resizable: false,
width: "22em", width: "20em",
position: {my: "right top", at: "right-10 top+10", of: "svg"}, position: {my: "right top", at: "right-10 top+10", of: "svg"},
close: closeTectonicEditor close: closeTectonicEditor
}); });
@ -51,80 +52,80 @@ function generatePlateColors(count) {
return colors; return colors;
} }
// Height-to-color function matching FMG's heightmap editor
function tectonicHeightColor(h) { function tectonicHeightColor(h) {
if (h < 20) { if (h < 20) {
// Ocean: deep blue to light blue
const t = h / 20; const t = h / 20;
const r = Math.round(30 + t * 40); return `rgb(${Math.round(30 + t * 40)},${Math.round(60 + t * 80)},${Math.round(120 + t * 100)})`;
const g = Math.round(60 + t * 80);
const b = Math.round(120 + t * 100);
return `rgb(${r},${g},${b})`;
} else {
// Land: green to brown to white
const t = (h - 20) / 80;
if (t < 0.3) {
const s = t / 0.3;
return `rgb(${Math.round(80 + s * 60)},${Math.round(160 + s * 40)},${Math.round(60 + s * 20)})`;
} else if (t < 0.7) {
const s = (t - 0.3) / 0.4;
return `rgb(${Math.round(140 + s * 60)},${Math.round(200 - s * 80)},${Math.round(80 - s * 40)})`;
} else {
const s = (t - 0.7) / 0.3;
return `rgb(${Math.round(200 + s * 55)},${Math.round(120 + s * 135)},${Math.round(40 + s * 215)})`;
}
} }
const t = (h - 20) / 80;
if (t < 0.3) {
const s = t / 0.3;
return `rgb(${Math.round(80 + s * 60)},${Math.round(160 + s * 40)},${Math.round(60 + s * 20)})`;
}
if (t < 0.7) {
const s = (t - 0.3) / 0.4;
return `rgb(${Math.round(140 + s * 60)},${Math.round(200 - s * 80)},${Math.round(80 - s * 40)})`;
}
const s = (t - 0.7) / 0.3;
return `rgb(${Math.round(200 + s * 55)},${Math.round(120 + s * 135)},${Math.round(40 + s * 215)})`;
} }
function drawPlateOverlay(plateIds, plateColors, plates) { // ---- Overlay Drawing ----
function drawPlateOverlay() {
const plates = window.tectonicGenerator.getPlates();
const plateIds = window.tectonicMetadata.plateIds;
const colors = tectonicPlateColors;
viewbox.select("#tectonicOverlay").remove(); viewbox.select("#tectonicOverlay").remove();
const overlay = viewbox.insert("g", "#terrs").attr("id", "tectonicOverlay"); const overlay = viewbox.insert("g", "#terrs").attr("id", "tectonicOverlay");
const numCells = plateIds.length;
for (let i = 0; i < numCells; i++) { // Cell polygons
const cellGroup = overlay.append("g").attr("id", "plateCells");
for (let i = 0; i < plateIds.length; i++) {
const pid = plateIds[i]; const pid = plateIds[i];
if (pid < 0 || pid >= plates.length) continue; if (pid < 0 || pid >= plates.length) continue;
const points = getGridPolygon(i); const points = getGridPolygon(i);
if (!points) continue; if (!points) continue;
overlay.append("polygon") const selected = pid === tectonicSelectedPlate;
cellGroup.append("polygon")
.attr("points", points) .attr("points", points)
.attr("fill", plateColors[pid]) .attr("fill", colors[pid])
.attr("fill-opacity", 0.35) .attr("fill-opacity", tectonicSelectedPlate === -1 ? 0.35 : (selected ? 0.55 : 0.15))
.attr("stroke", plateColors[pid]) .attr("stroke", colors[pid])
.attr("stroke-opacity", 0.5) .attr("stroke-opacity", 0.4)
.attr("stroke-width", 0.2) .attr("stroke-width", 0.2)
.attr("data-plate", pid) .attr("data-plate", pid)
.on("click", function () { .on("click", function () { selectPlate(pid); });
highlightPlate(pid, plateColors);
});
} }
drawVelocityArrows(overlay, plates, plateIds, plateColors); // Velocity arrows (draggable)
drawVelocityArrows(overlay, plates, plateIds, colors);
} }
function drawHeightOverlay(heights) { function drawHeightOverlay(heights) {
viewbox.select("#tectonicOverlay").remove(); viewbox.select("#tectonicOverlay").remove();
const overlay = viewbox.insert("g", "#terrs").attr("id", "tectonicOverlay"); const overlay = viewbox.insert("g", "#terrs").attr("id", "tectonicOverlay");
const numCells = heights.length;
for (let i = 0; i < numCells; i++) { for (let i = 0; i < heights.length; i++) {
const points = getGridPolygon(i); const points = getGridPolygon(i);
if (!points) continue; if (!points) continue;
const c = tectonicHeightColor(heights[i]);
overlay.append("polygon") overlay.append("polygon")
.attr("points", points) .attr("points", points)
.attr("fill", tectonicHeightColor(heights[i])) .attr("fill", c)
.attr("fill-opacity", 0.85) .attr("fill-opacity", 0.85)
.attr("stroke", tectonicHeightColor(heights[i])) .attr("stroke", c)
.attr("stroke-opacity", 0.5) .attr("stroke-opacity", 0.5)
.attr("stroke-width", 0.1); .attr("stroke-width", 0.1);
} }
} }
function drawVelocityArrows(overlay, plates, plateIds, plateColors) { function drawVelocityArrows(overlay, plates, plateIds, colors) {
ensureArrowheadMarker();
const arrowGroup = overlay.append("g").attr("id", "velocityArrows"); const arrowGroup = overlay.append("g").attr("id", "velocityArrows");
const arrowScale = 30;
for (const plate of plates) { for (const plate of plates) {
const centroid = computeGridPlateCentroid(plate.id, plateIds); const centroid = computeGridPlateCentroid(plate.id, plateIds);
@ -132,196 +133,285 @@ function drawVelocityArrows(overlay, plates, plateIds, plateColors) {
const [cx, cy] = centroid; const [cx, cy] = centroid;
const vel = plate.velocity; const vel = plate.velocity;
const arrowScale = 30;
const dx = vel[0] * arrowScale; const dx = vel[0] * arrowScale;
const dy = -vel[1] * arrowScale; const dy = -vel[1] * arrowScale;
const mag = Math.sqrt(dx * dx + dy * dy); const mag = Math.sqrt(dx * dx + dy * dy);
if (mag < 2) continue;
const tipX = cx + dx;
const tipY = cy + dy;
// Arrow line
arrowGroup.append("line") arrowGroup.append("line")
.attr("class", "velocityLine")
.attr("data-plate", plate.id)
.attr("x1", cx).attr("y1", cy) .attr("x1", cx).attr("y1", cy)
.attr("x2", cx + dx).attr("y2", cy + dy) .attr("x2", tipX).attr("y2", tipY)
.attr("stroke", plateColors[plate.id]) .attr("stroke", colors[plate.id])
.attr("stroke-width", 2) .attr("stroke-width", mag < 2 ? 1 : 2)
.attr("stroke-opacity", 0.9) .attr("stroke-opacity", 0.9)
.attr("stroke-dasharray", mag < 2 ? "2,2" : "none")
.attr("marker-end", "url(#tectonicArrowhead)"); .attr("marker-end", "url(#tectonicArrowhead)");
// Draggable handle at arrow tip
arrowGroup.append("circle")
.attr("class", "velocityHandle")
.attr("data-plate", plate.id)
.attr("cx", tipX).attr("cy", tipY)
.attr("r", 5)
.attr("fill", colors[plate.id])
.attr("fill-opacity", 0.7)
.attr("stroke", "#fff")
.attr("stroke-width", 1)
.attr("cursor", "grab")
.call(d3.drag()
.on("start", function () { d3.select(this).attr("cursor", "grabbing"); })
.on("drag", function () { dragVelocityHandle(this, plate, cx, cy, arrowScale); })
.on("end", function () { d3.select(this).attr("cursor", "grab"); })
);
// Plate label
arrowGroup.append("text") arrowGroup.append("text")
.attr("x", cx).attr("y", cy - 5) .attr("x", cx).attr("y", cy - 6)
.attr("text-anchor", "middle") .attr("text-anchor", "middle")
.attr("font-size", "8px") .attr("font-size", "8px")
.attr("fill", plateColors[plate.id]) .attr("fill", colors[plate.id])
.attr("stroke", "#000") .attr("stroke", "#000")
.attr("stroke-width", 0.3) .attr("stroke-width", 0.3)
.attr("paint-order", "stroke") .attr("paint-order", "stroke")
.text(`P${plate.id}`); .attr("cursor", "pointer")
.text(`P${plate.id}`)
.on("click", function () { selectPlate(plate.id); });
} }
}
if (!document.getElementById("tectonicArrowhead")) { function dragVelocityHandle(handle, plate, cx, cy, arrowScale) {
const defs = d3.select("svg").select("defs"); const [mx, my] = d3.mouse(viewbox.node());
defs.append("marker")
.attr("id", "tectonicArrowhead") // Update handle position
.attr("viewBox", "0 0 10 10") d3.select(handle).attr("cx", mx).attr("cy", my);
.attr("refX", 8).attr("refY", 5)
.attr("markerWidth", 6).attr("markerHeight", 6) // Update arrow line
.attr("orient", "auto-start-reverse") viewbox.select(`.velocityLine[data-plate="${plate.id}"]`)
.append("path") .attr("x2", mx).attr("y2", my);
.attr("d", "M 0 0 L 10 5 L 0 10 z")
.attr("fill", "#fff") // Compute new velocity from drag position
.attr("stroke", "#333") const dx = mx - cx;
.attr("stroke-width", 0.5); const dy = my - cy;
plate.velocity[0] = dx / arrowScale;
plate.velocity[1] = -dy / arrowScale; // flip Y
plate.velocity[2] = 0;
// Update popup if this plate is selected
if (tectonicSelectedPlate === plate.id) {
updatePopupValues(plate);
} }
} }
function ensureArrowheadMarker() {
if (document.getElementById("tectonicArrowhead")) return;
d3.select("svg").select("defs").append("marker")
.attr("id", "tectonicArrowhead")
.attr("viewBox", "0 0 10 10")
.attr("refX", 8).attr("refY", 5)
.attr("markerWidth", 6).attr("markerHeight", 6)
.attr("orient", "auto-start-reverse")
.append("path")
.attr("d", "M 0 0 L 10 5 L 0 10 z")
.attr("fill", "#fff")
.attr("stroke", "#333")
.attr("stroke-width", 0.5);
}
function computeGridPlateCentroid(plateId, plateIds) { function computeGridPlateCentroid(plateId, plateIds) {
let sumX = 0, sumY = 0, count = 0; let sumX = 0, sumY = 0, count = 0;
for (let i = 0; i < plateIds.length; i++) { for (let i = 0; i < plateIds.length; i++) {
if (plateIds[i] !== plateId) continue; if (plateIds[i] !== plateId) continue;
const [x, y] = grid.points[i]; sumX += grid.points[i][0];
sumX += x; sumY += grid.points[i][1];
sumY += y;
count++; count++;
} }
if (count === 0) return null; if (count === 0) return null;
return [sumX / count, sumY / count]; return [sumX / count, sumY / count];
} }
function highlightPlate(plateId, plateColors) { // ---- Plate Selection & Popup ----
viewbox.select("#tectonicOverlay").selectAll("polygon")
function selectPlate(plateId) {
const plates = window.tectonicGenerator.getPlates();
if (plateId < 0 || plateId >= plates.length) return;
tectonicSelectedPlate = plateId;
// Update overlay opacity to highlight selected plate
viewbox.select("#plateCells").selectAll("polygon")
.attr("fill-opacity", function () { .attr("fill-opacity", function () {
return +this.getAttribute("data-plate") === plateId ? 0.6 : 0.15; return +this.getAttribute("data-plate") === plateId ? 0.55 : 0.15;
}); });
const row = byId(`tectonicPlate_${plateId}`); showPlatePopup(plates[plateId]);
if (row) {
row.scrollIntoView({behavior: "smooth", block: "nearest"});
row.style.outline = "2px solid " + plateColors[plateId];
setTimeout(() => row.style.outline = "", 1500);
}
} }
function buildPlateList(plates, plateColors) { function showPlatePopup(plate) {
const container = byId("tectonicPlateList"); closePlatePopup();
container.innerHTML = "";
const table = document.createElement("table"); const plateIds = window.tectonicMetadata.plateIds;
table.style.width = "100%"; const centroid = computeGridPlateCentroid(plate.id, plateIds);
table.style.borderCollapse = "collapse"; if (!centroid) return;
table.style.fontSize = "11px";
const header = document.createElement("tr"); // Count cells
header.innerHTML = ` let cellCount = 0;
<th style="width:30px">ID</th> for (let i = 0; i < plateIds.length; i++) {
<th style="width:60px">Type</th> if (plateIds[i] === plate.id) cellCount++;
<th>Velocity</th> }
<th style="width:50px">Dir</th> const pct = (cellCount / plateIds.length * 100).toFixed(1);
const vel = plate.velocity;
const speed = Math.sqrt(vel[0] ** 2 + vel[1] ** 2 + vel[2] ** 2);
const dirDeg = Math.round(Math.atan2(-vel[1], vel[0]) * 180 / Math.PI);
const color = tectonicPlateColors[plate.id];
const popup = document.createElement("div");
popup.id = "tectonicPlatePopup";
popup.style.cssText = `
position: absolute; z-index: 1000;
background: rgba(30,30,30,0.95); color: #eee;
border: 2px solid ${color}; border-radius: 6px;
padding: 10px 14px; font-size: 12px;
min-width: 180px; pointer-events: auto;
box-shadow: 0 4px 16px rgba(0,0,0,0.5);
`; `;
table.appendChild(header);
for (const plate of plates) { popup.innerHTML = `
const row = document.createElement("tr"); <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:6px">
row.id = `tectonicPlate_${plate.id}`; <strong style="color:${color}">Plate ${plate.id}</strong>
row.style.borderBottom = "1px solid #444"; <span style="font-size:10px;color:#999">${cellCount} cells (${pct}%)</span>
row.style.cursor = "pointer"; </div>
<div style="margin-bottom:6px">
<label style="font-size:11px">Type: </label>
<select id="popupPlateType" style="font-size:11px;margin-left:4px">
<option value="continental" ${!plate.isOceanic ? "selected" : ""}>Continental</option>
<option value="oceanic" ${plate.isOceanic ? "selected" : ""}>Oceanic</option>
</select>
</div>
<div style="margin-bottom:6px">
<label style="font-size:11px">Speed: </label>
<input id="popupPlateSpeed" type="range" min="0" max="1.5" step="0.05" value="${speed.toFixed(2)}"
style="width:80px;vertical-align:middle">
<span id="popupSpeedLabel" style="font-size:10px">${speed.toFixed(2)}</span>
</div>
<div style="margin-bottom:8px">
<label style="font-size:11px">Direction: </label>
<input id="popupPlateDir" type="range" min="-180" max="180" step="5" value="${dirDeg}"
style="width:80px;vertical-align:middle">
<span id="popupDirLabel" style="font-size:10px">${dirDeg}&deg;</span>
</div>
<div style="font-size:10px;color:#888;text-align:center">
Drag the arrow on the map to set velocity
</div>
`;
const vel = plate.velocity; document.body.appendChild(popup);
const speed = Math.sqrt(vel[0] * vel[0] + vel[1] * vel[1] + vel[2] * vel[2]).toFixed(2);
const dirDeg = Math.round(Math.atan2(-vel[1], vel[0]) * 180 / Math.PI);
row.innerHTML = ` // Position popup near the plate centroid but in screen coords
<td style="text-align:center"> const svgRect = document.querySelector("svg").getBoundingClientRect();
<span style="display:inline-block;width:12px;height:12px;background:${plateColors[plate.id]};border-radius:2px;vertical-align:middle"></span> const svgEl = document.querySelector("svg");
${plate.id} const ctm = svgEl.getScreenCTM();
</td> const screenX = centroid[0] * ctm.a + ctm.e;
<td> const screenY = centroid[1] * ctm.d + ctm.f;
<select data-plate="${plate.id}" class="plateTypeSelect" style="font-size:10px;width:100%">
<option value="continental" ${!plate.isOceanic ? "selected" : ""}>Land</option>
<option value="oceanic" ${plate.isOceanic ? "selected" : ""}>Ocean</option>
</select>
</td>
<td style="text-align:center">
<input type="range" data-plate="${plate.id}" class="plateSpeedRange"
min="0" max="1.5" step="0.05" value="${speed}"
style="width:60px;vertical-align:middle">
<span class="plateSpeedLabel" style="font-size:9px">${speed}</span>
</td>
<td style="text-align:center">
<input type="number" data-plate="${plate.id}" class="plateDirInput"
min="-180" max="180" step="15" value="${dirDeg}"
style="width:45px;font-size:10px">
</td>
`;
row.addEventListener("click", (e) => { popup.style.left = Math.min(screenX + 20, window.innerWidth - 220) + "px";
if (e.target.tagName === "SELECT" || e.target.tagName === "INPUT") return; popup.style.top = Math.max(screenY - 60, 10) + "px";
highlightPlate(plate.id, plateColors);
});
table.appendChild(row); // Listeners
} byId("popupPlateType").addEventListener("change", function () {
plate.isOceanic = this.value === "oceanic";
container.appendChild(table);
container.querySelectorAll(".plateTypeSelect").forEach(select => {
select.addEventListener("change", function () {
const pid = +this.getAttribute("data-plate");
plates[pid].isOceanic = this.value === "oceanic";
});
}); });
container.querySelectorAll(".plateSpeedRange").forEach(slider => { byId("popupPlateSpeed").addEventListener("input", function () {
slider.addEventListener("input", function () { const newSpeed = +this.value;
const pid = +this.getAttribute("data-plate"); byId("popupSpeedLabel").textContent = newSpeed.toFixed(2);
const plate = plates[pid]; const oldSpeed = Math.sqrt(plate.velocity[0] ** 2 + plate.velocity[1] ** 2 + plate.velocity[2] ** 2);
const newSpeed = +this.value; if (oldSpeed > 0.001) {
this.parentElement.querySelector(".plateSpeedLabel").textContent = newSpeed.toFixed(2); const s = newSpeed / oldSpeed;
plate.velocity[0] *= s;
const oldSpeed = Math.sqrt(plate.velocity[0] ** 2 + plate.velocity[1] ** 2 + plate.velocity[2] ** 2); plate.velocity[1] *= s;
if (oldSpeed > 0.001) { plate.velocity[2] *= s;
const scale = newSpeed / oldSpeed; } else {
plate.velocity[0] *= scale; plate.velocity[0] = newSpeed;
plate.velocity[1] *= scale; plate.velocity[1] = 0;
plate.velocity[2] *= scale;
} else {
plate.velocity[0] = newSpeed;
plate.velocity[1] = 0;
plate.velocity[2] = 0;
}
});
});
container.querySelectorAll(".plateDirInput").forEach(input => {
input.addEventListener("change", function () {
const pid = +this.getAttribute("data-plate");
const plate = plates[pid];
const dirRad = (+this.value) * Math.PI / 180;
const speed = Math.sqrt(plate.velocity[0] ** 2 + plate.velocity[1] ** 2 + plate.velocity[2] ** 2);
plate.velocity[0] = Math.cos(dirRad) * speed;
plate.velocity[1] = -Math.sin(dirRad) * speed;
plate.velocity[2] = 0; plate.velocity[2] = 0;
}); }
redrawArrowForPlate(plate);
});
byId("popupPlateDir").addEventListener("input", function () {
const deg = +this.value;
byId("popupDirLabel").textContent = deg + "\u00B0";
const speed = Math.sqrt(plate.velocity[0] ** 2 + plate.velocity[1] ** 2 + plate.velocity[2] ** 2);
const rad = deg * Math.PI / 180;
plate.velocity[0] = Math.cos(rad) * speed;
plate.velocity[1] = -Math.sin(rad) * speed;
plate.velocity[2] = 0;
redrawArrowForPlate(plate);
}); });
} }
function updatePopupValues(plate) {
const speedEl = byId("popupPlateSpeed");
const dirEl = byId("popupPlateDir");
if (!speedEl || !dirEl) return;
const vel = plate.velocity;
const speed = Math.sqrt(vel[0] ** 2 + vel[1] ** 2 + vel[2] ** 2);
const dirDeg = Math.round(Math.atan2(-vel[1], vel[0]) * 180 / Math.PI);
speedEl.value = speed.toFixed(2);
byId("popupSpeedLabel").textContent = speed.toFixed(2);
dirEl.value = dirDeg;
byId("popupDirLabel").textContent = dirDeg + "\u00B0";
}
function redrawArrowForPlate(plate) {
const plateIds = window.tectonicMetadata.plateIds;
const centroid = computeGridPlateCentroid(plate.id, plateIds);
if (!centroid) return;
const arrowScale = 30;
const [cx, cy] = centroid;
const dx = plate.velocity[0] * arrowScale;
const dy = -plate.velocity[1] * arrowScale;
const tipX = cx + dx;
const tipY = cy + dy;
viewbox.select(`.velocityLine[data-plate="${plate.id}"]`)
.attr("x2", tipX).attr("y2", tipY);
viewbox.select(`.velocityHandle[data-plate="${plate.id}"]`)
.attr("cx", tipX).attr("cy", tipY);
}
function closePlatePopup() {
const popup = byId("tectonicPlatePopup");
if (popup) popup.remove();
}
// ---- Actions ----
function regenerateFromEditor() { function regenerateFromEditor() {
const generator = window.tectonicGenerator; const generator = window.tectonicGenerator;
if (!generator) return tip("No tectonic generator available", false, "error"); if (!generator) return tip("No tectonic generator available", false, "error");
tip("Regenerating terrain from edited plates...", true, "warn"); tip("Regenerating terrain preview...", true, "warn");
closePlatePopup();
setTimeout(() => { setTimeout(() => {
try { try {
const result = generator.regenerate(); const result = generator.regenerate();
// Update grid heights
grid.cells.h = result.heights; grid.cells.h = result.heights;
window.tectonicMetadata = result.metadata; window.tectonicMetadata = result.metadata;
// Show the regenerated heightmap as a visual overlay
tectonicViewMode = "heights"; tectonicViewMode = "heights";
drawHeightOverlay(result.heights); drawHeightOverlay(result.heights);
// Log changes for debugging
let water = 0, land = 0, minH = 100, maxH = 0; let water = 0, land = 0, minH = 100, maxH = 0;
for (let i = 0; i < result.heights.length; i++) { for (let i = 0; i < result.heights.length; i++) {
const h = result.heights[i]; const h = result.heights[i];
@ -329,9 +419,9 @@ function regenerateFromEditor() {
if (h < minH) minH = h; if (h < minH) minH = h;
if (h > maxH) maxH = h; if (h > maxH) maxH = h;
} }
console.log(`Tectonic regeneration: ${land} land (${(land / result.heights.length * 100).toFixed(1)}%), heights ${minH}-${maxH}`); console.log(`Tectonic regen: ${land} land (${(land / result.heights.length * 100).toFixed(1)}%), heights ${minH}-${maxH}`);
tip("Terrain regenerated. Click 'Apply to Map' to regenerate the full map.", true, "success"); tip("Preview ready. Click 'Apply to Map' to rebuild.", true, "success");
} catch (e) { } catch (e) {
console.error("Tectonic regeneration failed:", e); console.error("Tectonic regeneration failed:", e);
tip("Regeneration failed: " + e.message, false, "error"); tip("Regeneration failed: " + e.message, false, "error");
@ -342,7 +432,7 @@ function regenerateFromEditor() {
function applyToMap() { function applyToMap() {
if (!window.tectonicGenerator) return tip("No tectonic generator available", false, "error"); if (!window.tectonicGenerator) return tip("No tectonic generator available", false, "error");
// Close the editor overlay closePlatePopup();
closeTectonicEditor(); closeTectonicEditor();
$("#tectonicEditor").dialog("close"); $("#tectonicEditor").dialog("close");
@ -350,8 +440,6 @@ function applyToMap() {
setTimeout(() => { setTimeout(() => {
try { try {
// grid.cells.h is already set by regenerateFromEditor
// Run the full downstream pipeline WITHOUT regenerating the heightmap
undraw(); undraw();
pack = {}; pack = {};
@ -415,21 +503,16 @@ function applyToMap() {
} }
function togglePlateOverlay() { function togglePlateOverlay() {
const overlay = viewbox.select("#tectonicOverlay");
if (tectonicViewMode === "heights") { if (tectonicViewMode === "heights") {
// Switch back to plate view
tectonicViewMode = "plates"; tectonicViewMode = "plates";
const plates = window.tectonicGenerator.getPlates(); tectonicSelectedPlate = -1;
const plateColors = generatePlateColors(plates.length); drawPlateOverlay();
drawPlateOverlay(window.tectonicMetadata.plateIds, plateColors, plates);
return; return;
} }
const overlay = viewbox.select("#tectonicOverlay");
if (overlay.empty()) { if (overlay.empty()) {
const plates = window.tectonicGenerator.getPlates(); drawPlateOverlay();
const plateColors = generatePlateColors(plates.length);
drawPlateOverlay(window.tectonicMetadata.plateIds, plateColors, plates);
} else { } else {
const visible = overlay.style("display") !== "none"; const visible = overlay.style("display") !== "none";
overlay.style("display", visible ? "none" : null); overlay.style("display", visible ? "none" : null);
@ -437,7 +520,9 @@ function togglePlateOverlay() {
} }
function closeTectonicEditor() { function closeTectonicEditor() {
closePlatePopup();
viewbox.select("#tectonicOverlay").remove(); viewbox.select("#tectonicOverlay").remove();
d3.select("#tectonicArrowhead").remove(); d3.select("#tectonicArrowhead").remove();
tectonicViewMode = "plates"; tectonicViewMode = "plates";
tectonicSelectedPlate = -1;
} }

View file

@ -4106,8 +4106,8 @@
</div> </div>
<div id="tectonicEditor" class="dialog stable" style="display: none"> <div id="tectonicEditor" class="dialog stable" style="display: none">
<div id="tectonicPlateList" style="max-height: 300px; overflow-y: auto"></div> <p style="font-size:11px;margin:0 0 6px">Click a plate to edit. Drag arrows to set velocity.</p>
<div style="margin-top: 8px"> <div style="margin-top: 4px">
<button id="tectonicRegenerate" data-tip="Regenerate terrain preview from edited plates">Regenerate Preview</button> <button id="tectonicRegenerate" data-tip="Regenerate terrain preview from edited plates">Regenerate Preview</button>
<button id="tectonicApplyMap" data-tip="Apply changes and regenerate the full map">Apply to Map</button> <button id="tectonicApplyMap" data-tip="Apply changes and regenerate the full map">Apply to Map</button>
</div> </div>