diff --git a/src/components/Dropdown.vue b/src/components/Dropdown.vue index 8a87b0d..3016a37 100644 --- a/src/components/Dropdown.vue +++ b/src/components/Dropdown.vue @@ -38,7 +38,10 @@ const toggle = (to?: boolean) => { }; const event = (e: MouseEvent) => { - if (wrapper.value.contains(e.target as HTMLElement)) { + if ( + wrapper.value.contains(e.target as HTMLElement) && + !(e.target as HTMLElement).closest('a') + ) { return; } open.value = false; diff --git a/src/components/DropdownButton.vue b/src/components/DropdownButton.vue new file mode 100644 index 0000000..7306dd0 --- /dev/null +++ b/src/components/DropdownButton.vue @@ -0,0 +1,56 @@ + + + + + toggle()" + :aria-expanded="open" + :class="[ + open ? 'text-gray-900' : 'text-gray-500', + 'group inline-flex items-center rounded-md bg-white text-base font-medium hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2', + ]" + > + {{ title }} + + + + + + + + + + + + + + + + diff --git a/src/components/PageHead.vue b/src/components/PageHead.vue new file mode 100644 index 0000000..0382195 --- /dev/null +++ b/src/components/PageHead.vue @@ -0,0 +1,19 @@ + + + + + + + + + + + + diff --git a/src/components/header/Building.vue b/src/components/header/Building.vue index 0212132..f7fa764 100644 --- a/src/components/header/Building.vue +++ b/src/components/header/Building.vue @@ -1,6 +1,6 @@ - - - + + + > + + + + + (), - { editable: true } + { editable: true, grid: true, headless: false, transparent: false } ); const emit = defineEmits<{ @@ -246,6 +271,16 @@ defineExpose({ updateLocalDocument(); return localFloorDocument.value; }, + setViewRectangle(view?: Vec2Box) { + if (!module.value) return; + if (!view && !localFloorDocument.value?.boundingBox) return; + module.value.manager?.setViewRectangle( + view || localFloorDocument.value!.boundingBox! + ); + }, + canvasDim, + canvasPos, + canvasZoom, }); onMounted(() => { @@ -256,6 +291,8 @@ onMounted(() => { canvasPos.value, canvasZoom.value, props.editable, + props.grid, + props.headless, localFloorDocument.value.boundingBox ); canvasPos.value = setPos; diff --git a/src/components/house-planner/PlannerBuildingSelect.vue b/src/components/house-planner/PlannerBuildingSelect.vue index a71af9c..1798488 100644 --- a/src/components/house-planner/PlannerBuildingSelect.vue +++ b/src/components/house-planner/PlannerBuildingSelect.vue @@ -1,5 +1,7 @@ - + - Go back + + Go back + Building: this.onTouchMove(e)); this.canvas.addEventListener('touchstart', (e) => this.onTouchStart(e)); this.canvas.addEventListener('touchend', (e) => this.onTouchEnd(e)); + this.canvas.addEventListener('touchcancel', (e) => this.onTouchEnd(e)); this.canvas.addEventListener('wheel', (e) => this.onMouseWheel(e)); this.canvas.addEventListener('pointerleave', () => this.onPointerLeave()); @@ -349,14 +363,11 @@ export class HousePlannerCanvas { this.ctx.font = '16px Arial'; this.ctx.fillStyle = line.color; const { width } = this.ctx.measureText(line.name); - this.ctx.fillText( - line.name, - centerPoint[0] - width / 2, - centerPoint[1] - 8 - ); + this.ctx.fillText(line.name, centerPoint[0] - width / 2, centerPoint[1]); } private drawLine(line: Line) { + if (this.blacklist?.includes(line.type)) return; const path = this.makeLinePath(line); line.render = path; this.setupLine(line); @@ -408,7 +419,7 @@ export class HousePlannerCanvas { (ev.touches[0].clientY - this.canvasPos[1]) / this.canvasZoom; delta > 0 ? (this.canvasZoom *= delta) : (this.canvasZoom /= delta); - this.canvasZoom = clamp(this.canvasZoom, 1, 100); + this.canvasZoom = clamp(this.canvasZoom, 0.25, 10); this.canvasPos = [ ev.touches[0].clientX - scaleX * this.canvasZoom, @@ -425,8 +436,8 @@ export class HousePlannerCanvas { onTouchStart(e: TouchEvent) { e.preventDefault(); const touch = e.touches[0] || e.changedTouches[0]; - this.mouseClickPosition = [touch.clientX, touch.clientY]; - this.realMousePos(touch.clientX, touch.clientY); + // this.mouseClickPosition = [touch.clientX, touch.clientY]; + this.realMousePos(touch.clientX, touch.clientY, true); this.moved = false; @@ -451,7 +462,7 @@ export class HousePlannerCanvas { this.dragging = false; } - initViewport(boundingBox: Vec2Box) { + setViewRectangle(boundingBox: Vec2Box) { if (!isValidVec2(boundingBox[0]) || !isValidVec2(boundingBox[1])) return; const [zoom, pos] = this.calculateViewport(boundingBox); this.canvasZoom = zoom; @@ -468,15 +479,15 @@ export class HousePlannerCanvas { private calculateViewport(box: Vec2Box): [number, Vec2] { let [min, max] = box; - const gap = 80; + const gap = this.headless ? 10 : 80; min = vec2Sub(min, [gap, gap]); max = vec2Add(max, [gap, gap]); const { width: windowWidth, height: windowHeight } = - this.canvas.parentElement!.getBoundingClientRect(); + this.canvas.parentElement!.parentElement!.getBoundingClientRect(); const diagonal = vec2Distance(min, max); const target = vec2Sub(max, min); - const zoomScale = windowHeight / diagonal; + const zoomScale = Math.min(windowWidth, windowHeight) / diagonal; let scaledPos = vec2MultiplyScalar(min, zoomScale); const scaled = vec2MultiplyScalar(target, zoomScale); @@ -553,7 +564,7 @@ export class HousePlannerCanvas { // 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 + vec2Distance(this.mouseClickPosition, this.mousePosition) < 1 ) { this.moved = false; } diff --git a/src/modules/house-planner/index.ts b/src/modules/house-planner/index.ts index 6ef7d32..c211ea4 100644 --- a/src/modules/house-planner/index.ts +++ b/src/modules/house-planner/index.ts @@ -13,6 +13,8 @@ export class HousePlanner { canvasPos: Vec2, canvasZoom = 1, editable = true, + grid = true, + headless = false, boundingBox?: Vec2Box ): [number, Vec2] { this.canvas = canvas; @@ -21,7 +23,9 @@ export class HousePlanner { canvasDim, canvasPos, canvasZoom, - editable + editable, + grid, + headless ); this.manager.layers = initialData; @@ -37,7 +41,7 @@ export class HousePlanner { this.manager.draw(); if (boundingBox) { - this.manager.initViewport(boundingBox); + this.manager.setViewRectangle(boundingBox); } return [this.manager.canvasZoom, this.manager.canvasPos]; } diff --git a/src/modules/house-planner/tools/tool-base.ts b/src/modules/house-planner/tools/tool-base.ts index cb55206..cdadf61 100644 --- a/src/modules/house-planner/tools/tool-base.ts +++ b/src/modules/house-planner/tools/tool-base.ts @@ -2,8 +2,6 @@ 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; @@ -28,14 +26,19 @@ export class CanvasToolBase implements ICanvasToolBase { return this.manager.selectedLayer; } + get mousePosition() { + return this.renderer.mousePositionSnapped; + } + + get mousePositionAbsolute() { + return this.renderer.mousePosition; + } + drawHighlights() {} drawControls() {} mouseDown(targetObject?: LayerObject) {} - mouseMoved(mouse: Vec2, offset: Vec2, mouseAbsolute: Vec2) { - this.mousePosition = mouse; - this.mousePositionAbsolute = mouseAbsolute; - } + mouseMoved(mouse: Vec2, offset: Vec2, mouseAbsolute: Vec2) {} mouseUp(moved = false) {} setSubTool(subTool: T) { diff --git a/src/router/index.ts b/src/router/index.ts index 24fbc65..059531d 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -1,9 +1,12 @@ import { NavigationGuardWithThis, RouteRecordRaw } from 'vue-router'; import Dashboard from '../views/Dashboard.vue'; import Login from '../views/Login.vue'; +import HousePlanner from '../views/HousePlanner.vue'; +import BuildingViewBase from '../views/building/BuildingViewBase.vue'; +import BuildingView from '../views/building/BuildingView.vue'; +import FloorView from '../views/building/floors/FloorView.vue'; import { createRouter, createWebHashHistory } from 'vue-router'; import { useUserStore } from '../store/user.store'; -import HousePlanner from '../views/HousePlanner.vue'; const routes: RouteRecordRaw[] = [ { @@ -21,6 +24,46 @@ const routes: RouteRecordRaw[] = [ path: '/planner', component: HousePlanner, }, + { + name: 'buildings', + path: '/building/:id', + component: BuildingViewBase, + children: [ + { + name: 'building', + path: '', + meta: { + breadcrumbs: [ + { + name: 'buildings', + title: 'Building', + props: ['id'], + }, + ], + }, + component: BuildingView, + }, + { + name: 'floor', + path: 'floor/:number', + component: FloorView, + meta: { + breadcrumbs: [ + { + name: 'buildings', + title: 'Building', + props: ['id'], + }, + { + name: 'floor', + title: 'Floor', + props: ['id', 'number'], + }, + ], + }, + }, + ], + }, ]; const router = createRouter({ diff --git a/src/store/building.store.ts b/src/store/building.store.ts index 8066f74..5b91c00 100644 --- a/src/store/building.store.ts +++ b/src/store/building.store.ts @@ -2,7 +2,10 @@ import omit from 'lodash.omit'; import { defineStore } from 'pinia'; import { useAccessToken } from '../composables/useAccessToken'; import { BACKEND_URL } from '../constants'; -import { BuildingListItem } from '../interfaces/building.interfaces'; +import { + BuildingListItem, + BuildingResponse, +} from '../interfaces/building.interfaces'; import { FloorListItem } from '../interfaces/floor.interfaces'; import { RoomListItem, UpsertRoomItem } from '../interfaces/room.interfaces'; import jfetch from '../utils/jfetch'; @@ -13,9 +16,19 @@ export const useBuildingStore = defineStore('building', { return { buildings: [] as BuildingListItem[], floors: [] as FloorListItem[], + building: null as null | BuildingResponse, }; }, actions: { + async getBuilding(id: number) { + const { data: building } = await jfetch( + `${BACKEND_URL}/buildings/${id}`, + { + headers: authHeader.value, + } + ); + this.building = building; + }, async getBuildings() { const { data: buildings } = await jfetch(`${BACKEND_URL}/buildings`, { headers: authHeader.value, diff --git a/src/views/HousePlanner.vue b/src/views/HousePlanner.vue index 4ccb062..2df4d67 100644 --- a/src/views/HousePlanner.vue +++ b/src/views/HousePlanner.vue @@ -34,7 +34,8 @@ diff --git a/src/views/building/BuildingView.vue b/src/views/building/BuildingView.vue new file mode 100644 index 0000000..1f163dd --- /dev/null +++ b/src/views/building/BuildingView.vue @@ -0,0 +1,44 @@ + + + + + Edit floor plans + + + + + {{ building?.displayName }} + {{ + building?.address + }} + + + + + + Floors + + + + + + + + + diff --git a/src/views/building/BuildingViewBase.vue b/src/views/building/BuildingViewBase.vue new file mode 100644 index 0000000..e9457b9 --- /dev/null +++ b/src/views/building/BuildingViewBase.vue @@ -0,0 +1,26 @@ + + + + + + diff --git a/src/views/building/floors/FloorListItem.vue b/src/views/building/floors/FloorListItem.vue new file mode 100644 index 0000000..3393ccd --- /dev/null +++ b/src/views/building/floors/FloorListItem.vue @@ -0,0 +1,43 @@ + + + + + + {{ floor.displayName }} + Floor {{ floor.number }} + + + + + + + + diff --git a/src/views/building/floors/FloorView.vue b/src/views/building/floors/FloorView.vue new file mode 100644 index 0000000..2dd37fb --- /dev/null +++ b/src/views/building/floors/FloorView.vue @@ -0,0 +1,192 @@ + + + + Edit floor plan + + + + + {{ floor?.displayName }} + Floor {{ floor?.number }} in {{ building?.displayName }} + + + + + + + + + {{ + room.displayName + }} + + + + + {{ storage.displayName }} + + + + + + + + + +