From 668b2c500139d9c712bc4ed7a2c629850f2e7ae9 Mon Sep 17 00:00:00 2001 From: Evert Prants Date: Wed, 18 Jan 2023 19:00:51 +0200 Subject: [PATCH] revamp tooling a bit --- src/components/ColorInput.vue | 47 ++ src/components/house-planner/HousePlanner.vue | 20 +- .../house-planner/PlannerLayerPanel.vue | 2 +- .../house-planner/PlannerPropertyPanel.vue | 68 +-- .../house-planner/PlannerSidebar.vue | 7 +- .../house-planner/PlannerSidebars.vue | 8 +- src/components/house-planner/PlannerTool.vue | 3 + .../house-planner/PropertyFormItem.vue | 70 +++ .../interfaces/toolbar.interfaces.ts | 5 +- src/modules/house-planner/canvas.ts | 5 +- src/modules/house-planner/index.ts | 2 + src/modules/house-planner/interfaces.ts | 26 +- src/modules/house-planner/tools.ts | 466 +++--------------- src/modules/house-planner/tools/line.ts | 192 ++++++++ src/modules/house-planner/tools/move.ts | 249 ++++++++++ src/modules/house-planner/tools/tool-base.ts | 56 +++ src/modules/house-planner/types.ts | 3 - src/style.scss | 3 + 18 files changed, 760 insertions(+), 472 deletions(-) create mode 100644 src/components/ColorInput.vue create mode 100644 src/components/house-planner/PropertyFormItem.vue create mode 100644 src/modules/house-planner/tools/line.ts create mode 100644 src/modules/house-planner/tools/move.ts create mode 100644 src/modules/house-planner/tools/tool-base.ts diff --git a/src/components/ColorInput.vue b/src/components/ColorInput.vue new file mode 100644 index 0000000..f336bf3 --- /dev/null +++ b/src/components/ColorInput.vue @@ -0,0 +1,47 @@ + + + diff --git a/src/components/house-planner/HousePlanner.vue b/src/components/house-planner/HousePlanner.vue index 4822c08..e46c658 100644 --- a/src/components/house-planner/HousePlanner.vue +++ b/src/components/house-planner/HousePlanner.vue @@ -4,6 +4,7 @@ -
+
@@ -64,6 +23,7 @@ import { ObjectProperty, } from './interfaces/properties.interfaces'; import { computed } from 'vue'; +import PropertyFormItem from './PropertyFormItem.vue'; const props = defineProps<{ layers: Layer[]; @@ -100,6 +60,18 @@ const lineProps: ObjectProperty[] = [ { value: 'square', title: 'Square' }, ], }, + { + key: 'lineJoin', + title: 'Line Join', + type: 'select', + groupable: true, + options: [ + { value: undefined, title: '' }, + { value: 'miter', title: 'Miter' }, + { value: 'bevel', title: 'Bevel' }, + { value: 'round', title: 'Round' }, + ], + }, { key: 'closed', title: 'Closed', type: 'boolean', groupable: true }, ]; diff --git a/src/components/house-planner/PlannerSidebar.vue b/src/components/house-planner/PlannerSidebar.vue index a49e76e..5c44cb7 100644 --- a/src/components/house-planner/PlannerSidebar.vue +++ b/src/components/house-planner/PlannerSidebar.vue @@ -1,5 +1,5 @@ diff --git a/src/components/house-planner/PlannerTool.vue b/src/components/house-planner/PlannerTool.vue index 8b5f106..1a15f89 100644 --- a/src/components/house-planner/PlannerTool.vue +++ b/src/components/house-planner/PlannerTool.vue @@ -10,6 +10,7 @@ ]" > (); diff --git a/src/components/house-planner/PropertyFormItem.vue b/src/components/house-planner/PropertyFormItem.vue new file mode 100644 index 0000000..4e6ecf0 --- /dev/null +++ b/src/components/house-planner/PropertyFormItem.vue @@ -0,0 +1,70 @@ + + + diff --git a/src/components/house-planner/interfaces/toolbar.interfaces.ts b/src/components/house-planner/interfaces/toolbar.interfaces.ts index e08bc11..9bd95c1 100644 --- a/src/components/house-planner/interfaces/toolbar.interfaces.ts +++ b/src/components/house-planner/interfaces/toolbar.interfaces.ts @@ -1,10 +1,9 @@ import type { Component } from 'vue'; -import { SubToolType, ToolType } from '../../../modules/house-planner/types'; export interface ToolbarTool { title: string; icon: Component; - tool: ToolType; - subTool?: SubToolType; + tool: string; + subTool?: string; children?: ToolbarTool[]; } diff --git a/src/modules/house-planner/canvas.ts b/src/modules/house-planner/canvas.ts index 91afe6f..0d07300 100644 --- a/src/modules/house-planner/canvas.ts +++ b/src/modules/house-planner/canvas.ts @@ -118,9 +118,8 @@ export class HousePlannerCanvas { this.ctx.strokeStyle = overrideColor || line.color || '#000'; this.ctx.lineWidth = line.width + overBounds; - if (line.lineCap) { - this.ctx.lineCap = line.lineCap; - } + this.ctx.lineCap = line.lineCap || 'butt'; + this.ctx.lineJoin = line.lineJoin || 'miter'; } isOnLine(line: Line, x: number, y: number, selectError = 16) { diff --git a/src/modules/house-planner/index.ts b/src/modules/house-planner/index.ts index 9251d31..d7a12e8 100644 --- a/src/modules/house-planner/index.ts +++ b/src/modules/house-planner/index.ts @@ -14,6 +14,8 @@ export class HousePlanner { this.manager.tools.selectLayer( initialData[initialData.findIndex((layer) => layer.active)] ); + this.manager.tools.setInitialSelection(); + this.manager.tools.setTool('move'); this.manager.draw(); return () => this.cleanUp(); } diff --git a/src/modules/house-planner/interfaces.ts b/src/modules/house-planner/interfaces.ts index e35cff9..90c5a09 100644 --- a/src/modules/house-planner/interfaces.ts +++ b/src/modules/house-planner/interfaces.ts @@ -1,4 +1,4 @@ -import { LayerObjectType, SubToolType, ToolType } from './types'; +import { LayerObjectType } from './types'; export type Vec2 = [number, number]; export interface LineSegment { @@ -25,6 +25,7 @@ export interface Line extends LayerObject { color: string; render?: Path2D; lineCap?: CanvasLineCap; + lineJoin?: CanvasLineJoin; closed?: boolean; lineDash?: number[]; } @@ -50,6 +51,25 @@ export interface UpdateEvent { } export interface ToolEvent { - primary: ToolType; - secondary: SubToolType; + primary: string; + secondary?: unknown; +} + +export interface ICanvasToolBase { + name: string; + subTool: U | undefined; + drawHighlights(): void; + drawControls(): void; + mouseDown(targetObject?: LayerObject): void; + mouseMoved(mouse: Vec2, offset: Vec2, mouseAbsolute: Vec2): void; + mouseUp(moved: boolean): void; + enterPress(e: KeyboardEvent): void; + escapePress(e: KeyboardEvent): void; + setSubTool(subTool?: U): void; + activate(): void; + deactivate(): void; + selectionDeleted(): void; + selectionChanged(selection: LayerObject[]): void; + emitEvent(e: CustomEvent): void; + isToolCancelable(): boolean; } diff --git a/src/modules/house-planner/tools.ts b/src/modules/house-planner/tools.ts index 311e040..c28bba3 100644 --- a/src/modules/house-planner/tools.ts +++ b/src/modules/house-planner/tools.ts @@ -1,25 +1,17 @@ import type { HousePlannerCanvas } from './canvas'; import { HousePlannerCanvasHistory } from './history'; import { - BezierSegment, History, + ICanvasToolBase, Layer, LayerObject, Line, - LineSegment, ToolEvent, Vec2, } from './interfaces'; -import { ToolType, SubToolType, BezierControl, LineControl } from './types'; -import { - vec2Add, - vec2Distance, - vec2Equals, - vec2InCircle, - vec2Inverse, - vec2Snap, - vec2Sub, -} from './utils'; +import { LineTool } from './tools/line'; +import { MoveTool } from './tools/move'; +import { vec2Distance, vec2Snap, vec2Sub } from './utils'; export class HousePlannerCanvasTools { public selectedLayer?: Layer; @@ -29,24 +21,21 @@ export class HousePlannerCanvasTools { public mousePositionSnapped: Vec2 = [0, 0]; public gridSnap = true; public gridSnapScale = 8; - public tool: ToolType = 'line'; - public subTool: SubToolType = 'line'; + public tool?: ICanvasToolBase; + public tools: Record> = { + ['move']: new MoveTool(this), + ['line']: new LineTool(this), + }; public history = new HousePlannerCanvasHistory(); public lastStrokeWidth = 16; - public lastColor = '#000'; + public lastColor = '#000000'; + public holdShift = false; + public selectError = 16; private dragging = false; private pinching = false; private lastPinchLength = 0; private moved = false; - private holdShift = false; - private selectError = 16; - private bezierControls: BezierSegment[] = []; - private lineControls: LineSegment[] = []; - private handlingBezier: BezierControl | null = null; - private handlingLine: LineControl | null = null; - private drawingLine: Line | null = null; private clickedOn: LayerObject | null = null; - private movingObject = false; constructor(public manager: HousePlannerCanvas) {} @@ -93,6 +82,7 @@ export class HousePlannerCanvasTools { object.selected = false; }); this.selectedObjects.length = 0; + this.tool?.selectionChanged(this.selectedObjects); } this.manager.draw(); this.selectedEvent(); @@ -105,6 +95,7 @@ export class HousePlannerCanvasTools { const foundAt = this.selectedObjects.indexOf(object); object.selected = false; this.selectedObjects.splice(foundAt, 1); + this.tool?.selectionChanged(this.selectedObjects); this.manager.draw(); this.selectedEvent(); } @@ -115,6 +106,7 @@ export class HousePlannerCanvasTools { if (add) { object.selected = true; this.selectedObjects.push(object); + this.tool?.selectionChanged(this.selectedObjects); this.manager.draw(); this.selectedEvent(); return; @@ -127,10 +119,18 @@ export class HousePlannerCanvasTools { 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.mousePosition; @@ -182,93 +182,55 @@ export class HousePlannerCanvasTools { } // TODO: other kinds of objects } - } - 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.selectError / 2, 0, 2 * Math.PI); - this.ctx.fill(); - - this.ctx.beginPath(); - this.ctx.arc(cp2x, cp2y, this.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.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.selectError / 2, 0, 2 * Math.PI); - this.ctx.fill(); - } + this.tool?.drawHighlights(); } drawControls() { - this.bezierControls.length = 0; - this.lineControls.length = 0; - for (const object of this.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 - } - - if (this.tool === 'line') { - if (this.selectedObjects.length) return; - const [mx, my] = this.mousePositionSnapped; - this.ctx.fillStyle = '#00ddffaa'; - this.ctx.beginPath(); - this.ctx.arc(mx, my, this.selectError / 2, 0, 2 * Math.PI); - this.ctx.fill(); - } + this.tool?.drawControls(); } cleanUp() {} - setTool(tool: typeof this.tool, subTool?: typeof this.subTool) { - this.tool = tool; - if (subTool !== undefined) this.subTool = subTool; + 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('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('hpc:tool', { - detail: { primary: this.tool, secondary: this.subTool }, + detail: { primary: this.tool.name, secondary: this.tool.subTool }, }) ); + this.manager.draw(); + } + + get activeTool() { + return this.tool?.name; } onMouseMove(e: MouseEvent) { @@ -404,64 +366,16 @@ export class HousePlannerCanvasTools { onKeyUp(e: KeyboardEvent) { if (e.key === 'Enter') { - if (this.drawingLine && this.selectedLayer) { - if (this.subTool === 'room') { - this.drawingLine.closed = true; - } - this.drawingLine.segments.splice( - this.drawingLine.segments.length - 1, - 1 - ); - this.history.appendToHistory([ - { - object: this.selectedLayer, - property: 'contents', - value: [...this.selectedLayer.contents].filter( - (item) => item !== this.drawingLine - ), - } as History, - { - object: this, - property: 'selectedObjects', - value: [], - }, - ]); - this.canvas.dispatchEvent( - new CustomEvent('hpc:newobject', { - detail: this.drawingLine, - }) - ); - this.canvas.dispatchEvent( - new CustomEvent('hpc:update', { - detail: { event: 'newobject', object: this.drawingLine }, - }) - ); - this.drawingLine = null; - this.manager.draw(); - } } if (e.key === 'Escape') { - if (this.selectedObjects.length === 0 && this.tool && !this.drawingLine) { - this.setTool(null); + this.tool?.escapePress(e); + + if (this.selectedObjects.length === 0 && this.tool?.isToolCancelable()) { + this.setTool(); } this.selectObject(null); - - if (this.drawingLine && this.selectedLayer) { - const indexOf = this.selectedLayer.contents.indexOf(this.drawingLine); - if (indexOf > -1) { - this.selectedLayer.contents.splice(indexOf, 1); - } - this.drawingLine = null; - this.manager.draw(); - - this.canvas.dispatchEvent( - new CustomEvent('hpc:update', { - detail: { event: 'draw-cancel' }, - }) - ); - } } if (e.key === 'Shift') { @@ -492,6 +406,8 @@ export class HousePlannerCanvasTools { ); this.selectedObjects.length = 0; + this.tool?.selectionDeleted(); + this.tool?.selectionChanged([]); this.manager.draw(); this.canvas.dispatchEvent( new CustomEvent('hpc:update', { @@ -500,72 +416,9 @@ export class HousePlannerCanvasTools { ); } - 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.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.selectError / 2 - ) - ) { - clickedOnControl = [line, asType, line[asType] as Vec2]; - } - } - } - return clickedOnControl; - } - - private translate(offset: Vec2) { - for (const object of this.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.manager.draw(); - } - private pointerDown() { - if (this.drawingLine) return; this.clickedOn = this.getMousedObject(); - const bezierControl = this.grabbedBezierControl(); - const lineControl = this.grabbedLineControl(); - if (bezierControl) { - this.handlingBezier = bezierControl; - } else if (lineControl) { - this.handlingLine = lineControl; - } + this.tool?.mouseDown(this.clickedOn || undefined); } private pointerUp() { @@ -577,149 +430,9 @@ export class HousePlannerCanvasTools { this.moved = false; } - if (!this.moved && !this.handlingBezier && !this.handlingLine) { - if (this.tool === 'line') { - this.startLine(); - } else if (!this.drawingLine) { - this.pick(); - } - } + this.tool?.mouseUp(this.moved); - 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.clickedOn = null; - this.movingObject = false; - } - - private getSequentialId() { - return ( - this.manager.layers.reduce( - (total, current) => - current.contents.reduce( - (total2, current2) => current2.id + total2, - 0 - ) + total, - 0 - ) + 1 - ); - } - - private startLine() { - if (!this.selectedLayer?.visible) return; - if (this.drawingLine) { - if ( - vec2Equals( - this.mousePositionSnapped, - this.drawingLine.segments[0].start as Vec2 - ) || - this.drawingLine.type === 'curve' - ) { - if (this.drawingLine.type !== 'curve') { - this.drawingLine.segments.splice( - this.drawingLine.segments.length - 1, - 1 - ); - } - this.history.appendToHistory([ - { - object: this.selectedLayer, - property: 'contents', - value: [...this.selectedLayer.contents].filter( - (item) => item !== this.drawingLine - ), - } as History, - { - object: this, - property: 'selectedObjects', - value: [], - }, - ]); - this.drawingLine.closed = true; - this.canvas.dispatchEvent( - new CustomEvent('hpc:newobject', { - detail: this.drawingLine, - }) - ); - this.canvas.dispatchEvent( - new CustomEvent('hpc:update', { - detail: { event: 'newobject', object: this.drawingLine }, - }) - ); - this.drawingLine = null; - return; - } - - this.drawingLine.segments.push({ - end: [...this.mousePositionSnapped], - }); - return; - } - - const newLineObject: Line = { - id: this.getSequentialId(), - name: 'New Line', - type: this.subTool!, - visible: true, - selected: false, - closed: false, - color: this.lastColor, - width: this.subTool === 'curve' ? 2 : this.lastStrokeWidth, - segments: [ - { - start: [...this.mousePositionSnapped], - end: [...this.mousePositionSnapped], - }, - ], - }; - this.drawingLine = newLineObject; - this.selectedLayer.contents.unshift(newLineObject); - this.selectObject(this.drawingLine); - this.canvas.dispatchEvent( - new CustomEvent('hpc:startdrawing', { - detail: newLineObject, - }) - ); - } - - private pick() { - if (this.clickedOn && this.isSelected(this.clickedOn)) { - // Pick line segment - if ((this.clickedOn as Line).segments) { - const segment = this.getMousedLineSegment(this.clickedOn as Line); - } - } - this.selectObject(this.clickedOn, this.holdShift); } private dragEvent(x: number, y: number) { @@ -737,44 +450,11 @@ export class HousePlannerCanvasTools { let offset = vec2Sub(currentPos, this.mousePositionSnapped); - if (!this.selectedLayer) return; - - if (this.tool) { - if (this.drawingLine) { - const lastSegment = this.drawingLine.segments.at(-1); - if (lastSegment) { - lastSegment.end = [...this.mousePositionSnapped]; - } - } - this.manager.draw(); - } - - if (this.drawingLine) { - return; - } - - if (this.handlingBezier) { - const [segment, property] = this.handlingBezier; - segment[property] = [...this.mousePositionSnapped]; - this.manager.draw(); - } else if (this.handlingLine) { - const [segment, property] = this.handlingLine; - segment[property] = [...this.mousePositionSnapped]; - this.manager.draw(); - } else if (this.clickedOn) { - if (!this.movingObject) { - // TODO: optimize history storage - this.history.appendToHistory([ - { - object: this.selectedLayer, - property: 'contents', - value: JSON.parse(JSON.stringify(this.selectedLayer['contents'])), - }, - ]); - this.movingObject = true; - } - this.translate(vec2Inverse(offset)); - } + this.tool?.mouseMoved( + this.mousePositionSnapped, + offset, + this.mousePosition + ); } private selectedEvent() { diff --git a/src/modules/house-planner/tools/line.ts b/src/modules/house-planner/tools/line.ts new file mode 100644 index 0000000..8da6ff1 --- /dev/null +++ b/src/modules/house-planner/tools/line.ts @@ -0,0 +1,192 @@ +import { History, LayerObject, Line, Vec2 } from '../interfaces'; +import { LayerObjectType } from '../types'; +import { vec2Equals } from '../utils'; +import { CanvasToolBase } from './tool-base'; + +export type LineToolType = 'line' | 'curve' | 'room'; +export class LineTool extends CanvasToolBase { + public name = 'line'; + private drawingLine: Line | null = null; + public subTool: LineToolType = 'line'; + + drawControls(): void { + const [mx, my] = this.mousePosition; + this.ctx.fillStyle = '#00ddffaa'; + this.ctx.beginPath(); + this.ctx.arc(mx, my, this.manager.selectError / 2, 0, 2 * Math.PI); + this.ctx.fill(); + } + + mouseDown(targetObject?: LayerObject | undefined): void {} + + mouseMoved(mouse: Vec2, offset: Vec2, mouseAbsolute: Vec2): void { + super.mouseMoved(mouse, offset, mouseAbsolute); + if (this.drawingLine) { + const lastSegment = this.drawingLine.segments.at(-1); + if (lastSegment) { + lastSegment.end = [...mouse]; + } + } + this.renderer.draw(); + } + + mouseUp(moved?: boolean): void { + if (!moved) this.startLine(); + } + + enterPress(e: KeyboardEvent): void { + if (this.drawingLine && this.layer) { + if (this.subTool === 'room') { + this.drawingLine.closed = true; + } + this.drawingLine.segments.splice(this.drawingLine.segments.length - 1, 1); + this.history.appendToHistory([ + { + object: this.layer, + property: 'contents', + value: [...this.layer.contents].filter( + (item) => item !== this.drawingLine + ), + } as History, + { + object: this, + property: 'selectedObjects', + value: [], + }, + ]); + this.emitEvent( + new CustomEvent('hpc:newobject', { + detail: this.drawingLine, + }) + ); + this.emitEvent( + new CustomEvent('hpc:update', { + detail: { event: 'newobject', object: this.drawingLine }, + }) + ); + this.drawingLine = null; + this.renderer.draw(); + } + } + + escapePress(e: KeyboardEvent): void { + this.cancel(); + } + + selectionDeleted(): void { + this.drawingLine = null; + } + + private cancel() { + if (!this.layer || !this.drawingLine) return; + const indexOf = this.layer.contents.indexOf(this.drawingLine); + if (indexOf > -1) { + this.layer.contents.splice(indexOf, 1); + } + this.drawingLine = null; + this.renderer.draw(); + + this.emitEvent( + new CustomEvent('hpc:update', { + detail: { event: 'draw-cancel' }, + }) + ); + } + + private getSequentialId() { + return ( + this.renderer.layers.reduce( + (total, current) => + current.contents.reduce( + (total2, current2) => current2.id + total2, + 0 + ) + total, + 0 + ) + 1 + ); + } + + private startLine() { + if (!this.layer?.visible) return; + if (this.drawingLine) { + if ( + vec2Equals( + this.mousePosition, + this.drawingLine.segments[0].start as Vec2 + ) || + this.drawingLine.type === 'curve' + ) { + if (this.drawingLine.type !== 'curve') { + this.drawingLine.segments.splice( + this.drawingLine.segments.length - 1, + 1 + ); + } + this.history.appendToHistory([ + { + object: this.layer, + property: 'contents', + value: [...this.layer.contents].filter( + (item) => item !== this.drawingLine + ), + } as History, + { + object: this, + property: 'selectedObjects', + value: [], + }, + ]); + this.drawingLine.closed = true; + this.canvas.dispatchEvent( + new CustomEvent('hpc:newobject', { + detail: this.drawingLine, + }) + ); + this.canvas.dispatchEvent( + new CustomEvent('hpc:update', { + detail: { event: 'newobject', object: this.drawingLine }, + }) + ); + this.drawingLine = null; + return; + } + + this.drawingLine.segments.push({ + end: [...this.mousePosition], + }); + return; + } + + const newLineObject: Line = { + id: this.getSequentialId(), + name: this.defaultName, + type: this.subTool! as LayerObjectType, + visible: true, + selected: false, + closed: false, + color: this.manager.lastColor, + width: this.subTool === 'curve' ? 2 : this.manager.lastStrokeWidth, + segments: [ + { + start: [...this.mousePosition], + end: [...this.mousePosition], + }, + ], + }; + this.drawingLine = newLineObject; + this.layer.contents.unshift(newLineObject); + this.manager.selectObject(this.drawingLine); + this.canvas.dispatchEvent( + new CustomEvent('hpc:startdrawing', { + detail: newLineObject, + }) + ); + } + + private get defaultName() { + let name = 'New '; + if (this.subTool === 'curve') return name + 'Curve'; + if (this.subTool === 'room') return name + 'Room'; + return name + 'Line'; + } +} diff --git a/src/modules/house-planner/tools/move.ts b/src/modules/house-planner/tools/move.ts new file mode 100644 index 0000000..9b6357f --- /dev/null +++ b/src/modules/house-planner/tools/move.ts @@ -0,0 +1,249 @@ +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 { + 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) { + 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(); + } +} diff --git a/src/modules/house-planner/tools/tool-base.ts b/src/modules/house-planner/tools/tool-base.ts new file mode 100644 index 0000000..cb55206 --- /dev/null +++ b/src/modules/house-planner/tools/tool-base.ts @@ -0,0 +1,56 @@ +import { ICanvasToolBase, LayerObject, Vec2 } from '../interfaces'; +import type { HousePlannerCanvasTools } from '../tools'; + +export class CanvasToolBase implements ICanvasToolBase { + protected mousePosition: Vec2 = [0, 0]; + protected mousePositionAbsolute: Vec2 = [0, 0]; + public name = 'tool'; + public subTool: T | undefined; + + constructor(public manager: HousePlannerCanvasTools) {} + get ctx() { + return this.manager.ctx; + } + + get canvas() { + return this.manager.canvas; + } + + get history() { + return this.manager.history; + } + + get renderer() { + return this.manager.manager; + } + + get layer() { + return this.manager.selectedLayer; + } + + drawHighlights() {} + drawControls() {} + + mouseDown(targetObject?: LayerObject) {} + mouseMoved(mouse: Vec2, offset: Vec2, mouseAbsolute: Vec2) { + this.mousePosition = mouse; + this.mousePositionAbsolute = mouseAbsolute; + } + mouseUp(moved = false) {} + + setSubTool(subTool: T) { + this.subTool = subTool; + } + activate() {} + deactivate() {} + enterPress(e: KeyboardEvent): void {} + escapePress(e: KeyboardEvent): void {} + emitEvent(e: CustomEvent): void { + this.canvas.dispatchEvent(e); + } + isToolCancelable(): boolean { + return true; + } + selectionDeleted(): void {} + selectionChanged(selection: LayerObject[]): void {} +} diff --git a/src/modules/house-planner/types.ts b/src/modules/house-planner/types.ts index 793f234..27e2bf0 100644 --- a/src/modules/house-planner/types.ts +++ b/src/modules/house-planner/types.ts @@ -1,8 +1,5 @@ import { BezierSegment, LineSegment, Vec2 } from './interfaces'; -export type ToolType = 'line' | null; -export type SubToolType = 'line' | 'curve' | 'room'; - export type BezierControl = [ BezierSegment, 'startControl' | 'endControl', diff --git a/src/style.scss b/src/style.scss index a90f074..4c18a7b 100644 --- a/src/style.scss +++ b/src/style.scss @@ -2,3 +2,6 @@ @tailwind components; @tailwind utilities; +.min-h-half { + min-height: 50%; +}