Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 5 additions & 8 deletions docs/architecture/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -360,15 +360,12 @@ Widgets like `hierarchy-tree` and `minimap` may move to `components/` if they ge

## Cross-layer subsystems

Most folders are flat. A tightly-coupled subsystem that genuinely spans layers appears as
a **same-named subfolder inside each layer it touches**, rather than one mixed folder.
Heraldry is the current example:
Most folders are flat. When a feature spans layers, each part stays under the folder for
its role. Heraldry is the current example:

- `src/generators/emblems/` — emblem generation + heraldry data (registers `window.COA`)
- `src/renderers/emblems/` — SVG drawing of emblems (registers `window.COArenderer`)

This keeps each half under the correct layer (generation vs view) while the shared
`emblems/` name signals they form one feature.
- `src/data/emblems/` — static heraldry catalogs
- `src/generators/emblems-generator.ts` — emblem generation (registers `window.Emblems`)
- `src/renderers/emblems/` — SVG drawing of emblems (registers `window.EmblemRenderer`)

## Why no `core/`

Expand Down
13 changes: 8 additions & 5 deletions public/modules/ui/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -349,8 +349,9 @@ function changeCultureSet() {
}

function changeEmblemShape(emblemShape) {
Emblems.setShape(emblemShape);
const image = ensureEl("emblemShapeImage");
const shapePath = window.COArenderer && COArenderer.shieldPaths[emblemShape];
const shapePath = window.EmblemRenderer && EmblemRenderer.shieldPaths[emblemShape];
shapePath ? image.setAttribute("d", shapePath) : image.removeAttribute("d");

const specificShape = ["culture", "state", "random"].includes(emblemShape) ? null : emblemShape;
Expand All @@ -361,12 +362,12 @@ function changeEmblemShape(emblemShape) {
const coaEl = ensureEl(id);
if (!coaEl) return; // not rendered
coaEl.remove();
COArenderer.trigger(id, coa);
EmblemRenderer.trigger(id, coa);
};

pack.states.forEach(state => {
if (!state.i || state.removed || !state.coa || state.coa.custom) return;
const newShield = specificShape || COA.getShield(state.culture, null);
const newShield = specificShape || Emblems.getShield(state.culture, null);
if (newShield === state.coa.shield) return;
state.coa.shield = newShield;
rerenderCOA("stateCOA" + state.i, state.coa);
Expand All @@ -375,15 +376,15 @@ function changeEmblemShape(emblemShape) {
pack.provinces.forEach(province => {
if (!province.i || province.removed || !province.coa || province.coa.custom) return;
const culture = pack.cells.culture[province.center];
const newShield = specificShape || COA.getShield(culture, province.state);
const newShield = specificShape || Emblems.getShield(culture, province.state);
if (newShield === province.coa.shield) return;
province.coa.shield = newShield;
rerenderCOA("provinceCOA" + province.i, province.coa);
});

pack.burgs.forEach(burg => {
if (!burg.i || burg.removed || !burg.coa || burg.coa.custom) return;
const newShield = specificShape || COA.getShield(burg.culture, burg.state);
const newShield = specificShape || Emblems.getShield(burg.culture, burg.state);
if (newShield === burg.coa.shield) return;
burg.coa.shield = newShield;
rerenderCOA("burgCOA" + burg.i, burg.coa);
Expand Down Expand Up @@ -558,6 +559,8 @@ function applyStoredOptions() {
if (key.slice(0, 5) === "style") applyOption(stylePreset, key, key.slice(5));
}

Emblems.setShape(emblemShape.value);

if (stored("winds")) options.winds = stored("winds").split(",").map(Number);
if (stored("temperatureEquator")) options.temperatureEquator = +stored("temperatureEquator");
if (stored("temperatureNorthPole")) options.temperatureNorthPole = +stored("temperatureNorthPole");
Expand Down
3 changes: 2 additions & 1 deletion src/components/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { refreshEditors } from "@/components/dialog/dialog-helpers";
import { Layers } from "@/components/layers";
import { tip } from "@/components/tooltips";
import { Controllers } from "@/controllers";
import { Emblems } from "@/generators/emblems-generator";
import { Population } from "@/generators/population-generator";
import { unfog } from "@/renderers/overlays/fogging";
import { ensureEl, gauss, isCtrlClick } from "@/utils";
Expand Down Expand Up @@ -176,7 +177,7 @@ function regenerateProduction(): void {
}

function regenerateEmblems(): void {
COA.regenerate();
Emblems.regenerate();
Layers.draw("emblems");
}

Expand Down
1 change: 0 additions & 1 deletion src/components/zoom.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import { beforeEach, describe, expect, it, vi } from "vitest";

vi.mock("@/components/layers", () => ({ Layers: { draw: vi.fn() } }));
vi.mock("@/renderers/draw-emblems", () => ({ redrawEmblemGroup: vi.fn() }));
vi.mock("@/renderers/viewport/viewport-renderer", () => ({
ViewportLayers: { schedule: vi.fn(), renderNow: vi.fn() }
}));
Expand Down
14 changes: 1 addition & 13 deletions src/components/zoom.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { type D3ZoomEvent, select, zoom, zoomIdentity } from "d3";
import { Layers } from "@/components/layers";
import { redrawEmblemGroup } from "@/renderers/draw-emblems";
import { ViewportLayers } from "@/renderers/viewport/viewport-renderer";
import { ensureEl, findEl } from "@/utils/nodeUtils";
import { rn } from "@/utils/numberUtils";
Expand Down Expand Up @@ -72,7 +71,6 @@ function handleZoomEnd(): void {
cancelAnimationFrame(frameId);
frameId = null;
handleZoomPerFrame();
ViewportLayers.renderNow();
}

invokeActiveZooming();
Expand All @@ -96,17 +94,7 @@ function redrawTracedImage(): void {
function invokeActiveZooming(): void {
const isOptimized = ensureEl<HTMLSelectElement>("shapeRendering").value === "optimizeSpeed";

const emblems = select<SVGGElement, unknown>("#emblems");
if (emblems.style("display") !== "none") {
const hideSmallEmblems = ensureEl<HTMLInputElement>("hideEmblems").checked;
for (const group of emblems.selectAll<SVGGElement, unknown>("g").nodes()) {
const size = Number(group.getAttribute("font-size")) * scale;
const hidden = hideSmallEmblems && (size < 25 || size > 300);
group.classList.toggle("hidden", hidden);
const emblem = group.children[0];
if (!hidden && window.COArenderer && emblem && !emblem.getAttribute("href")) redrawEmblemGroup(group);
}
}
ViewportLayers.renderNow();

if (!customization && !isOptimized) {
const statesHalo = select("#statesHalo");
Expand Down
4 changes: 3 additions & 1 deletion src/controllers/burg-creator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { closeDialogs, refreshEditors } from "@/components/dialog/dialog-helpers
import { Layers } from "@/components/layers";
import { stopMapPlacement, toggleMapPlacement } from "@/components/map-placement";
import { tip } from "@/components/tooltips";
import { redrawEmblem } from "@/renderers/draw-emblems";

function toggle(): void {
if (isActive()) {
Expand Down Expand Up @@ -37,7 +38,8 @@ function addOnClick(event: MouseEvent): void {
return;
}

Burgs.add(point);
const burgId = Burgs.add(point);
redrawEmblem("burg", burgId);
refreshEditors();
Layers.draw("burgIcons", "labels", "routes");

Expand Down
5 changes: 4 additions & 1 deletion src/controllers/burg-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { Layers } from "@/components/layers";
import { clearMainTip, tip } from "@/components/tooltips";
import { applyDefaultViewboxEvents } from "@/components/viewbox-events";
import { Controllers } from "@/controllers";
import { removeEmblem } from "@/renderers/draw-emblems";
import { EmblemRenderer } from "@/renderers/emblems/renderer";
import { getHeight, openURL, speak } from "@/utils";
import { MAX_ZOOM, PAN_ZOOM_IDENTITY, type PanZoom, panBy, zoomAt } from "@/utils/panZoomUtils";
import type { Burg } from "../generators/burgs-generator";
Expand Down Expand Up @@ -326,7 +328,7 @@ function updateBurgValues(): void {

// set emblem image
const coaID = `burgCOA${id}`;
COArenderer.trigger(coaID, b.coa);
EmblemRenderer.trigger(coaID, b.coa);
ensureEl("burgEmblem").setAttribute("href", `#${coaID}`);

updateBurgPreview(b);
Expand Down Expand Up @@ -826,6 +828,7 @@ function removeSelectedBurg(): void {
confirm: "Remove",
onConfirm: () => {
Burgs.remove(burgId);
removeEmblem("burg", burgId);
Layers.draw("burgIcons", "labels");
$("#burgEditor").dialog("close");
}
Expand Down
9 changes: 8 additions & 1 deletion src/controllers/burgs-overview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { Layers } from "@/components/layers";
import { tip } from "@/components/tooltips";
import { Controllers } from "@/controllers";
import type { Burg } from "@/generators/burgs-generator";
import { removeEmblem } from "@/renderers/draw-emblems";
import { downloadFile, getFileName, getHeight, getLatitude, getLongitude, uploadFile } from "@/utils";
import { convertTemperature, ensureEl, getTemperatureLikeness, rn, si } from "../utils";

Expand Down Expand Up @@ -445,6 +446,7 @@ function triggerBurgRemove(this: HTMLElement): void {
confirm: "Remove",
onConfirm: () => {
Burgs.remove(burgId);
removeEmblem("burg", burgId);
burgsTable.refresh();
Layers.draw("burgIcons", "labels");
}
Expand Down Expand Up @@ -769,7 +771,12 @@ function triggerAllBurgsRemove(): void {
<br><i>To remove a capital you have to remove its state first</i>`,
confirm: "Remove",
onConfirm: () => {
pack.burgs.filter(b => b.i && !(b.capital || b.lock)).forEach(b => void Burgs.remove(b.i));
pack.burgs
.filter(b => b.i && !(b.capital || b.lock))
.forEach(b => {
Burgs.remove(b.i);
removeEmblem("burg", b.i);
});
burgsTable.refresh();
Layers.draw("burgIcons", "labels");
}
Expand Down
11 changes: 7 additions & 4 deletions src/controllers/cultures-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@ import { clearMainTip, showMainTip, tip } from "@/components/tooltips";
import { applyDefaultViewboxEvents } from "@/components/viewbox-events";
import { Controllers } from "@/controllers";
import { CULTURE_TYPES, type Culture } from "@/generators/cultures-generator";
import { Emblems } from "@/generators/emblems-generator";
import { clearLegend, drawLegend } from "@/renderers/draw-legend";
import { EmblemRenderer } from "@/renderers/emblems/renderer";
import { moveCircle, removeCircle } from "@/renderers/overlays/brush-circle";
import { highlightElement } from "@/renderers/overlays/highlight";
import type { Emblem } from "@/types/emblems";
import { downloadFile, getArea, getAreaUnit, getFileName } from "@/utils";
import {
abbreviate,
Expand Down Expand Up @@ -463,7 +466,7 @@ function getBaseOptions(base: number): string {
function getShapeOptions(selectShape: boolean, selected: string): string {
if (!selectShape) return "";

const shapes = Object.keys(COA.shields.types).flatMap(type => Object.keys(COA.shields[type]));
const shapes = Object.keys(Emblems.shields.types).flatMap(type => Object.keys(Emblems.shields[type]));
const options = shapes.map(
shape => `<option ${shape === selected ? "selected" : ""} value="${shape}">${capitalize(shape)}</option>`
);
Expand Down Expand Up @@ -568,11 +571,11 @@ function cultureChangeEmblemsShape(this: HTMLSelectElement): void {
const shape = this.value;
row.dataset.emblems = pack.cultures[culture].shield = shape;

const rerenderCOA = (id: string, coa: any) => {
const rerenderCOA = (id: string, coa: Emblem) => {
const $coa = document.getElementById(id);
if (!$coa) return; // not rendered
$coa.remove();
COArenderer.trigger(id, coa);
EmblemRenderer.trigger(id, coa);
};

pack.states.forEach(state => {
Expand Down Expand Up @@ -1179,7 +1182,7 @@ async function uploadCulturesData(this: HTMLInputElement): Promise<void> {
}));

const { cultures, cells } = pack as any;
const shapes = Object.keys(COA.shields.types).flatMap(type => Object.keys(COA.shields[type]));
const shapes = Object.keys(Emblems.shields.types).flatMap(type => Object.keys(Emblems.shields[type]));

const populated = cells.pop.map((c: number, i: number) => (c ? i : null)).filter((c: number | null) => c);
cultures.forEach((item: any) => {
Expand Down
5 changes: 3 additions & 2 deletions src/controllers/diplomacy-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { Layers } from "@/components/layers";
import { clearMainTip, tip } from "@/components/tooltips";
import { applyDefaultViewboxEvents } from "@/components/viewbox-events";
import type { State } from "@/generators/states-generator";
import { EmblemRenderer } from "@/renderers/emblems/renderer";
import { downloadFile, getFileName } from "@/utils";
import { ensureEl, findEl, getAdjective, getPointer } from "../utils";

Expand Down Expand Up @@ -204,7 +205,7 @@ function renderDiplomacyPage(view: TableView<State>): void {
const selectedId = selectedDiplomacyId;
const selectedName = states[selectedId].name;

COArenderer.trigger(`stateCOA${selectedId}`, states[selectedId].coa);
EmblemRenderer.trigger(`stateCOA${selectedId}`, states[selectedId].coa);
let lines = /* html */ `<div class="states Self" data-id=${selectedId} data-tip="List below shows relations to ${selectedName}">
<div data-col="name"><svg class="coaIcon" viewBox="0 0 200 200"><use href="#stateCOA${selectedId}"></use></svg><span>${states[selectedId].fullName}</span></div>
<div data-col="relations"></div>
Expand All @@ -220,7 +221,7 @@ function renderDiplomacyPage(view: TableView<State>): void {
const tipChange = `Click to change relations. ${tipText}`;

const name = state.fullName!.length < 23 ? state.fullName : state.name;
COArenderer.trigger(`stateCOA${state.i}`, state.coa);
EmblemRenderer.trigger(`stateCOA${state.i}`, state.coa);

lines += /* html */ `<div class="states" data-id=${state.i} data-name="${name}" data-relations="${relation}">
<div data-col="name" data-tip="${tipSelect}"><svg class="coaIcon" viewBox="0 0 200 200"><use href="#stateCOA${state.i}"></use></svg><span>${name}</span></div>
Expand Down
Loading
Loading