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

255 lines
7.1 KiB
TypeScript

import {
BezierSegment,
LayerObject,
Line,
LineSegment,
Vec2,
} from '../interfaces';
import { BezierControl, LineControl } from '../types';
import { vec2InCircle, vec2Add, vec2Inverse } from '../utils';
import { CanvasToolBase } from './tool-base';
export class MoveTool extends CanvasToolBase<undefined> {
public name = 'move';
private bezierControls: BezierSegment[] = [];
private lineControls: LineSegment[] = [];
private handlingBezier: BezierControl | null = null;
private handlingLine: LineControl | null = null;
private movingObject = false;
private clickedOn?: LayerObject;
drawControls() {
this.bezierControls.length = 0;
this.lineControls.length = 0;
for (const object of this.manager.selectedObjects) {
const line = object as Line;
if (line.segments && line.render) {
let lastSegment = null;
for (const segment of line.segments) {
const bezier = segment as BezierSegment;
const previousPoint = lastSegment ? lastSegment.end : segment.start!;
if (bezier.startControl && bezier.endControl) {
this.drawBezierControls(bezier, previousPoint);
}
this.drawLineControls(segment, previousPoint);
lastSegment = segment;
}
}
// TODO: other kinds of objects
}
}
mouseDown(targetObject?: LayerObject | undefined): void {
this.clickedOn = targetObject;
const bezierControl = this.grabbedBezierControl();
const lineControl = this.grabbedLineControl();
if (bezierControl) {
this.handlingBezier = bezierControl;
} else if (lineControl) {
this.handlingLine = lineControl;
} else if (
targetObject &&
!this.manager.selectedObjects.includes(targetObject)
) {
this.manager.selectObject(targetObject);
}
}
mouseUp(moved?: boolean): void {
if (
!this.handlingBezier &&
!this.handlingLine &&
!this.movingObject &&
!moved
) {
this.pick();
}
if (this.handlingBezier) {
this.history.appendToHistory([
{
object: this.handlingBezier[0],
property: this.handlingBezier[1],
value: this.handlingBezier[2],
},
]);
}
if (this.handlingLine) {
this.history.appendToHistory([
{
object: this.handlingLine[0],
property: this.handlingLine[1],
value: this.handlingLine[2],
},
]);
}
if (this.movingObject || this.handlingLine || this.handlingBezier) {
this.canvas.dispatchEvent(
new CustomEvent('hpc:update', {
detail: {
event: 'line-move',
object: this.handlingLine || this.handlingBezier || this.clickedOn,
},
})
);
}
this.handlingBezier = null;
this.handlingLine = null;
this.movingObject = false;
this.clickedOn = undefined;
}
private pick() {
if (this.clickedOn && this.manager.isSelected(this.clickedOn)) {
// Pick line segment
if ((this.clickedOn as Line).segments) {
const segment = this.manager.getMousedLineSegment(
this.clickedOn as Line
);
}
}
this.manager.selectObject(this.clickedOn, this.manager.holdShift);
}
mouseMoved(mouse: Vec2, offset: Vec2, mouseAbsolute: Vec2): void {
super.mouseMoved(mouse, offset, mouseAbsolute);
if (!this.layer) return;
if (this.handlingBezier) {
const [segment, property] = this.handlingBezier;
segment[property] = [...mouse];
this.renderer.draw();
} else if (this.handlingLine) {
const [segment, property] = this.handlingLine;
segment[property] = [...mouse];
this.renderer.draw();
} else if (this.clickedOn) {
if (!this.movingObject) {
// TODO: optimize history storage
this.history.appendToHistory([
{
object: this.layer,
property: 'contents',
value: JSON.parse(JSON.stringify(this.layer['contents'])),
},
]);
this.movingObject = true;
}
this.translate(vec2Inverse(offset));
}
}
drawBezierControls(bezier: BezierSegment, previousEnd: Vec2) {
this.bezierControls.push(bezier);
const [cp1x, cp1y] = bezier.startControl;
const [cp2x, cp2y] = bezier.endControl;
const [endx, endy] = bezier.end;
const [prevx, prevy] = previousEnd;
this.ctx.fillStyle = '#00ddffaa';
this.ctx.strokeStyle = '#00ddffaa';
this.ctx.lineWidth = 2;
this.ctx.beginPath();
this.ctx.arc(cp1x, cp1y, this.manager.selectError / 2, 0, 2 * Math.PI);
this.ctx.fill();
this.ctx.beginPath();
this.ctx.arc(cp2x, cp2y, this.manager.selectError / 2, 0, 2 * Math.PI);
this.ctx.fill();
this.ctx.beginPath();
this.ctx.moveTo(cp1x, cp1y);
this.ctx.lineTo(prevx, prevy);
this.ctx.stroke();
this.ctx.beginPath();
this.ctx.moveTo(cp2x, cp2y);
this.ctx.lineTo(endx, endy);
this.ctx.stroke();
}
drawLineControls(line: LineSegment, previousEnd: Vec2) {
this.lineControls.push(line);
const [endx, endy] = line.end;
this.ctx.fillStyle = '#00ddffaa';
this.ctx.beginPath();
this.ctx.arc(endx, endy, this.manager.selectError / 2, 0, 2 * Math.PI);
this.ctx.fill();
if (line.start) {
const [startx, starty] = line.start;
this.ctx.beginPath();
this.ctx.arc(
startx,
starty,
this.manager.selectError / 2,
0,
2 * Math.PI
);
this.ctx.fill();
}
}
private grabbedBezierControl() {
let clickedOnControl: BezierControl | null = null;
for (const bezier of this.bezierControls) {
for (const control of ['startControl', 'endControl']) {
const asType = control as 'startControl' | 'endControl';
if (
vec2InCircle(
bezier[asType],
this.mousePosition,
this.manager.selectError / 2
)
) {
clickedOnControl = [bezier, asType, bezier[asType]];
}
}
}
return clickedOnControl;
}
private grabbedLineControl() {
let clickedOnControl: LineControl | null = null;
for (const line of this.lineControls) {
for (const control of ['start', 'end']) {
const asType = control as 'start' | 'end';
if (
line[asType] &&
vec2InCircle(
line[asType] as Vec2,
this.mousePosition,
this.manager.selectError / 2
)
) {
clickedOnControl = [line, asType, line[asType] as Vec2];
}
}
}
return clickedOnControl;
}
private translate(offset: Vec2) {
for (const object of this.manager.selectedObjects) {
if ((object as Line).segments) {
for (const segment of (object as Line).segments) {
if ((segment as BezierSegment).startControl) {
const bezier = segment as BezierSegment;
bezier.startControl = vec2Add(bezier.startControl, offset);
bezier.endControl = vec2Add(bezier.endControl, offset);
}
if (segment.start) {
segment.start = vec2Add(segment.start, offset);
}
segment.end = vec2Add(segment.end, offset);
}
}
}
this.renderer.draw();
}
}