Files
Aislo/B07_DesignDetail/openwebcad/src/helpers/scene-cache.ts
T
eomsangdonandClaude Opus 5 4cb9b15939 style: 저장소 전체 포맷터 일괄 적용 (prettier·biome·ruff)
파일마다 포맷 폭이 달라(≈80 대 100) 한 줄만 고쳐도 포맷터가 무관한 줄을 대량
재포맷했음. 사용자 지시로 전체를 한 번에 맞춤. 코드 동작 변경 없음 — 포맷만.

- 프론트엔드 `.ts/.css/.html` → 저장소 prettier (`.prettierrc`, printWidth 100)
- `B07_DesignDetail/openwebcad/**` → 자체 biome (tab 들여쓰기·single quote·lineWidth 100).
  `biome format` 만 사용 — `biome lint --write` 는 포맷 아닌 코드 수정까지 하므로 제외
- 파이썬 → `ruff format` (엔진 코드는 이미 정합, resources·scratch 스크립트 24개만 변경)

두 포맷터가 서로 되돌리지 않도록 `.prettierignore` 신규 — openwebcad 와 빌드·산출물
폴더를 prettier 대상에서 뺌. `.prettierrc` 에 `endOfLine: "auto"` 추가 — 기본값 `lf` 가
`core.autocrlf=true` 로 받은 CRLF 파일을 매번 전부 다시 써서 `--list-different` 가
실제 포맷 차이를 가리고 있었음.

검증: `tsc --noEmit` 통과(루트·openwebcad 둘 다), pytest 349 passed / 17 skipped /
0 failed, CAD vitest 87건 중 81 passed / 6 failed(laptop-sub 기준선과 동일, 회귀 없음).
포맷터 재실행 시 prettier·biome 모두 변경 0건.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 07:08:24 +09:00

144 lines
4.8 KiB
TypeScript

import type { ScreenCanvasDrawController } from '../drawControllers/screenCanvas.drawController';
import { getGridEnabled, getHighlightedEntityIds, setHighlightedEntityIds } from '../state';
import { drawEntities } from './draw-functions';
import { getSceneVersion } from './scene-version';
import { queryEntitiesInBox } from './spatial-index';
/**
* Static scene cache: all entities are rendered once into an offscreen canvas.
* While panning, the cached bitmap is blitted at a pixel offset instead of
* re-stroking every entity each frame. The cache is rebuilt when the scene
* version bumps (entities/layers/selection changed), when zoom or canvas size
* changes, or after the pan offset has settled for PAN_SETTLE_MS.
*
* Highlight is intentionally NOT baked into the cache (it changes on every
* mouse move) — draw.ts re-draws highlighted entities on top each frame.
*/
const PAN_SETTLE_MS = 120;
interface RenderedParams {
version: number;
scale: number;
offsetX: number;
offsetY: number;
sizeX: number;
sizeY: number;
}
let offscreenCanvas: HTMLCanvasElement | null = null;
let rendered: RenderedParams | null = null;
let prevOffsetX = Number.NaN;
let prevOffsetY = Number.NaN;
let lastOffsetChangeAt = 0;
export const scenePerf = {
sceneRebuilds: 0,
lastRebuildMs: 0,
avgFrameMs: 0,
};
(window as unknown as Record<string, unknown>).__aisloCadPerf = scenePerf;
export function invalidateSceneCache(): void {
rendered = null;
}
/**
* Viewport culling: only entities whose bbox intersects the current view
* (expanded by one viewport on each side, so short pans stay covered by the
* blit before the settle-rebuild) are rendered into the scene cache.
*/
function sceneEntitiesForViewport(drawController: ScreenCanvasDrawController) {
const size = drawController.getCanvasSize();
const scale = drawController.getScreenScale();
const offset = drawController.getScreenOffset();
const viewWidth = size.x / scale;
const viewHeight = size.y / scale;
return queryEntitiesInBox(
offset.x - viewWidth,
offset.y - viewHeight,
offset.x + 2 * viewWidth,
offset.y + 2 * viewHeight
);
}
function rebuildScene(drawController: ScreenCanvasDrawController): void {
const size = drawController.getCanvasSize();
if (!offscreenCanvas) {
offscreenCanvas = document.createElement('canvas');
}
if (offscreenCanvas.width !== size.x || offscreenCanvas.height !== size.y) {
offscreenCanvas.width = Math.max(1, size.x);
offscreenCanvas.height = Math.max(1, size.y);
}
const offscreenContext = offscreenCanvas.getContext('2d');
if (!offscreenContext) return;
const startedAt = performance.now();
// Exclude the (rapidly changing) hover highlight from the baked bitmap.
const savedHighlight = getHighlightedEntityIds();
if (savedHighlight.length) setHighlightedEntityIds([]);
drawController.withContext(offscreenContext, () => {
drawController.clear();
// Style-run batching + sub-pixel decimation: one stroke per style run
drawController.beginBatch();
drawEntities(drawController, sceneEntitiesForViewport(drawController));
drawController.endBatch();
});
if (savedHighlight.length) setHighlightedEntityIds(savedHighlight);
scenePerf.lastRebuildMs = performance.now() - startedAt;
scenePerf.sceneRebuilds++;
const offset = drawController.getScreenOffset();
rendered = {
version: getSceneVersion(),
scale: drawController.getScreenScale(),
offsetX: offset.x,
offsetY: offset.y,
sizeX: size.x,
sizeY: size.y,
};
}
/**
* Draw the static scene: rebuild the cache when needed, otherwise blit the
* cached bitmap (shifted by the pan delta). Called once per frame by draw().
*/
export function drawScene(drawController: ScreenCanvasDrawController, now: number): void {
const size = drawController.getCanvasSize();
const scale = drawController.getScreenScale();
const offset = drawController.getScreenOffset();
if (offset.x !== prevOffsetX || offset.y !== prevOffsetY) {
lastOffsetChangeAt = now;
prevOffsetX = offset.x;
prevOffsetY = offset.y;
}
const paramsChanged =
!rendered ||
rendered.version !== getSceneVersion() ||
rendered.scale !== scale ||
rendered.sizeX !== size.x ||
rendered.sizeY !== size.y;
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)
) {
rebuildScene(drawController);
}
if (!offscreenCanvas || !rendered) return;
// screenX = (worldX - offsetX) * scale, canvasY is y-flipped afterwards:
// content shifts left when offset.x grows, down when offset.y grows.
const dx = (rendered.offsetX - offset.x) * scale;
const dy = (offset.y - rendered.offsetY) * scale;
drawController.blitImage(offscreenCanvas, dx, dy);
}