database connection
This commit is contained in:
parent
452bcd5c01
commit
ccfe07a0b8
@ -23,7 +23,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ChevronDownIcon } from '@heroicons/vue/24/outline';
|
import { ChevronDownIcon } from '@heroicons/vue/24/outline';
|
||||||
import { onMounted, ref } from 'vue';
|
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
||||||
import { onBeforeRouteLeave } from 'vue-router';
|
import { onBeforeRouteLeave } from 'vue-router';
|
||||||
|
|
||||||
const open = ref(false);
|
const open = ref(false);
|
||||||
@ -37,15 +37,19 @@ const toggle = (to?: boolean) => {
|
|||||||
open.value = to ?? !open.value;
|
open.value = to ?? !open.value;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const event = (e: MouseEvent) => {
|
||||||
|
if (wrapper.value.contains(e.target as HTMLElement)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
open.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const event = (e: MouseEvent) => {
|
|
||||||
if (wrapper.value.contains(e.target as HTMLElement)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
open.value = false;
|
|
||||||
};
|
|
||||||
window.addEventListener('click', event);
|
window.addEventListener('click', event);
|
||||||
return () => window.removeEventListener('click', event);
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('click', event);
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeRouteLeave(() => {
|
onBeforeRouteLeave(() => {
|
||||||
|
@ -38,14 +38,14 @@
|
|||||||
|
|
||||||
<PlannerSidebars>
|
<PlannerSidebars>
|
||||||
<PlannerLayerPanel
|
<PlannerLayerPanel
|
||||||
:layers="serializedLayers"
|
:layers="localFloorDocument.layers"
|
||||||
@layer-name="commitLayerName"
|
@layer-name="commitLayerName"
|
||||||
@object-name="commitObjectName"
|
@object-name="commitObjectName"
|
||||||
@select-object="clickedOnObject"
|
@select-object="clickedOnObject"
|
||||||
@select-layer="clickedOnLayer"
|
@select-layer="clickedOnLayer"
|
||||||
/>
|
/>
|
||||||
<PlannerPropertyPanel
|
<PlannerPropertyPanel
|
||||||
:layers="serializedLayers"
|
:layers="localFloorDocument.layers"
|
||||||
@update="updateObjectProperty"
|
@update="updateObjectProperty"
|
||||||
/>
|
/>
|
||||||
</PlannerSidebars>
|
</PlannerSidebars>
|
||||||
@ -62,16 +62,17 @@ import {
|
|||||||
ScissorsIcon,
|
ScissorsIcon,
|
||||||
XMarkIcon,
|
XMarkIcon,
|
||||||
} from '@heroicons/vue/24/outline';
|
} from '@heroicons/vue/24/outline';
|
||||||
import { useSessionStorage } from '@vueuse/core';
|
import { useDebounceFn } from '@vueuse/shared';
|
||||||
import { 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 {
|
||||||
Layer,
|
|
||||||
RepositionEvent,
|
RepositionEvent,
|
||||||
ToolEvent,
|
ToolEvent,
|
||||||
Vec2,
|
Vec2,
|
||||||
} from '../../modules/house-planner/interfaces';
|
} from '../../modules/house-planner/interfaces';
|
||||||
|
import type { HousePlannerCanvasTools } from '../../modules/house-planner/tools';
|
||||||
import deepUnref from '../../utils/deep-unref';
|
import deepUnref from '../../utils/deep-unref';
|
||||||
|
import { FloorDocument } from './interfaces/floor-document.interface';
|
||||||
import { ToolbarTool } from './interfaces/toolbar.interfaces';
|
import { ToolbarTool } from './interfaces/toolbar.interfaces';
|
||||||
import PlannerLayerPanel from './PlannerLayerPanel.vue';
|
import PlannerLayerPanel from './PlannerLayerPanel.vue';
|
||||||
import PlannerPropertyPanel from './PlannerPropertyPanel.vue';
|
import PlannerPropertyPanel from './PlannerPropertyPanel.vue';
|
||||||
@ -86,27 +87,20 @@ const subTool = ref<string | undefined>(undefined);
|
|||||||
const canvasDim = ref<Vec2>([1920, 1080]);
|
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 serializedLayers = useSessionStorage<Layer[]>(
|
|
||||||
'roomData',
|
const props = defineProps<{
|
||||||
[
|
floorDocument: FloorDocument;
|
||||||
{
|
}>();
|
||||||
id: 1,
|
|
||||||
name: 'Rooms',
|
const emit = defineEmits<{
|
||||||
color: '#00ddff',
|
(e: 'update', document: FloorDocument): void;
|
||||||
contents: [],
|
(e: 'edited'): void;
|
||||||
visible: true,
|
}>();
|
||||||
active: false,
|
|
||||||
},
|
const localFloorDocument = ref(deepUnref(props.floorDocument));
|
||||||
{
|
const debouncedUpdate = useDebounceFn(
|
||||||
id: 0,
|
() => emit('update', localFloorDocument.value),
|
||||||
name: 'Base',
|
5000
|
||||||
color: '#00ddff',
|
|
||||||
contents: [],
|
|
||||||
visible: true,
|
|
||||||
active: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
{ writeDefaults: false }
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const toolbar: ToolbarTool[] = [
|
const toolbar: ToolbarTool[] = [
|
||||||
@ -148,7 +142,7 @@ const toolbar: ToolbarTool[] = [
|
|||||||
subTool: 'cut',
|
subTool: 'cut',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
title: 'Cut Segment',
|
title: 'Remove Segment',
|
||||||
icon: XMarkIcon,
|
icon: XMarkIcon,
|
||||||
tool: 'cut',
|
tool: 'cut',
|
||||||
subTool: 'remove-segment',
|
subTool: 'remove-segment',
|
||||||
@ -161,7 +155,10 @@ const selectTool = (newTool?: string, newSubTool?: string) => {
|
|||||||
if (newTool === tool.value && !newSubTool) {
|
if (newTool === tool.value && !newSubTool) {
|
||||||
newTool = undefined;
|
newTool = undefined;
|
||||||
}
|
}
|
||||||
module.value.manager?.tools?.setTool(newTool, newSubTool);
|
(module.value.manager?.tools as HousePlannerCanvasTools).setTool(
|
||||||
|
newTool,
|
||||||
|
newSubTool
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const commitObjectName = (layerId: number, objectId: number, name: string) => {
|
const commitObjectName = (layerId: number, objectId: number, name: string) => {
|
||||||
@ -175,13 +172,16 @@ const commitLayerName = (layerId: number, name: string) => {
|
|||||||
const clickedOnObject = (layerId: number, objectId: number, add?: boolean) => {
|
const clickedOnObject = (layerId: number, objectId: number, add?: boolean) => {
|
||||||
const object = module.value.manager?.getLayerObjectById(layerId, objectId);
|
const object = module.value.manager?.getLayerObjectById(layerId, objectId);
|
||||||
if (!object) return;
|
if (!object) return;
|
||||||
module.value.manager?.tools?.selectObject(object, add);
|
(module.value.manager?.tools as HousePlannerCanvasTools).selectObject(
|
||||||
|
object,
|
||||||
|
add
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const clickedOnLayer = (layerId: number) => {
|
const clickedOnLayer = (layerId: number) => {
|
||||||
const layer = module.value.manager?.getLayerById(layerId);
|
const layer = module.value.manager?.getLayerById(layerId);
|
||||||
if (!layer) return;
|
if (!layer) return;
|
||||||
module.value.manager?.tools?.selectLayer(layer);
|
(module.value.manager?.tools as HousePlannerCanvasTools).selectLayer(layer);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateObjectProperty = (
|
const updateObjectProperty = (
|
||||||
@ -195,41 +195,44 @@ const updateObjectProperty = (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const events: Record<string, (e: CustomEvent) => void> = {
|
||||||
|
'hpc:update': (e: CustomEvent) => {
|
||||||
|
localFloorDocument.value.layers = deepUnref(module.value.manager!.layers);
|
||||||
|
emit('edited');
|
||||||
|
debouncedUpdate();
|
||||||
|
},
|
||||||
|
'hpc:selectionchange': (e: CustomEvent) => {
|
||||||
|
localFloorDocument.value.layers = deepUnref(module.value.manager!.layers);
|
||||||
|
emit('edited');
|
||||||
|
debouncedUpdate();
|
||||||
|
},
|
||||||
|
'hpc:tool': (e: CustomEvent<ToolEvent>) => {
|
||||||
|
tool.value = e.detail.primary;
|
||||||
|
subTool.value = e.detail.secondary as string;
|
||||||
|
},
|
||||||
|
'hpc:position': (e: CustomEvent<RepositionEvent>) => {
|
||||||
|
canvasPos.value = e.detail.position;
|
||||||
|
canvasZoom.value = e.detail.zoom;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const cleanUp = module.value.initialize(
|
module.value.initialize(
|
||||||
canvas.value,
|
canvas.value,
|
||||||
deepUnref(serializedLayers.value),
|
deepUnref(localFloorDocument.value.layers),
|
||||||
[1920, 1080],
|
[localFloorDocument.value.width, localFloorDocument.value.height],
|
||||||
canvasPos.value,
|
canvasPos.value,
|
||||||
canvasZoom.value
|
canvasZoom.value
|
||||||
);
|
);
|
||||||
|
|
||||||
const events: Record<string, (e: CustomEvent) => void> = {
|
|
||||||
'hpc:update': (e: CustomEvent) => {
|
|
||||||
serializedLayers.value = deepUnref(module.value.manager!.layers);
|
|
||||||
},
|
|
||||||
'hpc:selectionchange': (e: CustomEvent) => {
|
|
||||||
serializedLayers.value = deepUnref(module.value.manager!.layers);
|
|
||||||
},
|
|
||||||
'hpc:tool': (e: CustomEvent<ToolEvent>) => {
|
|
||||||
tool.value = e.detail.primary;
|
|
||||||
subTool.value = e.detail.secondary as string;
|
|
||||||
},
|
|
||||||
'hpc:position': (e: CustomEvent<RepositionEvent>) => {
|
|
||||||
canvasPos.value = e.detail.position;
|
|
||||||
canvasZoom.value = e.detail.zoom;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
Object.keys(events).forEach((event) =>
|
Object.keys(events).forEach((event) =>
|
||||||
canvas.value.addEventListener(event, events[event])
|
canvas.value.addEventListener(event, events[event])
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
onBeforeUnmount(() => {
|
||||||
Object.keys(events).forEach((event) =>
|
Object.keys(events).forEach((event) =>
|
||||||
canvas.value.removeEventListener(event, events[event])
|
canvas.value.removeEventListener(event, events[event])
|
||||||
);
|
);
|
||||||
cleanUp();
|
module.value?.cleanUp();
|
||||||
};
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -50,7 +50,7 @@ const lineProps: ObjectProperty[] = [
|
|||||||
{ key: 'color', title: 'Color', type: 'color', groupable: true },
|
{ key: 'color', title: 'Color', type: 'color', groupable: true },
|
||||||
{
|
{
|
||||||
key: 'lineCap',
|
key: 'lineCap',
|
||||||
title: 'Line Cap',
|
title: 'Line Cap Style',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
groupable: true,
|
groupable: true,
|
||||||
options: [
|
options: [
|
||||||
@ -62,7 +62,7 @@ const lineProps: ObjectProperty[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'lineJoin',
|
key: 'lineJoin',
|
||||||
title: 'Line Join',
|
title: 'Line Join Style',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
groupable: true,
|
groupable: true,
|
||||||
options: [
|
options: [
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="absolute right-0 top-0 bottom-0 z-10 my-4 overflow-hidden">
|
<div class="z-8 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,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="absolute bottom-0 z-10 w-screen max-w-md rounded-lg bg-white lg:left-1/2 lg:ml-0 lg:-translate-x-1/2"
|
class="z-8 absolute bottom-0 w-screen max-w-md rounded-lg bg-white lg:left-1/2 lg:ml-0 lg:-translate-x-1/2"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex flex-1 flex-row items-center justify-center space-x-2 py-4 px-4 shadow-lg ring-1 ring-black ring-opacity-5"
|
class="flex flex-1 flex-row items-center justify-center space-x-2 py-4 px-4 shadow-lg ring-1 ring-black ring-opacity-5"
|
||||||
|
26
src/components/house-planner/helpers/default-room.ts
Normal file
26
src/components/house-planner/helpers/default-room.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { FloorDocument } from '../interfaces/floor-document.interface';
|
||||||
|
|
||||||
|
export const defaultRoomData: FloorDocument = {
|
||||||
|
id: 0,
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
|
name: 'Floor 0',
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'Rooms',
|
||||||
|
color: '#00ddff',
|
||||||
|
contents: [],
|
||||||
|
visible: true,
|
||||||
|
active: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
name: 'Base',
|
||||||
|
color: '#00ddff',
|
||||||
|
contents: [],
|
||||||
|
visible: true,
|
||||||
|
active: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
@ -0,0 +1,9 @@
|
|||||||
|
import { Layer } from '../../../modules/house-planner/interfaces';
|
||||||
|
|
||||||
|
export interface FloorDocument {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
layers: Layer[];
|
||||||
|
}
|
11
src/interfaces/floor.interfaces.ts
Normal file
11
src/interfaces/floor.interfaces.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { RoomListItem } from './room.interfaces';
|
||||||
|
|
||||||
|
export interface FloorListItem {
|
||||||
|
id: number;
|
||||||
|
displayName: string;
|
||||||
|
number: number;
|
||||||
|
plan: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
rooms: RoomListItem[];
|
||||||
|
}
|
4
src/interfaces/room.interfaces.ts
Normal file
4
src/interfaces/room.interfaces.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export interface RoomListItem {
|
||||||
|
id: number;
|
||||||
|
displayName: string;
|
||||||
|
}
|
@ -1,8 +1,8 @@
|
|||||||
import { clamp } from '@vueuse/core';
|
import { clamp } from '@vueuse/core';
|
||||||
import type { Ref } from 'vue';
|
|
||||||
import { HousePlannerCanvasGrid } from './grid';
|
import { HousePlannerCanvasGrid } from './grid';
|
||||||
import {
|
import {
|
||||||
BezierSegment,
|
BezierSegment,
|
||||||
|
ICanvasToolkit,
|
||||||
Layer,
|
Layer,
|
||||||
LayerObject,
|
LayerObject,
|
||||||
Line,
|
Line,
|
||||||
@ -12,14 +12,12 @@ import {
|
|||||||
} from './interfaces';
|
} from './interfaces';
|
||||||
import { HousePlannerCanvasTools } from './tools';
|
import { HousePlannerCanvasTools } from './tools';
|
||||||
import {
|
import {
|
||||||
rad2deg,
|
|
||||||
vec2Add,
|
vec2Add,
|
||||||
vec2AngleFromOrigin,
|
vec2AngleFromOrigin,
|
||||||
vec2Clamp,
|
|
||||||
vec2Distance,
|
vec2Distance,
|
||||||
vec2DivideScalar,
|
vec2DivideScalar,
|
||||||
|
vec2Equals,
|
||||||
vec2MultiplyScalar,
|
vec2MultiplyScalar,
|
||||||
vec2PointFromAngle,
|
|
||||||
vec2Snap,
|
vec2Snap,
|
||||||
vec2Sub,
|
vec2Sub,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
@ -27,7 +25,7 @@ import {
|
|||||||
export class HousePlannerCanvas {
|
export class HousePlannerCanvas {
|
||||||
public ctx!: CanvasRenderingContext2D;
|
public ctx!: CanvasRenderingContext2D;
|
||||||
public layers: Layer[] = [];
|
public layers: Layer[] = [];
|
||||||
public tools?;
|
public tools?: ICanvasToolkit;
|
||||||
public grid = new HousePlannerCanvasGrid(this, 8);
|
public grid = new HousePlannerCanvasGrid(this, 8);
|
||||||
public mousePosition: Vec2 = [0, 0];
|
public mousePosition: Vec2 = [0, 0];
|
||||||
public mouseClickPosition: Vec2 = [0, 0];
|
public mouseClickPosition: Vec2 = [0, 0];
|
||||||
@ -290,23 +288,23 @@ export class HousePlannerCanvas {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private drawRoomText(line: Line) {
|
private drawRoomText(line: Line) {
|
||||||
|
const points = 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
|
||||||
|
);
|
||||||
const centerPoint = vec2DivideScalar(
|
const centerPoint = vec2DivideScalar(
|
||||||
line.segments.reduce<Vec2 | null>((prev, curr) => {
|
points.reduce<Vec2 | null>(
|
||||||
if (!prev) {
|
(prev, current) => (prev ? vec2Add(prev, current) : current),
|
||||||
if (curr.start) {
|
null
|
||||||
return vec2Add(curr.start, curr.end);
|
) as Vec2,
|
||||||
}
|
points.length
|
||||||
return curr.end;
|
|
||||||
}
|
|
||||||
|
|
||||||
let preadd = vec2Add(prev, curr.end);
|
|
||||||
if (curr.start) {
|
|
||||||
preadd = vec2Add(curr.start, preadd);
|
|
||||||
}
|
|
||||||
return preadd;
|
|
||||||
}, null) as Vec2,
|
|
||||||
line.segments.length + 1
|
|
||||||
);
|
);
|
||||||
|
|
||||||
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);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Ref } from 'vue';
|
import { Ref } from 'vue';
|
||||||
import { HousePlannerCanvas } from './canvas';
|
import { HousePlannerCanvas } from './canvas';
|
||||||
import { Layer, Vec2 } from './interfaces';
|
import { Layer, Vec2 } from './interfaces';
|
||||||
|
import { HousePlannerCanvasTools } from './tools';
|
||||||
|
|
||||||
export class HousePlanner {
|
export class HousePlanner {
|
||||||
public canvas!: HTMLCanvasElement;
|
public canvas!: HTMLCanvasElement;
|
||||||
@ -25,15 +26,15 @@ export class HousePlanner {
|
|||||||
this.manager.layers = initialData;
|
this.manager.layers = initialData;
|
||||||
|
|
||||||
if (editable && this.manager.tools) {
|
if (editable && this.manager.tools) {
|
||||||
this.manager.tools.selectLayer(
|
const stdToolkit = this.manager.tools as HousePlannerCanvasTools;
|
||||||
|
stdToolkit.selectLayer(
|
||||||
initialData[initialData.findIndex((layer) => layer.active)]
|
initialData[initialData.findIndex((layer) => layer.active)]
|
||||||
);
|
);
|
||||||
this.manager.tools.setInitialSelection();
|
stdToolkit.setInitialSelection();
|
||||||
this.manager.tools.setTool('move');
|
stdToolkit.setTool('move');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.manager.draw();
|
this.manager.draw();
|
||||||
return () => this.cleanUp();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanUp() {
|
cleanUp() {
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { HousePlannerCanvasHistory } from './history';
|
||||||
import { LayerObjectType } from './types';
|
import { LayerObjectType } from './types';
|
||||||
|
|
||||||
export type Vec2 = [number, number];
|
export type Vec2 = [number, number];
|
||||||
@ -13,6 +14,7 @@ export interface BezierSegment extends LineSegment {
|
|||||||
|
|
||||||
export interface LayerObject {
|
export interface LayerObject {
|
||||||
id: number;
|
id: number;
|
||||||
|
databaseId?: number;
|
||||||
name: string;
|
name: string;
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
@ -66,6 +68,16 @@ export interface ICanvasToolMouseEvents {
|
|||||||
mouseUp(moved: boolean): void;
|
mouseUp(moved: boolean): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ICanvasToolkit extends ICanvasToolMouseEvents {
|
||||||
|
history: HousePlannerCanvasHistory;
|
||||||
|
drawHighlights(): void;
|
||||||
|
drawControls(): void;
|
||||||
|
cleanUp(): void;
|
||||||
|
onKeyDown(e: KeyboardEvent): void;
|
||||||
|
onKeyUp(e: KeyboardEvent): void;
|
||||||
|
getMousedObject(): LayerObject | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ICanvasToolBase<U = undefined> extends ICanvasToolMouseEvents {
|
export interface ICanvasToolBase<U = undefined> extends ICanvasToolMouseEvents {
|
||||||
name: string;
|
name: string;
|
||||||
subTool: U | undefined;
|
subTool: U | undefined;
|
||||||
|
@ -3,7 +3,7 @@ import { HousePlannerCanvasHistory } from './history';
|
|||||||
import {
|
import {
|
||||||
History,
|
History,
|
||||||
ICanvasToolBase,
|
ICanvasToolBase,
|
||||||
ICanvasToolMouseEvents,
|
ICanvasToolkit,
|
||||||
Layer,
|
Layer,
|
||||||
LayerObject,
|
LayerObject,
|
||||||
Line,
|
Line,
|
||||||
@ -14,11 +14,12 @@ import { CutTool } from './tools/cut';
|
|||||||
import { LineTool } from './tools/line';
|
import { LineTool } from './tools/line';
|
||||||
import { MoveTool } from './tools/move';
|
import { MoveTool } from './tools/move';
|
||||||
|
|
||||||
export class HousePlannerCanvasTools implements ICanvasToolMouseEvents {
|
export class HousePlannerCanvasTools implements ICanvasToolkit {
|
||||||
public selectedLayer?: Layer;
|
public selectedLayer?: Layer;
|
||||||
public selectedObjects: LayerObject[] = [];
|
public selectedObjects: LayerObject[] = [];
|
||||||
public gridSnap = true;
|
public gridSnap = true;
|
||||||
public gridSnapScale = 8;
|
public gridSnapScale = 8;
|
||||||
|
public autoClose = false;
|
||||||
public tool?: ICanvasToolBase<unknown>;
|
public tool?: ICanvasToolBase<unknown>;
|
||||||
public tools: Record<string, ICanvasToolBase<unknown>> = {
|
public tools: Record<string, ICanvasToolBase<unknown>> = {
|
||||||
['move']: new MoveTool(this),
|
['move']: new MoveTool(this),
|
||||||
|
@ -1,10 +1,4 @@
|
|||||||
import {
|
import { LayerObject, Line, LineSegment, Vec2 } from '../interfaces';
|
||||||
BezierSegment,
|
|
||||||
LayerObject,
|
|
||||||
Line,
|
|
||||||
LineSegment,
|
|
||||||
Vec2,
|
|
||||||
} from '../interfaces';
|
|
||||||
import { CanvasToolBase } from './tool-base';
|
import { CanvasToolBase } from './tool-base';
|
||||||
|
|
||||||
export type CutToolType = 'cut' | 'remove-segment';
|
export type CutToolType = 'cut' | 'remove-segment';
|
||||||
@ -46,34 +40,7 @@ export class CutTool extends CanvasToolBase<CutToolType> {
|
|||||||
this.renderer.draw();
|
this.renderer.draw();
|
||||||
}
|
}
|
||||||
|
|
||||||
drawBezierControls(bezier: BezierSegment, previousEnd: Vec2) {
|
drawLineControls(line: LineSegment) {
|
||||||
const [cp1x, cp1y] = bezier.startControl;
|
|
||||||
const [cp2x, cp2y] = bezier.endControl;
|
|
||||||
const [endx, endy] = bezier.end;
|
|
||||||
const [prevx, prevy] = previousEnd;
|
|
||||||
this.ctx.fillStyle = '#00ddffaa';
|
|
||||||
this.ctx.strokeStyle = '#00ddffaa';
|
|
||||||
this.ctx.lineWidth = 2;
|
|
||||||
this.ctx.beginPath();
|
|
||||||
this.ctx.arc(cp1x, cp1y, this.manager.selectError / 2, 0, 2 * Math.PI);
|
|
||||||
this.ctx.fill();
|
|
||||||
|
|
||||||
this.ctx.beginPath();
|
|
||||||
this.ctx.arc(cp2x, cp2y, this.manager.selectError / 2, 0, 2 * Math.PI);
|
|
||||||
this.ctx.fill();
|
|
||||||
|
|
||||||
this.ctx.beginPath();
|
|
||||||
this.ctx.moveTo(cp1x, cp1y);
|
|
||||||
this.ctx.lineTo(prevx, prevy);
|
|
||||||
this.ctx.stroke();
|
|
||||||
|
|
||||||
this.ctx.beginPath();
|
|
||||||
this.ctx.moveTo(cp2x, cp2y);
|
|
||||||
this.ctx.lineTo(endx, endy);
|
|
||||||
this.ctx.stroke();
|
|
||||||
}
|
|
||||||
|
|
||||||
drawLineControls(line: LineSegment, previousEnd: Vec2) {
|
|
||||||
const [endx, endy] = line.end;
|
const [endx, endy] = line.end;
|
||||||
this.ctx.fillStyle = '#00ddffaa';
|
this.ctx.fillStyle = '#00ddffaa';
|
||||||
|
|
||||||
@ -99,15 +66,8 @@ export class CutTool extends CanvasToolBase<CutToolType> {
|
|||||||
for (const object of this.manager.selectedObjects) {
|
for (const object of this.manager.selectedObjects) {
|
||||||
const line = object as Line;
|
const line = object as Line;
|
||||||
if (line.segments && line.render) {
|
if (line.segments && line.render) {
|
||||||
let lastSegment = null;
|
|
||||||
for (const segment of line.segments) {
|
for (const segment of line.segments) {
|
||||||
const bezier = segment as BezierSegment;
|
this.drawLineControls(segment);
|
||||||
const previousPoint = lastSegment ? lastSegment.end : segment.start!;
|
|
||||||
if (bezier.startControl && bezier.endControl) {
|
|
||||||
this.drawBezierControls(bezier, previousPoint);
|
|
||||||
}
|
|
||||||
this.drawLineControls(segment, previousPoint);
|
|
||||||
lastSegment = segment;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,6 @@ import { CanvasToolBase } from './tool-base';
|
|||||||
export type LineToolType = 'line' | 'curve' | 'room';
|
export type LineToolType = 'line' | 'curve' | 'room';
|
||||||
export class LineTool extends CanvasToolBase<LineToolType> {
|
export class LineTool extends CanvasToolBase<LineToolType> {
|
||||||
public name = 'line';
|
public name = 'line';
|
||||||
public autoClose = false;
|
|
||||||
private drawingLine: Line | null = null;
|
private drawingLine: Line | null = null;
|
||||||
public subTool: LineToolType = 'line';
|
public subTool: LineToolType = 'line';
|
||||||
|
|
||||||
@ -115,7 +114,7 @@ export class LineTool extends CanvasToolBase<LineToolType> {
|
|||||||
) ||
|
) ||
|
||||||
this.drawingLine.type === 'curve'
|
this.drawingLine.type === 'curve'
|
||||||
) {
|
) {
|
||||||
if (this.drawingLine.type !== 'curve' && this.autoClose) {
|
if (this.drawingLine.type !== 'curve' && this.manager.autoClose) {
|
||||||
this.drawingLine.segments.splice(
|
this.drawingLine.segments.splice(
|
||||||
this.drawingLine.segments.length - 1,
|
this.drawingLine.segments.length - 1,
|
||||||
1
|
1
|
||||||
@ -135,7 +134,10 @@ export class LineTool extends CanvasToolBase<LineToolType> {
|
|||||||
value: [],
|
value: [],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
this.drawingLine.closed = this.autoClose;
|
this.drawingLine.closed = this.manager.autoClose;
|
||||||
|
if (!this.manager.autoClose) {
|
||||||
|
this.drawingLine.lineCap = 'square';
|
||||||
|
}
|
||||||
this.canvas.dispatchEvent(
|
this.canvas.dispatchEvent(
|
||||||
new CustomEvent('hpc:newobject', {
|
new CustomEvent('hpc:newobject', {
|
||||||
detail: this.drawingLine,
|
detail: this.drawingLine,
|
||||||
|
@ -3,7 +3,7 @@ import Dashboard from '../views/Dashboard.vue';
|
|||||||
import Login from '../views/Login.vue';
|
import Login from '../views/Login.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 '../components/house-planner/HousePlanner.vue';
|
import HousePlanner from '../views/HousePlanner.vue';
|
||||||
|
|
||||||
const routes: RouteRecordRaw[] = [
|
const routes: RouteRecordRaw[] = [
|
||||||
{
|
{
|
||||||
|
@ -2,6 +2,8 @@ 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 { RoomListItem } from '../interfaces/room.interfaces';
|
||||||
import jfetch from '../utils/jfetch';
|
import jfetch from '../utils/jfetch';
|
||||||
|
|
||||||
const { authHeader } = useAccessToken();
|
const { authHeader } = useAccessToken();
|
||||||
@ -9,6 +11,7 @@ export const useBuildingStore = defineStore('building', {
|
|||||||
state: () => {
|
state: () => {
|
||||||
return {
|
return {
|
||||||
buildings: [] as BuildingListItem[],
|
buildings: [] as BuildingListItem[],
|
||||||
|
floors: [] as FloorListItem[],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
@ -18,5 +21,34 @@ export const useBuildingStore = defineStore('building', {
|
|||||||
});
|
});
|
||||||
this.buildings = buildings;
|
this.buildings = buildings;
|
||||||
},
|
},
|
||||||
|
async getFloors(building: number) {
|
||||||
|
const { data: floors } = await jfetch(
|
||||||
|
`${BACKEND_URL}/buildings/${building}/floors`,
|
||||||
|
{
|
||||||
|
headers: authHeader.value,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
this.floors = floors;
|
||||||
|
},
|
||||||
|
async saveFloor(
|
||||||
|
building: number,
|
||||||
|
number: number,
|
||||||
|
floor: Partial<FloorListItem>
|
||||||
|
) {
|
||||||
|
await jfetch(`${BACKEND_URL}/buildings/${building}/floors/${number}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
...authHeader.value,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(floor),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async upsertFloorRooms(
|
||||||
|
building: number,
|
||||||
|
floorNo: number,
|
||||||
|
rooms: RoomListItem[],
|
||||||
|
removedRooms: number[]
|
||||||
|
) {},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
104
src/views/HousePlanner.vue
Normal file
104
src/views/HousePlanner.vue
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
<template>
|
||||||
|
<div class="relative h-full">
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
<div class="flex flex-row items-center space-x-4 px-4">
|
||||||
|
<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"
|
||||||
|
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
|
||||||
|
v-if="selectedFloorId"
|
||||||
|
:floor-document="floorPlan"
|
||||||
|
@update="($newValue) => updateDocument($newValue)"
|
||||||
|
@edited="status = 'Modified'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
import { defaultRoomData } from '../components/house-planner/helpers/default-room';
|
||||||
|
import HousePlanner from '../components/house-planner/HousePlanner.vue';
|
||||||
|
import { FloorDocument } from '../components/house-planner/interfaces/floor-document.interface';
|
||||||
|
import { useBuildingStore } from '../store/building.store';
|
||||||
|
const building = useBuildingStore();
|
||||||
|
|
||||||
|
const { buildings, floors } = storeToRefs(building);
|
||||||
|
const selectedBuildingId = ref<number>();
|
||||||
|
const selectedFloorId = ref<number>();
|
||||||
|
const status = ref('No changes');
|
||||||
|
|
||||||
|
const currentFloor = computed(
|
||||||
|
() =>
|
||||||
|
selectedFloorId.value &&
|
||||||
|
floors.value.find((floor) => floor.id === selectedFloorId.value)
|
||||||
|
);
|
||||||
|
const floorPlan = computed(
|
||||||
|
() =>
|
||||||
|
currentFloor.value &&
|
||||||
|
Object.assign({
|
||||||
|
...defaultRoomData,
|
||||||
|
id: selectedFloorId.value,
|
||||||
|
...JSON.parse(currentFloor.value.plan),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const buildingSelected = async () => {
|
||||||
|
if (selectedBuildingId.value == null) return;
|
||||||
|
await building.getFloors(selectedBuildingId.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateDocument = async (data: FloorDocument) => {
|
||||||
|
if (
|
||||||
|
!selectedBuildingId.value ||
|
||||||
|
!selectedFloorId.value ||
|
||||||
|
!currentFloor.value
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
|
status.value = 'Saving...';
|
||||||
|
await building.saveFloor(
|
||||||
|
selectedBuildingId.value,
|
||||||
|
currentFloor.value.number,
|
||||||
|
{
|
||||||
|
plan: JSON.stringify(data),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
status.value = 'Saved!';
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
floors.value = [];
|
||||||
|
building.getBuildings();
|
||||||
|
});
|
||||||
|
</script>
|
Loading…
Reference in New Issue
Block a user