stuff
This commit is contained in:
parent
eb48192542
commit
47f4df4971
@ -38,7 +38,10 @@ const toggle = (to?: boolean) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const event = (e: MouseEvent) => {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
open.value = false;
|
open.value = false;
|
||||||
|
56
src/components/DropdownButton.vue
Normal file
56
src/components/DropdownButton.vue
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<template>
|
||||||
|
<div class="relative">
|
||||||
|
<Dropdown :title="title">
|
||||||
|
<template #trigger="{ title, open, toggle }">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="() => 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',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<span>{{ title }}</span>
|
||||||
|
<ChevronDownIcon class="ml-2 h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #default="{ title, open, toggle }">
|
||||||
|
<div
|
||||||
|
:class="[
|
||||||
|
positionClass,
|
||||||
|
'absolute z-10 -ml-4 mt-1 w-screen max-w-xs transform px-2 sm:px-0 lg:ml-0',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="overflow-hidden rounded-lg bg-white shadow-lg ring-1 ring-black ring-opacity-5"
|
||||||
|
>
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ChevronDownIcon } from '@heroicons/vue/24/outline';
|
||||||
|
import { computed } from '@vue/reactivity';
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
import { onBeforeRouteLeave } from 'vue-router';
|
||||||
|
import Dropdown from './Dropdown.vue';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
title: string;
|
||||||
|
position?: 'left' | 'center' | 'right';
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const positionClass = computed(() => {
|
||||||
|
if (!props.position || props.position === 'center')
|
||||||
|
return 'left-1/2 -translate-x-1/2';
|
||||||
|
if (props.position === 'left') return 'left-0';
|
||||||
|
if (props.position === 'right') return 'right-0';
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
</script>
|
19
src/components/PageHead.vue
Normal file
19
src/components/PageHead.vue
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="mb-8 flex items-center justify-between border-b-2 border-gray-100 pb-2"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
<DropdownButton title="Actions" :position="'right'" v-if="withActions">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<slot name="actions" />
|
||||||
|
</div>
|
||||||
|
</DropdownButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import DropdownButton from './DropdownButton.vue';
|
||||||
|
const props = defineProps<{
|
||||||
|
withActions?: boolean;
|
||||||
|
}>();
|
||||||
|
</script>
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<RouterLink
|
<RouterLink
|
||||||
:to="{ name: 'dashboard' }"
|
:to="{ name: 'building', params: { id: building.id } }"
|
||||||
class="flex flex-row items-center space-x-2 px-5 py-3 hover:bg-gray-100"
|
class="flex flex-row items-center space-x-2 px-5 py-3 hover:bg-gray-100"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
@ -1,16 +1,38 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative h-full w-full bg-white">
|
<div class="relative h-full w-full" :class="{ 'bg-white': !transparent }">
|
||||||
<div class="relative h-full w-full overflow-hidden bg-gray-100">
|
<div
|
||||||
<canvas
|
class="relative h-full w-full overflow-hidden"
|
||||||
ref="canvas"
|
:class="{ 'bg-gray-100': !transparent }"
|
||||||
class="border-none bg-white"
|
>
|
||||||
|
<div
|
||||||
|
class="relative"
|
||||||
:style="{
|
:style="{
|
||||||
width: `${canvasDim[0] * canvasZoom}px`,
|
|
||||||
height: `${canvasDim[1] * canvasZoom}px`,
|
|
||||||
transformOrigin: 'top left',
|
transformOrigin: 'top left',
|
||||||
transform: `translate(${canvasPos[0]}px, ${canvasPos[1]}px)`,
|
transform: `translate(${canvasPos[0]}px, ${canvasPos[1]}px)`,
|
||||||
}"
|
}"
|
||||||
|
>
|
||||||
|
<canvas
|
||||||
|
ref="canvas"
|
||||||
|
class="h-full w-full border-none"
|
||||||
|
:class="{ 'bg-white': !transparent }"
|
||||||
|
:style="{
|
||||||
|
width: `${canvasDim[0] * canvasZoom}px`,
|
||||||
|
height: `${canvasDim[1] * canvasZoom}px`,
|
||||||
|
}"
|
||||||
/>
|
/>
|
||||||
|
<div
|
||||||
|
class="pointer-events-none absolute top-0 left-0 h-full w-full"
|
||||||
|
v-if="!editable"
|
||||||
|
:style="{
|
||||||
|
transformOrigin: 'top left',
|
||||||
|
transform: `scale(${canvasZoom})`,
|
||||||
|
width: `${canvasDim[0]}px`,
|
||||||
|
height: `${canvasDim[1]}px`,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<PlannerToolbar v-if="editable">
|
<PlannerToolbar v-if="editable">
|
||||||
<PlannerTool
|
<PlannerTool
|
||||||
@ -94,8 +116,11 @@ const props = withDefaults(
|
|||||||
defineProps<{
|
defineProps<{
|
||||||
floorDocument: FloorDocument;
|
floorDocument: FloorDocument;
|
||||||
editable: boolean;
|
editable: boolean;
|
||||||
|
grid?: boolean;
|
||||||
|
transparent?: boolean;
|
||||||
|
headless?: boolean;
|
||||||
}>(),
|
}>(),
|
||||||
{ editable: true }
|
{ editable: true, grid: true, headless: false, transparent: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@ -246,6 +271,16 @@ defineExpose({
|
|||||||
updateLocalDocument();
|
updateLocalDocument();
|
||||||
return localFloorDocument.value;
|
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(() => {
|
onMounted(() => {
|
||||||
@ -256,6 +291,8 @@ onMounted(() => {
|
|||||||
canvasPos.value,
|
canvasPos.value,
|
||||||
canvasZoom.value,
|
canvasZoom.value,
|
||||||
props.editable,
|
props.editable,
|
||||||
|
props.grid,
|
||||||
|
props.headless,
|
||||||
localFloorDocument.value.boundingBox
|
localFloorDocument.value.boundingBox
|
||||||
);
|
);
|
||||||
canvasPos.value = setPos;
|
canvasPos.value = setPos;
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="absolute top-0 left-0 z-10">
|
<div
|
||||||
|
class="pointer-events-none absolute top-0 left-0 z-10 w-screen sm:w-auto"
|
||||||
|
>
|
||||||
<Transition
|
<Transition
|
||||||
enter-active-class="transition-max-height ease-out duration-200 overflow-hidden"
|
enter-active-class="transition-max-height ease-out duration-200 overflow-hidden"
|
||||||
enter-from-class="max-h-0"
|
enter-from-class="max-h-0"
|
||||||
@ -10,14 +12,14 @@
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="open"
|
v-if="open"
|
||||||
class="rounded-br-md border-2 border-t-0 border-gray-200 bg-white shadow-lg"
|
class="pointer-events-auto w-full overflow-auto border-2 border-t-0 border-gray-200 bg-white shadow-lg sm:overflow-visible sm:rounded-br-md"
|
||||||
>
|
>
|
||||||
<div class="h-14 px-2 py-2.5">
|
<div class="h-14 px-2 py-2.5">
|
||||||
<div class="flex flex-row items-center space-x-4 px-4">
|
<div class="flex flex-row items-center space-x-4 px-4">
|
||||||
<RouterLink to="/" title="Go back"
|
<button @click="router.go(-1)" title="Go back">
|
||||||
><span class="sr-only">Go back</span
|
<span class="sr-only">Go back</span
|
||||||
><ChevronLeftIcon class="-ml-4 h-6 w-6"
|
><ChevronLeftIcon class="-ml-4 h-6 w-6" />
|
||||||
/></RouterLink>
|
</button>
|
||||||
<div class="flex flex-row items-center space-x-4">
|
<div class="flex flex-row items-center space-x-4">
|
||||||
<label for="building">Building:</label>
|
<label for="building">Building:</label>
|
||||||
<select
|
<select
|
||||||
@ -65,7 +67,7 @@
|
|||||||
<button
|
<button
|
||||||
:class="[
|
:class="[
|
||||||
'bg-white',
|
'bg-white',
|
||||||
'-mt-0.5 ml-2 h-8 rounded-br-md rounded-bl-md px-2 py-2 ring-1 ring-black ring-opacity-5',
|
'pointer-events-auto -mt-0.5 ml-2 h-8 rounded-br-md rounded-bl-md px-2 py-2 ring-1 ring-black ring-opacity-5',
|
||||||
]"
|
]"
|
||||||
:title="`${open ? 'Hide' : 'Open'} panel`"
|
:title="`${open ? 'Hide' : 'Open'} panel`"
|
||||||
:aria-expanded="open"
|
:aria-expanded="open"
|
||||||
@ -86,8 +88,10 @@ import {
|
|||||||
} from '@heroicons/vue/24/outline';
|
} from '@heroicons/vue/24/outline';
|
||||||
import { BuildingListItem } from '../../interfaces/building.interfaces';
|
import { BuildingListItem } from '../../interfaces/building.interfaces';
|
||||||
import { FloorListItem } from '../../interfaces/floor.interfaces';
|
import { FloorListItem } from '../../interfaces/floor.interfaces';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
const open = ref(true);
|
const open = ref(true);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
floors: FloorListItem[];
|
floors: FloorListItem[];
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { FloorListItem } from './floor.interfaces';
|
||||||
|
|
||||||
export interface BuildingListItem {
|
export interface BuildingListItem {
|
||||||
id: number;
|
id: number;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
@ -6,3 +8,7 @@ export interface BuildingListItem {
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BuildingResponse extends BuildingListItem {
|
||||||
|
floors: FloorListItem[];
|
||||||
|
}
|
||||||
|
@ -5,6 +5,7 @@ export interface FloorListItem {
|
|||||||
displayName: string;
|
displayName: string;
|
||||||
number: number;
|
number: number;
|
||||||
plan: string;
|
plan: string;
|
||||||
|
planImage?: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
rooms: RoomListItem[];
|
rooms: RoomListItem[];
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
import { Vec2, Vec2Box } from '../modules/house-planner/interfaces';
|
||||||
|
import { StorageListItem } from './storage.interfaces';
|
||||||
|
|
||||||
export interface RoomListItem {
|
export interface RoomListItem {
|
||||||
id: number;
|
id: number;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
@ -10,3 +13,10 @@ export interface UpsertRoomItem {
|
|||||||
displayName: string;
|
displayName: string;
|
||||||
plan: string;
|
plan: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RoomLayoutObject {
|
||||||
|
id: number;
|
||||||
|
displayName: string;
|
||||||
|
plan: Vec2[];
|
||||||
|
boundingBox: Vec2Box;
|
||||||
|
}
|
||||||
|
11
src/interfaces/storage.interfaces.ts
Normal file
11
src/interfaces/storage.interfaces.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export interface StorageListItem {
|
||||||
|
id: number;
|
||||||
|
displayName: string;
|
||||||
|
type: string;
|
||||||
|
location: string;
|
||||||
|
locationDescription: string;
|
||||||
|
color: string;
|
||||||
|
itemCount: number;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
@ -35,6 +35,7 @@ export class HousePlannerCanvas {
|
|||||||
public mousePosition: Vec2 = [0, 0];
|
public mousePosition: Vec2 = [0, 0];
|
||||||
public mouseClickPosition: Vec2 = [0, 0];
|
public mouseClickPosition: Vec2 = [0, 0];
|
||||||
public mousePositionSnapped: Vec2 = [0, 0];
|
public mousePositionSnapped: Vec2 = [0, 0];
|
||||||
|
public blacklist: LayerObjectType[] = [];
|
||||||
private dragging = false;
|
private dragging = false;
|
||||||
private pinching = false;
|
private pinching = false;
|
||||||
private lastPinchLength = 0;
|
private lastPinchLength = 0;
|
||||||
@ -46,14 +47,21 @@ export class HousePlannerCanvas {
|
|||||||
public canvasDim: Vec2,
|
public canvasDim: Vec2,
|
||||||
public canvasPos: Vec2,
|
public canvasPos: Vec2,
|
||||||
public canvasZoom = 1,
|
public canvasZoom = 1,
|
||||||
public editable = true
|
public editable = true,
|
||||||
|
public enableGrid = true,
|
||||||
|
public headless = false
|
||||||
) {
|
) {
|
||||||
this.ctx = this.canvas.getContext('2d')!;
|
this.ctx = this.canvas.getContext('2d')!;
|
||||||
this.setupEvents();
|
this.setupEvents();
|
||||||
this.resizeCanvas();
|
this.resizeCanvas();
|
||||||
if (editable) {
|
if (editable && !headless) {
|
||||||
this.tools = new HousePlannerCanvasTools(this);
|
this.tools = new HousePlannerCanvasTools(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO expose
|
||||||
|
if (headless) {
|
||||||
|
this.blacklist.push('room');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get width() {
|
get width() {
|
||||||
@ -77,6 +85,7 @@ export class HousePlannerCanvas {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cleanUp() {
|
cleanUp() {
|
||||||
|
if (this.headless) return;
|
||||||
this.tools?.cleanUp();
|
this.tools?.cleanUp();
|
||||||
window.removeEventListener('keyup', this.boundKeyUpEvent);
|
window.removeEventListener('keyup', this.boundKeyUpEvent);
|
||||||
window.removeEventListener('keydown', this.boundKeyDownEvent);
|
window.removeEventListener('keydown', this.boundKeyDownEvent);
|
||||||
@ -84,7 +93,7 @@ export class HousePlannerCanvas {
|
|||||||
|
|
||||||
draw() {
|
draw() {
|
||||||
this.ctx.clearRect(0, 0, this.width, this.height);
|
this.ctx.clearRect(0, 0, this.width, this.height);
|
||||||
this.grid.draw();
|
this.enableGrid && this.grid.draw();
|
||||||
this.tools?.drawHighlights();
|
this.tools?.drawHighlights();
|
||||||
for (const layer of this.layers.slice().reverse()) {
|
for (const layer of this.layers.slice().reverse()) {
|
||||||
if (!layer.visible) continue;
|
if (!layer.visible) continue;
|
||||||
@ -255,7 +264,7 @@ export class HousePlannerCanvas {
|
|||||||
this.repositionEvent();
|
this.repositionEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
realMousePos(x: number, y: number) {
|
realMousePos(x: number, y: number, click = false) {
|
||||||
const rect = this.canvas.getBoundingClientRect();
|
const rect = this.canvas.getBoundingClientRect();
|
||||||
const scaleX = this.canvas.width / rect.width;
|
const scaleX = this.canvas.width / rect.width;
|
||||||
const scaleY = this.canvas.height / rect.height;
|
const scaleY = this.canvas.height / rect.height;
|
||||||
@ -263,6 +272,9 @@ export class HousePlannerCanvas {
|
|||||||
this.mousePositionSnapped = !!this.grid.gridSnap
|
this.mousePositionSnapped = !!this.grid.gridSnap
|
||||||
? vec2Snap(this.mousePosition, this.grid.gridSnap)
|
? vec2Snap(this.mousePosition, this.grid.gridSnap)
|
||||||
: this.mousePosition;
|
: this.mousePosition;
|
||||||
|
if (click) {
|
||||||
|
this.mouseClickPosition = [...this.mousePosition];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllObjectsOfType(type: LayerObjectType) {
|
getAllObjectsOfType(type: LayerObjectType) {
|
||||||
@ -321,6 +333,7 @@ export class HousePlannerCanvas {
|
|||||||
private boundKeyUpEvent = this.keyUpEvent.bind(this);
|
private boundKeyUpEvent = this.keyUpEvent.bind(this);
|
||||||
|
|
||||||
private setupEvents() {
|
private setupEvents() {
|
||||||
|
if (this.headless) return;
|
||||||
window.addEventListener('keyup', this.boundKeyUpEvent);
|
window.addEventListener('keyup', this.boundKeyUpEvent);
|
||||||
window.addEventListener('keydown', this.boundKeyDownEvent);
|
window.addEventListener('keydown', this.boundKeyDownEvent);
|
||||||
|
|
||||||
@ -331,6 +344,7 @@ export class HousePlannerCanvas {
|
|||||||
this.canvas.addEventListener('touchmove', (e) => this.onTouchMove(e));
|
this.canvas.addEventListener('touchmove', (e) => this.onTouchMove(e));
|
||||||
this.canvas.addEventListener('touchstart', (e) => this.onTouchStart(e));
|
this.canvas.addEventListener('touchstart', (e) => this.onTouchStart(e));
|
||||||
this.canvas.addEventListener('touchend', (e) => this.onTouchEnd(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('wheel', (e) => this.onMouseWheel(e));
|
||||||
|
|
||||||
this.canvas.addEventListener('pointerleave', () => this.onPointerLeave());
|
this.canvas.addEventListener('pointerleave', () => this.onPointerLeave());
|
||||||
@ -349,14 +363,11 @@ export class HousePlannerCanvas {
|
|||||||
this.ctx.font = '16px Arial';
|
this.ctx.font = '16px Arial';
|
||||||
this.ctx.fillStyle = line.color;
|
this.ctx.fillStyle = line.color;
|
||||||
const { width } = this.ctx.measureText(line.name);
|
const { width } = this.ctx.measureText(line.name);
|
||||||
this.ctx.fillText(
|
this.ctx.fillText(line.name, centerPoint[0] - width / 2, centerPoint[1]);
|
||||||
line.name,
|
|
||||||
centerPoint[0] - width / 2,
|
|
||||||
centerPoint[1] - 8
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private drawLine(line: Line) {
|
private drawLine(line: Line) {
|
||||||
|
if (this.blacklist?.includes(line.type)) return;
|
||||||
const path = this.makeLinePath(line);
|
const path = this.makeLinePath(line);
|
||||||
line.render = path;
|
line.render = path;
|
||||||
this.setupLine(line);
|
this.setupLine(line);
|
||||||
@ -408,7 +419,7 @@ export class HousePlannerCanvas {
|
|||||||
(ev.touches[0].clientY - this.canvasPos[1]) / this.canvasZoom;
|
(ev.touches[0].clientY - this.canvasPos[1]) / this.canvasZoom;
|
||||||
|
|
||||||
delta > 0 ? (this.canvasZoom *= delta) : (this.canvasZoom /= delta);
|
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 = [
|
this.canvasPos = [
|
||||||
ev.touches[0].clientX - scaleX * this.canvasZoom,
|
ev.touches[0].clientX - scaleX * this.canvasZoom,
|
||||||
@ -425,8 +436,8 @@ export class HousePlannerCanvas {
|
|||||||
onTouchStart(e: TouchEvent) {
|
onTouchStart(e: TouchEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const touch = e.touches[0] || e.changedTouches[0];
|
const touch = e.touches[0] || e.changedTouches[0];
|
||||||
this.mouseClickPosition = [touch.clientX, touch.clientY];
|
// this.mouseClickPosition = [touch.clientX, touch.clientY];
|
||||||
this.realMousePos(touch.clientX, touch.clientY);
|
this.realMousePos(touch.clientX, touch.clientY, true);
|
||||||
|
|
||||||
this.moved = false;
|
this.moved = false;
|
||||||
|
|
||||||
@ -451,7 +462,7 @@ export class HousePlannerCanvas {
|
|||||||
this.dragging = false;
|
this.dragging = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
initViewport(boundingBox: Vec2Box) {
|
setViewRectangle(boundingBox: Vec2Box) {
|
||||||
if (!isValidVec2(boundingBox[0]) || !isValidVec2(boundingBox[1])) return;
|
if (!isValidVec2(boundingBox[0]) || !isValidVec2(boundingBox[1])) return;
|
||||||
const [zoom, pos] = this.calculateViewport(boundingBox);
|
const [zoom, pos] = this.calculateViewport(boundingBox);
|
||||||
this.canvasZoom = zoom;
|
this.canvasZoom = zoom;
|
||||||
@ -468,15 +479,15 @@ export class HousePlannerCanvas {
|
|||||||
|
|
||||||
private calculateViewport(box: Vec2Box): [number, Vec2] {
|
private calculateViewport(box: Vec2Box): [number, Vec2] {
|
||||||
let [min, max] = box;
|
let [min, max] = box;
|
||||||
const gap = 80;
|
const gap = this.headless ? 10 : 80;
|
||||||
min = vec2Sub(min, [gap, gap]);
|
min = vec2Sub(min, [gap, gap]);
|
||||||
max = vec2Add(max, [gap, gap]);
|
max = vec2Add(max, [gap, gap]);
|
||||||
|
|
||||||
const { width: windowWidth, height: windowHeight } =
|
const { width: windowWidth, height: windowHeight } =
|
||||||
this.canvas.parentElement!.getBoundingClientRect();
|
this.canvas.parentElement!.parentElement!.getBoundingClientRect();
|
||||||
const diagonal = vec2Distance(min, max);
|
const diagonal = vec2Distance(min, max);
|
||||||
const target = vec2Sub(max, min);
|
const target = vec2Sub(max, min);
|
||||||
const zoomScale = windowHeight / diagonal;
|
const zoomScale = Math.min(windowWidth, windowHeight) / diagonal;
|
||||||
|
|
||||||
let scaledPos = vec2MultiplyScalar(min, zoomScale);
|
let scaledPos = vec2MultiplyScalar(min, zoomScale);
|
||||||
const scaled = vec2MultiplyScalar(target, 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
|
// FIXME: possibly there's a better approach, but right now some clicks do not register
|
||||||
if (
|
if (
|
||||||
this.moved &&
|
this.moved &&
|
||||||
vec2Distance(this.mouseClickPosition, this.mousePosition) < 0.25
|
vec2Distance(this.mouseClickPosition, this.mousePosition) < 1
|
||||||
) {
|
) {
|
||||||
this.moved = false;
|
this.moved = false;
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,8 @@ export class HousePlanner {
|
|||||||
canvasPos: Vec2,
|
canvasPos: Vec2,
|
||||||
canvasZoom = 1,
|
canvasZoom = 1,
|
||||||
editable = true,
|
editable = true,
|
||||||
|
grid = true,
|
||||||
|
headless = false,
|
||||||
boundingBox?: Vec2Box
|
boundingBox?: Vec2Box
|
||||||
): [number, Vec2] {
|
): [number, Vec2] {
|
||||||
this.canvas = canvas;
|
this.canvas = canvas;
|
||||||
@ -21,7 +23,9 @@ export class HousePlanner {
|
|||||||
canvasDim,
|
canvasDim,
|
||||||
canvasPos,
|
canvasPos,
|
||||||
canvasZoom,
|
canvasZoom,
|
||||||
editable
|
editable,
|
||||||
|
grid,
|
||||||
|
headless
|
||||||
);
|
);
|
||||||
this.manager.layers = initialData;
|
this.manager.layers = initialData;
|
||||||
|
|
||||||
@ -37,7 +41,7 @@ export class HousePlanner {
|
|||||||
this.manager.draw();
|
this.manager.draw();
|
||||||
|
|
||||||
if (boundingBox) {
|
if (boundingBox) {
|
||||||
this.manager.initViewport(boundingBox);
|
this.manager.setViewRectangle(boundingBox);
|
||||||
}
|
}
|
||||||
return [this.manager.canvasZoom, this.manager.canvasPos];
|
return [this.manager.canvasZoom, this.manager.canvasPos];
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,6 @@ import { ICanvasToolBase, LayerObject, Vec2 } from '../interfaces';
|
|||||||
import type { HousePlannerCanvasTools } from '../tools';
|
import type { HousePlannerCanvasTools } from '../tools';
|
||||||
|
|
||||||
export class CanvasToolBase<T> implements ICanvasToolBase<T> {
|
export class CanvasToolBase<T> implements ICanvasToolBase<T> {
|
||||||
protected mousePosition: Vec2 = [0, 0];
|
|
||||||
protected mousePositionAbsolute: Vec2 = [0, 0];
|
|
||||||
public name = 'tool';
|
public name = 'tool';
|
||||||
public subTool: T | undefined;
|
public subTool: T | undefined;
|
||||||
|
|
||||||
@ -28,14 +26,19 @@ export class CanvasToolBase<T> implements ICanvasToolBase<T> {
|
|||||||
return this.manager.selectedLayer;
|
return this.manager.selectedLayer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get mousePosition() {
|
||||||
|
return this.renderer.mousePositionSnapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
get mousePositionAbsolute() {
|
||||||
|
return this.renderer.mousePosition;
|
||||||
|
}
|
||||||
|
|
||||||
drawHighlights() {}
|
drawHighlights() {}
|
||||||
drawControls() {}
|
drawControls() {}
|
||||||
|
|
||||||
mouseDown(targetObject?: LayerObject) {}
|
mouseDown(targetObject?: LayerObject) {}
|
||||||
mouseMoved(mouse: Vec2, offset: Vec2, mouseAbsolute: Vec2) {
|
mouseMoved(mouse: Vec2, offset: Vec2, mouseAbsolute: Vec2) {}
|
||||||
this.mousePosition = mouse;
|
|
||||||
this.mousePositionAbsolute = mouseAbsolute;
|
|
||||||
}
|
|
||||||
mouseUp(moved = false) {}
|
mouseUp(moved = false) {}
|
||||||
|
|
||||||
setSubTool(subTool: T) {
|
setSubTool(subTool: T) {
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
import { NavigationGuardWithThis, RouteRecordRaw } from 'vue-router';
|
import { NavigationGuardWithThis, RouteRecordRaw } from 'vue-router';
|
||||||
import Dashboard from '../views/Dashboard.vue';
|
import Dashboard from '../views/Dashboard.vue';
|
||||||
import Login from '../views/Login.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 { createRouter, createWebHashHistory } from 'vue-router';
|
||||||
import { useUserStore } from '../store/user.store';
|
import { useUserStore } from '../store/user.store';
|
||||||
import HousePlanner from '../views/HousePlanner.vue';
|
|
||||||
|
|
||||||
const routes: RouteRecordRaw[] = [
|
const routes: RouteRecordRaw[] = [
|
||||||
{
|
{
|
||||||
@ -21,6 +24,46 @@ const routes: RouteRecordRaw[] = [
|
|||||||
path: '/planner',
|
path: '/planner',
|
||||||
component: HousePlanner,
|
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({
|
const router = createRouter({
|
||||||
|
@ -2,7 +2,10 @@ import omit from 'lodash.omit';
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { useAccessToken } from '../composables/useAccessToken';
|
import { useAccessToken } from '../composables/useAccessToken';
|
||||||
import { BACKEND_URL } from '../constants';
|
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 { FloorListItem } from '../interfaces/floor.interfaces';
|
||||||
import { RoomListItem, UpsertRoomItem } from '../interfaces/room.interfaces';
|
import { RoomListItem, UpsertRoomItem } from '../interfaces/room.interfaces';
|
||||||
import jfetch from '../utils/jfetch';
|
import jfetch from '../utils/jfetch';
|
||||||
@ -13,9 +16,19 @@ export const useBuildingStore = defineStore('building', {
|
|||||||
return {
|
return {
|
||||||
buildings: [] as BuildingListItem[],
|
buildings: [] as BuildingListItem[],
|
||||||
floors: [] as FloorListItem[],
|
floors: [] as FloorListItem[],
|
||||||
|
building: null as null | BuildingResponse,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
|
async getBuilding(id: number) {
|
||||||
|
const { data: building } = await jfetch(
|
||||||
|
`${BACKEND_URL}/buildings/${id}`,
|
||||||
|
{
|
||||||
|
headers: authHeader.value,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
this.building = building;
|
||||||
|
},
|
||||||
async getBuildings() {
|
async getBuildings() {
|
||||||
const { data: buildings } = await jfetch(`${BACKEND_URL}/buildings`, {
|
const { data: buildings } = await jfetch(`${BACKEND_URL}/buildings`, {
|
||||||
headers: authHeader.value,
|
headers: authHeader.value,
|
||||||
|
@ -34,7 +34,8 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
import { defaultRoomData } from '../components/house-planner/helpers/default-room';
|
import { defaultRoomData } from '../components/house-planner/helpers/default-room';
|
||||||
import HousePlanner from '../components/house-planner/HousePlanner.vue';
|
import HousePlanner from '../components/house-planner/HousePlanner.vue';
|
||||||
import { FloorDocument } from '../components/house-planner/interfaces/floor-document.interface';
|
import { FloorDocument } from '../components/house-planner/interfaces/floor-document.interface';
|
||||||
@ -43,7 +44,9 @@ import { UpsertRoomItem } from '../interfaces/room.interfaces';
|
|||||||
import { Line, Vec2Box } from '../modules/house-planner/interfaces';
|
import { Line, Vec2Box } from '../modules/house-planner/interfaces';
|
||||||
import { useBuildingStore } from '../store/building.store';
|
import { useBuildingStore } from '../store/building.store';
|
||||||
import extractLinePoints from '../utils/extract-line-points';
|
import extractLinePoints from '../utils/extract-line-points';
|
||||||
|
|
||||||
const building = useBuildingStore();
|
const building = useBuildingStore();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
const { buildings, floors } = storeToRefs(building);
|
const { buildings, floors } = storeToRefs(building);
|
||||||
const selectedBuildingId = ref<number>();
|
const selectedBuildingId = ref<number>();
|
||||||
@ -154,8 +157,19 @@ const updateDocument = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const initialize = async () => {
|
||||||
|
await building.getBuildings();
|
||||||
|
if (route.query.buildingId) {
|
||||||
|
await buildingSelected(Number(route.query.buildingId));
|
||||||
|
|
||||||
|
if (route.query.floorId) {
|
||||||
|
selectedFloorId.value = Number(route.query.floorId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
floors.value = [];
|
floors.value = [];
|
||||||
building.getBuildings();
|
initialize();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
44
src/views/building/BuildingView.vue
Normal file
44
src/views/building/BuildingView.vue
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<PageHead with-actions>
|
||||||
|
<template #actions>
|
||||||
|
<RouterLink
|
||||||
|
class="px-2 py-2"
|
||||||
|
:to="{ name: 'planner', query: { buildingId: building?.id } }"
|
||||||
|
>Edit floor plans</RouterLink
|
||||||
|
>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #default>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<h1 class="text-2xl font-bold">{{ building?.displayName }}</h1>
|
||||||
|
<span class="text-sm font-light text-gray-800 line-clamp-1">{{
|
||||||
|
building?.address
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</PageHead>
|
||||||
|
|
||||||
|
<div class="mb-4 flex items-center justify-between">
|
||||||
|
<h2 class="text-xl font-bold">Floors</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3" v-if="building">
|
||||||
|
<FloorListItem
|
||||||
|
:building="building!.id"
|
||||||
|
v-for="floor of building?.floors || []"
|
||||||
|
:floor="floor"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import PageHead from '../../components/PageHead.vue';
|
||||||
|
import { useBuildingStore } from '../../store/building.store';
|
||||||
|
import FloorListItem from './floors/FloorListItem.vue';
|
||||||
|
|
||||||
|
const buildingStore = useBuildingStore();
|
||||||
|
const { building } = storeToRefs(buildingStore);
|
||||||
|
</script>
|
26
src/views/building/BuildingViewBase.vue
Normal file
26
src/views/building/BuildingViewBase.vue
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<template>
|
||||||
|
<StandardLayout>
|
||||||
|
<RouterView />
|
||||||
|
</StandardLayout>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { watch } from 'vue';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
import StandardLayout from '../../components/StandardLayout.vue';
|
||||||
|
import { useBuildingStore } from '../../store/building.store';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const buildingStore = useBuildingStore();
|
||||||
|
|
||||||
|
const getBuilding = async (id: number) => {
|
||||||
|
try {
|
||||||
|
await buildingStore.getBuilding(id);
|
||||||
|
} catch (e) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.params.id,
|
||||||
|
() => getBuilding(Number(route.params.id)),
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
</script>
|
43
src/views/building/floors/FloorListItem.vue
Normal file
43
src/views/building/floors/FloorListItem.vue
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<template>
|
||||||
|
<RouterLink
|
||||||
|
:to="{ name: 'floor', params: { id: building, number: floor.number } }"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="hoverbox relative flex h-60 flex-col overflow-hidden rounded-lg bg-gray-50 shadow-lg ring-1 ring-black ring-opacity-5 transition-all duration-500 hover:scale-105 md:h-80"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:src="`${BACKEND_URL}/usercontent/${floor.planImage}`"
|
||||||
|
v-if="floor.planImage"
|
||||||
|
class="absolute object-cover"
|
||||||
|
/>
|
||||||
|
<div class="z-10 mt-auto flex flex-col bg-white px-4 py-4">
|
||||||
|
<span class="text-lg font-bold">{{ floor.displayName }}</span>
|
||||||
|
<span class="text-sm">Floor {{ floor.number }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</RouterLink>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { BACKEND_URL } from '../../../constants';
|
||||||
|
import { FloorListItem } from '../../../interfaces/floor.interfaces';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
building: number;
|
||||||
|
floor: FloorListItem;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.hoverbox {
|
||||||
|
img {
|
||||||
|
transition: all 3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
img {
|
||||||
|
transform: scale(1.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
192
src/views/building/floors/FloorView.vue
Normal file
192
src/views/building/floors/FloorView.vue
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
<template>
|
||||||
|
<PageHead with-actions>
|
||||||
|
<template #actions>
|
||||||
|
<RouterLink
|
||||||
|
class="px-2 py-2"
|
||||||
|
:to="{
|
||||||
|
name: 'planner',
|
||||||
|
query: { buildingId: building?.id, floorId: floor?.id },
|
||||||
|
}"
|
||||||
|
>Edit floor plan</RouterLink
|
||||||
|
>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #default>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<h1 class="text-2xl font-bold">{{ floor?.displayName }}</h1>
|
||||||
|
<span class="text-sm font-light text-gray-800 line-clamp-1"
|
||||||
|
>Floor {{ floor?.number }} in {{ building?.displayName }}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</PageHead>
|
||||||
|
|
||||||
|
<HousePlanner
|
||||||
|
v-if="floorDocument"
|
||||||
|
:key="`fdoc${floorDocument.id}`"
|
||||||
|
ref="canvas"
|
||||||
|
class="h-[70vh]"
|
||||||
|
:editable="false"
|
||||||
|
:grid="false"
|
||||||
|
transparent
|
||||||
|
headless
|
||||||
|
:floor-document="floorDocument"
|
||||||
|
@click="clickOnRoom()"
|
||||||
|
>
|
||||||
|
<template v-for="room of rooms">
|
||||||
|
<div
|
||||||
|
v-if="!highlighted || highlighted.id === room.id"
|
||||||
|
:class="[
|
||||||
|
highlighted
|
||||||
|
? ''
|
||||||
|
: 'flex cursor-pointer items-center justify-center transition-transform hover:scale-105',
|
||||||
|
'pointer-events-auto absolute',
|
||||||
|
]"
|
||||||
|
@click.stop="clickOnRoom(room)"
|
||||||
|
:style="getRoomPositionCSS(room)"
|
||||||
|
>
|
||||||
|
<div class="absolute" :style="getRoomPolygonCSS(room)"></div>
|
||||||
|
<span class="pointer-events-none" v-if="!highlighted">{{
|
||||||
|
room.displayName
|
||||||
|
}}</span>
|
||||||
|
<template v-if="highlighted?.id === room.id">
|
||||||
|
<template v-for="storage of storages">
|
||||||
|
<div
|
||||||
|
class="custom-storage absolute"
|
||||||
|
:style="getStoragePosition(storage)"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="absolute bottom-2 left-1/2 h-20 w-20 -translate-x-1/2 rounded-lg bg-blue-400 px-2 py-2"
|
||||||
|
>
|
||||||
|
<span>{{ storage.displayName }}</span>
|
||||||
|
<div
|
||||||
|
class="absolute -bottom-2 left-1/2 h-0 w-0 -translate-x-1/2 border-x-8 border-t-[16px] border-x-transparent border-t-blue-400"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</template></div
|
||||||
|
></template>
|
||||||
|
</HousePlanner>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import PageHead from '../../../components/PageHead.vue';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
import { useBuildingStore } from '../../../store/building.store';
|
||||||
|
import { computed, ref } from '@vue/reactivity';
|
||||||
|
import HousePlanner from '../../../components/house-planner/HousePlanner.vue';
|
||||||
|
import { boundingBox } from '../../../modules/house-planner/utils';
|
||||||
|
import { Vec2 } from '../../../modules/house-planner/interfaces';
|
||||||
|
import { watch } from 'vue';
|
||||||
|
import { RoomLayoutObject } from '../../../interfaces/room.interfaces';
|
||||||
|
import jfetch from '../../../utils/jfetch';
|
||||||
|
import { BACKEND_URL } from '../../../constants';
|
||||||
|
import { useAccessToken } from '../../../composables/useAccessToken';
|
||||||
|
import { StorageListItem } from '../../../interfaces/storage.interfaces';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const buildingStore = useBuildingStore();
|
||||||
|
const { building } = storeToRefs(buildingStore);
|
||||||
|
const { authHeader } = useAccessToken();
|
||||||
|
const canvas = ref<InstanceType<typeof HousePlanner>>();
|
||||||
|
const highlighted = ref<RoomLayoutObject>();
|
||||||
|
const storages = ref<StorageListItem[]>([]);
|
||||||
|
|
||||||
|
const floor = computed(() =>
|
||||||
|
building.value?.floors.find(
|
||||||
|
(floor) => floor.number === Number(route.params.number)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const floorDocument = computed(
|
||||||
|
() => floor.value?.plan && JSON.parse(floor.value.plan)
|
||||||
|
);
|
||||||
|
|
||||||
|
const clickOnRoom = (room?: RoomLayoutObject) => {
|
||||||
|
canvas.value?.setViewRectangle(room?.boundingBox);
|
||||||
|
if (room && !highlighted.value) {
|
||||||
|
getRoomStorages(room);
|
||||||
|
} else if (!room && highlighted.value) {
|
||||||
|
storages.value = [];
|
||||||
|
}
|
||||||
|
highlighted.value = room;
|
||||||
|
};
|
||||||
|
|
||||||
|
const rooms = computed<RoomLayoutObject[]>(
|
||||||
|
() =>
|
||||||
|
(floor.value &&
|
||||||
|
floor.value.rooms
|
||||||
|
.filter((room) => room.plan?.includes('polygon'))
|
||||||
|
.map((room) => {
|
||||||
|
const { polygon } = JSON.parse(room.plan);
|
||||||
|
const aabb = boundingBox(polygon, [
|
||||||
|
[Infinity, Infinity],
|
||||||
|
[0, 0],
|
||||||
|
]);
|
||||||
|
return {
|
||||||
|
...room,
|
||||||
|
plan: polygon as Vec2[],
|
||||||
|
boundingBox: aabb,
|
||||||
|
storages: [],
|
||||||
|
} as RoomLayoutObject;
|
||||||
|
})) ||
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getRoomPolygonCSS = (room: RoomLayoutObject) => {
|
||||||
|
const [min, max] = room.boundingBox;
|
||||||
|
return {
|
||||||
|
clipPath: `polygon(${room.plan
|
||||||
|
.map((point) => `${point[0] - min[0]}px ${point[1] - min[1]}px`)
|
||||||
|
.join(', ')})`,
|
||||||
|
width: `${max[0] - min[0]}px`,
|
||||||
|
height: `${max[1] - min[1]}px`,
|
||||||
|
backgroundColor: 'rgb(40 180 255 / 40%)',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRoomPositionCSS = (room: RoomLayoutObject) => {
|
||||||
|
const [min, max] = room.boundingBox;
|
||||||
|
return {
|
||||||
|
top: `${min[1]}px`,
|
||||||
|
left: `${min[0]}px`,
|
||||||
|
width: `${max[0] - min[0]}px`,
|
||||||
|
height: `${max[1] - min[1]}px`,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const getStoragePosition = (storage: any) => {
|
||||||
|
const locationSplit = storage.location
|
||||||
|
.split(',')
|
||||||
|
.map((int: string) => `${int}px`);
|
||||||
|
return {
|
||||||
|
left: locationSplit[0],
|
||||||
|
top: locationSplit[1],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRoomStorages = async (room: RoomLayoutObject) => {
|
||||||
|
try {
|
||||||
|
const { data: storageList } = await jfetch(
|
||||||
|
`${BACKEND_URL}/storage/room/${room.id}?includeWithSets=false`,
|
||||||
|
{
|
||||||
|
headers: authHeader.value,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const { data: setList } = await jfetch(
|
||||||
|
`${BACKEND_URL}/storage/set/room/${room.id}`,
|
||||||
|
{
|
||||||
|
headers: authHeader.value,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
storages.value = [...storageList, ...setList];
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.custom-storage {
|
||||||
|
}
|
||||||
|
</style>
|
Loading…
Reference in New Issue
Block a user