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

348 lines
8.1 KiB
TypeScript

import type { HousePlannerCanvas } from './canvas';
import { HousePlannerCanvasHistory } from './history';
import {
History,
ICanvasToolBase,
ICanvasToolMouseEvents,
Layer,
LayerObject,
Line,
ToolEvent,
Vec2,
} from './interfaces';
import { LineTool } from './tools/line';
import { MoveTool } from './tools/move';
export class HousePlannerCanvasTools implements ICanvasToolMouseEvents {
public selectedLayer?: Layer;
public selectedObjects: LayerObject[] = [];
public gridSnap = true;
public gridSnapScale = 8;
public tool?: ICanvasToolBase<unknown>;
public tools: Record<string, ICanvasToolBase<unknown>> = {
['move']: new MoveTool(this),
['line']: new LineTool(this),
};
public history = new HousePlannerCanvasHistory();
public lastStrokeWidth = 16;
public lastColor = '#000000';
public holdShift = false;
public selectError = 16;
constructor(public manager: HousePlannerCanvas) {}
get canvas() {
return this.manager.canvas;
}
get ctx() {
return this.manager.ctx;
}
isSelected(object: LayerObject) {
return this.selectedObjects.indexOf(object) > -1;
}
selectLayer(layer: Layer) {
if (this.selectedLayer) {
this.selectedLayer.active = false;
for (const item of this.selectedLayer.contents) {
item.selected = false;
}
this.selectedObjects.length = 0;
}
this.selectedLayer = layer;
this.selectedLayer.active = true;
this.canvas.dispatchEvent(
new CustomEvent('hpc:layerchange', {
detail: this.selectedObjects,
})
);
this.canvas.dispatchEvent(
new CustomEvent('hpc:update', {
detail: {
event: 'layerchange',
},
})
);
this.selectedEvent();
this.manager.draw();
}
selectObject(object?: LayerObject | null, add = false) {
if (!object) {
if (!add) {
this.selectedObjects.forEach((object) => {
object.selected = false;
});
this.selectedObjects.length = 0;
this.tool?.selectionChanged(this.selectedObjects);
}
this.manager.draw();
this.selectedEvent();
return;
}
if (this.selectedObjects.includes(object)) {
// Unselect in multi-select
if (add) {
const foundAt = this.selectedObjects.indexOf(object);
object.selected = false;
this.selectedObjects.splice(foundAt, 1);
this.tool?.selectionChanged(this.selectedObjects);
this.manager.draw();
this.selectedEvent();
}
return;
}
// Select in multi-select
if (add) {
object.selected = true;
this.selectedObjects.push(object);
this.tool?.selectionChanged(this.selectedObjects);
this.manager.draw();
this.selectedEvent();
return;
}
this.selectedObjects.forEach((obj) => {
obj.selected = false;
});
object.selected = true;
this.selectedObjects = [object];
this.tool?.selectionChanged(this.selectedObjects);
this.manager.draw();
this.selectedEvent();
}
setInitialSelection() {
if (!this.selectedLayer) return;
this.selectedObjects = this.selectedLayer.contents.filter(
(object) => object.selected
);
}
getMousedObject() {
if (!this.selectedLayer?.visible) return null;
const [x, y] = this.manager.mousePosition;
for (const object of this.selectedLayer.contents) {
if ((object as Line).segments) {
const moused = this.manager.isOnLine(
object as Line,
x,
y,
this.selectError
);
if (moused) return object;
}
// TODO: other kinds of objects
}
return null;
}
getMousedLineSegment(line: Line) {
if (!this.selectedLayer?.visible) return null;
const [x, y] = this.manager.mousePosition;
let lastSegment = null;
for (const segment of line.segments) {
const lastPoint = lastSegment ? lastSegment.end : (segment.start as Vec2);
if (
this.manager.isOnSegment(
line,
segment,
lastPoint,
x,
y,
this.selectError
)
) {
return segment;
}
lastSegment = segment;
}
return null;
}
drawHighlights() {
for (const object of this.selectedObjects) {
const line = object as Line;
if (line.segments) {
const path = this.manager.makeLinePath(line);
this.manager.setupLine(line, this.selectError, '#00ddff55');
this.ctx.stroke(path);
}
// TODO: other kinds of objects
}
this.tool?.drawHighlights();
}
drawControls() {
this.tool?.drawControls();
}
cleanUp() {}
setTool(tool?: string, subTool?: string) {
if (!tool) {
this.tool?.deactivate();
this.setTool('move');
return;
}
if (this.tool?.name === tool) {
if (!subTool) return;
this.tool.setSubTool(subTool);
this.canvas.dispatchEvent(
new CustomEvent<ToolEvent>('hpc:tool', {
detail: { primary: this.tool.name, secondary: this.tool.subTool },
})
);
this.manager.draw();
return;
}
const findTool = this.tools[tool];
if (!findTool) return;
if (this.tool) this.tool.deactivate();
this.tool = findTool;
this.tool.activate();
if (subTool) {
this.tool.setSubTool(subTool);
}
this.canvas.dispatchEvent(
new CustomEvent<ToolEvent>('hpc:tool', {
detail: { primary: this.tool.name, secondary: this.tool.subTool },
})
);
this.manager.draw();
}
get activeTool() {
return this.tool?.name;
}
mouseDown(targetObject?: LayerObject | undefined): void {
this?.tool?.mouseDown(targetObject);
}
mouseMoved(mouse: Vec2, offset: Vec2, mouseAbsolute: Vec2): void {
this?.tool?.mouseMoved(mouse, offset, mouseAbsolute);
}
mouseUp(moved: boolean): void {
this?.tool?.mouseUp(moved);
}
onKeyDown(e: KeyboardEvent) {
if (e.key === 'z' && e.ctrlKey) {
e.preventDefault();
this.history.undo();
this.manager.draw();
this.canvas.dispatchEvent(
new CustomEvent('hpc:undo', {
detail: 'keyboard',
})
);
this.canvas.dispatchEvent(
new CustomEvent('hpc:update', {
detail: {
event: 'undo',
},
})
);
}
if (e.key === 'y' && e.ctrlKey) {
e.preventDefault();
this.history.redo();
this.manager.draw();
this.canvas.dispatchEvent(
new CustomEvent('hpc:redo', {
detail: 'keyboard',
})
);
this.canvas.dispatchEvent(
new CustomEvent('hpc:update', {
detail: { event: 'redo' },
})
);
}
if (e.key === 'Shift') {
this.holdShift = true;
}
}
onKeyUp(e: KeyboardEvent) {
if (e.key === 'Enter') {
e.preventDefault();
this.tool?.enterPress(e);
}
if (e.key === 'Escape') {
e.preventDefault();
this.tool?.escapePress(e);
if (this.selectedObjects.length === 0 && this.tool?.isToolCancelable()) {
this.setTool();
}
this.selectObject(null);
}
if (e.key === 'Shift') {
e.preventDefault();
this.holdShift = false;
}
if (e.key === 'l') {
e.preventDefault();
this.setTool('line');
}
if (e.key === 'Delete' || e.key === 'x') {
e.preventDefault();
this.deleteSelection();
}
}
deleteSelection() {
if (!this.selectedObjects.length || !this.selectedLayer) return;
this.history.appendToHistory([
{
object: this.selectedLayer,
property: 'contents',
value: [...this.selectedLayer.contents],
} as History<typeof this.selectedLayer>,
]);
this.selectedLayer.contents = this.selectedLayer.contents.filter(
(item) => this.selectedObjects.indexOf(item) === -1
);
this.selectedObjects.length = 0;
this.tool?.selectionDeleted();
this.tool?.selectionChanged([]);
this.manager.draw();
this.canvas.dispatchEvent(
new CustomEvent('hpc:update', {
detail: { event: 'delete' },
})
);
}
private selectedEvent() {
this.canvas.dispatchEvent(
new CustomEvent('hpc:selectionchange', {
detail: this.selectedObjects,
})
);
}
}