From c4feb14540ae867a7a11f5e0526722abaea6495c Mon Sep 17 00:00:00 2001 From: barrulus Date: Wed, 10 Jun 2026 10:07:04 +0100 Subject: [PATCH 1/4] fix(states-editor): pick painter target from a dropdown, not a rendered row MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous painter fix rendered every state row while in manual-assignment mode so off-page states would be selectable. On large maps that re-rendered every state's COA emblem at once (millions of SVG nodes), pinning the CPU at 100% and freezing the tab — pagination existed specifically to avoid that. Revert to the normal paginated row list and instead choose the brush target from a lightweight "Paint as" +
Brush size:
@@ -126,6 +129,7 @@ function addListeners() { ensureEl("statesRandomize").on("click", randomizeStatesExpansion); ensureEl("statesGrowthRate").on("input", () => recalculateStates(false)); ensureEl("statesManually").on("click", enterStatesManualAssignent); + ensureEl("statesManuallyState").on("change", highlightBrushRow); ensureEl("statesManuallyUndo").on("click", undoStatesManualAssignment); ensureEl("statesManuallyApply").on("click", applyStatesManualAssignent); ensureEl("statesManuallyCancel").on("click", () => exitStatesManualAssignment(false)); @@ -192,15 +196,10 @@ function statesEditorAddLines() { totalBurgs += s.burgs; } - // Manual assignment picks the brush state by clicking its row, so every state must have a - // row in the DOM. Pagination would hide states beyond the first page (the pager is hidden in - // this mode too), making them unselectable. Render the full set while painting. - const isManualAssignment = customization === 2; const pageInfo = getEditorPage(allStates, statesPage); - const rows = isManualAssignment ? allStates : pageInfo.items; let lines = ""; - for (const s of rows) { + for (const s of pageInfo.items) { const area = getArea(s.area); const rural = s.rural * populationRate; const urban = s.urban * populationRate * urbanization; @@ -314,15 +313,10 @@ function statesEditorAddLines() { ensureEl("statesFooterPopulation").innerHTML = si(totalPopulation); ensureEl("statesFooterPopulation").dataset.population = totalPopulation; - // No pager while painting: all rows are rendered and the footer is hidden anyway. - if (isManualAssignment) { - ensureEl("statesFooter").querySelector(":scope > .editorPagination")?.remove(); - } else { - renderEditorPagination(ensureEl("statesFooter"), pageInfo, page => { - statesPage.page = page; - statesEditorAddLines(); - }); - } + renderEditorPagination(ensureEl("statesFooter"), pageInfo, page => { + statesPage.page = page; + statesEditorAddLines(); + }); // add listeners $body.querySelectorAll(":scope > div").forEach($line => { @@ -947,22 +941,53 @@ function enterStatesManualAssignent() { $body.querySelectorAll("div > input, select, span, svg").forEach(e => (e.style.pointerEvents = "none")); $("#statesEditor").dialog({ position: { my: "right top", at: "right-10 top+10", of: "svg", collision: "fit" } }); - tip("Click on state to select, drag the circle to change state", true); + // The editor list is paginated, so the brush target is chosen from a lightweight all-states + // dropdown (plain text, no emblems) rather than from a rendered row — otherwise states beyond + // the current page would be unselectable, and rendering every emblem at once freezes the tab. + populateBrushStateSelect(); + highlightBrushRow(); + + tip("Pick a state to paint with, then drag the circle. Click the map to pick the state under the cursor", true); viewbox .style("cursor", "crosshair") .on("click", selectStateOnMapClick) .call(d3.drag().on("start", dragStateBrush)) .on("touchmove mousemove", moveStateBrush); - $body.querySelector("div").classList.add("selected"); statesManualHistory = []; } +// Fill the brush-target dropdown with every (non-removed) state, ordered to match the editor's +// active sort. Cheap: one plain `) + .join(""); +} + +// Active brush target: the state the next stroke paints cells into. +function getBrushStateId() { + const select = ensureEl("statesManuallyState"); + return select && select.value !== "" ? +select.value : 0; +} + +// Mirror the dropdown selection onto a visible row (purely cosmetic; the row may be off-page). +function highlightBrushRow() { + const id = getBrushStateId(); + $body.querySelector("div.selected")?.classList.remove("selected"); + $body.querySelector("div[data-id='" + id + "']")?.classList.add("selected"); +} + function selectStateOnLineClick() { if (customization !== 2) return; if (this.parentNode.id !== "statesBodySection") return; - $body.querySelector("div.selected").classList.remove("selected"); - this.classList.add("selected"); + const select = ensureEl("statesManuallyState"); + if (select) select.value = this.dataset.id; + highlightBrushRow(); } function selectStateOnMapClick() { @@ -973,10 +998,10 @@ function selectStateOnMapClick() { const assigned = statesBody.select("#temp").select("polygon[data-cell='" + i + "']"); const state = assigned.size() ? +assigned.attr("data-state") : pack.cells.state[i]; - const $row = $body.querySelector("div[data-id='" + state + "']"); - if (!$row) return; // clicked state's row is on another page; ignore to avoid a crash - $body.querySelector("div.selected")?.classList.remove("selected"); - $row.classList.add("selected"); + // Set the brush to the clicked state even when its row is on another page. + const select = ensureEl("statesManuallyState"); + if (select) select.value = state; + highlightBrushRow(); } function dragStateBrush() { @@ -998,8 +1023,7 @@ function dragStateBrush() { function changeStateForSelection(selection) { const temp = statesBody.select("#temp"); - const $selected = $body.querySelector("div.selected"); - const stateNew = +$selected.dataset.id; + const stateNew = getBrushStateId(); const color = pack.states[stateNew].color || "#ffffff"; const preventOverwrite = document.getElementById("statesManuallyProtect")?.checked; From 7e031911a89f50bd5b9503a5bf436bb156f5eaf1 Mon Sep 17 00:00:00 2001 From: barrulus Date: Wed, 10 Jun 2026 14:05:10 +0100 Subject: [PATCH 2/4] feat(states-editor): paint-mode picker to demote a whole state to a province MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a "demote clicked state to a province" checkbox to the manual-assignment (paint) toolbar. With it on, the state dropdown becomes the receiving state and clicking any state on the map stages that entire state for demotion into a single province of the receiving state — keeping its name, government form, colour and emblem, exactly like the merge dialog's "merge down to provinces". - Picks stage as a recoloured preview in #temp (tagged data-demote) and are undoable like brush strokes; nothing changes until Apply. - On Apply, staged demotions are grouped by receiving state and committed via mergeStates(ids, owner, true). To enable that reuse, mergeStates (and its demoteStateToProvince helper) is lifted from the merge-dialog closure to module scope; the dialog still calls it unchanged. - Cell brushing is disabled while picker mode is on (click = pick a state). Bump states-editor cache-bust token to 1.122.16. --- .../modules/dynamic/editors/states-editor.js | 83 ++++++++++++++++++- public/modules/ui/editors.js | 2 +- 2 files changed, 81 insertions(+), 4 deletions(-) diff --git a/public/modules/dynamic/editors/states-editor.js b/public/modules/dynamic/editors/states-editor.js index 3fc167d52d..84c688362e 100644 --- a/public/modules/dynamic/editors/states-editor.js +++ b/public/modules/dynamic/editors/states-editor.js @@ -87,7 +87,11 @@ function insertEditorHtml() {