mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2026-03-25 08:37:23 +01:00
Added tectonic painter editor
This commit is contained in:
parent
18bddd0cec
commit
2a341346ab
3 changed files with 185 additions and 33 deletions
|
|
@ -2,10 +2,13 @@
|
|||
|
||||
// Tectonic Plate Editor
|
||||
// Click plates to select & edit, drag arrows to set velocity/direction
|
||||
// Paint mode: brush to reassign cells between plates
|
||||
|
||||
let tectonicViewMode = "plates"; // "plates" or "heights"
|
||||
let tectonicPlateColors = [];
|
||||
let tectonicSelectedPlate = -1;
|
||||
let tectonicPaintMode = false;
|
||||
let tectonicBrushRadius = 10;
|
||||
|
||||
function editTectonics() {
|
||||
if (customization) return tip("Please exit the customization mode first", false, "error");
|
||||
|
|
@ -17,12 +20,14 @@ function editTectonics() {
|
|||
closeDialogs(".stable");
|
||||
tectonicViewMode = "plates";
|
||||
tectonicSelectedPlate = -1;
|
||||
tectonicPaintMode = false;
|
||||
|
||||
const plates = window.tectonicGenerator.getPlates();
|
||||
tectonicPlateColors = generatePlateColors(plates.length);
|
||||
|
||||
drawPlateOverlay();
|
||||
closePlatePopup();
|
||||
updatePaintButtonState();
|
||||
|
||||
$("#tectonicEditor").dialog({
|
||||
title: "Tectonic Plate Editor",
|
||||
|
|
@ -38,9 +43,16 @@ function editTectonics() {
|
|||
byId("tectonicRegenerate").addEventListener("click", regenerateFromEditor);
|
||||
byId("tectonicToggleOverlay").addEventListener("click", togglePlateOverlay);
|
||||
byId("tectonicApplyMap").addEventListener("click", applyToMap);
|
||||
byId("tectonicPaintToggle").addEventListener("click", togglePaintMode);
|
||||
byId("tectonicBrushSize").addEventListener("input", function () {
|
||||
tectonicBrushRadius = +this.value;
|
||||
byId("tectonicBrushSizeLabel").textContent = this.value;
|
||||
});
|
||||
byId("tectonicClose").addEventListener("click", () => $("#tectonicEditor").dialog("close"));
|
||||
}
|
||||
|
||||
// ---- Color Utilities ----
|
||||
|
||||
function generatePlateColors(count) {
|
||||
const colors = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
|
|
@ -80,7 +92,6 @@ function drawPlateOverlay() {
|
|||
viewbox.select("#tectonicOverlay").remove();
|
||||
const overlay = viewbox.insert("g", "#terrs").attr("id", "tectonicOverlay");
|
||||
|
||||
// Cell polygons
|
||||
const cellGroup = overlay.append("g").attr("id", "plateCells");
|
||||
for (let i = 0; i < plateIds.length; i++) {
|
||||
const pid = plateIds[i];
|
||||
|
|
@ -97,10 +108,12 @@ function drawPlateOverlay() {
|
|||
.attr("stroke-opacity", 0.4)
|
||||
.attr("stroke-width", 0.2)
|
||||
.attr("data-plate", pid)
|
||||
.on("click", function () { selectPlate(pid); });
|
||||
.attr("data-cell", i)
|
||||
.on("click", function () {
|
||||
if (!tectonicPaintMode) selectPlate(pid);
|
||||
});
|
||||
}
|
||||
|
||||
// Velocity arrows (draggable)
|
||||
drawVelocityArrows(overlay, plates, plateIds, colors);
|
||||
}
|
||||
|
||||
|
|
@ -136,11 +149,9 @@ function drawVelocityArrows(overlay, plates, plateIds, colors) {
|
|||
const dx = vel[0] * arrowScale;
|
||||
const dy = -vel[1] * arrowScale;
|
||||
const mag = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
const tipX = cx + dx;
|
||||
const tipY = cy + dy;
|
||||
|
||||
// Arrow line
|
||||
arrowGroup.append("line")
|
||||
.attr("class", "velocityLine")
|
||||
.attr("data-plate", plate.id)
|
||||
|
|
@ -152,7 +163,6 @@ function drawVelocityArrows(overlay, plates, plateIds, colors) {
|
|||
.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)
|
||||
|
|
@ -169,7 +179,6 @@ function drawVelocityArrows(overlay, plates, plateIds, colors) {
|
|||
.on("end", function () { d3.select(this).attr("cursor", "grab"); })
|
||||
);
|
||||
|
||||
// Plate label
|
||||
arrowGroup.append("text")
|
||||
.attr("x", cx).attr("y", cy - 6)
|
||||
.attr("text-anchor", "middle")
|
||||
|
|
@ -186,25 +195,15 @@ function drawVelocityArrows(overlay, plates, plateIds, colors) {
|
|||
|
||||
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[0] = (mx - cx) / arrowScale;
|
||||
plate.velocity[1] = -(my - cy) / arrowScale;
|
||||
plate.velocity[2] = 0;
|
||||
|
||||
// Update popup if this plate is selected
|
||||
if (tectonicSelectedPlate === plate.id) {
|
||||
updatePopupValues(plate);
|
||||
}
|
||||
if (tectonicSelectedPlate === plate.id) updatePopupValues(plate);
|
||||
}
|
||||
|
||||
function ensureArrowheadMarker() {
|
||||
|
|
@ -242,7 +241,6 @@ function selectPlate(plateId) {
|
|||
|
||||
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.55 : 0.15;
|
||||
|
|
@ -258,7 +256,6 @@ function showPlatePopup(plate) {
|
|||
const centroid = computeGridPlateCentroid(plate.id, plateIds);
|
||||
if (!centroid) return;
|
||||
|
||||
// Count cells
|
||||
let cellCount = 0;
|
||||
for (let i = 0; i < plateIds.length; i++) {
|
||||
if (plateIds[i] === plate.id) cellCount++;
|
||||
|
|
@ -306,23 +303,19 @@ function showPlatePopup(plate) {
|
|||
<span id="popupDirLabel" style="font-size:10px">${dirDeg}°</span>
|
||||
</div>
|
||||
<div style="font-size:10px;color:#888;text-align:center">
|
||||
Drag the arrow on the map to set velocity
|
||||
Drag arrow or use sliders • Enable Paint to reshape
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(popup);
|
||||
|
||||
// Position popup near the plate centroid but in screen coords
|
||||
const svgRect = document.querySelector("svg").getBoundingClientRect();
|
||||
const svgEl = document.querySelector("svg");
|
||||
const ctm = svgEl.getScreenCTM();
|
||||
const screenX = centroid[0] * ctm.a + ctm.e;
|
||||
const screenY = centroid[1] * ctm.d + ctm.f;
|
||||
|
||||
popup.style.left = Math.min(screenX + 20, window.innerWidth - 220) + "px";
|
||||
popup.style.top = Math.max(screenY - 60, 10) + "px";
|
||||
|
||||
// Listeners
|
||||
byId("popupPlateType").addEventListener("change", function () {
|
||||
plate.isOceanic = this.value === "oceanic";
|
||||
});
|
||||
|
|
@ -378,10 +371,8 @@ function redrawArrowForPlate(plate) {
|
|||
|
||||
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;
|
||||
const tipX = cx + plate.velocity[0] * arrowScale;
|
||||
const tipY = cy + -plate.velocity[1] * arrowScale;
|
||||
|
||||
viewbox.select(`.velocityLine[data-plate="${plate.id}"]`)
|
||||
.attr("x2", tipX).attr("y2", tipY);
|
||||
|
|
@ -394,14 +385,140 @@ function closePlatePopup() {
|
|||
if (popup) popup.remove();
|
||||
}
|
||||
|
||||
// ---- Paint Mode ----
|
||||
|
||||
function togglePaintMode() {
|
||||
tectonicPaintMode = !tectonicPaintMode;
|
||||
updatePaintButtonState();
|
||||
|
||||
if (tectonicPaintMode) {
|
||||
if (tectonicSelectedPlate === -1) {
|
||||
tip("Select a plate first (click on a plate), then paint to expand it", true, "warn");
|
||||
tectonicPaintMode = false;
|
||||
updatePaintButtonState();
|
||||
return;
|
||||
}
|
||||
enterPaintMode();
|
||||
} else {
|
||||
exitPaintMode();
|
||||
}
|
||||
}
|
||||
|
||||
function updatePaintButtonState() {
|
||||
const btn = byId("tectonicPaintToggle");
|
||||
if (!btn) return;
|
||||
btn.classList.toggle("pressed", tectonicPaintMode);
|
||||
btn.textContent = tectonicPaintMode ? "Paint: ON" : "Paint";
|
||||
|
||||
const brushControls = byId("tectonicBrushControls");
|
||||
if (brushControls) brushControls.style.display = tectonicPaintMode ? "block" : "none";
|
||||
}
|
||||
|
||||
function enterPaintMode() {
|
||||
tip(`Paint mode: drag on map to assign cells to Plate ${tectonicSelectedPlate}`, true, "warn");
|
||||
viewbox.style("cursor", "crosshair");
|
||||
|
||||
// Add drag handler for painting
|
||||
viewbox.call(
|
||||
d3.drag()
|
||||
.on("start", paintStart)
|
||||
.on("drag", paintDrag)
|
||||
.on("end", paintEnd)
|
||||
);
|
||||
}
|
||||
|
||||
function exitPaintMode() {
|
||||
viewbox.style("cursor", "default");
|
||||
// Restore default zoom behavior
|
||||
viewbox.on(".drag", null);
|
||||
svg.call(zoom);
|
||||
removeBrushCircle();
|
||||
clearMainTip();
|
||||
}
|
||||
|
||||
function paintStart() {
|
||||
if (!tectonicPaintMode || tectonicSelectedPlate === -1) return;
|
||||
const [x, y] = d3.mouse(this);
|
||||
paintCellsAt(x, y);
|
||||
}
|
||||
|
||||
function paintDrag() {
|
||||
if (!tectonicPaintMode || tectonicSelectedPlate === -1) return;
|
||||
const [x, y] = d3.mouse(this);
|
||||
moveBrushCircle(x, y);
|
||||
paintCellsAt(x, y);
|
||||
}
|
||||
|
||||
function paintEnd() {
|
||||
if (!tectonicPaintMode) return;
|
||||
removeBrushCircle();
|
||||
// Redraw overlay to reflect changes
|
||||
drawPlateOverlay();
|
||||
}
|
||||
|
||||
function paintCellsAt(x, y) {
|
||||
const r = tectonicBrushRadius;
|
||||
const cellsInRadius = findGridAll(x, y, r);
|
||||
if (!cellsInRadius || cellsInRadius.length === 0) return;
|
||||
|
||||
const generator = window.tectonicGenerator;
|
||||
const plateIds = window.tectonicMetadata.plateIds;
|
||||
|
||||
// Reassign cells on the sphere
|
||||
generator.reassignCells(cellsInRadius, tectonicSelectedPlate);
|
||||
|
||||
// Update grid-level metadata to match
|
||||
for (const gc of cellsInRadius) {
|
||||
plateIds[gc] = tectonicSelectedPlate;
|
||||
}
|
||||
|
||||
// Update visual overlay for painted cells
|
||||
const colors = tectonicPlateColors;
|
||||
const cellGroup = viewbox.select("#plateCells");
|
||||
for (const gc of cellsInRadius) {
|
||||
const poly = cellGroup.select(`polygon[data-cell="${gc}"]`);
|
||||
if (!poly.empty()) {
|
||||
poly.attr("fill", colors[tectonicSelectedPlate])
|
||||
.attr("stroke", colors[tectonicSelectedPlate])
|
||||
.attr("data-plate", tectonicSelectedPlate)
|
||||
.attr("fill-opacity", 0.55);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function moveBrushCircle(x, y) {
|
||||
let circle = byId("tectonicBrushCircle");
|
||||
if (!circle) {
|
||||
const svg = viewbox.node().ownerSVGElement;
|
||||
const ns = "http://www.w3.org/2000/svg";
|
||||
circle = document.createElementNS(ns, "circle");
|
||||
circle.id = "tectonicBrushCircle";
|
||||
circle.setAttribute("fill", "none");
|
||||
circle.setAttribute("stroke", tectonicPlateColors[tectonicSelectedPlate] || "#fff");
|
||||
circle.setAttribute("stroke-width", "1.5");
|
||||
circle.setAttribute("stroke-dasharray", "4,3");
|
||||
circle.setAttribute("pointer-events", "none");
|
||||
viewbox.node().appendChild(circle);
|
||||
}
|
||||
circle.setAttribute("cx", x);
|
||||
circle.setAttribute("cy", y);
|
||||
circle.setAttribute("r", tectonicBrushRadius);
|
||||
}
|
||||
|
||||
function removeBrushCircle() {
|
||||
const circle = byId("tectonicBrushCircle");
|
||||
if (circle) circle.remove();
|
||||
}
|
||||
|
||||
// ---- Actions ----
|
||||
|
||||
function regenerateFromEditor() {
|
||||
const generator = window.tectonicGenerator;
|
||||
if (!generator) return tip("No tectonic generator available", false, "error");
|
||||
|
||||
tip("Regenerating terrain preview...", true, "warn");
|
||||
if (tectonicPaintMode) { exitPaintMode(); tectonicPaintMode = false; updatePaintButtonState(); }
|
||||
closePlatePopup();
|
||||
tip("Regenerating terrain preview...", true, "warn");
|
||||
|
||||
setTimeout(() => {
|
||||
try {
|
||||
|
|
@ -432,6 +549,7 @@ function regenerateFromEditor() {
|
|||
function applyToMap() {
|
||||
if (!window.tectonicGenerator) return tip("No tectonic generator available", false, "error");
|
||||
|
||||
if (tectonicPaintMode) { exitPaintMode(); tectonicPaintMode = false; updatePaintButtonState(); }
|
||||
closePlatePopup();
|
||||
closeTectonicEditor();
|
||||
$("#tectonicEditor").dialog("close");
|
||||
|
|
@ -520,6 +638,7 @@ function togglePlateOverlay() {
|
|||
}
|
||||
|
||||
function closeTectonicEditor() {
|
||||
if (tectonicPaintMode) { exitPaintMode(); tectonicPaintMode = false; }
|
||||
closePlatePopup();
|
||||
viewbox.select("#tectonicOverlay").remove();
|
||||
d3.select("#tectonicArrowhead").remove();
|
||||
|
|
|
|||
|
|
@ -4108,10 +4108,17 @@
|
|||
<div id="tectonicEditor" class="dialog stable" style="display: none">
|
||||
<p style="font-size:11px;margin:0 0 6px">Click a plate to edit. Drag arrows to set velocity.</p>
|
||||
<div style="margin-top: 4px">
|
||||
<button id="tectonicPaintToggle" data-tip="Toggle paint mode to reshape plates with a brush">Paint</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>
|
||||
</div>
|
||||
<div id="tectonicBrushControls" style="display:none;margin-top:4px;font-size:11px">
|
||||
<label>Brush size: </label>
|
||||
<input id="tectonicBrushSize" type="range" min="3" max="40" step="1" value="10"
|
||||
style="width:80px;vertical-align:middle">
|
||||
<span id="tectonicBrushSizeLabel">10</span>
|
||||
</div>
|
||||
<div style="margin-top: 4px">
|
||||
<button id="tectonicApplyMap" data-tip="Apply changes and regenerate the full map">Apply to Map</button>
|
||||
<button id="tectonicToggleOverlay" data-tip="Switch between plate view and height view">Toggle View</button>
|
||||
<button id="tectonicClose" data-tip="Close editor and remove overlay">Close</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -221,6 +221,9 @@ export class TectonicPlateGenerator {
|
|||
private plates: TectonicPlate[] = [];
|
||||
private boundaries: PlateBoundary[] = [];
|
||||
|
||||
// Grid→sphere mapping for editor cell reassignment
|
||||
private gridToSphere!: Int32Array;
|
||||
|
||||
// Seeded PRNG for deterministic elevation pipeline
|
||||
private elevationSeed: number = 0;
|
||||
private rng: () => number = Math.random;
|
||||
|
|
@ -291,6 +294,27 @@ export class TectonicPlateGenerator {
|
|||
return this.projectAndFinalize();
|
||||
}
|
||||
|
||||
// Reassign grid cells to a different plate, propagating to sphere faces
|
||||
reassignCells(gridCells: number[], newPlateId: number): void {
|
||||
if (newPlateId < 0 || newPlateId >= this.plates.length) return;
|
||||
|
||||
for (const gc of gridCells) {
|
||||
if (gc < 0 || gc >= this.numGridCells) continue;
|
||||
|
||||
// Find current plate and remove from it
|
||||
const sphereFace = this.gridToSphere[gc];
|
||||
if (sphereFace >= 0) {
|
||||
const oldPlateId = this.plateAssignment[sphereFace];
|
||||
if (oldPlateId >= 0 && oldPlateId < this.plates.length) {
|
||||
this.plates[oldPlateId].cells.delete(sphereFace);
|
||||
}
|
||||
// Assign to new plate on sphere
|
||||
this.plateAssignment[sphereFace] = newPlateId;
|
||||
this.plates[newPlateId].cells.add(sphereFace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get current plates for editor access
|
||||
getPlates(): TectonicPlate[] {
|
||||
return this.plates;
|
||||
|
|
@ -1053,6 +1077,7 @@ export class TectonicPlateGenerator {
|
|||
// y: 0..gridHeight → lat: π/2..-π/2 (top = north pole, bottom = south pole)
|
||||
const gridElevations = new Float32Array(this.numGridCells);
|
||||
const gridPlateIds = new Int8Array(this.numGridCells).fill(-1);
|
||||
this.gridToSphere = new Int32Array(this.numGridCells).fill(-1);
|
||||
|
||||
// Build a bucketed spatial index for fast nearest-face lookup
|
||||
const lonBuckets = 72; // 5° per bucket
|
||||
|
|
@ -1122,6 +1147,7 @@ export class TectonicPlateGenerator {
|
|||
|
||||
gridElevations[gi] = this.elevations[bestFace];
|
||||
gridPlateIds[gi] = this.plateAssignment[bestFace];
|
||||
this.gridToSphere[gi] = bestFace;
|
||||
}
|
||||
|
||||
// Light smoothing to remove pixelation from sphere→grid sampling
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue