editor progress
This commit is contained in:
parent
e2cf0a1be9
commit
caa34afd3e
@ -22,6 +22,20 @@
|
||||
</template>
|
||||
</PlannerTool>
|
||||
</PlannerToolbar>
|
||||
|
||||
<PlannerSidebars>
|
||||
<PlannerLayerPanel
|
||||
:layers="serializedLayers"
|
||||
@layer-name="commitLayerName"
|
||||
@object-name="commitObjectName"
|
||||
@select-object="clickedOnObject"
|
||||
@select-layer="clickedOnLayer"
|
||||
/>
|
||||
<PlannerPropertyPanel
|
||||
:layers="serializedLayers"
|
||||
@update="updateObjectProperty"
|
||||
/>
|
||||
</PlannerSidebars>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -32,13 +46,24 @@ import {
|
||||
HomeIcon,
|
||||
ArrowsPointingOutIcon,
|
||||
ArrowDownOnSquareIcon,
|
||||
Square3Stack3DIcon,
|
||||
} from '@heroicons/vue/24/outline';
|
||||
import { useSessionStorage } from '@vueuse/core';
|
||||
import type { Component } from 'vue';
|
||||
import { onMounted, ref, shallowRef } from 'vue';
|
||||
import { HousePlanner } from '../../modules/house-planner';
|
||||
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 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 PlannerToolbar from './PlannerToolbar.vue';
|
||||
|
||||
@ -50,21 +75,21 @@ const serializedLayers = useSessionStorage<Layer[]>(
|
||||
'roomData',
|
||||
[
|
||||
{
|
||||
index: 0,
|
||||
name: 'Base',
|
||||
color: '#00ddff',
|
||||
contents: [],
|
||||
visible: true,
|
||||
active: true,
|
||||
},
|
||||
{
|
||||
index: 1,
|
||||
id: 1,
|
||||
name: 'Rooms',
|
||||
color: '#00ddff',
|
||||
contents: [],
|
||||
visible: true,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
id: 0,
|
||||
name: 'Base',
|
||||
color: '#00ddff',
|
||||
contents: [],
|
||||
visible: true,
|
||||
active: true,
|
||||
},
|
||||
],
|
||||
{ writeDefaults: false }
|
||||
);
|
||||
@ -110,15 +135,49 @@ const selectTool = (newTool: ToolType, newSubTool?: SubToolType) => {
|
||||
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(() => {
|
||||
const cleanUp = module.value.initialize(
|
||||
canvas.value,
|
||||
JSON.parse(JSON.stringify(serializedLayers.value))
|
||||
deepUnref(serializedLayers.value)
|
||||
);
|
||||
|
||||
const events: Record<string, (e: CustomEvent) => void> = {
|
||||
'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>) => {
|
||||
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 { BezierSegment, Layer, Line, LineSegment, Vec2 } from './interfaces';
|
||||
import {
|
||||
BezierSegment,
|
||||
Layer,
|
||||
LayerObject,
|
||||
Line,
|
||||
LineSegment,
|
||||
Vec2,
|
||||
} from './interfaces';
|
||||
import { HousePlannerCanvasTools } from './tools';
|
||||
import {
|
||||
rad2deg,
|
||||
@ -139,6 +146,67 @@ export class HousePlannerCanvas {
|
||||
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) {
|
||||
if (e.target !== document.body && e.target != null) return;
|
||||
e.preventDefault();
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { SubToolType, ToolType } from './types';
|
||||
import { LayerObjectType, SubToolType, ToolType } from './types';
|
||||
|
||||
export type Vec2 = [number, number];
|
||||
export interface LineSegment {
|
||||
@ -12,10 +12,11 @@ export interface BezierSegment extends LineSegment {
|
||||
}
|
||||
|
||||
export interface LayerObject {
|
||||
id: number;
|
||||
name: string;
|
||||
visible: boolean;
|
||||
selected: boolean;
|
||||
type: 'line' | 'room' | 'curve' | 'object';
|
||||
type: LayerObjectType;
|
||||
}
|
||||
|
||||
export interface Line extends LayerObject {
|
||||
@ -29,7 +30,7 @@ export interface Line extends LayerObject {
|
||||
}
|
||||
|
||||
export interface Layer {
|
||||
index: number;
|
||||
id: number;
|
||||
contents: LayerObject[];
|
||||
name: string;
|
||||
color: string;
|
||||
|
@ -13,6 +13,7 @@ import {
|
||||
import { ToolType, SubToolType, BezierControl, LineControl } from './types';
|
||||
import {
|
||||
vec2Add,
|
||||
vec2Distance,
|
||||
vec2Equals,
|
||||
vec2InCircle,
|
||||
vec2Inverse,
|
||||
@ -24,6 +25,7 @@ export class HousePlannerCanvasTools {
|
||||
public selectedLayer?: Layer;
|
||||
public selectedObjects: LayerObject[] = [];
|
||||
public mousePosition: Vec2 = [0, 0];
|
||||
public mouseClickPosition: Vec2 = [0, 0];
|
||||
public mousePositionSnapped: Vec2 = [0, 0];
|
||||
public gridSnap = true;
|
||||
public gridSnapScale = 8;
|
||||
@ -70,6 +72,18 @@ export class HousePlannerCanvasTools {
|
||||
}
|
||||
this.selectedLayer = layer;
|
||||
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) {
|
||||
@ -81,6 +95,7 @@ export class HousePlannerCanvasTools {
|
||||
this.selectedObjects.length = 0;
|
||||
}
|
||||
this.manager.draw();
|
||||
this.selectedEvent();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -91,6 +106,7 @@ export class HousePlannerCanvasTools {
|
||||
object.selected = false;
|
||||
this.selectedObjects.splice(foundAt, 1);
|
||||
this.manager.draw();
|
||||
this.selectedEvent();
|
||||
}
|
||||
return;
|
||||
}
|
||||
@ -100,6 +116,7 @@ export class HousePlannerCanvasTools {
|
||||
object.selected = true;
|
||||
this.selectedObjects.push(object);
|
||||
this.manager.draw();
|
||||
this.selectedEvent();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -111,6 +128,7 @@ export class HousePlannerCanvasTools {
|
||||
|
||||
this.selectedObjects = [object];
|
||||
this.manager.draw();
|
||||
this.selectedEvent();
|
||||
}
|
||||
|
||||
getMousedObject() {
|
||||
@ -258,6 +276,7 @@ export class HousePlannerCanvasTools {
|
||||
}
|
||||
onMouseDown(e: MouseEvent) {
|
||||
this.mousePosition = [e.clientX, e.clientY];
|
||||
this.mouseClickPosition = [...this.mousePosition];
|
||||
this.mousePositionSnapped = this.gridSnap
|
||||
? vec2Snap(this.mousePosition, this.gridSnapScale)
|
||||
: this.mousePosition;
|
||||
@ -302,6 +321,7 @@ export class HousePlannerCanvasTools {
|
||||
e.preventDefault();
|
||||
const touch = e.touches[0] || e.changedTouches[0];
|
||||
this.mousePosition = [touch.pageX, touch.pageY];
|
||||
this.mouseClickPosition = [...this.mousePosition];
|
||||
this.mousePositionSnapped = this.gridSnap
|
||||
? vec2Snap(this.mousePosition, this.gridSnapScale)
|
||||
: this.mousePosition;
|
||||
@ -435,6 +455,12 @@ export class HousePlannerCanvasTools {
|
||||
}
|
||||
this.drawingLine = null;
|
||||
this.manager.draw();
|
||||
|
||||
this.canvas.dispatchEvent(
|
||||
new CustomEvent('hpc:update', {
|
||||
detail: { event: 'draw-cancel' },
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -543,6 +569,14 @@ export class HousePlannerCanvasTools {
|
||||
}
|
||||
|
||||
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.tool === 'line') {
|
||||
this.startLine();
|
||||
@ -588,6 +622,19 @@ export class HousePlannerCanvasTools {
|
||||
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() {
|
||||
if (!this.selectedLayer?.visible) return;
|
||||
if (this.drawingLine) {
|
||||
@ -640,6 +687,7 @@ export class HousePlannerCanvasTools {
|
||||
}
|
||||
|
||||
const newLineObject: Line = {
|
||||
id: this.getSequentialId(),
|
||||
name: 'New Line',
|
||||
type: this.subTool!,
|
||||
visible: true,
|
||||
|
@ -9,3 +9,4 @@ export type BezierControl = [
|
||||
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 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