316 lines
8.2 KiB
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>
|