editor progress
This commit is contained in:
parent
e2cf0a1be9
commit
caa34afd3e
@ -22,6 +22,20 @@
|
|||||||
</template>
|
</template>
|
||||||
</PlannerTool>
|
</PlannerTool>
|
||||||
</PlannerToolbar>
|
</PlannerToolbar>
|
||||||
|
|
||||||
|
<PlannerSidebars>
|
||||||
|
<PlannerLayerPanel
|
||||||
|
:layers="serializedLayers"
|
||||||
|
@layer-name="commitLayerName"
|
||||||
|
@object-name="commitObjectName"
|
||||||
|
@select-object="clickedOnObject"
|
||||||
|
@select-layer="clickedOnLayer"
|
||||||
|
/>
|
||||||
|
<PlannerPropertyPanel
|
||||||
|
:layers="serializedLayers"
|
||||||
|
@update="updateObjectProperty"
|
||||||
|
/>
|
||||||
|
</PlannerSidebars>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -32,13 +46,24 @@ import {
|
|||||||
HomeIcon,
|
HomeIcon,
|
||||||
ArrowsPointingOutIcon,
|
ArrowsPointingOutIcon,
|
||||||
ArrowDownOnSquareIcon,
|
ArrowDownOnSquareIcon,
|
||||||
|
Square3Stack3DIcon,
|
||||||
} from '@heroicons/vue/24/outline';
|
} from '@heroicons/vue/24/outline';
|
||||||
import { useSessionStorage } from '@vueuse/core';
|
import { useSessionStorage } from '@vueuse/core';
|
||||||
|
import type { Component } from 'vue';
|
||||||
import { onMounted, ref, shallowRef } from 'vue';
|
import { onMounted, ref, shallowRef } from 'vue';
|
||||||
import { HousePlanner } from '../../modules/house-planner';
|
import { HousePlanner } from '../../modules/house-planner';
|
||||||
import { Layer, ToolEvent } from '../../modules/house-planner/interfaces';
|
import { Layer, ToolEvent } from '../../modules/house-planner/interfaces';
|
||||||
import { SubToolType, ToolType } from '../../modules/house-planner/types';
|
import {
|
||||||
|
LayerObjectType,
|
||||||
|
SubToolType,
|
||||||
|
ToolType,
|
||||||
|
} from '../../modules/house-planner/types';
|
||||||
|
import deepUnref from '../../utils/deep-unref';
|
||||||
import { ToolbarTool } from './interfaces/toolbar.interfaces';
|
import { ToolbarTool } from './interfaces/toolbar.interfaces';
|
||||||
|
import PlannerLayerPanel from './PlannerLayerPanel.vue';
|
||||||
|
import PlannerPropertyPanel from './PlannerPropertyPanel.vue';
|
||||||
|
import PlannerSidebar from './PlannerSidebar.vue';
|
||||||
|
import PlannerSidebars from './PlannerSidebars.vue';
|
||||||
import PlannerTool from './PlannerTool.vue';
|
import PlannerTool from './PlannerTool.vue';
|
||||||
import PlannerToolbar from './PlannerToolbar.vue';
|
import PlannerToolbar from './PlannerToolbar.vue';
|
||||||
|
|
||||||
@ -50,21 +75,21 @@ const serializedLayers = useSessionStorage<Layer[]>(
|
|||||||
'roomData',
|
'roomData',
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
index: 0,
|
id: 1,
|
||||||
name: 'Base',
|
|
||||||
color: '#00ddff',
|
|
||||||
contents: [],
|
|
||||||
visible: true,
|
|
||||||
active: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
index: 1,
|
|
||||||
name: 'Rooms',
|
name: 'Rooms',
|
||||||
color: '#00ddff',
|
color: '#00ddff',
|
||||||
contents: [],
|
contents: [],
|
||||||
visible: true,
|
visible: true,
|
||||||
active: false,
|
active: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
name: 'Base',
|
||||||
|
color: '#00ddff',
|
||||||
|
contents: [],
|
||||||
|
visible: true,
|
||||||
|
active: true,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
{ writeDefaults: false }
|
{ writeDefaults: false }
|
||||||
);
|
);
|
||||||
@ -110,15 +135,49 @@ const selectTool = (newTool: ToolType, newSubTool?: SubToolType) => {
|
|||||||
module.value.manager?.tools.setTool(newTool, newSubTool);
|
module.value.manager?.tools.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.selectObject(object, add);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clickedOnLayer = (layerId: number) => {
|
||||||
|
const layer = module.value.manager?.getLayerById(layerId);
|
||||||
|
if (!layer) return;
|
||||||
|
module.value.manager?.tools.selectLayer(layer);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateObjectProperty = (
|
||||||
|
layerId: number,
|
||||||
|
objectId: number,
|
||||||
|
key: string,
|
||||||
|
value: unknown
|
||||||
|
) => {
|
||||||
|
module.value.manager?.updateObjectProperties(layerId, objectId, {
|
||||||
|
[key]: value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const cleanUp = module.value.initialize(
|
const cleanUp = module.value.initialize(
|
||||||
canvas.value,
|
canvas.value,
|
||||||
JSON.parse(JSON.stringify(serializedLayers.value))
|
deepUnref(serializedLayers.value)
|
||||||
);
|
);
|
||||||
|
|
||||||
const events: Record<string, (e: CustomEvent) => void> = {
|
const events: Record<string, (e: CustomEvent) => void> = {
|
||||||
'hpc:update': (e: CustomEvent) => {
|
'hpc:update': (e: CustomEvent) => {
|
||||||
serializedLayers.value = module.value.manager!.layers;
|
serializedLayers.value = deepUnref(module.value.manager!.layers);
|
||||||
|
},
|
||||||
|
'hpc:selectionchange': (e: CustomEvent) => {
|
||||||
|
serializedLayers.value = deepUnref(module.value.manager!.layers);
|
||||||
},
|
},
|
||||||
'hpc:tool': (e: CustomEvent<ToolEvent>) => {
|
'hpc:tool': (e: CustomEvent<ToolEvent>) => {
|
||||||
tool.value = e.detail.primary;
|
tool.value = e.detail.primary;
|
||||||
|
98
src/components/house-planner/PlannerLayerPanel.vue
Normal file
98
src/components/house-planner/PlannerLayerPanel.vue
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
<template>
|
||||||
|
<PlannerSidebar title="Layers">
|
||||||
|
<div class="bg-white-50 flex flex-col">
|
||||||
|
<template v-for="layer of layers">
|
||||||
|
<button
|
||||||
|
@dblclick="editingLayerName = layer.id"
|
||||||
|
@click="!editingLayerName && emit('selectLayer', layer.id)"
|
||||||
|
:class="[
|
||||||
|
layer.active ? 'bg-blue-50' : '',
|
||||||
|
'flex w-full flex-row items-center justify-start space-x-2 px-2 py-2',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<Square3Stack3DIcon class="h-4 w-4" />
|
||||||
|
<input
|
||||||
|
v-model="layer.name"
|
||||||
|
v-if="editingLayerName === layer.id"
|
||||||
|
@blur="commitLayerName(layer.name)"
|
||||||
|
@keypress.enter="commitLayerName(layer.name)"
|
||||||
|
/>
|
||||||
|
<span v-else>{{ layer.name }}</span>
|
||||||
|
</button>
|
||||||
|
<div class="flex flex-col bg-gray-50" v-if="layer.active">
|
||||||
|
<div v-for="object of layer.contents">
|
||||||
|
<button
|
||||||
|
@dblclick="editingObjectName = object.id"
|
||||||
|
@click="
|
||||||
|
(e) =>
|
||||||
|
!editingObjectName &&
|
||||||
|
emit('selectObject', layer.id, object.id, e.shiftKey)
|
||||||
|
"
|
||||||
|
:class="[
|
||||||
|
object.selected ? 'bg-blue-100' : '',
|
||||||
|
'flex w-full flex-row items-center justify-start space-x-2 px-2 py-2 pl-6',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<component :is="objectTypeIcons[object.type]" class="h-4 w-4" />
|
||||||
|
<input
|
||||||
|
v-model="object.name"
|
||||||
|
v-if="editingObjectName === object.id"
|
||||||
|
@blur="commitObjectName(layer.id, object.name)"
|
||||||
|
@keypress.enter="commitObjectName(layer.id, object.name)"
|
||||||
|
/>
|
||||||
|
<span v-else>{{ object.name }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</PlannerSidebar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
PencilSquareIcon,
|
||||||
|
PencilIcon,
|
||||||
|
HomeIcon,
|
||||||
|
ArrowDownOnSquareIcon,
|
||||||
|
Square3Stack3DIcon,
|
||||||
|
} from '@heroicons/vue/24/outline';
|
||||||
|
import type { Component } from 'vue';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import PlannerSidebar from './PlannerSidebar.vue';
|
||||||
|
import { Layer } from '../../modules/house-planner/interfaces';
|
||||||
|
import { LayerObjectType } from '../../modules/house-planner/types';
|
||||||
|
|
||||||
|
const objectTypeIcons: Record<LayerObjectType, Component> = {
|
||||||
|
line: PencilIcon,
|
||||||
|
room: HomeIcon,
|
||||||
|
curve: ArrowDownOnSquareIcon,
|
||||||
|
object: PencilSquareIcon,
|
||||||
|
};
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
layers: Layer[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'layerName', layer: number, name: string): void;
|
||||||
|
(e: 'objectName', layer: number, object: number, name: string): void;
|
||||||
|
(e: 'selectLayer', layer: number): void;
|
||||||
|
(e: 'selectObject', layer: number, object: number, add?: boolean): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const editingLayerName = ref<number | null>(null);
|
||||||
|
const editingObjectName = ref<number | null>(null);
|
||||||
|
|
||||||
|
const commitObjectName = (layerId: number, name: string) => {
|
||||||
|
if (editingObjectName.value == null) return;
|
||||||
|
emit('objectName', layerId, editingObjectName.value, name);
|
||||||
|
editingObjectName.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const commitLayerName = (name: string) => {
|
||||||
|
if (editingLayerName.value == null) return;
|
||||||
|
emit('layerName', editingLayerName.value, name);
|
||||||
|
editingLayerName.value = null;
|
||||||
|
};
|
||||||
|
</script>
|
153
src/components/house-planner/PlannerPropertyPanel.vue
Normal file
153
src/components/house-planner/PlannerPropertyPanel.vue
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
<template>
|
||||||
|
<PlannerSidebar title="Properties">
|
||||||
|
<div
|
||||||
|
class="bg-white-50 flex flex-col"
|
||||||
|
v-if="selectedObject && applicableProperties"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="grid grid-cols-2 px-2 py-2"
|
||||||
|
v-for="prop of applicableProperties.properties"
|
||||||
|
>
|
||||||
|
<label :for="`${prop.key}-${selectedObject.id}`">{{
|
||||||
|
prop.title
|
||||||
|
}}</label>
|
||||||
|
<input
|
||||||
|
v-if="prop.type === 'string'"
|
||||||
|
:id="`${prop.key}-${selectedObject.id}`"
|
||||||
|
type="text"
|
||||||
|
:value="selectedObject[prop.key as keyof typeof selectedObject]"
|
||||||
|
@input="updateProp(prop, ($event.target as HTMLInputElement)?.value)"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-else-if="prop.type === 'color'"
|
||||||
|
:id="`${prop.key}-${selectedObject.id}`"
|
||||||
|
type="color"
|
||||||
|
:value="selectedObject[prop.key as keyof typeof selectedObject]"
|
||||||
|
@change="updateProp(prop, ($event.target as HTMLInputElement)?.value)"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-else-if="prop.type === 'number'"
|
||||||
|
:id="`${prop.key}-${selectedObject.id}`"
|
||||||
|
type="number"
|
||||||
|
:value="selectedObject[prop.key as keyof typeof selectedObject]"
|
||||||
|
@input="updateProp(prop, ($event.target as HTMLInputElement)?.value)"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-else-if="prop.type === 'boolean'"
|
||||||
|
:id="`${prop.key}-${selectedObject.id}`"
|
||||||
|
type="checkbox"
|
||||||
|
:checked="(selectedObject[prop.key as keyof typeof selectedObject] as boolean)"
|
||||||
|
@input="
|
||||||
|
updateProp(prop, ($event.target as HTMLInputElement)?.checked)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<select
|
||||||
|
v-else-if="prop.type === 'select'"
|
||||||
|
:id="`${prop.key}-${selectedObject.id}`"
|
||||||
|
:value="selectedObject[prop.key as keyof typeof selectedObject]"
|
||||||
|
@input="updateProp(prop, ($event.target as HTMLInputElement)?.value)"
|
||||||
|
>
|
||||||
|
<option v-for="option of prop.options" :value="option.value">
|
||||||
|
{{ option.title }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PlannerSidebar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import PlannerSidebar from './PlannerSidebar.vue';
|
||||||
|
import { Layer } from '../../modules/house-planner/interfaces';
|
||||||
|
import {
|
||||||
|
ObjectProperties,
|
||||||
|
ObjectProperty,
|
||||||
|
} from './interfaces/properties.interfaces';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
layers: Layer[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(
|
||||||
|
e: 'update',
|
||||||
|
layerId: number,
|
||||||
|
objectId: number,
|
||||||
|
key: string,
|
||||||
|
value: unknown
|
||||||
|
): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const commonProps: ObjectProperty[] = [
|
||||||
|
{ key: 'name', title: 'Name', type: 'string' },
|
||||||
|
{ key: 'visible', title: 'Visible', type: 'boolean', groupable: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
const lineProps: ObjectProperty[] = [
|
||||||
|
...commonProps,
|
||||||
|
{ key: 'width', title: 'Line Width', type: 'number', groupable: true },
|
||||||
|
{ key: 'color', title: 'Color', type: 'color', groupable: true },
|
||||||
|
{
|
||||||
|
key: 'lineCap',
|
||||||
|
title: 'Line Cap',
|
||||||
|
type: 'select',
|
||||||
|
groupable: true,
|
||||||
|
options: [
|
||||||
|
{ value: undefined, title: '' },
|
||||||
|
{ value: 'butt', title: 'Butt' },
|
||||||
|
{ value: 'round', title: 'Round' },
|
||||||
|
{ value: 'square', title: 'Square' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ key: 'closed', title: 'Closed', type: 'boolean', groupable: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
const objectTypeProperties: ObjectProperties[] = [
|
||||||
|
{
|
||||||
|
type: 'line',
|
||||||
|
properties: lineProps,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'curve',
|
||||||
|
properties: lineProps,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'room',
|
||||||
|
properties: lineProps,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
properties: commonProps,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const currentLayer = computed(() =>
|
||||||
|
props.layers.find((layer) => layer.active && layer.visible)
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO multi edit
|
||||||
|
const selectedObject = computed(
|
||||||
|
() => currentLayer.value?.contents?.filter((obj) => obj.selected)[0]
|
||||||
|
);
|
||||||
|
|
||||||
|
const applicableProperties = computed(
|
||||||
|
() =>
|
||||||
|
selectedObject.value &&
|
||||||
|
objectTypeProperties.find(
|
||||||
|
(prop) => prop.type === selectedObject.value?.type
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateProp = (prop: ObjectProperty, value: unknown) => {
|
||||||
|
if (!currentLayer.value || !selectedObject.value) return;
|
||||||
|
if (prop.type === 'number') value = parseFloat(value as string);
|
||||||
|
emit(
|
||||||
|
'update',
|
||||||
|
currentLayer.value!.id,
|
||||||
|
selectedObject.value!.id,
|
||||||
|
prop.key,
|
||||||
|
value
|
||||||
|
);
|
||||||
|
};
|
||||||
|
</script>
|
51
src/components/house-planner/PlannerSidebar.vue
Normal file
51
src/components/house-planner/PlannerSidebar.vue
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<template>
|
||||||
|
<div class="relative flex flex-row overflow-hidden px-2 pb-4 pr-0">
|
||||||
|
<button
|
||||||
|
:class="[
|
||||||
|
open ? 'bg-gray-200' : 'bg-white',
|
||||||
|
'h-8 rounded-tl-md rounded-bl-md px-2 py-2 ring-1 ring-black ring-opacity-5',
|
||||||
|
]"
|
||||||
|
@click="() => (open = !open)"
|
||||||
|
>
|
||||||
|
<ChevronDoubleRightIcon class="h-4 w-4" v-if="open" />
|
||||||
|
<ChevronDoubleLeftIcon class="h-4 w-4" v-else />
|
||||||
|
</button>
|
||||||
|
<Transition
|
||||||
|
enter-active-class="transition-max-width ease-out duration-200 overflow-hidden"
|
||||||
|
enter-from-class="max-w-0"
|
||||||
|
enter-to-class="max-w-xs"
|
||||||
|
leave-active-class="transition-max-width ease-in duration-150 overflow-hidden"
|
||||||
|
leave-from-class="max-w-xs"
|
||||||
|
leave-to-class="max-w-0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="h-screen max-h-96 rounded-bl-md bg-white shadow-lg ring-1 ring-black ring-opacity-5"
|
||||||
|
v-if="open"
|
||||||
|
>
|
||||||
|
<div class="w-screen max-w-xs">
|
||||||
|
<slot name="header">
|
||||||
|
<div
|
||||||
|
class="select-none bg-gray-200 px-2 py-1 font-bold uppercase text-gray-400"
|
||||||
|
>
|
||||||
|
{{ title }}
|
||||||
|
</div>
|
||||||
|
</slot>
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
ChevronDoubleRightIcon,
|
||||||
|
ChevronDoubleLeftIcon,
|
||||||
|
} from '@heroicons/vue/24/outline';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
title: string;
|
||||||
|
}>();
|
||||||
|
const open = ref(true);
|
||||||
|
</script>
|
7
src/components/house-planner/PlannerSidebars.vue
Normal file
7
src/components/house-planner/PlannerSidebars.vue
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="absolute right-0 top-0 z-10 mt-4 flex flex-col items-end space-y-2"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
@ -0,0 +1,19 @@
|
|||||||
|
import { LayerObjectType } from '../../../modules/house-planner/types';
|
||||||
|
|
||||||
|
export interface SelectOptions {
|
||||||
|
value: string | null | undefined;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ObjectProperty {
|
||||||
|
key: string;
|
||||||
|
title: string;
|
||||||
|
type: 'string' | 'boolean' | 'color' | 'number' | 'select';
|
||||||
|
groupable?: boolean;
|
||||||
|
options?: SelectOptions[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ObjectProperties {
|
||||||
|
type: LayerObjectType;
|
||||||
|
properties: ObjectProperty[];
|
||||||
|
}
|
@ -1,5 +1,12 @@
|
|||||||
import { HousePlannerCanvasGrid } from './grid';
|
import { HousePlannerCanvasGrid } from './grid';
|
||||||
import { BezierSegment, Layer, Line, LineSegment, Vec2 } from './interfaces';
|
import {
|
||||||
|
BezierSegment,
|
||||||
|
Layer,
|
||||||
|
LayerObject,
|
||||||
|
Line,
|
||||||
|
LineSegment,
|
||||||
|
Vec2,
|
||||||
|
} from './interfaces';
|
||||||
import { HousePlannerCanvasTools } from './tools';
|
import { HousePlannerCanvasTools } from './tools';
|
||||||
import {
|
import {
|
||||||
rad2deg,
|
rad2deg,
|
||||||
@ -139,6 +146,67 @@ export class HousePlannerCanvas {
|
|||||||
return this.ctx.isPointInStroke(fakePath, x, y);
|
return this.ctx.isPointInStroke(fakePath, x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getLayerById(layerId: number) {
|
||||||
|
return this.layers.find((layer) => layer.id === layerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
getLayerObjectById(layerId: number, objectId: number) {
|
||||||
|
const findLayer = this.getLayerById(layerId);
|
||||||
|
if (!findLayer) return undefined;
|
||||||
|
const findObject = findLayer.contents.find(
|
||||||
|
(content) => content.id === objectId
|
||||||
|
);
|
||||||
|
if (!findObject) return undefined;
|
||||||
|
return findObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateObjectProperties(
|
||||||
|
layerId: number,
|
||||||
|
objectId: number,
|
||||||
|
properties: Partial<Omit<LayerObject | Line, 'segments'>>
|
||||||
|
) {
|
||||||
|
const object = this.getLayerObjectById(layerId, objectId);
|
||||||
|
if (!object) return;
|
||||||
|
this.tools.history.appendToHistory(
|
||||||
|
Object.keys(properties).map((key) => ({
|
||||||
|
object,
|
||||||
|
property: key,
|
||||||
|
value: object[key as keyof typeof object],
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
Object.assign(object, properties);
|
||||||
|
this.canvas.dispatchEvent(
|
||||||
|
new CustomEvent('hpc:update', {
|
||||||
|
detail: { event: 'properties-object', object },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
this.draw();
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLayerProperties(
|
||||||
|
layerId: number,
|
||||||
|
properties: Partial<Omit<Layer, 'contents'>>
|
||||||
|
) {
|
||||||
|
const layer = this.getLayerById(layerId);
|
||||||
|
if (!layer) return;
|
||||||
|
this.tools.history.appendToHistory(
|
||||||
|
Object.keys(properties).map((key) => ({
|
||||||
|
object: layer,
|
||||||
|
property: key,
|
||||||
|
value: layer[key as keyof typeof layer],
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
Object.assign(layer, properties);
|
||||||
|
this.canvas.dispatchEvent(
|
||||||
|
new CustomEvent('hpc:update', {
|
||||||
|
detail: { event: 'properties-layer', layer },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
this.draw();
|
||||||
|
return layer;
|
||||||
|
}
|
||||||
|
|
||||||
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;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { SubToolType, ToolType } from './types';
|
import { LayerObjectType, SubToolType, ToolType } from './types';
|
||||||
|
|
||||||
export type Vec2 = [number, number];
|
export type Vec2 = [number, number];
|
||||||
export interface LineSegment {
|
export interface LineSegment {
|
||||||
@ -12,10 +12,11 @@ export interface BezierSegment extends LineSegment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface LayerObject {
|
export interface LayerObject {
|
||||||
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
type: 'line' | 'room' | 'curve' | 'object';
|
type: LayerObjectType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Line extends LayerObject {
|
export interface Line extends LayerObject {
|
||||||
@ -29,7 +30,7 @@ export interface Line extends LayerObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Layer {
|
export interface Layer {
|
||||||
index: number;
|
id: number;
|
||||||
contents: LayerObject[];
|
contents: LayerObject[];
|
||||||
name: string;
|
name: string;
|
||||||
color: string;
|
color: string;
|
||||||
|
@ -13,6 +13,7 @@ import {
|
|||||||
import { ToolType, SubToolType, BezierControl, LineControl } from './types';
|
import { ToolType, SubToolType, BezierControl, LineControl } from './types';
|
||||||
import {
|
import {
|
||||||
vec2Add,
|
vec2Add,
|
||||||
|
vec2Distance,
|
||||||
vec2Equals,
|
vec2Equals,
|
||||||
vec2InCircle,
|
vec2InCircle,
|
||||||
vec2Inverse,
|
vec2Inverse,
|
||||||
@ -24,6 +25,7 @@ export class HousePlannerCanvasTools {
|
|||||||
public selectedLayer?: Layer;
|
public selectedLayer?: Layer;
|
||||||
public selectedObjects: LayerObject[] = [];
|
public selectedObjects: LayerObject[] = [];
|
||||||
public mousePosition: Vec2 = [0, 0];
|
public mousePosition: Vec2 = [0, 0];
|
||||||
|
public mouseClickPosition: Vec2 = [0, 0];
|
||||||
public mousePositionSnapped: Vec2 = [0, 0];
|
public mousePositionSnapped: Vec2 = [0, 0];
|
||||||
public gridSnap = true;
|
public gridSnap = true;
|
||||||
public gridSnapScale = 8;
|
public gridSnapScale = 8;
|
||||||
@ -70,6 +72,18 @@ export class HousePlannerCanvasTools {
|
|||||||
}
|
}
|
||||||
this.selectedLayer = layer;
|
this.selectedLayer = layer;
|
||||||
this.selectedLayer.active = true;
|
this.selectedLayer.active = true;
|
||||||
|
this.canvas.dispatchEvent(
|
||||||
|
new CustomEvent('hpc:layerchange', {
|
||||||
|
detail: this.selectedObjects,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
this.canvas.dispatchEvent(
|
||||||
|
new CustomEvent('hpc:update', {
|
||||||
|
detail: {
|
||||||
|
event: 'layerchange',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
selectObject(object?: LayerObject | null, add = false) {
|
selectObject(object?: LayerObject | null, add = false) {
|
||||||
@ -81,6 +95,7 @@ export class HousePlannerCanvasTools {
|
|||||||
this.selectedObjects.length = 0;
|
this.selectedObjects.length = 0;
|
||||||
}
|
}
|
||||||
this.manager.draw();
|
this.manager.draw();
|
||||||
|
this.selectedEvent();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,6 +106,7 @@ export class HousePlannerCanvasTools {
|
|||||||
object.selected = false;
|
object.selected = false;
|
||||||
this.selectedObjects.splice(foundAt, 1);
|
this.selectedObjects.splice(foundAt, 1);
|
||||||
this.manager.draw();
|
this.manager.draw();
|
||||||
|
this.selectedEvent();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -100,6 +116,7 @@ export class HousePlannerCanvasTools {
|
|||||||
object.selected = true;
|
object.selected = true;
|
||||||
this.selectedObjects.push(object);
|
this.selectedObjects.push(object);
|
||||||
this.manager.draw();
|
this.manager.draw();
|
||||||
|
this.selectedEvent();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,6 +128,7 @@ export class HousePlannerCanvasTools {
|
|||||||
|
|
||||||
this.selectedObjects = [object];
|
this.selectedObjects = [object];
|
||||||
this.manager.draw();
|
this.manager.draw();
|
||||||
|
this.selectedEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
getMousedObject() {
|
getMousedObject() {
|
||||||
@ -258,6 +276,7 @@ export class HousePlannerCanvasTools {
|
|||||||
}
|
}
|
||||||
onMouseDown(e: MouseEvent) {
|
onMouseDown(e: MouseEvent) {
|
||||||
this.mousePosition = [e.clientX, e.clientY];
|
this.mousePosition = [e.clientX, e.clientY];
|
||||||
|
this.mouseClickPosition = [...this.mousePosition];
|
||||||
this.mousePositionSnapped = this.gridSnap
|
this.mousePositionSnapped = this.gridSnap
|
||||||
? vec2Snap(this.mousePosition, this.gridSnapScale)
|
? vec2Snap(this.mousePosition, this.gridSnapScale)
|
||||||
: this.mousePosition;
|
: this.mousePosition;
|
||||||
@ -302,6 +321,7 @@ export class HousePlannerCanvasTools {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const touch = e.touches[0] || e.changedTouches[0];
|
const touch = e.touches[0] || e.changedTouches[0];
|
||||||
this.mousePosition = [touch.pageX, touch.pageY];
|
this.mousePosition = [touch.pageX, touch.pageY];
|
||||||
|
this.mouseClickPosition = [...this.mousePosition];
|
||||||
this.mousePositionSnapped = this.gridSnap
|
this.mousePositionSnapped = this.gridSnap
|
||||||
? vec2Snap(this.mousePosition, this.gridSnapScale)
|
? vec2Snap(this.mousePosition, this.gridSnapScale)
|
||||||
: this.mousePosition;
|
: this.mousePosition;
|
||||||
@ -435,6 +455,12 @@ export class HousePlannerCanvasTools {
|
|||||||
}
|
}
|
||||||
this.drawingLine = null;
|
this.drawingLine = null;
|
||||||
this.manager.draw();
|
this.manager.draw();
|
||||||
|
|
||||||
|
this.canvas.dispatchEvent(
|
||||||
|
new CustomEvent('hpc:update', {
|
||||||
|
detail: { event: 'draw-cancel' },
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -543,6 +569,14 @@ export class HousePlannerCanvasTools {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private pointerUp() {
|
private pointerUp() {
|
||||||
|
// FIXME: possibly there's a better approach, but right now some clicks do not register
|
||||||
|
if (
|
||||||
|
this.moved &&
|
||||||
|
vec2Distance(this.mouseClickPosition, this.mousePosition) < 0.25
|
||||||
|
) {
|
||||||
|
this.moved = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.moved && !this.handlingBezier && !this.handlingLine) {
|
if (!this.moved && !this.handlingBezier && !this.handlingLine) {
|
||||||
if (this.tool === 'line') {
|
if (this.tool === 'line') {
|
||||||
this.startLine();
|
this.startLine();
|
||||||
@ -588,6 +622,19 @@ export class HousePlannerCanvasTools {
|
|||||||
this.movingObject = false;
|
this.movingObject = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getSequentialId() {
|
||||||
|
return (
|
||||||
|
this.manager.layers.reduce(
|
||||||
|
(total, current) =>
|
||||||
|
current.contents.reduce(
|
||||||
|
(total2, current2) => current2.id + total2,
|
||||||
|
0
|
||||||
|
) + total,
|
||||||
|
0
|
||||||
|
) + 1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private startLine() {
|
private startLine() {
|
||||||
if (!this.selectedLayer?.visible) return;
|
if (!this.selectedLayer?.visible) return;
|
||||||
if (this.drawingLine) {
|
if (this.drawingLine) {
|
||||||
@ -640,6 +687,7 @@ export class HousePlannerCanvasTools {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const newLineObject: Line = {
|
const newLineObject: Line = {
|
||||||
|
id: this.getSequentialId(),
|
||||||
name: 'New Line',
|
name: 'New Line',
|
||||||
type: this.subTool!,
|
type: this.subTool!,
|
||||||
visible: true,
|
visible: true,
|
||||||
|
@ -9,3 +9,4 @@ export type BezierControl = [
|
|||||||
Vec2
|
Vec2
|
||||||
];
|
];
|
||||||
export type LineControl = [LineSegment, 'start' | 'end', Vec2];
|
export type LineControl = [LineSegment, 'start' | 'end', Vec2];
|
||||||
|
export type LayerObjectType = 'line' | 'room' | 'curve' | 'object';
|
||||||
|
@ -54,3 +54,6 @@ export const vec2PointFromAngle = (
|
|||||||
|
|
||||||
export const deg2rad = (deg: number) => deg * (Math.PI / 180);
|
export const deg2rad = (deg: number) => deg * (Math.PI / 180);
|
||||||
export const rad2deg = (rad: number) => rad * (180 / Math.PI);
|
export const rad2deg = (rad: number) => rad * (180 / Math.PI);
|
||||||
|
|
||||||
|
export const randomNumber = (min: number, max: number) =>
|
||||||
|
Math.floor(Math.random() * (max - min + 1) + min);
|
||||||
|
6
src/utils/deep-unref.ts
Normal file
6
src/utils/deep-unref.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { MaybeRef } from '@vueuse/core';
|
||||||
|
import { isRef } from 'vue';
|
||||||
|
|
||||||
|
export default function deepUnref<T>(input: MaybeRef<T>) {
|
||||||
|
return JSON.parse(JSON.stringify(isRef(input) ? input.value : input)) as T;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user