revamp tooling a bit
This commit is contained in:
parent
caa34afd3e
commit
668b2c5001
47
src/components/ColorInput.vue
Normal file
47
src/components/ColorInput.vue
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<template>
|
||||||
|
<button
|
||||||
|
class="relative"
|
||||||
|
@click="inputRef.click()"
|
||||||
|
:class="[
|
||||||
|
'flex items-center space-x-1 rounded-full bg-gray-100 py-1 hover:bg-gray-50 focus:ring-2 focus:ring-blue-200',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
:id="forId"
|
||||||
|
:value="modelValue"
|
||||||
|
class="absolute left-0 bottom-0 h-0 w-0 opacity-0"
|
||||||
|
type="color"
|
||||||
|
ref="inputRef"
|
||||||
|
@change="valueChanged(($event.target as HTMLInputElement).value)"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
:style="{ backgroundColor: modelValue }"
|
||||||
|
class="h-6 w-6 rounded-full ring-1 ring-gray-400"
|
||||||
|
></div>
|
||||||
|
<span>{{ modelValue }}</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
forId?: string;
|
||||||
|
modelValue: string;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
modelValue: '#000000',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const inputRef = ref();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', value: string): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const valueChanged = (value: string) => {
|
||||||
|
emit('update:modelValue', value);
|
||||||
|
};
|
||||||
|
</script>
|
@ -4,6 +4,7 @@
|
|||||||
<PlannerToolbar>
|
<PlannerToolbar>
|
||||||
<PlannerTool
|
<PlannerTool
|
||||||
v-for="toolItem of toolbar"
|
v-for="toolItem of toolbar"
|
||||||
|
:title="toolItem.title"
|
||||||
:icon="toolItem.icon"
|
:icon="toolItem.icon"
|
||||||
:multiple="!!toolItem.children?.length"
|
:multiple="!!toolItem.children?.length"
|
||||||
:selected="tool === toolItem.tool"
|
:selected="tool === toolItem.tool"
|
||||||
@ -12,6 +13,7 @@
|
|||||||
<template v-if="toolItem.children?.length">
|
<template v-if="toolItem.children?.length">
|
||||||
<PlannerTool
|
<PlannerTool
|
||||||
v-for="subItem of toolItem.children"
|
v-for="subItem of toolItem.children"
|
||||||
|
:title="subItem.title"
|
||||||
:icon="subItem.icon"
|
:icon="subItem.icon"
|
||||||
:selected="
|
:selected="
|
||||||
tool === subItem.tool &&
|
tool === subItem.tool &&
|
||||||
@ -53,11 +55,7 @@ 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 {
|
import { LayerObjectType } from '../../modules/house-planner/types';
|
||||||
LayerObjectType,
|
|
||||||
SubToolType,
|
|
||||||
ToolType,
|
|
||||||
} from '../../modules/house-planner/types';
|
|
||||||
import deepUnref from '../../utils/deep-unref';
|
import deepUnref from '../../utils/deep-unref';
|
||||||
import { ToolbarTool } from './interfaces/toolbar.interfaces';
|
import { ToolbarTool } from './interfaces/toolbar.interfaces';
|
||||||
import PlannerLayerPanel from './PlannerLayerPanel.vue';
|
import PlannerLayerPanel from './PlannerLayerPanel.vue';
|
||||||
@ -69,8 +67,8 @@ import PlannerToolbar from './PlannerToolbar.vue';
|
|||||||
|
|
||||||
const canvas = ref();
|
const canvas = ref();
|
||||||
const module = shallowRef(new HousePlanner());
|
const module = shallowRef(new HousePlanner());
|
||||||
const tool = ref<ToolType>('line');
|
const tool = ref<string | undefined>('move');
|
||||||
const subTool = ref<SubToolType>('line');
|
const subTool = ref<string | undefined>(undefined);
|
||||||
const serializedLayers = useSessionStorage<Layer[]>(
|
const serializedLayers = useSessionStorage<Layer[]>(
|
||||||
'roomData',
|
'roomData',
|
||||||
[
|
[
|
||||||
@ -98,7 +96,7 @@ const toolbar: ToolbarTool[] = [
|
|||||||
{
|
{
|
||||||
title: 'Move',
|
title: 'Move',
|
||||||
icon: ArrowsPointingOutIcon,
|
icon: ArrowsPointingOutIcon,
|
||||||
tool: null,
|
tool: 'move',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Draw',
|
title: 'Draw',
|
||||||
@ -128,9 +126,9 @@ const toolbar: ToolbarTool[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const selectTool = (newTool: ToolType, newSubTool?: SubToolType) => {
|
const selectTool = (newTool?: string, newSubTool?: string) => {
|
||||||
if (newTool === tool.value && !newSubTool) {
|
if (newTool === tool.value && !newSubTool) {
|
||||||
newTool = null;
|
newTool = undefined;
|
||||||
}
|
}
|
||||||
module.value.manager?.tools.setTool(newTool, newSubTool);
|
module.value.manager?.tools.setTool(newTool, newSubTool);
|
||||||
};
|
};
|
||||||
@ -181,7 +179,7 @@ onMounted(() => {
|
|||||||
},
|
},
|
||||||
'hpc:tool': (e: CustomEvent<ToolEvent>) => {
|
'hpc:tool': (e: CustomEvent<ToolEvent>) => {
|
||||||
tool.value = e.detail.primary;
|
tool.value = e.detail.primary;
|
||||||
subTool.value = e.detail.secondary;
|
subTool.value = e.detail.secondary as string;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<PlannerSidebar title="Layers">
|
<PlannerSidebar title="Layers">
|
||||||
<div class="bg-white-50 flex flex-col">
|
<div class="bg-white-50 flex h-full flex-col overflow-auto">
|
||||||
<template v-for="layer of layers">
|
<template v-for="layer of layers">
|
||||||
<button
|
<button
|
||||||
@dblclick="editingLayerName = layer.id"
|
@dblclick="editingLayerName = layer.id"
|
||||||
|
@ -1,57 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<PlannerSidebar title="Properties">
|
<PlannerSidebar title="Properties">
|
||||||
<div
|
<div
|
||||||
class="bg-white-50 flex flex-col"
|
class="bg-white-50 flex h-full flex-col overflow-auto"
|
||||||
v-if="selectedObject && applicableProperties"
|
v-if="selectedObject && applicableProperties"
|
||||||
>
|
>
|
||||||
<div
|
<PropertyFormItem
|
||||||
class="grid grid-cols-2 px-2 py-2"
|
|
||||||
v-for="prop of applicableProperties.properties"
|
v-for="prop of applicableProperties.properties"
|
||||||
>
|
:id="selectedObject.id"
|
||||||
<label :for="`${prop.key}-${selectedObject.id}`">{{
|
:prop="prop"
|
||||||
prop.title
|
:value="selectedObject[prop.key as keyof typeof selectedObject]"
|
||||||
}}</label>
|
@update="(newValue) => updateProp(prop, newValue)"
|
||||||
<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>
|
</div>
|
||||||
</PlannerSidebar>
|
</PlannerSidebar>
|
||||||
</template>
|
</template>
|
||||||
@ -64,6 +23,7 @@ import {
|
|||||||
ObjectProperty,
|
ObjectProperty,
|
||||||
} from './interfaces/properties.interfaces';
|
} from './interfaces/properties.interfaces';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
import PropertyFormItem from './PropertyFormItem.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
layers: Layer[];
|
layers: Layer[];
|
||||||
@ -100,6 +60,18 @@ const lineProps: ObjectProperty[] = [
|
|||||||
{ value: 'square', title: 'Square' },
|
{ value: 'square', title: 'Square' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'lineJoin',
|
||||||
|
title: 'Line Join',
|
||||||
|
type: 'select',
|
||||||
|
groupable: true,
|
||||||
|
options: [
|
||||||
|
{ value: undefined, title: '' },
|
||||||
|
{ value: 'miter', title: 'Miter' },
|
||||||
|
{ value: 'bevel', title: 'Bevel' },
|
||||||
|
{ value: 'round', title: 'Round' },
|
||||||
|
],
|
||||||
|
},
|
||||||
{ key: 'closed', title: 'Closed', type: 'boolean', groupable: true },
|
{ key: 'closed', title: 'Closed', type: 'boolean', groupable: true },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative flex flex-row overflow-hidden px-2 pb-4 pr-0">
|
<div class="relative flex flex-row px-2 pb-4 pr-0">
|
||||||
<button
|
<button
|
||||||
:class="[
|
:class="[
|
||||||
open ? 'bg-gray-200' : 'bg-white',
|
open ? 'bg-gray-200' : 'bg-white',
|
||||||
@ -19,10 +19,11 @@
|
|||||||
leave-to-class="max-w-0"
|
leave-to-class="max-w-0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="h-screen max-h-96 rounded-bl-md bg-white shadow-lg ring-1 ring-black ring-opacity-5"
|
class="h-full rounded-bl-md bg-white shadow-lg ring-1 ring-black ring-opacity-5"
|
||||||
|
style="height: 40vh"
|
||||||
v-if="open"
|
v-if="open"
|
||||||
>
|
>
|
||||||
<div class="w-screen max-w-xs">
|
<div class="flex h-full w-screen max-w-xs flex-col">
|
||||||
<slot name="header">
|
<slot name="header">
|
||||||
<div
|
<div
|
||||||
class="select-none bg-gray-200 px-2 py-1 font-bold uppercase text-gray-400"
|
class="select-none bg-gray-200 px-2 py-1 font-bold uppercase text-gray-400"
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div class="absolute right-0 top-0 bottom-0 z-10 my-4 overflow-hidden">
|
||||||
class="absolute right-0 top-0 z-10 mt-4 flex flex-col items-end space-y-2"
|
<div class="flex h-full flex-col items-end space-y-2">
|
||||||
>
|
<slot />
|
||||||
<slot />
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
|
:title="title"
|
||||||
:class="[
|
:class="[
|
||||||
selected && !multiple ? ' bg-purple-400 hover:bg-purple-300' : '',
|
selected && !multiple ? ' bg-purple-400 hover:bg-purple-300' : '',
|
||||||
selected && multiple ? ' bg-purple-500 hover:bg-purple-400' : '',
|
selected && multiple ? ' bg-purple-500 hover:bg-purple-400' : '',
|
||||||
@ -17,6 +18,7 @@
|
|||||||
'flex h-12 w-12 items-center justify-center rounded-full shadow-md ring-1 ring-black ring-opacity-5',
|
'flex h-12 w-12 items-center justify-center rounded-full shadow-md ring-1 ring-black ring-opacity-5',
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
|
<span class="sr-only">{{ title }}</span>
|
||||||
<component :is="icon" class="h-8 w-8" />
|
<component :is="icon" class="h-8 w-8" />
|
||||||
</button>
|
</button>
|
||||||
<Transition
|
<Transition
|
||||||
@ -41,6 +43,7 @@ import type { Component } from 'vue';
|
|||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
icon: Component;
|
icon: Component;
|
||||||
|
title: string;
|
||||||
multiple?: boolean;
|
multiple?: boolean;
|
||||||
selected?: boolean;
|
selected?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
70
src/components/house-planner/PropertyFormItem.vue
Normal file
70
src/components/house-planner/PropertyFormItem.vue
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="mx-2 grid grid-cols-2 items-center border-b-2 border-gray-100 py-2 last:border-b-0"
|
||||||
|
:key="key"
|
||||||
|
>
|
||||||
|
<label :for="key">{{ prop.title }}</label>
|
||||||
|
<input
|
||||||
|
v-if="prop.type === 'string'"
|
||||||
|
:class="classes"
|
||||||
|
:id="key"
|
||||||
|
type="text"
|
||||||
|
:value="value"
|
||||||
|
@input="emit('update', ($event.target as HTMLInputElement)?.value)"
|
||||||
|
/>
|
||||||
|
<ColorInput
|
||||||
|
v-else-if="prop.type === 'color'"
|
||||||
|
:for-id="key"
|
||||||
|
:model-value="(value as string)"
|
||||||
|
@update:model-value="(newValue) => emit('update', newValue)"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-else-if="prop.type === 'number'"
|
||||||
|
:class="classes"
|
||||||
|
:id="key"
|
||||||
|
type="number"
|
||||||
|
:value="value"
|
||||||
|
@input="emit('update', ($event.target as HTMLInputElement)?.value)"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-else-if="prop.type === 'boolean'"
|
||||||
|
:class="classes"
|
||||||
|
:id="key"
|
||||||
|
type="checkbox"
|
||||||
|
:checked="(value as boolean)"
|
||||||
|
@input="emit('update', ($event.target as HTMLInputElement)?.checked)"
|
||||||
|
/>
|
||||||
|
<select
|
||||||
|
v-else-if="prop.type === 'select'"
|
||||||
|
:class="classes"
|
||||||
|
:id="key"
|
||||||
|
:value="value"
|
||||||
|
@input="emit('update', ($event.target as HTMLInputElement)?.value)"
|
||||||
|
>
|
||||||
|
<option v-for="option of prop.options" :value="option.value">
|
||||||
|
{{ option.title }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from '@vue/reactivity';
|
||||||
|
import ColorInput from '../ColorInput.vue';
|
||||||
|
import { ObjectProperty } from './interfaces/properties.interfaces';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
id: number;
|
||||||
|
prop: ObjectProperty;
|
||||||
|
value: unknown;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update', value: unknown): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const key = computed(() => `${props.prop.key}-${props.id}`);
|
||||||
|
const classes = [
|
||||||
|
'px-1 py-1 focus:ring-2 focus:ring-blue-200 border-gray-300 rounded-sm',
|
||||||
|
];
|
||||||
|
</script>
|
@ -1,10 +1,9 @@
|
|||||||
import type { Component } from 'vue';
|
import type { Component } from 'vue';
|
||||||
import { SubToolType, ToolType } from '../../../modules/house-planner/types';
|
|
||||||
|
|
||||||
export interface ToolbarTool {
|
export interface ToolbarTool {
|
||||||
title: string;
|
title: string;
|
||||||
icon: Component;
|
icon: Component;
|
||||||
tool: ToolType;
|
tool: string;
|
||||||
subTool?: SubToolType;
|
subTool?: string;
|
||||||
children?: ToolbarTool[];
|
children?: ToolbarTool[];
|
||||||
}
|
}
|
||||||
|
@ -118,9 +118,8 @@ export class HousePlannerCanvas {
|
|||||||
this.ctx.strokeStyle = overrideColor || line.color || '#000';
|
this.ctx.strokeStyle = overrideColor || line.color || '#000';
|
||||||
this.ctx.lineWidth = line.width + overBounds;
|
this.ctx.lineWidth = line.width + overBounds;
|
||||||
|
|
||||||
if (line.lineCap) {
|
this.ctx.lineCap = line.lineCap || 'butt';
|
||||||
this.ctx.lineCap = line.lineCap;
|
this.ctx.lineJoin = line.lineJoin || 'miter';
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isOnLine(line: Line, x: number, y: number, selectError = 16) {
|
isOnLine(line: Line, x: number, y: number, selectError = 16) {
|
||||||
|
@ -14,6 +14,8 @@ export class HousePlanner {
|
|||||||
this.manager.tools.selectLayer(
|
this.manager.tools.selectLayer(
|
||||||
initialData[initialData.findIndex((layer) => layer.active)]
|
initialData[initialData.findIndex((layer) => layer.active)]
|
||||||
);
|
);
|
||||||
|
this.manager.tools.setInitialSelection();
|
||||||
|
this.manager.tools.setTool('move');
|
||||||
this.manager.draw();
|
this.manager.draw();
|
||||||
return () => this.cleanUp();
|
return () => this.cleanUp();
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { LayerObjectType, SubToolType, ToolType } from './types';
|
import { LayerObjectType } from './types';
|
||||||
|
|
||||||
export type Vec2 = [number, number];
|
export type Vec2 = [number, number];
|
||||||
export interface LineSegment {
|
export interface LineSegment {
|
||||||
@ -25,6 +25,7 @@ export interface Line extends LayerObject {
|
|||||||
color: string;
|
color: string;
|
||||||
render?: Path2D;
|
render?: Path2D;
|
||||||
lineCap?: CanvasLineCap;
|
lineCap?: CanvasLineCap;
|
||||||
|
lineJoin?: CanvasLineJoin;
|
||||||
closed?: boolean;
|
closed?: boolean;
|
||||||
lineDash?: number[];
|
lineDash?: number[];
|
||||||
}
|
}
|
||||||
@ -50,6 +51,25 @@ export interface UpdateEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ToolEvent {
|
export interface ToolEvent {
|
||||||
primary: ToolType;
|
primary: string;
|
||||||
secondary: SubToolType;
|
secondary?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ICanvasToolBase<U = undefined> {
|
||||||
|
name: string;
|
||||||
|
subTool: U | undefined;
|
||||||
|
drawHighlights(): void;
|
||||||
|
drawControls(): void;
|
||||||
|
mouseDown(targetObject?: LayerObject): void;
|
||||||
|
mouseMoved(mouse: Vec2, offset: Vec2, mouseAbsolute: Vec2): void;
|
||||||
|
mouseUp(moved: boolean): void;
|
||||||
|
enterPress(e: KeyboardEvent): void;
|
||||||
|
escapePress(e: KeyboardEvent): void;
|
||||||
|
setSubTool(subTool?: U): void;
|
||||||
|
activate(): void;
|
||||||
|
deactivate(): void;
|
||||||
|
selectionDeleted(): void;
|
||||||
|
selectionChanged(selection: LayerObject[]): void;
|
||||||
|
emitEvent(e: CustomEvent): void;
|
||||||
|
isToolCancelable(): boolean;
|
||||||
}
|
}
|
||||||
|
@ -1,25 +1,17 @@
|
|||||||
import type { HousePlannerCanvas } from './canvas';
|
import type { HousePlannerCanvas } from './canvas';
|
||||||
import { HousePlannerCanvasHistory } from './history';
|
import { HousePlannerCanvasHistory } from './history';
|
||||||
import {
|
import {
|
||||||
BezierSegment,
|
|
||||||
History,
|
History,
|
||||||
|
ICanvasToolBase,
|
||||||
Layer,
|
Layer,
|
||||||
LayerObject,
|
LayerObject,
|
||||||
Line,
|
Line,
|
||||||
LineSegment,
|
|
||||||
ToolEvent,
|
ToolEvent,
|
||||||
Vec2,
|
Vec2,
|
||||||
} from './interfaces';
|
} from './interfaces';
|
||||||
import { ToolType, SubToolType, BezierControl, LineControl } from './types';
|
import { LineTool } from './tools/line';
|
||||||
import {
|
import { MoveTool } from './tools/move';
|
||||||
vec2Add,
|
import { vec2Distance, vec2Snap, vec2Sub } from './utils';
|
||||||
vec2Distance,
|
|
||||||
vec2Equals,
|
|
||||||
vec2InCircle,
|
|
||||||
vec2Inverse,
|
|
||||||
vec2Snap,
|
|
||||||
vec2Sub,
|
|
||||||
} from './utils';
|
|
||||||
|
|
||||||
export class HousePlannerCanvasTools {
|
export class HousePlannerCanvasTools {
|
||||||
public selectedLayer?: Layer;
|
public selectedLayer?: Layer;
|
||||||
@ -29,24 +21,21 @@ export class HousePlannerCanvasTools {
|
|||||||
public mousePositionSnapped: Vec2 = [0, 0];
|
public mousePositionSnapped: Vec2 = [0, 0];
|
||||||
public gridSnap = true;
|
public gridSnap = true;
|
||||||
public gridSnapScale = 8;
|
public gridSnapScale = 8;
|
||||||
public tool: ToolType = 'line';
|
public tool?: ICanvasToolBase<unknown>;
|
||||||
public subTool: SubToolType = 'line';
|
public tools: Record<string, ICanvasToolBase<unknown>> = {
|
||||||
|
['move']: new MoveTool(this),
|
||||||
|
['line']: new LineTool(this),
|
||||||
|
};
|
||||||
public history = new HousePlannerCanvasHistory();
|
public history = new HousePlannerCanvasHistory();
|
||||||
public lastStrokeWidth = 16;
|
public lastStrokeWidth = 16;
|
||||||
public lastColor = '#000';
|
public lastColor = '#000000';
|
||||||
|
public holdShift = false;
|
||||||
|
public selectError = 16;
|
||||||
private dragging = false;
|
private dragging = false;
|
||||||
private pinching = false;
|
private pinching = false;
|
||||||
private lastPinchLength = 0;
|
private lastPinchLength = 0;
|
||||||
private moved = false;
|
private moved = false;
|
||||||
private holdShift = false;
|
|
||||||
private selectError = 16;
|
|
||||||
private bezierControls: BezierSegment[] = [];
|
|
||||||
private lineControls: LineSegment[] = [];
|
|
||||||
private handlingBezier: BezierControl | null = null;
|
|
||||||
private handlingLine: LineControl | null = null;
|
|
||||||
private drawingLine: Line | null = null;
|
|
||||||
private clickedOn: LayerObject | null = null;
|
private clickedOn: LayerObject | null = null;
|
||||||
private movingObject = false;
|
|
||||||
|
|
||||||
constructor(public manager: HousePlannerCanvas) {}
|
constructor(public manager: HousePlannerCanvas) {}
|
||||||
|
|
||||||
@ -93,6 +82,7 @@ export class HousePlannerCanvasTools {
|
|||||||
object.selected = false;
|
object.selected = false;
|
||||||
});
|
});
|
||||||
this.selectedObjects.length = 0;
|
this.selectedObjects.length = 0;
|
||||||
|
this.tool?.selectionChanged(this.selectedObjects);
|
||||||
}
|
}
|
||||||
this.manager.draw();
|
this.manager.draw();
|
||||||
this.selectedEvent();
|
this.selectedEvent();
|
||||||
@ -105,6 +95,7 @@ export class HousePlannerCanvasTools {
|
|||||||
const foundAt = this.selectedObjects.indexOf(object);
|
const foundAt = this.selectedObjects.indexOf(object);
|
||||||
object.selected = false;
|
object.selected = false;
|
||||||
this.selectedObjects.splice(foundAt, 1);
|
this.selectedObjects.splice(foundAt, 1);
|
||||||
|
this.tool?.selectionChanged(this.selectedObjects);
|
||||||
this.manager.draw();
|
this.manager.draw();
|
||||||
this.selectedEvent();
|
this.selectedEvent();
|
||||||
}
|
}
|
||||||
@ -115,6 +106,7 @@ export class HousePlannerCanvasTools {
|
|||||||
if (add) {
|
if (add) {
|
||||||
object.selected = true;
|
object.selected = true;
|
||||||
this.selectedObjects.push(object);
|
this.selectedObjects.push(object);
|
||||||
|
this.tool?.selectionChanged(this.selectedObjects);
|
||||||
this.manager.draw();
|
this.manager.draw();
|
||||||
this.selectedEvent();
|
this.selectedEvent();
|
||||||
return;
|
return;
|
||||||
@ -127,10 +119,18 @@ export class HousePlannerCanvasTools {
|
|||||||
object.selected = true;
|
object.selected = true;
|
||||||
|
|
||||||
this.selectedObjects = [object];
|
this.selectedObjects = [object];
|
||||||
|
this.tool?.selectionChanged(this.selectedObjects);
|
||||||
this.manager.draw();
|
this.manager.draw();
|
||||||
this.selectedEvent();
|
this.selectedEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setInitialSelection() {
|
||||||
|
if (!this.selectedLayer) return;
|
||||||
|
this.selectedObjects = this.selectedLayer.contents.filter(
|
||||||
|
(object) => object.selected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
getMousedObject() {
|
getMousedObject() {
|
||||||
if (!this.selectedLayer?.visible) return null;
|
if (!this.selectedLayer?.visible) return null;
|
||||||
const [x, y] = this.mousePosition;
|
const [x, y] = this.mousePosition;
|
||||||
@ -182,93 +182,55 @@ export class HousePlannerCanvasTools {
|
|||||||
}
|
}
|
||||||
// TODO: other kinds of objects
|
// TODO: other kinds of objects
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
drawBezierControls(bezier: BezierSegment, previousEnd: Vec2) {
|
this.tool?.drawHighlights();
|
||||||
this.bezierControls.push(bezier);
|
|
||||||
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.selectError / 2, 0, 2 * Math.PI);
|
|
||||||
this.ctx.fill();
|
|
||||||
|
|
||||||
this.ctx.beginPath();
|
|
||||||
this.ctx.arc(cp2x, cp2y, this.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) {
|
|
||||||
this.lineControls.push(line);
|
|
||||||
const [endx, endy] = line.end;
|
|
||||||
this.ctx.fillStyle = '#00ddffaa';
|
|
||||||
|
|
||||||
this.ctx.beginPath();
|
|
||||||
this.ctx.arc(endx, endy, this.selectError / 2, 0, 2 * Math.PI);
|
|
||||||
this.ctx.fill();
|
|
||||||
|
|
||||||
if (line.start) {
|
|
||||||
const [startx, starty] = line.start;
|
|
||||||
this.ctx.beginPath();
|
|
||||||
this.ctx.arc(startx, starty, this.selectError / 2, 0, 2 * Math.PI);
|
|
||||||
this.ctx.fill();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
drawControls() {
|
drawControls() {
|
||||||
this.bezierControls.length = 0;
|
this.tool?.drawControls();
|
||||||
this.lineControls.length = 0;
|
|
||||||
for (const object of this.selectedObjects) {
|
|
||||||
const line = object as Line;
|
|
||||||
if (line.segments && line.render) {
|
|
||||||
let lastSegment = null;
|
|
||||||
for (const segment of line.segments) {
|
|
||||||
const bezier = segment as BezierSegment;
|
|
||||||
const previousPoint = lastSegment ? lastSegment.end : segment.start!;
|
|
||||||
if (bezier.startControl && bezier.endControl) {
|
|
||||||
this.drawBezierControls(bezier, previousPoint);
|
|
||||||
}
|
|
||||||
this.drawLineControls(segment, previousPoint);
|
|
||||||
lastSegment = segment;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// TODO: other kinds of objects
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.tool === 'line') {
|
|
||||||
if (this.selectedObjects.length) return;
|
|
||||||
const [mx, my] = this.mousePositionSnapped;
|
|
||||||
this.ctx.fillStyle = '#00ddffaa';
|
|
||||||
this.ctx.beginPath();
|
|
||||||
this.ctx.arc(mx, my, this.selectError / 2, 0, 2 * Math.PI);
|
|
||||||
this.ctx.fill();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanUp() {}
|
cleanUp() {}
|
||||||
|
|
||||||
setTool(tool: typeof this.tool, subTool?: typeof this.subTool) {
|
setTool(tool?: string, subTool?: string) {
|
||||||
this.tool = tool;
|
if (!tool) {
|
||||||
if (subTool !== undefined) this.subTool = subTool;
|
this.tool?.deactivate();
|
||||||
|
this.setTool('move');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.tool?.name === tool) {
|
||||||
|
if (!subTool) return;
|
||||||
|
this.tool.setSubTool(subTool);
|
||||||
|
this.canvas.dispatchEvent(
|
||||||
|
new CustomEvent<ToolEvent>('hpc:tool', {
|
||||||
|
detail: { primary: this.tool.name, secondary: this.tool.subTool },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
this.manager.draw();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const findTool = this.tools[tool];
|
||||||
|
if (!findTool) return;
|
||||||
|
|
||||||
|
if (this.tool) this.tool.deactivate();
|
||||||
|
this.tool = findTool;
|
||||||
|
this.tool.activate();
|
||||||
|
if (subTool) {
|
||||||
|
this.tool.setSubTool(subTool);
|
||||||
|
}
|
||||||
|
|
||||||
this.canvas.dispatchEvent(
|
this.canvas.dispatchEvent(
|
||||||
new CustomEvent<ToolEvent>('hpc:tool', {
|
new CustomEvent<ToolEvent>('hpc:tool', {
|
||||||
detail: { primary: this.tool, secondary: this.subTool },
|
detail: { primary: this.tool.name, secondary: this.tool.subTool },
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
this.manager.draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
get activeTool() {
|
||||||
|
return this.tool?.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
onMouseMove(e: MouseEvent) {
|
onMouseMove(e: MouseEvent) {
|
||||||
@ -404,64 +366,16 @@ export class HousePlannerCanvasTools {
|
|||||||
|
|
||||||
onKeyUp(e: KeyboardEvent) {
|
onKeyUp(e: KeyboardEvent) {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
if (this.drawingLine && this.selectedLayer) {
|
|
||||||
if (this.subTool === 'room') {
|
|
||||||
this.drawingLine.closed = true;
|
|
||||||
}
|
|
||||||
this.drawingLine.segments.splice(
|
|
||||||
this.drawingLine.segments.length - 1,
|
|
||||||
1
|
|
||||||
);
|
|
||||||
this.history.appendToHistory([
|
|
||||||
{
|
|
||||||
object: this.selectedLayer,
|
|
||||||
property: 'contents',
|
|
||||||
value: [...this.selectedLayer.contents].filter(
|
|
||||||
(item) => item !== this.drawingLine
|
|
||||||
),
|
|
||||||
} as History<typeof this.selectedLayer>,
|
|
||||||
{
|
|
||||||
object: this,
|
|
||||||
property: 'selectedObjects',
|
|
||||||
value: [],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
this.canvas.dispatchEvent(
|
|
||||||
new CustomEvent('hpc:newobject', {
|
|
||||||
detail: this.drawingLine,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
this.canvas.dispatchEvent(
|
|
||||||
new CustomEvent('hpc:update', {
|
|
||||||
detail: { event: 'newobject', object: this.drawingLine },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
this.drawingLine = null;
|
|
||||||
this.manager.draw();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
if (this.selectedObjects.length === 0 && this.tool && !this.drawingLine) {
|
this.tool?.escapePress(e);
|
||||||
this.setTool(null);
|
|
||||||
|
if (this.selectedObjects.length === 0 && this.tool?.isToolCancelable()) {
|
||||||
|
this.setTool();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.selectObject(null);
|
this.selectObject(null);
|
||||||
|
|
||||||
if (this.drawingLine && this.selectedLayer) {
|
|
||||||
const indexOf = this.selectedLayer.contents.indexOf(this.drawingLine);
|
|
||||||
if (indexOf > -1) {
|
|
||||||
this.selectedLayer.contents.splice(indexOf, 1);
|
|
||||||
}
|
|
||||||
this.drawingLine = null;
|
|
||||||
this.manager.draw();
|
|
||||||
|
|
||||||
this.canvas.dispatchEvent(
|
|
||||||
new CustomEvent('hpc:update', {
|
|
||||||
detail: { event: 'draw-cancel' },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.key === 'Shift') {
|
if (e.key === 'Shift') {
|
||||||
@ -492,6 +406,8 @@ export class HousePlannerCanvasTools {
|
|||||||
);
|
);
|
||||||
|
|
||||||
this.selectedObjects.length = 0;
|
this.selectedObjects.length = 0;
|
||||||
|
this.tool?.selectionDeleted();
|
||||||
|
this.tool?.selectionChanged([]);
|
||||||
this.manager.draw();
|
this.manager.draw();
|
||||||
this.canvas.dispatchEvent(
|
this.canvas.dispatchEvent(
|
||||||
new CustomEvent('hpc:update', {
|
new CustomEvent('hpc:update', {
|
||||||
@ -500,72 +416,9 @@ export class HousePlannerCanvasTools {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private grabbedBezierControl() {
|
|
||||||
let clickedOnControl: BezierControl | null = null;
|
|
||||||
for (const bezier of this.bezierControls) {
|
|
||||||
for (const control of ['startControl', 'endControl']) {
|
|
||||||
const asType = control as 'startControl' | 'endControl';
|
|
||||||
if (
|
|
||||||
vec2InCircle(bezier[asType], this.mousePosition, this.selectError / 2)
|
|
||||||
) {
|
|
||||||
clickedOnControl = [bezier, asType, bezier[asType]];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return clickedOnControl;
|
|
||||||
}
|
|
||||||
|
|
||||||
private grabbedLineControl() {
|
|
||||||
let clickedOnControl: LineControl | null = null;
|
|
||||||
for (const line of this.lineControls) {
|
|
||||||
for (const control of ['start', 'end']) {
|
|
||||||
const asType = control as 'start' | 'end';
|
|
||||||
if (
|
|
||||||
line[asType] &&
|
|
||||||
vec2InCircle(
|
|
||||||
line[asType] as Vec2,
|
|
||||||
this.mousePosition,
|
|
||||||
this.selectError / 2
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
clickedOnControl = [line, asType, line[asType] as Vec2];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return clickedOnControl;
|
|
||||||
}
|
|
||||||
|
|
||||||
private translate(offset: Vec2) {
|
|
||||||
for (const object of this.selectedObjects) {
|
|
||||||
if ((object as Line).segments) {
|
|
||||||
for (const segment of (object as Line).segments) {
|
|
||||||
if ((segment as BezierSegment).startControl) {
|
|
||||||
const bezier = segment as BezierSegment;
|
|
||||||
bezier.startControl = vec2Add(bezier.startControl, offset);
|
|
||||||
bezier.endControl = vec2Add(bezier.endControl, offset);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (segment.start) {
|
|
||||||
segment.start = vec2Add(segment.start, offset);
|
|
||||||
}
|
|
||||||
|
|
||||||
segment.end = vec2Add(segment.end, offset);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.manager.draw();
|
|
||||||
}
|
|
||||||
|
|
||||||
private pointerDown() {
|
private pointerDown() {
|
||||||
if (this.drawingLine) return;
|
|
||||||
this.clickedOn = this.getMousedObject();
|
this.clickedOn = this.getMousedObject();
|
||||||
const bezierControl = this.grabbedBezierControl();
|
this.tool?.mouseDown(this.clickedOn || undefined);
|
||||||
const lineControl = this.grabbedLineControl();
|
|
||||||
if (bezierControl) {
|
|
||||||
this.handlingBezier = bezierControl;
|
|
||||||
} else if (lineControl) {
|
|
||||||
this.handlingLine = lineControl;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private pointerUp() {
|
private pointerUp() {
|
||||||
@ -577,149 +430,9 @@ export class HousePlannerCanvasTools {
|
|||||||
this.moved = false;
|
this.moved = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.moved && !this.handlingBezier && !this.handlingLine) {
|
this.tool?.mouseUp(this.moved);
|
||||||
if (this.tool === 'line') {
|
|
||||||
this.startLine();
|
|
||||||
} else if (!this.drawingLine) {
|
|
||||||
this.pick();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.handlingBezier) {
|
|
||||||
this.history.appendToHistory([
|
|
||||||
{
|
|
||||||
object: this.handlingBezier[0],
|
|
||||||
property: this.handlingBezier[1],
|
|
||||||
value: this.handlingBezier[2],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.handlingLine) {
|
|
||||||
this.history.appendToHistory([
|
|
||||||
{
|
|
||||||
object: this.handlingLine[0],
|
|
||||||
property: this.handlingLine[1],
|
|
||||||
value: this.handlingLine[2],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.movingObject || this.handlingLine || this.handlingBezier) {
|
|
||||||
this.canvas.dispatchEvent(
|
|
||||||
new CustomEvent('hpc:update', {
|
|
||||||
detail: {
|
|
||||||
event: 'line-move',
|
|
||||||
object: this.handlingLine || this.handlingBezier || this.clickedOn,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.handlingBezier = null;
|
|
||||||
this.handlingLine = null;
|
|
||||||
this.clickedOn = null;
|
this.clickedOn = null;
|
||||||
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) {
|
|
||||||
if (
|
|
||||||
vec2Equals(
|
|
||||||
this.mousePositionSnapped,
|
|
||||||
this.drawingLine.segments[0].start as Vec2
|
|
||||||
) ||
|
|
||||||
this.drawingLine.type === 'curve'
|
|
||||||
) {
|
|
||||||
if (this.drawingLine.type !== 'curve') {
|
|
||||||
this.drawingLine.segments.splice(
|
|
||||||
this.drawingLine.segments.length - 1,
|
|
||||||
1
|
|
||||||
);
|
|
||||||
}
|
|
||||||
this.history.appendToHistory([
|
|
||||||
{
|
|
||||||
object: this.selectedLayer,
|
|
||||||
property: 'contents',
|
|
||||||
value: [...this.selectedLayer.contents].filter(
|
|
||||||
(item) => item !== this.drawingLine
|
|
||||||
),
|
|
||||||
} as History<typeof this.selectedLayer>,
|
|
||||||
{
|
|
||||||
object: this,
|
|
||||||
property: 'selectedObjects',
|
|
||||||
value: [],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
this.drawingLine.closed = true;
|
|
||||||
this.canvas.dispatchEvent(
|
|
||||||
new CustomEvent('hpc:newobject', {
|
|
||||||
detail: this.drawingLine,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
this.canvas.dispatchEvent(
|
|
||||||
new CustomEvent('hpc:update', {
|
|
||||||
detail: { event: 'newobject', object: this.drawingLine },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
this.drawingLine = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.drawingLine.segments.push({
|
|
||||||
end: [...this.mousePositionSnapped],
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newLineObject: Line = {
|
|
||||||
id: this.getSequentialId(),
|
|
||||||
name: 'New Line',
|
|
||||||
type: this.subTool!,
|
|
||||||
visible: true,
|
|
||||||
selected: false,
|
|
||||||
closed: false,
|
|
||||||
color: this.lastColor,
|
|
||||||
width: this.subTool === 'curve' ? 2 : this.lastStrokeWidth,
|
|
||||||
segments: [
|
|
||||||
{
|
|
||||||
start: [...this.mousePositionSnapped],
|
|
||||||
end: [...this.mousePositionSnapped],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
this.drawingLine = newLineObject;
|
|
||||||
this.selectedLayer.contents.unshift(newLineObject);
|
|
||||||
this.selectObject(this.drawingLine);
|
|
||||||
this.canvas.dispatchEvent(
|
|
||||||
new CustomEvent('hpc:startdrawing', {
|
|
||||||
detail: newLineObject,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private pick() {
|
|
||||||
if (this.clickedOn && this.isSelected(this.clickedOn)) {
|
|
||||||
// Pick line segment
|
|
||||||
if ((this.clickedOn as Line).segments) {
|
|
||||||
const segment = this.getMousedLineSegment(this.clickedOn as Line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.selectObject(this.clickedOn, this.holdShift);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private dragEvent(x: number, y: number) {
|
private dragEvent(x: number, y: number) {
|
||||||
@ -737,44 +450,11 @@ export class HousePlannerCanvasTools {
|
|||||||
|
|
||||||
let offset = vec2Sub(currentPos, this.mousePositionSnapped);
|
let offset = vec2Sub(currentPos, this.mousePositionSnapped);
|
||||||
|
|
||||||
if (!this.selectedLayer) return;
|
this.tool?.mouseMoved(
|
||||||
|
this.mousePositionSnapped,
|
||||||
if (this.tool) {
|
offset,
|
||||||
if (this.drawingLine) {
|
this.mousePosition
|
||||||
const lastSegment = this.drawingLine.segments.at(-1);
|
);
|
||||||
if (lastSegment) {
|
|
||||||
lastSegment.end = [...this.mousePositionSnapped];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.manager.draw();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.drawingLine) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.handlingBezier) {
|
|
||||||
const [segment, property] = this.handlingBezier;
|
|
||||||
segment[property] = [...this.mousePositionSnapped];
|
|
||||||
this.manager.draw();
|
|
||||||
} else if (this.handlingLine) {
|
|
||||||
const [segment, property] = this.handlingLine;
|
|
||||||
segment[property] = [...this.mousePositionSnapped];
|
|
||||||
this.manager.draw();
|
|
||||||
} else if (this.clickedOn) {
|
|
||||||
if (!this.movingObject) {
|
|
||||||
// TODO: optimize history storage
|
|
||||||
this.history.appendToHistory([
|
|
||||||
{
|
|
||||||
object: this.selectedLayer,
|
|
||||||
property: 'contents',
|
|
||||||
value: JSON.parse(JSON.stringify(this.selectedLayer['contents'])),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
this.movingObject = true;
|
|
||||||
}
|
|
||||||
this.translate(vec2Inverse(offset));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private selectedEvent() {
|
private selectedEvent() {
|
||||||
|
192
src/modules/house-planner/tools/line.ts
Normal file
192
src/modules/house-planner/tools/line.ts
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
import { History, LayerObject, Line, Vec2 } from '../interfaces';
|
||||||
|
import { LayerObjectType } from '../types';
|
||||||
|
import { vec2Equals } from '../utils';
|
||||||
|
import { CanvasToolBase } from './tool-base';
|
||||||
|
|
||||||
|
export type LineToolType = 'line' | 'curve' | 'room';
|
||||||
|
export class LineTool extends CanvasToolBase<LineToolType> {
|
||||||
|
public name = 'line';
|
||||||
|
private drawingLine: Line | null = null;
|
||||||
|
public subTool: LineToolType = 'line';
|
||||||
|
|
||||||
|
drawControls(): void {
|
||||||
|
const [mx, my] = this.mousePosition;
|
||||||
|
this.ctx.fillStyle = '#00ddffaa';
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.arc(mx, my, this.manager.selectError / 2, 0, 2 * Math.PI);
|
||||||
|
this.ctx.fill();
|
||||||
|
}
|
||||||
|
|
||||||
|
mouseDown(targetObject?: LayerObject | undefined): void {}
|
||||||
|
|
||||||
|
mouseMoved(mouse: Vec2, offset: Vec2, mouseAbsolute: Vec2): void {
|
||||||
|
super.mouseMoved(mouse, offset, mouseAbsolute);
|
||||||
|
if (this.drawingLine) {
|
||||||
|
const lastSegment = this.drawingLine.segments.at(-1);
|
||||||
|
if (lastSegment) {
|
||||||
|
lastSegment.end = [...mouse];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.renderer.draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
mouseUp(moved?: boolean): void {
|
||||||
|
if (!moved) this.startLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
enterPress(e: KeyboardEvent): void {
|
||||||
|
if (this.drawingLine && this.layer) {
|
||||||
|
if (this.subTool === 'room') {
|
||||||
|
this.drawingLine.closed = true;
|
||||||
|
}
|
||||||
|
this.drawingLine.segments.splice(this.drawingLine.segments.length - 1, 1);
|
||||||
|
this.history.appendToHistory([
|
||||||
|
{
|
||||||
|
object: this.layer,
|
||||||
|
property: 'contents',
|
||||||
|
value: [...this.layer.contents].filter(
|
||||||
|
(item) => item !== this.drawingLine
|
||||||
|
),
|
||||||
|
} as History<typeof this.layer>,
|
||||||
|
{
|
||||||
|
object: this,
|
||||||
|
property: 'selectedObjects',
|
||||||
|
value: [],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
this.emitEvent(
|
||||||
|
new CustomEvent('hpc:newobject', {
|
||||||
|
detail: this.drawingLine,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
this.emitEvent(
|
||||||
|
new CustomEvent('hpc:update', {
|
||||||
|
detail: { event: 'newobject', object: this.drawingLine },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
this.drawingLine = null;
|
||||||
|
this.renderer.draw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
escapePress(e: KeyboardEvent): void {
|
||||||
|
this.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
selectionDeleted(): void {
|
||||||
|
this.drawingLine = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private cancel() {
|
||||||
|
if (!this.layer || !this.drawingLine) return;
|
||||||
|
const indexOf = this.layer.contents.indexOf(this.drawingLine);
|
||||||
|
if (indexOf > -1) {
|
||||||
|
this.layer.contents.splice(indexOf, 1);
|
||||||
|
}
|
||||||
|
this.drawingLine = null;
|
||||||
|
this.renderer.draw();
|
||||||
|
|
||||||
|
this.emitEvent(
|
||||||
|
new CustomEvent('hpc:update', {
|
||||||
|
detail: { event: 'draw-cancel' },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getSequentialId() {
|
||||||
|
return (
|
||||||
|
this.renderer.layers.reduce(
|
||||||
|
(total, current) =>
|
||||||
|
current.contents.reduce(
|
||||||
|
(total2, current2) => current2.id + total2,
|
||||||
|
0
|
||||||
|
) + total,
|
||||||
|
0
|
||||||
|
) + 1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private startLine() {
|
||||||
|
if (!this.layer?.visible) return;
|
||||||
|
if (this.drawingLine) {
|
||||||
|
if (
|
||||||
|
vec2Equals(
|
||||||
|
this.mousePosition,
|
||||||
|
this.drawingLine.segments[0].start as Vec2
|
||||||
|
) ||
|
||||||
|
this.drawingLine.type === 'curve'
|
||||||
|
) {
|
||||||
|
if (this.drawingLine.type !== 'curve') {
|
||||||
|
this.drawingLine.segments.splice(
|
||||||
|
this.drawingLine.segments.length - 1,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.history.appendToHistory([
|
||||||
|
{
|
||||||
|
object: this.layer,
|
||||||
|
property: 'contents',
|
||||||
|
value: [...this.layer.contents].filter(
|
||||||
|
(item) => item !== this.drawingLine
|
||||||
|
),
|
||||||
|
} as History<typeof this.layer>,
|
||||||
|
{
|
||||||
|
object: this,
|
||||||
|
property: 'selectedObjects',
|
||||||
|
value: [],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
this.drawingLine.closed = true;
|
||||||
|
this.canvas.dispatchEvent(
|
||||||
|
new CustomEvent('hpc:newobject', {
|
||||||
|
detail: this.drawingLine,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
this.canvas.dispatchEvent(
|
||||||
|
new CustomEvent('hpc:update', {
|
||||||
|
detail: { event: 'newobject', object: this.drawingLine },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
this.drawingLine = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.drawingLine.segments.push({
|
||||||
|
end: [...this.mousePosition],
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newLineObject: Line = {
|
||||||
|
id: this.getSequentialId(),
|
||||||
|
name: this.defaultName,
|
||||||
|
type: this.subTool! as LayerObjectType,
|
||||||
|
visible: true,
|
||||||
|
selected: false,
|
||||||
|
closed: false,
|
||||||
|
color: this.manager.lastColor,
|
||||||
|
width: this.subTool === 'curve' ? 2 : this.manager.lastStrokeWidth,
|
||||||
|
segments: [
|
||||||
|
{
|
||||||
|
start: [...this.mousePosition],
|
||||||
|
end: [...this.mousePosition],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
this.drawingLine = newLineObject;
|
||||||
|
this.layer.contents.unshift(newLineObject);
|
||||||
|
this.manager.selectObject(this.drawingLine);
|
||||||
|
this.canvas.dispatchEvent(
|
||||||
|
new CustomEvent('hpc:startdrawing', {
|
||||||
|
detail: newLineObject,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private get defaultName() {
|
||||||
|
let name = 'New ';
|
||||||
|
if (this.subTool === 'curve') return name + 'Curve';
|
||||||
|
if (this.subTool === 'room') return name + 'Room';
|
||||||
|
return name + 'Line';
|
||||||
|
}
|
||||||
|
}
|
249
src/modules/house-planner/tools/move.ts
Normal file
249
src/modules/house-planner/tools/move.ts
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
import {
|
||||||
|
BezierSegment,
|
||||||
|
LayerObject,
|
||||||
|
Line,
|
||||||
|
LineSegment,
|
||||||
|
Vec2,
|
||||||
|
} from '../interfaces';
|
||||||
|
import { BezierControl, LineControl } from '../types';
|
||||||
|
import { vec2InCircle, vec2Add, vec2Inverse } from '../utils';
|
||||||
|
import { CanvasToolBase } from './tool-base';
|
||||||
|
|
||||||
|
export class MoveTool extends CanvasToolBase<undefined> {
|
||||||
|
public name = 'move';
|
||||||
|
private bezierControls: BezierSegment[] = [];
|
||||||
|
private lineControls: LineSegment[] = [];
|
||||||
|
private handlingBezier: BezierControl | null = null;
|
||||||
|
private handlingLine: LineControl | null = null;
|
||||||
|
private movingObject = false;
|
||||||
|
private clickedOn?: LayerObject;
|
||||||
|
|
||||||
|
drawControls() {
|
||||||
|
this.bezierControls.length = 0;
|
||||||
|
this.lineControls.length = 0;
|
||||||
|
for (const object of this.manager.selectedObjects) {
|
||||||
|
const line = object as Line;
|
||||||
|
if (line.segments && line.render) {
|
||||||
|
let lastSegment = null;
|
||||||
|
for (const segment of line.segments) {
|
||||||
|
const bezier = segment as BezierSegment;
|
||||||
|
const previousPoint = lastSegment ? lastSegment.end : segment.start!;
|
||||||
|
if (bezier.startControl && bezier.endControl) {
|
||||||
|
this.drawBezierControls(bezier, previousPoint);
|
||||||
|
}
|
||||||
|
this.drawLineControls(segment, previousPoint);
|
||||||
|
lastSegment = segment;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO: other kinds of objects
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mouseDown(targetObject?: LayerObject | undefined): void {
|
||||||
|
this.clickedOn = targetObject;
|
||||||
|
const bezierControl = this.grabbedBezierControl();
|
||||||
|
const lineControl = this.grabbedLineControl();
|
||||||
|
if (bezierControl) {
|
||||||
|
this.handlingBezier = bezierControl;
|
||||||
|
} else if (lineControl) {
|
||||||
|
this.handlingLine = lineControl;
|
||||||
|
} else if (
|
||||||
|
targetObject &&
|
||||||
|
!this.manager.selectedObjects.includes(targetObject)
|
||||||
|
) {
|
||||||
|
this.manager.selectObject(targetObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mouseUp(moved?: boolean): void {
|
||||||
|
if (!this.handlingBezier && !this.handlingLine && !this.movingObject) {
|
||||||
|
this.pick();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.handlingBezier) {
|
||||||
|
this.history.appendToHistory([
|
||||||
|
{
|
||||||
|
object: this.handlingBezier[0],
|
||||||
|
property: this.handlingBezier[1],
|
||||||
|
value: this.handlingBezier[2],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.handlingLine) {
|
||||||
|
this.history.appendToHistory([
|
||||||
|
{
|
||||||
|
object: this.handlingLine[0],
|
||||||
|
property: this.handlingLine[1],
|
||||||
|
value: this.handlingLine[2],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.movingObject || this.handlingLine || this.handlingBezier) {
|
||||||
|
this.canvas.dispatchEvent(
|
||||||
|
new CustomEvent('hpc:update', {
|
||||||
|
detail: {
|
||||||
|
event: 'line-move',
|
||||||
|
object: this.handlingLine || this.handlingBezier || this.clickedOn,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.handlingBezier = null;
|
||||||
|
this.handlingLine = null;
|
||||||
|
this.movingObject = false;
|
||||||
|
this.clickedOn = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private pick() {
|
||||||
|
if (this.clickedOn && this.manager.isSelected(this.clickedOn)) {
|
||||||
|
// Pick line segment
|
||||||
|
if ((this.clickedOn as Line).segments) {
|
||||||
|
const segment = this.manager.getMousedLineSegment(
|
||||||
|
this.clickedOn as Line
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.manager.selectObject(this.clickedOn, this.manager.holdShift);
|
||||||
|
}
|
||||||
|
|
||||||
|
mouseMoved(mouse: Vec2, offset: Vec2, mouseAbsolute: Vec2): void {
|
||||||
|
super.mouseMoved(mouse, offset, mouseAbsolute);
|
||||||
|
if (!this.layer) return;
|
||||||
|
if (this.handlingBezier) {
|
||||||
|
const [segment, property] = this.handlingBezier;
|
||||||
|
segment[property] = [...mouse];
|
||||||
|
this.renderer.draw();
|
||||||
|
} else if (this.handlingLine) {
|
||||||
|
const [segment, property] = this.handlingLine;
|
||||||
|
segment[property] = [...mouse];
|
||||||
|
this.renderer.draw();
|
||||||
|
} else if (this.clickedOn) {
|
||||||
|
if (!this.movingObject) {
|
||||||
|
// TODO: optimize history storage
|
||||||
|
this.history.appendToHistory([
|
||||||
|
{
|
||||||
|
object: this.layer,
|
||||||
|
property: 'contents',
|
||||||
|
value: JSON.parse(JSON.stringify(this.layer['contents'])),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
this.movingObject = true;
|
||||||
|
}
|
||||||
|
this.translate(vec2Inverse(offset));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drawBezierControls(bezier: BezierSegment, previousEnd: Vec2) {
|
||||||
|
this.bezierControls.push(bezier);
|
||||||
|
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) {
|
||||||
|
this.lineControls.push(line);
|
||||||
|
const [endx, endy] = line.end;
|
||||||
|
this.ctx.fillStyle = '#00ddffaa';
|
||||||
|
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.arc(endx, endy, this.manager.selectError / 2, 0, 2 * Math.PI);
|
||||||
|
this.ctx.fill();
|
||||||
|
|
||||||
|
if (line.start) {
|
||||||
|
const [startx, starty] = line.start;
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.arc(
|
||||||
|
startx,
|
||||||
|
starty,
|
||||||
|
this.manager.selectError / 2,
|
||||||
|
0,
|
||||||
|
2 * Math.PI
|
||||||
|
);
|
||||||
|
this.ctx.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private grabbedBezierControl() {
|
||||||
|
let clickedOnControl: BezierControl | null = null;
|
||||||
|
for (const bezier of this.bezierControls) {
|
||||||
|
for (const control of ['startControl', 'endControl']) {
|
||||||
|
const asType = control as 'startControl' | 'endControl';
|
||||||
|
if (
|
||||||
|
vec2InCircle(
|
||||||
|
bezier[asType],
|
||||||
|
this.mousePosition,
|
||||||
|
this.manager.selectError / 2
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
clickedOnControl = [bezier, asType, bezier[asType]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return clickedOnControl;
|
||||||
|
}
|
||||||
|
|
||||||
|
private grabbedLineControl() {
|
||||||
|
let clickedOnControl: LineControl | null = null;
|
||||||
|
for (const line of this.lineControls) {
|
||||||
|
for (const control of ['start', 'end']) {
|
||||||
|
const asType = control as 'start' | 'end';
|
||||||
|
if (
|
||||||
|
line[asType] &&
|
||||||
|
vec2InCircle(
|
||||||
|
line[asType] as Vec2,
|
||||||
|
this.mousePosition,
|
||||||
|
this.manager.selectError / 2
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
clickedOnControl = [line, asType, line[asType] as Vec2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return clickedOnControl;
|
||||||
|
}
|
||||||
|
|
||||||
|
private translate(offset: Vec2) {
|
||||||
|
for (const object of this.manager.selectedObjects) {
|
||||||
|
if ((object as Line).segments) {
|
||||||
|
for (const segment of (object as Line).segments) {
|
||||||
|
if ((segment as BezierSegment).startControl) {
|
||||||
|
const bezier = segment as BezierSegment;
|
||||||
|
bezier.startControl = vec2Add(bezier.startControl, offset);
|
||||||
|
bezier.endControl = vec2Add(bezier.endControl, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (segment.start) {
|
||||||
|
segment.start = vec2Add(segment.start, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
segment.end = vec2Add(segment.end, offset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.renderer.draw();
|
||||||
|
}
|
||||||
|
}
|
56
src/modules/house-planner/tools/tool-base.ts
Normal file
56
src/modules/house-planner/tools/tool-base.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { ICanvasToolBase, LayerObject, Vec2 } from '../interfaces';
|
||||||
|
import type { HousePlannerCanvasTools } from '../tools';
|
||||||
|
|
||||||
|
export class CanvasToolBase<T> implements ICanvasToolBase<T> {
|
||||||
|
protected mousePosition: Vec2 = [0, 0];
|
||||||
|
protected mousePositionAbsolute: Vec2 = [0, 0];
|
||||||
|
public name = 'tool';
|
||||||
|
public subTool: T | undefined;
|
||||||
|
|
||||||
|
constructor(public manager: HousePlannerCanvasTools) {}
|
||||||
|
get ctx() {
|
||||||
|
return this.manager.ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
get canvas() {
|
||||||
|
return this.manager.canvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
get history() {
|
||||||
|
return this.manager.history;
|
||||||
|
}
|
||||||
|
|
||||||
|
get renderer() {
|
||||||
|
return this.manager.manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
get layer() {
|
||||||
|
return this.manager.selectedLayer;
|
||||||
|
}
|
||||||
|
|
||||||
|
drawHighlights() {}
|
||||||
|
drawControls() {}
|
||||||
|
|
||||||
|
mouseDown(targetObject?: LayerObject) {}
|
||||||
|
mouseMoved(mouse: Vec2, offset: Vec2, mouseAbsolute: Vec2) {
|
||||||
|
this.mousePosition = mouse;
|
||||||
|
this.mousePositionAbsolute = mouseAbsolute;
|
||||||
|
}
|
||||||
|
mouseUp(moved = false) {}
|
||||||
|
|
||||||
|
setSubTool(subTool: T) {
|
||||||
|
this.subTool = subTool;
|
||||||
|
}
|
||||||
|
activate() {}
|
||||||
|
deactivate() {}
|
||||||
|
enterPress(e: KeyboardEvent): void {}
|
||||||
|
escapePress(e: KeyboardEvent): void {}
|
||||||
|
emitEvent(e: CustomEvent<any>): void {
|
||||||
|
this.canvas.dispatchEvent(e);
|
||||||
|
}
|
||||||
|
isToolCancelable(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
selectionDeleted(): void {}
|
||||||
|
selectionChanged(selection: LayerObject[]): void {}
|
||||||
|
}
|
@ -1,8 +1,5 @@
|
|||||||
import { BezierSegment, LineSegment, Vec2 } from './interfaces';
|
import { BezierSegment, LineSegment, Vec2 } from './interfaces';
|
||||||
|
|
||||||
export type ToolType = 'line' | null;
|
|
||||||
export type SubToolType = 'line' | 'curve' | 'room';
|
|
||||||
|
|
||||||
export type BezierControl = [
|
export type BezierControl = [
|
||||||
BezierSegment,
|
BezierSegment,
|
||||||
'startControl' | 'endControl',
|
'startControl' | 'endControl',
|
||||||
|
@ -2,3 +2,6 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
.min-h-half {
|
||||||
|
min-height: 50%;
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user