zoom in on the object on load
This commit is contained in:
parent
2dc4256f0f
commit
eb48192542
@ -70,6 +70,7 @@ import {
|
|||||||
RepositionEvent,
|
RepositionEvent,
|
||||||
ToolEvent,
|
ToolEvent,
|
||||||
Vec2,
|
Vec2,
|
||||||
|
Vec2Box,
|
||||||
} from '../../modules/house-planner/interfaces';
|
} from '../../modules/house-planner/interfaces';
|
||||||
import type { HousePlannerCanvasTools } from '../../modules/house-planner/tools';
|
import type { HousePlannerCanvasTools } from '../../modules/house-planner/tools';
|
||||||
import deepUnref from '../../utils/deep-unref';
|
import deepUnref from '../../utils/deep-unref';
|
||||||
@ -98,16 +99,23 @@ const props = withDefaults(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'update', document: FloorDocument, rooms: Line[]): void;
|
(
|
||||||
|
e: 'update',
|
||||||
|
document: FloorDocument,
|
||||||
|
rooms: Line[],
|
||||||
|
boundingBox?: Vec2Box
|
||||||
|
): void;
|
||||||
(e: 'edited'): void;
|
(e: 'edited'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const localFloorDocument = ref(deepUnref(props.floorDocument));
|
const localFloorDocument = ref(deepUnref(props.floorDocument));
|
||||||
const emitUpdate = () =>
|
const emitUpdate = () =>
|
||||||
|
module.value.manager &&
|
||||||
emit(
|
emit(
|
||||||
'update',
|
'update',
|
||||||
localFloorDocument.value,
|
localFloorDocument.value,
|
||||||
(module.value.manager?.getAllObjectsOfType('room') || []) as Line[]
|
module.value.manager.getAllObjectsOfType('room') as Line[],
|
||||||
|
module.value.manager.getBoundingBox()
|
||||||
);
|
);
|
||||||
const debouncedUpdate = useDebounceFn(emitUpdate, 10000);
|
const debouncedUpdate = useDebounceFn(emitUpdate, 10000);
|
||||||
|
|
||||||
@ -241,14 +249,18 @@ defineExpose({
|
|||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
module.value.initialize(
|
const [setZoom, setPos] = module.value.initialize(
|
||||||
canvas.value,
|
canvas.value,
|
||||||
deepUnref(localFloorDocument.value.layers),
|
deepUnref(localFloorDocument.value.layers),
|
||||||
[localFloorDocument.value.width, localFloorDocument.value.height],
|
[localFloorDocument.value.width, localFloorDocument.value.height],
|
||||||
canvasPos.value,
|
canvasPos.value,
|
||||||
canvasZoom.value,
|
canvasZoom.value,
|
||||||
props.editable
|
props.editable,
|
||||||
|
localFloorDocument.value.boundingBox
|
||||||
);
|
);
|
||||||
|
canvasPos.value = setPos;
|
||||||
|
canvasZoom.value = setZoom;
|
||||||
|
|
||||||
Object.keys(events).forEach((event) =>
|
Object.keys(events).forEach((event) =>
|
||||||
canvas.value.addEventListener(event, events[event])
|
canvas.value.addEventListener(event, events[event])
|
||||||
);
|
);
|
||||||
|
104
src/components/house-planner/PlannerBuildingSelect.vue
Normal file
104
src/components/house-planner/PlannerBuildingSelect.vue
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
<template>
|
||||||
|
<div class="absolute top-0 left-0 z-10">
|
||||||
|
<Transition
|
||||||
|
enter-active-class="transition-max-height ease-out duration-200 overflow-hidden"
|
||||||
|
enter-from-class="max-h-0"
|
||||||
|
enter-to-class="max-h-14"
|
||||||
|
leave-active-class="transition-max-height ease-in duration-150 overflow-hidden"
|
||||||
|
leave-from-class="max-h-14"
|
||||||
|
leave-to-class="max-h-0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="open"
|
||||||
|
class="rounded-br-md border-2 border-t-0 border-gray-200 bg-white shadow-lg"
|
||||||
|
>
|
||||||
|
<div class="h-14 px-2 py-2.5">
|
||||||
|
<div class="flex flex-row items-center space-x-4 px-4">
|
||||||
|
<RouterLink to="/" title="Go back"
|
||||||
|
><span class="sr-only">Go back</span
|
||||||
|
><ChevronLeftIcon class="-ml-4 h-6 w-6"
|
||||||
|
/></RouterLink>
|
||||||
|
<div class="flex flex-row items-center space-x-4">
|
||||||
|
<label for="building">Building:</label>
|
||||||
|
<select
|
||||||
|
id="building"
|
||||||
|
class="rounded-sm border-gray-300 py-1 focus:ring-2 focus:ring-blue-200"
|
||||||
|
:value="selectedBuildingId"
|
||||||
|
@change="
|
||||||
|
emit(
|
||||||
|
'update:selectedBuildingId',
|
||||||
|
Number(($event.target as HTMLSelectElement).value)
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<option v-for="building of buildings" :value="building.id">
|
||||||
|
{{ building.displayName }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="flex flex-row items-center space-x-4"
|
||||||
|
v-if="selectedBuildingId"
|
||||||
|
>
|
||||||
|
<label for="floor">Floor:</label>
|
||||||
|
<select
|
||||||
|
id="floor"
|
||||||
|
class="rounded-sm border-gray-300 py-1 focus:ring-2 focus:ring-blue-200"
|
||||||
|
:value="selectedFloorId"
|
||||||
|
@change="
|
||||||
|
emit(
|
||||||
|
'update:selectedFloorId',
|
||||||
|
Number(($event.target as HTMLSelectElement).value)
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<option v-for="floor of floors" :value="floor.id">
|
||||||
|
{{ floor.displayName }} ({{ floor.number }})
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div v-if="selectedFloorId">{{ status }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
<button
|
||||||
|
:class="[
|
||||||
|
'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',
|
||||||
|
]"
|
||||||
|
:title="`${open ? 'Hide' : 'Open'} panel`"
|
||||||
|
:aria-expanded="open"
|
||||||
|
@click="() => (open = !open)"
|
||||||
|
>
|
||||||
|
<span class="sr-only">{{ open ? 'Hide' : 'Open' }} panel</span>
|
||||||
|
<ChevronDoubleUpIcon class="h-4 w-4" v-if="open" />
|
||||||
|
<ChevronDoubleDownIcon class="h-4 w-4" v-else />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import {
|
||||||
|
ChevronLeftIcon,
|
||||||
|
ChevronDoubleDownIcon,
|
||||||
|
ChevronDoubleUpIcon,
|
||||||
|
} from '@heroicons/vue/24/outline';
|
||||||
|
import { BuildingListItem } from '../../interfaces/building.interfaces';
|
||||||
|
import { FloorListItem } from '../../interfaces/floor.interfaces';
|
||||||
|
|
||||||
|
const open = ref(true);
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
floors: FloorListItem[];
|
||||||
|
buildings: BuildingListItem[];
|
||||||
|
selectedBuildingId?: number;
|
||||||
|
selectedFloorId?: number;
|
||||||
|
status: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:selectedBuildingId', buildingId: number): void;
|
||||||
|
(e: 'update:selectedFloorId', buildingId: number): void;
|
||||||
|
}>();
|
||||||
|
</script>
|
@ -10,7 +10,7 @@
|
|||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<Square3Stack3DIcon class="h-4 w-4" />
|
<Square3Stack3DIcon class="h-4 w-4" />
|
||||||
<span>{{ layer.name }}</span>
|
<span>{{ layer.name }} ({{ layer.contents.length }})</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="flex flex-col bg-gray-50" v-if="layer.active">
|
<div class="flex flex-col bg-gray-50" v-if="layer.active">
|
||||||
<div v-for="object of layer.contents">
|
<div v-for="object of layer.contents">
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative flex flex-row px-2 pb-4 pr-0">
|
<div class="pointer-events-auto relative flex flex-row px-2 pb-4 pr-0">
|
||||||
<button
|
<button
|
||||||
:class="[
|
:class="[
|
||||||
open ? 'bg-gray-200' : 'bg-white',
|
open ? 'bg-gray-200' : 'bg-white',
|
||||||
'h-8 rounded-tl-md rounded-bl-md px-2 py-2 ring-1 ring-black ring-opacity-5',
|
'h-8 rounded-tl-md rounded-bl-md px-2 py-2 ring-1 ring-black ring-opacity-5',
|
||||||
]"
|
]"
|
||||||
|
:title="`${open ? 'Hide' : 'Open'} panel`"
|
||||||
|
:aria-expanded="open"
|
||||||
@click="() => (open = !open)"
|
@click="() => (open = !open)"
|
||||||
>
|
>
|
||||||
|
<span class="sr-only">{{ open ? 'Hide' : 'Open' }} panel</span>
|
||||||
<ChevronDoubleRightIcon class="h-4 w-4" v-if="open" />
|
<ChevronDoubleRightIcon class="h-4 w-4" v-if="open" />
|
||||||
<ChevronDoubleLeftIcon class="h-4 w-4" v-else />
|
<ChevronDoubleLeftIcon class="h-4 w-4" v-else />
|
||||||
</button>
|
</button>
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="z-8 absolute right-0 top-0 bottom-0 my-4 overflow-hidden">
|
<div
|
||||||
|
class="z-8 pointer-events-none absolute right-0 top-0 bottom-0 my-4 overflow-hidden"
|
||||||
|
>
|
||||||
<div class="flex h-full flex-col items-end space-y-2">
|
<div class="flex h-full flex-col items-end space-y-2">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Layer } from '../../../modules/house-planner/interfaces';
|
import { Layer, Vec2 } from '../../../modules/house-planner/interfaces';
|
||||||
|
|
||||||
export interface FloorDocument {
|
export interface FloorDocument {
|
||||||
id: number;
|
id: number;
|
||||||
@ -6,4 +6,8 @@ export interface FloorDocument {
|
|||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
layers: Layer[];
|
layers: Layer[];
|
||||||
|
/**
|
||||||
|
* Min, Max
|
||||||
|
*/
|
||||||
|
boundingBox?: [Vec2, Vec2];
|
||||||
}
|
}
|
||||||
|
@ -10,10 +10,13 @@ import {
|
|||||||
LineSegment,
|
LineSegment,
|
||||||
RepositionEvent,
|
RepositionEvent,
|
||||||
Vec2,
|
Vec2,
|
||||||
|
Vec2Box,
|
||||||
} from './interfaces';
|
} from './interfaces';
|
||||||
import { HousePlannerCanvasTools } from './tools';
|
import { HousePlannerCanvasTools } from './tools';
|
||||||
import { LayerObjectType } from './types';
|
import { LayerObjectType } from './types';
|
||||||
import {
|
import {
|
||||||
|
boundingBox,
|
||||||
|
isValidVec2,
|
||||||
vec2Add,
|
vec2Add,
|
||||||
vec2AngleFromOrigin,
|
vec2AngleFromOrigin,
|
||||||
vec2Distance,
|
vec2Distance,
|
||||||
@ -270,6 +273,42 @@ export class HousePlannerCanvas {
|
|||||||
return objects;
|
return objects;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getBoundingBox(): Vec2Box | undefined {
|
||||||
|
let box: Vec2Box = [
|
||||||
|
[Infinity, Infinity],
|
||||||
|
[0, 0],
|
||||||
|
];
|
||||||
|
|
||||||
|
this.layers.forEach((layer) => {
|
||||||
|
let layerPoints = layer.contents
|
||||||
|
.filter((object) => ['line', 'curve', 'room'].includes(object.type))
|
||||||
|
.reduce<Vec2[]>(
|
||||||
|
(list, object) => [...list, ...extractLinePoints(object as Line)],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
if (!layerPoints.length) return;
|
||||||
|
box = boundingBox(layerPoints, box);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
vec2Distance(box[0], box[1]) < 80 ||
|
||||||
|
!isValidVec2(box[0]) ||
|
||||||
|
!isValidVec2(box[1])
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
|
return box;
|
||||||
|
}
|
||||||
|
|
||||||
|
private drawBoxBounds(box: Vec2Box) {
|
||||||
|
for (const point of box) {
|
||||||
|
const [x, y] = point;
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.arc(x, y, 4, 0, 2 * Math.PI);
|
||||||
|
this.ctx.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private keyDownEvent(e: KeyboardEvent) {
|
private keyDownEvent(e: KeyboardEvent) {
|
||||||
if (e.target !== document.body && e.target != null) return;
|
if (e.target !== document.body && e.target != null) return;
|
||||||
this.tools?.onKeyDown(e);
|
this.tools?.onKeyDown(e);
|
||||||
@ -386,7 +425,9 @@ 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.dragging = true;
|
this.mouseClickPosition = [touch.clientX, touch.clientY];
|
||||||
|
this.realMousePos(touch.clientX, touch.clientY);
|
||||||
|
|
||||||
this.moved = false;
|
this.moved = false;
|
||||||
|
|
||||||
if (e.touches.length === 2) {
|
if (e.touches.length === 2) {
|
||||||
@ -410,6 +451,41 @@ export class HousePlannerCanvas {
|
|||||||
this.dragging = false;
|
this.dragging = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initViewport(boundingBox: Vec2Box) {
|
||||||
|
if (!isValidVec2(boundingBox[0]) || !isValidVec2(boundingBox[1])) return;
|
||||||
|
const [zoom, pos] = this.calculateViewport(boundingBox);
|
||||||
|
this.canvasZoom = zoom;
|
||||||
|
this.canvasPos = vec2MultiplyScalar(pos, -1);
|
||||||
|
this.canvas.dispatchEvent(
|
||||||
|
new CustomEvent<RepositionEvent>('hpc:position', {
|
||||||
|
detail: {
|
||||||
|
position: this.canvasPos,
|
||||||
|
zoom: this.canvasZoom,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private calculateViewport(box: Vec2Box): [number, Vec2] {
|
||||||
|
let [min, max] = box;
|
||||||
|
const gap = 80;
|
||||||
|
min = vec2Sub(min, [gap, gap]);
|
||||||
|
max = vec2Add(max, [gap, gap]);
|
||||||
|
|
||||||
|
const { width: windowWidth, height: windowHeight } =
|
||||||
|
this.canvas.parentElement!.getBoundingClientRect();
|
||||||
|
const diagonal = vec2Distance(min, max);
|
||||||
|
const target = vec2Sub(max, min);
|
||||||
|
const zoomScale = windowHeight / diagonal;
|
||||||
|
|
||||||
|
let scaledPos = vec2MultiplyScalar(min, zoomScale);
|
||||||
|
const scaled = vec2MultiplyScalar(target, zoomScale);
|
||||||
|
const overlap = vec2Sub([windowWidth, windowHeight], scaled);
|
||||||
|
scaledPos = vec2Sub(scaledPos, vec2DivideScalar(overlap, 2));
|
||||||
|
|
||||||
|
return [zoomScale, scaledPos];
|
||||||
|
}
|
||||||
|
|
||||||
private onMouseWheel(e: WheelEvent) {
|
private onMouseWheel(e: WheelEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.realMousePos(e.clientX, e.clientY);
|
this.realMousePos(e.clientX, e.clientY);
|
||||||
|
@ -3,8 +3,17 @@ import { LayerObject } from './interfaces';
|
|||||||
|
|
||||||
export class HousePlannerCanvasClipboard {
|
export class HousePlannerCanvasClipboard {
|
||||||
public storedObjects: LayerObject[] = [];
|
public storedObjects: LayerObject[] = [];
|
||||||
|
public wasCutOperation = false;
|
||||||
|
|
||||||
|
storeToClipboard(items: LayerObject[], cut = false) {
|
||||||
|
this.wasCutOperation = cut;
|
||||||
|
|
||||||
|
// If we are cutting, we store the object by reference and return it all in its original state
|
||||||
|
if (cut) {
|
||||||
|
this.storedObjects = [...items];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
storeToClipboard(items: LayerObject[]) {
|
|
||||||
this.storedObjects = [
|
this.storedObjects = [
|
||||||
...items.map((item) => {
|
...items.map((item) => {
|
||||||
const itemCopy = {
|
const itemCopy = {
|
||||||
@ -19,6 +28,12 @@ export class HousePlannerCanvasClipboard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getFromClipboard(newId: number, selected = true): LayerObject[] {
|
getFromClipboard(newId: number, selected = true): LayerObject[] {
|
||||||
|
if (this.wasCutOperation) {
|
||||||
|
const unalteredList = [...this.storedObjects];
|
||||||
|
this.storeToClipboard(this.storedObjects, false);
|
||||||
|
return unalteredList;
|
||||||
|
}
|
||||||
|
|
||||||
const newObjects = deepUnref(this.storedObjects);
|
const newObjects = deepUnref(this.storedObjects);
|
||||||
return newObjects.map((item, index) => ({
|
return newObjects.map((item, index) => ({
|
||||||
...item,
|
...item,
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { Ref } from 'vue';
|
|
||||||
import { HousePlannerCanvas } from './canvas';
|
import { HousePlannerCanvas } from './canvas';
|
||||||
import { Layer, Vec2 } from './interfaces';
|
import { Layer, Vec2, Vec2Box } from './interfaces';
|
||||||
import { HousePlannerCanvasTools } from './tools';
|
import { HousePlannerCanvasTools } from './tools';
|
||||||
|
|
||||||
export class HousePlanner {
|
export class HousePlanner {
|
||||||
@ -13,8 +12,9 @@ export class HousePlanner {
|
|||||||
canvasDim: Vec2,
|
canvasDim: Vec2,
|
||||||
canvasPos: Vec2,
|
canvasPos: Vec2,
|
||||||
canvasZoom = 1,
|
canvasZoom = 1,
|
||||||
editable = true
|
editable = true,
|
||||||
) {
|
boundingBox?: Vec2Box
|
||||||
|
): [number, Vec2] {
|
||||||
this.canvas = canvas;
|
this.canvas = canvas;
|
||||||
this.manager = new HousePlannerCanvas(
|
this.manager = new HousePlannerCanvas(
|
||||||
canvas,
|
canvas,
|
||||||
@ -35,6 +35,11 @@ export class HousePlanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.manager.draw();
|
this.manager.draw();
|
||||||
|
|
||||||
|
if (boundingBox) {
|
||||||
|
this.manager.initViewport(boundingBox);
|
||||||
|
}
|
||||||
|
return [this.manager.canvasZoom, this.manager.canvasPos];
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanUp() {
|
cleanUp() {
|
||||||
|
@ -2,6 +2,7 @@ import { HousePlannerCanvasHistory } from './history';
|
|||||||
import { LayerObjectType } from './types';
|
import { LayerObjectType } from './types';
|
||||||
|
|
||||||
export type Vec2 = [number, number];
|
export type Vec2 = [number, number];
|
||||||
|
export type Vec2Box = [Vec2, Vec2];
|
||||||
export interface LineSegment {
|
export interface LineSegment {
|
||||||
start?: Vec2;
|
start?: Vec2;
|
||||||
end: Vec2;
|
end: Vec2;
|
||||||
|
@ -297,8 +297,14 @@ export class HousePlannerCanvasTools implements ICanvasToolkit {
|
|||||||
if (e.key === 'x' && e.ctrlKey) {
|
if (e.key === 'x' && e.ctrlKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (this.selectedObjects.length && this.selectedLayer) {
|
if (this.selectedObjects.length && this.selectedLayer) {
|
||||||
this.clipboard.storeToClipboard(this.selectedObjects);
|
this.clipboard.storeToClipboard(this.selectedObjects, true);
|
||||||
this.deleteSelection();
|
this.deleteSelection([
|
||||||
|
{
|
||||||
|
object: this.clipboard,
|
||||||
|
property: 'wasCutOperation',
|
||||||
|
value: false,
|
||||||
|
},
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -361,7 +367,7 @@ export class HousePlannerCanvasTools implements ICanvasToolkit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteSelection() {
|
deleteSelection(additionalHistory?: History<any>[]) {
|
||||||
if (!this.selectedObjects.length || !this.selectedLayer) return;
|
if (!this.selectedObjects.length || !this.selectedLayer) return;
|
||||||
this.history.appendToHistory([
|
this.history.appendToHistory([
|
||||||
{
|
{
|
||||||
@ -369,6 +375,7 @@ export class HousePlannerCanvasTools implements ICanvasToolkit {
|
|||||||
property: 'contents',
|
property: 'contents',
|
||||||
value: [...this.selectedLayer.contents],
|
value: [...this.selectedLayer.contents],
|
||||||
} as History<typeof this.selectedLayer>,
|
} as History<typeof this.selectedLayer>,
|
||||||
|
...(additionalHistory || []),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
this.selectedLayer.contents = this.selectedLayer.contents.filter(
|
this.selectedLayer.contents = this.selectedLayer.contents.filter(
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { clamp } from '@vueuse/shared';
|
import { clamp } from '@vueuse/shared';
|
||||||
import { Vec2 } from './interfaces';
|
import { Vec2, Vec2Box } from './interfaces';
|
||||||
|
|
||||||
export const vec2Length = ([x, y]: Vec2) => Math.abs(Math.sqrt(x * x + y * y));
|
export const vec2Length = ([x, y]: Vec2) => Math.abs(Math.sqrt(x * x + y * y));
|
||||||
|
|
||||||
@ -69,3 +69,34 @@ export const rad2deg = (rad: number) => rad * (180 / Math.PI);
|
|||||||
|
|
||||||
export const randomNumber = (min: number, max: number) =>
|
export const randomNumber = (min: number, max: number) =>
|
||||||
Math.floor(Math.random() * (max - min + 1) + min);
|
Math.floor(Math.random() * (max - min + 1) + min);
|
||||||
|
|
||||||
|
export const boundingBox = (points: Vec2[], start: Vec2Box): Vec2Box => {
|
||||||
|
let minX = start[0][0];
|
||||||
|
let minY = start[0][1];
|
||||||
|
let maxX = start[1][0];
|
||||||
|
let maxY = start[1][1];
|
||||||
|
|
||||||
|
for (let i = 0; i < points.length; i++) {
|
||||||
|
if (points[i][0] > maxX) {
|
||||||
|
maxX = points[i][0];
|
||||||
|
}
|
||||||
|
if (points[i][0] < minX) {
|
||||||
|
minX = points[i][0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (points[i][1] > maxY) {
|
||||||
|
maxY = points[i][1];
|
||||||
|
}
|
||||||
|
if (points[i][1] < minY) {
|
||||||
|
minY = points[i][1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
[minX, minY],
|
||||||
|
[maxX, maxY],
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isValidVec2 = ([x, y]: Vec2) =>
|
||||||
|
x != null && y != null && !isNaN(x) && !isNaN(y);
|
||||||
|
@ -20,9 +20,6 @@ const routes: RouteRecordRaw[] = [
|
|||||||
name: 'planner',
|
name: 'planner',
|
||||||
path: '/planner',
|
path: '/planner',
|
||||||
component: HousePlanner,
|
component: HousePlanner,
|
||||||
meta: {
|
|
||||||
authenticated: false,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ export const useUserStore = defineStore('user', {
|
|||||||
state: () => {
|
state: () => {
|
||||||
return {
|
return {
|
||||||
currentUser: null as User | null,
|
currentUser: null as User | null,
|
||||||
accessToken: useLocalStorage<string>('accessToken', null, {
|
accessToken: useLocalStorage<string | null>('accessToken', null, {
|
||||||
writeDefaults: false,
|
writeDefaults: false,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
@ -21,11 +21,17 @@ export const useUserStore = defineStore('user', {
|
|||||||
actions: {
|
actions: {
|
||||||
loginFromToken() {
|
loginFromToken() {
|
||||||
const token = this.accessToken;
|
const token = this.accessToken;
|
||||||
|
if (!token) return;
|
||||||
try {
|
try {
|
||||||
const decoded = jwtDecode(token) as Record<string, unknown>;
|
const decoded = jwtDecode(token) as Record<string, unknown>;
|
||||||
if (!decoded.sub) {
|
if (!decoded.sub) {
|
||||||
throw new Error('Invalid token');
|
throw new Error('Invalid token');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ((decoded.exp as number) * 1000 < Date.now()) {
|
||||||
|
throw new Error('Expired token');
|
||||||
|
}
|
||||||
|
|
||||||
this.currentUser = {
|
this.currentUser = {
|
||||||
sub: decoded.sub as string,
|
sub: decoded.sub as string,
|
||||||
name: decoded.name as string,
|
name: decoded.name as string,
|
||||||
@ -33,7 +39,8 @@ export const useUserStore = defineStore('user', {
|
|||||||
picture: decoded.picture as string,
|
picture: decoded.picture as string,
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
// TODO
|
this.currentUser = null;
|
||||||
|
this.accessToken = null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async login({ email, password }: { email: string; password: string }) {
|
async login({ email, password }: { email: string; password: string }) {
|
||||||
|
@ -1,47 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative h-full bg-gray-100">
|
<div class="relative h-full bg-gray-100">
|
||||||
<div
|
<PlannerBuildingSelect
|
||||||
class="absolute top-0 left-0 z-10 rounded-br-md border-b-2 border-r-2 border-gray-200 bg-white px-2 py-2 shadow-lg"
|
:buildings="buildings"
|
||||||
>
|
:floors="floors"
|
||||||
<div class="flex flex-row items-center space-x-4 px-4">
|
:status="status"
|
||||||
<div class="flex flex-row items-center space-x-4">
|
:selected-building-id="selectedBuildingId"
|
||||||
<label for="building">Building:</label>
|
:selected-floor-id="selectedFloorId"
|
||||||
<select
|
@update:selected-building-id="(newValue) => buildingSelected(newValue)"
|
||||||
id="building"
|
@update:selected-floor-id="(newValue) => (selectedFloorId = newValue)"
|
||||||
class="rounded-sm border-gray-300 py-1 focus:ring-2 focus:ring-blue-200"
|
/>
|
||||||
v-model="selectedBuildingId"
|
|
||||||
@change="buildingSelected()"
|
|
||||||
>
|
|
||||||
<option v-for="building of buildings" :value="building.id">
|
|
||||||
{{ building.displayName }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="flex flex-row items-center space-x-4"
|
|
||||||
v-if="selectedBuildingId"
|
|
||||||
>
|
|
||||||
<label for="floor">Floor:</label>
|
|
||||||
<select
|
|
||||||
id="floor"
|
|
||||||
class="rounded-sm border-gray-300 py-1 focus:ring-2 focus:ring-blue-200"
|
|
||||||
v-model="selectedFloorId"
|
|
||||||
>
|
|
||||||
<option v-for="floor of floors" :value="floor.id">
|
|
||||||
{{ floor.displayName }} ({{ floor.number }})
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div v-if="selectedFloorId">{{ status }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<HousePlanner
|
<HousePlanner
|
||||||
v-if="selectedFloorId"
|
v-if="selectedFloorId"
|
||||||
editable
|
editable
|
||||||
ref="plannerRef"
|
ref="plannerRef"
|
||||||
:key="`planner-${selectedFloorId}`"
|
:key="`planner-${selectedFloorId}`"
|
||||||
:floor-document="floorPlan"
|
:floor-document="floorPlan"
|
||||||
@update="(layers, rooms) => updateDocument(layers, rooms)"
|
@update="
|
||||||
|
(layers, rooms, boundingBox) =>
|
||||||
|
updateDocument(layers, rooms, boundingBox)
|
||||||
|
"
|
||||||
@edited="status = 'Modified'"
|
@edited="status = 'Modified'"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
@ -61,8 +38,9 @@ import { computed, onMounted, ref } from 'vue';
|
|||||||
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';
|
||||||
|
import PlannerBuildingSelect from '../components/house-planner/PlannerBuildingSelect.vue';
|
||||||
import { UpsertRoomItem } from '../interfaces/room.interfaces';
|
import { UpsertRoomItem } from '../interfaces/room.interfaces';
|
||||||
import { Line } 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();
|
||||||
@ -88,10 +66,11 @@ const floorPlan = computed(
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const buildingSelected = async () => {
|
const buildingSelected = async (id: number) => {
|
||||||
if (selectedBuildingId.value == null) return;
|
if (id == null) return;
|
||||||
selectedFloorId.value = undefined;
|
selectedFloorId.value = undefined;
|
||||||
await building.getFloors(selectedBuildingId.value);
|
selectedBuildingId.value = id;
|
||||||
|
await building.getFloors(id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateRooms = async (data: FloorDocument, rooms: Line[]) => {
|
const updateRooms = async (data: FloorDocument, rooms: Line[]) => {
|
||||||
@ -136,10 +115,15 @@ const updateRooms = async (data: FloorDocument, rooms: Line[]) => {
|
|||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateDocument = async (data: FloorDocument, rooms: Line[]) => {
|
const updateDocument = async (
|
||||||
|
data: FloorDocument,
|
||||||
|
rooms: Line[],
|
||||||
|
boundingBox?: Vec2Box
|
||||||
|
) => {
|
||||||
if (
|
if (
|
||||||
!selectedBuildingId.value ||
|
!selectedBuildingId.value ||
|
||||||
!selectedFloorId.value ||
|
!selectedFloorId.value ||
|
||||||
|
selectedFloorId.value !== data.id ||
|
||||||
!currentFloor.value ||
|
!currentFloor.value ||
|
||||||
status.value === 'Saving...'
|
status.value === 'Saving...'
|
||||||
)
|
)
|
||||||
@ -150,7 +134,7 @@ const updateDocument = async (data: FloorDocument, rooms: Line[]) => {
|
|||||||
data = await updateRooms(data, rooms);
|
data = await updateRooms(data, rooms);
|
||||||
|
|
||||||
// Prevent useless requests
|
// Prevent useless requests
|
||||||
const floorPlan = JSON.stringify(data);
|
const floorPlan = JSON.stringify({ ...data, boundingBox });
|
||||||
if (currentFloor.value.plan === floorPlan) {
|
if (currentFloor.value.plan === floorPlan) {
|
||||||
status.value = 'Saved!';
|
status.value = 'Saved!';
|
||||||
return;
|
return;
|
||||||
|
Loading…
Reference in New Issue
Block a user