From 18bddd0cec35e16c680fc5d1556ee9895b564172 Mon Sep 17 00:00:00 2001 From: Dobidop <67412288+Dobidop@users.noreply.github.com> Date: Sun, 22 Mar 2026 12:17:56 +0100 Subject: [PATCH] improved tectonic edit UI --- public/modules/ui/tectonic-editor.js | 457 ++++++++++++++++----------- src/index.html | 4 +- 2 files changed, 273 insertions(+), 188 deletions(-) diff --git a/public/modules/ui/tectonic-editor.js b/public/modules/ui/tectonic-editor.js index fe60402e..538c7c93 100644 --- a/public/modules/ui/tectonic-editor.js +++ b/public/modules/ui/tectonic-editor.js @@ -1,10 +1,11 @@ "use strict"; // Tectonic Plate Editor -// Visualizes tectonic plates and allows editing plate properties (type, velocity) -// then regenerates terrain from the modified plate configuration +// Click plates to select & edit, drag arrows to set velocity/direction let tectonicViewMode = "plates"; // "plates" or "heights" +let tectonicPlateColors = []; +let tectonicSelectedPlate = -1; function editTectonics() { if (customization) return tip("Please exit the customization mode first", false, "error"); @@ -15,18 +16,18 @@ function editTectonics() { closeDialogs(".stable"); tectonicViewMode = "plates"; + tectonicSelectedPlate = -1; const plates = window.tectonicGenerator.getPlates(); - const plateIds = window.tectonicMetadata.plateIds; - const plateColors = generatePlateColors(plates.length); + tectonicPlateColors = generatePlateColors(plates.length); - drawPlateOverlay(plateIds, plateColors, plates); - buildPlateList(plates, plateColors); + drawPlateOverlay(); + closePlatePopup(); $("#tectonicEditor").dialog({ title: "Tectonic Plate Editor", resizable: false, - width: "22em", + width: "20em", position: {my: "right top", at: "right-10 top+10", of: "svg"}, close: closeTectonicEditor }); @@ -51,80 +52,80 @@ function generatePlateColors(count) { return colors; } -// Height-to-color function matching FMG's heightmap editor function tectonicHeightColor(h) { if (h < 20) { - // Ocean: deep blue to light blue const t = h / 20; - const r = Math.round(30 + t * 40); - 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)})`; - } + return `rgb(${Math.round(30 + t * 40)},${Math.round(60 + t * 80)},${Math.round(120 + t * 100)})`; } + 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(); 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]; if (pid < 0 || pid >= plates.length) continue; - const points = getGridPolygon(i); if (!points) continue; - overlay.append("polygon") + const selected = pid === tectonicSelectedPlate; + cellGroup.append("polygon") .attr("points", points) - .attr("fill", plateColors[pid]) - .attr("fill-opacity", 0.35) - .attr("stroke", plateColors[pid]) - .attr("stroke-opacity", 0.5) + .attr("fill", colors[pid]) + .attr("fill-opacity", tectonicSelectedPlate === -1 ? 0.35 : (selected ? 0.55 : 0.15)) + .attr("stroke", colors[pid]) + .attr("stroke-opacity", 0.4) .attr("stroke-width", 0.2) .attr("data-plate", pid) - .on("click", function () { - highlightPlate(pid, plateColors); - }); + .on("click", function () { selectPlate(pid); }); } - drawVelocityArrows(overlay, plates, plateIds, plateColors); + // Velocity arrows (draggable) + drawVelocityArrows(overlay, plates, plateIds, colors); } function drawHeightOverlay(heights) { viewbox.select("#tectonicOverlay").remove(); 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); if (!points) continue; - + const c = tectonicHeightColor(heights[i]); overlay.append("polygon") .attr("points", points) - .attr("fill", tectonicHeightColor(heights[i])) + .attr("fill", c) .attr("fill-opacity", 0.85) - .attr("stroke", tectonicHeightColor(heights[i])) + .attr("stroke", c) .attr("stroke-opacity", 0.5) .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 arrowScale = 30; for (const plate of plates) { const centroid = computeGridPlateCentroid(plate.id, plateIds); @@ -132,196 +133,285 @@ function drawVelocityArrows(overlay, plates, plateIds, plateColors) { const [cx, cy] = centroid; const vel = plate.velocity; - const arrowScale = 30; const dx = vel[0] * arrowScale; const dy = -vel[1] * arrowScale; 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") + .attr("class", "velocityLine") + .attr("data-plate", plate.id) .attr("x1", cx).attr("y1", cy) - .attr("x2", cx + dx).attr("y2", cy + dy) - .attr("stroke", plateColors[plate.id]) - .attr("stroke-width", 2) + .attr("x2", tipX).attr("y2", tipY) + .attr("stroke", colors[plate.id]) + .attr("stroke-width", mag < 2 ? 1 : 2) .attr("stroke-opacity", 0.9) + .attr("stroke-dasharray", mag < 2 ? "2,2" : "none") .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") - .attr("x", cx).attr("y", cy - 5) + .attr("x", cx).attr("y", cy - 6) .attr("text-anchor", "middle") .attr("font-size", "8px") - .attr("fill", plateColors[plate.id]) + .attr("fill", colors[plate.id]) .attr("stroke", "#000") .attr("stroke-width", 0.3) .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")) { - const defs = d3.select("svg").select("defs"); - 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 dragVelocityHandle(handle, plate, cx, cy, arrowScale) { + const [mx, my] = d3.mouse(viewbox.node()); + + // Update handle position + d3.select(handle).attr("cx", mx).attr("cy", my); + + // Update arrow line + viewbox.select(`.velocityLine[data-plate="${plate.id}"]`) + .attr("x2", mx).attr("y2", my); + + // Compute new velocity from drag position + const dx = mx - cx; + 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) { let sumX = 0, sumY = 0, count = 0; for (let i = 0; i < plateIds.length; i++) { if (plateIds[i] !== plateId) continue; - const [x, y] = grid.points[i]; - sumX += x; - sumY += y; + sumX += grid.points[i][0]; + sumY += grid.points[i][1]; count++; } if (count === 0) return null; return [sumX / count, sumY / count]; } -function highlightPlate(plateId, plateColors) { - viewbox.select("#tectonicOverlay").selectAll("polygon") +// ---- Plate Selection & Popup ---- + +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 () { - 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}`); - if (row) { - row.scrollIntoView({behavior: "smooth", block: "nearest"}); - row.style.outline = "2px solid " + plateColors[plateId]; - setTimeout(() => row.style.outline = "", 1500); - } + showPlatePopup(plates[plateId]); } -function buildPlateList(plates, plateColors) { - const container = byId("tectonicPlateList"); - container.innerHTML = ""; +function showPlatePopup(plate) { + closePlatePopup(); - const table = document.createElement("table"); - table.style.width = "100%"; - table.style.borderCollapse = "collapse"; - table.style.fontSize = "11px"; + const plateIds = window.tectonicMetadata.plateIds; + const centroid = computeGridPlateCentroid(plate.id, plateIds); + if (!centroid) return; - const header = document.createElement("tr"); - header.innerHTML = ` -