횡단설계(B06) 다음을 상세설계 → 수량산출 → 설계도서 순으로 재배열하고, 폴더 번호가 흐름과 일치하도록 이름을 맞바꾼다. - B08_DesignDetail → B07_DesignDetail, B07_Quantity → B08_Quantity (파일 접두어·식별자·라우트·locale 키 전량 스왑) - STAGE_KEYS 4=DESIGN_DETAIL, 5=QUANTITY 스왑 + 라우터 stage 리터럴 교체 - CAD 마운트 /b08-cad → /b07-cad (main.py·vite proxy·iframe URL), openwebcad Toolbar 라벨 B07로 수정 후 재빌드 - 유지: openwebcad postMessage 프로토콜 aislo:b08:*·패키지명(내부 식별자) - 기존 프로젝트 storage 폴더 rename + project_manifest 갱신, DB project_workflow_stages stage_no 4↔5 행 스왑 완료 Co-Authored-By: Claude Fable 5 <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);
|
|
}
|