Add grid snapping, @TODO: Add settings popup with grid settings + render grid.
This commit is contained in:
parent
3f04bb3d0c
commit
0b1809c3f6
|
|
@ -63,12 +63,23 @@ 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 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);
|
||||
|
||||
|
|
@ -618,7 +629,7 @@ useShortcuts({
|
|||
</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)" />
|
||||
<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)" />
|
||||
|
|
|
|||
|
|
@ -34,14 +34,16 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Direction } from '#shared/canvas.util';
|
||||
import { gridSnap, type Direction } from '#shared/canvas.util';
|
||||
import type { Element } from '../CanvasEditor.vue';
|
||||
import FakeA from '../prose/FakeA.vue';
|
||||
import type { CanvasNode } from '~/types/canvas';
|
||||
|
||||
const { node, zoom } = defineProps<{
|
||||
const { node, zoom, snapping, grid } = defineProps<{
|
||||
node: CanvasNode
|
||||
zoom: number
|
||||
snapping: boolean
|
||||
grid: number
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
|
@ -80,14 +82,20 @@ function resizeNode(e: MouseEvent, x: number, y: number, w: number, h: number) {
|
|||
e.stopImmediatePropagation();
|
||||
|
||||
const startx = node.x, starty = node.y, startw = node.width, starth = node.height;
|
||||
let realx = node.x, realy = node.y, realw = node.width, realh = node.height;
|
||||
const resizemove = (e: MouseEvent) => {
|
||||
if(e.button !== 0)
|
||||
return;
|
||||
|
||||
node.x += (e.movementX / zoom) * x;
|
||||
node.y += (e.movementY / zoom) * y;
|
||||
node.width += (e.movementX / zoom) * w;
|
||||
node.height += (e.movementY / zoom) * h;
|
||||
realx += (e.movementX / zoom) * x;
|
||||
realy += (e.movementY / zoom) * y;
|
||||
realw += (e.movementX / zoom) * w;
|
||||
realh += (e.movementY / zoom) * h;
|
||||
|
||||
node.x = snapping ? gridSnap(realx, grid) : realx;
|
||||
node.y = snapping ? gridSnap(realy, grid) : realy;
|
||||
node.width = snapping ? gridSnap(realw, grid) : realw;
|
||||
node.height = snapping ? gridSnap(realh, grid) : realh;
|
||||
};
|
||||
const resizeend = (e: MouseEvent) => {
|
||||
if(e.button !== 0)
|
||||
|
|
@ -126,12 +134,16 @@ function unselect() {
|
|||
}
|
||||
|
||||
let lastx = 0, lasty = 0;
|
||||
let realx = 0, realy = 0;
|
||||
const dragmove = (e: MouseEvent) => {
|
||||
if(e.button !== 0)
|
||||
return;
|
||||
|
||||
node.x += e.movementX / zoom;
|
||||
node.y += e.movementY / zoom;
|
||||
realx += e.movementX / zoom;
|
||||
realy += e.movementY / zoom;
|
||||
|
||||
node.x = snapping ? gridSnap(realx, grid) : realx;
|
||||
node.y = snapping ? gridSnap(realy, grid) : realy;
|
||||
};
|
||||
const dragend = (e: MouseEvent) => {
|
||||
if(e.button !== 0)
|
||||
|
|
@ -148,6 +160,7 @@ const dragstart = (e: MouseEvent) => {
|
|||
return;
|
||||
|
||||
lastx = node.x, lasty = node.y;
|
||||
realx = node.x, realy = node.y;
|
||||
|
||||
window.addEventListener('mousemove', dragmove, { passive: true });
|
||||
window.addEventListener('mouseup', dragend, { passive: true });
|
||||
|
|
|
|||
27
package.json
27
package.json
|
|
@ -11,44 +11,45 @@
|
|||
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3",
|
||||
"@iconify/vue": "^4.3.0",
|
||||
"@nuxtjs/color-mode": "^3.5.2",
|
||||
"@nuxtjs/sitemap": "^7.0.1",
|
||||
"@nuxtjs/tailwindcss": "^6.12.2",
|
||||
"@nuxtjs/sitemap": "^7.2.3",
|
||||
"@nuxtjs/tailwindcss": "^6.13.1",
|
||||
"@vueuse/gesture": "^2.0.0",
|
||||
"@vueuse/math": "^11.3.0",
|
||||
"@vueuse/nuxt": "^11.3.0",
|
||||
"@vueuse/math": "^12.5.0",
|
||||
"@vueuse/nuxt": "^12.5.0",
|
||||
"codemirror": "^6.0.1",
|
||||
"drizzle-orm": "^0.35.3",
|
||||
"drizzle-orm": "^0.38.4",
|
||||
"hast": "^1.0.0",
|
||||
"hast-util-heading": "^3.0.0",
|
||||
"hast-util-heading-rank": "^3.0.0",
|
||||
"lodash.capitalize": "^4.2.1",
|
||||
"mdast-util-find-and-replace": "^3.0.2",
|
||||
"nodemailer": "^6.9.16",
|
||||
"nuxt": "^3.15.0",
|
||||
"nodemailer": "^6.10.0",
|
||||
"nuxt": "3.15.1",
|
||||
"nuxt-security": "^2.1.5",
|
||||
"radix-vue": "^1.9.12",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"remark-breaks": "^4.0.0",
|
||||
"remark-frontmatter": "^5.0.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"remark-ofm": "link:remark-ofm",
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-rehype": "^11.1.1",
|
||||
"rollup-plugin-postcss": "^4.0.2",
|
||||
"rollup-plugin-vue": "^6.0.0",
|
||||
"unified": "^11.0.5",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"vue": "latest",
|
||||
"vue-router": "latest",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.1.14",
|
||||
"@types/bun": "^1.2.0",
|
||||
"@types/lodash.capitalize": "^4.2.9",
|
||||
"@types/nodemailer": "^6.4.17",
|
||||
"@types/unist": "^3.0.3",
|
||||
"better-sqlite3": "^11.7.0",
|
||||
"bun-types": "^1.1.42",
|
||||
"drizzle-kit": "^0.26.2",
|
||||
"better-sqlite3": "^11.8.1",
|
||||
"bun-types": "^1.2.0",
|
||||
"drizzle-kit": "^0.30.2",
|
||||
"mdast-util-to-string": "^4.0.0",
|
||||
"rehype-stringify": "^10.0.1"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,8 +89,8 @@
|
|||
</div>
|
||||
<DraggableTree class="ps-4 pe-2 xl:text-base text-sm"
|
||||
:items="navigation ?? undefined" :get-key="(item: Partial<TreeItemEditable>) => item.path !== undefined ? getPath(item as TreeItemEditable) : ''" @updateTree="drop"
|
||||
v-model="selected" :defaultExpanded="defaultExpanded" >
|
||||
<template #default="{ handleToggle, handleSelect, isExpanded, isSelected, isDragging, item }">
|
||||
v-model="selected" :defaultExpanded="defaultExpanded" :get-children="(item: Partial<TreeItemEditable>) => item.type === 'folder' ? item.children : undefined" >
|
||||
<template #default="{ handleToggle, handleSelect, isExpanded, isDragging, item }">
|
||||
<div class="flex flex-1 items-center px-2 max-w-full pe-4" :class="{ 'opacity-50': isDragging }" :style="{ 'padding-left': `${item.level - 0.5}em` }">
|
||||
<span class="py-2 px-2" @click="handleToggle" v-if="item.hasChildren" >
|
||||
<Icon :icon="isExpanded ? 'lucide:folder-open' : 'lucide:folder'"/>
|
||||
|
|
@ -148,7 +148,7 @@
|
|||
<div class="flex flex-row justify-between items-center gap-x-4">
|
||||
<div v-if="selected.customPath" class="flex lg:items-center truncate">
|
||||
<pre class="md:text-base text-sm truncate" style="direction: rtl">/{{ selected.parent !== '' ? selected.parent + '/' : '' }}</pre>
|
||||
<TextInput v-model="selected.name" @input="(e) => {
|
||||
<TextInput v-model="selected.name" @input="(e: Event) => {
|
||||
if(selected && selected.customPath)
|
||||
{
|
||||
selected.name = parsePath(selected.name);
|
||||
|
|
@ -192,7 +192,7 @@
|
|||
<span class="flex flex-1 justify-center items-center"><ProseH3>Editeur de carte en cours de développement</ProseH3></span>
|
||||
</template>
|
||||
<template v-else-if="selected.type === 'file'">
|
||||
<span>Modifier le contenu :</span><input type="file" @change="(e) => console.log((e.target as HTMLInputElement).files?.length)" />
|
||||
<span>Modifier le contenu :</span><input type="file" @change="(e: Event) => console.log((e.target as HTMLInputElement).files?.length)" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -206,7 +206,7 @@
|
|||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
import type { Instruction } from '@atlaskit/pragmatic-drag-and-drop-hitbox/dist/types/tree-item';
|
||||
import { convertContentFromText, convertContentToText, parsePath } from '#shared/general.utils';
|
||||
import { convertContentFromText, convertContentToText, DEFAULT_CONTENT, parsePath } from '#shared/general.utils';
|
||||
import type { CanvasContent, ExploreContent, FileType, TreeItem } from '~/types/content';
|
||||
import { iconByType } from '#shared/general.utils';
|
||||
import FakeA from '~/components/prose/FakeA.vue';
|
||||
|
|
@ -418,7 +418,7 @@ function add(type: FileType): void
|
|||
|
||||
const news = [...tree.search(navigation.value, 'title', 'Nouveau')].filter((e, i, a) => a.indexOf(e) === i);
|
||||
const title = `Nouveau${news.length > 0 ? ' (' + news.length +')' : ''}`;
|
||||
const item: TreeItemEditable = { navigable: true, private: false, parent: '', path: '', title: title, name: parsePath(title), type: type, order: 0, children: type === 'folder' ? [] : undefined, customPath: false, content: undefined, owner: -1, timestamp: new Date(), visit: 0 };
|
||||
const item: TreeItemEditable = { navigable: true, private: false, parent: '', path: '', title: title, name: parsePath(title), type: type, order: 0, children: [], customPath: false, content: DEFAULT_CONTENT[type], owner: -1, timestamp: new Date(), visit: 0 };
|
||||
|
||||
if(!selected.value)
|
||||
{
|
||||
|
|
@ -518,6 +518,7 @@ function rebuildPath(tree: TreeItemEditable[] | null | undefined, parentPath: st
|
|||
}
|
||||
async function save(redirect: boolean): Promise<void>
|
||||
{
|
||||
//@ts-ignore
|
||||
const map = (e: TreeItemEditable[]): TreeItemEditable[] => e.map(f => ({ ...f, content: f.content ? convertContentToText(f.type, f.content) : undefined, children: f.children ? map(f.children) : undefined }));
|
||||
saveStatus.value = 'pending';
|
||||
try {
|
||||
|
|
@ -532,6 +533,7 @@ async function save(redirect: boolean): Promise<void>
|
|||
toaster.clear('error');
|
||||
toaster.add({ type: 'success', content: 'Contenu enregistré', timer: true, duration: 10000 });
|
||||
|
||||
//@ts-ignore
|
||||
complete.value = result as ExploreContent[];
|
||||
if(redirect) router.go(-1);
|
||||
} catch(e: any) {
|
||||
|
|
|
|||
|
|
@ -83,3 +83,8 @@ export function getCenter(n: Position, i: Position, r: Position, o: Position, e:
|
|||
y: s * n.y + l * r.y + c * o.y + u * i.y
|
||||
};
|
||||
}
|
||||
|
||||
export function gridSnap(value: number, grid: number): number
|
||||
{
|
||||
return Math.round(value / grid) * grid;
|
||||
}
|
||||
|
|
@ -1,6 +1,13 @@
|
|||
import type { CanvasContent } from '~/types/canvas';
|
||||
import type { ContentMap, FileType } from '~/types/content';
|
||||
|
||||
export const DEFAULT_CONTENT: Record<FileType, ContentMap[FileType]['content']> = {
|
||||
map: {},
|
||||
canvas: { nodes: [], edges: []},
|
||||
markdown: '',
|
||||
file: '',
|
||||
folder: null,
|
||||
}
|
||||
export function unifySlug(slug: string | string[]): string
|
||||
{
|
||||
return (Array.isArray(slug) ? slug.join('/') : slug);
|
||||
|
|
|
|||
Loading…
Reference in New Issue