You've already forked obsidian-visualiser
Add grid snapping, grid preview, fix zoom slowdowns and canvas markdown editing being at the wrong size.
This commit is contained in:
@@ -1,10 +1,9 @@
|
||||
<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 CanvasEdgeEditor from './canvas/CanvasEdgeEditor.vue';
|
||||
import { SnapFinder, type SnapHint } from '#shared/physics.util';
|
||||
import type { CanvasPreferences } from '~/types/general';
|
||||
import Id from '~/pages/user/[id].vue';
|
||||
export type Element = { type: 'node' | 'edge', id: string };
|
||||
|
||||
interface ActionMap {
|
||||
@@ -67,17 +66,28 @@ const props = defineProps<{
|
||||
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 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 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 viewport = computed<Box>(() => {
|
||||
const width = viewportSize.width.value / zoom.value, height = viewportSize.height.value / zoom.value;
|
||||
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 };
|
||||
});
|
||||
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 };
|
||||
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);
|
||||
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();
|
||||
}, { passive: true });
|
||||
@@ -227,7 +238,23 @@ 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());
|
||||
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()
|
||||
@@ -680,7 +707,7 @@ useShortcuts({
|
||||
</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 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="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10">
|
||||
<Tooltip message="Zoom avant" side="right">
|
||||
@@ -717,6 +744,20 @@ useShortcuts({
|
||||
</Tooltip>
|
||||
</div>
|
||||
<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">
|
||||
<Dialog title="Aide" iconClose>
|
||||
<template #trigger>
|
||||
@@ -746,6 +787,12 @@ useShortcuts({
|
||||
</Tooltip>
|
||||
</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="{
|
||||
'transform-origin': 'center center',
|
||||
}" class="h-full">
|
||||
@@ -855,7 +902,7 @@ useShortcuts({
|
||||
</template>
|
||||
</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" />
|
||||
</div>
|
||||
<div>
|
||||
@@ -871,7 +918,7 @@ useShortcuts({
|
||||
</svg>
|
||||
</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">
|
||||
<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" />
|
||||
|
||||
@@ -1,37 +1,11 @@
|
||||
<script lang="ts">
|
||||
import type { Editor, EditorConfiguration, EditorChange } from 'codemirror';
|
||||
import { changeEnd } from 'codemirror';
|
||||
import { fromTextArea } from 'hypermd';
|
||||
|
||||
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 setup lang="ts">
|
||||
|
||||
const { placeholder, autofocus = false, gutters = true, format = 'd-any' } = defineProps<{
|
||||
placeholder?: string
|
||||
autofocus?: boolean
|
||||
@@ -73,10 +47,7 @@ onMounted(() => {
|
||||
} as EditorConfiguration);
|
||||
|
||||
e.setValue(model.value ?? '');
|
||||
updateBottomMargin(e);
|
||||
e.on('change', onChange);
|
||||
e.on('change', (cm: Editor, change: EditorChange) => model.value = cm.getValue());
|
||||
e.on('refresh', updateBottomMargin);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -87,6 +58,8 @@ watchEffect(() => {
|
||||
editor.value.clearHistory();
|
||||
}
|
||||
});
|
||||
|
||||
defineExpose({ focus: () => editor.value?.focus() });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -108,6 +81,10 @@ watchEffect(() => {
|
||||
{
|
||||
@apply hidden;
|
||||
}
|
||||
.CodeMirror-sizer
|
||||
{
|
||||
@apply !px-3;
|
||||
}
|
||||
.cancel-gutters .CodeMirror-sizer
|
||||
{
|
||||
@apply ms-2;
|
||||
|
||||
40
components/FramedEditor.vue
Normal file
40
components/FramedEditor.vue
Normal file
@@ -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>
|
||||
<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" />
|
||||
<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">
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
</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" >
|
||||
<Editor v-model="node.text" autofocus :gutters="false"/>
|
||||
<FramedEditor v-model="node.text" autofocus :gutters="false"/>
|
||||
</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" />
|
||||
|
||||
Reference in New Issue
Block a user