Skip to content
Merged
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
10 changes: 10 additions & 0 deletions ui/src/i18n/locales.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1097,6 +1097,16 @@ export const translations: Record<string, Record<Locale, string>> = {
fi: "Ammukset",
uk: "Снаряди",
},
layer_projectile_labels: {
ru: "Подписи снарядов",
en: "Projectile labels",
de: "Projektilbeschriftungen",
cs: "Popisky projektilů",
it: "Etichette proiettili",
fr: "Étiquettes de projectiles",
fi: "Ammusten nimet",
uk: "Підписи снарядів",
},
layer_map_icons: {
ru: "Иконки карты",
en: "Map icons",
Expand Down
22 changes: 22 additions & 0 deletions ui/src/pages/recording-playback/__tests__/ViewSettings.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,28 @@ describe("ViewSettings - map layers", () => {
});
});

describe("ViewSettings - projectile labels", () => {
it("shows the projectile labels checkbox (checked by default)", () => {
const { renderer } = renderViewSettings();
openPanel();

expect(screen.getByText("Projectile labels")).toBeTruthy();
expect(renderer.projectileLabelsVisible()).toBe(true);
});

it("toggles projectile labels via renderer.setProjectileLabelsVisible", () => {
const { renderer } = renderViewSettings();
const spy = vi.spyOn(renderer, "setProjectileLabelsVisible");
openPanel();

fireEvent.click(screen.getByText("Projectile labels"));
expect(spy).toHaveBeenCalledWith(false);

fireEvent.click(screen.getByText("Projectile labels"));
expect(spy).toHaveBeenCalledWith(true);
});
});

describe("ViewSettings - time format", () => {
it("selects a time mode and calls onTimeMode", () => {
const manifest = makeManifest([], [], 200);
Expand Down
29 changes: 29 additions & 0 deletions ui/src/pages/recording-playback/components/ViewSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,35 @@ export function ViewSettings(props: ViewSettingsProps): JSX.Element {
}}
</For>

{/* Projectile labels — toggle name labels while keeping icons */}
<button
class={styles.checkItem}
onClick={() =>
renderer.setProjectileLabelsVisible(!renderer.projectileLabelsVisible())
}
>
<div
class={styles.checkbox}
classList={{
[styles.checkboxActive]: renderer.projectileLabelsVisible(),
[styles.checkboxInactive]: !renderer.projectileLabelsVisible(),
}}
>
<Show when={renderer.projectileLabelsVisible()}>
<div class={styles.checkboxDot} />
</Show>
</div>
<span
class={styles.itemText}
classList={{
[styles.itemTextActive]: renderer.projectileLabelsVisible(),
[styles.itemTextInactive]: !renderer.projectileLabelsVisible(),
}}
>
{t("layer_projectile_labels")}
</span>
</button>

{/* ── Time Format ── */}
<div class={`${styles.sectionLabel} ${styles.sectionBorder}`}>
{t("section_time_format")}
Expand Down
28 changes: 28 additions & 0 deletions ui/src/renderers/leaflet/__tests__/entityCanvasLayer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ function makeConfig(overrides?: Partial<EntityCanvasConfig>): EntityCanvasConfig
nameDisplayMode: () => "all",
layerVisible: () => true,
projectileLayerVisible: () => true,
projectileLabelsVisible: () => true,
worldSize: 30720,
latLngToArma: (ll) => [ll.lng, ll.lat] as [number, number],
...overrides,
Expand Down Expand Up @@ -676,6 +677,7 @@ describe("EntityCanvasLayer — render paths", () => {
nameDisplayMode: () => "all" as const,
layerVisible: () => true,
projectileLayerVisible: () => true,
projectileLabelsVisible: () => true,
worldSize: 30720,
latLngToArma: (ll) => [ll.lng, ll.lat] as [number, number],
};
Expand Down Expand Up @@ -736,6 +738,32 @@ describe("EntityCanvasLayer — render paths", () => {
expect(mockCtx.drawImage).toHaveBeenCalled();
});

it("draws projectile labels when projectile labels are visible", () => {
layer.addProjectile(1, {
iconUrl: "http://example.com/grenade.png",
iconSize: [35, 35],
text: "Shooter Grenade",
});
layer.updateProjectile(1, { position: [100, 100], direction: 0, alpha: 1 });
render();
const texts = mockCtx.fillText.mock.calls.map((c: any[]) => c[0]);
expect(texts).toContain("Shooter Grenade");
});

it("hides projectile labels while keeping icons when disabled", () => {
(layer as any).config.projectileLabelsVisible = () => false;
layer.addProjectile(1, {
iconUrl: "http://example.com/grenade.png",
iconSize: [35, 35],
text: "Shooter Grenade",
});
layer.updateProjectile(1, { position: [100, 100], direction: 0, alpha: 1 });
render();
expect(mockCtx.drawImage).toHaveBeenCalled();
const texts = mockCtx.fillText.mock.calls.map((c: any[]) => c[0]);
expect(texts).not.toContain("Shooter Grenade");
});

it("continues when layer hidden but grid visible", () => {
(layer as any).config.layerVisible = () => false;
layer.setGridVisible(true);
Expand Down
1 change: 1 addition & 0 deletions ui/src/renderers/leaflet/canvasLeafletRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export class CanvasLeafletRenderer extends LeafletRenderer {
nameDisplayMode: () => this.nameDisplayMode(),
layerVisible: () => this.layerVisibility().entities ?? true,
projectileLayerVisible: () => this.layerVisibility().projectileMarkers ?? true,
projectileLabelsVisible: () => this.projectileLabelsVisible(),
worldSize: world.worldSize,
latLngToArma: (ll) => this.latLngToArma(ll),
});
Expand Down
3 changes: 2 additions & 1 deletion ui/src/renderers/leaflet/entityCanvasLayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ export interface EntityCanvasConfig {
nameDisplayMode: () => "players" | "all" | "none";
layerVisible: () => boolean;
projectileLayerVisible: () => boolean;
projectileLabelsVisible: () => boolean;
// Grid
worldSize: number;
latLngToArma: (latlng: L.LatLng) => ArmaCoord;
Expand Down Expand Up @@ -744,7 +745,7 @@ export class EntityCanvasLayer {
ctx.drawImage(img, -dw / 2, -dh / 2, dw, dh);

// Draw label above icon (matching Leaflet popup placement)
if (p.text) {
if (p.text && this.config.projectileLabelsVisible()) {
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.font = fontNormal;
ctx.textAlign = "center";
Expand Down
43 changes: 39 additions & 4 deletions ui/src/renderers/leaflet/leafletRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@ export class LeafletRenderer implements MapRenderer {
private readonly _setNameDisplayMode: Setter<"players" | "all" | "none">;
private readonly _markerDisplayMode: Accessor<"all" | "noLabels" | "none">;
private readonly _setMarkerDisplayMode: Setter<"all" | "noLabels" | "none">;
private readonly _projectileLabels: Accessor<boolean>;
private readonly _setProjectileLabels: Setter<boolean>;
private readonly _mapStylesSig: Accessor<import("../renderer.types").MapStyleInfo[]>;
private readonly _setMapStylesSig: Setter<import("../renderer.types").MapStyleInfo[]>;
private readonly _activeStyleIndexSig: Accessor<number>;
Expand Down Expand Up @@ -177,6 +179,10 @@ export class LeafletRenderer implements MapRenderer {
this._markerDisplayMode = mdm;
this._setMarkerDisplayMode = setMdm;

const [pl, setPl] = createSignal<boolean>(true);
this._projectileLabels = pl;
this._setProjectileLabels = setPl;

const [ms, setMs] = createSignal<import("../renderer.types").MapStyleInfo[]>([]);
this._mapStylesSig = ms;
this._setMapStylesSig = setMs;
Expand Down Expand Up @@ -960,10 +966,16 @@ export class LeafletRenderer implements MapRenderer {
const layerKey = def.layer ?? "briefingMarkers";
layer.addTo(this.layers[layerKey]);

// Open popup after adding to map so the DOM element exists
if (def.text && layer instanceof L.Marker && this._markerDisplayMode() === "all") {
layer.openPopup();
}
// Open popup after adding to map so the DOM element exists.
// Projectile labels are governed independently (see setProjectileLabelsVisible);
// all other briefing markers follow markerDisplayMode.
const showLabel =
def.text &&
layer instanceof L.Marker &&
(layerKey === "projectileMarkers"
? this._projectileLabels()
: this._markerDisplayMode() === "all");
if (showLabel) layer.openPopup();
// Hide text labels when in "noLabels" mode
if (def.type.includes("Empty") && def.text && this._markerDisplayMode() !== "all") {
const el = (layer as L.Marker).getElement?.();
Expand Down Expand Up @@ -1161,6 +1173,7 @@ export class LeafletRenderer implements MapRenderer {
get layerVisibility() { return this._layerVisibility; }
get nameDisplayMode() { return this._nameDisplayMode; }
get markerDisplayMode() { return this._markerDisplayMode; }
get projectileLabelsVisible() { return this._projectileLabels; }
get mapStyles() { return this._mapStylesSig; }
get activeStyleIndex() { return this._activeStyleIndexSig; }

Expand Down Expand Up @@ -1333,6 +1346,28 @@ export class LeafletRenderer implements MapRenderer {
return icon instanceof L.DivIcon && icon.options.className === "marker-text-label";
}

/**
* Toggle projectile labels on/off without removing their icons.
* When disabled, only the projectile icons remain visible (canvas renderer)
* or the popups are closed (DOM/Leaflet renderer).
*/
setProjectileLabelsVisible(visible: boolean): void {
this._setProjectileLabels(visible);

// DOM renderer: open/close popups on projectile markers.
// (The canvas renderer reads the signal directly while drawing.)
const group = this.layers.projectileMarkers;
group.eachLayer((layer) => {
if (!(layer instanceof L.Marker)) return;
if (!layer.getPopup()) return;
if (visible) {
layer.openPopup();
} else {
layer.closePopup();
}
});
}

// ==================== Map styles ====================

setMapStyle(index: number): void {
Expand Down
12 changes: 12 additions & 0 deletions ui/src/renderers/mockRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ export class MockRenderer implements MapRenderer {
private readonly _setNameDisplayMode: Setter<"players" | "all" | "none">;
private readonly _markerDisplayMode: Accessor<"all" | "noLabels" | "none">;
private readonly _setMarkerDisplayMode: Setter<"all" | "noLabels" | "none">;
private readonly _projectileLabels: Accessor<boolean>;
private readonly _setProjectileLabels: Setter<boolean>;
private readonly _mapStyles: Accessor<MapStyleInfo[]>;
private readonly _setMapStyles: Setter<MapStyleInfo[]>;
private readonly _activeStyleIndex: Accessor<number>;
Expand All @@ -52,6 +54,10 @@ export class MockRenderer implements MapRenderer {
this._markerDisplayMode = mdm;
this._setMarkerDisplayMode = setMdm;

const [pl, setPl] = createSignal<boolean>(true);
this._projectileLabels = pl;
this._setProjectileLabels = setPl;

const [ms, setMs] = createSignal<MapStyleInfo[]>([]);
this._mapStyles = ms;
this._setMapStyles = setMs;
Expand All @@ -72,6 +78,8 @@ export class MockRenderer implements MapRenderer {
this._setLayerVisibility = setLv;
}

get projectileLabelsVisible() { return this._projectileLabels; }

init(_container: HTMLElement, _world: WorldConfig): void {
// no-op
}
Expand Down Expand Up @@ -166,6 +174,10 @@ export class MockRenderer implements MapRenderer {
this._setNameDisplayMode(mode);
}

setProjectileLabelsVisible(visible: boolean): void {
this._setProjectileLabels(visible);
}

on(event: RendererEvent, cb: (...args: any[]) => void): void {
let set = this.listeners.get(event);
if (!set) {
Expand Down
4 changes: 4 additions & 0 deletions ui/src/renderers/renderer.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ export interface MapRenderer {
markerDisplayMode: () => "all" | "noLabels" | "none";
setMarkerDisplayMode(mode: "all" | "noLabels" | "none"): void;

// Projectile label visibility (signal accessor)
projectileLabelsVisible: () => boolean;
setProjectileLabelsVisible(visible: boolean): void;

// Map styles (signal accessors)
mapStyles: () => MapStyleInfo[];
activeStyleIndex: () => number;
Expand Down
Loading