Add edge editor, generalize selection and edition to both node and edge. Still trying to find a proper tween.
This commit is contained in:
parent
76db788192
commit
348c991c54
|
|
@ -1,22 +1,13 @@
|
|||
<script lang="ts">
|
||||
import type { Box, Position } from '#shared/canvas.util';
|
||||
import type { Position } from '#shared/canvas.util';
|
||||
import type CanvasNodeEditor from './canvas/CanvasNodeEditor.vue';
|
||||
type Direction = 'bottom' | 'top' | 'left' | 'right';
|
||||
const rotation: Record<Direction, string> = {
|
||||
top: "180",
|
||||
bottom: "0",
|
||||
left: "90",
|
||||
right: "270"
|
||||
};
|
||||
type Element = { type: 'node' | 'edge', id: string };
|
||||
import type CanvasEdgeEditor from './canvas/CanvasEdgeEditor.vue';
|
||||
export type Element = { type: 'node' | 'edge', id: string };
|
||||
|
||||
interface ActionMap {
|
||||
move: Position;
|
||||
edit: string;
|
||||
resize: Box;
|
||||
remove: CanvasNode | undefined;
|
||||
create: CanvasNode | undefined;
|
||||
property: CanvasNode;
|
||||
remove: CanvasNode | CanvasEdge | undefined;
|
||||
create: CanvasNode | CanvasEdge | undefined;
|
||||
property: CanvasNode | CanvasEdge;
|
||||
}
|
||||
type Action = keyof ActionMap;
|
||||
interface HistoryEvent<T extends Action = Action>
|
||||
|
|
@ -26,11 +17,16 @@ interface HistoryEvent<T extends Action = Action>
|
|||
}
|
||||
interface HistoryAction<T extends Action>
|
||||
{
|
||||
element: string;
|
||||
element: Element;
|
||||
from: ActionMap[T];
|
||||
to: ActionMap[T];
|
||||
}
|
||||
|
||||
type NodeEditor = InstanceType<typeof CanvasNodeEditor>;
|
||||
type EdgeEditor = InstanceType<typeof CanvasEdgeEditor>;
|
||||
|
||||
const TWEEN_DURATION = 200;
|
||||
|
||||
const cancelEvent = (e: Event) => e.preventDefault();
|
||||
const stopPropagation = (e: Event) => e.stopImmediatePropagation();
|
||||
function getID(length: number)
|
||||
|
|
@ -67,27 +63,18 @@ function contains(group: CanvasNode, node: CanvasNode): boolean
|
|||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
import { clamp } from '#shared/general.utils';
|
||||
import type { CanvasContent, CanvasNode } from '~/types/canvas';
|
||||
import { labelCenter, getPath } from '#shared/canvas.util';
|
||||
import type { CanvasContent, CanvasEdge, CanvasNode } from '~/types/canvas';
|
||||
|
||||
const canvas = defineModel<CanvasContent>({ required: true, });
|
||||
|
||||
const dispX = ref(0), dispY = ref(0), minZoom = ref(0.1), zoom = ref(0.5);
|
||||
const focusing = ref<string>(), editing = ref<string>();
|
||||
const focusing = ref<Element>(), editing = ref<Element>();
|
||||
const canvasRef = useTemplateRef('canvasRef'), transformRef = useTemplateRef('transformRef');
|
||||
const nodes = useTemplateRef<InstanceType<typeof CanvasNodeEditor>[]>('nodes');
|
||||
const nodes = useTemplateRef<NodeEditor[]>('nodes'), edges = useTemplateRef<EdgeEditor[]>('edges');
|
||||
|
||||
const xTween = useTween(dispX, linear, updateTransform), yTween = useTween(dispY, linear, updateTransform), zoomTween = useTween(zoom, linear, updateTransform);
|
||||
|
||||
const focusedNode = computed(() => nodes.value?.find(e => !!e && e.id === focusing.value)), editedNode = computed(() => nodes.value?.find(e => !!e && e.id === editing.value));
|
||||
|
||||
const edges = computed(() => {
|
||||
return canvas.value.edges?.map(e => {
|
||||
const from = canvas.value.nodes!.find(f => f.id === e.fromNode), to = canvas.value.nodes!.find(f => f.id === e.toNode);
|
||||
const path = getPath(from!, e.fromSide, to!, e.toSide)!;
|
||||
return { ...e, from, to, path };
|
||||
});
|
||||
});
|
||||
const focused = computed(() => focusing.value ? focusing.value?.type === 'node' ? nodes.value?.find(e => !!e && e.id === focusing.value!.id) : edges.value?.find(e => !!e && e.id === focusing.value!.id) : undefined), edited = computed(() => editing.value ? editing.value?.type === 'node' ? nodes.value?.find(e => !!e && e.id === editing.value!.id) : edges.value?.find(e => !!e && e.id === editing.value!.id) : undefined);
|
||||
|
||||
const history = ref<HistoryEvent[]>([]);
|
||||
const historyPos = ref(-1);
|
||||
|
|
@ -159,10 +146,10 @@ onMounted(() => {
|
|||
const centerX = (box.x + box.width / 2), centerY = (box.y + box.height / 2);
|
||||
const mousex = centerX - e.clientX, mousey = centerY - e.clientY;
|
||||
|
||||
xTween.update(dispX.value - (mousex / (diff * zoom.value) - mousex / zoom.value), 250);
|
||||
yTween.update(dispY.value - (mousey / (diff * zoom.value) - mousey / zoom.value), 250);
|
||||
xTween.update(dispX.value - (mousex / (diff * zoom.value) - mousex / zoom.value), TWEEN_DURATION);
|
||||
yTween.update(dispY.value - (mousey / (diff * zoom.value) - mousey / zoom.value), TWEEN_DURATION);
|
||||
|
||||
zoomTween.update(clamp(zoom.value * diff, minZoom.value, 3), 250);
|
||||
zoomTween.update(clamp(zoom.value * diff, minZoom.value, 3), TWEEN_DURATION);
|
||||
|
||||
updateTransform();
|
||||
}, { passive: true });
|
||||
|
|
@ -233,47 +220,53 @@ function updateTransform()
|
|||
}
|
||||
function moveNode(ids: string[], deltax: number, deltay: number)
|
||||
{
|
||||
const actions: HistoryAction<'move'>[] = [];
|
||||
if(ids.length === 0)
|
||||
return;
|
||||
|
||||
const actions: HistoryAction<'property'>[] = [];
|
||||
for(const id of ids)
|
||||
{
|
||||
const node = canvas.value.nodes!.find(e => e.id === id)!;
|
||||
|
||||
actions.push({ element: id, from: { x: node.x - deltax, y: node.y - deltay }, to: { x: node.x, y: node.y } });
|
||||
actions.push({ element: { type: 'node', id }, from: { ...node, x: node.x - deltax, y: node.y - deltay }, to: { ...node } });
|
||||
}
|
||||
|
||||
addAction('move', actions);
|
||||
addAction('property', actions);
|
||||
}
|
||||
function resizeNode(ids: string[], deltax: number, deltay: number, deltaw: number, deltah: number)
|
||||
{
|
||||
const actions: HistoryAction<'resize'>[] = [];
|
||||
if(ids.length === 0)
|
||||
return;
|
||||
|
||||
const actions: HistoryAction<'property'>[] = [];
|
||||
for(const id of ids)
|
||||
{
|
||||
const node = canvas.value.nodes!.find(e => e.id === id)!;
|
||||
|
||||
actions.push({ element: id, from: { x: node.x - deltax, y: node.y - deltay, w: node.width - deltaw, h: node.height - deltah }, to: { x: node.x, y: node.y, w: node.width, h: node.height } });
|
||||
actions.push({ element: { type: 'node', id }, from: { ...node, x: node.x - deltax, y: node.y - deltay, width: node.width - deltaw, height: node.height - deltah }, to: { ...node } });
|
||||
}
|
||||
|
||||
addAction('resize', actions);
|
||||
addAction('property', actions);
|
||||
}
|
||||
function selectNode(id: string)
|
||||
function select(element: Element)
|
||||
{
|
||||
if(focusing.value !== id)
|
||||
if(focusing.value && (focusing.value.id !== element.id || focusing.value.type !== element.type))
|
||||
{
|
||||
unselectNode();
|
||||
unselect();
|
||||
}
|
||||
|
||||
focusing.value = id;
|
||||
focusing.value = element;
|
||||
|
||||
focusedNode.value?.dom?.addEventListener('click', stopPropagation, { passive: true });
|
||||
canvasRef.value?.addEventListener('click', unselectNode, { once: true });
|
||||
focused.value?.dom?.addEventListener('click', stopPropagation, { passive: true });
|
||||
canvasRef.value?.addEventListener('click', unselect, { once: true });
|
||||
}
|
||||
function editNode(id: string)
|
||||
function edit(element: Element)
|
||||
{
|
||||
editing.value = id;
|
||||
editing.value = element;
|
||||
|
||||
focusedNode.value?.dom?.addEventListener('wheel', stopPropagation, { passive: true });
|
||||
focusedNode.value?.dom?.addEventListener('dblclick', stopPropagation, { passive: true });
|
||||
canvasRef.value?.addEventListener('click', unselectNode, { once: true });
|
||||
focused.value?.dom?.addEventListener('wheel', stopPropagation, { passive: true });
|
||||
focused.value?.dom?.addEventListener('dblclick', stopPropagation, { passive: true });
|
||||
canvasRef.value?.addEventListener('click', unselect, { once: true });
|
||||
}
|
||||
function createNode(e: MouseEvent)
|
||||
{
|
||||
|
|
@ -285,49 +278,74 @@ function createNode(e: MouseEvent)
|
|||
else
|
||||
canvas.value.nodes.push(node);
|
||||
|
||||
addAction('create', [{ element: node.id, from: undefined, to: node }]);
|
||||
addAction('create', [{ element: { type: 'node', id: node.id }, from: undefined, to: node }]);
|
||||
}
|
||||
function removeNode(ids: string[])
|
||||
function remove(elements: Element[])
|
||||
{
|
||||
const actions: HistoryAction<'remove'>[] = [];
|
||||
unselectNode();
|
||||
if(elements.length === 0)
|
||||
return;
|
||||
|
||||
for(const id of ids)
|
||||
const actions: HistoryAction<'remove'>[] = [];
|
||||
focusing.value = undefined;
|
||||
editing.value = undefined;
|
||||
|
||||
const c = canvas.value;
|
||||
|
||||
for(const element of elements)
|
||||
{
|
||||
const index = canvas.value.nodes!.findIndex(e => e.id === id);
|
||||
actions.push({ element: id, from: canvas.value.nodes!.splice(index, 1)[0], to: undefined });
|
||||
if(element.type === 'node')
|
||||
{
|
||||
const edges = c.edges?.map((e, i) => ({ id: e.id, from: e.fromNode, to: e.toNode, index: i }))?.filter(e => e.from === element.id || e.to === element.id) ?? [];
|
||||
for(let i = edges.length - 1; i >= 0; i--)
|
||||
{
|
||||
actions.push({ element: { type: 'edge', id: edges[i].id }, from: c.edges!.splice(edges[i].index, 1)[0], to: undefined });
|
||||
}
|
||||
|
||||
const index = c.nodes!.findIndex(e => e.id === element.id);
|
||||
actions.push({ element: { type: 'node', id: element.id }, from: c.nodes!.splice(index, 1)[0], to: undefined });
|
||||
}
|
||||
else if(element.type === 'edge' && !actions.find(e => e.element.type === 'edge' && e.element.id === element.id))
|
||||
{
|
||||
const index = c.edges!.findIndex(e => e.id === element.id);
|
||||
actions.push({ element: { type: 'edge', id: element.id }, from: c.edges!.splice(index, 1)[0], to: undefined });
|
||||
}
|
||||
}
|
||||
|
||||
canvas.value = c;
|
||||
|
||||
addAction('remove', actions);
|
||||
}
|
||||
function editNodeProperty<T extends keyof CanvasNode>(ids: string[], property: T, value: CanvasNode[T])
|
||||
{
|
||||
const actions: HistoryAction<'remove'>[] = [];
|
||||
if(ids.length === 0)
|
||||
return;
|
||||
|
||||
const actions: HistoryAction<'property'>[] = [];
|
||||
|
||||
for(const id of ids)
|
||||
{
|
||||
const copy = JSON.parse(JSON.stringify(canvas.value.nodes!.find(e => e.id === id)!)) as CanvasNode;
|
||||
canvas.value.nodes!.find(e => e.id === id)![property] = value;
|
||||
actions.push({ element: id, from: copy, to: canvas.value.nodes!.find(e => e.id === id)! });
|
||||
actions.push({ element: { type: 'node', id }, from: copy, to: canvas.value.nodes!.find(e => e.id === id)! });
|
||||
}
|
||||
|
||||
addAction('property', actions);
|
||||
}
|
||||
|
||||
const unselectNode = () => {
|
||||
const unselect = () => {
|
||||
if(focusing.value !== undefined)
|
||||
{
|
||||
focusedNode.value?.dom?.removeEventListener('click', stopPropagation);
|
||||
focusedNode.value?.unselect();
|
||||
focused.value?.dom?.removeEventListener('click', stopPropagation);
|
||||
focused.value?.unselect();
|
||||
}
|
||||
focusing.value = undefined;
|
||||
|
||||
if(editing.value !== undefined)
|
||||
{
|
||||
editedNode.value?.dom?.removeEventListener('wheel', stopPropagation);
|
||||
editedNode.value?.dom?.removeEventListener('dblclick', stopPropagation);
|
||||
editedNode.value?.dom?.removeEventListener('click', stopPropagation);
|
||||
editedNode.value?.unselect();
|
||||
edited.value?.dom?.removeEventListener('wheel', stopPropagation);
|
||||
edited.value?.dom?.removeEventListener('dblclick', stopPropagation);
|
||||
edited.value?.dom?.removeEventListener('click', stopPropagation);
|
||||
edited.value?.unselect();
|
||||
}
|
||||
editing.value = undefined;
|
||||
};
|
||||
|
|
@ -337,57 +355,61 @@ const undo = () => {
|
|||
|
||||
for(const action of historyCursor.value.actions)
|
||||
{
|
||||
const node = canvas.value.nodes!.find(e => e.id === action.element)!;
|
||||
if(action.element.type === 'node')
|
||||
{
|
||||
switch(historyCursor.value.event)
|
||||
{
|
||||
case 'move':
|
||||
{
|
||||
const a = action as HistoryAction<'move'>;
|
||||
node.x = a.from.x;
|
||||
node.y = a.from.y;
|
||||
break;
|
||||
}
|
||||
case 'resize':
|
||||
{
|
||||
const a = action as HistoryAction<'resize'>;
|
||||
node.x = a.from.x;
|
||||
node.y = a.from.y;
|
||||
node.width = a.from.w;
|
||||
node.height = a.from.h;
|
||||
break;
|
||||
}
|
||||
case 'edit':
|
||||
{
|
||||
const a = action as HistoryAction<'edit'>;
|
||||
node.label = a.from;
|
||||
break;
|
||||
}
|
||||
case 'create':
|
||||
{
|
||||
const a = action as HistoryAction<'create'>;
|
||||
const index = canvas.value.nodes!.findIndex(e => e.id === action.element);
|
||||
const index = canvas.value.nodes!.findIndex(e => e.id === action.element.id);
|
||||
canvas.value.nodes!.splice(index, 1);
|
||||
break;
|
||||
}
|
||||
case 'remove':
|
||||
{
|
||||
const a = action as HistoryAction<'remove'>;
|
||||
canvas.value.nodes!.push(a.from!);
|
||||
canvas.value.nodes!.push(a.from as CanvasNode);
|
||||
break;
|
||||
}
|
||||
case 'property':
|
||||
{
|
||||
const a = action as HistoryAction<'property'>;
|
||||
const index = canvas.value.nodes!.findIndex(e => e.id === action.element);
|
||||
canvas.value.nodes![index] = a.from;
|
||||
const index = canvas.value.nodes!.findIndex(e => e.id === action.element.id);
|
||||
canvas.value.nodes![index] = a.from as CanvasNode;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if(action.element.type === 'edge')
|
||||
{
|
||||
switch(historyCursor.value.event)
|
||||
{
|
||||
case 'create':
|
||||
{
|
||||
const a = action as HistoryAction<'create'>;
|
||||
const index = canvas.value.edges!.findIndex(e => e.id === action.element.id);
|
||||
canvas.value.edges!.splice(index, 1);
|
||||
break;
|
||||
}
|
||||
case 'remove':
|
||||
{
|
||||
const a = action as HistoryAction<'remove'>;
|
||||
canvas.value.edges!.push(a.from! as CanvasEdge);
|
||||
break;
|
||||
}
|
||||
case 'property':
|
||||
{
|
||||
const a = action as HistoryAction<'property'>;
|
||||
const index = canvas.value.edges!.findIndex(e => e.id === action.element.id);
|
||||
canvas.value.edges![index] = a.from as CanvasEdge;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
historyPos.value--;
|
||||
|
||||
console.log(historyPos.value, history.value.length);
|
||||
};
|
||||
const redo = () => {
|
||||
if(!history.value || history.value.length - 1 <= historyPos.value)
|
||||
|
|
@ -403,61 +425,65 @@ const redo = () => {
|
|||
|
||||
for(const action of historyCursor.value.actions)
|
||||
{
|
||||
const node = canvas.value.nodes!.find(e => e.id === action.element)!;
|
||||
if(action.element.type === 'node')
|
||||
{
|
||||
switch(historyCursor.value.event)
|
||||
{
|
||||
case 'move':
|
||||
{
|
||||
const a = action as HistoryAction<'move'>;
|
||||
node.x = a.to.x;
|
||||
node.y = a.to.y;
|
||||
break;
|
||||
}
|
||||
case 'resize':
|
||||
{
|
||||
const a = action as HistoryAction<'resize'>;
|
||||
node.x = a.to.x;
|
||||
node.y = a.to.y;
|
||||
node.width = a.to.w;
|
||||
node.height = a.to.h;
|
||||
break;
|
||||
}
|
||||
case 'edit':
|
||||
{
|
||||
const a = action as HistoryAction<'edit'>;
|
||||
node.label = a.to;
|
||||
break;
|
||||
}
|
||||
case 'create':
|
||||
{
|
||||
const a = action as HistoryAction<'remove'>;
|
||||
canvas.value.nodes!.push(a.to!);
|
||||
const a = action as HistoryAction<'create'>;
|
||||
canvas.value.nodes!.push(a.to as CanvasNode);
|
||||
break;
|
||||
}
|
||||
case 'remove':
|
||||
{
|
||||
const a = action as HistoryAction<'remove'>;
|
||||
const index = canvas.value.nodes!.findIndex(e => e.id === action.element);
|
||||
const index = canvas.value.nodes!.findIndex(e => e.id === action.element.id);
|
||||
canvas.value.nodes!.splice(index, 1);
|
||||
break;
|
||||
}
|
||||
case 'property':
|
||||
{
|
||||
const a = action as HistoryAction<'property'>;
|
||||
const index = canvas.value.nodes!.findIndex(e => e.id === action.element);
|
||||
canvas.value.nodes![index] = a.to;
|
||||
const index = canvas.value.nodes!.findIndex(e => e.id === action.element.id);
|
||||
canvas.value.nodes![index] = a.to as CanvasNode;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(historyPos.value, history.value.length);
|
||||
else if(action.element.type === 'edge')
|
||||
{
|
||||
switch(historyCursor.value.event)
|
||||
{
|
||||
case 'create':
|
||||
{
|
||||
const a = action as HistoryAction<'create'>;
|
||||
canvas.value.edges!.push(a.to as CanvasEdge);
|
||||
break;
|
||||
}
|
||||
case 'remove':
|
||||
{
|
||||
const a = action as HistoryAction<'remove'>;
|
||||
const index = canvas.value.edges!.findIndex(e => e.id === action.element.id);
|
||||
canvas.value.edges!.splice(index, 1);
|
||||
break;
|
||||
}
|
||||
case 'property':
|
||||
{
|
||||
const a = action as HistoryAction<'property'>;
|
||||
const index = canvas.value.edges!.findIndex(e => e.id === action.element.id);
|
||||
canvas.value.edges![index] = a.to as CanvasEdge;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useShortcuts({
|
||||
meta_z: undo,
|
||||
meta_y: redo,
|
||||
Delete: () => { if(focusing.value !== undefined) { removeNode([focusing.value]) } }
|
||||
Delete: () => { if(focusing.value !== undefined) { remove([focusing.value]) } }
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
@ -466,7 +492,7 @@ useShortcuts({
|
|||
<div class="flex flex-col absolute sm:top-2 top-10 left-2 z-[35] overflow-hidden gap-4" @click="stopPropagation">
|
||||
<div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10">
|
||||
<Tooltip message="Zoom avant" side="right">
|
||||
<div @click="zoomTween.update(clamp(zoom * 1.1, minZoom, 3), 250); updateTransform()" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
|
||||
<div @click="zoomTween.update(clamp(zoom * 1.1, minZoom, 3), TWEEN_DURATION); updateTransform()" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
|
||||
<Icon icon="radix-icons:plus" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
|
@ -481,7 +507,7 @@ useShortcuts({
|
|||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip message="Zoom arrière" side="right">
|
||||
<div @click="zoomTween.update(clamp(zoom / 1.1, minZoom, 3), 250); updateTransform()" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
|
||||
<div @click="zoomTween.update(clamp(zoom / 1.1, minZoom, 3), TWEEN_DURATION); updateTransform()" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
|
||||
<Icon icon="radix-icons:minus" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
|
@ -532,7 +558,7 @@ useShortcuts({
|
|||
'transform-origin': 'center center',
|
||||
}" class="h-full">
|
||||
<div class="absolute top-0 left-0 w-full h-full pointer-events-none *:pointer-events-auto *:select-none touch-none">
|
||||
<div v-if="focusing !== undefined && focusedNode !== undefined" class="absolute z-20 origin-bottom" :style="{transform: `translate(${focusedNode.x}px, ${focusedNode.y}px) translateY(-100%) translateY(-12px) translateX(-50%) translateX(${focusedNode.width / 2}px) scale(calc(1 / var(--tw-scale)))`}">
|
||||
<div v-if="focusing !== undefined && focusing.type === 'node'" class="absolute z-20 origin-bottom" :style="{transform: `translate(${canvas.nodes!.find(e => e.id === focusing!.id)!.x}px, ${canvas.nodes!.find(e => e.id === focusing!.id)!.y}px) translateY(-100%) translateY(-12px) translateX(-50%) translateX(${canvas.nodes!.find(e => e.id === focusing!.id)!.width / 2}px) scale(calc(1 / var(--tw-scale)))`}">
|
||||
<div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10 flex flex-row">
|
||||
<PopoverRoot>
|
||||
<PopoverTrigger asChild>
|
||||
|
|
@ -547,30 +573,30 @@ useShortcuts({
|
|||
<PopoverPortal disabled>
|
||||
<PopoverContent align="center" side="top" class="bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 m-2">
|
||||
<div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10 flex flex-row *:cursor-pointer">
|
||||
<div @click="editNodeProperty([focusing], 'color', undefined)" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
|
||||
<div @click="editNodeProperty([focusing.id], 'color', undefined)" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
|
||||
<span class="bg-light-40 dark:bg-dark-40 w-4 h-4 block"></span>
|
||||
</div>
|
||||
<div @click="editNodeProperty([focusing], 'color', { class: 'red' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
|
||||
<div @click="editNodeProperty([focusing.id], 'color', { class: 'red' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
|
||||
<span class="bg-light-red dark:bg-dark-red w-4 h-4 block"></span>
|
||||
</div>
|
||||
<div @click="editNodeProperty([focusing], 'color', { class: 'orange' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
|
||||
<div @click="editNodeProperty([focusing.id], 'color', { class: 'orange' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
|
||||
<span class="bg-light-orange dark:bg-dark-orange w-4 h-4 block"></span>
|
||||
</div>
|
||||
<div @click="editNodeProperty([focusing], 'color', { class: 'yellow' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
|
||||
<div @click="editNodeProperty([focusing.id], 'color', { class: 'yellow' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
|
||||
<span class="bg-light-yellow dark:bg-dark-yellow w-4 h-4 block"></span>
|
||||
</div>
|
||||
<div @click="editNodeProperty([focusing], 'color', { class: 'green' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
|
||||
<div @click="editNodeProperty([focusing.id], 'color', { class: 'green' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
|
||||
<span class="bg-light-green dark:bg-dark-green w-4 h-4 block"></span>
|
||||
</div>
|
||||
<div @click="editNodeProperty([focusing], 'color', { class: 'cyan' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
|
||||
<div @click="editNodeProperty([focusing.id], 'color', { class: 'cyan' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
|
||||
<span class="bg-light-cyan dark:bg-dark-cyan w-4 h-4 block"></span>
|
||||
</div>
|
||||
<div @click="editNodeProperty([focusing], 'color', { class: 'purple' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
|
||||
<div @click="editNodeProperty([focusing.id], 'color', { class: 'purple' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
|
||||
<span class="bg-light-purple dark:bg-dark-purple w-4 h-4 block"></span>
|
||||
</div>
|
||||
<label>
|
||||
<div @click="stopPropagation" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
|
||||
<span style="background: conic-gradient(red, yellow, green, blue, purple, red)" class="w-4 h-4 block relative"></span><input @change="(e: Event) => editNodeProperty([focusing!], 'color', { hex: (e.target as HTMLInputElement).value })" type="color" class="appearance-none w-0 h-0 absolute" />
|
||||
<span style="background: conic-gradient(red, yellow, green, blue, purple, red)" class="w-4 h-4 block relative"></span><input @change="(e: Event) => editNodeProperty([focusing!.id], 'color', { hex: (e.target as HTMLInputElement).value })" type="color" class="appearance-none w-0 h-0 absolute" />
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
|
@ -578,29 +604,18 @@ useShortcuts({
|
|||
</PopoverPortal>
|
||||
</PopoverRoot>
|
||||
<Tooltip message="Supprimer" side="top">
|
||||
<div @click="removeNode([focusing])" class="w-10 h-10 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
|
||||
<div @click="remove([focusing])" class="w-10 h-10 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
|
||||
<Icon icon="radix-icons:trash" class="text-light-red dark:text-dark-red w-6 h-6" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<CanvasNodeEditor v-for="(node, index) of canvas.nodes" :key="node.id" ref="nodes" :node="node" :index="index" :zoom="zoom" @select="selectNode" @edit="editNode" @move="(i, x, y) => moveNode([i], x, y)" @resize="(i, x, y, w, h) => resizeNode([i], x, y, w, h)" />
|
||||
<CanvasNodeEditor v-for="node of canvas.nodes" :key="node.id" ref="nodes" :node="node" :zoom="zoom" @select="select" @edit="edit" @move="(i, x, y) => moveNode([i], x, y)" @resize="(i, x, y, w, h) => resizeNode([i], x, y, w, h)" @input="(id, text) => editNodeProperty([id], node.type === 'group' ? 'label' : 'text', text)" />
|
||||
</div>
|
||||
<template v-for="edge of edges">
|
||||
<div :key="edge.id" v-if="edge.label" class="absolute z-10"
|
||||
:style="{ transform: labelCenter(edge.from!, edge.fromSide, edge.to!, edge.toSide) }">
|
||||
<div class="relative bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 px-4 py-2 -translate-x-[50%] -translate-y-[50%]">{{ edge.label }}</div>
|
||||
<div>
|
||||
<CanvasEdgeEditor v-for="edge of canvas.edges" :key="edge.id" ref="edges" :edge="edge" :nodes="canvas.nodes!" @select="select" @edit="edit" />
|
||||
</div>
|
||||
</template>
|
||||
<svg class="absolute top-0 left-0 w-1 h-1 overflow-visible">
|
||||
<g v-for="edge of edges" :key="edge.id" :style="{'--canvas-color': edge.color?.hex}" class="z-0">
|
||||
<path :style="`stroke-linecap: butt; stroke-width: calc(3px * var(--zoom-multiplier));`" :class="edge.color?.class ? `stroke-light-${edge.color.class} dark:stroke-dark-${edge.color.class}` : ((edge.color && edge.color?.hex !== undefined) ? 'stroke-[color:var(--canvas-color)]' : 'stroke-light-40 dark:stroke-dark-40')" class="fill-none stroke-[4px]" :d="edge.path.path"></path>
|
||||
<g :style="`transform: translate(${edge.path.to.x}px, ${edge.path.to.y}px) scale(var(--zoom-multiplier)) rotate(${rotation[edge.path.side]}deg);`">
|
||||
<polygon :class="edge.color?.class ? `fill-light-${edge.color.class} dark:fill-dark-${edge.color.class}` : ((edge.color && edge.color?.hex !== undefined) ? 'fill-[color:var(--canvas-color)]' : 'fill-light-40 dark:fill-dark-40')" points="0,0 6.5,10.4 -6.5,10.4"></polygon>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
<template>
|
||||
<div class="absolute overflow-visible h-px w-px">
|
||||
<div v-if="edge.label" :style="{ transform: labelPos }" class="relative bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 px-4 py-2 -translate-x-[50%] -translate-y-[50%]">{{ edge.label }}</div>
|
||||
<input v-else-if="editing" @click="e => e.stopImmediatePropagation()" :style="{ transform: `${labelPos} translate(-50%, -50%)` }" class="relative bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 px-4 py-2 z-20" :v-model="edge.label" />
|
||||
<svg ref="dom" class="absolute top-0 overflow-visible h-px w-px" @click="select" @dblclick="edit">
|
||||
<g :style="{'--canvas-color': edge.color?.hex}" class="z-0">
|
||||
<path :style="`stroke-linecap: butt; stroke-width: calc(3px * var(--zoom-multiplier));`" :class="[edge.color?.class ? `stroke-light-${edge.color.class} dark:stroke-dark-${edge.color.class}` : ((edge.color && edge.color?.hex !== undefined) ? 'stroke-[color:var(--canvas-color)]' : 'stroke-light-40 dark:stroke-dark-40'), { 'outline-4': focusing }]" class="fill-none stroke-[4px]" :d="path!.path"></path>
|
||||
<g :style="`transform: translate(${path!.to.x}px, ${path!.to.y}px) scale(var(--zoom-multiplier)) rotate(${rotation[path!.side]}deg);`">
|
||||
<polygon :class="edge.color?.class ? `fill-light-${edge.color.class} dark:fill-dark-${edge.color.class}` : ((edge.color && edge.color?.hex !== undefined) ? 'fill-[color:var(--canvas-color)]' : 'fill-light-40 dark:fill-dark-40')" points="0,0 6.5,10.4 -6.5,10.4"></polygon>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getPath, labelCenter, type Direction } from '#shared/canvas.util';
|
||||
import type { Element } from '../CanvasEditor.vue';
|
||||
import type { CanvasEdge, CanvasNode } from '~/types/canvas';
|
||||
|
||||
const rotation: Record<Direction, string> = {
|
||||
top: "180",
|
||||
bottom: "0",
|
||||
left: "90",
|
||||
right: "270"
|
||||
};
|
||||
|
||||
const { edge, nodes } = defineProps<{
|
||||
edge: CanvasEdge
|
||||
nodes: CanvasNode[]
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'select', id: Element): void,
|
||||
(e: 'edit', id: Element): void,
|
||||
(e: 'move', id: string, from: string, to: string): void,
|
||||
}>();
|
||||
|
||||
const dom = useTemplateRef('dom');
|
||||
const focusing = ref(false), editing = ref(false);
|
||||
|
||||
const from = computed(() => nodes!.find(f => f.id === edge.fromNode))
|
||||
const to = computed(() => nodes!.find(f => f.id === edge.toNode));
|
||||
const path = computed(() => getPath(from.value!, edge.fromSide, to.value!, edge.toSide));
|
||||
const labelPos = computed(() => labelCenter(from.value!, edge.fromSide, to.value!, edge.toSide));
|
||||
|
||||
function select(e: Event) {
|
||||
if(editing.value)
|
||||
return;
|
||||
|
||||
focusing.value = true;
|
||||
emit('select', { type: 'edge', id: edge.id });
|
||||
}
|
||||
function edit(e: Event) {
|
||||
focusing.value = true;
|
||||
editing.value = true;
|
||||
|
||||
e.stopImmediatePropagation();
|
||||
emit('edit', { type: 'edge', id: edge.id });
|
||||
}
|
||||
function unselect() {
|
||||
focusing.value = false;
|
||||
editing.value = false;
|
||||
}
|
||||
|
||||
defineExpose({ unselect, dom, id: edge.id });
|
||||
</script>
|
||||
|
|
@ -34,7 +34,8 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Direction } from '~/shared/canvas.util';
|
||||
import type { Direction } from '#shared/canvas.util';
|
||||
import type { Element } from '../CanvasEditor.vue';
|
||||
import FakeA from '../prose/FakeA.vue';
|
||||
import type { CanvasNode } from '~/types/canvas';
|
||||
|
||||
|
|
@ -44,21 +45,23 @@ const { node, zoom } = defineProps<{
|
|||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'select', id: string): void,
|
||||
(e: 'edit', id: string): void,
|
||||
(e: 'select', id: Element): void,
|
||||
(e: 'edit', id: Element): void,
|
||||
(e: 'move', id: string, x: number, y: number): void,
|
||||
(e: 'resize', id: string, x: number, y: number, w: number, h: number): void,
|
||||
(e: 'input', id: string, text: string): void,
|
||||
}>();
|
||||
|
||||
const dom = useTemplateRef('dom');
|
||||
const focusing = ref(false), editing = ref(false);
|
||||
let oldText = node.type === 'group' ? node.label : node.text;
|
||||
|
||||
function selectNode(e: Event) {
|
||||
if(editing.value)
|
||||
return;
|
||||
|
||||
focusing.value = true;
|
||||
emit('select', node.id);
|
||||
emit('select', { type: 'node', id: node.id });
|
||||
|
||||
dom.value?.addEventListener('mousedown', dragstart, { passive: true });
|
||||
}
|
||||
|
|
@ -66,10 +69,12 @@ function editNode(e: Event) {
|
|||
focusing.value = true;
|
||||
editing.value = true;
|
||||
|
||||
oldText = node.type === 'group' ? node.label : node.text;
|
||||
|
||||
e.stopImmediatePropagation();
|
||||
|
||||
dom.value?.removeEventListener('mousedown', dragstart);
|
||||
emit('edit', node.id);
|
||||
emit('edit', { type: 'node', id: node.id });
|
||||
}
|
||||
function resizeNode(e: MouseEvent, x: number, y: number, w: number, h: number) {
|
||||
e.stopImmediatePropagation();
|
||||
|
|
@ -100,6 +105,18 @@ function dragEdge(e: Event, direction: Direction) {
|
|||
e.stopImmediatePropagation();
|
||||
}
|
||||
function unselect() {
|
||||
if(editing.value)
|
||||
{
|
||||
const text = node.type === 'group' ? node.label : node.text;
|
||||
if(text !== oldText)
|
||||
{
|
||||
if(node.type === 'group')
|
||||
node.label = oldText;
|
||||
else
|
||||
node.text = oldText;
|
||||
emit('input', node.id, text);
|
||||
}
|
||||
}
|
||||
focusing.value = false;
|
||||
editing.value = false;
|
||||
|
||||
|
|
@ -134,7 +151,7 @@ const dragstart = (e: MouseEvent) => {
|
|||
window.addEventListener('mouseup', dragend, { passive: true });
|
||||
};
|
||||
|
||||
defineExpose({ unselect, dom, ...node });
|
||||
defineExpose({ unselect, dom, id: node.id });
|
||||
|
||||
const style = computed(() => {
|
||||
return node.color ? node.color?.class ?
|
||||
|
|
|
|||
|
|
@ -1,54 +1,70 @@
|
|||
import { clamp, lerp } from "~/shared/general.utils";
|
||||
import { clamp } from "~/shared/general.utils";
|
||||
|
||||
export const linear = (progress: number): number => progress;
|
||||
export const ease = (progress: number): number => -(Math.cos(Math.PI * progress) - 1) / 2;
|
||||
|
||||
export function useTween(ref: Ref<number>, animation: (progress: number) => number, then: () => void)
|
||||
{
|
||||
let initial = ref.value, current = ref.value, end = ref.value, progress = 0, time = 0, animationFrame: number, stop = true, last = 0;
|
||||
// State variables for animation
|
||||
let initial = ref.value;
|
||||
let current = ref.value;
|
||||
let end = ref.value;
|
||||
let animationFrame: number;
|
||||
let stop = true;
|
||||
let last = 0;
|
||||
|
||||
function loop(t: DOMHighResTimeStamp)
|
||||
{
|
||||
const elapsed = t - last;
|
||||
progress = clamp(progress + elapsed, 0, time);
|
||||
// Velocity tracking with sign preservation
|
||||
let velocity = 0;
|
||||
const velocityDecay = 0.92; // Slightly faster decay for more responsive feel
|
||||
|
||||
function loop(t: DOMHighResTimeStamp) {
|
||||
// Cap the elapsed time to prevent jumps during lag or tab switches
|
||||
const elapsed = Math.min(t - last, 32);
|
||||
last = t;
|
||||
|
||||
const step = animation(clamp(progress / time, 0, 1));
|
||||
current = lerp(initial, end, step);
|
||||
// Update velocity considering the sign of the movement
|
||||
velocity = velocity * velocityDecay + (end - current) * (1 - velocityDecay);
|
||||
|
||||
// Apply velocity to current position
|
||||
// Normalize by elapsed time to maintain consistent speed regardless of frame rate
|
||||
current = clamp(current + velocity * (elapsed / 16), initial, end);
|
||||
|
||||
if(current === end)
|
||||
stop = true;
|
||||
|
||||
// Trigger callback
|
||||
then();
|
||||
|
||||
if(progress < time && !stop)
|
||||
{
|
||||
// Continue animation if not stopped
|
||||
if (!stop) {
|
||||
animationFrame = requestAnimationFrame(loop);
|
||||
}
|
||||
else
|
||||
{
|
||||
progress = 0;
|
||||
stop = true;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
stop: () => {
|
||||
cancelAnimationFrame(animationFrame);
|
||||
stop = true;
|
||||
velocity = 0; // Reset velocity when stopping
|
||||
},
|
||||
update: (target: number, duration: number) => {
|
||||
if (stop) {
|
||||
// Only reset initial position when starting a new animation
|
||||
initial = current;
|
||||
time = duration + progress;
|
||||
last = performance.now();
|
||||
}
|
||||
|
||||
end = target;
|
||||
|
||||
ref.value = target;
|
||||
|
||||
if(stop)
|
||||
{
|
||||
if (stop) {
|
||||
stop = false;
|
||||
last = performance.now();
|
||||
|
||||
loop(performance.now());
|
||||
}
|
||||
},
|
||||
refresh: () => { current = ref.value; },
|
||||
current: () => { return current; },
|
||||
refresh: () => {
|
||||
current = ref.value;
|
||||
velocity = 0; // Reset velocity on refresh
|
||||
},
|
||||
current: () => current,
|
||||
};
|
||||
}
|
||||
BIN
db.sqlite-shm
BIN
db.sqlite-shm
Binary file not shown.
BIN
db.sqlite-wal
BIN
db.sqlite-wal
Binary file not shown.
Loading…
Reference in New Issue