파일마다 포맷 폭이 달라(≈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>
144 lines
4.8 KiB
TypeScript
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);
|
|
}
|