auto: 2026-07-26 20:54 (ESD_LAPTOP)

This commit is contained in:
2026-07-26 20:54:20 +09:00
parent 99babfacaf
commit eb5bdfebad
7 changed files with 316 additions and 14 deletions
@@ -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) => {