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) => {
|
||||
|
||||
Reference in New Issue
Block a user