homemanager-fe/src/components/house-planner/HousePlanner.vue

316 lines
8.2 KiB
Vue

<template>
<div class="relative h-full w-full" :class="{ 'bg-white': !transparent }">
<div
class="relative h-full w-full overflow-hidden"
:class="{ 'bg-gray-100': !transparent }"
>
<div
class="relative"
:style="{
transformOrigin: 'top left',
transform: `translate(${canvasPos[0]}px, ${canvasPos[1]}px)`,
}"
>
<canvas
ref="canvas"
class="h-full w-full border-none"
:class="{ 'bg-white': !transparent }"
:style="{
width: `${canvasDim[0] * canvasZoom}px`,
height: `${canvasDim[1] * canvasZoom}px`,
}"
/>
<div
class="pointer-events-none absolute top-0 left-0 h-full w-full"
v-if="!editable"
:style="{
transformOrigin: 'top left',
transform: `scale(${canvasZoom})`,
width: `${canvasDim[0]}px`,
height: `${canvasDim[1]}px`,
}"
>
<slot />
</div>
</div>
</div>
<PlannerToolbar v-if="editable">
<PlannerTool
v-for="toolItem of toolbar"
:title="toolItem.title"
:icon="toolItem.icon"
:multiple="!!toolItem.children?.length"
:selected="tool === toolItem.tool"
@click="selectTool(toolItem.tool, toolItem.subTool)"
>
<template v-if="toolItem.children?.length">
<PlannerTool
v-for="subItem of toolItem.children"
:title="subItem.title"
:icon="subItem.icon"
:selected="
tool === subItem.tool &&
(!subItem.subTool || subItem.subTool === subTool)
"
@click.stop="selectTool(subItem.tool, subItem.subTool)"
/>
</template>
</PlannerTool>
</PlannerToolbar>
<PlannerSidebars v-if="editable">
<PlannerLayerPanel
:layers="localFloorDocument.layers"
@layer-name="commitLayerName"
@object-name="commitObjectName"
@select-object="clickedOnObject"
@select-layer="clickedOnLayer"
/>
<PlannerPropertyPanel
:layers="localFloorDocument.layers"
@update="updateObjectProperty"
/>
</PlannerSidebars>
</div>
</template>
<script setup lang="ts">
import {
PencilSquareIcon,
PencilIcon,
HomeIcon,
ArrowsPointingOutIcon,
ArrowDownOnSquareIcon,
ScissorsIcon,
XMarkIcon,
} from '@heroicons/vue/24/outline';
import { useDebounceFn } from '@vueuse/shared';
import { onBeforeUnmount, onMounted, ref, shallowRef } from 'vue';
import { HousePlanner } from '../../modules/house-planner';
import {
Line,
RepositionEvent,
ToolEvent,
Vec2,
Vec2Box,
} from '../../modules/house-planner/interfaces';
import type { HousePlannerCanvasTools } from '../../modules/house-planner/tools';
import deepUnref from '../../utils/deep-unref';
import { FloorDocument } from './interfaces/floor-document.interface';
import { ToolbarTool } from './interfaces/toolbar.interfaces';
import PlannerLayerPanel from './PlannerLayerPanel.vue';
import PlannerPropertyPanel from './PlannerPropertyPanel.vue';
import PlannerSidebars from './PlannerSidebars.vue';
import PlannerTool from './PlannerTool.vue';
import PlannerToolbar from './PlannerToolbar.vue';
const canvas = ref();
const module = shallowRef(new HousePlanner());
const tool = ref<string | undefined>('move');
const subTool = ref<string | undefined>(undefined);
const canvasDim = ref<Vec2>([1920, 1080]);
const canvasPos = ref<Vec2>([0, 0]);
const canvasZoom = ref<number>(1);
const props = withDefaults(
defineProps<{
floorDocument: FloorDocument;
editable: boolean;
grid?: boolean;
transparent?: boolean;
headless?: boolean;
}>(),
{ editable: true, grid: true, headless: false, transparent: false }
);
const emit = defineEmits<{
(
e: 'update',
document: FloorDocument,
rooms: Line[],
boundingBox?: Vec2Box
): void;
(e: 'edited'): void;
(e: 'zoom', scale: number): 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.getBoundingBox()
);
const debouncedUpdate = useDebounceFn(emitUpdate, 10000);
const toolbar: ToolbarTool[] = [
{
title: 'Move',
icon: ArrowsPointingOutIcon,
tool: 'move',
},
{
title: 'Draw',
icon: PencilSquareIcon,
tool: 'line',
subTool: 'line',
children: [
{
title: 'Outlines',
icon: PencilIcon,
tool: 'line',
subTool: 'line',
},
{
title: 'Rooms',
icon: HomeIcon,
tool: 'line',
subTool: 'room',
},
{
title: 'Curves',
icon: ArrowDownOnSquareIcon,
tool: 'line',
subTool: 'curve',
},
],
},
{
title: 'Cut',
icon: ScissorsIcon,
tool: 'cut',
subTool: 'cut',
children: [
{
title: 'Remove Segment',
icon: XMarkIcon,
tool: 'cut',
subTool: 'remove-segment',
},
],
},
];
const selectTool = (newTool?: string, newSubTool?: string) => {
if (newTool === tool.value && !newSubTool) {
newTool = undefined;
}
(module.value.manager?.tools as HousePlannerCanvasTools).setTool(
newTool,
newSubTool
);
};
const commitObjectName = (layerId: number, objectId: number, name: string) => {
module.value.manager?.updateObjectProperties(layerId, objectId, { name });
};
const commitLayerName = (layerId: number, name: string) => {
module.value.manager?.updateLayerProperties(layerId, { name });
};
const clickedOnObject = (layerId: number, objectId: number, add?: boolean) => {
const object = module.value.manager?.getLayerObjectById(layerId, objectId);
if (!object) return;
(module.value.manager?.tools as HousePlannerCanvasTools).selectObject(
object,
add
);
};
const clickedOnLayer = (layerId: number) => {
const layer = module.value.manager?.getLayerById(layerId);
if (!layer) return;
(module.value.manager?.tools as HousePlannerCanvasTools).selectLayer(layer);
};
const updateObjectProperty = (
layerId: number,
objectId: number,
key: string,
value: unknown
) => {
module.value.manager?.updateObjectProperties(layerId, objectId, {
[key]: value,
});
};
const updateLocalDocument = () => {
localFloorDocument.value.layers = deepUnref(module.value.manager!.layers);
};
const events: Record<string, (e: CustomEvent) => void> = {
'hpc:update': (e: CustomEvent) => {
updateLocalDocument();
emit('edited');
debouncedUpdate();
},
'hpc:selectionchange': (e: CustomEvent) => {
updateLocalDocument();
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;
emit('zoom', e.detail.zoom);
},
'hpc:save': (e: CustomEvent) => {
updateLocalDocument();
emit('edited');
emitUpdate();
},
};
defineExpose({
updateLocalDocument() {
updateLocalDocument();
return localFloorDocument.value;
},
setViewRectangle(view?: Vec2Box) {
if (!module.value) return;
if (!view && !localFloorDocument.value?.boundingBox) return;
module.value.manager?.setViewRectangle(
view || localFloorDocument.value!.boundingBox!
);
},
canvasDim,
canvasPos,
canvasZoom,
});
onMounted(() => {
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.grid,
props.headless,
localFloorDocument.value.boundingBox
);
canvasPos.value = setPos;
canvasZoom.value = setZoom;
emit('zoom', setZoom);
Object.keys(events).forEach((event) =>
canvas.value.addEventListener(event, events[event])
);
});
onBeforeUnmount(() => {
Object.keys(events).forEach((event) =>
canvas.value.removeEventListener(event, events[event])
);
module.value?.cleanUp();
});
</script>