feat: routes editor - split route

This commit is contained in:
Azgaar 2024-06-23 15:43:11 +02:00
parent cd45c3a01b
commit 41354a857e
5 changed files with 154 additions and 55 deletions

View file

@ -2937,6 +2937,11 @@
data-tip="Create a new route selecting route cells" data-tip="Create a new route selecting route cells"
class="icon-map-pin" class="icon-map-pin"
></button> ></button>
<button
id="routeSplit"
data-tip="Click on a control point to split the route there"
class="icon-unlink"
></button>
<button <button
id="routeElevationProfile" id="routeElevationProfile"
data-tip="Show the elevation profile for the route" data-tip="Show the elevation profile for the route"
@ -8120,6 +8125,7 @@
<script src="utils/stringUtils.js"></script> <script src="utils/stringUtils.js"></script>
<script src="utils/languageUtils.js"></script> <script src="utils/languageUtils.js"></script>
<script src="utils/unitUtils.js?v=1.87.00"></script> <script src="utils/unitUtils.js?v=1.87.00"></script>
<script defer src="utils/debugUtils.js"></script>
<script src="modules/voronoi.js"></script> <script src="modules/voronoi.js"></script>
<script src="config/heightmap-templates.js"></script> <script src="config/heightmap-templates.js"></script>

View file

@ -36,6 +36,7 @@ function editRoute(id) {
// add listeners // add listeners
byId("routeCreateSelectingCells").on("click", showCreationDialog); byId("routeCreateSelectingCells").on("click", showCreationDialog);
byId("routeSplit").on("click", toggleSplitRoute);
byId("routeElevationProfile").on("click", showRouteElevationProfile); byId("routeElevationProfile").on("click", showRouteElevationProfile);
byId("routeLegend").on("click", editRouteLegend); byId("routeLegend").on("click", editRouteLegend);
byId("routeRemove").on("click", removeRoute); byId("routeRemove").on("click", removeRoute);
@ -84,7 +85,7 @@ function editRoute(id) {
.attr("cy", d => d[1]) .attr("cy", d => d[1])
.attr("r", 0.6) .attr("r", 0.6)
.call(d3.drag().on("start", dragControlPoint)) .call(d3.drag().on("start", dragControlPoint))
.on("click", removeControlPoint); .on("click", handleControlPointClick);
} }
function drawCells() { function drawCells() {
@ -170,30 +171,76 @@ function editRoute(id) {
redrawRoute(); redrawRoute();
} }
function removeControlPoint() { function handleControlPointClick() {
const route = getRoute(); const controlPoint = d3.select(this);
const isSplitMode = byId("routeSplit").classList.contains("pressed");
isSplitMode ? splitRoute(controlPoint) : removeControlPoint(controlPoint);
if (!route.points) route.points = debug.selectAll("#controlPoints > *").data(); function splitRoute(controlPoint) {
const [x, y] = d3.select(this).datum(); const allPoints = debug.selectAll("#controlPoints > *").data();
const cellId = findCell(x, y); const pointIndex = allPoints.indexOf(controlPoint.datum());
const routeAllCells = route.points.map(([x, y]) => findCell(x, y));
const isOnlyPointInCell = routeAllCells.filter(cell => cell === cellId).length === 1; const oldRoutePoints = allPoints.slice(0, pointIndex + 1);
if (isOnlyPointInCell) { const newRoutePoints = allPoints.slice(pointIndex);
const index = route.cells.indexOf(cellId);
const prev = route.cells[index - 1]; // update old route
const next = route.cells[index + 1]; const oldRoute = getRoute();
if (prev) removeConnection(prev, cellId); oldRoute.points = oldRoutePoints;
if (next) removeConnection(cellId, next); oldRoute.cells = unique(oldRoute.points.map(([x, y]) => findCell(x, y)));
if (prev && next) addConnection(prev, next, route.i); drawControlPoints(oldRoute.points);
drawCells();
redrawRoute();
// create new route
const newRoute = {
...oldRoute,
i: Math.max(...pack.routes.map(route => route.i)) + 1,
cells: unique(newRoutePoints.map(([x, y]) => findCell(x, y))),
points: newRoutePoints
};
pack.routes.push(newRoute);
for (let i = 0; i < newRoute.cells.length; i++) {
const cellId = newRoute.cells[i];
const nextCellId = newRoute.cells[i + 1];
if (nextCellId) addConnection(cellId, nextCellId, newRoute.i);
}
const lineGen = d3.line();
lineGen.curve(ROUTE_CURVES[newRoute.group] || ROUTE_CURVES.default);
routes
.select("#" + newRoute.group)
.append("path")
.attr("d", round(lineGen(Routes.getPoints(newRoute, newRoutePoints)), 1))
.attr("id", "route" + newRoute.i);
byId("routeSplit").classList.remove("pressed");
} }
this.remove(); function removeControlPoint(controlPoint) {
route.points = debug.selectAll("#controlPoints > *").data(); const route = getRoute();
route.cells = unique(route.points.map(([x, y]) => findCell(x, y)));
drawCells(); if (!route.points) route.points = debug.selectAll("#controlPoints > *").data();
redrawRoute(); const cellId = findCell(...controlPoint.datum());
const routeAllCells = route.points.map(([x, y]) => findCell(x, y));
const isOnlyPointInCell = routeAllCells.filter(cell => cell === cellId).length === 1;
if (isOnlyPointInCell) {
const index = route.cells.indexOf(cellId);
const prev = route.cells[index - 1];
const next = route.cells[index + 1];
if (prev) removeConnection(prev, cellId);
if (next) removeConnection(cellId, next);
if (prev && next) addConnection(prev, next, route.i);
}
controlPoint.remove();
route.points = debug.selectAll("#controlPoints > *").data();
route.cells = unique(route.points.map(([x, y]) => findCell(x, y)));
drawCells();
redrawRoute();
}
} }
function showCreationDialog() { function showCreationDialog() {
@ -201,6 +248,10 @@ function editRoute(id) {
createRoute(route.group); createRoute(route.group);
} }
function toggleSplitRoute() {
this.classList.toggle("pressed");
}
function removeConnection(from, to) { function removeConnection(from, to) {
const routes = pack.cells.routes; const routes = pack.cells.routes;
if (routes[from]) delete routes[from][to]; if (routes[from]) delete routes[from][to];

View file

@ -14,11 +14,27 @@ function toHEX(rgb) {
: ""; : "";
} }
const C_12 = [
"#dababf",
"#fb8072",
"#80b1d3",
"#fdb462",
"#b3de69",
"#fccde5",
"#c6b9c1",
"#bc80bd",
"#ccebc5",
"#ffed6f",
"#8dd3c7",
"#eb8de7"
];
const scaleRainbow = d3.scaleSequential(d3.interpolateRainbow);
// return array of standard shuffled colors // return array of standard shuffled colors
function getColors(number) { function getColors(number) {
const c12 = ["#dababf", "#fb8072", "#80b1d3", "#fdb462", "#b3de69", "#fccde5", "#c6b9c1", "#bc80bd", "#ccebc5", "#ffed6f", "#8dd3c7", "#eb8de7"]; const colors = d3.shuffle(
const cRB = d3.scaleSequential(d3.interpolateRainbow); d3.range(number).map(i => (i < 12 ? C_12[i] : d3.color(scaleRainbow((i - 12) / (number - 12))).hex()))
const colors = d3.shuffle(d3.range(number).map(i => (i < 12 ? c12[i] : d3.color(cRB((i - 12) / (number - 12))).hex()))); );
return colors; return colors;
} }

58
utils/debugUtils.js Normal file
View file

@ -0,0 +1,58 @@
"use strict";
// FMG utils used for debugging
function drawCellsValue(data) {
debug.selectAll("text").remove();
debug
.selectAll("text")
.data(data)
.enter()
.append("text")
.attr("x", (d, i) => pack.cells.p[i][0])
.attr("y", (d, i) => pack.cells.p[i][1])
.text(d => d);
}
function drawPolygons(data) {
const max = d3.max(data);
const min = d3.min(data);
const scheme = getColorScheme(terrs.select("#landHeights").attr("scheme"));
data = data.map(d => 1 - normalize(d, min, max));
debug.selectAll("polygon").remove();
debug
.selectAll("polygon")
.data(data)
.enter()
.append("polygon")
.attr("points", (d, i) => getGridPolygon(i))
.attr("fill", d => scheme(d))
.attr("stroke", d => scheme(d));
}
function drawRouteConnections() {
debug.select("#connections").remove();
const routes = debug.append("g").attr("id", "connections").attr("stroke-width", 0.4);
const points = pack.cells.p;
const links = pack.cells.routes;
for (const from in links) {
for (const to in links[from]) {
const [x1, y1] = points[from];
const [x3, y3] = points[to];
const [x2, y2] = [(x1 + x3) / 2, (y1 + y3) / 2];
const routeId = links[from][to];
routes
.append("line")
.attr("x1", x1)
.attr("y1", y1)
.attr("x2", x2)
.attr("y2", y2)
.attr("data-id", routeId)
.attr("stroke", C_12[routeId % 12]);
}
}
}

View file

@ -312,38 +312,6 @@ void (function addFindAll() {
}; };
})(); })();
// helper function non-used for the generation
function drawCellsValue(data) {
debug.selectAll("text").remove();
debug
.selectAll("text")
.data(data)
.enter()
.append("text")
.attr("x", (d, i) => pack.cells.p[i][0])
.attr("y", (d, i) => pack.cells.p[i][1])
.text(d => d);
}
// helper function non-used for the main generation
function drawPolygons(data) {
const max = d3.max(data);
const min = d3.min(data);
const scheme = getColorScheme(terrs.select("#landHeights").attr("scheme"));
data = data.map(d => 1 - normalize(d, min, max));
debug.selectAll("polygon").remove();
debug
.selectAll("polygon")
.data(data)
.enter()
.append("polygon")
.attr("points", (d, i) => getGridPolygon(i))
.attr("fill", d => scheme(d))
.attr("stroke", d => scheme(d));
}
// draw raster heightmap preview (not used in main generation) // draw raster heightmap preview (not used in main generation)
function drawHeights({heights, width, height, scheme, renderOcean}) { function drawHeights({heights, width, height, scheme, renderOcean}) {
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");