Add grid snapping, grid preview, fix zoom slowdowns and canvas markdown editing being at the wrong size.
This commit is contained in:
parent
0b97e9a295
commit
f2d00097d6
|
|
@ -1,10 +1,9 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { bezier, getBbox, getPath, opposite, posFromDir, rotation, type Box, type Direction, type Path, type Position } from '#shared/canvas.util';
|
import { bezier, getBbox, opposite, posFromDir, rotation, type Box, type Direction, type Path, type Position } from '#shared/canvas.util';
|
||||||
import type CanvasNodeEditor from './canvas/CanvasNodeEditor.vue';
|
import type CanvasNodeEditor from './canvas/CanvasNodeEditor.vue';
|
||||||
import type CanvasEdgeEditor from './canvas/CanvasEdgeEditor.vue';
|
import type CanvasEdgeEditor from './canvas/CanvasEdgeEditor.vue';
|
||||||
import { SnapFinder, type SnapHint } from '#shared/physics.util';
|
import { SnapFinder, type SnapHint } from '#shared/physics.util';
|
||||||
import type { CanvasPreferences } from '~/types/general';
|
import type { CanvasPreferences } from '~/types/general';
|
||||||
import Id from '~/pages/user/[id].vue';
|
|
||||||
export type Element = { type: 'node' | 'edge', id: string };
|
export type Element = { type: 'node' | 'edge', id: string };
|
||||||
|
|
||||||
interface ActionMap {
|
interface ActionMap {
|
||||||
|
|
@ -67,17 +66,28 @@ const props = defineProps<{
|
||||||
path: string,
|
path: string,
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const dispX = ref(0), dispY = ref(0), minZoom = ref(0.1), zoom = ref(0.5);
|
const dispX = ref(0), dispY = ref(0), minZoom = ref(0.1), zoom = ref(0.5), spacing = ref<number | undefined>(32);
|
||||||
const focusing = ref<Element>(), editing = ref<Element>();
|
const focusing = ref<Element>(), editing = ref<Element>();
|
||||||
const canvasRef = useTemplateRef('canvasRef'), transformRef = useTemplateRef('transformRef'), toolbarRef = useTemplateRef('toolbarRef'), viewportSize = useElementBounding(canvasRef);
|
const canvasRef = useTemplateRef('canvasRef'), transformRef = useTemplateRef('transformRef'), patternRef = useTemplateRef('patternRef'), toolbarRef = useTemplateRef('toolbarRef'), viewportSize = useElementBounding(canvasRef);
|
||||||
const nodes = useTemplateRef<NodeEditor[]>('nodes'), edges = useTemplateRef<EdgeEditor[]>('edges');
|
const nodes = useTemplateRef<NodeEditor[]>('nodes'), edges = useTemplateRef<EdgeEditor[]>('edges');
|
||||||
const canvasSettings = useCookie<CanvasPreferences>('canvasPreference', { default: () => ({ gridSnap: true, neighborSnap: true }) });
|
const canvasSettings = useCookie<CanvasPreferences>('canvasPreference', { default: () => ({ gridSnap: true, neighborSnap: true, spacing: 32 }) });
|
||||||
const hints = ref<SnapHint[]>([]);
|
const hints = ref<SnapHint[]>([]);
|
||||||
const viewport = computed<Box>(() => {
|
const viewport = computed<Box>(() => {
|
||||||
const width = viewportSize.width.value / zoom.value, height = viewportSize.height.value / zoom.value;
|
const width = viewportSize.width.value / zoom.value, height = viewportSize.height.value / zoom.value;
|
||||||
const movementX = viewportSize.width.value - width, movementY = viewportSize.height.value - height;
|
const movementX = viewportSize.width.value - width, movementY = viewportSize.height.value - height;
|
||||||
return { x: -dispX.value + movementX / 2, y: -dispY.value + movementY / 2, w: width, h: height };
|
return { x: -dispX.value + movementX / 2, y: -dispY.value + movementY / 2, w: width, h: height };
|
||||||
});
|
});
|
||||||
|
const updateScaleVar = useDebounceFn(() => {
|
||||||
|
if(transformRef.value)
|
||||||
|
{
|
||||||
|
console.log(zoom.value);
|
||||||
|
transformRef.value.style.setProperty('--tw-scale', zoom.value.toString());
|
||||||
|
}
|
||||||
|
if(canvasRef.value)
|
||||||
|
{
|
||||||
|
canvasRef.value.style.setProperty('--zoom-multiplier', (1 / Math.pow(zoom.value, 0.7)).toFixed(3));
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
|
||||||
type DragOrigin = { type: 'edge', id: string, destination: 'from' | 'to', node: string } | { type: 'node', id: string };
|
type DragOrigin = { type: 'edge', id: string, destination: 'from' | 'to', node: string } | { type: 'node', id: string };
|
||||||
const fakeEdge = ref<{ from?: Position, fromSide?: Direction, to?: Position, toSide?: Direction, path?: Path, style?: { stroke: string, fill: string }, hex?: string, drag?: DragOrigin, snapped?: { node: string, side: Direction } }>({});
|
const fakeEdge = ref<{ from?: Position, fromSide?: Direction, to?: Position, toSide?: Direction, path?: Path, style?: { stroke: string, fill: string }, hex?: string, drag?: DragOrigin, snapped?: { node: string, side: Direction } }>({});
|
||||||
|
|
@ -165,7 +175,8 @@ onMounted(() => {
|
||||||
dispX.value = dispX.value - (mousex / (diff * zoom.value) - mousex / zoom.value);
|
dispX.value = dispX.value - (mousex / (diff * zoom.value) - mousex / zoom.value);
|
||||||
dispY.value = dispY.value - (mousey / (diff * zoom.value) - mousey / zoom.value);
|
dispY.value = dispY.value - (mousey / (diff * zoom.value) - mousey / zoom.value);
|
||||||
|
|
||||||
zoom.value = clamp(zoom.value * diff, minZoom.value, 3)
|
zoom.value = clamp(zoom.value * diff, minZoom.value, 3);
|
||||||
|
spacing.value = canvasSettings.value.gridSnap ? canvasSettings.value.spacing ?? 32 : undefined;
|
||||||
|
|
||||||
updateTransform();
|
updateTransform();
|
||||||
}, { passive: true });
|
}, { passive: true });
|
||||||
|
|
@ -227,7 +238,23 @@ function updateTransform()
|
||||||
if(transformRef.value)
|
if(transformRef.value)
|
||||||
{
|
{
|
||||||
transformRef.value.style.transform = `scale3d(${zoom.value}, ${zoom.value}, 1) translate3d(${dispX.value}px, ${dispY.value}px, 0)`;
|
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());
|
updateScaleVar();
|
||||||
|
}
|
||||||
|
if(patternRef.value && canvasSettings.value.gridSnap)
|
||||||
|
{
|
||||||
|
patternRef.value.parentElement?.classList.remove('hidden');
|
||||||
|
patternRef.value.setAttribute("x", (viewportSize.width.value / 2 + dispX.value % spacing.value! * zoom.value).toFixed(3));
|
||||||
|
patternRef.value.setAttribute("y", (viewportSize.height.value / 2 + dispY.value % spacing.value! * zoom.value).toFixed(3));
|
||||||
|
patternRef.value.setAttribute("width", (zoom.value * spacing.value!).toFixed(3));
|
||||||
|
patternRef.value.setAttribute("height", (zoom.value * spacing.value!).toFixed(3));
|
||||||
|
|
||||||
|
patternRef.value.children[0].setAttribute('cx', (zoom.value).toFixed(3));
|
||||||
|
patternRef.value.children[0].setAttribute('cy', (zoom.value).toFixed(3));
|
||||||
|
patternRef.value.children[0].setAttribute('r', (zoom.value).toFixed(3));
|
||||||
|
}
|
||||||
|
else if(patternRef.value && !canvasSettings.value.gridSnap)
|
||||||
|
{
|
||||||
|
patternRef.value.parentElement?.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function updateToolbarTransform()
|
function updateToolbarTransform()
|
||||||
|
|
@ -680,7 +707,7 @@ useShortcuts({
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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 ref="canvasRef" class="absolute top-0 left-0 overflow-hidden w-full h-full touch-none" @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="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">
|
<div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10">
|
||||||
<Tooltip message="Zoom avant" side="right">
|
<Tooltip message="Zoom avant" side="right">
|
||||||
|
|
@ -717,6 +744,20 @@ useShortcuts({
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10">
|
<div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10">
|
||||||
|
<Tooltip message="Préférences" side="right">
|
||||||
|
<Dialog title="Préférences" 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:gear" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #default>
|
||||||
|
<Switch v-model="canvasSettings.neighborSnap" label="S'accrocher aux voisins" @update:model-value="snapFinder.config.preferences = canvasSettings" />
|
||||||
|
<Switch v-model="canvasSettings.gridSnap" label="S'accrocher à la grille" @update:model-value="(v) => { canvasSettings.spacing = v ? 32 : undefined; snapFinder.config.preferences = canvasSettings }" />
|
||||||
|
<NumberPicker v-model="canvasSettings.spacing" label="Taille de la grille" :disabled="!canvasSettings.gridSnap" @update:model-value="(v) => { spacing = v; updateTransform(); snapFinder.config.preferences = canvasSettings}" />
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</Tooltip>
|
||||||
<Tooltip message="Aide" side="right">
|
<Tooltip message="Aide" side="right">
|
||||||
<Dialog title="Aide" iconClose>
|
<Dialog title="Aide" iconClose>
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
|
|
@ -746,6 +787,12 @@ useShortcuts({
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<svg class="absolute top-0 left-0 w-full h-full pointer-events-none">
|
||||||
|
<pattern ref="patternRef" id="canvasPattern" patternUnits="userSpaceOnUse">
|
||||||
|
<circle cx="0.75" cy="0.75" r="0.75" class="fill-light-35 dark:fill-dark-35"></circle>
|
||||||
|
</pattern>
|
||||||
|
<rect x="0" y="0" width="100%" height="100%" fill="url(#canvasPattern)"></rect>
|
||||||
|
</svg>
|
||||||
<div ref="transformRef" :style="{
|
<div ref="transformRef" :style="{
|
||||||
'transform-origin': 'center center',
|
'transform-origin': 'center center',
|
||||||
}" class="h-full">
|
}" class="h-full">
|
||||||
|
|
@ -855,7 +902,7 @@ useShortcuts({
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<CanvasNodeEditor v-for="node of canvas.nodes" :key="node.id" ref="nodes" :node="node" :zoom="zoom"
|
<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)" :snap="snapFinder.findNodeSnapPosition.bind(snapFinder)" @edge="dragStartEdgeTo" />
|
@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)" :snap="snapFinder.findNodeSnapPosition.bind(snapFinder)" @edge="dragStartEdgeTo" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -871,7 +918,7 @@ useShortcuts({
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<svg class="absolute overflow-visible top-0 overflow-visible h-px w-px fill-accent-purple stroke-accent-purple stroke-1 z-50">
|
<svg class="absolute overflow-visible top-0 h-px w-px fill-accent-purple stroke-accent-purple stroke-1 z-50">
|
||||||
<g v-for="hint of hints">
|
<g v-for="hint of hints">
|
||||||
<circle :cx="hint.start.x" :cy="hint.start.y" r="3" />
|
<circle :cx="hint.start.x" :cy="hint.start.y" r="3" />
|
||||||
<circle v-if="hint.end" :cx="hint.end.x" :cy="hint.end.y" r="3" />
|
<circle v-if="hint.end" :cx="hint.end.x" :cy="hint.end.y" r="3" />
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,11 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Editor, EditorConfiguration, EditorChange } from 'codemirror';
|
import type { Editor, EditorConfiguration, EditorChange } from 'codemirror';
|
||||||
import { changeEnd } from 'codemirror';
|
|
||||||
import { fromTextArea } from 'hypermd';
|
import { fromTextArea } from 'hypermd';
|
||||||
|
|
||||||
import '#shared/hypermd.extend';
|
import '#shared/hypermd.extend';
|
||||||
|
|
||||||
function onChange(cm: Editor, change: EditorChange)
|
|
||||||
{
|
|
||||||
if (changeEnd(change).line == cm.lastLine())
|
|
||||||
updateBottomMargin(cm);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function updateBottomMargin(cm: Editor)
|
|
||||||
{
|
|
||||||
let padding = "";
|
|
||||||
if (cm.lineCount() > 1)
|
|
||||||
{
|
|
||||||
//@ts-ignore
|
|
||||||
let totalH = cm.display.scroller.clientHeight - 30, lastLineH = cm.getLineHandle(cm.lastLine()).height;
|
|
||||||
padding = (totalH / 2 - lastLineH) + "px";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cm.state.scrollPastEndPadding != padding)
|
|
||||||
{
|
|
||||||
cm.state.scrollPastEndPadding = padding;
|
|
||||||
cm.display.lineSpace.parentNode.style.paddingBottom = padding;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
||||||
const { placeholder, autofocus = false, gutters = true, format = 'd-any' } = defineProps<{
|
const { placeholder, autofocus = false, gutters = true, format = 'd-any' } = defineProps<{
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
autofocus?: boolean
|
autofocus?: boolean
|
||||||
|
|
@ -73,10 +47,7 @@ onMounted(() => {
|
||||||
} as EditorConfiguration);
|
} as EditorConfiguration);
|
||||||
|
|
||||||
e.setValue(model.value ?? '');
|
e.setValue(model.value ?? '');
|
||||||
updateBottomMargin(e);
|
|
||||||
e.on('change', onChange);
|
|
||||||
e.on('change', (cm: Editor, change: EditorChange) => model.value = cm.getValue());
|
e.on('change', (cm: Editor, change: EditorChange) => model.value = cm.getValue());
|
||||||
e.on('refresh', updateBottomMargin);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -87,6 +58,8 @@ watchEffect(() => {
|
||||||
editor.value.clearHistory();
|
editor.value.clearHistory();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
defineExpose({ focus: () => editor.value?.focus() });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -108,6 +81,10 @@ watchEffect(() => {
|
||||||
{
|
{
|
||||||
@apply hidden;
|
@apply hidden;
|
||||||
}
|
}
|
||||||
|
.CodeMirror-sizer
|
||||||
|
{
|
||||||
|
@apply !px-3;
|
||||||
|
}
|
||||||
.cancel-gutters .CodeMirror-sizer
|
.cancel-gutters .CodeMirror-sizer
|
||||||
{
|
{
|
||||||
@apply ms-2;
|
@apply ms-2;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
<template>
|
||||||
|
<Editor ref="editor" v-model="model" autofocus :gutters="false" />
|
||||||
|
<iframe ref="iframe" class="w-full h-full border-0" sandbox="allow-same-origin allow-scripts"></iframe>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const model = defineModel<string>();
|
||||||
|
const editor = useTemplateRef('editor'), iframe = useTemplateRef('iframe');
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if(iframe.value && iframe.value.contentDocument && editor.value)
|
||||||
|
{
|
||||||
|
editor.value.$el.remove();
|
||||||
|
|
||||||
|
iframe.value.contentDocument.documentElement.setAttribute('class', document.documentElement.getAttribute('class') ?? '');
|
||||||
|
iframe.value.contentDocument.documentElement.setAttribute('style', document.documentElement.getAttribute('style') ?? '');
|
||||||
|
|
||||||
|
const base = iframe.value.contentDocument.head.appendChild(iframe.value.contentDocument.createElement('base'));
|
||||||
|
base.setAttribute('href', window.location.href);
|
||||||
|
|
||||||
|
for(let element of document.getElementsByTagName('link'))
|
||||||
|
{
|
||||||
|
if(element.getAttribute('rel') === 'stylesheet')
|
||||||
|
iframe.value.contentDocument.head.appendChild(element.cloneNode(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
for(let element of document.getElementsByTagName('style'))
|
||||||
|
{
|
||||||
|
iframe.value.contentDocument.head.appendChild(element.cloneNode(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
iframe.value.contentDocument.body.setAttribute('class', document.body.getAttribute('class') ?? '');
|
||||||
|
iframe.value.contentDocument.body.setAttribute('style', document.body.getAttribute('style') ?? '');
|
||||||
|
|
||||||
|
iframe.value.contentDocument.body.appendChild(editor.value.$el);
|
||||||
|
|
||||||
|
editor.value.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="absolute overflow-visible group">
|
<div class="absolute overflow-visible group" :class="{ 'z-[1]': focusing }">
|
||||||
<input v-autofocus v-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" />
|
<input v-autofocus v-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" />
|
||||||
<div v-else-if="edge.label" :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" @click.left="select" @dblclick.left="edit">{{ edge.label }}</div>
|
<div v-else-if="edge.label" :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" @click.left="select" @dblclick.left="edit">{{ edge.label }}</div>
|
||||||
<svg ref="dom" class="absolute top-0 overflow-visible h-px w-px">
|
<svg ref="dom" class="absolute top-0 overflow-visible h-px w-px">
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else style="outline-style: solid;" :class="[style.border, style.outline, { '!outline-4': focusing }]" class="outline-0 transition-[outline-width] border-2 bg-light-20 dark:bg-dark-20 overflow-hidden contain-strict w-full h-full flex py-2" >
|
<div v-else style="outline-style: solid;" :class="[style.border, style.outline, { '!outline-4': focusing }]" class="outline-0 transition-[outline-width] border-2 bg-light-20 dark:bg-dark-20 overflow-hidden contain-strict w-full h-full flex py-2" >
|
||||||
<Editor v-model="node.text" autofocus :gutters="false"/>
|
<FramedEditor v-model="node.text" autofocus :gutters="false"/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!editing && node.type === 'group' && node.label !== undefined" @click.left="(e) => selectNode(e)" @dblclick.left="(e) => editNode(e)" :class="style.border" style="max-width: 100%; font-size: calc(18px * var(--zoom-multiplier))" class="origin-bottom-left tracking-wider border-4 truncate inline-block text-light-100 dark:text-dark-100 absolute bottom-[100%] mb-2 px-2 py-1 font-thin">{{ node.label }}</div>
|
<div v-if="!editing && node.type === 'group' && node.label !== undefined" @click.left="(e) => selectNode(e)" @dblclick.left="(e) => editNode(e)" :class="style.border" style="max-width: 100%; font-size: calc(18px * var(--zoom-multiplier))" class="origin-bottom-left tracking-wider border-4 truncate inline-block text-light-100 dark:text-dark-100 absolute bottom-[100%] mb-2 px-2 py-1 font-thin">{{ node.label }}</div>
|
||||||
<input v-else-if="editing && node.type === 'group'" v-model="node.label" @click="e => e.stopImmediatePropagation()" v-autofocus :class="[style.border, style.outline]" style="max-width: 100%; font-size: calc(18px * var(--zoom-multiplier))" class="origin-bottom-left tracking-wider border-4 truncate inline-block text-light-100 dark:text-dark-100 absolute bottom-[100%] appearance-none bg-transparent outline-4 mb-2 px-2 py-1 font-thin min-w-4" />
|
<input v-else-if="editing && node.type === 'group'" v-model="node.label" @click="e => e.stopImmediatePropagation()" v-autofocus :class="[style.border, style.outline]" style="max-width: 100%; font-size: calc(18px * var(--zoom-multiplier))" class="origin-bottom-left tracking-wider border-4 truncate inline-block text-light-100 dark:text-dark-100 absolute bottom-[100%] appearance-none bg-transparent outline-4 mb-2 px-2 py-1 font-thin min-w-4" />
|
||||||
|
|
|
||||||
BIN
db.sqlite-shm
BIN
db.sqlite-shm
Binary file not shown.
BIN
db.sqlite-wal
BIN
db.sqlite-wal
Binary file not shown.
|
|
@ -149,7 +149,8 @@ export default defineNuxtConfig({
|
||||||
rateLimiter: false,
|
rateLimiter: false,
|
||||||
headers: {
|
headers: {
|
||||||
contentSecurityPolicy: {
|
contentSecurityPolicy: {
|
||||||
"img-src": "'self' data: blob:"
|
"img-src": "'self' data: blob:",
|
||||||
|
"base-uri": "localhost:*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
xssValidator: false,
|
xssValidator: false,
|
||||||
|
|
|
||||||
|
|
@ -176,7 +176,7 @@
|
||||||
</div>
|
</div>
|
||||||
<span v-else-if="contentError">{{ contentError }}</span>
|
<span v-else-if="contentError">{{ contentError }}</span>
|
||||||
<template v-else-if="preferences.markdown.editing === 'editing'">
|
<template v-else-if="preferences.markdown.editing === 'editing'">
|
||||||
<Editor v-model="selected.content" placeholder="Commencer votre aventure ..." class="flex-1 bg-transparent appearance-none outline-none max-h-full resize-none !overflow-y-auto lg:mx-16 xl:mx-32 2xl:mx-64" />
|
<Editor v-model="selected.content" autofocus class="flex-1 bg-transparent appearance-none outline-none max-h-full resize-none !overflow-y-auto lg:mx-16 xl:mx-32 2xl:mx-64" />
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="preferences.markdown.editing === 'reading'">
|
<template v-else-if="preferences.markdown.editing === 'reading'">
|
||||||
<div class="flex-1 max-h-full !overflow-y-auto px-4 xl:px-32 2xl:px-64"><MarkdownRenderer :content="(debounced as string)" :proses="{ 'a': FakeA }" /></div>
|
<div class="flex-1 max-h-full !overflow-y-auto px-4 xl:px-32 2xl:px-64"><MarkdownRenderer :content="(debounced as string)" :proses="{ 'a': FakeA }" /></div>
|
||||||
|
|
@ -184,7 +184,7 @@
|
||||||
<template v-else-if="preferences.markdown.editing === 'split'">
|
<template v-else-if="preferences.markdown.editing === 'split'">
|
||||||
<SplitterGroup direction="horizontal" class="flex-1 w-full flex">
|
<SplitterGroup direction="horizontal" class="flex-1 w-full flex">
|
||||||
<SplitterPanel asChild collapsible :collapsedSize="0" :minSize="20" v-slot="{ isCollapsed }" :defaultSize="50">
|
<SplitterPanel asChild collapsible :collapsedSize="0" :minSize="20" v-slot="{ isCollapsed }" :defaultSize="50">
|
||||||
<Editor v-model="selected.content" placeholder="Commencer votre aventure ..." class="flex-1 bg-transparent appearance-none outline-none max-h-full resize-none !overflow-y-auto" :class="{ 'hidden': isCollapsed }" />
|
<Editor v-model="selected.content" autofocus class="flex-1 bg-transparent appearance-none outline-none max-h-full resize-none !overflow-y-auto" :class="{ 'hidden': isCollapsed }" />
|
||||||
</SplitterPanel>
|
</SplitterPanel>
|
||||||
<SplitterResizeHandle class="bg-light-35 dark:bg-dark-35 w-px xl!mx-4 mx-2" />
|
<SplitterResizeHandle class="bg-light-35 dark:bg-dark-35 w-px xl!mx-4 mx-2" />
|
||||||
<SplitterPanel asChild collapsible :collapsedSize="0" :minSize="20" v-slot="{ isCollapsed }">
|
<SplitterPanel asChild collapsible :collapsedSize="0" :minSize="20" v-slot="{ isCollapsed }">
|
||||||
|
|
|
||||||
|
|
@ -202,7 +202,7 @@ class SnapPointCache {
|
||||||
export class SnapFinder {
|
export class SnapFinder {
|
||||||
private spatialGrid: SpatialGrid;
|
private spatialGrid: SpatialGrid;
|
||||||
private snapPointCache: SnapPointCache;
|
private snapPointCache: SnapPointCache;
|
||||||
private config: SnapConfig;
|
config: SnapConfig;
|
||||||
|
|
||||||
hints: Ref<SnapHint[]>;
|
hints: Ref<SnapHint[]>;
|
||||||
viewport: Ref<Box>;
|
viewport: Ref<Box>;
|
||||||
|
|
@ -216,24 +216,28 @@ export class SnapFinder {
|
||||||
this.viewport = viewport;
|
this.viewport = viewport;
|
||||||
}
|
}
|
||||||
|
|
||||||
add(node: CanvasNode): void {
|
add(node: CanvasNode): void
|
||||||
|
{
|
||||||
this.spatialGrid.insert(node);
|
this.spatialGrid.insert(node);
|
||||||
this.snapPointCache.insert(node);
|
this.snapPointCache.insert(node);
|
||||||
this.hints.value.length = 0;
|
this.hints.value.length = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
remove(node: CanvasNode): void {
|
remove(node: CanvasNode): void
|
||||||
|
{
|
||||||
this.spatialGrid.remove(node);
|
this.spatialGrid.remove(node);
|
||||||
this.snapPointCache.invalidate(node);
|
this.snapPointCache.invalidate(node);
|
||||||
this.hints.value.length = 0;
|
this.hints.value.length = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
update(node: CanvasNode): void {
|
update(node: CanvasNode): void
|
||||||
|
{
|
||||||
this.remove(node);
|
this.remove(node);
|
||||||
this.add(node);
|
this.add(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
findEdgeSnapPosition(node: string, x: number, y: number): { x: number, y: number, node: string, direction: Direction } | undefined {
|
findEdgeSnapPosition(node: string, x: number, y: number): { x: number, y: number, node: string, direction: Direction } | undefined
|
||||||
|
{
|
||||||
const near = [...this.spatialGrid.fetch(x, y)?.values().filter(e => e !== node).flatMap(e => this.snapPointCache.getSnapPoints(e)?.map(_e => ({ ..._e, node: e })) ?? []) ?? []].filter(e => e.type === TYPE.EDGE);
|
const near = [...this.spatialGrid.fetch(x, y)?.values().filter(e => e !== node).flatMap(e => this.snapPointCache.getSnapPoints(e)?.map(_e => ({ ..._e, node: e })) ?? []) ?? []].filter(e => e.type === TYPE.EDGE);
|
||||||
let nearestDistance = this.config.threshold, nearest = undefined;
|
let nearestDistance = this.config.threshold, nearest = undefined;
|
||||||
|
|
||||||
|
|
@ -248,7 +252,8 @@ export class SnapFinder {
|
||||||
return nearest;
|
return nearest;
|
||||||
}
|
}
|
||||||
|
|
||||||
findNodeSnapPosition(node: CanvasNode, resizeHandle?: Box): Partial<Box> {
|
findNodeSnapPosition(node: CanvasNode, resizeHandle?: Box): Partial<Box>
|
||||||
|
{
|
||||||
const result: Partial<Box> = {
|
const result: Partial<Box> = {
|
||||||
x: undefined,
|
x: undefined,
|
||||||
y: undefined,
|
y: undefined,
|
||||||
|
|
@ -256,6 +261,16 @@ export class SnapFinder {
|
||||||
h: undefined,
|
h: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if(!this.config.preferences.neighborSnap)
|
||||||
|
{
|
||||||
|
result.x = this.snapToGrid(node.x);
|
||||||
|
result.w = this.snapToGrid(node.width);
|
||||||
|
result.y = this.snapToGrid(node.y);
|
||||||
|
result.h = this.snapToGrid(node.height);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
this.hints.value.length = 0;
|
this.hints.value.length = 0;
|
||||||
|
|
||||||
this.snapPointCache.invalidate(node);
|
this.snapPointCache.invalidate(node);
|
||||||
|
|
@ -267,7 +282,8 @@ export class SnapFinder {
|
||||||
return this.applySnap(node, bestSnap.x, bestSnap.y, resizeHandle);
|
return this.applySnap(node, bestSnap.x, bestSnap.y, resizeHandle);
|
||||||
}
|
}
|
||||||
|
|
||||||
private findBestSnap(activePoints: SnapPoint[], otherPoints: SnapPoint[], threshold: number, resizeHandle?: Box): Partial<Position> {
|
private findBestSnap(activePoints: SnapPoint[], otherPoints: SnapPoint[], threshold: number, resizeHandle?: Box): Partial<Position>
|
||||||
|
{
|
||||||
let bestSnap: Partial<Position> = {};
|
let bestSnap: Partial<Position> = {};
|
||||||
let bestDiffX = threshold, bestDiffY = threshold;
|
let bestDiffX = threshold, bestDiffY = threshold;
|
||||||
let xHints: SnapHint[] = [], yHints: SnapHint[] = [];
|
let xHints: SnapHint[] = [], yHints: SnapHint[] = [];
|
||||||
|
|
@ -312,20 +328,27 @@ export class SnapFinder {
|
||||||
return bestSnap;
|
return bestSnap;
|
||||||
}
|
}
|
||||||
|
|
||||||
private applySnap(node: CanvasNode, offsetx?: number, offsety?: number, resizeHandle?: Box): Partial<Box> {
|
private snapToGrid(pos?: number): number | undefined
|
||||||
|
{
|
||||||
|
return pos && this.config.preferences.gridSnap && this.config.preferences.spacing ? Math.round(pos / this.config.preferences.spacing) * this.config.preferences.spacing : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private applySnap(node: CanvasNode, offsetx?: number, offsety?: number, resizeHandle?: Box): Partial<Box>
|
||||||
|
{
|
||||||
const result: Partial<Box> = { x: undefined, y: undefined, w: undefined, h: undefined };
|
const result: Partial<Box> = { x: undefined, y: undefined, w: undefined, h: undefined };
|
||||||
|
|
||||||
if (resizeHandle) {
|
if (resizeHandle)
|
||||||
if (offsetx) result.x = node.x + offsetx * resizeHandle.x;
|
{
|
||||||
if (offsetx) result.w = node.width + offsetx * resizeHandle.w;
|
result.x = offsetx ? node.x + offsetx * resizeHandle.x : this.snapToGrid(node.x);
|
||||||
if (offsety) result.y = node.y + offsety * resizeHandle.y;
|
result.w = offsetx ? node.width + offsetx * resizeHandle.w : this.snapToGrid(node.width);
|
||||||
if (offsety) result.h = node.height - offsety * resizeHandle.h;
|
result.y = offsety ? node.y + offsety * resizeHandle.y : this.snapToGrid(node.y);
|
||||||
} else {
|
result.h = offsety ? node.height - offsety * resizeHandle.h : this.snapToGrid(node.height);
|
||||||
if (offsetx) result.x = node.x + offsetx;
|
}
|
||||||
if (offsety) result.y = node.y + offsety;
|
else
|
||||||
|
{
|
||||||
|
result.x = offsetx ? node.x + offsetx : this.snapToGrid(node.x);
|
||||||
|
result.y = offsety ? node.y + offsety : this.snapToGrid(node.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
//console.log(result, offsetx, offsety);
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,5 +11,6 @@ type MarkdownPreferences = {
|
||||||
};
|
};
|
||||||
type CanvasPreferences = {
|
type CanvasPreferences = {
|
||||||
gridSnap: boolean;
|
gridSnap: boolean;
|
||||||
|
spacing?: number;
|
||||||
neighborSnap: boolean;
|
neighborSnap: boolean;
|
||||||
};
|
};
|
||||||
Loading…
Reference in New Issue