homemanager-be/src/app-building/plan-renderer/renderer/index.ts

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();
}
}