diff --git a/src/components/house-planner/HousePlanner.vue b/src/components/house-planner/HousePlanner.vue
index 3cc1783..d65af18 100644
--- a/src/components/house-planner/HousePlanner.vue
+++ b/src/components/house-planner/HousePlanner.vue
@@ -70,6 +70,7 @@ import {
RepositionEvent,
ToolEvent,
Vec2,
+ Vec2Box,
} from '../../modules/house-planner/interfaces';
import type { HousePlannerCanvasTools } from '../../modules/house-planner/tools';
import deepUnref from '../../utils/deep-unref';
@@ -98,16 +99,23 @@ const props = withDefaults(
);
const emit = defineEmits<{
- (e: 'update', document: FloorDocument, rooms: Line[]): void;
+ (
+ e: 'update',
+ document: FloorDocument,
+ rooms: Line[],
+ boundingBox?: Vec2Box
+ ): void;
(e: 'edited'): void;
}>();
const localFloorDocument = ref(deepUnref(props.floorDocument));
const emitUpdate = () =>
+ module.value.manager &&
emit(
'update',
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);
@@ -241,14 +249,18 @@ defineExpose({
});
onMounted(() => {
- module.value.initialize(
+ const [setZoom, setPos] = module.value.initialize(
canvas.value,
deepUnref(localFloorDocument.value.layers),
[localFloorDocument.value.width, localFloorDocument.value.height],
canvasPos.value,
canvasZoom.value,
- props.editable
+ props.editable,
+ localFloorDocument.value.boundingBox
);
+ canvasPos.value = setPos;
+ canvasZoom.value = setZoom;
+
Object.keys(events).forEach((event) =>
canvas.value.addEventListener(event, events[event])
);
diff --git a/src/components/house-planner/PlannerBuildingSelect.vue b/src/components/house-planner/PlannerBuildingSelect.vue
new file mode 100644
index 0000000..a71af9c
--- /dev/null
+++ b/src/components/house-planner/PlannerBuildingSelect.vue
@@ -0,0 +1,104 @@
+
+
+
+
+
+
+
Go back
+
+
+
+
+
+
+
+
+
{{ status }}
+
+
+
+
+
+
+
+
diff --git a/src/components/house-planner/PlannerLayerPanel.vue b/src/components/house-planner/PlannerLayerPanel.vue
index f47d176..c3e3159 100644
--- a/src/components/house-planner/PlannerLayerPanel.vue
+++ b/src/components/house-planner/PlannerLayerPanel.vue
@@ -10,7 +10,7 @@
]"
>
- {{ layer.name }}
+ {{ layer.name }} ({{ layer.contents.length }})
diff --git a/src/components/house-planner/PlannerSidebar.vue b/src/components/house-planner/PlannerSidebar.vue
index 5c44cb7..239037e 100644
--- a/src/components/house-planner/PlannerSidebar.vue
+++ b/src/components/house-planner/PlannerSidebar.vue
@@ -1,12 +1,15 @@
-
+
diff --git a/src/components/house-planner/PlannerSidebars.vue b/src/components/house-planner/PlannerSidebars.vue
index a3c8eca..29e4de1 100644
--- a/src/components/house-planner/PlannerSidebars.vue
+++ b/src/components/house-planner/PlannerSidebars.vue
@@ -1,5 +1,7 @@
-
+
diff --git a/src/components/house-planner/interfaces/floor-document.interface.ts b/src/components/house-planner/interfaces/floor-document.interface.ts
index 8eb996b..f479283 100644
--- a/src/components/house-planner/interfaces/floor-document.interface.ts
+++ b/src/components/house-planner/interfaces/floor-document.interface.ts
@@ -1,4 +1,4 @@
-import { Layer } from '../../../modules/house-planner/interfaces';
+import { Layer, Vec2 } from '../../../modules/house-planner/interfaces';
export interface FloorDocument {
id: number;
@@ -6,4 +6,8 @@ export interface FloorDocument {
width: number;
height: number;
layers: Layer[];
+ /**
+ * Min, Max
+ */
+ boundingBox?: [Vec2, Vec2];
}
diff --git a/src/modules/house-planner/canvas.ts b/src/modules/house-planner/canvas.ts
index 0fdb538..26f273f 100644
--- a/src/modules/house-planner/canvas.ts
+++ b/src/modules/house-planner/canvas.ts
@@ -10,10 +10,13 @@ import {
LineSegment,
RepositionEvent,
Vec2,
+ Vec2Box,
} from './interfaces';
import { HousePlannerCanvasTools } from './tools';
import { LayerObjectType } from './types';
import {
+ boundingBox,
+ isValidVec2,
vec2Add,
vec2AngleFromOrigin,
vec2Distance,
@@ -270,6 +273,42 @@ export class HousePlannerCanvas {
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
(
+ (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) {
if (e.target !== document.body && e.target != null) return;
this.tools?.onKeyDown(e);
@@ -386,7 +425,9 @@ export class HousePlannerCanvas {
onTouchStart(e: TouchEvent) {
e.preventDefault();
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;
if (e.touches.length === 2) {
@@ -410,6 +451,41 @@ export class HousePlannerCanvas {
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('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) {
e.preventDefault();
this.realMousePos(e.clientX, e.clientY);
diff --git a/src/modules/house-planner/clipboard.ts b/src/modules/house-planner/clipboard.ts
index 1d91826..2a365ce 100644
--- a/src/modules/house-planner/clipboard.ts
+++ b/src/modules/house-planner/clipboard.ts
@@ -3,8 +3,17 @@ import { LayerObject } from './interfaces';
export class HousePlannerCanvasClipboard {
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 = [
...items.map((item) => {
const itemCopy = {
@@ -19,6 +28,12 @@ export class HousePlannerCanvasClipboard {
}
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);
return newObjects.map((item, index) => ({
...item,
diff --git a/src/modules/house-planner/index.ts b/src/modules/house-planner/index.ts
index 1f75a60..6ef7d32 100644
--- a/src/modules/house-planner/index.ts
+++ b/src/modules/house-planner/index.ts
@@ -1,6 +1,5 @@
-import { Ref } from 'vue';
import { HousePlannerCanvas } from './canvas';
-import { Layer, Vec2 } from './interfaces';
+import { Layer, Vec2, Vec2Box } from './interfaces';
import { HousePlannerCanvasTools } from './tools';
export class HousePlanner {
@@ -13,8 +12,9 @@ export class HousePlanner {
canvasDim: Vec2,
canvasPos: Vec2,
canvasZoom = 1,
- editable = true
- ) {
+ editable = true,
+ boundingBox?: Vec2Box
+ ): [number, Vec2] {
this.canvas = canvas;
this.manager = new HousePlannerCanvas(
canvas,
@@ -35,6 +35,11 @@ export class HousePlanner {
}
this.manager.draw();
+
+ if (boundingBox) {
+ this.manager.initViewport(boundingBox);
+ }
+ return [this.manager.canvasZoom, this.manager.canvasPos];
}
cleanUp() {
diff --git a/src/modules/house-planner/interfaces.ts b/src/modules/house-planner/interfaces.ts
index 38718a5..2774a1d 100644
--- a/src/modules/house-planner/interfaces.ts
+++ b/src/modules/house-planner/interfaces.ts
@@ -2,6 +2,7 @@ import { HousePlannerCanvasHistory } from './history';
import { LayerObjectType } from './types';
export type Vec2 = [number, number];
+export type Vec2Box = [Vec2, Vec2];
export interface LineSegment {
start?: Vec2;
end: Vec2;
diff --git a/src/modules/house-planner/tools.ts b/src/modules/house-planner/tools.ts
index ed2a80f..e10bee6 100644
--- a/src/modules/house-planner/tools.ts
+++ b/src/modules/house-planner/tools.ts
@@ -297,8 +297,14 @@ export class HousePlannerCanvasTools implements ICanvasToolkit {
if (e.key === 'x' && e.ctrlKey) {
e.preventDefault();
if (this.selectedObjects.length && this.selectedLayer) {
- this.clipboard.storeToClipboard(this.selectedObjects);
- this.deleteSelection();
+ this.clipboard.storeToClipboard(this.selectedObjects, true);
+ this.deleteSelection([
+ {
+ object: this.clipboard,
+ property: 'wasCutOperation',
+ value: false,
+ },
+ ]);
}
}
@@ -361,7 +367,7 @@ export class HousePlannerCanvasTools implements ICanvasToolkit {
}
}
- deleteSelection() {
+ deleteSelection(additionalHistory?: History[]) {
if (!this.selectedObjects.length || !this.selectedLayer) return;
this.history.appendToHistory([
{
@@ -369,6 +375,7 @@ export class HousePlannerCanvasTools implements ICanvasToolkit {
property: 'contents',
value: [...this.selectedLayer.contents],
} as History,
+ ...(additionalHistory || []),
]);
this.selectedLayer.contents = this.selectedLayer.contents.filter(
diff --git a/src/modules/house-planner/utils.ts b/src/modules/house-planner/utils.ts
index 44c8870..4a67d47 100644
--- a/src/modules/house-planner/utils.ts
+++ b/src/modules/house-planner/utils.ts
@@ -1,5 +1,5 @@
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));
@@ -69,3 +69,34 @@ export const rad2deg = (rad: number) => rad * (180 / Math.PI);
export const randomNumber = (min: number, max: number) =>
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);
diff --git a/src/router/index.ts b/src/router/index.ts
index 2c152a6..24fbc65 100644
--- a/src/router/index.ts
+++ b/src/router/index.ts
@@ -20,9 +20,6 @@ const routes: RouteRecordRaw[] = [
name: 'planner',
path: '/planner',
component: HousePlanner,
- meta: {
- authenticated: false,
- },
},
];
diff --git a/src/store/user.store.ts b/src/store/user.store.ts
index 3413434..1c06e21 100644
--- a/src/store/user.store.ts
+++ b/src/store/user.store.ts
@@ -9,7 +9,7 @@ export const useUserStore = defineStore('user', {
state: () => {
return {
currentUser: null as User | null,
- accessToken: useLocalStorage('accessToken', null, {
+ accessToken: useLocalStorage('accessToken', null, {
writeDefaults: false,
}),
};
@@ -21,11 +21,17 @@ export const useUserStore = defineStore('user', {
actions: {
loginFromToken() {
const token = this.accessToken;
+ if (!token) return;
try {
const decoded = jwtDecode(token) as Record;
if (!decoded.sub) {
throw new Error('Invalid token');
}
+
+ if ((decoded.exp as number) * 1000 < Date.now()) {
+ throw new Error('Expired token');
+ }
+
this.currentUser = {
sub: decoded.sub as string,
name: decoded.name as string,
@@ -33,7 +39,8 @@ export const useUserStore = defineStore('user', {
picture: decoded.picture as string,
};
} catch {
- // TODO
+ this.currentUser = null;
+ this.accessToken = null;
}
},
async login({ email, password }: { email: string; password: string }) {
diff --git a/src/views/HousePlanner.vue b/src/views/HousePlanner.vue
index cb5f963..4ccb062 100644
--- a/src/views/HousePlanner.vue
+++ b/src/views/HousePlanner.vue
@@ -1,47 +1,24 @@
-
-
-
-
-
-
-
-
-
-
-
{{ status }}
-
-
+
buildingSelected(newValue)"
+ @update:selected-floor-id="(newValue) => (selectedFloorId = newValue)"
+ />
updateDocument(layers, rooms)"
+ @update="
+ (layers, rooms, boundingBox) =>
+ updateDocument(layers, rooms, boundingBox)
+ "
@edited="status = 'Modified'"
/>
{
- if (selectedBuildingId.value == null) return;
+const buildingSelected = async (id: number) => {
+ if (id == null) return;
selectedFloorId.value = undefined;
- await building.getFloors(selectedBuildingId.value);
+ selectedBuildingId.value = id;
+ await building.getFloors(id);
};
const updateRooms = async (data: FloorDocument, rooms: Line[]) => {
@@ -136,10 +115,15 @@ const updateRooms = async (data: FloorDocument, rooms: Line[]) => {
return data;
};
-const updateDocument = async (data: FloorDocument, rooms: Line[]) => {
+const updateDocument = async (
+ data: FloorDocument,
+ rooms: Line[],
+ boundingBox?: Vec2Box
+) => {
if (
!selectedBuildingId.value ||
!selectedFloorId.value ||
+ selectedFloorId.value !== data.id ||
!currentFloor.value ||
status.value === 'Saving...'
)
@@ -150,7 +134,7 @@ const updateDocument = async (data: FloorDocument, rooms: Line[]) => {
data = await updateRooms(data, rooms);
// Prevent useless requests
- const floorPlan = JSON.stringify(data);
+ const floorPlan = JSON.stringify({ ...data, boundingBox });
if (currentFloor.value.plan === floorPlan) {
status.value = 'Saved!';
return;