Progressing on canvas editor class
This commit is contained in:
parent
2c80cb2456
commit
9ca546f490
|
|
@ -69,7 +69,7 @@ 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 };
|
||||
return { x: -dispX.value + movementX / 2, y: -dispY.value + movementY / 2, width, height };
|
||||
});
|
||||
const updateScaleVar = useDebounceFn(() => {
|
||||
if(transformRef.value)
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ function editNode(e: Event) {
|
|||
dom.value?.removeEventListener('mousedown', dragstart);
|
||||
emit('edit', { type: 'node', id: node.id });
|
||||
}
|
||||
function resizeNode(e: MouseEvent, x: number, y: number, w: number, h: number) {
|
||||
function resizeNode(e: MouseEvent, x: number, y: number, width: number, height: number) {
|
||||
e.stopImmediatePropagation();
|
||||
|
||||
const startx = node.x, starty = node.y, startw = node.width, starth = node.height;
|
||||
|
|
@ -96,15 +96,15 @@ function resizeNode(e: MouseEvent, x: number, y: number, w: number, h: number) {
|
|||
|
||||
realx = realx + (e.movementX / zoom) * x;
|
||||
realy = realy + (e.movementY / zoom) * y;
|
||||
realw = Math.max(realw + (e.movementX / zoom) * w, 64);
|
||||
realh = Math.max(realh + (e.movementY / zoom) * h, 64);
|
||||
realw = Math.max(realw + (e.movementX / zoom) * width, 64);
|
||||
realh = Math.max(realh + (e.movementY / zoom) * height, 64);
|
||||
|
||||
const result = e.altKey ? undefined : snap({ ...node, x: realx, y: realy, width: realw, height: realh }, { x, y, w, h });
|
||||
const result = e.altKey ? undefined : snap({ ...node, x: realx, y: realy, width: realw, height: realh }, { x, y, width, height });
|
||||
|
||||
node.x = result?.x ?? realx;
|
||||
node.y = result?.y ?? realy;
|
||||
node.width = result?.w ?? realw;
|
||||
node.height = result?.h ?? realh;
|
||||
node.width = result?.width ?? realw;
|
||||
node.height = result?.height ?? realh;
|
||||
};
|
||||
const resizeend = (e: MouseEvent) => {
|
||||
if(e.button !== 0)
|
||||
|
|
|
|||
BIN
db.sqlite-shm
BIN
db.sqlite-shm
Binary file not shown.
BIN
db.sqlite-wal
BIN
db.sqlite-wal
Binary file not shown.
|
|
@ -35,7 +35,6 @@ export default defineEventHandler(async (e) => {
|
|||
return data.content;
|
||||
}
|
||||
|
||||
setResponseStatus(e, 404);
|
||||
return;
|
||||
}
|
||||
catch(_e)
|
||||
|
|
|
|||
|
|
@ -1,12 +1,6 @@
|
|||
/*import useDatabase from "~/composables/useDatabase";
|
||||
import type { FileType } from '~/types/content';
|
||||
import { explorerContentTable } from "~/db/schema";
|
||||
import { eq, ne } from "drizzle-orm";
|
||||
|
||||
const typeMapping: Record<string, FileType> = {
|
||||
".md": "markdown",
|
||||
".canvas": "canvas"
|
||||
};
|
||||
import useDatabase from "~/composables/useDatabase";
|
||||
import { projectFilesTable, projectContentTable } from "~/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
export default defineTask({
|
||||
meta: {
|
||||
|
|
@ -16,7 +10,7 @@ export default defineTask({
|
|||
async run(event) {
|
||||
try {
|
||||
const db = useDatabase();
|
||||
const files = db.select().from(explorerContentTable).where(ne(explorerContentTable.type, 'folder')).all();
|
||||
const files = db.select().from(projectFilesTable).leftJoin(projectContentTable, eq(projectContentTable.id, projectFilesTable.id)).all();
|
||||
|
||||
|
||||
|
||||
|
|
@ -27,4 +21,4 @@ export default defineTask({
|
|||
return { result: false, error: e };
|
||||
}
|
||||
},
|
||||
})*/
|
||||
})
|
||||
|
|
@ -4,12 +4,14 @@ import { dom, icon, svg, text } from "./dom.util";
|
|||
import render from "./markdown.util";
|
||||
import { popper } from "#shared/floating.util";
|
||||
import { Content } from "./content.util";
|
||||
import type { History } from "./history.util";
|
||||
import { fakeA } from "./proses";
|
||||
import { History } from "./history.util";
|
||||
import { fakeA, link } from "./proses";
|
||||
import { SnapFinder, SpatialGrid } from "./physics.util";
|
||||
import type { CanvasPreferences } from "~/types/general";
|
||||
|
||||
export type Direction = 'bottom' | 'top' | 'left' | 'right';
|
||||
export type Position = { x: number, y: number };
|
||||
export type Box = Position & { w: number, h: number };
|
||||
export type Box = Position & { width: number, height: number };
|
||||
export type Path = {
|
||||
path: string;
|
||||
from: Position;
|
||||
|
|
@ -173,8 +175,11 @@ export class Node extends EventTarget
|
|||
}
|
||||
export class NodeEditable extends Node
|
||||
{
|
||||
|
||||
private static input: HTMLInputElement = dom('input', { 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', style: { 'max-width': '100%', 'font-size': 'calc(18px * var(--zoom-multiplier))' }, listeners: { click: e => e.stopImmediatePropagation() } });
|
||||
|
||||
edges: Set<EdgeEditable> = new Set();
|
||||
|
||||
private dirty: boolean = false;
|
||||
constructor(properties: CanvasNode)
|
||||
{
|
||||
super(properties);
|
||||
|
|
@ -183,26 +188,9 @@ export class NodeEditable extends Node
|
|||
{
|
||||
const style = this.style;
|
||||
|
||||
/*
|
||||
<div class="absolute" ref="dom" :style="{transform: `translate(${node.x}px, ${node.y}px)`, width: `${node.width}px`, height: `${node.height}px`, '--canvas-color': node.color?.hex}" :class="{'-z-10': node.type === 'group', 'z-10': node.type !== 'group'}">
|
||||
<div v-if="!editing || node.type === 'group'" style="outline-style: solid;" :class="[style.border, style.outline, { '!outline-4 cursor-move': focusing }]" class="outline-0 transition-[outline-width] border-2 bg-light-20 dark:bg-dark-20 w-full h-full hover:outline-4">
|
||||
<div class="w-full h-full py-2 px-4 flex !bg-opacity-[0.07] overflow-auto" :class="style.bg" @click.left="(e) => { if(node.type !== 'group') selectNode(e) }" @dblclick.left="(e) => { if(node.type !== 'group') editNode(e) }">
|
||||
<div v-if="node.text && node.text.length > 0" class="flex items-center">
|
||||
<MarkdownRenderer :content="node.text" :proses="{ a: FakeA }" />
|
||||
</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" >
|
||||
<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" />
|
||||
</div>
|
||||
*/
|
||||
|
||||
this.nodeDom = dom('div', { class: ['absolute', {'-z-10': this.properties.type === 'group', 'z-10': this.properties.type !== 'group'}], style: { transform: `translate(${this.properties.x}px, ${this.properties.y}px)`, width: `${this.properties.width}px`, height: `${this.properties.height}px`, '--canvas-color': this.properties.color?.hex } }, [
|
||||
dom('div', { class: ['outline-0 transition-[outline-width] border-2 bg-light-20 dark:bg-dark-20 w-full h-full hover:outline-4', style.border, style.outline] }, [
|
||||
dom('div', { class: ['w-full h-full py-2 px-4 flex !bg-opacity-[0.07] overflow-auto', style.bg], listeners: this.properties.type === 'group' ? { mouseenter: e => this.dispatchEvent(new CustomEvent('focus', { detail: this })), mouseleave: e => this.dispatchEvent(new CustomEvent('unfocus', { detail: this })), click: e => this.dispatchEvent(new CustomEvent('select', { detail: this })), dblclick: e => this.dispatchEvent(new CustomEvent('edit', { detail: this })) } : undefined }, [this.properties.text ? dom('div', { class: 'flex items-center' }, [render(this.properties.text, undefined, { tags: { a: fakeA } })]) : undefined])
|
||||
this.nodeDom = dom('div', { class: ['absolute group', {'-z-10': this.properties.type === 'group', 'z-10': this.properties.type !== 'group'}], style: { transform: `translate(${this.properties.x}px, ${this.properties.y}px)`, width: `${this.properties.width}px`, height: `${this.properties.height}px`, '--canvas-color': this.properties.color?.hex } }, [
|
||||
dom('div', { class: ['outline-0 transition-[outline-width] border-2 bg-light-20 dark:bg-dark-20 w-full h-full group-hover:outline-4', style.border, style.outline] }, [
|
||||
dom('div', { class: ['w-full h-full py-2 px-4 flex !bg-opacity-[0.07] overflow-auto', style.bg], listeners: this.properties.type === 'text' ? { mouseenter: e => this.dispatchEvent(new CustomEvent('focus', { detail: this })), click: e => this.dispatchEvent(new CustomEvent('select', { detail: this })), dblclick: e => this.dispatchEvent(new CustomEvent('edit', { detail: this })) } : undefined }, [this.properties.text ? dom('div', { class: 'flex items-center' }, [render(this.properties.text, undefined, { tags: { a: fakeA } })]) : undefined])
|
||||
])
|
||||
]);
|
||||
|
||||
|
|
@ -214,6 +202,19 @@ export class NodeEditable extends Node
|
|||
}
|
||||
}
|
||||
}
|
||||
update()
|
||||
{
|
||||
if(!this.dirty)
|
||||
return;
|
||||
|
||||
Object.assign(this.nodeDom.style, {
|
||||
transform: `translate(${this.properties.x}px, ${this.properties.y}px)`,
|
||||
width: `${this.properties.width}px`,
|
||||
height: `${this.properties.height}px`,
|
||||
});
|
||||
this.edges.forEach(e => e.update());
|
||||
this.dirty = false;
|
||||
}
|
||||
|
||||
override get style()
|
||||
{
|
||||
|
|
@ -222,6 +223,43 @@ export class NodeEditable extends Node
|
|||
{ bg: `bg-colored`, border: `border-[color:var(--canvas-color)]`, outline: `outline-[color:var(--canvas-color)]` } :
|
||||
{ border: `border-light-40 dark:border-dark-40`, bg: `bg-light-40 dark:bg-dark-40`, outline: `outline-light-40 dark:outline-dark-40` }
|
||||
}
|
||||
|
||||
get x()
|
||||
{
|
||||
return this.properties.x;
|
||||
}
|
||||
set x(value: number)
|
||||
{
|
||||
this.properties.x = value;
|
||||
this.dirty = true;
|
||||
}
|
||||
get y()
|
||||
{
|
||||
return this.properties.y;
|
||||
}
|
||||
set y(value: number)
|
||||
{
|
||||
this.properties.y = value;
|
||||
this.dirty = true;
|
||||
}
|
||||
get width()
|
||||
{
|
||||
return this.properties.width;
|
||||
}
|
||||
set width(value: number)
|
||||
{
|
||||
this.properties.width = value;
|
||||
this.dirty = true;
|
||||
}
|
||||
get height()
|
||||
{
|
||||
return this.properties.height;
|
||||
}
|
||||
set height(value: number)
|
||||
{
|
||||
this.properties.height = value;
|
||||
this.dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
export class Edge extends EventTarget
|
||||
|
|
@ -229,20 +267,20 @@ export class Edge extends EventTarget
|
|||
properties: CanvasEdge;
|
||||
|
||||
edgeDom!: HTMLDivElement;
|
||||
protected from: CanvasNode;
|
||||
protected to: CanvasNode;
|
||||
protected from: Node;
|
||||
protected to: Node;
|
||||
protected path: Path;
|
||||
protected labelPos: string;
|
||||
|
||||
constructor(properties: CanvasEdge, nodes: CanvasNode[])
|
||||
constructor(properties: CanvasEdge, from: Node, to: Node)
|
||||
{
|
||||
super();
|
||||
this.properties = properties;
|
||||
|
||||
this.from = nodes.find(f => f.id === properties.fromNode)!;
|
||||
this.to = nodes.find(f => f.id === properties.toNode)!;
|
||||
this.path = getPath(this.from, properties.fromSide, this.to, properties.toSide)!;
|
||||
this.labelPos = labelCenter(this.from, properties.fromSide, this.to, properties.toSide);
|
||||
this.from = from;
|
||||
this.to = to;
|
||||
this.path = getPath(this.from.properties, properties.fromSide, this.to.properties, properties.toSide)!;
|
||||
this.labelPos = labelCenter(this.from.properties, properties.fromSide, this.to.properties, properties.toSide);
|
||||
|
||||
this.getDOM();
|
||||
}
|
||||
|
|
@ -279,14 +317,16 @@ export class EdgeEditable extends Edge
|
|||
|
||||
private pathDom!: SVGPathElement;
|
||||
private inputDom!: HTMLDivElement;
|
||||
constructor(properties: CanvasEdge, nodes: CanvasNode[])
|
||||
constructor(properties: CanvasEdge, from: NodeEditable, to: NodeEditable)
|
||||
{
|
||||
super(properties, nodes);
|
||||
super(properties, from, to);
|
||||
from.edges.add(this);
|
||||
to.edges.add(this);
|
||||
}
|
||||
protected override getDOM()
|
||||
{
|
||||
const style = this.style;
|
||||
this.pathDom = svg('path', { style: { 'stroke-width': `calc(3px * var(--zoom-multiplier))`, 'stroke-linecap': 'butt' }, class: ['transition-[stroke-width] fill-none stroke-[4px]', style.stroke], listeners: { mouseenter: e => this.dispatchEvent(new CustomEvent('focus', { detail: this })), click: e => this.dispatchEvent(new CustomEvent('select', { detail: this })), dblclick: e => this.dispatchEvent(new CustomEvent('edit', { detail: this })) } });
|
||||
this.pathDom = svg('path', { style: { 'stroke-width': `calc(3px * var(--zoom-multiplier))`, 'stroke-linecap': 'butt' }, class: ['transition-[stroke-width] fill-none stroke-[4px]', style.stroke], attributes: { d: this.path.path }, listeners: { mouseenter: e => this.dispatchEvent(new CustomEvent('focus', { detail: this })), click: e => this.dispatchEvent(new CustomEvent('select', { detail: this })), dblclick: e => this.dispatchEvent(new CustomEvent('edit', { detail: this })) } });
|
||||
this.inputDom = dom('div', { class: ['relative bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 px-4 py-2 z-20 -translate-x-1/2 -translate-y-1/2', { 'hidden': this.properties.label === undefined }], style: { transform: this.labelPos }, text: this.properties.label });
|
||||
this.edgeDom = dom('div', { class: 'absolute overflow-visible group' }, [
|
||||
this.inputDom,
|
||||
|
|
@ -301,24 +341,30 @@ export class EdgeEditable extends Edge
|
|||
]),
|
||||
]);
|
||||
}
|
||||
update()
|
||||
{
|
||||
this.path = getPath(this.from.properties, this.properties.fromSide, this.to.properties, this.properties.toSide)!;
|
||||
this.pathDom.setAttribute('d', this.path.path);
|
||||
}
|
||||
}
|
||||
|
||||
export class Canvas
|
||||
{
|
||||
static minZoom: number = 0.08;
|
||||
static maxZoom: number = 3;
|
||||
|
||||
protected content: Required<CanvasContent>;
|
||||
protected zoom: number = 0.5;
|
||||
protected x: number = 0;
|
||||
protected y: number = 0;
|
||||
protected _zoom: number = 0.5;
|
||||
protected _x: number = 0;
|
||||
protected _y: number = 0;
|
||||
|
||||
protected containZoom: number = this.zoom;
|
||||
protected centerX: number = this.x;
|
||||
protected centerY: number = this.y;
|
||||
protected containZoom: number = this._zoom;
|
||||
protected centerX: number = this._x;
|
||||
protected centerY: number = this._y;
|
||||
|
||||
protected visualZoom: number = this.zoom;
|
||||
protected visualX: number = this.x;
|
||||
protected visualY: number = this.y;
|
||||
protected visualZoom: number = this._zoom;
|
||||
protected visualX: number = this._x;
|
||||
protected visualY: number = this._y;
|
||||
|
||||
protected tweener: Tweener = new Tweener();
|
||||
private debouncedTimeout: Timer = setTimeout(() => {}, 0);
|
||||
|
|
@ -326,6 +372,15 @@ export class Canvas
|
|||
protected transform!: HTMLDivElement;
|
||||
container!: HTMLDivElement;
|
||||
|
||||
protected firstX = 0;
|
||||
protected firstY = 0;
|
||||
protected lastX = 0;
|
||||
protected lastY = 0;
|
||||
protected lastDistance = 0;
|
||||
|
||||
protected nodes: Node[] = [];
|
||||
protected edges: Edge[] = [];
|
||||
|
||||
constructor(content?: CanvasContent)
|
||||
{
|
||||
if(!content)
|
||||
|
|
@ -347,38 +402,31 @@ export class Canvas
|
|||
|
||||
protected createDOM()
|
||||
{
|
||||
this.nodes = this.content.nodes.map(e => new Node(e));
|
||||
this.edges = this.content.edges.map(e => new Edge(e, this.nodes.find(f => e.fromNode === f.properties.id)!, this.nodes.find(f => e.toNode === f.properties.id)!));
|
||||
//const { loggedIn, user } = useUserSession();
|
||||
this.transform = dom('div', { class: 'origin-center h-full' }, [
|
||||
dom('div', { class: 'absolute top-0 left-0 w-full h-full pointer-events-none *:pointer-events-auto *:select-none touch-none' }, [
|
||||
dom('div', {}, [...this.content.nodes.map(e => new Node(e).nodeDom)]), dom('div', {}, [...this.content.edges.map(e => new Edge(e, this.content.nodes).edgeDom)]),
|
||||
dom('div', {}, this.nodes.map(e => e.nodeDom)), dom('div', {}, this.edges.map(e => e.edgeDom)),
|
||||
])
|
||||
]);
|
||||
//TODO: --zoom-multiplier dynamic
|
||||
|
||||
this.container = dom('div', { class: 'absolute top-0 left-0 overflow-hidden w-full h-full touch-none' }, [
|
||||
dom('div', { class: 'flex flex-col absolute sm:top-2 top-10 left-2 z-[35] overflow-hidden gap-4' }, [
|
||||
dom('div', { class: 'border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10' }, [
|
||||
popper(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: () => this.zoomTo(this.x, this.y, clamp(this.zoom * 1.1, this.containZoom, Canvas.maxZoom)) } }, [icon('radix-icons:plus')]), {
|
||||
popper(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: () => this.zoomTo(this._x, this._y, clamp(this._zoom * 1.1, this.containZoom, Canvas.maxZoom)) } }, [icon('radix-icons:plus')]), {
|
||||
delay: 120, arrow: true, placement: 'right', offset: 8, content: [text('Zoom avant')], class: 'TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70 z-50'
|
||||
}),
|
||||
popper(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: (e: MouseEvent) => { this.reset(); } } }, [icon('radix-icons:reload')]), {
|
||||
delay: 120, arrow: true, placement: 'right', offset: 8, content: [text('Reset')], class: 'TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70 z-50'
|
||||
}),
|
||||
popper(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: (e: MouseEvent) => { this.zoomTo(this.x, this.y, this.containZoom); } } }, [icon('radix-icons:corners')]), {
|
||||
popper(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: (e: MouseEvent) => { this.reset(); } } }, [icon('radix-icons:corners')]), {
|
||||
delay: 120, arrow: true, placement: 'right', offset: 8, content: [text('Tout contenir')], class: 'TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70 z-50'
|
||||
}),
|
||||
popper(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: () => this.zoomTo(this.x, this.y, clamp(this.zoom / 1.1, this.containZoom, Canvas.maxZoom)) } }, [icon('radix-icons:minus')]), {
|
||||
popper(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: () => this.zoomTo(this._x, this._y, clamp(this._zoom / 1.1, this.containZoom, Canvas.maxZoom)) } }, [icon('radix-icons:minus')]), {
|
||||
delay: 120, arrow: true, placement: 'right', offset: 8, content: [text('Zoom arrière')], class: 'TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70 z-50'
|
||||
}),
|
||||
]), //dom('a') Edition link
|
||||
]),
|
||||
//link({}, { name: 'explore-edit' }),
|
||||
]), this.transform,
|
||||
]);
|
||||
|
||||
/*
|
||||
<NuxtLink v-if="overview && isOwner || hasPermissions(user?.permissions ?? [], ['admin', 'editor'])" :to="{ name: 'explore-edit', hash: `#${encodeURIComponent(overview!.path)}` }" class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10">
|
||||
<Tooltip message="Modifier" side="right">
|
||||
<Icon icon="radix-icons:pencil-1" class="w-8 h-8 p-2" />
|
||||
</Tooltip>
|
||||
</NuxtLink>
|
||||
*/
|
||||
}
|
||||
|
||||
protected computeLimits()
|
||||
|
|
@ -393,26 +441,21 @@ export class Canvas
|
|||
maxY = Math.max(maxY, e.y + e.height);
|
||||
});
|
||||
|
||||
this.containZoom = Math.pow(1 / Math.max((maxX - minX) / box.width, (maxY - minY) / box.height), 1.05);
|
||||
this.containZoom = clamp(Math.pow(1 / Math.max((maxX - minX) / box.width, (maxY - minY) / box.height), 1.05), Canvas.minZoom, 1);
|
||||
this.centerX = -(minX + (maxX - minX) / 2) + box.width / 2;
|
||||
this.centerY = -(minY + (maxY - minY) / 2) + box.height / 2;
|
||||
}
|
||||
|
||||
mount()
|
||||
{
|
||||
let lastX = 0, lastY = 0, lastDistance = 0;
|
||||
const dragMove = (e: MouseEvent) => {
|
||||
this.x = this.visualX = this.x - (lastX - e.layerX) / this.zoom;
|
||||
this.y = this.visualY = this.y - (lastY - e.layerY) / this.zoom;
|
||||
|
||||
lastX = e.layerX;
|
||||
lastY = e.layerY;
|
||||
|
||||
this.updateTransform();
|
||||
this.dragMove(e);
|
||||
};
|
||||
const dragEnd = (e: MouseEvent) => {
|
||||
window.removeEventListener('mouseup', dragEnd);
|
||||
window.removeEventListener('mousemove', dragMove);
|
||||
|
||||
this.dragEnd(e);
|
||||
};
|
||||
this.container.addEventListener('mouseenter', () => {
|
||||
window.addEventListener('wheel', cancelEvent, { passive: false });
|
||||
|
|
@ -424,32 +467,36 @@ export class Canvas
|
|||
document.removeEventListener('gesturestart', cancelEvent);
|
||||
document.removeEventListener('gesturechange', cancelEvent);
|
||||
});
|
||||
})
|
||||
});
|
||||
this.container.addEventListener('mousedown', (e) => {
|
||||
lastX = e.layerX;
|
||||
lastY = e.layerY;
|
||||
this.lastX = e.clientX;
|
||||
this.lastY = e.clientY;
|
||||
|
||||
const pos = this.getPosFromCursor(e.clientX, e.clientY);
|
||||
this.firstX = pos.x;
|
||||
this.firstY = pos.y;
|
||||
|
||||
window.addEventListener('mouseup', dragEnd, { passive: true });
|
||||
window.addEventListener('mousemove', dragMove, { passive: true });
|
||||
|
||||
this.dragStart(e);
|
||||
}, { passive: true });
|
||||
this.container.addEventListener('wheel', (e) => {
|
||||
if((this.zoom >= Canvas.maxZoom && e.deltaY < 0) || (this.zoom <= this.containZoom && e.deltaY > 0))
|
||||
if((this._zoom >= Canvas.maxZoom && e.deltaY < 0) || (this._zoom <= this.containZoom && e.deltaY > 0))
|
||||
return;
|
||||
|
||||
let box = this.container.getBoundingClientRect()!;
|
||||
|
||||
const diff = Math.exp(e.deltaY * -0.001);
|
||||
const box = this.container.getBoundingClientRect(), 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;
|
||||
|
||||
this.zoomTo(this.x - (mousex / (diff * this.zoom) - mousex / this.zoom), this.y - (mousey / (diff * this.zoom) - mousey / this.zoom), clamp(this.zoom * diff, this.containZoom, Canvas.maxZoom));
|
||||
this.zoomTo(this._x - (mousex / (diff * this._zoom) - mousex / this._zoom), this._y - (mousey / (diff * this._zoom) - mousey / this._zoom), clamp(this._zoom * diff, this.containZoom, Canvas.maxZoom));
|
||||
}, { passive: true });
|
||||
this.container.addEventListener('touchstart', (e) => {
|
||||
({ x: lastX, y: lastY } = center(e.touches));
|
||||
({ x: this.lastX, y: this.lastY } = center(e.touches));
|
||||
|
||||
if(e.touches.length > 1)
|
||||
{
|
||||
lastDistance = distance(e.touches);
|
||||
this.lastDistance = distance(e.touches);
|
||||
}
|
||||
|
||||
this.container.addEventListener('touchend', touchend, { passive: true });
|
||||
|
|
@ -459,7 +506,7 @@ export class Canvas
|
|||
const touchend = (e: TouchEvent) => {
|
||||
if(e.touches.length > 1)
|
||||
{
|
||||
({ x: lastX, y: lastY } = center(e.touches));
|
||||
({ x: this.lastX, y: this.lastY } = center(e.touches));
|
||||
}
|
||||
|
||||
this.container.removeEventListener('touchend', touchend);
|
||||
|
|
@ -469,7 +516,7 @@ export class Canvas
|
|||
const touchcancel = (e: TouchEvent) => {
|
||||
if(e.touches.length > 1)
|
||||
{
|
||||
({ x: lastX, y: lastY } = center(e.touches));
|
||||
({ x: this.lastX, y: this.lastY } = center(e.touches));
|
||||
}
|
||||
|
||||
this.container.removeEventListener('touchend', touchend);
|
||||
|
|
@ -478,17 +525,17 @@ export class Canvas
|
|||
};
|
||||
const touchmove = (e: TouchEvent) => {
|
||||
const pos = center(e.touches);
|
||||
this.x = this.visualX = this.x - (lastX - pos.x) / this.zoom;
|
||||
this.y = this.visualY = this.y - (lastY - pos.y) / this.zoom;
|
||||
lastX = pos.x;
|
||||
lastY = pos.y;
|
||||
this._x = this.visualX = this._x - (this.lastX - pos.x) / this._zoom;
|
||||
this._y = this.visualY = this._y - (this.lastY - pos.y) / this._zoom;
|
||||
this.lastX = pos.x;
|
||||
this.lastY = pos.y;
|
||||
|
||||
if(e.touches.length === 2)
|
||||
{
|
||||
const dist = distance(e.touches);
|
||||
const diff = dist / lastDistance;
|
||||
const diff = dist / this.lastDistance;
|
||||
|
||||
this.zoom = clamp(this.zoom * diff, this.containZoom, Canvas.maxZoom);
|
||||
this._zoom = clamp(this._zoom * diff, this.containZoom, Canvas.maxZoom);
|
||||
}
|
||||
|
||||
this.updateTransform();
|
||||
|
|
@ -498,7 +545,7 @@ export class Canvas
|
|||
this.reset();
|
||||
}
|
||||
|
||||
private updateTransform()
|
||||
protected updateTransform()
|
||||
{
|
||||
this.transform.style.transform = `scale3d(${this.visualZoom}, ${this.visualZoom}, 1) translate3d(${this.visualX}px, ${this.visualY}px, 0)`;
|
||||
|
||||
|
|
@ -514,10 +561,10 @@ export class Canvas
|
|||
|
||||
protected zoomTo(x: number, y: number, zoom: number)
|
||||
{
|
||||
const oldX = this.x, oldY = this.y, oldZoom = this.zoom;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.zoom = zoom;
|
||||
const oldX = this._x, oldY = this._y, oldZoom = this._zoom;
|
||||
this._x = x;
|
||||
this._y = y;
|
||||
this._zoom = zoom;
|
||||
|
||||
this.tweener.update((e) => {
|
||||
this.visualX = lerp(e, oldX, x);
|
||||
|
|
@ -532,44 +579,144 @@ export class Canvas
|
|||
{
|
||||
this.zoomTo(this.centerX, this.centerY, this.containZoom);
|
||||
}
|
||||
|
||||
protected dragStart(e: MouseEvent) {}
|
||||
protected dragMove(e: MouseEvent)
|
||||
{
|
||||
this._x = this.visualX = this._x - (this.lastX - e.clientX) / this._zoom;
|
||||
this._y = this.visualY = this._y - (this.lastY - e.clientY) / this._zoom;
|
||||
|
||||
this.lastX = e.clientX;
|
||||
this.lastY = e.clientY;
|
||||
|
||||
this.updateTransform();
|
||||
}
|
||||
protected dragEnd(e: MouseEvent) {}
|
||||
|
||||
protected getPosFromCursor(x: number, y: number): Position
|
||||
{
|
||||
const box = this.container.getBoundingClientRect();
|
||||
const centerX = box.x + box.width / 2, centerY = box.y + box.height / 2;
|
||||
return {x: (x - centerX) / this._zoom - this._x + box.width / 2, y: (y - centerY) / this._zoom - this._y + box.height / 2 };
|
||||
}
|
||||
|
||||
get zoom()
|
||||
{
|
||||
return this._zoom;
|
||||
}
|
||||
get x()
|
||||
{
|
||||
return this._x;
|
||||
}
|
||||
get y()
|
||||
{
|
||||
return this._y;
|
||||
}
|
||||
|
||||
get viewport()
|
||||
{
|
||||
const box = this.container.getBoundingClientRect();
|
||||
const width = box.width / this._zoom, height = box.height / this._zoom;
|
||||
const movementX = box.width - width, movementY = box.height - height;
|
||||
return { x: -this._x + movementX / 2, y: -this._y + movementY / 2, width, height };
|
||||
}
|
||||
}
|
||||
export class CanvasEditor extends Canvas
|
||||
{
|
||||
private static SPACING = 32;
|
||||
private history: History;
|
||||
private selection: Array<NodeEditable | EdgeEditable> = [];
|
||||
private focused: NodeEditable | EdgeEditable | undefined;
|
||||
private selection: Set<NodeEditable> = new Set();
|
||||
|
||||
private nodes!: NodeEditable[];
|
||||
private edges!: EdgeEditable[];
|
||||
private dragging: boolean = false;
|
||||
private dragger: HTMLElement = dom('div', { class: 'border border-accent-blue absolute shadow-accent-blue pointer-events-none', style: { 'box-shadow': '0 0 2px var(--tw-shadow-color)' } });
|
||||
|
||||
constructor(history: History, content?: CanvasContent)
|
||||
private pattern: SVGElement = svg('svg', { class: 'absolute top-0 left-0 w-full h-full pointer-events-none' }, [
|
||||
svg('pattern', { attributes: { id: 'canvasPattern', patternUnits: 'userSpaceOnUse' } }, [ svg('circle', { class: 'fill-light-35 dark:fill-dark-35', attributes: { cx: '0.75', cy: '0.75', r: '0.75' } }) ]),
|
||||
svg('rect', { attributes: { x: '0', y: '0', width: '100%', height: '100%', fill: 'url(#canvasPattern)' } })
|
||||
]);
|
||||
private nodeHelper: HTMLElement = dom('div', { class: 'cursor-move absolute z-40', listeners: { mousedown: e => this.moveSelection(e) }, style: { width: '0px', height: '0px' } }, [
|
||||
dom('span', { class: 'cursor-n-resize absolute -top-3 -right-3 -left-3 h-6 group', listeners: { mousedown: e => this.resizeSelection(e, 0, 1, 0, -1) } }, [ dom('span', { class: 'hidden group-hover:block absolute rounded-full border-2 border-light-70 dark:border-dark-70 w-4 h-4 top-1 left-1/2 -translate-x-2', listeners: { mousedown: e => this.dragNewEdge(e, 'top') } }) ]),
|
||||
dom('span', { class: 'cursor-s-resize absolute -bottom-3 -right-3 -left-3 h-6 group', listeners: { mousedown: e => this.resizeSelection(e, 0, 0, 0, 1) } }, [ dom('span', { class: 'hidden group-hover:block absolute rounded-full border-2 border-light-70 dark:border-dark-70 w-4 h-4 bottom-1 left-1/2 -translate-x-2', listeners: { mousedown: e => this.dragNewEdge(e, 'bottom') } }) ]),
|
||||
dom('span', { class: 'cursor-e-resize absolute -top-3 -bottom-3 -right-3 w-6 group', listeners: { mousedown: e => this.resizeSelection(e, 0, 0, 1, 0) } }, [ dom('span', { class: 'hidden group-hover:block absolute rounded-full border-2 border-light-70 dark:border-dark-70 w-4 h-4 right-1 top-1/2 -translate-y-2', listeners: { mousedown: e => this.dragNewEdge(e, 'right') } }) ]),
|
||||
dom('span', { class: 'cursor-w-resize absolute -top-3 -bottom-3 -left-3 w-6 group', listeners: { mousedown: e => this.resizeSelection(e, 1, 0, -1, 0) } }, [ dom('span', { class: 'hidden group-hover:block absolute rounded-full border-2 border-light-70 dark:border-dark-70 w-4 h-4 left-1 top-1/2 -translate-y-2', listeners: { mousedown: e => this.dragNewEdge(e, 'left') } }) ]),
|
||||
dom('span', { class: 'cursor-nw-resize absolute -top-4 -left-4 w-8 h-8', listeners: { mousedown: e => this.resizeSelection(e, 1, 1, -1, -1) } }),
|
||||
dom('span', { class: 'cursor-ne-resize absolute -top-4 -right-4 w-8 h-8', listeners: { mousedown: e => this.resizeSelection(e, 0, 1, 1, -1) } }),
|
||||
dom('span', { class: 'cursor-se-resize absolute -bottom-4 -right-4 w-8 h-8', listeners: { mousedown: e => this.resizeSelection(e, 0, 0, 1, 1) } }),
|
||||
dom('span', { class: 'cursor-sw-resize absolute -bottom-4 -left-4 w-8 h-8', listeners: { mousedown: e => this.resizeSelection(e, 1, 0, -1, 1) } }),
|
||||
]);
|
||||
private edgeHelper: HTMLElement = dom('div', { class: 'absolute', listeners: { } });
|
||||
private boxHelper: HTMLElement = dom('div', { class: '-m-2 border border-accent-purple absolute z-10 p-2 box-content', listeners: { mouseenter: () => this.focusSelection() } });
|
||||
|
||||
protected override nodes: NodeEditable[] = [];
|
||||
protected override edges: EdgeEditable[] = [];
|
||||
|
||||
private preferences: Ref<CanvasPreferences>;
|
||||
private grid: SpatialGrid<NodeEditable> = new SpatialGrid<NodeEditable>(128);
|
||||
|
||||
constructor(content?: CanvasContent)
|
||||
{
|
||||
super(content);
|
||||
this.history = history;
|
||||
|
||||
this.createDOM();
|
||||
|
||||
this.history = new History();
|
||||
this.history.register('canvas', {
|
||||
move: {
|
||||
undo: action => {
|
||||
|
||||
},
|
||||
redo: action => {
|
||||
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
this.preferences = useCookie<CanvasPreferences>('canvasPreference', { default: () => ({ gridSnap: true, neighborSnap: true, spacing: 32 }) });
|
||||
}
|
||||
protected override createDOM()
|
||||
{
|
||||
this.nodes = this.content.nodes.map(e => new NodeEditable(e));
|
||||
this.edges = this.content.edges.map(e => new EdgeEditable(e, this.content.nodes));
|
||||
if(!this.grid)
|
||||
return;
|
||||
|
||||
this.nodes = this.content.nodes.map(e => {
|
||||
const node = new NodeEditable(e);
|
||||
//@ts-ignore
|
||||
node.properties.type === "text" && node.addEventListener('focus', this.focusNode.bind(this));
|
||||
//@ts-ignore
|
||||
node.addEventListener('select', this.selectNode.bind(this));
|
||||
//@ts-ignore
|
||||
node.addEventListener('edit', this.editNode.bind(this));
|
||||
node.properties.type === "text" && this.grid.insert(node);
|
||||
return node;
|
||||
});
|
||||
this.edges = this.content.edges.map(e => {
|
||||
const edge = new EdgeEditable(e, this.nodes.find(f => e.fromNode === f.properties.id)!, this.nodes.find(f => e.toNode === f.properties.id)!);
|
||||
//@ts-ignore
|
||||
edge.addEventListener('focus', this.focusEdge.bind(this));
|
||||
//@ts-ignore
|
||||
edge.addEventListener('select', this.selectEdge.bind(this));
|
||||
//@ts-ignore
|
||||
edge.addEventListener('edit', this.editEdge.bind(this));
|
||||
return edge;
|
||||
});
|
||||
|
||||
this.transform = dom('div', { class: 'origin-center h-full' }, [
|
||||
dom('div', { class: 'absolute top-0 left-0 w-full h-full pointer-events-none *:pointer-events-auto *:select-none touch-none' }, [
|
||||
dom('div', {}, [...this.nodes.map(e => e.nodeDom)]), dom('div', {}, [...this.edges.map(e => e.edgeDom)]),
|
||||
])
|
||||
dom('div', {}, [...this.nodes.map(e => e.nodeDom), this.nodeHelper]), dom('div', {}, [...this.edges.map(e => e.edgeDom)]),
|
||||
]), this.edgeHelper,
|
||||
]);
|
||||
//TODO: --zoom-multiplier dynamic
|
||||
this.container = dom('div', { class: 'absolute top-0 left-0 overflow-hidden w-full h-full touch-none' }, [
|
||||
|
||||
this.container = dom('div', { class: 'absolute top-0 left-0 overflow-hidden w-full h-full touch-none', listeners: { mousedown: () => { this.selection.clear(); this.updateSelection() } } }, [
|
||||
dom('div', { class: 'flex flex-col absolute sm:top-2 top-10 left-2 z-[35] overflow-hidden gap-4' }, [
|
||||
dom('div', { class: 'border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10' }, [
|
||||
popper(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: () => this.zoomTo(this.x, this.y, clamp(this.zoom * 1.1, this.containZoom, Canvas.maxZoom)) } }, [icon('radix-icons:plus')]), {
|
||||
popper(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: () => this.zoomTo(this._x, this._y, clamp(this._zoom * 1.1, this.containZoom, Canvas.maxZoom)) } }, [icon('radix-icons:plus')]), {
|
||||
delay: 120, arrow: true, placement: 'right', offset: 8, content: [text('Zoom avant')], class: 'TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70 z-50'
|
||||
}),
|
||||
popper(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: (e: MouseEvent) => { this.reset(); } } }, [icon('radix-icons:reload')]), {
|
||||
delay: 120, arrow: true, placement: 'right', offset: 8, content: [text('Reset')], class: 'TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70 z-50'
|
||||
}),
|
||||
popper(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: (e: MouseEvent) => { this.zoomTo(this.x, this.y, this.containZoom); } } }, [icon('radix-icons:corners')]), {
|
||||
popper(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: () => this.reset() } }, [icon('radix-icons:corners')]), {
|
||||
delay: 120, arrow: true, placement: 'right', offset: 8, content: [text('Tout contenir')], class: 'TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70 z-50'
|
||||
}),
|
||||
popper(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: () => this.zoomTo(this.x, this.y, clamp(this.zoom / 1.1, this.containZoom, Canvas.maxZoom)) } }, [icon('radix-icons:minus')]), {
|
||||
popper(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: () => this.zoomTo(this._x, this._y, clamp(this._zoom / 1.1, this.containZoom, Canvas.maxZoom)) } }, [icon('radix-icons:minus')]), {
|
||||
delay: 120, arrow: true, placement: 'right', offset: 8, content: [text('Zoom arrière')], class: 'TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70 z-50'
|
||||
}),
|
||||
]),
|
||||
|
|
@ -582,16 +729,229 @@ export class CanvasEditor extends Canvas
|
|||
}),
|
||||
]),
|
||||
dom('div', { class: 'border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10' }, [
|
||||
popper(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: () => this.history.undo() } }, [icon('radix-icons:gear')]), {
|
||||
popper(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: () => {} } }, [icon('radix-icons:gear')]), {
|
||||
delay: 120, arrow: true, placement: 'right', offset: 8, content: [text('Préférences')], class: 'TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70 z-50'
|
||||
}),
|
||||
popper(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: () => this.history.redo() } }, [icon('radix-icons:question-mark-circled')]), {
|
||||
popper(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: () => {} } }, [icon('radix-icons:question-mark-circled')]), {
|
||||
delay: 120, arrow: true, placement: 'right', offset: 8, content: [text('Aide')], class: 'TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70 z-50'
|
||||
}),
|
||||
]),
|
||||
]), this.transform,
|
||||
]), this.pattern, this.transform
|
||||
]);
|
||||
}
|
||||
|
||||
private focusNode(e: CustomEvent<NodeEditable>)
|
||||
{
|
||||
if(this.dragging)
|
||||
return;
|
||||
|
||||
e.stopImmediatePropagation();
|
||||
|
||||
this.focused = e.detail;
|
||||
Object.assign(this.nodeHelper.style, {
|
||||
transform: `translate(${e.detail.x}px, ${e.detail.y}px)`,
|
||||
width: `${e.detail.width}px`,
|
||||
height: `${e.detail.height}px`,
|
||||
});
|
||||
}
|
||||
private selectNode(e: CustomEvent<NodeEditable>)
|
||||
{
|
||||
|
||||
}
|
||||
private editNode(e: CustomEvent<NodeEditable>)
|
||||
{
|
||||
|
||||
}
|
||||
private updateSelection()
|
||||
{
|
||||
if(this.selection.size > 0)
|
||||
{
|
||||
const selectionBox = getBox(this.selection);
|
||||
Object.assign(this.boxHelper.style, {
|
||||
left: `${selectionBox.x}px`,
|
||||
top: `${selectionBox.y}px`,
|
||||
width: `${selectionBox.width}px`,
|
||||
height: `${selectionBox.height}px`,
|
||||
});
|
||||
|
||||
if(this.boxHelper.parentElement === null) this.transform.appendChild(this.boxHelper);
|
||||
}
|
||||
else
|
||||
this.boxHelper.remove();
|
||||
}
|
||||
private focusSelection()
|
||||
{
|
||||
if(this.dragging)
|
||||
return;
|
||||
|
||||
const box = this.boxHelper.getBoundingClientRect();
|
||||
Object.assign(this.nodeHelper.style, {
|
||||
top: `${box.top}px`,
|
||||
left: `${box.left}px`,
|
||||
width: `${box.width}px`,
|
||||
height: `${box.height}px`,
|
||||
});
|
||||
}
|
||||
private moveSelection(e: MouseEvent)
|
||||
{
|
||||
if(!(e.buttons & 1))
|
||||
return;
|
||||
|
||||
e.stopImmediatePropagation();
|
||||
|
||||
if(this.selection.size === 0 && this.focused !== undefined)
|
||||
{
|
||||
this.selection.add(this.focused as NodeEditable);
|
||||
this.updateSelection();
|
||||
}
|
||||
|
||||
const end = () => {
|
||||
if(moveX !== 0 && moveY !== 0)
|
||||
this.history.add('canvas', 'move', [...this.selection.values()].map(e => ({ element: e, from: { x: startX, y: startY }, to: { x: startX + moveX, y: startY + moveY } })));
|
||||
|
||||
window.removeEventListener('mousemove', move);
|
||||
window.removeEventListener('mouseup', end);
|
||||
};
|
||||
const move = (e: MouseEvent) => {
|
||||
const movementX = e.movementX / this._zoom, movementY = e.movementY / this._zoom;
|
||||
|
||||
moveX += movementX;
|
||||
moveY += movementY;
|
||||
|
||||
this.selection.forEach(_e => {
|
||||
_e.x += movementX;
|
||||
_e.y += movementY;
|
||||
|
||||
_e.update();
|
||||
});
|
||||
|
||||
let box = this.boxHelper.getBoundingClientRect();
|
||||
Object.assign(this.boxHelper.style, {
|
||||
left: `${box.x + movementX}px`,
|
||||
top: `${box.y + movementY}px`,
|
||||
})
|
||||
|
||||
box = this.nodeHelper.getBoundingClientRect();
|
||||
Object.assign(this.nodeHelper.style, {
|
||||
left: `${box.x + movementX}px`,
|
||||
top: `${box.y + movementY}px`,
|
||||
})
|
||||
};
|
||||
|
||||
const startX = e.clientX, startY = e.clientY;
|
||||
let moveX = 0, moveY = 0;
|
||||
window.addEventListener('mousemove', move);
|
||||
window.addEventListener('mouseup', end);
|
||||
}
|
||||
private resizeSelection(e: MouseEvent, x: number, y: number, w: number, h: number)
|
||||
{
|
||||
if(!(e.buttons & 1))
|
||||
return;
|
||||
|
||||
e.stopImmediatePropagation();
|
||||
|
||||
|
||||
}
|
||||
private focusEdge(e: CustomEvent<EdgeEditable>)
|
||||
{
|
||||
this.focused = e.detail;
|
||||
}
|
||||
private selectEdge(e: CustomEvent<EdgeEditable>)
|
||||
{
|
||||
|
||||
}
|
||||
private editEdge(e: CustomEvent<EdgeEditable>)
|
||||
{
|
||||
|
||||
}
|
||||
private dragNewEdge(e: MouseEvent, direction: Direction)
|
||||
{
|
||||
e.stopImmediatePropagation();
|
||||
}
|
||||
|
||||
override updateTransform()
|
||||
{
|
||||
super.updateTransform();
|
||||
|
||||
this.pattern.parentElement?.classList.toggle('hidden', !this.preferences.value.gridSnap);
|
||||
if(this.preferences.value.gridSnap)
|
||||
{
|
||||
this.pattern.setAttribute("x", (this.viewport.width / 2 + this._x % CanvasEditor.SPACING * this._zoom).toFixed(3));
|
||||
this.pattern.setAttribute("y", (this.viewport.height / 2 + this._y % CanvasEditor.SPACING * this._zoom).toFixed(3));
|
||||
this.pattern.setAttribute("width", (this._zoom * CanvasEditor.SPACING).toFixed(3));
|
||||
this.pattern.setAttribute("height", (this._zoom * CanvasEditor.SPACING).toFixed(3));
|
||||
|
||||
this.pattern.children[0].setAttribute('cx', (this._zoom).toFixed(3));
|
||||
this.pattern.children[0].setAttribute('cy', (this._zoom).toFixed(3));
|
||||
this.pattern.children[0].setAttribute('r', (this._zoom).toFixed(3));
|
||||
}
|
||||
}
|
||||
override mount()
|
||||
{
|
||||
super.mount();
|
||||
|
||||
this.container.addEventListener('contextmenu', cancelEvent);
|
||||
}
|
||||
protected override dragStart(e: MouseEvent)
|
||||
{
|
||||
super.dragStart(e);
|
||||
|
||||
this.dragging = !!(e.buttons & 1);
|
||||
|
||||
if(this.dragging)
|
||||
{
|
||||
this.transform.appendChild(this.dragger);
|
||||
|
||||
const pos = this.getPosFromCursor(e.clientX, e.clientY);
|
||||
Object.assign(this.dragger.style, {
|
||||
left: `${pos.x}px`,
|
||||
top: `${pos.y}px`,
|
||||
width: `0px`,
|
||||
height: `0px`,
|
||||
});
|
||||
}
|
||||
}
|
||||
protected override dragMove(e: MouseEvent)
|
||||
{
|
||||
if(this.dragging)
|
||||
{
|
||||
const pos = this.getPosFromCursor(e.clientX, e.clientY);
|
||||
const minX = Math.min(this.firstX, pos.x), minY = Math.min(this.firstY, pos.y), maxX = Math.max(this.firstX, pos.x), maxY = Math.max(this.firstY, pos.y);
|
||||
|
||||
Object.assign(this.dragger.style, {
|
||||
left: `${minX}px`,
|
||||
top: `${minY}px`,
|
||||
width: `${maxX - minX}px`,
|
||||
height: `${maxY - minY}px`,
|
||||
});
|
||||
|
||||
this.selection = new Set(this.grid.query(minX, minY, maxX, maxY));
|
||||
this.updateSelection();
|
||||
}
|
||||
else if(!this.dragging)
|
||||
{
|
||||
super.dragMove(e);
|
||||
}
|
||||
}
|
||||
protected override dragEnd(e: MouseEvent)
|
||||
{
|
||||
this.dragging = false;
|
||||
this.dragger.remove();
|
||||
}
|
||||
}
|
||||
|
||||
function getBox<T extends Box>(selection: Set<T>): Box
|
||||
{
|
||||
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
||||
|
||||
selection.forEach(e => {
|
||||
minX = Math.min(minX, e.x);
|
||||
minY = Math.min(minY, e.y);
|
||||
maxX = Math.max(maxX, e.x + e.width);
|
||||
maxY = Math.max(maxY, e.y + e.height);
|
||||
});
|
||||
|
||||
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
||||
}
|
||||
|
||||
class Tweener
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { safeDestr as parse } from 'destr';
|
||||
import { Canvas } from "#shared/canvas.util";
|
||||
import { Canvas, CanvasEditor } from "#shared/canvas.util";
|
||||
import render from "#shared/markdown.util";
|
||||
import { confirm, contextmenu, popper } from "#shared/floating.util";
|
||||
import { cancelPropagation, dom, icon, text, type Node } from "#shared/dom.util";
|
||||
|
|
@ -129,6 +129,7 @@ export class Content
|
|||
return false;
|
||||
|
||||
Content.root = await navigator.storage.getDirectory();
|
||||
Content.root.getDirectoryHandle('storage', { create: true });
|
||||
|
||||
const overview = await Content.read('overview', { create: true });
|
||||
try
|
||||
|
|
@ -153,11 +154,11 @@ export class Content
|
|||
|
||||
return Content.initPromise;
|
||||
}
|
||||
static getFromPath(path: string, content: boolean = false)
|
||||
static getFromPath(path: string)
|
||||
{
|
||||
return Object.values(Content._overview).find(e => e.path === path);
|
||||
}
|
||||
static get(id: string, content: boolean = false)
|
||||
static get(id: string)
|
||||
{
|
||||
return Content._overview[id];
|
||||
}
|
||||
|
|
@ -238,13 +239,16 @@ export class Content
|
|||
{
|
||||
Content._overview[file.id] = file;
|
||||
|
||||
if(file.type === 'folder')
|
||||
continue;
|
||||
|
||||
Content.queue.queue(() => {
|
||||
return useRequestFetch()(`/api/file/content/${file.id}`, { cache: 'no-cache' }).then(async (content: string | undefined) => {
|
||||
if(content)
|
||||
Content.queue.queue(() => Content.write(file.id, content, { create: true }));
|
||||
{
|
||||
if(file.type !== 'folder')
|
||||
{
|
||||
Content.queue.queue(() => Content.write(file.id, content, { create: true }));
|
||||
Content.queue.queue(() => Content.write('storage/' + file.path + (file.type === 'canvas' ? '.canvas' : '.md'), Content.toString({ ...file, content: Content.fromString(file, content) }), { create: true }));
|
||||
}
|
||||
}
|
||||
else
|
||||
Content._overview[file.id].error = true;
|
||||
}).catch(e => {
|
||||
|
|
@ -293,6 +297,23 @@ export class Content
|
|||
console.timeEnd(`Reading '${path}'`);
|
||||
}
|
||||
}
|
||||
private static async goto(path: string, options?: FileSystemGetDirectoryOptions): Promise<FileSystemDirectoryHandle | undefined>
|
||||
{
|
||||
const splitPath = path.split("/");
|
||||
let handle = Content.root;
|
||||
try
|
||||
{
|
||||
for(const p of splitPath)
|
||||
{
|
||||
handle = await handle.getDirectoryHandle(p, options);
|
||||
}
|
||||
return handle;
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
//Easy to use, but not very performant.
|
||||
private static async write(path: string, content: string, options?: FileSystemGetFileOptions): Promise<void>
|
||||
{
|
||||
|
|
@ -300,7 +321,8 @@ export class Content
|
|||
console.time(`Writing ${size} bytes to '${path}'`);
|
||||
try
|
||||
{
|
||||
const handle = await Content.root.getFileHandle(path, options);
|
||||
const parent = path.split('/').slice(0, -1).join('/'), basename = path.split('/').slice(-1).join('/');
|
||||
const handle = await (await Content.goto(parent, { create: true }) ?? Content.root).getFileHandle(basename, options);
|
||||
const file = await handle.createWritable({ keepExistingData: false });
|
||||
|
||||
await file.write(content);
|
||||
|
|
@ -403,16 +425,58 @@ type ContentTypeHandler<T extends FileType> = {
|
|||
renderEditor: (content: LocalContent<T>) => Node;
|
||||
};
|
||||
|
||||
function reshapeLinks(content: string | null, all: ProjectContent[])
|
||||
{
|
||||
return content?.replace(/\[\[(.*?)?(#.*?)?(\|.*?)?\]\]/g, (str, link, header, title) => {
|
||||
return `[[${link ? parsePath(all.find(e => e.path.endsWith(parsePath(link)))?.path ?? parsePath(link)) : ''}${header ?? ''}${title ?? ''}]]`;
|
||||
});
|
||||
}
|
||||
|
||||
const handlers: { [K in FileType]: ContentTypeHandler<K> } = {
|
||||
canvas: {
|
||||
toString: (content) => JSON.stringify(content),
|
||||
toString: (content) => {
|
||||
const mapping: Record<string, string> = {
|
||||
'red': '1',
|
||||
'orange': '2',
|
||||
'yellow': '3',
|
||||
'green': '4',
|
||||
'cyan': '5',
|
||||
'purple': '6',
|
||||
};
|
||||
content.edges?.forEach(e => e.color = e.color ? e.color.hex ?? (e.color.class ? mapping[e.color.class]! : undefined) : undefined);
|
||||
content.nodes?.forEach(e => e.color = e.color ? e.color.hex ?? (e.color.class ? mapping[e.color.class]! : undefined) : undefined);
|
||||
|
||||
return JSON.stringify(content);
|
||||
},
|
||||
fromString: (str) => JSON.parse(str),
|
||||
render: (content) => {
|
||||
const c = new Canvas(content.content);
|
||||
queueMicrotask(() => c.mount());
|
||||
return c.container;
|
||||
},
|
||||
renderEditor: (content) => prose('h2', h2, [text('En cours de développement')], { class: 'flex-1 text-center' }),
|
||||
renderEditor: (content) => {
|
||||
let element: HTMLElement;
|
||||
if(content.hasOwnProperty('content'))
|
||||
{
|
||||
const c = new CanvasEditor(content.content);
|
||||
queueMicrotask(() => c.mount());
|
||||
element = c.container;
|
||||
}
|
||||
else
|
||||
{
|
||||
element = loading("large");
|
||||
Content.getContent(content.id).then(e => {
|
||||
if(!e)
|
||||
return element.parentElement?.replaceChild(dom('div', { class: '', text: 'Une erreur est survenue.' }), element);
|
||||
|
||||
content.content = e.content as CanvasContent;
|
||||
const c = new CanvasEditor(content.content);
|
||||
queueMicrotask(() => c.mount());
|
||||
element.parentElement?.replaceChild(c.container, element);
|
||||
});
|
||||
}
|
||||
return element;
|
||||
},
|
||||
},
|
||||
markdown: {
|
||||
toString: (content) => content,
|
||||
|
|
@ -624,6 +688,8 @@ export class Editor
|
|||
this.cleanup = this.setupDnD();
|
||||
|
||||
this.container = dom('div', { class: 'flex flex-1 flex-col items-start justify-start max-h-full relative' }, [dom('div', { class: 'py-4 flex-1 w-full max-h-full flex overflow-auto xl:px-12 lg:px-8 px-6 relative' })]);
|
||||
|
||||
this.select(this.tree.tree.find(useRouter().currentRoute.value.hash.substring(1)) as Recursive<LocalContent & { element?: HTMLElement }> | undefined);
|
||||
}
|
||||
private contextmenu(e: MouseEvent, item: Recursive<LocalContent>)
|
||||
{
|
||||
|
|
@ -812,6 +878,8 @@ export class Editor
|
|||
this.selected = item;
|
||||
}
|
||||
|
||||
useRouter().push({ hash: this.selected ? '#' + this.selected.id : '' })
|
||||
|
||||
this.container.firstElementChild!.replaceChildren();
|
||||
this.selected && this.container.firstElementChild!.appendChild(this.render(this.selected) as HTMLElement);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,8 +43,9 @@ export default function(content: string, filter?: string, properties?: MDPropert
|
|||
{
|
||||
const load = loading('normal');
|
||||
|
||||
useMarkdown().parse(content).then(data => {
|
||||
if(filter)
|
||||
queueMicrotask(() => {
|
||||
useMarkdown().parse(content).then(data => {
|
||||
if(filter)
|
||||
{
|
||||
const start = data?.children.findIndex(e => heading(e) && parseId(e.properties.id as string | undefined) === filter) ?? -1;
|
||||
|
||||
|
|
@ -67,7 +68,8 @@ export default function(content: string, filter?: string, properties?: MDPropert
|
|||
if(properties) styling(el, properties);
|
||||
|
||||
load.parentElement?.replaceChild(el, load);
|
||||
});
|
||||
});
|
||||
})
|
||||
|
||||
return load;
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import type { CanvasContent, CanvasNode } from "~/types/canvas";
|
||||
import type { CanvasPreferences } from "~/types/general";
|
||||
import type { Position, Box, Direction } from "./canvas.util";
|
||||
import type { Position, Box, Direction, CanvasEditor } from "./canvas.util";
|
||||
|
||||
interface SnapPoint {
|
||||
pos: Position;
|
||||
|
|
@ -26,8 +26,20 @@ const enum TYPE {
|
|||
EDGE,
|
||||
}
|
||||
|
||||
class SpatialGrid {
|
||||
private cells: Map<number, Map<number, Set<string>>> = new Map();
|
||||
export function overlapsBoxes(a: Box, b: Box): boolean
|
||||
{
|
||||
return overlaps(a.x, a.y, a.width, a.height, b.x, b.y, b.width, b.height);
|
||||
}
|
||||
export function overlaps(ax: number, ay: number, aw: number, ah: number, bx: number, by: number, bw: number, bh: number): boolean
|
||||
{
|
||||
return !(bx > (ax + aw)
|
||||
|| (bx + bw) < ax
|
||||
|| by > (ay + ah)
|
||||
|| (by + bh) < ay);
|
||||
}
|
||||
|
||||
export class SpatialGrid<T extends Box> {
|
||||
private cells: Map<number, Map<number, Set<T>>> = new Map();
|
||||
private cellSize: number;
|
||||
|
||||
private minx: number = Infinity;
|
||||
|
|
@ -35,20 +47,23 @@ class SpatialGrid {
|
|||
private maxx: number = -Infinity;
|
||||
private maxy: number = -Infinity;
|
||||
|
||||
private cacheSet: Set<string> = new Set<string>();
|
||||
private cacheSet: Set<T> = new Set<T>();
|
||||
|
||||
constructor(cellSize: number) {
|
||||
constructor(cellSize: number)
|
||||
{
|
||||
this.cellSize = cellSize;
|
||||
}
|
||||
|
||||
private updateBorders(startX: number, startY: number, endX: number, endY: number) {
|
||||
private updateBorders(startX: number, startY: number, endX: number, endY: number)
|
||||
{
|
||||
this.minx = Math.min(this.minx, startX);
|
||||
this.miny = Math.min(this.miny, startY);
|
||||
this.maxx = Math.max(this.maxx, endX);
|
||||
this.maxy = Math.max(this.maxy, endY);
|
||||
}
|
||||
|
||||
insert(node: CanvasNode): void {
|
||||
insert(node: T): void
|
||||
{
|
||||
const startX = Math.floor(node.x / this.cellSize);
|
||||
const startY = Math.floor(node.y / this.cellSize);
|
||||
const endX = Math.ceil((node.x + node.width) / this.cellSize);
|
||||
|
|
@ -59,22 +74,22 @@ class SpatialGrid {
|
|||
for (let i = startX; i <= endX; i++) {
|
||||
let gridX = this.cells.get(i);
|
||||
if (!gridX) {
|
||||
gridX = new Map<number, Set<string>>();
|
||||
gridX = new Map<number, Set<T>>();
|
||||
this.cells.set(i, gridX);
|
||||
}
|
||||
|
||||
for (let j = startY; j <= endY; j++) {
|
||||
let gridY = gridX.get(j);
|
||||
if (!gridY) {
|
||||
gridY = new Set<string>();
|
||||
gridY = new Set<T>();
|
||||
gridX.set(j, gridY);
|
||||
}
|
||||
gridY.add(node.id);
|
||||
gridY.add(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
remove(node: CanvasNode): void {
|
||||
remove(node: T): void {
|
||||
const startX = Math.floor(node.x / this.cellSize);
|
||||
const startY = Math.floor(node.y / this.cellSize);
|
||||
const endX = Math.ceil((node.x + node.width) / this.cellSize);
|
||||
|
|
@ -84,17 +99,17 @@ class SpatialGrid {
|
|||
const gridX = this.cells.get(i);
|
||||
if (gridX) {
|
||||
for (let j = startY; j <= endY; j++) {
|
||||
gridX.get(j)?.delete(node.id);
|
||||
gridX.get(j)?.delete(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fetch(x: number, y: number): Set<string> | undefined {
|
||||
fetch(x: number, y: number): Set<T> | undefined {
|
||||
return this.query(x, y, x, y);
|
||||
}
|
||||
|
||||
query(x1: number, y1: number, x2: number, y2: number): Set<string> {
|
||||
query(x1: number, y1: number, x2: number, y2: number): Set<T> {
|
||||
this.cacheSet.clear();
|
||||
|
||||
const startX = Math.floor(x1 / this.cellSize);
|
||||
|
|
@ -108,7 +123,7 @@ class SpatialGrid {
|
|||
for (let dy = startY; dy <= endY; dy++) {
|
||||
const cellNodes = gridX.get(dy);
|
||||
if (cellNodes) {
|
||||
cellNodes.forEach(neighbor => this.cacheSet.add(neighbor));
|
||||
cellNodes.forEach(neighbor => !this.cacheSet.has(neighbor) && (overlaps(x1, y1, x2 - x1, y2 - y1, neighbor.x, neighbor.y, neighbor.width, neighbor.height)) && this.cacheSet.add(neighbor));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -117,7 +132,7 @@ class SpatialGrid {
|
|||
return this.cacheSet;
|
||||
}
|
||||
|
||||
getViewportNeighbors(node: CanvasNode, viewport?: Box): Set<string> {
|
||||
getViewportNeighbors(node: T, viewport?: Box): Set<T> {
|
||||
this.cacheSet.clear();
|
||||
|
||||
const startX = Math.floor(node.x / this.cellSize);
|
||||
|
|
@ -127,8 +142,8 @@ class SpatialGrid {
|
|||
|
||||
const minX = Math.max(viewport ? Math.max(this.minx, Math.floor(viewport.x / this.cellSize)) : this.minx, startX - 8);
|
||||
const minY = Math.max(viewport ? Math.max(this.miny, Math.floor(viewport.y / this.cellSize)) : this.miny, startY - 8);
|
||||
const maxX = Math.min(viewport ? Math.min(this.maxx, Math.ceil((viewport.x + viewport.w) / this.cellSize)) : this.maxx, endX + 8);
|
||||
const maxY = Math.min(viewport ? Math.min(this.maxy, Math.ceil((viewport.y + viewport.h) / this.cellSize)) : this.maxy, endY + 8);
|
||||
const maxX = Math.min(viewport ? Math.min(this.maxx, Math.ceil((viewport.x + viewport.width) / this.cellSize)) : this.maxx, endX + 8);
|
||||
const maxY = Math.min(viewport ? Math.min(this.maxy, Math.ceil((viewport.y + viewport.height) / this.cellSize)) : this.maxy, endY + 8);
|
||||
|
||||
for (let dx = minX; dx <= maxX; dx++) {
|
||||
const gridX = this.cells.get(dx);
|
||||
|
|
@ -137,7 +152,7 @@ class SpatialGrid {
|
|||
const cellNodes = gridX.get(dy);
|
||||
if (cellNodes) {
|
||||
cellNodes.forEach(neighbor => {
|
||||
if (neighbor !== node.id) this.cacheSet.add(neighbor);
|
||||
if (neighbor !== node) this.cacheSet.add(neighbor);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -150,7 +165,7 @@ class SpatialGrid {
|
|||
const cellNodes = gridX.get(dy);
|
||||
if (cellNodes) {
|
||||
cellNodes.forEach(neighbor => {
|
||||
if (neighbor !== node.id) this.cacheSet.add(neighbor);
|
||||
if (neighbor !== node) this.cacheSet.add(neighbor);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -200,34 +215,33 @@ class SnapPointCache {
|
|||
}
|
||||
|
||||
export class SnapFinder {
|
||||
private spatialGrid: SpatialGrid;
|
||||
private spatialGrid: SpatialGrid<CanvasNode>;
|
||||
private snapPointCache: SnapPointCache;
|
||||
config: SnapConfig;
|
||||
private config: SnapConfig;
|
||||
|
||||
hints: Ref<SnapHint[]>;
|
||||
viewport: Ref<Box>;
|
||||
private canvas: CanvasEditor;
|
||||
private hints: SnapHint[] = [];
|
||||
|
||||
constructor(hints: Ref<SnapHint[]>, viewport: Ref<Box>, config: SnapConfig) {
|
||||
constructor(canvas: CanvasEditor, config: SnapConfig) {
|
||||
this.spatialGrid = new SpatialGrid(config.cellSize);
|
||||
this.snapPointCache = new SnapPointCache();
|
||||
this.config = config;
|
||||
|
||||
this.hints = hints;
|
||||
this.viewport = viewport;
|
||||
this.canvas = canvas;
|
||||
}
|
||||
|
||||
add(node: CanvasNode): void
|
||||
{
|
||||
this.spatialGrid.insert(node);
|
||||
this.snapPointCache.insert(node);
|
||||
this.hints.value.length = 0;
|
||||
this.hints.length = 0;
|
||||
}
|
||||
|
||||
remove(node: CanvasNode): void
|
||||
{
|
||||
this.spatialGrid.remove(node);
|
||||
this.snapPointCache.invalidate(node);
|
||||
this.hints.value.length = 0;
|
||||
this.hints.length = 0;
|
||||
}
|
||||
|
||||
update(node: CanvasNode): void
|
||||
|
|
@ -257,26 +271,26 @@ export class SnapFinder {
|
|||
const result: Partial<Box> = {
|
||||
x: undefined,
|
||||
y: undefined,
|
||||
w: undefined,
|
||||
h: undefined,
|
||||
width: undefined,
|
||||
height: undefined,
|
||||
};
|
||||
|
||||
if(!this.config.preferences.neighborSnap)
|
||||
{
|
||||
result.x = this.snapToGrid(node.x);
|
||||
result.w = this.snapToGrid(node.width);
|
||||
result.width = this.snapToGrid(node.width);
|
||||
result.y = this.snapToGrid(node.y);
|
||||
result.h = this.snapToGrid(node.height);
|
||||
result.height = this.snapToGrid(node.height);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
this.hints.value.length = 0;
|
||||
this.hints.length = 0;
|
||||
|
||||
this.snapPointCache.invalidate(node);
|
||||
this.snapPointCache.insert(node);
|
||||
|
||||
const neighbors = [...this.spatialGrid.getViewportNeighbors(node, this.viewport.value)].flatMap(e => this.snapPointCache.getSnapPoints(e)).filter(e => !!e);
|
||||
const neighbors = [...this.spatialGrid.getViewportNeighbors(node, this.canvas.viewport)].flatMap(e => this.snapPointCache.getSnapPoints(e)).filter(e => !!e);
|
||||
const bestSnap = this.findBestSnap(this.snapPointCache.getSnapPoints(node.id)!, neighbors, this.config.threshold, resizeHandle);
|
||||
|
||||
return this.applySnap(node, bestSnap.x, bestSnap.y, resizeHandle);
|
||||
|
|
@ -323,7 +337,7 @@ export class SnapFinder {
|
|||
yHints.forEach(e => e.start.x += bestSnap.x!);
|
||||
}
|
||||
|
||||
this.hints.value = [...xHints, ...yHints];
|
||||
this.hints = [...xHints, ...yHints];
|
||||
|
||||
return bestSnap;
|
||||
}
|
||||
|
|
@ -335,14 +349,14 @@ export class SnapFinder {
|
|||
|
||||
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, width: undefined, height: undefined };
|
||||
|
||||
if (resizeHandle)
|
||||
{
|
||||
result.x = offsetx ? node.x + offsetx * resizeHandle.x : this.snapToGrid(node.x);
|
||||
result.w = offsetx ? node.width + offsetx * resizeHandle.w : this.snapToGrid(node.width);
|
||||
result.w = offsetx ? node.width + offsetx * resizeHandle.width : this.snapToGrid(node.width);
|
||||
result.y = offsety ? node.y + offsety * resizeHandle.y : this.snapToGrid(node.y);
|
||||
result.h = offsety ? node.height - offsety * resizeHandle.h : this.snapToGrid(node.height);
|
||||
result.h = offsety ? node.height - offsety * resizeHandle.height : this.snapToGrid(node.height);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue