Split view and data for labels (#2)

* Initial plan

* Implement label view/data separation with pack.labels

Co-authored-by: StempunkDev <39553418+StempunkDev@users.noreply.github.com>

* Update label editor to sync changes with pack.labels

Co-authored-by: StempunkDev <39553418+StempunkDev@users.noreply.github.com>

* Address code review feedback: optimize filtering and add pathData property

Co-authored-by: StempunkDev <39553418+StempunkDev@users.noreply.github.com>

* Move label migration code from load.js to auto-update.js

Co-authored-by: StempunkDev <39553418+StempunkDev@users.noreply.github.com>

* Implement label generation and rendering for states and burgs

* Bump version to 1.113.0

* Remove initialization of labels array in generate function

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: StempunkDev <39553418+StempunkDev@users.noreply.github.com>
This commit is contained in:
Copilot 2026-02-09 00:50:01 +01:00 committed by GitHub
parent be82ddb0a4
commit 25d06265f1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 962 additions and 351 deletions

View file

@ -1106,4 +1106,105 @@ export function resolveVersionConflicts(mapVersion) {
}
}
if (isOlderThan("1.113.0")) {
// v1.113 separates label data from view layer by introducing pack.labels array
// Migrate labels from SVG to pack.labels for backward compatibility
if (!pack.labels || pack.labels.length === 0) {
pack.labels = [];
// Extract burg labels from SVG
burgLabels.selectAll("text").each(function() {
const textEl = d3.select(this);
const burgId = +textEl.attr("data-id");
const id = textEl.attr("id");
const group = this.parentNode.id;
if (id && burgId !== undefined) {
pack.labels.push({
i: id,
type: "burg",
name: textEl.text(),
group: group,
burgId: burgId
});
}
});
// Extract state labels from SVG
labels.select("g#states").selectAll("text").each(function() {
const textEl = d3.select(this);
const id = textEl.attr("id");
const stateId = id ? +id.replace("stateLabel", "") : null;
if (id && stateId !== null) {
const textPathEl = textEl.select("textPath");
const pathId = textPathEl.attr("href")?.replace("#textPath_", "");
const path = pathId ? defs.select(`#textPath_${id}`) : null;
let pathData;
if (path && !path.empty()) {
pathData = path.attr("d");
}
const lines = [];
textPathEl.selectAll("tspan").each(function() {
lines.push(d3.select(this).text());
});
pack.labels.push({
i: id,
type: "state",
name: lines.join("|"),
stateId: stateId,
pathData: pathData,
startOffset: parseFloat(textPathEl.attr("startOffset")) || 50,
fontSize: parseFloat(textPathEl.attr("font-size")) || 100,
letterSpacing: parseFloat(textPathEl.attr("letter-spacing")) || 0,
transform: textEl.attr("transform") || ""
});
}
});
// Extract custom labels from other groups
labels.selectAll(":scope > g").each(function() {
const groupId = this.id;
if (groupId === "states" || groupId === "burgLabels") return;
d3.select(this).selectAll("text").each(function() {
const textEl = d3.select(this);
const id = textEl.attr("id");
if (id) {
const textPathEl = textEl.select("textPath");
const pathId = textPathEl.attr("href")?.replace("#textPath_", "");
const path = pathId ? defs.select(`#textPath_${id}`) : null;
let pathData;
if (path && !path.empty()) {
pathData = path.attr("d");
}
const lines = [];
textPathEl.selectAll("tspan").each(function() {
lines.push(d3.select(this).text());
});
pack.labels.push({
i: id,
type: "custom",
name: lines.join("|"),
group: groupId,
pathData: pathData,
startOffset: parseFloat(textPathEl.attr("startOffset")) || 50,
fontSize: parseFloat(textPathEl.attr("font-size")) || 100,
letterSpacing: parseFloat(textPathEl.attr("letter-spacing")) || 0,
transform: textEl.attr("transform") || ""
});
}
});
});
}
}
}

View file

@ -392,6 +392,7 @@ async function parseLoadedData(data, mapVersion) {
pack.markers = data[35] ? JSON.parse(data[35]) : [];
pack.routes = data[37] ? JSON.parse(data[37]) : [];
pack.zones = data[38] ? JSON.parse(data[38]) : [];
pack.labels = data[40] ? JSON.parse(data[40]) : [];
pack.cells.biome = Uint8Array.from(data[16].split(","));
pack.cells.burg = Uint16Array.from(data[17].split(","));
pack.cells.conf = Uint8Array.from(data[18].split(","));

View file

@ -104,6 +104,7 @@ function prepareMapData() {
const routes = JSON.stringify(pack.routes);
const zones = JSON.stringify(pack.zones);
const ice = JSON.stringify(pack.ice);
const labels = JSON.stringify(pack.labels || []);
// store name array only if not the same as default
const defaultNB = Names.getNameBases();
@ -158,7 +159,8 @@ function prepareMapData() {
cellRoutes,
routes,
zones,
ice
ice,
labels
].join("\r\n");
return mapData;
}

View file

@ -1,4 +1,77 @@
"use strict";
// Helper function to sync label data with pack.labels
function syncLabelToPack(labelId) {
if (!pack.labels) pack.labels = [];
const textEl = document.getElementById(labelId);
if (!textEl) return;
const group = textEl.parentNode.id;
const isBurgLabel = labelId.startsWith("burgLabel");
const isStateLabel = labelId.startsWith("stateLabel");
// Remove existing entry
const existingIndex = pack.labels.findIndex(l => l.i === labelId);
// Gather label data
const labelData = {
i: labelId,
type: isBurgLabel ? "burg" : isStateLabel ? "state" : "custom"
};
if (isBurgLabel) {
const burgId = +textEl.getAttribute("data-id");
labelData.name = textEl.textContent;
labelData.group = group;
labelData.burgId = burgId;
} else {
// State or custom label with textPath
const textPathEl = textEl.querySelector("textPath");
if (textPathEl) {
const lines = [];
textPathEl.querySelectorAll("tspan").forEach(tspan => {
lines.push(tspan.textContent);
});
labelData.name = lines.join("|");
labelData.startOffset = parseFloat(textPathEl.getAttribute("startOffset")) || 50;
labelData.fontSize = parseFloat(textPathEl.getAttribute("font-size")) || 100;
labelData.letterSpacing = parseFloat(textPathEl.getAttribute("letter-spacing")) || 0;
labelData.transform = textEl.getAttribute("transform") || "";
if (isStateLabel) {
const stateId = +labelId.replace("stateLabel", "");
labelData.stateId = stateId;
} else {
labelData.group = group;
}
// Extract path points
const pathId = `textPath_${labelId}`;
const pathEl = document.getElementById(pathId);
if (pathEl) {
const pathData = pathEl.getAttribute("d");
// Store the path data - ideally we'd parse it into points, but for now store as-is
// This will be improved when we implement more sophisticated label editing
labelData.pathData = pathData;
}
}
}
// Update or add to pack.labels
if (existingIndex >= 0) {
pack.labels[existingIndex] = labelData;
} else {
pack.labels.push(labelData);
}
}
// Helper function to remove label from pack.labels
function removeLabelFromPack(labelId) {
if (!pack.labels) return;
pack.labels = pack.labels.filter(l => l.i !== labelId);
}
function editLabel() {
if (customization) return;
closeDialogs();
@ -133,6 +206,9 @@ function editLabel() {
const d = round(lineGen(points));
path.setAttribute("d", d);
debug.select("#controlPoints > path").attr("d", d);
// Sync changes to pack.labels
syncLabelToPack(elSelected.attr("id"));
}
function clickControlPoint() {
@ -188,6 +264,11 @@ function editLabel() {
elSelected.attr("transform", transform);
debug.select("#controlPoints").attr("transform", transform);
});
d3.event.on("end", function () {
// Sync changes to pack.labels after drag ends
syncLabelToPack(elSelected.attr("id"));
});
}
function showGroupSection() {
@ -205,6 +286,9 @@ function editLabel() {
function changeGroup() {
byId(this.value).appendChild(elSelected.node());
// Sync changes to pack.labels
syncLabelToPack(elSelected.attr("id"));
}
function toggleNewGroupInput() {
@ -280,6 +364,10 @@ function editLabel() {
.selectAll("text")
.each(function () {
byId("textPath_" + this.id).remove();
// Remove from pack.labels
removeLabelFromPack(this.id);
this.remove();
});
if (!basic) labels.select("#" + group).remove();
@ -313,6 +401,9 @@ function editLabel() {
if (elSelected.attr("id").slice(0, 10) === "stateLabel")
tip("Use States Editor to change an actual state name, not just a label", false, "warning");
// Sync changes to pack.labels
syncLabelToPack(elSelected.attr("id"));
}
function generateRandomName() {
@ -359,6 +450,9 @@ function editLabel() {
function changeStartOffset() {
elSelected.select("textPath").attr("startOffset", this.value + "%");
tip("Label offset: " + this.value + "%");
// Sync changes to pack.labels
syncLabelToPack(elSelected.attr("id"));
}
function changeRelativeSize() {
@ -395,9 +489,13 @@ function editLabel() {
buttons: {
Remove: function () {
$(this).dialog("close");
defs.select("#textPath_" + elSelected.attr("id")).remove();
const labelId = elSelected.attr("id");
defs.select("#textPath_" + labelId).remove();
elSelected.remove();
$("#labelEditor").dialog("close");
// Remove from pack.labels
removeLabelFromPack(labelId);
},
Cancel: function () {
$(this).dialog("close");

View file

@ -13,7 +13,7 @@
* Example: 1.102.2 -> Major version 1, Minor version 102, Patch version 2
*/
const VERSION = "1.112.1";
const VERSION = "1.113.0";
if (parseMapVersion(VERSION) !== VERSION) alert("versioning.js: Invalid format or parsing function");
{