homemanager-fe/src/modules/house-planner/canvas.ts

224 lines
6.1 KiB
TypeScript

import { HousePlannerCanvasGrid } from './grid';
import { BezierSegment, Layer, Line, LineSegment, Vec2 } from './interfaces';
import { HousePlannerCanvasTools } from './tools';
import {
rad2deg,
vec2Add,
vec2AngleFromOrigin,
vec2Distance,
vec2DivideScalar,
vec2PointFromAngle,
} from './utils';
export class HousePlannerCanvas {
public ctx!: CanvasRenderingContext2D;
public layers: Layer[] = [];
public tools = new HousePlannerCanvasTools(this);
public grid = new HousePlannerCanvasGrid(this, 8);
constructor(public canvas: HTMLCanvasElement) {
this.ctx = this.canvas.getContext('2d')!;
this.setupEvents();
}
get width() {
return this.canvas.width;
}
get height() {
return this.canvas.height;
}
addLayer(layer: Layer) {
this.layers.push(layer);
this.draw();
}
cleanUp() {
this.tools.cleanUp();
window.removeEventListener('keyup', this.boundKeyUpEvent);
window.removeEventListener('keydown', this.boundKeyDownEvent);
}
draw() {
this.ctx.clearRect(0, 0, this.width, this.height);
this.grid.draw();
this.tools.drawHighlights();
for (const layer of this.layers) {
if (!layer.visible) continue;
if (!layer.contents?.length) continue;
this.drawLayer(layer);
}
this.tools.drawControls();
}
makeBezier(segment: BezierSegment, path: Path2D) {
const bezier = segment as BezierSegment;
const [cp1x, cp1y] = bezier.startControl;
const [cp2x, cp2y] = bezier.endControl;
const [x, y] = bezier.end;
path.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y);
}
makeLinePath(line: Line) {
const path = new Path2D();
const [firstSegment, ...segments] = line.segments;
// first segment must have a starting point
if (!firstSegment.start) return path;
path.moveTo(...firstSegment.start);
if (line.type === 'curve') {
const lineLength = vec2Distance(firstSegment.start, firstSegment.end);
const lineAngle = vec2AngleFromOrigin(
firstSegment.end,
firstSegment.start!
);
const ninety = lineAngle + Math.PI / 2;
path.moveTo(...firstSegment.end);
path.arc(
firstSegment.start[0],
firstSegment.start[1],
lineLength,
lineAngle,
ninety
);
path.lineTo(...firstSegment.start);
} else if ((firstSegment as BezierSegment).startControl) {
this.makeBezier(firstSegment as BezierSegment, path);
} else {
path.lineTo(...firstSegment.end);
}
for (const segment of segments) {
if ((segment as BezierSegment).startControl) {
this.makeBezier(segment as BezierSegment, path);
continue;
}
path.lineTo(...segment.end);
}
if (line.closed && line.type !== 'curve') {
path.closePath();
}
return path;
}
setupLine(line: Line, overBounds = 0, overrideColor?: string) {
if (line.lineDash) {
this.ctx.setLineDash(line.lineDash);
}
this.ctx.strokeStyle = overrideColor || line.color || '#000';
this.ctx.lineWidth = line.width + overBounds;
if (line.lineCap) {
this.ctx.lineCap = line.lineCap;
}
}
isOnLine(line: Line, x: number, y: number, selectError = 16) {
let path = line.render || this.makeLinePath(line);
this.setupLine(line, selectError);
return this.ctx.isPointInStroke(path, x, y);
}
isOnSegment(
line: Line,
segment: LineSegment,
lastPoint: Vec2,
x: number,
y: number,
selectError = 16
) {
const fakePath = this.makeLinePath({
...line,
closed: false,
segments: [{ ...segment, start: lastPoint }],
});
this.setupLine(line, selectError);
return this.ctx.isPointInStroke(fakePath, x, y);
}
private keyDownEvent(e: KeyboardEvent) {
if (e.target !== document.body && e.target != null) return;
e.preventDefault();
this.tools.onKeyDown(e);
}
private boundKeyDownEvent = this.keyDownEvent.bind(this);
private keyUpEvent(e: KeyboardEvent) {
if (e.target !== document.body && e.target != null) return;
e.preventDefault();
this.tools.onKeyUp(e);
}
private boundKeyUpEvent = this.keyUpEvent.bind(this);
private setupEvents() {
window.addEventListener('keyup', this.boundKeyUpEvent);
window.addEventListener('keydown', this.boundKeyDownEvent);
this.canvas.addEventListener('mousemove', (e) => this.tools.onMouseMove(e));
this.canvas.addEventListener('mousedown', (e) => this.tools.onMouseDown(e));
this.canvas.addEventListener('mouseup', (e) => this.tools.onMouseUp(e));
this.canvas.addEventListener('touchmove', (e) => this.tools.onTouchMove(e));
this.canvas.addEventListener('touchstart', (e) =>
this.tools.onTouchStart(e)
);
this.canvas.addEventListener('touchend', (e) => this.tools.onTouchEnd(e));
this.canvas.addEventListener('wheel', (e) => this.tools.onMouseWheel(e));
this.canvas.addEventListener('pointerleave', () =>
this.tools.onPointerLeave()
);
}
private drawRoomText(line: Line) {
const centerPoint = vec2DivideScalar(
line.segments.reduce<Vec2 | null>((prev, curr) => {
if (!prev) {
if (curr.start) {
return vec2Add(curr.start, curr.end);
}
return curr.end;
}
let preadd = vec2Add(prev, curr.end);
if (curr.start) {
preadd = vec2Add(curr.start, preadd);
}
return preadd;
}, null) as Vec2,
line.segments.length + 1
);
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] - 8
);
}
private drawLine(line: Line) {
const path = this.makeLinePath(line);
line.render = path;
this.setupLine(line);
this.ctx.stroke(path);
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;
}
}
}
}