mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2026-03-23 15:47:24 +01:00
Refactor code structure for improved readability and maintainability
This commit is contained in:
parent
769ef9eff0
commit
8c78fe2ec1
19 changed files with 2661 additions and 47 deletions
|
|
@ -175,11 +175,13 @@ describe("WebGL2LayerFrameworkClass", () => {
|
|||
});
|
||||
|
||||
it("requestRender() does not throw when called multiple times", () => {
|
||||
vi.stubGlobal("requestAnimationFrame", vi.fn().mockReturnValue(0));
|
||||
expect(() => {
|
||||
framework.requestRender();
|
||||
framework.requestRender();
|
||||
framework.requestRender();
|
||||
}).not.toThrow();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("clearLayer() does not throw and preserves layer registration in the Map", () => {
|
||||
|
|
@ -317,3 +319,243 @@ describe("WebGL2LayerFrameworkClass — init()", () => {
|
|||
expect((framework as any).resizeObserver).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── WebGL2LayerFrameworkClass — lifecycle & render loop (Story 1.3) ───────────
|
||||
|
||||
describe("WebGL2LayerFrameworkClass — lifecycle & render loop (Story 1.3)", () => {
|
||||
let framework: WebGL2LayerFrameworkClass;
|
||||
|
||||
const makeConfig = (id = "terrain") => ({
|
||||
id,
|
||||
anchorLayerId: id,
|
||||
renderOrder: 1,
|
||||
setup: vi.fn(),
|
||||
render: vi.fn(),
|
||||
dispose: vi.fn(),
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
framework = new WebGL2LayerFrameworkClass();
|
||||
vi.stubGlobal("requestAnimationFrame", vi.fn().mockReturnValue(42));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
// ── requestRender() / RAF coalescing ──────────────────────────────────────
|
||||
|
||||
it("requestRender() schedules exactly one RAF for three rapid calls (AC6)", () => {
|
||||
framework.requestRender();
|
||||
framework.requestRender();
|
||||
framework.requestRender();
|
||||
expect((globalThis as any).requestAnimationFrame).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("requestRender() resets rafId to null after the frame callback executes (AC6)", () => {
|
||||
let storedCallback: (() => void) | null = null;
|
||||
vi.stubGlobal(
|
||||
"requestAnimationFrame",
|
||||
vi.fn().mockImplementation((cb: () => void) => {
|
||||
storedCallback = cb;
|
||||
return 42;
|
||||
}),
|
||||
);
|
||||
framework.requestRender();
|
||||
expect((framework as any).rafId).not.toBeNull();
|
||||
storedCallback!();
|
||||
expect((framework as any).rafId).toBeNull();
|
||||
});
|
||||
|
||||
// ── syncTransform() ───────────────────────────────────────────────────────
|
||||
|
||||
it("syncTransform() applies buildCameraBounds(0,0,1,960,540) to camera (AC8)", () => {
|
||||
const mockCamera = {
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
updateProjectionMatrix: vi.fn(),
|
||||
};
|
||||
(framework as any).camera = mockCamera;
|
||||
vi.stubGlobal("viewX", 0);
|
||||
vi.stubGlobal("viewY", 0);
|
||||
vi.stubGlobal("scale", 1);
|
||||
vi.stubGlobal("graphWidth", 960);
|
||||
vi.stubGlobal("graphHeight", 540);
|
||||
framework.syncTransform();
|
||||
const expected = buildCameraBounds(0, 0, 1, 960, 540);
|
||||
expect(mockCamera.left).toBe(expected.left);
|
||||
expect(mockCamera.right).toBe(expected.right);
|
||||
expect(mockCamera.top).toBe(expected.top);
|
||||
expect(mockCamera.bottom).toBe(expected.bottom);
|
||||
expect(mockCamera.updateProjectionMatrix).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("syncTransform() uses ?? defaults when globals are absent (AC8)", () => {
|
||||
const mockCamera = {
|
||||
left: 99,
|
||||
right: 99,
|
||||
top: 99,
|
||||
bottom: 99,
|
||||
updateProjectionMatrix: vi.fn(),
|
||||
};
|
||||
(framework as any).camera = mockCamera;
|
||||
// No globals stubbed — ?? fallbacks (0, 0, 1, 960, 540) take effect
|
||||
framework.syncTransform();
|
||||
const expected = buildCameraBounds(0, 0, 1, 960, 540);
|
||||
expect(mockCamera.left).toBe(expected.left);
|
||||
expect(mockCamera.right).toBe(expected.right);
|
||||
});
|
||||
|
||||
// ── render() — dispatch order ─────────────────────────────────────────────
|
||||
|
||||
it("render() calls syncTransform, then per-layer render, then renderer.render in order (AC7)", () => {
|
||||
const order: string[] = [];
|
||||
const layerRenderFn = vi.fn(() => order.push("layer.render"));
|
||||
const mockRenderer = { render: vi.fn(() => order.push("renderer.render")) };
|
||||
const mockCamera = {
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
updateProjectionMatrix: vi.fn(),
|
||||
};
|
||||
(framework as any).renderer = mockRenderer;
|
||||
(framework as any).scene = {};
|
||||
(framework as any).camera = mockCamera;
|
||||
(framework as any).layers.set("terrain", {
|
||||
config: { ...makeConfig(), render: layerRenderFn },
|
||||
group: { visible: true },
|
||||
});
|
||||
const syncSpy = vi
|
||||
.spyOn(framework as any, "syncTransform")
|
||||
.mockImplementation(() => order.push("syncTransform"));
|
||||
vi.stubGlobal(
|
||||
"requestAnimationFrame",
|
||||
vi.fn().mockImplementation((cb: () => void) => {
|
||||
cb();
|
||||
return 1;
|
||||
}),
|
||||
);
|
||||
framework.requestRender();
|
||||
expect(order).toEqual(["syncTransform", "layer.render", "renderer.render"]);
|
||||
syncSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("render() skips invisible layers — config.render not called (AC7)", () => {
|
||||
const invisibleRenderFn = vi.fn();
|
||||
const mockRenderer = { render: vi.fn() };
|
||||
(framework as any).renderer = mockRenderer;
|
||||
(framework as any).scene = {};
|
||||
(framework as any).camera = {
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
updateProjectionMatrix: vi.fn(),
|
||||
};
|
||||
(framework as any).layers.set("terrain", {
|
||||
config: { ...makeConfig(), render: invisibleRenderFn },
|
||||
group: { visible: false },
|
||||
});
|
||||
vi.stubGlobal(
|
||||
"requestAnimationFrame",
|
||||
vi.fn().mockImplementation((cb: () => void) => {
|
||||
cb();
|
||||
return 1;
|
||||
}),
|
||||
);
|
||||
framework.requestRender();
|
||||
expect(invisibleRenderFn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// ── setVisible() ──────────────────────────────────────────────────────────
|
||||
|
||||
it("setVisible(false) sets group.visible=false without calling dispose (AC3, NFR-P6)", () => {
|
||||
const config = makeConfig();
|
||||
const group = { visible: true };
|
||||
(framework as any).layers.set("terrain", { config, group });
|
||||
(framework as any).canvas = { style: { display: "block" } };
|
||||
framework.setVisible("terrain", false);
|
||||
expect(group.visible).toBe(false);
|
||||
expect(config.dispose).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("setVisible(false) hides canvas when all layers become invisible (AC3)", () => {
|
||||
const canvas = { style: { display: "block" } };
|
||||
(framework as any).canvas = canvas;
|
||||
(framework as any).layers.set("terrain", {
|
||||
config: makeConfig(),
|
||||
group: { visible: true },
|
||||
});
|
||||
(framework as any).layers.set("rivers", {
|
||||
config: makeConfig("rivers"),
|
||||
group: { visible: false },
|
||||
});
|
||||
framework.setVisible("terrain", false);
|
||||
expect(canvas.style.display).toBe("none");
|
||||
});
|
||||
|
||||
it("setVisible(true) calls requestRender() (AC4)", () => {
|
||||
const group = { visible: false };
|
||||
(framework as any).layers.set("terrain", { config: makeConfig(), group });
|
||||
(framework as any).canvas = { style: { display: "none" } };
|
||||
const renderSpy = vi.spyOn(framework, "requestRender");
|
||||
framework.setVisible("terrain", true);
|
||||
expect(group.visible).toBe(true);
|
||||
expect(renderSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
// ── clearLayer() ──────────────────────────────────────────────────────────
|
||||
|
||||
it("clearLayer() calls group.clear() and preserves layer in the Map (AC5)", () => {
|
||||
const clearFn = vi.fn();
|
||||
(framework as any).layers.set("terrain", {
|
||||
config: makeConfig(),
|
||||
group: { visible: true, clear: clearFn },
|
||||
});
|
||||
framework.clearLayer("terrain");
|
||||
expect(clearFn).toHaveBeenCalledOnce();
|
||||
expect((framework as any).layers.has("terrain")).toBe(true);
|
||||
});
|
||||
|
||||
it("clearLayer() does not call renderer.dispose (AC5, NFR-P6)", () => {
|
||||
const mockRenderer = { render: vi.fn(), dispose: vi.fn() };
|
||||
(framework as any).renderer = mockRenderer;
|
||||
(framework as any).layers.set("terrain", {
|
||||
config: makeConfig(),
|
||||
group: { visible: true, clear: vi.fn() },
|
||||
});
|
||||
framework.clearLayer("terrain");
|
||||
expect(mockRenderer.dispose).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// ── unregister() ──────────────────────────────────────────────────────────
|
||||
|
||||
it("unregister() calls dispose, removes from scene and Map (AC9)", () => {
|
||||
const config = makeConfig();
|
||||
const group = { visible: true };
|
||||
const mockScene = { remove: vi.fn() };
|
||||
(framework as any).scene = mockScene;
|
||||
(framework as any).canvas = { style: { display: "block" } };
|
||||
(framework as any).layers.set("terrain", { config, group });
|
||||
framework.unregister("terrain");
|
||||
expect(config.dispose).toHaveBeenCalledWith(group);
|
||||
expect(mockScene.remove).toHaveBeenCalledWith(group);
|
||||
expect((framework as any).layers.has("terrain")).toBe(false);
|
||||
});
|
||||
|
||||
it("unregister() hides canvas when it was the last registered layer (AC9)", () => {
|
||||
const canvas = { style: { display: "block" } };
|
||||
(framework as any).canvas = canvas;
|
||||
(framework as any).scene = { remove: vi.fn() };
|
||||
(framework as any).layers.set("terrain", {
|
||||
config: makeConfig(),
|
||||
group: { visible: true },
|
||||
});
|
||||
framework.unregister("terrain");
|
||||
expect(canvas.style.display).toBe("none");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -88,13 +88,11 @@ interface RegisteredLayer {
|
|||
export class WebGL2LayerFrameworkClass {
|
||||
private canvas: HTMLCanvasElement | null = null;
|
||||
private renderer: WebGLRenderer | null = null;
|
||||
// biome-ignore lint/correctness/noUnusedPrivateClassMembers: assigned in init(); read in Story 1.3 render() + syncTransform()
|
||||
private camera: OrthographicCamera | null = null;
|
||||
private scene: Scene | null = null;
|
||||
private layers: Map<string, RegisteredLayer> = new Map();
|
||||
private pendingConfigs: WebGLLayerConfig[] = []; // queue for register() before init()
|
||||
private resizeObserver: ResizeObserver | null = null;
|
||||
// biome-ignore lint/correctness/noUnusedPrivateClassMembers: read/written in Story 1.3 requestRender()
|
||||
private rafId: number | null = null;
|
||||
private container: HTMLElement | null = null;
|
||||
|
||||
|
|
@ -190,25 +188,64 @@ export class WebGL2LayerFrameworkClass {
|
|||
this.layers.set(config.id, { config, group });
|
||||
}
|
||||
|
||||
unregister(_id: string): void {
|
||||
// Story 1.3: call config.dispose(group); remove from layers Map; cleanup canvas if empty.
|
||||
unregister(id: string): void {
|
||||
if (this._fallback) return;
|
||||
const layer = this.layers.get(id);
|
||||
if (!layer || !this.scene) return;
|
||||
const scene = this.scene;
|
||||
layer.config.dispose(layer.group);
|
||||
scene.remove(layer.group);
|
||||
this.layers.delete(id);
|
||||
const anyVisible = [...this.layers.values()].some((l) => l.group.visible);
|
||||
if (this.canvas && !anyVisible) this.canvas.style.display = "none";
|
||||
}
|
||||
|
||||
setVisible(_id: string, _visible: boolean): void {
|
||||
// Story 1.3: toggle group.visible; hide canvas only when ALL layers invisible (NFR-P6).
|
||||
setVisible(id: string, visible: boolean): void {
|
||||
if (this._fallback) return;
|
||||
const layer = this.layers.get(id);
|
||||
if (!layer) return;
|
||||
layer.group.visible = visible;
|
||||
const anyVisible = [...this.layers.values()].some((l) => l.group.visible);
|
||||
if (this.canvas) this.canvas.style.display = anyVisible ? "block" : "none";
|
||||
if (visible) this.requestRender();
|
||||
}
|
||||
|
||||
clearLayer(_id: string): void {
|
||||
// Story 1.3: group.clear() — wipes Mesh children without disposing renderer (NFR-P6).
|
||||
clearLayer(id: string): void {
|
||||
if (this._fallback) return;
|
||||
const layer = this.layers.get(id);
|
||||
if (!layer) return;
|
||||
layer.group.clear();
|
||||
}
|
||||
|
||||
requestRender(): void {
|
||||
// Story 1.3: RAF-coalesced render request; schedules this.render() via requestAnimationFrame.
|
||||
this.render();
|
||||
if (this._fallback) return;
|
||||
if (this.rafId !== null) return;
|
||||
this.rafId = requestAnimationFrame(() => {
|
||||
this.rafId = null;
|
||||
this.render();
|
||||
});
|
||||
}
|
||||
|
||||
syncTransform(): void {
|
||||
// Story 1.3: read window globals viewX/viewY/scale; apply buildCameraBounds to camera.
|
||||
if (this._fallback || !this.camera) return;
|
||||
const camera = this.camera;
|
||||
const viewX = (globalThis as any).viewX ?? 0;
|
||||
const viewY = (globalThis as any).viewY ?? 0;
|
||||
const scale = (globalThis as any).scale ?? 1;
|
||||
const graphWidth = (globalThis as any).graphWidth ?? 960;
|
||||
const graphHeight = (globalThis as any).graphHeight ?? 540;
|
||||
const bounds = buildCameraBounds(
|
||||
viewX,
|
||||
viewY,
|
||||
scale,
|
||||
graphWidth,
|
||||
graphHeight,
|
||||
);
|
||||
camera.left = bounds.left;
|
||||
camera.right = bounds.right;
|
||||
camera.top = bounds.top;
|
||||
camera.bottom = bounds.bottom;
|
||||
camera.updateProjectionMatrix();
|
||||
}
|
||||
|
||||
// ─── Private helpers ───────────────────────────────────────────────────────
|
||||
|
|
@ -232,7 +269,17 @@ export class WebGL2LayerFrameworkClass {
|
|||
}
|
||||
|
||||
private render(): void {
|
||||
// Story 1.3: syncTransform → per-layer render(group) callbacks → renderer.render(scene, camera).
|
||||
if (this._fallback || !this.renderer || !this.scene || !this.camera) return;
|
||||
const renderer = this.renderer;
|
||||
const scene = this.scene;
|
||||
const camera = this.camera;
|
||||
this.syncTransform();
|
||||
for (const layer of this.layers.values()) {
|
||||
if (layer.group.visible) {
|
||||
layer.config.render(layer.group);
|
||||
}
|
||||
}
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue