Merge remote-tracking branch 'origin/sub_laptop_8' into main_laptop_1
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import type { ScreenCanvasDrawController } from '../drawControllers/screenCanvas.drawController';
|
||||
import { getGridEnabled, getHighlightedEntityIds, setHighlightedEntityIds } from '../state';
|
||||
import { getHighlightedEntityIds, setHighlightedEntityIds } from '../state';
|
||||
import { drawEntities } from './draw-functions';
|
||||
import { getSceneVersion } from './scene-version';
|
||||
import { queryEntitiesInBox } from './spatial-index';
|
||||
@@ -78,7 +78,8 @@ function rebuildScene(drawController: ScreenCanvasDrawController): void {
|
||||
const savedHighlight = getHighlightedEntityIds();
|
||||
if (savedHighlight.length) setHighlightedEntityIds([]);
|
||||
drawController.withContext(offscreenContext, () => {
|
||||
drawController.clear();
|
||||
// 투명 바탕 — 배경 · 모눈은 화면 clear() 가 그림(모눈이 비트맵에 박히면 매 프레임 다시 그려야 함)
|
||||
offscreenContext.clearRect(0, 0, size.x, size.y);
|
||||
// Style-run batching + sub-pixel decimation: one stroke per style run
|
||||
drawController.beginBatch();
|
||||
drawEntities(drawController, sceneEntitiesForViewport(drawController));
|
||||
@@ -124,13 +125,7 @@ export function drawScene(drawController: ScreenCanvasDrawController, now: numbe
|
||||
const offsetChanged =
|
||||
!!rendered && (rendered.offsetX !== offset.x || rendered.offsetY !== offset.y);
|
||||
|
||||
// Grid lines are screen-fixed (drawn in clear()), so blitting a shifted
|
||||
// bitmap would drag the grid along — always re-render while grid is on.
|
||||
if (
|
||||
paramsChanged ||
|
||||
getGridEnabled() ||
|
||||
(offsetChanged && now - lastOffsetChangeAt >= PAN_SETTLE_MS)
|
||||
) {
|
||||
if (paramsChanged || (offsetChanged && now - lastOffsetChangeAt >= PAN_SETTLE_MS)) {
|
||||
rebuildScene(drawController);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,8 +26,12 @@ let gridMinX = 0;
|
||||
let gridMinY = 0;
|
||||
let gridCols = 1;
|
||||
|
||||
/** 칸 번호 범위 — 엔티티가 놓인 칸(0~GRID_CELLS_PER_AXIS) 밖은 비었으니 잘라 냄(줌아웃 때 빈 칸 수억 개를 돌지 않게). */
|
||||
function cellRange(min: number, max: number, gridMin: number): [number, number] {
|
||||
return [Math.floor((min - gridMin) / cellSize), Math.floor((max - gridMin) / cellSize)];
|
||||
return [
|
||||
Math.max(0, Math.floor((min - gridMin) / cellSize)),
|
||||
Math.min(GRID_CELLS_PER_AXIS, Math.floor((max - gridMin) / cellSize)),
|
||||
];
|
||||
}
|
||||
|
||||
function ensureIndex(): void {
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
import { Point } from '@flatten-js/core';
|
||||
import { beforeAll, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
/** 줌아웃 느려짐(227-1) — 가짜 캔버스로 한 화면 그리기 비용을 잰다. */
|
||||
const counts = vi.hoisted(() => {
|
||||
const tally = { segments: 0, arcs: 0, strokes: 0, fills: 0, texts: 0 };
|
||||
const g = globalThis as Record<string, unknown>;
|
||||
g.window = { dispatchEvent: () => true, matchMedia: () => ({ matches: true }) };
|
||||
g.Path2D = class {
|
||||
moveTo() {}
|
||||
lineTo() {
|
||||
tally.segments++;
|
||||
}
|
||||
arc() {
|
||||
tally.arcs++;
|
||||
}
|
||||
};
|
||||
const noop = () => {};
|
||||
g.__fakeContext = {
|
||||
fillRect: noop,
|
||||
clearRect: noop,
|
||||
setLineDash: noop,
|
||||
beginPath: noop,
|
||||
moveTo: noop,
|
||||
lineTo: () => tally.segments++,
|
||||
arc: () => tally.arcs++,
|
||||
closePath: noop,
|
||||
stroke: () => tally.strokes++,
|
||||
fill: () => tally.fills++,
|
||||
save: noop,
|
||||
restore: noop,
|
||||
translate: noop,
|
||||
rotate: noop,
|
||||
fillText: () => tally.texts++,
|
||||
measureText: (label: string) => ({ width: label.length * 7 }),
|
||||
drawImage: noop,
|
||||
};
|
||||
return tally;
|
||||
});
|
||||
|
||||
import type { Layer } from '../App.types';
|
||||
import { ScreenCanvasDrawController } from '../drawControllers/screenCanvas.drawController';
|
||||
import type { Entity } from '../entities/Entity';
|
||||
import { HatchEntity } from '../entities/HatchEntity';
|
||||
import { LineEntity } from '../entities/LineEntity';
|
||||
import { MeasurementEntity } from '../entities/MeasurementEntity';
|
||||
import { PolyLineEntity } from '../entities/PolyLineEntity';
|
||||
import { TextEntity } from '../entities/TextEntity';
|
||||
import { setEntities, setGridEnabled, setLayers } from '../state';
|
||||
import { drawScene, invalidateSceneCache, scenePerf } from './scene-cache';
|
||||
import { queryEntitiesInBox } from './spatial-index';
|
||||
|
||||
const LAYER = 'perf';
|
||||
|
||||
function polyline(points: Point[]): PolyLineEntity {
|
||||
const segments = points
|
||||
.slice(1)
|
||||
.map((point, index) => new LineEntity(LAYER, points[index], point));
|
||||
return new PolyLineEntity(LAYER, segments);
|
||||
}
|
||||
|
||||
/** B07 횡단 장 꼴 — 횡단 40 개(지반선 · 계획선 · 절토 · 성토 해치 · 글자 · 치수 · 모눈) + 등고선 */
|
||||
function crossSectionSheet(): Entity[] {
|
||||
const entities: Entity[] = [];
|
||||
for (let index = 0; index < 40; index++) {
|
||||
const ox = (index % 4) * 80;
|
||||
const oy = Math.floor(index / 4) * 30;
|
||||
const ground = Array.from(
|
||||
{ length: 61 },
|
||||
(_, k) => new Point(ox + k, oy + 10 + 3 * Math.sin(k / 7 + index))
|
||||
);
|
||||
entities.push(polyline(ground));
|
||||
const design = [0, 15, 20, 25, 40, 45, 60].map(
|
||||
(x, k) => new Point(ox + x, oy + 9 + (k % 2 ? 1 : 0))
|
||||
);
|
||||
entities.push(polyline(design));
|
||||
const hatchOptions = { style: 'pattern', spacing: 0.3 } as const;
|
||||
entities.push(
|
||||
new HatchEntity(
|
||||
LAYER,
|
||||
[new Point(ox + 15, oy + 9), new Point(ox + 25, oy + 9), new Point(ox + 20, oy + 13)],
|
||||
hatchOptions
|
||||
),
|
||||
new HatchEntity(
|
||||
LAYER,
|
||||
[new Point(ox + 40, oy + 9), new Point(ox + 60, oy + 9), new Point(ox + 50, oy + 6)],
|
||||
hatchOptions
|
||||
)
|
||||
);
|
||||
for (let k = 0; k < 25; k++) {
|
||||
entities.push(
|
||||
new TextEntity(LAYER, `${index}+${k}.00`, new Point(ox + 2 * k, oy + 2), { fontSize: 0.5 })
|
||||
);
|
||||
}
|
||||
for (let k = 0; k < 6; k++) {
|
||||
entities.push(
|
||||
new MeasurementEntity(
|
||||
LAYER,
|
||||
new Point(ox + 10 * k, oy + 8),
|
||||
new Point(ox + 10 * k + 8, oy + 8),
|
||||
new Point(ox + 10 * k, oy + 7)
|
||||
)
|
||||
);
|
||||
}
|
||||
for (let k = 0; k <= 12; k++) {
|
||||
entities.push(
|
||||
new LineEntity(LAYER, new Point(ox + 5 * k, oy), new Point(ox + 5 * k, oy + 20))
|
||||
);
|
||||
}
|
||||
}
|
||||
for (let c = 0; c < 178; c++) {
|
||||
const contour = Array.from(
|
||||
{ length: 85 },
|
||||
(_, k) => new Point(k * 3.7, c * 1.7 + 2 * Math.sin(k / 5 + c))
|
||||
);
|
||||
entities.push(polyline(contour));
|
||||
}
|
||||
return entities;
|
||||
}
|
||||
|
||||
function viewAt(controller: ScreenCanvasDrawController, factor: number, fitScale: number) {
|
||||
const size = controller.getCanvasSize();
|
||||
const center = controller.targetToWorld(new Point(size.x / 2, size.y / 2));
|
||||
const scale = fitScale * factor;
|
||||
controller.setScreenScale(scale);
|
||||
controller.setScreenOffset(
|
||||
new Point(center.x - size.x / 2 / scale, center.y - size.y / 2 / scale)
|
||||
);
|
||||
}
|
||||
|
||||
describe('줌아웃 그리기 비용(227-1)', () => {
|
||||
const controller = new ScreenCanvasDrawController(
|
||||
(globalThis as unknown as { __fakeContext: CanvasRenderingContext2D }).__fakeContext
|
||||
);
|
||||
let fitScale = 1;
|
||||
|
||||
beforeAll(() => {
|
||||
const g = globalThis as Record<string, unknown>;
|
||||
g.document = {
|
||||
documentElement: { hasAttribute: () => false, getAttribute: () => null },
|
||||
createElement: () => ({ width: 0, height: 0, getContext: () => g.__fakeContext }),
|
||||
};
|
||||
g.getComputedStyle = () => ({ getPropertyValue: () => '' });
|
||||
setLayers(
|
||||
[{ id: LAYER, name: LAYER, isVisible: true, isLocked: false } as unknown as Layer],
|
||||
false
|
||||
);
|
||||
setEntities(crossSectionSheet(), false);
|
||||
controller.setCanvasSize(new Point(1600, 900));
|
||||
controller.zoomToFitScreen();
|
||||
fitScale = controller.getScreenScale();
|
||||
});
|
||||
|
||||
it('배율별 한 화면 다시 그리기 — 시간 · 그린 수(보고용 표)', () => {
|
||||
const rows = [];
|
||||
for (const factor of [10, 1, 0.1, 0.01]) {
|
||||
viewAt(controller, factor, fitScale);
|
||||
for (const key of Object.keys(counts)) counts[key as keyof typeof counts] = 0;
|
||||
invalidateSceneCache();
|
||||
const startedAt = performance.now();
|
||||
drawScene(controller, 0);
|
||||
const ms = performance.now() - startedAt;
|
||||
rows.push({ factor, ms: Number(ms.toFixed(1)), ...counts });
|
||||
}
|
||||
console.table(rows);
|
||||
expect(rows).toHaveLength(4);
|
||||
});
|
||||
|
||||
it('화면보다 아주 큰 영역 조회도 빈 칸을 돌지 않음 — 0.001배 상자 50ms 안', () => {
|
||||
viewAt(controller, 1, fitScale);
|
||||
const all = queryEntitiesInBox(-1e9, -1e9, 1e9, 1e9).length;
|
||||
const size = controller.getCanvasSize();
|
||||
const half = (Math.max(size.x, size.y) / (fitScale * 0.001)) * 1.5;
|
||||
const startedAt = performance.now();
|
||||
const found = queryEntitiesInBox(-half, -half, half, half);
|
||||
const ms = performance.now() - startedAt;
|
||||
expect(found.length).toBe(all);
|
||||
expect(ms).toBeLessThan(50);
|
||||
});
|
||||
|
||||
it('모눈을 켜도 화면이 그대로면 장면을 다시 그리지 않음', () => {
|
||||
viewAt(controller, 1, fitScale);
|
||||
setGridEnabled(true);
|
||||
drawScene(controller, 0);
|
||||
const rebuilds = scenePerf.sceneRebuilds;
|
||||
drawScene(controller, 1000);
|
||||
drawScene(controller, 2000);
|
||||
setGridEnabled(false);
|
||||
expect(scenePerf.sceneRebuilds).toBe(rebuilds);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user