446 lines
11 KiB
TypeScript
446 lines
11 KiB
TypeScript
import type { HousePlannerCanvas } from './canvas';
|
|
import { HousePlannerCanvasClipboard } from './clipboard';
|
|
import { HousePlannerCanvasHistory } from './history';
|
|
import {
|
|
History,
|
|
ICanvasToolBase,
|
|
ICanvasToolkit,
|
|
Layer,
|
|
LayerObject,
|
|
Line,
|
|
ToolEvent,
|
|
Vec2,
|
|
} from './interfaces';
|
|
import { CutTool } from './tools/cut';
|
|
import { LineTool } from './tools/line';
|
|
import { MoveTool } from './tools/move';
|
|
|
|
export class HousePlannerCanvasTools implements ICanvasToolkit {
|
|
public selectedLayer?: Layer;
|
|
public selectedObjects: LayerObject[] = [];
|
|
public gridSnap = true;
|
|
public gridSnapScale = 8;
|
|
public autoClose = false;
|
|
public tool?: ICanvasToolBase<unknown>;
|
|
public tools: Record<string, ICanvasToolBase<unknown>> = {
|
|
['move']: new MoveTool(this),
|
|
['line']: new LineTool(this),
|
|
['cut']: new CutTool(this),
|
|
};
|
|
public history = new HousePlannerCanvasHistory();
|
|
public clipboard = new HousePlannerCanvasClipboard();
|
|
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 === 'Shift') {
|
|
this.holdShift = true;
|
|
}
|
|
|
|
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 === 'a' && e.ctrlKey) {
|
|
e.preventDefault();
|
|
this.selectAll();
|
|
}
|
|
|
|
if (e.key === 'c' && e.ctrlKey) {
|
|
e.preventDefault();
|
|
if (this.selectedObjects.length)
|
|
this.clipboard.storeToClipboard(this.selectedObjects);
|
|
}
|
|
|
|
if (e.key === 'x' && e.ctrlKey) {
|
|
e.preventDefault();
|
|
if (this.selectedObjects.length && this.selectedLayer) {
|
|
this.clipboard.storeToClipboard(this.selectedObjects, true);
|
|
this.deleteSelection([
|
|
{
|
|
object: this.clipboard,
|
|
property: 'wasCutOperation',
|
|
value: false,
|
|
},
|
|
]);
|
|
}
|
|
}
|
|
|
|
if (e.key === 'v' && e.ctrlKey) {
|
|
e.preventDefault();
|
|
if (!this.selectedLayer) {
|
|
return;
|
|
}
|
|
|
|
const items = this.clipboard.getFromClipboard(
|
|
this.getSequentialId(),
|
|
true
|
|
);
|
|
|
|
if (items.length) {
|
|
this.selectedLayer?.contents.push(...items);
|
|
this.selectObject(null);
|
|
this.selectedObjects = items;
|
|
this.tool?.selectionChanged(this.selectedObjects);
|
|
this.manager.draw();
|
|
}
|
|
}
|
|
|
|
if (e.key === 's' && e.ctrlKey) {
|
|
e.preventDefault();
|
|
this.canvas.dispatchEvent(new CustomEvent('hpc:save'));
|
|
}
|
|
}
|
|
|
|
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.preventDefault();
|
|
this.deleteSelection();
|
|
}
|
|
}
|
|
|
|
deleteSelection(additionalHistory?: History<any>[]) {
|
|
if (!this.selectedObjects.length || !this.selectedLayer) return;
|
|
this.history.appendToHistory([
|
|
{
|
|
object: this.selectedLayer,
|
|
property: 'contents',
|
|
value: [...this.selectedLayer.contents],
|
|
} as History<typeof this.selectedLayer>,
|
|
...(additionalHistory || []),
|
|
]);
|
|
|
|
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' },
|
|
})
|
|
);
|
|
}
|
|
|
|
selectAll() {
|
|
if (!this.selectedLayer) return;
|
|
if (
|
|
this.selectedLayer.contents
|
|
.filter((item) => item.visible)
|
|
.every((item) => item.selected)
|
|
) {
|
|
for (const item of this.selectedLayer.contents) {
|
|
item.selected = false;
|
|
}
|
|
this.selectedObjects.length = 0;
|
|
this.selectedEvent();
|
|
this.tool?.selectionChanged([]);
|
|
this.manager.draw();
|
|
return;
|
|
}
|
|
|
|
const visibleItems = this.selectedLayer.contents.filter(
|
|
(item) => item.visible
|
|
);
|
|
for (const item of visibleItems) {
|
|
item.selected = true;
|
|
}
|
|
this.selectedObjects = [...visibleItems];
|
|
this.tool?.selectionChanged(this.selectedObjects);
|
|
this.selectedEvent();
|
|
this.manager.draw();
|
|
}
|
|
|
|
getSequentialId() {
|
|
return (
|
|
this.manager.layers.reduce((total, current) => {
|
|
const biggestInLayer = current.contents.reduce<number>(
|
|
(biggest2, current2) =>
|
|
biggest2 > current2.id ? biggest2 : current2.id,
|
|
0
|
|
);
|
|
return total > biggestInLayer ? total : biggestInLayer;
|
|
}, 0) + 1
|
|
);
|
|
}
|
|
|
|
private selectedEvent() {
|
|
this.canvas.dispatchEvent(
|
|
new CustomEvent('hpc:selectionchange', {
|
|
detail: this.selectedObjects,
|
|
})
|
|
);
|
|
}
|
|
}
|