164 lines
4.3 KiB
TypeScript
164 lines
4.3 KiB
TypeScript
import { Canvas, CanvasRenderingContext2D } from 'canvas';
|
|
import {
|
|
extractLinePoints,
|
|
vec2Add,
|
|
vec2AngleFromOrigin,
|
|
vec2Distance,
|
|
vec2DivideScalar,
|
|
vec2Sub,
|
|
} from './renderer-utils';
|
|
import {
|
|
BezierSegment,
|
|
FloorDocument,
|
|
Layer,
|
|
Line,
|
|
Vec2,
|
|
} from './renderer.interfaces';
|
|
|
|
export class PlanRenderer {
|
|
constructor(
|
|
public width: number,
|
|
public height: number,
|
|
public origin: Vec2,
|
|
public ctx: CanvasRenderingContext2D,
|
|
) {}
|
|
|
|
draw(layers: Layer[]) {
|
|
for (const layer of layers.reverse()) {
|
|
if (!layer.visible) continue;
|
|
this.drawLayer(layer);
|
|
}
|
|
}
|
|
|
|
makeBezier(segment: BezierSegment) {
|
|
const [ox, oy] = this.origin;
|
|
const bezier = segment as BezierSegment;
|
|
const [cp1x, cp1y] = bezier.startControl;
|
|
const [cp2x, cp2y] = bezier.endControl;
|
|
const [x, y] = bezier.end;
|
|
this.ctx.bezierCurveTo(
|
|
cp1x - ox,
|
|
cp1y - oy,
|
|
cp2x - ox,
|
|
cp2y - oy,
|
|
x - ox,
|
|
y - oy,
|
|
);
|
|
}
|
|
|
|
makeLinePath(line: Line) {
|
|
const [ox, oy] = this.origin;
|
|
const [firstSegment, ...segments] = line.segments;
|
|
// first segment must have a starting point
|
|
if (!firstSegment.start) return;
|
|
this.ctx.moveTo(...vec2Sub(firstSegment.start, this.origin));
|
|
|
|
if (line.type === 'curve') {
|
|
const lineLength = vec2Distance(firstSegment.start, firstSegment.end);
|
|
const lineAngle = vec2AngleFromOrigin(
|
|
firstSegment.end,
|
|
firstSegment.start,
|
|
);
|
|
const ninety = lineAngle + Math.PI / 2;
|
|
this.ctx.moveTo(...vec2Sub(firstSegment.end, this.origin));
|
|
this.ctx.arc(
|
|
firstSegment.start[0] - ox,
|
|
firstSegment.start[1] - oy,
|
|
lineLength,
|
|
lineAngle,
|
|
ninety,
|
|
);
|
|
this.ctx.lineTo(...vec2Sub(firstSegment.start, this.origin));
|
|
} else if ((firstSegment as BezierSegment).startControl) {
|
|
this.makeBezier(firstSegment as BezierSegment);
|
|
} else {
|
|
this.ctx.lineTo(...vec2Sub(firstSegment.end, this.origin));
|
|
}
|
|
|
|
for (const segment of segments) {
|
|
if (segment.start) {
|
|
this.ctx.moveTo(...vec2Sub(segment.start, this.origin));
|
|
}
|
|
if ((segment as BezierSegment).startControl) {
|
|
this.makeBezier(segment as BezierSegment);
|
|
continue;
|
|
}
|
|
|
|
this.ctx.lineTo(...vec2Sub(segment.end, this.origin));
|
|
}
|
|
if (line.closed && line.type !== 'curve') {
|
|
this.ctx.closePath();
|
|
}
|
|
}
|
|
|
|
setupLine(line: Line) {
|
|
this.ctx.beginPath();
|
|
if (line.lineDash) {
|
|
this.ctx.setLineDash(line.lineDash);
|
|
}
|
|
|
|
this.ctx.strokeStyle = line.color || '#000000';
|
|
this.ctx.lineWidth = line.width;
|
|
|
|
this.ctx.lineCap = line.lineCap || 'butt';
|
|
this.ctx.lineJoin = line.lineJoin || 'miter';
|
|
}
|
|
|
|
private drawRoomText(line: Line) {
|
|
const points = extractLinePoints(line);
|
|
const centerPoint = vec2Sub(
|
|
vec2DivideScalar(
|
|
points.reduce<Vec2 | null>(
|
|
(prev, current) => (prev ? vec2Add(prev, current) : current),
|
|
null,
|
|
) as Vec2,
|
|
points.length,
|
|
),
|
|
this.origin,
|
|
);
|
|
|
|
this.ctx.font = '16px Arial';
|
|
this.ctx.fillStyle = line.color;
|
|
const { width } = this.ctx.measureText(line.name);
|
|
this.ctx.fillText(line.name, centerPoint[0] - width / 2, centerPoint[1]);
|
|
}
|
|
|
|
private drawLine(line: Line) {
|
|
this.setupLine(line);
|
|
this.makeLinePath(line);
|
|
this.ctx.stroke();
|
|
if (line.type === 'room') {
|
|
this.drawRoomText(line);
|
|
}
|
|
}
|
|
|
|
private drawLayer(layer: Layer) {
|
|
for (const item of layer.contents) {
|
|
if (!item.visible) continue;
|
|
const line = item as Line;
|
|
if (line.segments) {
|
|
this.drawLine(line);
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
public static renderFloorDocument(document: FloorDocument) {
|
|
let minBound: Vec2 = [0, 0];
|
|
let maxBound: Vec2 = [document.width, document.height];
|
|
if (document.boundingBox) {
|
|
const offset = 80;
|
|
minBound = vec2Sub(document.boundingBox[0], [offset, offset]);
|
|
maxBound = vec2Add(document.boundingBox[1], [offset, offset]);
|
|
}
|
|
|
|
const width = maxBound[0] - minBound[0];
|
|
const height = maxBound[1] - minBound[1];
|
|
const canvas = new Canvas(width, height, 'image');
|
|
const ctx = canvas.getContext('2d');
|
|
const render = new PlanRenderer(width, height, minBound, ctx);
|
|
render.draw(document.layers);
|
|
return canvas.createPNGStream();
|
|
}
|
|
}
|