640 lines
29 KiB
Vue
640 lines
29 KiB
Vue
<script lang="ts">
|
|
import type { Position } from '#shared/canvas.util';
|
|
import type CanvasNodeEditor from './canvas/CanvasNodeEditor.vue';
|
|
import type CanvasEdgeEditor from './canvas/CanvasEdgeEditor.vue';
|
|
export type Element = { type: 'node' | 'edge', id: string };
|
|
|
|
interface ActionMap {
|
|
remove: CanvasNode | CanvasEdge | undefined;
|
|
create: CanvasNode | CanvasEdge | undefined;
|
|
property: CanvasNode | CanvasEdge;
|
|
}
|
|
type Action = keyof ActionMap;
|
|
interface HistoryEvent<T extends Action = Action>
|
|
{
|
|
event: T;
|
|
actions: HistoryAction<T>[];
|
|
}
|
|
interface HistoryAction<T extends Action>
|
|
{
|
|
element: Element;
|
|
from: ActionMap[T];
|
|
to: ActionMap[T];
|
|
}
|
|
|
|
type NodeEditor = InstanceType<typeof CanvasNodeEditor>;
|
|
type EdgeEditor = InstanceType<typeof CanvasEdgeEditor>;
|
|
|
|
const cancelEvent = (e: Event) => e.preventDefault();
|
|
const stopPropagation = (e: Event) => e.stopImmediatePropagation();
|
|
function getID(length: number)
|
|
{
|
|
for (var id = [], i = 0; i < length; i++)
|
|
id.push((16 * Math.random() | 0).toString(16));
|
|
return id.join("");
|
|
}
|
|
function center(touches: TouchList): Position
|
|
{
|
|
const pos = { x: 0, y: 0 };
|
|
|
|
for(const touch of touches)
|
|
{
|
|
pos.x += touch.clientX;
|
|
pos.y += touch.clientY;
|
|
}
|
|
|
|
pos.x /= touches.length;
|
|
pos.y /= touches.length;
|
|
return pos;
|
|
}
|
|
function distance(touches: TouchList): number
|
|
{
|
|
const [A, B] = touches;
|
|
return Math.hypot(B.clientX - A.clientX, B.clientY - A.clientY);
|
|
}
|
|
function contains(group: CanvasNode, node: CanvasNode): boolean
|
|
{
|
|
return group.x < node.x && group.y < node.y && group.x + group.width > node.x + node.width && group.y + group.height > node.y + node.height;
|
|
}
|
|
</script>
|
|
|
|
<script setup lang="ts">
|
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
|
import { clamp } from '#shared/general.utils';
|
|
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<Element>(), editing = ref<Element>();
|
|
const canvasRef = useTemplateRef('canvasRef'), transformRef = useTemplateRef('transformRef');
|
|
const nodes = useTemplateRef<NodeEditor[]>('nodes'), edges = useTemplateRef<EdgeEditor[]>('edges');
|
|
const canvasSettings = useCookie<{
|
|
snap: boolean,
|
|
size: number
|
|
}>('canvasPreference', { default: () => ({ snap: true, size: 32 }) });
|
|
const snap = computed({
|
|
get: () => canvasSettings.value.snap,
|
|
set: (value: boolean) => canvasSettings.value = { ...canvasSettings.value, snap: value },
|
|
}), gridSize = computed({
|
|
get: () => canvasSettings.value.size,
|
|
set: (value: number) => canvasSettings.value = { ...canvasSettings.value, size: value },
|
|
});
|
|
|
|
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);
|
|
const historyCursor = computed(() => history.value.length > 0 && historyPos.value > -1 ? history.value[historyPos.value] : undefined);
|
|
|
|
const reset = (_: MouseEvent) => {
|
|
zoom.value = minZoom.value;
|
|
|
|
dispX.value = 0;
|
|
dispY.value = 0;
|
|
|
|
updateTransform();
|
|
}
|
|
|
|
function addAction<T extends Action = Action>(event: T, actions: HistoryAction<T>[])
|
|
{
|
|
historyPos.value++;
|
|
history.value.splice(historyPos.value, history.value.length - historyPos.value);
|
|
history.value[historyPos.value] = { event, actions };
|
|
}
|
|
onMounted(() => {
|
|
let lastX = 0, lastY = 0, lastDistance = 0;
|
|
let box = canvasRef.value?.getBoundingClientRect()!;
|
|
const dragMove = (e: MouseEvent) => {
|
|
dispX.value = dispX.value - (lastX - e.clientX) / zoom.value;
|
|
dispY.value = dispY.value - (lastY - e.clientY) / zoom.value;
|
|
|
|
lastX = e.clientX;
|
|
lastY = e.clientY;
|
|
|
|
updateTransform();
|
|
};
|
|
const dragEnd = (e: MouseEvent) => {
|
|
window.removeEventListener('mouseup', dragEnd);
|
|
window.removeEventListener('mousemove', dragMove);
|
|
};
|
|
canvasRef.value?.addEventListener('mouseenter', () => {
|
|
window.addEventListener('wheel', cancelEvent, { passive: false });
|
|
document.addEventListener('gesturestart', cancelEvent);
|
|
document.addEventListener('gesturechange', cancelEvent);
|
|
|
|
canvasRef.value?.addEventListener('mouseleave', () => {
|
|
window.removeEventListener('wheel', cancelEvent);
|
|
document.removeEventListener('gesturestart', cancelEvent);
|
|
document.removeEventListener('gesturechange', cancelEvent);
|
|
});
|
|
})
|
|
window.addEventListener('resize', () => box = canvasRef.value?.getBoundingClientRect()!);
|
|
canvasRef.value?.addEventListener('mousedown', (e) => {
|
|
if(e.button === 1)
|
|
{
|
|
lastX = e.clientX;
|
|
lastY = e.clientY;
|
|
|
|
window.addEventListener('mouseup', dragEnd, { passive: true });
|
|
window.addEventListener('mousemove', dragMove, { passive: true });
|
|
}
|
|
}, { passive: true });
|
|
canvasRef.value?.addEventListener('wheel', (e) => {
|
|
if((zoom.value >= 3 && e.deltaY < 0) || (zoom.value <= minZoom.value && e.deltaY > 0))
|
|
return;
|
|
|
|
const diff = Math.exp(e.deltaY * -0.001);
|
|
const centerX = (box.x + box.width / 2), centerY = (box.y + box.height / 2);
|
|
const mousex = centerX - e.clientX, mousey = centerY - e.clientY;
|
|
|
|
dispX.value = dispX.value - (mousex / (diff * zoom.value) - mousex / zoom.value);
|
|
dispY.value = dispY.value - (mousey / (diff * zoom.value) - mousey / zoom.value);
|
|
|
|
zoom.value = clamp(zoom.value * diff, minZoom.value, 3)
|
|
|
|
updateTransform();
|
|
}, { passive: true });
|
|
canvasRef.value?.addEventListener('touchstart', (e) => {
|
|
({ x: lastX, y: lastY } = center(e.touches));
|
|
|
|
if(e.touches.length > 1)
|
|
{
|
|
lastDistance = distance(e.touches);
|
|
}
|
|
|
|
canvasRef.value?.addEventListener('touchend', touchend, { passive: true });
|
|
canvasRef.value?.addEventListener('touchcancel', touchcancel, { passive: true });
|
|
canvasRef.value?.addEventListener('touchmove', touchmove, { passive: true });
|
|
}, { passive: true });
|
|
const touchend = (e: TouchEvent) => {
|
|
if(e.touches.length > 1)
|
|
{
|
|
({ x: lastX, y: lastY } = center(e.touches));
|
|
}
|
|
|
|
canvasRef.value?.removeEventListener('touchend', touchend);
|
|
canvasRef.value?.removeEventListener('touchcancel', touchcancel);
|
|
canvasRef.value?.removeEventListener('touchmove', touchmove);
|
|
};
|
|
const touchcancel = (e: TouchEvent) => {
|
|
if(e.touches.length > 1)
|
|
{
|
|
({ x: lastX, y: lastY } = center(e.touches));
|
|
}
|
|
|
|
canvasRef.value?.removeEventListener('touchend', touchend);
|
|
canvasRef.value?.removeEventListener('touchcancel', touchcancel);
|
|
canvasRef.value?.removeEventListener('touchmove', touchmove);
|
|
};
|
|
const touchmove = (e: TouchEvent) => {
|
|
const pos = center(e.touches);
|
|
dispX.value = dispX.value - (lastX - pos.x) / zoom.value;
|
|
dispY.value = dispY.value - (lastY - pos.y) / zoom.value;
|
|
lastX = pos.x;
|
|
lastY = pos.y;
|
|
|
|
if(e.touches.length === 2)
|
|
{
|
|
const dist = distance(e.touches);
|
|
const diff = dist / lastDistance;
|
|
|
|
zoom.value = clamp(zoom.value * diff, minZoom.value, 3);
|
|
}
|
|
|
|
updateTransform();
|
|
};
|
|
|
|
updateTransform();
|
|
});
|
|
|
|
function updateTransform()
|
|
{
|
|
if(transformRef.value)
|
|
{
|
|
transformRef.value.style.transform = `scale3d(${zoom.value}, ${zoom.value}, 1) translate3d(${dispX.value}px, ${dispY.value}px, 0)`;
|
|
transformRef.value.style.setProperty('--tw-scale', zoom.value.toString());
|
|
}
|
|
}
|
|
function moveNode(ids: string[], deltax: number, deltay: number)
|
|
{
|
|
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: { type: 'node', id }, from: { ...node, x: node.x - deltax, y: node.y - deltay }, to: { ...node } });
|
|
}
|
|
|
|
addAction('property', actions);
|
|
}
|
|
function resizeNode(ids: string[], deltax: number, deltay: number, deltaw: number, deltah: number)
|
|
{
|
|
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: { type: 'node', id }, from: { ...node, x: node.x - deltax, y: node.y - deltay, width: node.width - deltaw, height: node.height - deltah }, to: { ...node } });
|
|
}
|
|
|
|
addAction('property', actions);
|
|
}
|
|
function select(element: Element)
|
|
{
|
|
if(focusing.value && (focusing.value.id !== element.id || focusing.value.type !== element.type))
|
|
{
|
|
unselect();
|
|
}
|
|
|
|
focusing.value = element;
|
|
|
|
focused.value?.dom?.addEventListener('click', stopPropagation, { passive: true });
|
|
canvasRef.value?.addEventListener('click', unselect, { once: true });
|
|
}
|
|
function edit(element: Element)
|
|
{
|
|
editing.value = element;
|
|
|
|
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)
|
|
{
|
|
let box = canvasRef.value?.getBoundingClientRect()!;
|
|
const width = 250, height = 100;
|
|
const x = (e.layerX / zoom.value) - dispX.value - (width / 2);
|
|
const y = (e.layerY / zoom.value) - dispY.value - (height / 2);
|
|
const node: CanvasNode = { id: getID(16), x, y, width, height, type: 'text' };
|
|
|
|
if(!canvas.value.nodes)
|
|
canvas.value.nodes = [node];
|
|
else
|
|
canvas.value.nodes.push(node);
|
|
|
|
addAction('create', [{ element: { type: 'node', id: node.id }, from: undefined, to: node }]);
|
|
}
|
|
function remove(elements: Element[])
|
|
{
|
|
if(elements.length === 0)
|
|
return;
|
|
|
|
const actions: HistoryAction<'remove'>[] = [];
|
|
focusing.value = undefined;
|
|
editing.value = undefined;
|
|
|
|
const c = canvas.value;
|
|
|
|
for(const element of elements)
|
|
{
|
|
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])
|
|
{
|
|
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: { type: 'node', id }, from: copy, to: canvas.value.nodes!.find(e => e.id === id)! });
|
|
}
|
|
|
|
addAction('property', actions);
|
|
}
|
|
function editEdgeProperty<T extends keyof CanvasEdge>(ids: string[], property: T, value: CanvasEdge[T])
|
|
{
|
|
if(ids.length === 0)
|
|
return;
|
|
|
|
const actions: HistoryAction<'property'>[] = [];
|
|
|
|
for(const id of ids)
|
|
{
|
|
const copy = JSON.parse(JSON.stringify(canvas.value.edges!.find(e => e.id === id)!)) as CanvasEdge;
|
|
canvas.value.edges!.find(e => e.id === id)![property] = value;
|
|
actions.push({ element: { type: 'edge', id }, from: copy, to: canvas.value.edges!.find(e => e.id === id)! });
|
|
}
|
|
|
|
addAction('property', actions);
|
|
}
|
|
|
|
const unselect = () => {
|
|
if(focusing.value) console.log("Unselecting %s (%s)", focusing.value.id, focusing.value.type);
|
|
if(focusing.value !== undefined)
|
|
{
|
|
focused.value?.dom?.removeEventListener('click', stopPropagation);
|
|
focused.value?.unselect();
|
|
}
|
|
focusing.value = undefined;
|
|
|
|
if(editing.value !== undefined)
|
|
{
|
|
edited.value?.dom?.removeEventListener('wheel', stopPropagation);
|
|
edited.value?.dom?.removeEventListener('dblclick', stopPropagation);
|
|
edited.value?.dom?.removeEventListener('click', stopPropagation);
|
|
edited.value?.unselect();
|
|
}
|
|
editing.value = undefined;
|
|
};
|
|
const undo = () => {
|
|
if(!historyCursor.value)
|
|
return;
|
|
|
|
for(const action of historyCursor.value.actions)
|
|
{
|
|
if(action.element.type === 'node')
|
|
{
|
|
switch(historyCursor.value.event)
|
|
{
|
|
case 'create':
|
|
{
|
|
const a = action as HistoryAction<'create'>;
|
|
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 as CanvasNode);
|
|
break;
|
|
}
|
|
case 'property':
|
|
{
|
|
const a = action as HistoryAction<'property'>;
|
|
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--;
|
|
};
|
|
const redo = () => {
|
|
if(!history.value || history.value.length - 1 <= historyPos.value)
|
|
return;
|
|
|
|
historyPos.value++;
|
|
|
|
if(!historyCursor.value)
|
|
{
|
|
historyPos.value--;
|
|
return;
|
|
}
|
|
|
|
for(const action of historyCursor.value.actions)
|
|
{
|
|
if(action.element.type === 'node')
|
|
{
|
|
switch(historyCursor.value.event)
|
|
{
|
|
case 'create':
|
|
{
|
|
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.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.id);
|
|
canvas.value.nodes![index] = a.to as CanvasNode;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
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) { remove([focusing.value]) } }
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div ref="canvasRef" class="absolute top-0 left-0 overflow-hidden w-full h-full touch-none" :style="{ '--zoom-multiplier': (1 / Math.pow(zoom, 0.7)) }" @dblclick.left="createNode">
|
|
<div class="flex flex-col absolute sm:top-2 top-10 left-2 z-[35] overflow-hidden gap-4" @click="stopPropagation" @dblclick="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="zoom = clamp(zoom * 1.1, minZoom, 3); 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>
|
|
<Tooltip message="Reset" side="right">
|
|
<div @click="zoom = 1; 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:reload" />
|
|
</div>
|
|
</Tooltip>
|
|
<Tooltip message="Tout contenir" side="right">
|
|
<div @click="reset" 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:corners" />
|
|
</div>
|
|
</Tooltip>
|
|
<Tooltip message="Zoom arrière" side="right">
|
|
<div @click="zoom = clamp(zoom / 1.1, minZoom, 3); 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>
|
|
</div>
|
|
<div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10">
|
|
<Tooltip message="Annuler (Ctrl+Z)" side="right">
|
|
<div @click="undo" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer" :class="{ 'text-light-50 dark:text-dark-50 !cursor-default hover:bg-transparent dark:hover:bg-transparent': historyPos === -1 }">
|
|
<Icon icon="ph:arrow-bend-up-left" />
|
|
</div>
|
|
</Tooltip>
|
|
<Tooltip message="Retablir (Ctrl+Y)" side="right">
|
|
<div @click="redo" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer" :class="{ 'text-light-50 dark:text-dark-50 !cursor-default hover:bg-transparent dark:hover:bg-transparent': historyPos === history.length - 1 }">
|
|
<Icon icon="ph:arrow-bend-up-right" />
|
|
</div>
|
|
</Tooltip>
|
|
</div>
|
|
<div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10">
|
|
<Tooltip message="Aide" side="right">
|
|
<Dialog title="Aide" iconClose>
|
|
<template #trigger>
|
|
<div 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:question-mark-circled" />
|
|
</div>
|
|
</template>
|
|
<template #default>
|
|
<div class="flex flex-row justify-between px-4">
|
|
<div class="flex flex-col gap-2">
|
|
<ProseH4>Ordinateur</ProseH4>
|
|
<div class="flex items-center"><Icon icon="ph:mouse-left-click-fill" class="w-6 h-6"/>: Selectionner</div>
|
|
<div class="flex items-center"><Icon icon="ph:mouse-left-click-fill" class="w-6 h-6"/><Icon icon="ph:mouse-left-click-fill" class="w-6 h-6"/>: Modifier</div>
|
|
<div class="flex items-center"><Icon icon="ph:mouse-middle-click-fill" class="w-6 h-6"/>: Déplacer</div>
|
|
<div class="flex items-center"><Icon icon="ph:mouse-right-click-fill" class="w-6 h-6"/>: Menu</div>
|
|
</div>
|
|
<div class="flex flex-col gap-2">
|
|
<ProseH4>Mobile</ProseH4>
|
|
<div class="flex items-center"><Icon icon="ph:hand-tap" class="w-6 h-6"/>: Selectionner</div>
|
|
<div class="flex items-center"><Icon icon="ph:hand-tap" class="w-6 h-6"/><Icon icon="ph:hand-tap" class="w-6 h-6"/>: Modifier</div>
|
|
<div class="flex items-center"><Icon icon="mdi:gesture-pinch" class="w-6 h-6"/>: Zoomer</div>
|
|
<div class="flex items-center"><Icon icon="ph:hand-tap" class="w-6 h-6"/> maintenu: Menu</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</Dialog>
|
|
</Tooltip>
|
|
</div>
|
|
</div>
|
|
<div ref="transformRef" :style="{
|
|
'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 && 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>
|
|
<div @click="stopPropagation">
|
|
<Tooltip message="Couleur" side="top">
|
|
<div 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="ph:palette" class="w-6 h-6" />
|
|
</div>
|
|
</Tooltip>
|
|
</div>
|
|
</PopoverTrigger>
|
|
<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.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.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.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.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.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.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.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!.id], 'color', { hex: (e.target as HTMLInputElement).value })" type="color" class="appearance-none w-0 h-0 absolute" />
|
|
</div>
|
|
</label>
|
|
</div>
|
|
</PopoverContent>
|
|
</PopoverPortal>
|
|
</PopoverRoot>
|
|
<Tooltip message="Supprimer" side="top">
|
|
<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 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)" :snapping="snap" :grid="gridSize" />
|
|
</div>
|
|
<div>
|
|
<CanvasEdgeEditor v-for="edge of canvas.edges" :key="edge.id" ref="edges" :edge="edge" :nodes="canvas.nodes!" @select="select" @edit="edit" @input="(id, text) => editEdgeProperty([id], 'label', text)" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template> |