mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 09:10:42 +00:00
Add renderer status panel
This commit is contained in:
@@ -343,6 +343,7 @@
|
||||
<chat-modal></chat-modal>
|
||||
<multi-tab-modal></multi-tab-modal>
|
||||
<game-left-sidebar></game-left-sidebar>
|
||||
<renderer-status-panel></renderer-status-panel>
|
||||
<performance-overlay></performance-overlay>
|
||||
<webgpu-debug-overlay></webgpu-debug-overlay>
|
||||
<player-info-overlay></player-info-overlay>
|
||||
|
||||
@@ -33,6 +33,7 @@ import { PerformanceOverlay } from "./layers/PerformanceOverlay";
|
||||
import { PlayerInfoOverlay } from "./layers/PlayerInfoOverlay";
|
||||
import { PlayerPanel } from "./layers/PlayerPanel";
|
||||
import { RailroadLayer } from "./layers/RailroadLayer";
|
||||
import { RendererStatusPanel } from "./layers/RendererStatusPanel";
|
||||
import { ReplayPanel } from "./layers/ReplayPanel";
|
||||
import { SAMRadiusLayer } from "./layers/SAMRadiusLayer";
|
||||
import { SettingsModal } from "./layers/SettingsModal";
|
||||
@@ -252,6 +253,15 @@ export function createRenderer(
|
||||
webgpuDebugOverlay.userSettings = userSettings;
|
||||
webgpuDebugOverlay.requestUpdate();
|
||||
|
||||
const rendererStatusPanel = document.querySelector(
|
||||
"renderer-status-panel",
|
||||
) as RendererStatusPanel;
|
||||
if (!(rendererStatusPanel instanceof RendererStatusPanel)) {
|
||||
console.error("renderer status panel not found");
|
||||
}
|
||||
rendererStatusPanel.userSettings = userSettings;
|
||||
rendererStatusPanel.requestUpdate();
|
||||
|
||||
const alertFrame = document.querySelector("alert-frame") as AlertFrame;
|
||||
if (!(alertFrame instanceof AlertFrame)) {
|
||||
console.error("alert frame not found");
|
||||
@@ -285,6 +295,7 @@ export function createRenderer(
|
||||
// Try to group layers by the return value of shouldTransform.
|
||||
// Not grouping the layers may cause excessive calls to context.save() and context.restore().
|
||||
const layers: Layer[] = [
|
||||
rendererStatusPanel,
|
||||
new TerritoryLayer(game, eventBus, transformHandler, userSettings),
|
||||
new RailroadLayer(game, eventBus, transformHandler, uiState),
|
||||
new CoordinateGridLayer(game, eventBus, transformHandler),
|
||||
|
||||
@@ -0,0 +1,383 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import {
|
||||
TERRITORY_RENDERER_KEY,
|
||||
USER_SETTINGS_CHANGED_EVENT,
|
||||
UserSettings,
|
||||
} from "../../../core/game/UserSettings";
|
||||
import { Layer } from "./Layer";
|
||||
import {
|
||||
TERRITORY_RENDERER_OPTIONS,
|
||||
TERRITORY_RENDERER_STATUS_EVENT,
|
||||
TerritoryRendererId,
|
||||
TerritoryRendererPreference,
|
||||
TerritoryRendererStatus,
|
||||
} from "./TerritoryBackend";
|
||||
|
||||
@customElement("renderer-status-panel")
|
||||
export class RendererStatusPanel extends LitElement implements Layer {
|
||||
@property({ type: Object })
|
||||
public userSettings!: UserSettings;
|
||||
|
||||
@state()
|
||||
private activeRenderer: TerritoryRendererId | null = null;
|
||||
|
||||
@state()
|
||||
private preference: TerritoryRendererPreference = "auto";
|
||||
|
||||
@state()
|
||||
private failedBackends: TerritoryRendererId[] = [];
|
||||
|
||||
@state()
|
||||
private message: string | null = null;
|
||||
|
||||
@state()
|
||||
private position: { x: number; y: number } | null = null;
|
||||
|
||||
@state()
|
||||
private isDragging = false;
|
||||
|
||||
private dragState: {
|
||||
pointerId: number;
|
||||
offsetX: number;
|
||||
offsetY: number;
|
||||
} | null = null;
|
||||
|
||||
private readonly positionStorageKey = "rendererStatusPanel.position.v1";
|
||||
|
||||
static styles = css`
|
||||
.panel {
|
||||
position: fixed;
|
||||
left: 16px;
|
||||
bottom: 16px;
|
||||
z-index: 9998;
|
||||
width: min(280px, calc(100vw - 32px));
|
||||
box-sizing: border-box;
|
||||
border: 1px solid rgba(255, 255, 255, 0.16);
|
||||
border-radius: 8px;
|
||||
background: rgba(13, 16, 20, 0.86);
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
font-family:
|
||||
Inter,
|
||||
ui-sans-serif,
|
||||
system-ui,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"Segoe UI",
|
||||
sans-serif;
|
||||
font-size: 12px;
|
||||
line-height: 1.35;
|
||||
pointer-events: auto;
|
||||
user-select: none;
|
||||
box-shadow: 0 14px 32px rgba(0, 0, 0, 0.24);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.panel.dragging {
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 8px 10px 6px;
|
||||
cursor: grab;
|
||||
touch-action: none;
|
||||
font-weight: 700;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.panel.dragging .title {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.body {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
padding: 8px 10px 10px;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: grid;
|
||||
grid-template-columns: 72px 1fr;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: rgba(255, 255, 255, 0.62);
|
||||
}
|
||||
|
||||
.value {
|
||||
min-width: 0;
|
||||
color: rgba(255, 255, 255, 0.94);
|
||||
font-weight: 650;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.active {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: rgb(67, 214, 142);
|
||||
box-shadow: 0 0 0 3px rgba(67, 214, 142, 0.16);
|
||||
}
|
||||
|
||||
select {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
border: 1px solid rgba(255, 255, 255, 0.16);
|
||||
border-radius: 6px;
|
||||
background: rgba(0, 0, 0, 0.38);
|
||||
color: rgba(255, 255, 255, 0.94);
|
||||
padding: 5px 7px;
|
||||
font: inherit;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.note {
|
||||
color: rgba(255, 255, 255, 0.66);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
`;
|
||||
|
||||
init() {
|
||||
this.preference = this.userSettings.territoryRenderer();
|
||||
this.restorePosition();
|
||||
globalThis.addEventListener(
|
||||
TERRITORY_RENDERER_STATUS_EVENT,
|
||||
this.handleRendererStatus,
|
||||
);
|
||||
globalThis.addEventListener(
|
||||
`${USER_SETTINGS_CHANGED_EVENT}:${TERRITORY_RENDERER_KEY}`,
|
||||
this.handlePreferenceChanged,
|
||||
);
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this.endDrag();
|
||||
globalThis.removeEventListener(
|
||||
TERRITORY_RENDERER_STATUS_EVENT,
|
||||
this.handleRendererStatus,
|
||||
);
|
||||
globalThis.removeEventListener(
|
||||
`${USER_SETTINGS_CHANGED_EVENT}:${TERRITORY_RENDERER_KEY}`,
|
||||
this.handlePreferenceChanged,
|
||||
);
|
||||
}
|
||||
|
||||
private readonly handleRendererStatus = (event: Event) => {
|
||||
const detail = (event as CustomEvent<TerritoryRendererStatus>).detail;
|
||||
if (!detail) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.activeRenderer = detail.active;
|
||||
this.preference = detail.preference;
|
||||
this.failedBackends = detail.failedBackends;
|
||||
this.message = detail.message;
|
||||
};
|
||||
|
||||
private readonly handlePreferenceChanged = () => {
|
||||
if (!this.userSettings) {
|
||||
return;
|
||||
}
|
||||
this.preference = this.userSettings.territoryRenderer();
|
||||
this.message = null;
|
||||
};
|
||||
|
||||
private changeRenderer(event: Event) {
|
||||
const value = (event.target as HTMLSelectElement).value;
|
||||
this.userSettings.setTerritoryRenderer(value);
|
||||
this.preference = this.userSettings.territoryRenderer();
|
||||
}
|
||||
|
||||
private rendererLabel(id: TerritoryRendererId | TerritoryRendererPreference) {
|
||||
if (id === "webgpu") return "WebGPU";
|
||||
if (id === "webgl") return "WebGL";
|
||||
if (id === "classic") return "Classic";
|
||||
return "Auto";
|
||||
}
|
||||
|
||||
private statusNote() {
|
||||
if (this.failedBackends.length > 0) {
|
||||
return `Skipped this cycle: ${this.failedBackends
|
||||
.map((id) => this.rendererLabel(id))
|
||||
.join(", ")}`;
|
||||
}
|
||||
if (
|
||||
this.activeRenderer &&
|
||||
this.preference !== "auto" &&
|
||||
this.activeRenderer !== this.preference
|
||||
) {
|
||||
return `Fallback from ${this.rendererLabel(this.preference)}`;
|
||||
}
|
||||
return this.message;
|
||||
}
|
||||
|
||||
private restorePosition() {
|
||||
try {
|
||||
const raw = localStorage.getItem(this.positionStorageKey);
|
||||
if (!raw) {
|
||||
return;
|
||||
}
|
||||
const parsed = JSON.parse(raw) as { x: unknown; y: unknown };
|
||||
if (
|
||||
typeof parsed.x === "number" &&
|
||||
typeof parsed.y === "number" &&
|
||||
Number.isFinite(parsed.x) &&
|
||||
Number.isFinite(parsed.y)
|
||||
) {
|
||||
this.position = this.clampPosition(parsed.x, parsed.y);
|
||||
}
|
||||
} catch {
|
||||
// Keep the default docked position.
|
||||
}
|
||||
}
|
||||
|
||||
private savePosition() {
|
||||
if (!this.position) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
localStorage.setItem(
|
||||
this.positionStorageKey,
|
||||
JSON.stringify(this.position),
|
||||
);
|
||||
} catch {
|
||||
// Position persistence is best-effort.
|
||||
}
|
||||
}
|
||||
|
||||
private clampPosition(x: number, y: number) {
|
||||
const panel = this.renderRoot.querySelector(".panel") as HTMLElement | null;
|
||||
const width = panel?.offsetWidth ?? 280;
|
||||
const height = panel?.offsetHeight ?? 120;
|
||||
const margin = 8;
|
||||
return {
|
||||
x: Math.max(margin, Math.min(window.innerWidth - width - margin, x)),
|
||||
y: Math.max(margin, Math.min(window.innerHeight - height - margin, y)),
|
||||
};
|
||||
}
|
||||
|
||||
private panelStyle() {
|
||||
if (!this.position) {
|
||||
return "";
|
||||
}
|
||||
return `left: ${this.position.x}px; top: ${this.position.y}px; bottom: auto;`;
|
||||
}
|
||||
|
||||
private stopPointerEvent(event: PointerEvent) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
private handleDragPointerDown(event: PointerEvent) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const panel = this.renderRoot.querySelector(".panel") as HTMLElement | null;
|
||||
if (!panel) {
|
||||
return;
|
||||
}
|
||||
const rect = panel.getBoundingClientRect();
|
||||
this.position = { x: rect.left, y: rect.top };
|
||||
this.isDragging = true;
|
||||
this.dragState = {
|
||||
pointerId: event.pointerId,
|
||||
offsetX: event.clientX - rect.left,
|
||||
offsetY: event.clientY - rect.top,
|
||||
};
|
||||
|
||||
globalThis.addEventListener("pointermove", this.handleDragPointerMove);
|
||||
globalThis.addEventListener("pointerup", this.handleDragPointerUp);
|
||||
globalThis.addEventListener("pointercancel", this.handleDragPointerUp);
|
||||
}
|
||||
|
||||
private readonly handleDragPointerMove = (event: PointerEvent) => {
|
||||
if (!this.dragState || event.pointerId !== this.dragState.pointerId) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.position = this.clampPosition(
|
||||
event.clientX - this.dragState.offsetX,
|
||||
event.clientY - this.dragState.offsetY,
|
||||
);
|
||||
};
|
||||
|
||||
private readonly handleDragPointerUp = (event: PointerEvent) => {
|
||||
if (!this.dragState || event.pointerId !== this.dragState.pointerId) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.savePosition();
|
||||
this.endDrag();
|
||||
};
|
||||
|
||||
private endDrag() {
|
||||
globalThis.removeEventListener("pointermove", this.handleDragPointerMove);
|
||||
globalThis.removeEventListener("pointerup", this.handleDragPointerUp);
|
||||
globalThis.removeEventListener("pointercancel", this.handleDragPointerUp);
|
||||
this.dragState = null;
|
||||
this.isDragging = false;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.userSettings) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const note = this.statusNote();
|
||||
return html`
|
||||
<div
|
||||
class="panel ${this.isDragging ? "dragging" : ""}"
|
||||
style=${this.panelStyle()}
|
||||
@pointerdown=${this.stopPointerEvent}
|
||||
>
|
||||
<div class="title" @pointerdown=${this.handleDragPointerDown}>
|
||||
<span>Renderer</span>
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="row">
|
||||
<div class="label">Active</div>
|
||||
<div class="value active">
|
||||
<span class="dot"></span>
|
||||
${this.activeRenderer
|
||||
? this.rendererLabel(this.activeRenderer)
|
||||
: "Pending"}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label class="label" for="renderer-select">Saved</label>
|
||||
<select
|
||||
id="renderer-select"
|
||||
.value=${this.preference}
|
||||
@change=${this.changeRenderer}
|
||||
>
|
||||
${TERRITORY_RENDERER_OPTIONS.map(
|
||||
(option) =>
|
||||
html`<option value=${option}>
|
||||
${this.rendererLabel(option)}
|
||||
</option>`,
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
${note ? html`<div class="note">${note}</div>` : null}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@ import { Layer } from "./Layer";
|
||||
|
||||
export type TerritoryRendererId = "classic" | "webgl" | "webgpu";
|
||||
export type TerritoryRendererPreference = "auto" | TerritoryRendererId;
|
||||
export const TERRITORY_RENDERER_STATUS_EVENT =
|
||||
"event:territory-renderer-status";
|
||||
|
||||
export const TERRITORY_RENDERER_OPTIONS: TerritoryRendererPreference[] = [
|
||||
"auto",
|
||||
@@ -17,6 +19,13 @@ export interface TerritoryBackend extends Layer {
|
||||
whenReady?: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
export interface TerritoryRendererStatus {
|
||||
active: TerritoryRendererId | null;
|
||||
preference: TerritoryRendererPreference;
|
||||
failedBackends: TerritoryRendererId[];
|
||||
message: string | null;
|
||||
}
|
||||
|
||||
export interface TerritoryBackendCandidate {
|
||||
readonly id: TerritoryRendererId;
|
||||
init?: () => void | Promise<void>;
|
||||
|
||||
@@ -8,8 +8,10 @@ import {
|
||||
import { TransformHandler } from "../TransformHandler";
|
||||
import { ClassicTerritoryBackend } from "./ClassicTerritoryBackend";
|
||||
import {
|
||||
TERRITORY_RENDERER_STATUS_EVENT,
|
||||
TerritoryBackend,
|
||||
TerritoryRendererId,
|
||||
TerritoryRendererStatus,
|
||||
selectTerritoryBackend,
|
||||
territoryRendererOrder,
|
||||
} from "./TerritoryBackend";
|
||||
@@ -25,6 +27,7 @@ export class TerritoryLayer implements TerritoryBackend {
|
||||
private initialized = false;
|
||||
private readonly settingsChanged = () => {
|
||||
this.failedBackends.clear();
|
||||
this.publishStatus("Retrying renderer selection");
|
||||
void this.selectConfiguredBackend();
|
||||
};
|
||||
|
||||
@@ -51,7 +54,10 @@ export class TerritoryLayer implements TerritoryBackend {
|
||||
);
|
||||
|
||||
// Keep the map visible while accelerated renderers initialize.
|
||||
this.activateBackend(this.createBackend("classic"));
|
||||
this.activateBackend(
|
||||
this.createBackend("classic"),
|
||||
"Using Classic while accelerated renderer initializes",
|
||||
);
|
||||
void this.selectConfiguredBackend();
|
||||
}
|
||||
|
||||
@@ -122,6 +128,8 @@ export class TerritoryLayer implements TerritoryBackend {
|
||||
|
||||
if (selection.backend !== null) {
|
||||
this.activateBackend(selection.backend);
|
||||
} else {
|
||||
this.publishStatus("No territory renderer is currently available");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,7 +169,10 @@ export class TerritoryLayer implements TerritoryBackend {
|
||||
}
|
||||
}
|
||||
|
||||
private activateBackend(backend: TerritoryBackend) {
|
||||
private activateBackend(
|
||||
backend: TerritoryBackend,
|
||||
message: string | null = null,
|
||||
) {
|
||||
if (this.activeBackend === backend) {
|
||||
return;
|
||||
}
|
||||
@@ -169,6 +180,7 @@ export class TerritoryLayer implements TerritoryBackend {
|
||||
this.activeBackend = backend;
|
||||
previous?.dispose?.();
|
||||
console.info(`[TerritoryLayer] active renderer: ${backend.id}`);
|
||||
this.publishStatus(message);
|
||||
}
|
||||
|
||||
private runActive(
|
||||
@@ -196,6 +208,7 @@ export class TerritoryLayer implements TerritoryBackend {
|
||||
if (backend.id !== "classic") {
|
||||
this.failedBackends.add(backend.id);
|
||||
}
|
||||
this.publishStatus(`${backend.id} failed: ${reason}`);
|
||||
if (this.activeBackend === backend) {
|
||||
this.activeBackend = null;
|
||||
backend.dispose?.();
|
||||
@@ -241,4 +254,17 @@ export class TerritoryLayer implements TerritoryBackend {
|
||||
context.fillRect(0, 0, context.canvas.width, context.canvas.height);
|
||||
context.restore();
|
||||
}
|
||||
|
||||
private publishStatus(message: string | null = null) {
|
||||
const detail: TerritoryRendererStatus = {
|
||||
active: this.activeBackend?.id ?? null,
|
||||
preference: this.userSettings.territoryRenderer(),
|
||||
failedBackends: Array.from(this.failedBackends),
|
||||
message,
|
||||
};
|
||||
|
||||
globalThis.dispatchEvent?.(
|
||||
new CustomEvent(TERRITORY_RENDERER_STATUS_EVENT, { detail }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,19 @@ export class WebGPUDebugOverlay extends LitElement implements Layer {
|
||||
@state()
|
||||
private tickComputeMs: number = 0;
|
||||
|
||||
@state()
|
||||
private position: { x: number; y: number } | null = null;
|
||||
|
||||
@state()
|
||||
private isDragging = false;
|
||||
|
||||
private frameTimes: number[] = [];
|
||||
private dragState: {
|
||||
pointerId: number;
|
||||
offsetX: number;
|
||||
offsetY: number;
|
||||
} | null = null;
|
||||
private readonly positionStorageKey = "webgpuDebugOverlay.position.v1";
|
||||
|
||||
static styles = css`
|
||||
.overlay {
|
||||
@@ -71,6 +83,10 @@ export class WebGPUDebugOverlay extends LitElement implements Layer {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.overlay.dragging {
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 700;
|
||||
margin-bottom: 8px;
|
||||
@@ -78,6 +94,12 @@ export class WebGPUDebugOverlay extends LitElement implements Layer {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
cursor: grab;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.overlay.dragging .title {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.metrics {
|
||||
@@ -154,6 +176,7 @@ export class WebGPUDebugOverlay extends LitElement implements Layer {
|
||||
`;
|
||||
|
||||
init() {
|
||||
this.restorePosition();
|
||||
this.eventBus.on(WebGPUComputeMetricsEvent, (e) => {
|
||||
if (typeof e.computeMs === "number" && Number.isFinite(e.computeMs)) {
|
||||
this.tickComputeMs = e.computeMs;
|
||||
@@ -163,6 +186,11 @@ export class WebGPUDebugOverlay extends LitElement implements Layer {
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this.endDrag();
|
||||
}
|
||||
|
||||
updateFrameMetrics(frameDurationMs: number): void {
|
||||
if (!this.userSettings || !this.userSettings.webgpuDebug()) {
|
||||
return;
|
||||
@@ -301,6 +329,118 @@ export class WebGPUDebugOverlay extends LitElement implements Layer {
|
||||
`;
|
||||
}
|
||||
|
||||
private restorePosition() {
|
||||
try {
|
||||
const raw = localStorage.getItem(this.positionStorageKey);
|
||||
if (!raw) {
|
||||
return;
|
||||
}
|
||||
const parsed = JSON.parse(raw) as { x: unknown; y: unknown };
|
||||
if (
|
||||
typeof parsed.x === "number" &&
|
||||
typeof parsed.y === "number" &&
|
||||
Number.isFinite(parsed.x) &&
|
||||
Number.isFinite(parsed.y)
|
||||
) {
|
||||
this.position = this.clampPosition(parsed.x, parsed.y);
|
||||
}
|
||||
} catch {
|
||||
// Keep the default position.
|
||||
}
|
||||
}
|
||||
|
||||
private savePosition() {
|
||||
if (!this.position) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
localStorage.setItem(
|
||||
this.positionStorageKey,
|
||||
JSON.stringify(this.position),
|
||||
);
|
||||
} catch {
|
||||
// Position persistence is best-effort.
|
||||
}
|
||||
}
|
||||
|
||||
private clampPosition(x: number, y: number) {
|
||||
const overlay = this.renderRoot.querySelector(
|
||||
".overlay",
|
||||
) as HTMLElement | null;
|
||||
const width = overlay?.offsetWidth ?? 340;
|
||||
const height = overlay?.offsetHeight ?? 420;
|
||||
const margin = 8;
|
||||
return {
|
||||
x: Math.max(margin, Math.min(window.innerWidth - width - margin, x)),
|
||||
y: Math.max(margin, Math.min(window.innerHeight - height - margin, y)),
|
||||
};
|
||||
}
|
||||
|
||||
private overlayStyle() {
|
||||
if (!this.position) {
|
||||
return "";
|
||||
}
|
||||
return `left: ${this.position.x}px; top: ${this.position.y}px;`;
|
||||
}
|
||||
|
||||
private stopPointerEvent(event: PointerEvent) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
private handleDragPointerDown(event: PointerEvent) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const overlay = this.renderRoot.querySelector(
|
||||
".overlay",
|
||||
) as HTMLElement | null;
|
||||
if (!overlay) {
|
||||
return;
|
||||
}
|
||||
const rect = overlay.getBoundingClientRect();
|
||||
this.position = { x: rect.left, y: rect.top };
|
||||
this.isDragging = true;
|
||||
this.dragState = {
|
||||
pointerId: event.pointerId,
|
||||
offsetX: event.clientX - rect.left,
|
||||
offsetY: event.clientY - rect.top,
|
||||
};
|
||||
|
||||
globalThis.addEventListener("pointermove", this.handleDragPointerMove);
|
||||
globalThis.addEventListener("pointerup", this.handleDragPointerUp);
|
||||
globalThis.addEventListener("pointercancel", this.handleDragPointerUp);
|
||||
}
|
||||
|
||||
private readonly handleDragPointerMove = (event: PointerEvent) => {
|
||||
if (!this.dragState || event.pointerId !== this.dragState.pointerId) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.position = this.clampPosition(
|
||||
event.clientX - this.dragState.offsetX,
|
||||
event.clientY - this.dragState.offsetY,
|
||||
);
|
||||
};
|
||||
|
||||
private readonly handleDragPointerUp = (event: PointerEvent) => {
|
||||
if (!this.dragState || event.pointerId !== this.dragState.pointerId) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.savePosition();
|
||||
this.endDrag();
|
||||
};
|
||||
|
||||
private endDrag() {
|
||||
globalThis.removeEventListener("pointermove", this.handleDragPointerMove);
|
||||
globalThis.removeEventListener("pointerup", this.handleDragPointerUp);
|
||||
globalThis.removeEventListener("pointercancel", this.handleDragPointerUp);
|
||||
this.dragState = null;
|
||||
this.isDragging = false;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.userSettings || !this.userSettings.webgpuDebug()) {
|
||||
return null;
|
||||
@@ -323,8 +463,12 @@ export class WebGPUDebugOverlay extends LitElement implements Layer {
|
||||
TERRITORY_POST_SMOOTHING[0];
|
||||
|
||||
return html`
|
||||
<div class="overlay">
|
||||
<div class="title">
|
||||
<div
|
||||
class="overlay ${this.isDragging ? "dragging" : ""}"
|
||||
style=${this.overlayStyle()}
|
||||
@pointerdown=${this.stopPointerEvent}
|
||||
>
|
||||
<div class="title" @pointerdown=${this.handleDragPointerDown}>
|
||||
<div>WebGPU Debug</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user