auto: 2026-07-26 20:54 (ESD_LAPTOP)
This commit is contained in:
@@ -27,12 +27,26 @@ import { DEFAULT_TEXT_OPTIONS, type DrawController } from './DrawController';
|
||||
*
|
||||
* To convert between the 2 coordinate systems, you need the screenOffset and screenScale
|
||||
*/
|
||||
// Batch-mode stroke decimation: skip chain segments shorter than this (screen px)
|
||||
const BATCH_LOD_PX = 0.5;
|
||||
// Batch-mode text smaller than this (screen px) is unreadable — skip drawing it
|
||||
const BATCH_MIN_TEXT_PX = 2;
|
||||
|
||||
export class ScreenCanvasDrawController implements DrawController {
|
||||
private screenOffset: Point = new Point(0, 0);
|
||||
private screenScale = 1;
|
||||
private screenMouseLocation: Point;
|
||||
private canvasSize: Point = new Point(100, 100);
|
||||
|
||||
// Style-run batching (static scene rendering): consecutive stroke calls
|
||||
// with the same style are collected into one Path2D and stroked once.
|
||||
private batching = false;
|
||||
private batchPath: Path2D | null = null;
|
||||
private batchKey: string | null = null;
|
||||
private batchStyle: { color: string; lineWidth: number; dash: number[] } | null = null;
|
||||
private batchLastX = Number.NaN;
|
||||
private batchLastY = Number.NaN;
|
||||
|
||||
constructor(private context: CanvasRenderingContext2D) {
|
||||
this.screenMouseLocation = new Point(this.canvasSize.x / 2, this.canvasSize.y / 2);
|
||||
this.setScreenOffset(new Point(0, 0)); // User expects mathematical coordinates, where y axis goes up, but canvas y axis goes down
|
||||
@@ -221,6 +235,18 @@ export class ScreenCanvasDrawController implements DrawController {
|
||||
lineWidth: number,
|
||||
dash: number[] = []
|
||||
) {
|
||||
if (this.batching) {
|
||||
const effectiveWidth = isHighlighted ? lineWidth + 1 : lineWidth;
|
||||
const effectiveDash = isSelected ? [5, 5] : dash;
|
||||
const key = `${color}|${effectiveWidth}|${effectiveDash.join(',')}`;
|
||||
if (key !== this.batchKey) {
|
||||
this.flushBatch();
|
||||
this.batchKey = key;
|
||||
this.batchStyle = { color, lineWidth: effectiveWidth, dash: effectiveDash };
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.context.strokeStyle = color;
|
||||
this.context.lineWidth = lineWidth;
|
||||
this.context.setLineDash(dash);
|
||||
@@ -234,6 +260,43 @@ export class ScreenCanvasDrawController implements DrawController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start style-run batching: consecutive stroke calls sharing a style are
|
||||
* accumulated into a single Path2D and stroked once (with sub-pixel
|
||||
* segment decimation). Used while rendering the static scene cache.
|
||||
*/
|
||||
public beginBatch() {
|
||||
this.flushBatch();
|
||||
this.batching = true;
|
||||
this.batchKey = null;
|
||||
this.batchStyle = null;
|
||||
}
|
||||
|
||||
public endBatch() {
|
||||
this.flushBatch();
|
||||
this.batching = false;
|
||||
this.batchKey = null;
|
||||
this.batchStyle = null;
|
||||
}
|
||||
|
||||
private flushBatch() {
|
||||
if (this.batchPath && this.batchStyle) {
|
||||
this.context.strokeStyle = this.batchStyle.color;
|
||||
this.context.lineWidth = this.batchStyle.lineWidth;
|
||||
this.context.setLineDash(this.batchStyle.dash);
|
||||
// Round caps/joins replace the per-segment endpoint dots drawn in
|
||||
// the unbatched path (see _drawRoundedEndpoint)
|
||||
this.context.lineCap = 'round';
|
||||
this.context.lineJoin = 'round';
|
||||
this.context.stroke(this.batchPath);
|
||||
this.context.lineCap = 'butt';
|
||||
this.context.lineJoin = 'miter';
|
||||
}
|
||||
this.batchPath = null;
|
||||
this.batchLastX = Number.NaN;
|
||||
this.batchLastY = Number.NaN;
|
||||
}
|
||||
|
||||
public setFillStyles(fillColor: string) {
|
||||
this.context.fillStyle = fillColor;
|
||||
}
|
||||
@@ -265,6 +328,7 @@ export class ScreenCanvasDrawController implements DrawController {
|
||||
if (this.canvasSize === null) return;
|
||||
|
||||
if (!this.context) return;
|
||||
if (this.batching) this.flushBatch();
|
||||
|
||||
this.context.fillStyle = CANVAS_BACKGROUND_COLOR;
|
||||
this.context.fillRect(0, 0, this.canvasSize?.x, this.canvasSize?.y);
|
||||
@@ -305,6 +369,33 @@ export class ScreenCanvasDrawController implements DrawController {
|
||||
* @param screenEndPoint
|
||||
*/
|
||||
public drawLineScreen(screenStartPoint: Point, screenEndPoint: Point): void {
|
||||
if (this.batching) {
|
||||
const startX = screenStartPoint.x;
|
||||
const startY = this.canvasSize.y - screenStartPoint.y;
|
||||
const endX = screenEndPoint.x;
|
||||
const endY = this.canvasSize.y - screenEndPoint.y;
|
||||
if (!this.batchPath) this.batchPath = new Path2D();
|
||||
// Chain break: start is not where the previous segment ended
|
||||
const chainBroken =
|
||||
Math.abs(startX - this.batchLastX) > BATCH_LOD_PX ||
|
||||
Math.abs(startY - this.batchLastY) > BATCH_LOD_PX;
|
||||
if (chainBroken || Number.isNaN(this.batchLastX)) {
|
||||
this.batchPath.moveTo(startX, startY);
|
||||
this.batchLastX = startX;
|
||||
this.batchLastY = startY;
|
||||
}
|
||||
const isTinyStep =
|
||||
Math.abs(endX - this.batchLastX) < BATCH_LOD_PX &&
|
||||
Math.abs(endY - this.batchLastY) < BATCH_LOD_PX;
|
||||
// Decimate sub-pixel steps inside a chain; isolated segments always draw
|
||||
if (!isTinyStep || chainBroken) {
|
||||
this.batchPath.lineTo(endX, endY);
|
||||
this.batchLastX = endX;
|
||||
this.batchLastY = endY;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.context.beginPath();
|
||||
this.context.moveTo(screenStartPoint.x, this.canvasSize.y - screenStartPoint.y);
|
||||
this.context.lineTo(screenEndPoint.x, this.canvasSize.y - screenEndPoint.y);
|
||||
@@ -357,6 +448,22 @@ export class ScreenCanvasDrawController implements DrawController {
|
||||
endAngle: number,
|
||||
counterClockWise: boolean
|
||||
) {
|
||||
if (this.batching) {
|
||||
if (screenRadius < BATCH_LOD_PX) return; // invisible at this zoom
|
||||
if (!this.batchPath) this.batchPath = new Path2D();
|
||||
const centerX = screenCenterPoint.x;
|
||||
const centerY = this.canvasSize.y - screenCenterPoint.y;
|
||||
this.batchPath.moveTo(
|
||||
centerX + screenRadius * Math.cos(startAngle),
|
||||
centerY + screenRadius * Math.sin(startAngle)
|
||||
);
|
||||
this.batchPath.arc(centerX, centerY, screenRadius, startAngle, endAngle, counterClockWise);
|
||||
// Arc end becomes the new chain tail
|
||||
this.batchLastX = centerX + screenRadius * Math.cos(endAngle);
|
||||
this.batchLastY = centerY + screenRadius * Math.sin(endAngle);
|
||||
return;
|
||||
}
|
||||
|
||||
this.context.beginPath();
|
||||
this.context.arc(
|
||||
screenCenterPoint.x,
|
||||
@@ -434,6 +541,10 @@ export class ScreenCanvasDrawController implements DrawController {
|
||||
...DEFAULT_TEXT_OPTIONS,
|
||||
...options,
|
||||
};
|
||||
if (this.batching) {
|
||||
if (opts.fontSize < BATCH_MIN_TEXT_PX) return; // unreadable at this zoom
|
||||
this.flushBatch(); // keep draw order: strokes so far go under this text
|
||||
}
|
||||
this.context.save();
|
||||
this.context.translate(basePoint.x, this.canvasSize.y - basePoint.y);
|
||||
const angle = getAngleWithXAxis(
|
||||
@@ -466,6 +577,7 @@ export class ScreenCanvasDrawController implements DrawController {
|
||||
height: number,
|
||||
angle: number
|
||||
): void {
|
||||
if (this.batching) this.flushBatch();
|
||||
const [screenBasePoint, screenDimensions] = this.worldsToTargets([
|
||||
new Point(xMin, yMin),
|
||||
new Point(width, height),
|
||||
@@ -516,6 +628,7 @@ export class ScreenCanvasDrawController implements DrawController {
|
||||
* @param color
|
||||
*/
|
||||
public fillRectScreen(xMin: number, yMin: number, width: number, height: number, color: string) {
|
||||
if (this.batching) this.flushBatch();
|
||||
// TODO see if we need to replace this with a call to fillPolygon
|
||||
this.context.fillStyle = color;
|
||||
this.context.fillRect(xMin, this.canvasSize.y - yMin, width, height);
|
||||
@@ -526,6 +639,7 @@ export class ScreenCanvasDrawController implements DrawController {
|
||||
* @param points
|
||||
*/
|
||||
public fillPolygon(...points: Point[]) {
|
||||
if (this.batching) this.flushBatch();
|
||||
const screenPoints = points.map(this.worldToTarget.bind(this));
|
||||
this.context.beginPath();
|
||||
screenPoints.forEach((screenPoint, index) => {
|
||||
|
||||
+11
-3
@@ -1,8 +1,8 @@
|
||||
import {
|
||||
getAngleGuideOriginPoint,
|
||||
getAngleStep,
|
||||
getEntities,
|
||||
getHoveredSnapPoints,
|
||||
getLayerById,
|
||||
getScreenCanvasDrawController,
|
||||
getShouldDrawHelpers,
|
||||
setAngleGuideEntities,
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from '../state.ts';
|
||||
import { HOVERED_SNAP_POINT_TIME, SNAP_POINT_DISTANCE } from '../App.consts.ts';
|
||||
import { getDrawHelpers } from './get-draw-guides.ts';
|
||||
import { queryEntitiesNearPoint } from './spatial-index.ts';
|
||||
import { compact } from 'es-toolkit';
|
||||
|
||||
/**
|
||||
@@ -19,9 +20,16 @@ import { compact } from 'es-toolkit';
|
||||
export function calculateAngleGuidesAndSnapPoints() {
|
||||
const angleStep = getAngleStep();
|
||||
const screenCanvasDrawController = getScreenCanvasDrawController();
|
||||
const entities = getEntities();
|
||||
const screenScale = screenCanvasDrawController.getScreenScale();
|
||||
const worldMouseLocation = screenCanvasDrawController.getWorldMouseLocation();
|
||||
// 스냅 후보: 공간 인덱스로 마우스 주변만 조회 (전 엔티티 O(n²) 교차 계산 제거),
|
||||
// 잠금 레이어(b07-frame 등 참조용)는 스냅 대상에서 제외한다.
|
||||
const maxSnapDistance = SNAP_POINT_DISTANCE / screenScale;
|
||||
const entities = queryEntitiesNearPoint(
|
||||
worldMouseLocation.x,
|
||||
worldMouseLocation.y,
|
||||
maxSnapDistance * 2,
|
||||
).filter(entity => !getLayerById(entity.layerId)?.isLocked);
|
||||
const hoveredSnapPoints = getHoveredSnapPoints();
|
||||
|
||||
const eligibleHoveredSnapPoints = hoveredSnapPoints.filter(
|
||||
@@ -39,7 +47,7 @@ export function calculateAngleGuidesAndSnapPoints() {
|
||||
compact([getAngleGuideOriginPoint(), ...eligibleHoveredPoints]),
|
||||
worldMouseLocation,
|
||||
angleStep,
|
||||
SNAP_POINT_DISTANCE / screenScale,
|
||||
maxSnapDistance,
|
||||
);
|
||||
setAngleGuideEntities(angleGuides);
|
||||
setSnapPoint(entitySnapPoint);
|
||||
|
||||
@@ -4,12 +4,12 @@ import {type SnapPoint, SnapPointType} from '../App.types';
|
||||
import type {DrawController} from '../drawControllers/DrawController';
|
||||
import type {ScreenCanvasDrawController} from '../drawControllers/screenCanvas.drawController';
|
||||
import type {Entity} from '../entities/Entity';
|
||||
import {getLayers, isEntityHighlighted, isEntitySelected} from '../state';
|
||||
import {getLayerById, isEntityHighlighted, isEntitySelected} from '../state';
|
||||
import {toast} from 'react-toastify';
|
||||
|
||||
export function drawEntities(drawController: DrawController, entities: Entity[]) {
|
||||
for (const entity of entities) {
|
||||
const layer = getLayers().find((layer) => layer.id === entity.layerId);
|
||||
const layer = getLayerById(entity.layerId);
|
||||
if (!layer) {
|
||||
toast.error(`Failed to find layer for entity: ${entity?.id}`);
|
||||
console.error('Failed to find layer for entity: ', entity);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { ScreenCanvasDrawController } from '../drawControllers/screenCanvas.drawController';
|
||||
import {
|
||||
getEntities,
|
||||
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.
|
||||
@@ -46,6 +46,25 @@ 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) {
|
||||
@@ -64,7 +83,10 @@ function rebuildScene(drawController: ScreenCanvasDrawController): void {
|
||||
if (savedHighlight.length) setHighlightedEntityIds([]);
|
||||
drawController.withContext(offscreenContext, () => {
|
||||
drawController.clear();
|
||||
drawEntities(drawController, getEntities());
|
||||
// Style-run batching + sub-pixel decimation: one stroke per style run
|
||||
drawController.beginBatch();
|
||||
drawEntities(drawController, sceneEntitiesForViewport(drawController));
|
||||
drawController.endBatch();
|
||||
});
|
||||
if (savedHighlight.length) setHighlightedEntityIds(savedHighlight);
|
||||
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import type { Entity } from '../entities/Entity';
|
||||
import { getEntities } from '../state';
|
||||
import { getSceneVersion } from './scene-version';
|
||||
|
||||
/**
|
||||
* Uniform-grid spatial index over top-level entity bounding boxes.
|
||||
* Rebuilt lazily whenever the scene version changes (entity edits bump it).
|
||||
* Queries return entities in original array order so z-order is preserved.
|
||||
*/
|
||||
const GRID_CELLS_PER_AXIS = 64;
|
||||
|
||||
interface IndexedEntity {
|
||||
entity: Entity;
|
||||
minX: number;
|
||||
minY: number;
|
||||
maxX: number;
|
||||
maxY: number;
|
||||
}
|
||||
|
||||
let indexVersion = -1;
|
||||
let indexedEntities: IndexedEntity[] = [];
|
||||
let unindexedEntities: Entity[] = []; // bbox unavailable — always included in results
|
||||
let cells: Map<number, number[]> = new Map();
|
||||
let cellSize = 1;
|
||||
let gridMinX = 0;
|
||||
let gridMinY = 0;
|
||||
let gridCols = 1;
|
||||
|
||||
function cellRange(min: number, max: number, gridMin: number): [number, number] {
|
||||
return [Math.floor((min - gridMin) / cellSize), Math.floor((max - gridMin) / cellSize)];
|
||||
}
|
||||
|
||||
function ensureIndex(): void {
|
||||
const version = getSceneVersion();
|
||||
if (version === indexVersion) return;
|
||||
|
||||
indexedEntities = [];
|
||||
unindexedEntities = [];
|
||||
cells = new Map();
|
||||
|
||||
const entities = getEntities();
|
||||
let minX = Number.POSITIVE_INFINITY;
|
||||
let minY = Number.POSITIVE_INFINITY;
|
||||
let maxX = Number.NEGATIVE_INFINITY;
|
||||
let maxY = Number.NEGATIVE_INFINITY;
|
||||
|
||||
for (const entity of entities) {
|
||||
let box: { xmin: number; ymin: number; xmax: number; ymax: number } | null = null;
|
||||
try {
|
||||
box = entity.getBoundingBox();
|
||||
} catch {
|
||||
box = null;
|
||||
}
|
||||
if (
|
||||
!box ||
|
||||
!Number.isFinite(box.xmin) ||
|
||||
!Number.isFinite(box.ymin) ||
|
||||
!Number.isFinite(box.xmax) ||
|
||||
!Number.isFinite(box.ymax)
|
||||
) {
|
||||
unindexedEntities.push(entity);
|
||||
continue;
|
||||
}
|
||||
indexedEntities.push({
|
||||
entity,
|
||||
minX: box.xmin,
|
||||
minY: box.ymin,
|
||||
maxX: box.xmax,
|
||||
maxY: box.ymax,
|
||||
});
|
||||
if (box.xmin < minX) minX = box.xmin;
|
||||
if (box.ymin < minY) minY = box.ymin;
|
||||
if (box.xmax > maxX) maxX = box.xmax;
|
||||
if (box.ymax > maxY) maxY = box.ymax;
|
||||
}
|
||||
|
||||
if (indexedEntities.length) {
|
||||
const extent = Math.max(maxX - minX, maxY - minY, 1e-9);
|
||||
cellSize = extent / GRID_CELLS_PER_AXIS;
|
||||
gridMinX = minX;
|
||||
gridMinY = minY;
|
||||
gridCols = GRID_CELLS_PER_AXIS + 2;
|
||||
|
||||
indexedEntities.forEach((item, index) => {
|
||||
const [cx0, cx1] = cellRange(item.minX, item.maxX, gridMinX);
|
||||
const [cy0, cy1] = cellRange(item.minY, item.maxY, gridMinY);
|
||||
for (let cy = cy0; cy <= cy1; cy++) {
|
||||
for (let cx = cx0; cx <= cx1; cx++) {
|
||||
const key = cy * gridCols + cx;
|
||||
const bucket = cells.get(key);
|
||||
if (bucket) bucket.push(index);
|
||||
else cells.set(key, [index]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
indexVersion = version;
|
||||
}
|
||||
|
||||
/** 뷰포트/사각 영역과 bbox가 겹치는 엔티티 (원본 배열 순서 유지). */
|
||||
export function queryEntitiesInBox(
|
||||
minX: number,
|
||||
minY: number,
|
||||
maxX: number,
|
||||
maxY: number
|
||||
): Entity[] {
|
||||
ensureIndex();
|
||||
if (!indexedEntities.length) return [...unindexedEntities];
|
||||
|
||||
const seen = new Set<number>();
|
||||
const [cx0, cx1] = cellRange(minX, maxX, gridMinX);
|
||||
const [cy0, cy1] = cellRange(minY, maxY, gridMinY);
|
||||
for (let cy = cy0; cy <= cy1; cy++) {
|
||||
for (let cx = cx0; cx <= cx1; cx++) {
|
||||
const bucket = cells.get(cy * gridCols + cx);
|
||||
if (!bucket) continue;
|
||||
for (const index of bucket) seen.add(index);
|
||||
}
|
||||
}
|
||||
|
||||
const result: Entity[] = [];
|
||||
let unindexedCursor = 0;
|
||||
for (let index = 0; index < indexedEntities.length; index++) {
|
||||
if (!seen.has(index)) continue;
|
||||
const item = indexedEntities[index];
|
||||
if (item.maxX < minX || item.minX > maxX || item.maxY < minY || item.minY > maxY) continue;
|
||||
result.push(item.entity);
|
||||
}
|
||||
// bbox 불명 엔티티는 항상 포함 (뒤에 붙여도 소수라 시각 영향 없음)
|
||||
for (; unindexedCursor < unindexedEntities.length; unindexedCursor++) {
|
||||
result.push(unindexedEntities[unindexedCursor]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 점 주변 반경 후보 엔티티 (스냅·호버용). */
|
||||
export function queryEntitiesNearPoint(x: number, y: number, radius: number): Entity[] {
|
||||
return queryEntitiesInBox(x - radius, y - radius, x + radius, y + radius);
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { ScreenCanvasDrawController } from './drawControllers/screenCanvas.drawC
|
||||
import { draw } from './helpers/draw';
|
||||
import { findClosestEntity } from './helpers/find-closest-entity';
|
||||
import { scenePerf } from './helpers/scene-cache';
|
||||
import { queryEntitiesNearPoint } from './helpers/spatial-index';
|
||||
import { getNewLayer } from './helpers/get-new-layer.ts';
|
||||
import { trackHoveredSnapPoint } from './helpers/track-hovered-snap-points';
|
||||
import { InputController } from './inputController/input-controller.ts';
|
||||
@@ -15,7 +16,6 @@ import { registerAisloDrawingBridge } from './integration/aislo-drawing-bridge.t
|
||||
import {
|
||||
getActiveToolActor,
|
||||
getCanvas,
|
||||
getEntities,
|
||||
getHoveredSnapPoints,
|
||||
getLastDrawTimestamp,
|
||||
getScreenCanvasDrawController,
|
||||
@@ -85,9 +85,15 @@ function startDrawLoop(
|
||||
lastHoverCheckAt = timestamp;
|
||||
lastHoverMouseX = mouseLocation.x;
|
||||
lastHoverMouseY = mouseLocation.y;
|
||||
const worldMouseLocation = screenCanvasDrawController.getWorldMouseLocation();
|
||||
const { distance, entity: closestEntity } = findClosestEntity(
|
||||
screenCanvasDrawController.getWorldMouseLocation(),
|
||||
getEntities()
|
||||
worldMouseLocation,
|
||||
// 공간 인덱스로 후보를 좁혀 O(전체) 스캔 제거
|
||||
queryEntitiesNearPoint(
|
||||
worldMouseLocation.x,
|
||||
worldMouseLocation.y,
|
||||
HIGHLIGHT_ENTITY_DISTANCE
|
||||
)
|
||||
);
|
||||
|
||||
if (distance < HIGHLIGHT_ENTITY_DISTANCE) {
|
||||
|
||||
@@ -42,11 +42,13 @@ let entities: Entity[] = [];
|
||||
* Entities that are highlighted: when the mouse is close to an entity
|
||||
*/
|
||||
let highlightedEntityIds: string[] = [];
|
||||
let highlightedEntityIdSet: Set<string> = new Set();
|
||||
|
||||
/**
|
||||
* Entities that are selected by the user by clicking on them with the select tool or by selecting them with a selection rectangle
|
||||
*/
|
||||
let selectedEntityIds: string[] = [];
|
||||
let selectedEntityIdSet: Set<string> = new Set();
|
||||
|
||||
/**
|
||||
* Whether to draw the cursor or not
|
||||
@@ -163,6 +165,12 @@ let layers: Layer[] = [
|
||||
*/
|
||||
let activeLayerId: string = layers[0].id;
|
||||
|
||||
/**
|
||||
* layerId → Layer lookup, kept in sync with `layers` (drawEntities runs this
|
||||
* lookup once per entity per frame — a linear find() was a hot spot)
|
||||
*/
|
||||
let layersById: Map<string, Layer> = new Map(layers.map((layer) => [layer.id, layer]));
|
||||
|
||||
let snapEnabled = true;
|
||||
let gridEnabled = false;
|
||||
|
||||
@@ -208,17 +216,18 @@ export const getInputController = (): InputController => {
|
||||
};
|
||||
|
||||
export const getSelectedEntities = (): Entity[] => {
|
||||
return entities.filter((e) => selectedEntityIds.includes(e.id));
|
||||
return entities.filter((e) => selectedEntityIdSet.has(e.id));
|
||||
};
|
||||
export const getNotSelectedEntities = (): Entity[] => {
|
||||
return entities.filter((e) => !selectedEntityIds.includes(e.id));
|
||||
return entities.filter((e) => !selectedEntityIdSet.has(e.id));
|
||||
};
|
||||
export const isEntitySelected = (entity: Entity) => selectedEntityIds.includes(entity.id);
|
||||
export const isEntityHighlighted = (entity: Entity) => highlightedEntityIds.includes(entity.id);
|
||||
export const isEntitySelected = (entity: Entity) => selectedEntityIdSet.has(entity.id);
|
||||
export const isEntityHighlighted = (entity: Entity) => highlightedEntityIdSet.has(entity.id);
|
||||
export const getHighlightedEntityIds = () => highlightedEntityIds;
|
||||
export const getLayers = () => {
|
||||
return layers;
|
||||
};
|
||||
export const getLayerById = (layerId: string): Layer | undefined => layersById.get(layerId);
|
||||
export const getActiveLayerId = (): string => {
|
||||
return activeLayerId;
|
||||
};
|
||||
@@ -288,9 +297,11 @@ export const setEntities = (newEntities: Entity[], trackInUndoStack = false) =>
|
||||
};
|
||||
export const setHighlightedEntityIds = (newEntityIds: string[]) => {
|
||||
highlightedEntityIds = newEntityIds;
|
||||
highlightedEntityIdSet = new Set(newEntityIds);
|
||||
};
|
||||
export const setSelectedEntityIds = (newEntityIds: string[]) => {
|
||||
selectedEntityIds = newEntityIds;
|
||||
selectedEntityIdSet = new Set(newEntityIds);
|
||||
bumpSceneVersion(); // selection style (dashed) is baked into the scene cache
|
||||
window.dispatchEvent(new CustomEvent(HtmlEvent.UPDATE_STATE));
|
||||
};
|
||||
@@ -378,6 +389,7 @@ export const setActiveTextStyle = (
|
||||
};
|
||||
export const setLayers = (newLayers: Layer[], triggerReact = true) => {
|
||||
layers = newLayers;
|
||||
layersById = new Map(newLayers.map((layer) => [layer.id, layer]));
|
||||
bumpSceneVersion(); // layer visibility/lock affects what the scene cache shows
|
||||
|
||||
if (triggerReact) {
|
||||
|
||||
Reference in New Issue
Block a user