more progress, clipboard
This commit is contained in:
parent
ccfe07a0b8
commit
2dc4256f0f
@ -12,7 +12,7 @@
|
|||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<PlannerToolbar>
|
<PlannerToolbar v-if="editable">
|
||||||
<PlannerTool
|
<PlannerTool
|
||||||
v-for="toolItem of toolbar"
|
v-for="toolItem of toolbar"
|
||||||
:title="toolItem.title"
|
:title="toolItem.title"
|
||||||
@ -36,7 +36,7 @@
|
|||||||
</PlannerTool>
|
</PlannerTool>
|
||||||
</PlannerToolbar>
|
</PlannerToolbar>
|
||||||
|
|
||||||
<PlannerSidebars>
|
<PlannerSidebars v-if="editable">
|
||||||
<PlannerLayerPanel
|
<PlannerLayerPanel
|
||||||
:layers="localFloorDocument.layers"
|
:layers="localFloorDocument.layers"
|
||||||
@layer-name="commitLayerName"
|
@layer-name="commitLayerName"
|
||||||
@ -66,6 +66,7 @@ import { useDebounceFn } from '@vueuse/shared';
|
|||||||
import { onBeforeUnmount, onMounted, ref, shallowRef } from 'vue';
|
import { onBeforeUnmount, onMounted, ref, shallowRef } from 'vue';
|
||||||
import { HousePlanner } from '../../modules/house-planner';
|
import { HousePlanner } from '../../modules/house-planner';
|
||||||
import {
|
import {
|
||||||
|
Line,
|
||||||
RepositionEvent,
|
RepositionEvent,
|
||||||
ToolEvent,
|
ToolEvent,
|
||||||
Vec2,
|
Vec2,
|
||||||
@ -88,20 +89,27 @@ const canvasDim = ref<Vec2>([1920, 1080]);
|
|||||||
const canvasPos = ref<Vec2>([0, 0]);
|
const canvasPos = ref<Vec2>([0, 0]);
|
||||||
const canvasZoom = ref<number>(1);
|
const canvasZoom = ref<number>(1);
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = withDefaults(
|
||||||
floorDocument: FloorDocument;
|
defineProps<{
|
||||||
}>();
|
floorDocument: FloorDocument;
|
||||||
|
editable: boolean;
|
||||||
|
}>(),
|
||||||
|
{ editable: true }
|
||||||
|
);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'update', document: FloorDocument): void;
|
(e: 'update', document: FloorDocument, rooms: Line[]): void;
|
||||||
(e: 'edited'): void;
|
(e: 'edited'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const localFloorDocument = ref(deepUnref(props.floorDocument));
|
const localFloorDocument = ref(deepUnref(props.floorDocument));
|
||||||
const debouncedUpdate = useDebounceFn(
|
const emitUpdate = () =>
|
||||||
() => emit('update', localFloorDocument.value),
|
emit(
|
||||||
5000
|
'update',
|
||||||
);
|
localFloorDocument.value,
|
||||||
|
(module.value.manager?.getAllObjectsOfType('room') || []) as Line[]
|
||||||
|
);
|
||||||
|
const debouncedUpdate = useDebounceFn(emitUpdate, 10000);
|
||||||
|
|
||||||
const toolbar: ToolbarTool[] = [
|
const toolbar: ToolbarTool[] = [
|
||||||
{
|
{
|
||||||
@ -195,14 +203,18 @@ const updateObjectProperty = (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateLocalDocument = () => {
|
||||||
|
localFloorDocument.value.layers = deepUnref(module.value.manager!.layers);
|
||||||
|
};
|
||||||
|
|
||||||
const events: Record<string, (e: CustomEvent) => void> = {
|
const events: Record<string, (e: CustomEvent) => void> = {
|
||||||
'hpc:update': (e: CustomEvent) => {
|
'hpc:update': (e: CustomEvent) => {
|
||||||
localFloorDocument.value.layers = deepUnref(module.value.manager!.layers);
|
updateLocalDocument();
|
||||||
emit('edited');
|
emit('edited');
|
||||||
debouncedUpdate();
|
debouncedUpdate();
|
||||||
},
|
},
|
||||||
'hpc:selectionchange': (e: CustomEvent) => {
|
'hpc:selectionchange': (e: CustomEvent) => {
|
||||||
localFloorDocument.value.layers = deepUnref(module.value.manager!.layers);
|
updateLocalDocument();
|
||||||
emit('edited');
|
emit('edited');
|
||||||
debouncedUpdate();
|
debouncedUpdate();
|
||||||
},
|
},
|
||||||
@ -214,15 +226,28 @@ const events: Record<string, (e: CustomEvent) => void> = {
|
|||||||
canvasPos.value = e.detail.position;
|
canvasPos.value = e.detail.position;
|
||||||
canvasZoom.value = e.detail.zoom;
|
canvasZoom.value = e.detail.zoom;
|
||||||
},
|
},
|
||||||
|
'hpc:save': (e: CustomEvent) => {
|
||||||
|
updateLocalDocument();
|
||||||
|
emit('edited');
|
||||||
|
emitUpdate();
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
updateLocalDocument() {
|
||||||
|
updateLocalDocument();
|
||||||
|
return localFloorDocument.value;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
module.value.initialize(
|
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
|
||||||
);
|
);
|
||||||
Object.keys(events).forEach((event) =>
|
Object.keys(events).forEach((event) =>
|
||||||
canvas.value.addEventListener(event, events[event])
|
canvas.value.addEventListener(event, events[event])
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
const env = import.meta.env;
|
const env = import.meta.env;
|
||||||
|
|
||||||
export const BACKEND_URL =
|
export const BACKEND_URL =
|
||||||
(env.BACKEND_URL as string) || 'http://localhost:3000';
|
(env.VITE_BACKEND_URL as string) || 'http://localhost:3000';
|
||||||
|
@ -1,4 +1,12 @@
|
|||||||
export interface RoomListItem {
|
export interface RoomListItem {
|
||||||
id: number;
|
id: number;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
|
plan: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpsertRoomItem {
|
||||||
|
id?: number;
|
||||||
|
canvasObjectId?: number;
|
||||||
|
displayName: string;
|
||||||
|
plan: string;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { clamp } from '@vueuse/core';
|
import { clamp } from '@vueuse/core';
|
||||||
|
import extractLinePoints from '../../utils/extract-line-points';
|
||||||
import { HousePlannerCanvasGrid } from './grid';
|
import { HousePlannerCanvasGrid } from './grid';
|
||||||
import {
|
import {
|
||||||
BezierSegment,
|
BezierSegment,
|
||||||
@ -11,6 +12,7 @@ import {
|
|||||||
Vec2,
|
Vec2,
|
||||||
} from './interfaces';
|
} from './interfaces';
|
||||||
import { HousePlannerCanvasTools } from './tools';
|
import { HousePlannerCanvasTools } from './tools';
|
||||||
|
import { LayerObjectType } from './types';
|
||||||
import {
|
import {
|
||||||
vec2Add,
|
vec2Add,
|
||||||
vec2AngleFromOrigin,
|
vec2AngleFromOrigin,
|
||||||
@ -260,6 +262,14 @@ export class HousePlannerCanvas {
|
|||||||
: this.mousePosition;
|
: this.mousePosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAllObjectsOfType(type: LayerObjectType) {
|
||||||
|
const objects: LayerObject[] = [];
|
||||||
|
this.layers.forEach((layer) => {
|
||||||
|
objects.push(...layer.contents.filter((object) => object.type === type));
|
||||||
|
});
|
||||||
|
return objects;
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
||||||
@ -288,15 +298,7 @@ export class HousePlannerCanvas {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private drawRoomText(line: Line) {
|
private drawRoomText(line: Line) {
|
||||||
const points = line.segments
|
const points = extractLinePoints(line);
|
||||||
.reduce<Vec2[]>((list, segment) => {
|
|
||||||
if (segment.start) return [...list, segment.start, segment.end];
|
|
||||||
return [...list, segment.end];
|
|
||||||
}, [])
|
|
||||||
.filter(
|
|
||||||
(vec, index, arry) =>
|
|
||||||
arry.findIndex((point) => vec2Equals(point, vec)) === index
|
|
||||||
);
|
|
||||||
const centerPoint = vec2DivideScalar(
|
const centerPoint = vec2DivideScalar(
|
||||||
points.reduce<Vec2 | null>(
|
points.reduce<Vec2 | null>(
|
||||||
(prev, current) => (prev ? vec2Add(prev, current) : current),
|
(prev, current) => (prev ? vec2Add(prev, current) : current),
|
||||||
@ -458,9 +460,14 @@ export class HousePlannerCanvas {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private pointerDown() {
|
private pointerDown() {
|
||||||
if (!this.tools) return;
|
if (!this.tools) {
|
||||||
|
this.dragging = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.clickedOn = this.tools.getMousedObject();
|
this.clickedOn = this.tools.getMousedObject();
|
||||||
this.tools.mouseDown(this.clickedOn || undefined);
|
this.tools.mouseDown(this.clickedOn || undefined);
|
||||||
|
|
||||||
if (!this.clickedOn) {
|
if (!this.clickedOn) {
|
||||||
this.dragging = true;
|
this.dragging = true;
|
||||||
}
|
}
|
||||||
|
29
src/modules/house-planner/clipboard.ts
Normal file
29
src/modules/house-planner/clipboard.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import deepUnref from '../../utils/deep-unref';
|
||||||
|
import { LayerObject } from './interfaces';
|
||||||
|
|
||||||
|
export class HousePlannerCanvasClipboard {
|
||||||
|
public storedObjects: LayerObject[] = [];
|
||||||
|
|
||||||
|
storeToClipboard(items: LayerObject[]) {
|
||||||
|
this.storedObjects = [
|
||||||
|
...items.map((item) => {
|
||||||
|
const itemCopy = {
|
||||||
|
...deepUnref(item),
|
||||||
|
visible: true,
|
||||||
|
name: item.name + ' Copy',
|
||||||
|
};
|
||||||
|
delete itemCopy.databaseId;
|
||||||
|
return itemCopy;
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
getFromClipboard(newId: number, selected = true): LayerObject[] {
|
||||||
|
const newObjects = deepUnref(this.storedObjects);
|
||||||
|
return newObjects.map((item, index) => ({
|
||||||
|
...item,
|
||||||
|
id: newId + index,
|
||||||
|
selected,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
import type { HousePlannerCanvas } from './canvas';
|
import type { HousePlannerCanvas } from './canvas';
|
||||||
|
import { HousePlannerCanvasClipboard } from './clipboard';
|
||||||
import { HousePlannerCanvasHistory } from './history';
|
import { HousePlannerCanvasHistory } from './history';
|
||||||
import {
|
import {
|
||||||
History,
|
History,
|
||||||
@ -27,6 +28,7 @@ export class HousePlannerCanvasTools implements ICanvasToolkit {
|
|||||||
['cut']: new CutTool(this),
|
['cut']: new CutTool(this),
|
||||||
};
|
};
|
||||||
public history = new HousePlannerCanvasHistory();
|
public history = new HousePlannerCanvasHistory();
|
||||||
|
public clipboard = new HousePlannerCanvasClipboard();
|
||||||
public lastStrokeWidth = 16;
|
public lastStrokeWidth = 16;
|
||||||
public lastColor = '#000000';
|
public lastColor = '#000000';
|
||||||
public holdShift = false;
|
public holdShift = false;
|
||||||
@ -243,6 +245,10 @@ export class HousePlannerCanvasTools implements ICanvasToolkit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onKeyDown(e: KeyboardEvent) {
|
onKeyDown(e: KeyboardEvent) {
|
||||||
|
if (e.key === 'Shift') {
|
||||||
|
this.holdShift = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (e.key === 'z' && e.ctrlKey) {
|
if (e.key === 'z' && e.ctrlKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.history.undo();
|
this.history.undo();
|
||||||
@ -277,8 +283,48 @@ export class HousePlannerCanvasTools implements ICanvasToolkit {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.key === 'Shift') {
|
if (e.key === 'a' && e.ctrlKey) {
|
||||||
this.holdShift = true;
|
e.preventDefault();
|
||||||
|
this.selectAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === 'c' && e.ctrlKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (this.selectedObjects.length)
|
||||||
|
this.clipboard.storeToClipboard(this.selectedObjects);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === 'x' && e.ctrlKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (this.selectedObjects.length && this.selectedLayer) {
|
||||||
|
this.clipboard.storeToClipboard(this.selectedObjects);
|
||||||
|
this.deleteSelection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === 'v' && e.ctrlKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!this.selectedLayer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = this.clipboard.getFromClipboard(
|
||||||
|
this.getSequentialId(),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
if (items.length) {
|
||||||
|
this.selectedLayer?.contents.push(...items);
|
||||||
|
this.selectObject(null);
|
||||||
|
this.selectedObjects = items;
|
||||||
|
this.tool?.selectionChanged(this.selectedObjects);
|
||||||
|
this.manager.draw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === 's' && e.ctrlKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.canvas.dispatchEvent(new CustomEvent('hpc:save'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -309,7 +355,7 @@ export class HousePlannerCanvasTools implements ICanvasToolkit {
|
|||||||
this.setTool('line');
|
this.setTool('line');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.key === 'Delete' || e.key === 'x') {
|
if (e.key === 'Delete') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.deleteSelection();
|
this.deleteSelection();
|
||||||
}
|
}
|
||||||
@ -340,6 +386,48 @@ export class HousePlannerCanvasTools implements ICanvasToolkit {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
selectAll() {
|
||||||
|
if (!this.selectedLayer) return;
|
||||||
|
if (
|
||||||
|
this.selectedLayer.contents
|
||||||
|
.filter((item) => item.visible)
|
||||||
|
.every((item) => item.selected)
|
||||||
|
) {
|
||||||
|
for (const item of this.selectedLayer.contents) {
|
||||||
|
item.selected = false;
|
||||||
|
}
|
||||||
|
this.selectedObjects.length = 0;
|
||||||
|
this.selectedEvent();
|
||||||
|
this.tool?.selectionChanged([]);
|
||||||
|
this.manager.draw();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const visibleItems = this.selectedLayer.contents.filter(
|
||||||
|
(item) => item.visible
|
||||||
|
);
|
||||||
|
for (const item of visibleItems) {
|
||||||
|
item.selected = true;
|
||||||
|
}
|
||||||
|
this.selectedObjects = [...visibleItems];
|
||||||
|
this.tool?.selectionChanged(this.selectedObjects);
|
||||||
|
this.selectedEvent();
|
||||||
|
this.manager.draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
getSequentialId() {
|
||||||
|
return (
|
||||||
|
this.manager.layers.reduce((total, current) => {
|
||||||
|
const biggestInLayer = current.contents.reduce<number>(
|
||||||
|
(biggest2, current2) =>
|
||||||
|
biggest2 > current2.id ? biggest2 : current2.id,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
return total > biggestInLayer ? total : biggestInLayer;
|
||||||
|
}, 0) + 1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private selectedEvent() {
|
private selectedEvent() {
|
||||||
this.canvas.dispatchEvent(
|
this.canvas.dispatchEvent(
|
||||||
new CustomEvent('hpc:selectionchange', {
|
new CustomEvent('hpc:selectionchange', {
|
||||||
|
@ -91,19 +91,6 @@ export class LineTool extends CanvasToolBase<LineToolType> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getSequentialId() {
|
|
||||||
return (
|
|
||||||
this.renderer.layers.reduce(
|
|
||||||
(total, current) =>
|
|
||||||
current.contents.reduce(
|
|
||||||
(total2, current2) => current2.id + total2,
|
|
||||||
0
|
|
||||||
) + total,
|
|
||||||
0
|
|
||||||
) + 1
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private startLine() {
|
private startLine() {
|
||||||
if (!this.layer?.visible) return;
|
if (!this.layer?.visible) return;
|
||||||
if (this.drawingLine) {
|
if (this.drawingLine) {
|
||||||
@ -159,7 +146,7 @@ export class LineTool extends CanvasToolBase<LineToolType> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const newLineObject: Line = {
|
const newLineObject: Line = {
|
||||||
id: this.getSequentialId(),
|
id: this.manager.getSequentialId(),
|
||||||
name: this.defaultName,
|
name: this.defaultName,
|
||||||
type: this.subTool! as LayerObjectType,
|
type: this.subTool! as LayerObjectType,
|
||||||
visible: true,
|
visible: true,
|
||||||
|
@ -1,9 +1,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 } from '../interfaces/building.interfaces';
|
||||||
import { FloorListItem } from '../interfaces/floor.interfaces';
|
import { FloorListItem } from '../interfaces/floor.interfaces';
|
||||||
import { RoomListItem } from '../interfaces/room.interfaces';
|
import { RoomListItem, UpsertRoomItem } from '../interfaces/room.interfaces';
|
||||||
import jfetch from '../utils/jfetch';
|
import jfetch from '../utils/jfetch';
|
||||||
|
|
||||||
const { authHeader } = useAccessToken();
|
const { authHeader } = useAccessToken();
|
||||||
@ -35,20 +36,105 @@ export const useBuildingStore = defineStore('building', {
|
|||||||
number: number,
|
number: number,
|
||||||
floor: Partial<FloorListItem>
|
floor: Partial<FloorListItem>
|
||||||
) {
|
) {
|
||||||
await jfetch(`${BACKEND_URL}/buildings/${building}/floors/${number}`, {
|
const { data: saveData } = await jfetch<FloorListItem>(
|
||||||
method: 'PATCH',
|
`${BACKEND_URL}/buildings/${building}/floors/${number}`,
|
||||||
headers: {
|
{
|
||||||
...authHeader.value,
|
method: 'PATCH',
|
||||||
'Content-Type': 'application/json',
|
headers: authHeader.value,
|
||||||
},
|
body: floor,
|
||||||
body: JSON.stringify(floor),
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
|
// Update local state
|
||||||
|
const currentFloor = this.floors.find(
|
||||||
|
(existing) => existing.number === number
|
||||||
|
);
|
||||||
|
if (currentFloor) {
|
||||||
|
Object.assign(currentFloor, saveData);
|
||||||
|
}
|
||||||
|
|
||||||
|
return saveData;
|
||||||
|
},
|
||||||
|
async createRoom(
|
||||||
|
building: number,
|
||||||
|
floor: number,
|
||||||
|
room: UpsertRoomItem
|
||||||
|
): Promise<UpsertRoomItem> {
|
||||||
|
const canvasId = room.canvasObjectId;
|
||||||
|
const { data: returned } = await jfetch<RoomListItem>(
|
||||||
|
`${BACKEND_URL}/buildings/${building}/floors/${floor}/rooms`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: authHeader.value,
|
||||||
|
body: omit(room, 'canvasObjectId'),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return { ...returned, canvasObjectId: canvasId };
|
||||||
|
},
|
||||||
|
async updateRoom(
|
||||||
|
building: number,
|
||||||
|
room: UpsertRoomItem
|
||||||
|
): Promise<UpsertRoomItem> {
|
||||||
|
const canvasId = room.canvasObjectId;
|
||||||
|
const roomId = room.id;
|
||||||
|
const { data: returned } = await jfetch<RoomListItem>(
|
||||||
|
`${BACKEND_URL}/buildings/${building}/rooms/${roomId}`,
|
||||||
|
{
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: authHeader.value,
|
||||||
|
body: omit(room, ['canvasObjectId', 'id']),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return { ...returned, canvasObjectId: canvasId };
|
||||||
|
},
|
||||||
|
async deleteRoom(building: number, roomId: number) {
|
||||||
|
await jfetch<RoomListItem>(
|
||||||
|
`${BACKEND_URL}/buildings/${building}/rooms/${roomId}`,
|
||||||
|
{
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: authHeader.value,
|
||||||
|
}
|
||||||
|
);
|
||||||
},
|
},
|
||||||
async upsertFloorRooms(
|
async upsertFloorRooms(
|
||||||
building: number,
|
building: number,
|
||||||
floorNo: number,
|
floorNo: number,
|
||||||
rooms: RoomListItem[],
|
rooms: UpsertRoomItem[]
|
||||||
removedRooms: number[]
|
) {
|
||||||
) {},
|
const currentBuilding = this.buildings.find(({ id }) => building === id);
|
||||||
|
const currentFloor = this.floors.find(({ number }) => number === floorNo);
|
||||||
|
if (!currentBuilding || !currentFloor) return;
|
||||||
|
|
||||||
|
// Gather data
|
||||||
|
const roomsToCreate = rooms.filter(
|
||||||
|
(room) => room.id === undefined && room.displayName && room.plan
|
||||||
|
);
|
||||||
|
const roomsToUpdate = rooms.filter((room) => {
|
||||||
|
if (!room.id) return false;
|
||||||
|
const exists = currentFloor.rooms.find(
|
||||||
|
(existing) => existing.id === room.id
|
||||||
|
);
|
||||||
|
if (!exists) return false;
|
||||||
|
return (
|
||||||
|
exists.displayName !== room.displayName || exists.plan !== room.plan
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const roomsToDelete = currentFloor.rooms.filter(
|
||||||
|
(room) => !rooms.find((provided) => provided.id === room.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Do requests
|
||||||
|
const createdRooms = await Promise.all(
|
||||||
|
roomsToCreate.map((room) => this.createRoom(building, floorNo, room))
|
||||||
|
);
|
||||||
|
await Promise.allSettled(
|
||||||
|
roomsToUpdate.map((room) => this.updateRoom(building, room))
|
||||||
|
);
|
||||||
|
await Promise.allSettled(
|
||||||
|
roomsToDelete.map((room) => this.deleteRoom(building, room.id))
|
||||||
|
);
|
||||||
|
|
||||||
|
return createdRooms;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
14
src/utils/extract-line-points.ts
Normal file
14
src/utils/extract-line-points.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { Line, Vec2 } from '../modules/house-planner/interfaces';
|
||||||
|
import { vec2Equals } from '../modules/house-planner/utils';
|
||||||
|
|
||||||
|
export default function extractLinePoints(line: Line) {
|
||||||
|
return line.segments
|
||||||
|
.reduce<Vec2[]>((list, segment) => {
|
||||||
|
if (segment.start) return [...list, segment.start, segment.end];
|
||||||
|
return [...list, segment.end];
|
||||||
|
}, [])
|
||||||
|
.filter(
|
||||||
|
(vec, index, arry) =>
|
||||||
|
arry.findIndex((point) => vec2Equals(point, vec)) === index
|
||||||
|
);
|
||||||
|
}
|
@ -21,6 +21,14 @@ export default async function jfetch<T = any>(
|
|||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const { returnType } = opts;
|
const { returnType } = opts;
|
||||||
|
if (opts['body'] && typeof opts['body'] === 'object') {
|
||||||
|
opts['body'] = JSON.stringify(opts['body']);
|
||||||
|
opts['headers'] = {
|
||||||
|
...(opts['headers'] || {}),
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
...(opts as Record<string, unknown>),
|
...(opts as Record<string, unknown>),
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative h-full">
|
<div class="relative h-full bg-gray-100">
|
||||||
<div
|
<div
|
||||||
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"
|
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"
|
||||||
>
|
>
|
||||||
@ -37,10 +37,21 @@
|
|||||||
</div>
|
</div>
|
||||||
<HousePlanner
|
<HousePlanner
|
||||||
v-if="selectedFloorId"
|
v-if="selectedFloorId"
|
||||||
|
editable
|
||||||
|
ref="plannerRef"
|
||||||
|
:key="`planner-${selectedFloorId}`"
|
||||||
:floor-document="floorPlan"
|
:floor-document="floorPlan"
|
||||||
@update="($newValue) => updateDocument($newValue)"
|
@update="(layers, rooms) => updateDocument(layers, rooms)"
|
||||||
@edited="status = 'Modified'"
|
@edited="status = 'Modified'"
|
||||||
/>
|
/>
|
||||||
|
<div
|
||||||
|
class="flex h-full w-full select-none items-center justify-center text-lg"
|
||||||
|
v-else
|
||||||
|
>
|
||||||
|
<span class="font-bold uppercase text-gray-300"
|
||||||
|
>Choose a floor to edit from the top left</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -50,13 +61,17 @@ 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 { UpsertRoomItem } from '../interfaces/room.interfaces';
|
||||||
|
import { Line } from '../modules/house-planner/interfaces';
|
||||||
import { useBuildingStore } from '../store/building.store';
|
import { useBuildingStore } from '../store/building.store';
|
||||||
|
import extractLinePoints from '../utils/extract-line-points';
|
||||||
const building = useBuildingStore();
|
const building = useBuildingStore();
|
||||||
|
|
||||||
const { buildings, floors } = storeToRefs(building);
|
const { buildings, floors } = storeToRefs(building);
|
||||||
const selectedBuildingId = ref<number>();
|
const selectedBuildingId = ref<number>();
|
||||||
const selectedFloorId = ref<number>();
|
const selectedFloorId = ref<number>();
|
||||||
const status = ref('No changes');
|
const status = ref('No changes');
|
||||||
|
const plannerRef = ref<InstanceType<typeof HousePlanner>>();
|
||||||
|
|
||||||
const currentFloor = computed(
|
const currentFloor = computed(
|
||||||
() =>
|
() =>
|
||||||
@ -68,33 +83,91 @@ const floorPlan = computed(
|
|||||||
currentFloor.value &&
|
currentFloor.value &&
|
||||||
Object.assign({
|
Object.assign({
|
||||||
...defaultRoomData,
|
...defaultRoomData,
|
||||||
|
...JSON.parse(currentFloor.value.plan || '{}'),
|
||||||
id: selectedFloorId.value,
|
id: selectedFloorId.value,
|
||||||
...JSON.parse(currentFloor.value.plan),
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const buildingSelected = async () => {
|
const buildingSelected = async () => {
|
||||||
if (selectedBuildingId.value == null) return;
|
if (selectedBuildingId.value == null) return;
|
||||||
|
selectedFloorId.value = undefined;
|
||||||
await building.getFloors(selectedBuildingId.value);
|
await building.getFloors(selectedBuildingId.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateDocument = async (data: FloorDocument) => {
|
const updateRooms = async (data: FloorDocument, rooms: Line[]) => {
|
||||||
if (
|
if (
|
||||||
!selectedBuildingId.value ||
|
!selectedBuildingId.value ||
|
||||||
!selectedFloorId.value ||
|
!selectedFloorId.value ||
|
||||||
!currentFloor.value
|
!currentFloor.value
|
||||||
|
)
|
||||||
|
return data;
|
||||||
|
|
||||||
|
const extractedRooms: UpsertRoomItem[] = rooms.map((room) => ({
|
||||||
|
id: room.databaseId,
|
||||||
|
canvasObjectId: room.id,
|
||||||
|
displayName: room.name,
|
||||||
|
plan: JSON.stringify({ polygon: extractLinePoints(room) }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!extractedRooms.length) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const createdRooms = await building.upsertFloorRooms(
|
||||||
|
selectedBuildingId.value,
|
||||||
|
currentFloor.value.number,
|
||||||
|
extractedRooms
|
||||||
|
);
|
||||||
|
|
||||||
|
if (createdRooms?.length) {
|
||||||
|
createdRooms.forEach((room) => {
|
||||||
|
if (!room.canvasObjectId || !room.id) return;
|
||||||
|
rooms.forEach((existing) => {
|
||||||
|
if (existing.id !== room.canvasObjectId) return;
|
||||||
|
existing.databaseId = room.id;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (plannerRef.value) {
|
||||||
|
data = plannerRef.value.updateLocalDocument();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateDocument = async (data: FloorDocument, rooms: Line[]) => {
|
||||||
|
if (
|
||||||
|
!selectedBuildingId.value ||
|
||||||
|
!selectedFloorId.value ||
|
||||||
|
!currentFloor.value ||
|
||||||
|
status.value === 'Saving...'
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
status.value = 'Saving...';
|
status.value = 'Saving...';
|
||||||
await building.saveFloor(
|
try {
|
||||||
selectedBuildingId.value,
|
data = await updateRooms(data, rooms);
|
||||||
currentFloor.value.number,
|
|
||||||
{
|
// Prevent useless requests
|
||||||
plan: JSON.stringify(data),
|
const floorPlan = JSON.stringify(data);
|
||||||
|
if (currentFloor.value.plan === floorPlan) {
|
||||||
|
status.value = 'Saved!';
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
);
|
|
||||||
status.value = 'Saved!';
|
await building.saveFloor(
|
||||||
|
selectedBuildingId.value,
|
||||||
|
currentFloor.value.number,
|
||||||
|
{
|
||||||
|
plan: floorPlan,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
status.value = 'Saved!';
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Failed to save floor document: ${(e as Error).stack}`);
|
||||||
|
status.value = 'Failed to save!';
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
Loading…
Reference in New Issue
Block a user