2023-01-16 19:37:39 +00:00
|
|
|
import { HousePlannerCanvasGrid } from './grid';
|
2023-01-17 18:32:15 +00:00
|
|
|
import {
|
|
|
|
BezierSegment,
|
|
|
|
Layer,
|
|
|
|
LayerObject,
|
|
|
|
Line,
|
|
|
|
LineSegment,
|
|
|
|
Vec2,
|
|
|
|
} from './interfaces';
|
2023-01-16 19:37:39 +00:00
|
|
|
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;
|
|
|
|
|
2023-01-18 17:00:51 +00:00
|
|
|
this.ctx.lineCap = line.lineCap || 'butt';
|
|
|
|
this.ctx.lineJoin = line.lineJoin || 'miter';
|
2023-01-16 19:37:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2023-01-17 18:32:15 +00:00
|
|
|
getLayerById(layerId: number) {
|
|
|
|
return this.layers.find((layer) => layer.id === layerId);
|
|
|
|
}
|
|
|
|
|
|
|
|
getLayerObjectById(layerId: number, objectId: number) {
|
|
|
|
const findLayer = this.getLayerById(layerId);
|
|
|
|
if (!findLayer) return undefined;
|
|
|
|
const findObject = findLayer.contents.find(
|
|
|
|
(content) => content.id === objectId
|
|
|
|
);
|
|
|
|
if (!findObject) return undefined;
|
|
|
|
return findObject;
|
|
|
|
}
|
|
|
|
|
|
|
|
updateObjectProperties(
|
|
|
|
layerId: number,
|
|
|
|
objectId: number,
|
|
|
|
properties: Partial<Omit<LayerObject | Line, 'segments'>>
|
|
|
|
) {
|
|
|
|
const object = this.getLayerObjectById(layerId, objectId);
|
|
|
|
if (!object) return;
|
|
|
|
this.tools.history.appendToHistory(
|
|
|
|
Object.keys(properties).map((key) => ({
|
|
|
|
object,
|
|
|
|
property: key,
|
|
|
|
value: object[key as keyof typeof object],
|
|
|
|
}))
|
|
|
|
);
|
|
|
|
Object.assign(object, properties);
|
|
|
|
this.canvas.dispatchEvent(
|
|
|
|
new CustomEvent('hpc:update', {
|
|
|
|
detail: { event: 'properties-object', object },
|
|
|
|
})
|
|
|
|
);
|
|
|
|
this.draw();
|
|
|
|
return object;
|
|
|
|
}
|
|
|
|
|
|
|
|
updateLayerProperties(
|
|
|
|
layerId: number,
|
|
|
|
properties: Partial<Omit<Layer, 'contents'>>
|
|
|
|
) {
|
|
|
|
const layer = this.getLayerById(layerId);
|
|
|
|
if (!layer) return;
|
|
|
|
this.tools.history.appendToHistory(
|
|
|
|
Object.keys(properties).map((key) => ({
|
|
|
|
object: layer,
|
|
|
|
property: key,
|
|
|
|
value: layer[key as keyof typeof layer],
|
|
|
|
}))
|
|
|
|
);
|
|
|
|
Object.assign(layer, properties);
|
|
|
|
this.canvas.dispatchEvent(
|
|
|
|
new CustomEvent('hpc:update', {
|
|
|
|
detail: { event: 'properties-layer', layer },
|
|
|
|
})
|
|
|
|
);
|
|
|
|
this.draw();
|
|
|
|
return layer;
|
|
|
|
}
|
|
|
|
|
2023-01-16 19:37:39 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|