From caa34afd3ebfaa7e5ee54531cea60fb4ddfe0d49 Mon Sep 17 00:00:00 2001 From: Evert Prants Date: Tue, 17 Jan 2023 20:32:15 +0200 Subject: [PATCH] editor progress --- src/components/house-planner/HousePlanner.vue | 83 ++++++++-- .../house-planner/PlannerLayerPanel.vue | 98 +++++++++++ .../house-planner/PlannerPropertyPanel.vue | 153 ++++++++++++++++++ .../house-planner/PlannerSidebar.vue | 51 ++++++ .../house-planner/PlannerSidebars.vue | 7 + .../interfaces/properties.interfaces.ts | 19 +++ src/modules/house-planner/canvas.ts | 70 +++++++- src/modules/house-planner/interfaces.ts | 7 +- src/modules/house-planner/tools.ts | 48 ++++++ src/modules/house-planner/types.ts | 1 + src/modules/house-planner/utils.ts | 3 + src/utils/deep-unref.ts | 6 + 12 files changed, 530 insertions(+), 16 deletions(-) create mode 100644 src/components/house-planner/PlannerLayerPanel.vue create mode 100644 src/components/house-planner/PlannerPropertyPanel.vue create mode 100644 src/components/house-planner/PlannerSidebar.vue create mode 100644 src/components/house-planner/PlannerSidebars.vue create mode 100644 src/components/house-planner/interfaces/properties.interfaces.ts create mode 100644 src/utils/deep-unref.ts diff --git a/src/components/house-planner/HousePlanner.vue b/src/components/house-planner/HousePlanner.vue index 4a65d7f..4822c08 100644 --- a/src/components/house-planner/HousePlanner.vue +++ b/src/components/house-planner/HousePlanner.vue @@ -22,6 +22,20 @@ + + + + + @@ -32,13 +46,24 @@ import { HomeIcon, ArrowsPointingOutIcon, ArrowDownOnSquareIcon, + Square3Stack3DIcon, } from '@heroicons/vue/24/outline'; import { useSessionStorage } from '@vueuse/core'; +import type { Component } from 'vue'; import { onMounted, ref, shallowRef } from 'vue'; import { HousePlanner } from '../../modules/house-planner'; import { Layer, ToolEvent } from '../../modules/house-planner/interfaces'; -import { SubToolType, ToolType } from '../../modules/house-planner/types'; +import { + LayerObjectType, + SubToolType, + ToolType, +} from '../../modules/house-planner/types'; +import deepUnref from '../../utils/deep-unref'; import { ToolbarTool } from './interfaces/toolbar.interfaces'; +import PlannerLayerPanel from './PlannerLayerPanel.vue'; +import PlannerPropertyPanel from './PlannerPropertyPanel.vue'; +import PlannerSidebar from './PlannerSidebar.vue'; +import PlannerSidebars from './PlannerSidebars.vue'; import PlannerTool from './PlannerTool.vue'; import PlannerToolbar from './PlannerToolbar.vue'; @@ -50,21 +75,21 @@ const serializedLayers = useSessionStorage( 'roomData', [ { - index: 0, - name: 'Base', - color: '#00ddff', - contents: [], - visible: true, - active: true, - }, - { - index: 1, + id: 1, name: 'Rooms', color: '#00ddff', contents: [], visible: true, active: false, }, + { + id: 0, + name: 'Base', + color: '#00ddff', + contents: [], + visible: true, + active: true, + }, ], { writeDefaults: false } ); @@ -110,15 +135,49 @@ const selectTool = (newTool: ToolType, newSubTool?: SubToolType) => { module.value.manager?.tools.setTool(newTool, newSubTool); }; +const commitObjectName = (layerId: number, objectId: number, name: string) => { + module.value.manager?.updateObjectProperties(layerId, objectId, { name }); +}; + +const commitLayerName = (layerId: number, name: string) => { + module.value.manager?.updateLayerProperties(layerId, { name }); +}; + +const clickedOnObject = (layerId: number, objectId: number, add?: boolean) => { + const object = module.value.manager?.getLayerObjectById(layerId, objectId); + if (!object) return; + module.value.manager?.tools.selectObject(object, add); +}; + +const clickedOnLayer = (layerId: number) => { + const layer = module.value.manager?.getLayerById(layerId); + if (!layer) return; + module.value.manager?.tools.selectLayer(layer); +}; + +const updateObjectProperty = ( + layerId: number, + objectId: number, + key: string, + value: unknown +) => { + module.value.manager?.updateObjectProperties(layerId, objectId, { + [key]: value, + }); +}; + onMounted(() => { const cleanUp = module.value.initialize( canvas.value, - JSON.parse(JSON.stringify(serializedLayers.value)) + deepUnref(serializedLayers.value) ); const events: Record void> = { 'hpc:update': (e: CustomEvent) => { - serializedLayers.value = module.value.manager!.layers; + serializedLayers.value = deepUnref(module.value.manager!.layers); + }, + 'hpc:selectionchange': (e: CustomEvent) => { + serializedLayers.value = deepUnref(module.value.manager!.layers); }, 'hpc:tool': (e: CustomEvent) => { tool.value = e.detail.primary; diff --git a/src/components/house-planner/PlannerLayerPanel.vue b/src/components/house-planner/PlannerLayerPanel.vue new file mode 100644 index 0000000..eeaca74 --- /dev/null +++ b/src/components/house-planner/PlannerLayerPanel.vue @@ -0,0 +1,98 @@ + + + diff --git a/src/components/house-planner/PlannerPropertyPanel.vue b/src/components/house-planner/PlannerPropertyPanel.vue new file mode 100644 index 0000000..0f32520 --- /dev/null +++ b/src/components/house-planner/PlannerPropertyPanel.vue @@ -0,0 +1,153 @@ + + + diff --git a/src/components/house-planner/PlannerSidebar.vue b/src/components/house-planner/PlannerSidebar.vue new file mode 100644 index 0000000..a49e76e --- /dev/null +++ b/src/components/house-planner/PlannerSidebar.vue @@ -0,0 +1,51 @@ + + + diff --git a/src/components/house-planner/PlannerSidebars.vue b/src/components/house-planner/PlannerSidebars.vue new file mode 100644 index 0000000..0db8253 --- /dev/null +++ b/src/components/house-planner/PlannerSidebars.vue @@ -0,0 +1,7 @@ + diff --git a/src/components/house-planner/interfaces/properties.interfaces.ts b/src/components/house-planner/interfaces/properties.interfaces.ts new file mode 100644 index 0000000..696c142 --- /dev/null +++ b/src/components/house-planner/interfaces/properties.interfaces.ts @@ -0,0 +1,19 @@ +import { LayerObjectType } from '../../../modules/house-planner/types'; + +export interface SelectOptions { + value: string | null | undefined; + title: string; +} + +export interface ObjectProperty { + key: string; + title: string; + type: 'string' | 'boolean' | 'color' | 'number' | 'select'; + groupable?: boolean; + options?: SelectOptions[]; +} + +export interface ObjectProperties { + type: LayerObjectType; + properties: ObjectProperty[]; +} diff --git a/src/modules/house-planner/canvas.ts b/src/modules/house-planner/canvas.ts index 77458ee..91afe6f 100644 --- a/src/modules/house-planner/canvas.ts +++ b/src/modules/house-planner/canvas.ts @@ -1,5 +1,12 @@ import { HousePlannerCanvasGrid } from './grid'; -import { BezierSegment, Layer, Line, LineSegment, Vec2 } from './interfaces'; +import { + BezierSegment, + Layer, + LayerObject, + Line, + LineSegment, + Vec2, +} from './interfaces'; import { HousePlannerCanvasTools } from './tools'; import { rad2deg, @@ -139,6 +146,67 @@ export class HousePlannerCanvas { return this.ctx.isPointInStroke(fakePath, x, y); } + 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> + ) { + 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> + ) { + 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; + } + private keyDownEvent(e: KeyboardEvent) { if (e.target !== document.body && e.target != null) return; e.preventDefault(); diff --git a/src/modules/house-planner/interfaces.ts b/src/modules/house-planner/interfaces.ts index 90e467c..e35cff9 100644 --- a/src/modules/house-planner/interfaces.ts +++ b/src/modules/house-planner/interfaces.ts @@ -1,4 +1,4 @@ -import { SubToolType, ToolType } from './types'; +import { LayerObjectType, SubToolType, ToolType } from './types'; export type Vec2 = [number, number]; export interface LineSegment { @@ -12,10 +12,11 @@ export interface BezierSegment extends LineSegment { } export interface LayerObject { + id: number; name: string; visible: boolean; selected: boolean; - type: 'line' | 'room' | 'curve' | 'object'; + type: LayerObjectType; } export interface Line extends LayerObject { @@ -29,7 +30,7 @@ export interface Line extends LayerObject { } export interface Layer { - index: number; + id: number; contents: LayerObject[]; name: string; color: string; diff --git a/src/modules/house-planner/tools.ts b/src/modules/house-planner/tools.ts index b29aa4c..311e040 100644 --- a/src/modules/house-planner/tools.ts +++ b/src/modules/house-planner/tools.ts @@ -13,6 +13,7 @@ import { import { ToolType, SubToolType, BezierControl, LineControl } from './types'; import { vec2Add, + vec2Distance, vec2Equals, vec2InCircle, vec2Inverse, @@ -24,6 +25,7 @@ export class HousePlannerCanvasTools { public selectedLayer?: Layer; public selectedObjects: LayerObject[] = []; public mousePosition: Vec2 = [0, 0]; + public mouseClickPosition: Vec2 = [0, 0]; public mousePositionSnapped: Vec2 = [0, 0]; public gridSnap = true; public gridSnapScale = 8; @@ -70,6 +72,18 @@ export class HousePlannerCanvasTools { } 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', + }, + }) + ); } selectObject(object?: LayerObject | null, add = false) { @@ -81,6 +95,7 @@ export class HousePlannerCanvasTools { this.selectedObjects.length = 0; } this.manager.draw(); + this.selectedEvent(); return; } @@ -91,6 +106,7 @@ export class HousePlannerCanvasTools { object.selected = false; this.selectedObjects.splice(foundAt, 1); this.manager.draw(); + this.selectedEvent(); } return; } @@ -100,6 +116,7 @@ export class HousePlannerCanvasTools { object.selected = true; this.selectedObjects.push(object); this.manager.draw(); + this.selectedEvent(); return; } @@ -111,6 +128,7 @@ export class HousePlannerCanvasTools { this.selectedObjects = [object]; this.manager.draw(); + this.selectedEvent(); } getMousedObject() { @@ -258,6 +276,7 @@ export class HousePlannerCanvasTools { } onMouseDown(e: MouseEvent) { this.mousePosition = [e.clientX, e.clientY]; + this.mouseClickPosition = [...this.mousePosition]; this.mousePositionSnapped = this.gridSnap ? vec2Snap(this.mousePosition, this.gridSnapScale) : this.mousePosition; @@ -302,6 +321,7 @@ export class HousePlannerCanvasTools { e.preventDefault(); const touch = e.touches[0] || e.changedTouches[0]; this.mousePosition = [touch.pageX, touch.pageY]; + this.mouseClickPosition = [...this.mousePosition]; this.mousePositionSnapped = this.gridSnap ? vec2Snap(this.mousePosition, this.gridSnapScale) : this.mousePosition; @@ -435,6 +455,12 @@ export class HousePlannerCanvasTools { } this.drawingLine = null; this.manager.draw(); + + this.canvas.dispatchEvent( + new CustomEvent('hpc:update', { + detail: { event: 'draw-cancel' }, + }) + ); } } @@ -543,6 +569,14 @@ export class HousePlannerCanvasTools { } private pointerUp() { + // FIXME: possibly there's a better approach, but right now some clicks do not register + if ( + this.moved && + vec2Distance(this.mouseClickPosition, this.mousePosition) < 0.25 + ) { + this.moved = false; + } + if (!this.moved && !this.handlingBezier && !this.handlingLine) { if (this.tool === 'line') { this.startLine(); @@ -588,6 +622,19 @@ export class HousePlannerCanvasTools { 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) { @@ -640,6 +687,7 @@ export class HousePlannerCanvasTools { } const newLineObject: Line = { + id: this.getSequentialId(), name: 'New Line', type: this.subTool!, visible: true, diff --git a/src/modules/house-planner/types.ts b/src/modules/house-planner/types.ts index 0aa81f1..793f234 100644 --- a/src/modules/house-planner/types.ts +++ b/src/modules/house-planner/types.ts @@ -9,3 +9,4 @@ export type BezierControl = [ Vec2 ]; export type LineControl = [LineSegment, 'start' | 'end', Vec2]; +export type LayerObjectType = 'line' | 'room' | 'curve' | 'object'; diff --git a/src/modules/house-planner/utils.ts b/src/modules/house-planner/utils.ts index 4a904c1..d709cfa 100644 --- a/src/modules/house-planner/utils.ts +++ b/src/modules/house-planner/utils.ts @@ -54,3 +54,6 @@ export const vec2PointFromAngle = ( export const deg2rad = (deg: number) => deg * (Math.PI / 180); export const rad2deg = (rad: number) => rad * (180 / Math.PI); + +export const randomNumber = (min: number, max: number) => + Math.floor(Math.random() * (max - min + 1) + min); diff --git a/src/utils/deep-unref.ts b/src/utils/deep-unref.ts new file mode 100644 index 0000000..2a729ab --- /dev/null +++ b/src/utils/deep-unref.ts @@ -0,0 +1,6 @@ +import { MaybeRef } from '@vueuse/core'; +import { isRef } from 'vue'; + +export default function deepUnref(input: MaybeRef) { + return JSON.parse(JSON.stringify(isRef(input) ? input.value : input)) as T; +}