Добавляем все файлы
This commit is contained in:
26
main_plugin/dgrm/diagram/canvas-clear.js
Executable file
26
main_plugin/dgrm/diagram/canvas-clear.js
Executable file
@@ -0,0 +1,26 @@
|
||||
import { CanvasSmbl } from '../infrastructure/canvas-smbl.js';
|
||||
import { PathSmbl } from '../shapes/path-smbl.js';
|
||||
import { ShapeSmbl } from '../shapes/shape-smbl.js';
|
||||
|
||||
/** @param {CanvasElement} canvas */
|
||||
export function canvasClear(canvas) {
|
||||
while (canvas.firstChild) {
|
||||
(canvas.firstChild[ShapeSmbl] || canvas.firstChild[PathSmbl]).del();
|
||||
}
|
||||
canvas[CanvasSmbl].move(0, 0, 1);
|
||||
}
|
||||
|
||||
//
|
||||
// selection clear function
|
||||
|
||||
/** @param {CanvasElement} canvas */
|
||||
export function canvasSelectionClear(canvas) {
|
||||
if (canvas[CanvasSmbl].selectClear) { canvas[CanvasSmbl].selectClear(); };
|
||||
}
|
||||
|
||||
/** @param {CanvasElement} canvas, @param {()=>void} clearFn */
|
||||
export function canvasSelectionClearSet(canvas, clearFn) {
|
||||
canvas[CanvasSmbl].selectClear = clearFn;
|
||||
}
|
||||
|
||||
/** @typedef { import('../infrastructure/move-scale-applay.js').CanvasElement } CanvasElement */
|
||||
30
main_plugin/dgrm/diagram/dgrm-png.js
Executable file
30
main_plugin/dgrm/diagram/dgrm-png.js
Executable file
@@ -0,0 +1,30 @@
|
||||
import { CanvasSmbl } from '../infrastructure/canvas-smbl.js';
|
||||
|
||||
/**
|
||||
* @param {CanvasElement} canvas
|
||||
* @param {any} serializedData
|
||||
* @param {function(Blob):void} callBack
|
||||
*/
|
||||
export function fileSaveSvg(canvas, serializedData, callBack) {
|
||||
const svgVirtual = /** @type {SVGSVGElement} */(canvas.ownerSVGElement.cloneNode(true));
|
||||
|
||||
svgVirtual.style.backgroundImage = null;
|
||||
svgVirtual.querySelectorAll('.select, .highlight').forEach(el => el.classList.remove('select', 'highlight'));
|
||||
|
||||
const nonSvgElems = svgVirtual.getElementsByTagName('foreignObject');
|
||||
while (nonSvgElems[0]) { nonSvgElems[0].parentNode.removeChild(nonSvgElems[0]); }
|
||||
|
||||
/* svgVirtual.querySelectorAll('g.hovertrack.shtxt.ta-1').forEach(group => {
|
||||
group.querySelectorAll('text, tspan').forEach(el => console.log(el));
|
||||
}); */
|
||||
|
||||
const svgStr = new XMLSerializer().serializeToString(svgVirtual);
|
||||
const blob = new Blob([svgStr], { type: 'image/svg+xml;charset=utf-8' });
|
||||
callBack(blob);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/** @typedef { {x:number, y:number} } Point */
|
||||
/** @typedef { import('../infrastructure/canvas-smbl.js').CanvasElement } CanvasElement */
|
||||
108
main_plugin/dgrm/diagram/dgrm-serialization.js
Executable file
108
main_plugin/dgrm/diagram/dgrm-serialization.js
Executable file
@@ -0,0 +1,108 @@
|
||||
import { CanvasSmbl } from '../infrastructure/canvas-smbl.js';
|
||||
import { PathSmbl } from '../shapes/path-smbl.js';
|
||||
import { ShapeSmbl } from '../shapes/shape-smbl.js';
|
||||
import { canvasClear } from './canvas-clear.js';
|
||||
|
||||
const v = '1.1';
|
||||
|
||||
/** @param {Element} canvas */
|
||||
export const serialize = (canvas) => serializeShapes(/** @type {Array<ShapeElement & PathElement>} */([...canvas.children]));
|
||||
|
||||
/** @param {Array<ShapeElement & PathElement>} shapes */
|
||||
export function serializeShapes(shapes) {
|
||||
/** @type {DiagramSerialized} */
|
||||
const diagramSerialized = { v, s: [] };
|
||||
for (const shape of shapes) {
|
||||
if (shape[ShapeSmbl]) {
|
||||
// shape
|
||||
diagramSerialized.s.push(shape[ShapeSmbl].data);
|
||||
} else {
|
||||
// path
|
||||
|
||||
/** @param {PathEnd} pathEnd */
|
||||
function pathSerialize(pathEnd) {
|
||||
const shapeIndex = shapes.indexOf(pathEnd.shape?.shapeEl);
|
||||
return (shapeIndex !== -1)
|
||||
? { s: shapeIndex, k: pathEnd.shape.connectorKey }
|
||||
: { p: pathEnd.data };
|
||||
}
|
||||
|
||||
const pathData = shape[PathSmbl].data;
|
||||
const pathJson = { type: 0, s: pathSerialize(pathData.s), e: pathSerialize(pathData.e) };
|
||||
if (pathData.styles) { pathJson.c = pathData.styles; }
|
||||
|
||||
diagramSerialized.s.push(pathJson);
|
||||
}
|
||||
}
|
||||
|
||||
return diagramSerialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {CanvasElement} canvas
|
||||
* @param {DiagramSerialized} data
|
||||
* @param {Boolean=} dontClear
|
||||
*/
|
||||
export function deserialize(canvas, data, dontClear) {
|
||||
if (data.v !== v) { alert('Wrong format'); return null; }
|
||||
if (!dontClear) { canvasClear(canvas); }
|
||||
|
||||
/** @type {Map<ShapeData, ShapeElement>} */
|
||||
const shapeDataToElem = new Map();
|
||||
|
||||
/** @param {ShapeData} shapeData */
|
||||
function shapeEnsure(shapeData) {
|
||||
let shapeEl = shapeDataToElem.get(shapeData);
|
||||
if (!shapeEl) {
|
||||
shapeEl = canvas[CanvasSmbl].shapeMap[shapeData.type].create(shapeData);
|
||||
canvas.append(shapeEl);
|
||||
shapeDataToElem.set(shapeData, shapeEl);
|
||||
}
|
||||
return shapeEl;
|
||||
}
|
||||
|
||||
/** @param {number?} index */
|
||||
const shapeByIndex = index => shapeEnsure(/** @type {ShapeData} */(data.s[index]));
|
||||
|
||||
/** @type {PathElement[]} */
|
||||
const paths = [];
|
||||
for (const shape of data.s) {
|
||||
switch (shape.type) {
|
||||
// path
|
||||
case 0: {
|
||||
/** @param {PathEndSerialized} pathEnd */
|
||||
const pathDeserialize = pathEnd => pathEnd.p
|
||||
? { data: pathEnd.p }
|
||||
: { shape: { shapeEl: shapeByIndex(pathEnd.s), connectorKey: pathEnd.k } };
|
||||
|
||||
const path = canvas[CanvasSmbl].shapeMap[0].create({
|
||||
styles: /** @type {PathSerialized} */(shape).c,
|
||||
s: pathDeserialize(/** @type {PathSerialized} */(shape).s),
|
||||
e: pathDeserialize(/** @type {PathSerialized} */(shape).e)
|
||||
});
|
||||
paths.push(path);
|
||||
canvas.append(path);
|
||||
break;
|
||||
}
|
||||
default: shapeEnsure(/** @type {ShapeData} */(shape)); break;
|
||||
}
|
||||
}
|
||||
|
||||
return [...shapeDataToElem.values(), ...paths];
|
||||
}
|
||||
|
||||
/** @typedef {{v:string, s: Array<ShapeData | PathSerialized>}} DiagramSerialized */
|
||||
|
||||
/** @typedef { import("../shapes/shape-smbl").ShapeElement } ShapeElement */
|
||||
/** @typedef { import('../shapes/shape-evt-proc').ShapeData } ShapeData */
|
||||
|
||||
/** @typedef { import("../shapes/path-smbl").PathElement } PathElement */
|
||||
/** @typedef { import('../shapes/path').PathEndData } PathEndData */
|
||||
/** @typedef { import('../shapes/path').PathEnd } PathEnd */
|
||||
/** @typedef { import('../shapes/path').PathData } PathData */
|
||||
|
||||
/** @typedef { {s?:number, k?:string, p?:PathEndData} } PathEndSerialized */
|
||||
/** @typedef { {type:number, c?:string, s:PathEndSerialized, e:PathEndSerialized} } PathSerialized */
|
||||
|
||||
/** @typedef { import('../shapes/shape-evt-proc').CanvasData } CanvasData */
|
||||
/** @typedef { import('../infrastructure/canvas-smbl.js').CanvasElement } CanvasElement */
|
||||
32
main_plugin/dgrm/diagram/dgrm-srv.js
Executable file
32
main_plugin/dgrm/diagram/dgrm-srv.js
Executable file
@@ -0,0 +1,32 @@
|
||||
const svrApi = 'https://localhost:7156/api';
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {DiagramSerialized} serialized
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export async function srvSave(key, serialized) {
|
||||
return await fetch(`${svrApi}/${key}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json;charset=utf-8' },
|
||||
body: JSON.stringify(serialized)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* get diagram json by key
|
||||
* @param {string} key
|
||||
* @returns {Promise<DiagramSerialized>}
|
||||
*/
|
||||
export async function srvGet(key) {
|
||||
return (await fetch(`${svrApi}/${key}`)).json();
|
||||
}
|
||||
|
||||
export function generateKey() {
|
||||
const arr = new Uint8Array((8 / 2));
|
||||
window.crypto.getRandomValues(arr);
|
||||
const date = new Date();
|
||||
return `${date.getUTCFullYear()}${(date.getUTCMonth() + 1).toString().padStart(2, '0')}${Array.from(arr, dec => dec.toString(16).padStart(2, '0')).join('')}`;
|
||||
}
|
||||
|
||||
/** @typedef { import("./dgrm-serialization").DiagramSerialized } DiagramSerialized */
|
||||
61
main_plugin/dgrm/diagram/group-move.js
Executable file
61
main_plugin/dgrm/diagram/group-move.js
Executable file
@@ -0,0 +1,61 @@
|
||||
import { CanvasSmbl } from '../infrastructure/canvas-smbl.js';
|
||||
import { placeToCell, pointInCanvas } from '../infrastructure/move-scale-applay.js';
|
||||
import { pointShift } from '../infrastructure/util.js';
|
||||
|
||||
/** @param {CanvasElement} canvas, @param {DiagramSerialized} data */
|
||||
export function groupMoveToCenter(canvas, data) {
|
||||
const screenCenter = pointInCanvas(canvas[CanvasSmbl].data, window.innerWidth / 2, window.innerHeight / 2);
|
||||
placeToCell(screenCenter, canvas[CanvasSmbl].data.cell);
|
||||
|
||||
const shift = pointShift(screenCenter, centerCalc(data), -1);
|
||||
iteratePoints(data, point => { if (point) { pointShift(point, shift); } });
|
||||
}
|
||||
|
||||
/** @param {DiagramSerialized} data */
|
||||
function centerCalc(data) {
|
||||
const minMax = maxAndMinPoint(data);
|
||||
return {
|
||||
x: minMax.min.x + (minMax.max.x - minMax.min.x) / 2,
|
||||
y: minMax.min.y + (minMax.max.y - minMax.min.y) / 2
|
||||
};
|
||||
}
|
||||
|
||||
/** @param {DiagramSerialized} data */
|
||||
function maxAndMinPoint(data) {
|
||||
/** @type {Point} */
|
||||
const min = { x: Infinity, y: Infinity };
|
||||
|
||||
/** @type {Point} */
|
||||
const max = { x: -Infinity, y: -Infinity };
|
||||
|
||||
iteratePoints(data, point => {
|
||||
if (!point) { return; }
|
||||
|
||||
if (min.x > point.x) { min.x = point.x; }
|
||||
if (min.y > point.y) { min.y = point.y; }
|
||||
|
||||
if (max.x < point.x) { max.x = point.x; }
|
||||
if (max.y < point.y) { max.y = point.y; }
|
||||
});
|
||||
return { min, max };
|
||||
}
|
||||
|
||||
/** @param {DiagramSerialized} data, @param {(point:Point)=>void} callbackfn */
|
||||
function iteratePoints(data, callbackfn) {
|
||||
data.s.forEach(shapeOrPath => {
|
||||
if (shapeOrPath.type === 0) {
|
||||
// path
|
||||
callbackfn(/** @type {PathSerialized} */(shapeOrPath).s.p?.position);
|
||||
callbackfn(/** @type {PathSerialized} */(shapeOrPath).e.p?.position);
|
||||
} else {
|
||||
// shape
|
||||
callbackfn(/** @type {ShapeData} */(shapeOrPath).position);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** @typedef { {x:number, y:number} } Point */
|
||||
/** @typedef { import('../infrastructure/canvas-smbl.js').CanvasElement } CanvasElement */
|
||||
/** @typedef { import('./dgrm-serialization.js').DiagramSerialized } DiagramSerialized */
|
||||
/** @typedef { import('./dgrm-serialization.js').PathSerialized } PathSerialized */
|
||||
/** @typedef { import('../shapes/shape-evt-proc.js').ShapeData } ShapeData */
|
||||
396
main_plugin/dgrm/diagram/group-select-applay.js
Executable file
396
main_plugin/dgrm/diagram/group-select-applay.js
Executable file
@@ -0,0 +1,396 @@
|
||||
import { CanvasSmbl } from '../infrastructure/canvas-smbl.js';
|
||||
import { movementApplay, ProcessedSmbl, shapeSelect } from '../infrastructure/move-evt-proc.js';
|
||||
import { placeToCell, pointInCanvas } from '../infrastructure/move-scale-applay.js';
|
||||
import { arrPop, classAdd, classDel, deepCopy, listen, listenDel, positionSet, svgEl } from '../infrastructure/util.js';
|
||||
import { PathSmbl } from '../shapes/path-smbl.js';
|
||||
import { ShapeSmbl } from '../shapes/shape-smbl.js';
|
||||
import { GroupSettings } from './group-settings.js';
|
||||
import { modalCreate } from '../shapes/modal-create.js';
|
||||
import { groupMoveToCenter } from './group-move.js';
|
||||
import { deserialize, serializeShapes } from './dgrm-serialization.js';
|
||||
import { canvasSelectionClear, canvasSelectionClearSet } from './canvas-clear.js';
|
||||
import { tipShow } from '../ui/ui.js';
|
||||
|
||||
//
|
||||
// copy past
|
||||
|
||||
const clipboardDataKey = 'dgrm';
|
||||
|
||||
/** @param {() => Array<ShapeElement & PathElement>} shapesToClipboardGetter */
|
||||
export function listenCopy(shapesToClipboardGetter) {
|
||||
/** @param {ClipboardEvent & {target:HTMLElement | SVGElement}} evt */
|
||||
function onCopy(evt) {
|
||||
const shapes = shapesToClipboardGetter();
|
||||
if (document.activeElement === shapes[0].ownerSVGElement) {
|
||||
evt.preventDefault();
|
||||
evt.clipboardData.setData(
|
||||
clipboardDataKey,
|
||||
JSON.stringify(copyDataCreate(shapes)));
|
||||
}
|
||||
}
|
||||
document.addEventListener('copy', onCopy);
|
||||
|
||||
// dispose fn
|
||||
return function() {
|
||||
listenDel(document, 'copy', onCopy);
|
||||
};
|
||||
}
|
||||
|
||||
/** @param {CanvasElement} canvas */
|
||||
export function copyPastApplay(canvas) {
|
||||
listen(document, 'paste', /** @param {ClipboardEvent & {target:HTMLElement | SVGElement}} evt */ evt => {
|
||||
if (evt.target.tagName.toUpperCase() === 'TEXTAREA') { return; }
|
||||
// if (document.activeElement !== canvas.ownerSVGElement) { return; }
|
||||
|
||||
const dataStr = evt.clipboardData.getData(clipboardDataKey);
|
||||
if (!dataStr) { return; }
|
||||
|
||||
tipShow(false);
|
||||
canvasSelectionClear(canvas);
|
||||
past(canvas, JSON.parse(dataStr));
|
||||
});
|
||||
}
|
||||
|
||||
/** @param {CanvasElement} canvas, @param {Array<ShapeElement & PathElement>} shapes */
|
||||
export const copyAndPast = (canvas, shapes) => past(canvas, copyDataCreate(shapes));
|
||||
|
||||
/** @param {Array<ShapeElement & PathElement>} shapes */
|
||||
const copyDataCreate = shapes => deepCopy(serializeShapes(shapes));
|
||||
|
||||
/** @param {CanvasElement} canvas, @param {DiagramSerialized} data */
|
||||
function past(canvas, data) {
|
||||
canvasSelectionClear(canvas);
|
||||
groupMoveToCenter(canvas, data);
|
||||
groupSelect(canvas, deserialize(canvas, data, true));
|
||||
}
|
||||
|
||||
//
|
||||
// group select
|
||||
|
||||
const highlightSClass = 'highlight-s';
|
||||
const highlightEClass = 'highlight-e';
|
||||
const highlightClass = 'highlight';
|
||||
|
||||
/** wait long press and draw selected rectangle
|
||||
* @param {CanvasElement} canvas
|
||||
*/
|
||||
export function groupSelectApplay(canvas) {
|
||||
const svg = canvas.ownerSVGElement;
|
||||
let timer;
|
||||
/** @type {Point} */ let selectStart;
|
||||
/** @type {SVGCircleElement} */ let startCircle;
|
||||
/** @type {SVGRectElement} */ let selectRect;
|
||||
/** @type {Point} */ let selectRectPos;
|
||||
|
||||
/** @param {PointerEvent} evt */
|
||||
function onMove(evt) {
|
||||
if (evt[ProcessedSmbl] || !selectRect) { reset(); return; }
|
||||
evt[ProcessedSmbl] = true;
|
||||
|
||||
if (startCircle) { startCircle.remove(); startCircle = null; }
|
||||
|
||||
// draw rect
|
||||
const x = evt.clientX - selectStart.x;
|
||||
const y = evt.clientY - selectStart.y;
|
||||
selectRect.width.baseVal.value = Math.abs(x);
|
||||
selectRect.height.baseVal.value = Math.abs(y);
|
||||
if (x < 0) { selectRectPos.x = evt.clientX; }
|
||||
if (y < 0) { selectRectPos.y = evt.clientY; }
|
||||
selectRect.style.transform = `translate(${selectRectPos.x}px, ${selectRectPos.y}px)`;
|
||||
}
|
||||
|
||||
function onUp() {
|
||||
if (selectRect) {
|
||||
/** @param {Point} point */
|
||||
const inRect = point => pointInRect(
|
||||
pointInCanvas(canvas[CanvasSmbl].data, selectRectPos.x, selectRectPos.y),
|
||||
selectRect.width.baseVal.value / canvas[CanvasSmbl].data.scale,
|
||||
selectRect.height.baseVal.value / canvas[CanvasSmbl].data.scale,
|
||||
point.x, point.y);
|
||||
|
||||
// select shapes in rect
|
||||
groupSelect(
|
||||
canvas,
|
||||
/** @type {Iterable<ShapeOrPathElement>} */(canvas.children),
|
||||
inRect);
|
||||
}
|
||||
|
||||
reset();
|
||||
}
|
||||
|
||||
function reset() {
|
||||
clearTimeout(timer); timer = null;
|
||||
startCircle?.remove(); startCircle = null;
|
||||
selectRect?.remove(); selectRect = null;
|
||||
|
||||
listenDel(svg, 'pointermove', onMove);
|
||||
listenDel(svg, 'wheel', reset);
|
||||
listenDel(svg, 'pointerup', onUp);
|
||||
}
|
||||
|
||||
listen(svg, 'pointerdown', /** @param {PointerEvent} evt */ evt => {
|
||||
if (evt[ProcessedSmbl] || !evt.isPrimary) { reset(); return; }
|
||||
|
||||
listen(svg, 'pointermove', onMove);
|
||||
listen(svg, 'wheel', reset, true);
|
||||
listen(svg, 'pointerup', onUp, true);
|
||||
|
||||
timer = setTimeout(_ => {
|
||||
canvasSelectionClear(canvas);
|
||||
|
||||
startCircle = svgEl('circle');
|
||||
classAdd(startCircle, 'ative-elem');
|
||||
startCircle.style.cssText = 'r:10px; fill: rgb(108 187 247 / 51%)';
|
||||
positionSet(startCircle, { x: evt.clientX, y: evt.clientY });
|
||||
svg.append(startCircle);
|
||||
|
||||
selectStart = { x: evt.clientX, y: evt.clientY };
|
||||
selectRectPos = { x: evt.clientX, y: evt.clientY };
|
||||
selectRect = svgEl('rect');
|
||||
selectRect.style.cssText = 'rx:10px; fill: rgb(108 187 247 / 51%)';
|
||||
positionSet(selectRect, selectRectPos);
|
||||
svg.append(selectRect);
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlight selected shapes and procces group operations (move, del, copy)
|
||||
* @param {CanvasElement} canvas
|
||||
* @param {Iterable<ShapeOrPathElement>} elems
|
||||
* @param {{(position:Point):boolean}=} inRect
|
||||
*/
|
||||
export function groupSelect(canvas, elems, inRect) {
|
||||
/** @param {{position:Point}} data */
|
||||
const shapeInRect = data => inRect ? inRect(data.position) : true;
|
||||
|
||||
/** @type {Selected} */
|
||||
const selected = {
|
||||
shapes: [],
|
||||
shapesPaths: [],
|
||||
pathEnds: [],
|
||||
pathEndsPaths: []
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {ShapeOrPathElement} pathEl, @param {PathEnd} pathEnd, @param {string} highlightClass
|
||||
* @returns {1|2|0}
|
||||
*/
|
||||
function pathEndInRect(pathEl, pathEnd, highlightClass) {
|
||||
if (!pathEnd.shape && shapeInRect(pathEnd.data)) {
|
||||
selected.pathEnds.push(pathEnd);
|
||||
classAdd(pathEl, highlightClass);
|
||||
return 1; // connect to end in rect
|
||||
} else if (pathEnd.shape && shapeInRect(pathEnd.shape.shapeEl[ShapeSmbl].data)) {
|
||||
return 2; // connect to shape in rect
|
||||
}
|
||||
return 0; // not in rect
|
||||
}
|
||||
|
||||
for (const shapeEl of elems) {
|
||||
if (shapeEl[ShapeSmbl]) {
|
||||
if (shapeInRect(shapeEl[ShapeSmbl].data)) {
|
||||
classAdd(shapeEl, highlightClass);
|
||||
selected.shapes.push(shapeEl);
|
||||
}
|
||||
} else if (shapeEl[PathSmbl]) {
|
||||
const isStartIn = pathEndInRect(shapeEl, shapeEl[PathSmbl].data.s, highlightSClass);
|
||||
const isEndIn = pathEndInRect(shapeEl, shapeEl[PathSmbl].data.e, highlightEClass);
|
||||
|
||||
if (isStartIn === 1 || isEndIn === 1) {
|
||||
selected.pathEndsPaths.push(shapeEl);
|
||||
}
|
||||
|
||||
if (isStartIn === 2 || isEndIn === 2) {
|
||||
selected.shapesPaths.push(shapeEl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
groupEvtProc(canvas, selected);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {CanvasElement} canvas
|
||||
* @param {Selected} selected
|
||||
*/
|
||||
function groupEvtProc(canvas, selected) {
|
||||
// only one shape selected
|
||||
if (selected.shapes?.length === 1 && !selected.pathEnds?.length) {
|
||||
classDel(selected.shapes[0], 'highlight');
|
||||
shapeSelect(selected.shapes[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
// only one pathEnd selected
|
||||
if (!selected.shapes?.length && selected.pathEnds?.length === 1) {
|
||||
pathUnhighlight(selected.pathEndsPaths[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
// only one path selected
|
||||
if (!selected.shapes?.length && selected.pathEnds?.length === 2 && selected.pathEndsPaths?.length === 1) {
|
||||
pathUnhighlight(selected.pathEndsPaths[0]);
|
||||
shapeSelect(selected.pathEndsPaths[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
const svg = canvas.ownerSVGElement;
|
||||
let isMove = false;
|
||||
let isDownOnSelectedShape = false;
|
||||
|
||||
/** @type {{del():void}} */
|
||||
let settingsPnl;
|
||||
const pnlDel = () => { settingsPnl?.del(); settingsPnl = null; };
|
||||
|
||||
/** @param {PointerEvent & {target:Node}} evt */
|
||||
function down(evt) {
|
||||
pnlDel();
|
||||
isDownOnSelectedShape =
|
||||
selected.shapes?.some(shapeEl => shapeEl.contains(evt.target)) ||
|
||||
selected.pathEnds?.some(pathEnd => pathEnd.el.contains(evt.target));
|
||||
|
||||
// down on not selected shape
|
||||
if (!isDownOnSelectedShape && evt.target !== svg) {
|
||||
dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDownOnSelectedShape) {
|
||||
evt.stopImmediatePropagation();
|
||||
}
|
||||
|
||||
svg.setPointerCapture(evt.pointerId);
|
||||
listen(svg, 'pointerup', up, true);
|
||||
listen(svg, 'pointermove', move);
|
||||
}
|
||||
|
||||
/** @param { {(point:Point):void} } pointMoveFn */
|
||||
function drawSelection(pointMoveFn) {
|
||||
selected.shapes?.forEach(shapeEl => {
|
||||
pointMoveFn(shapeEl[ShapeSmbl].data.position);
|
||||
shapeEl[ShapeSmbl].drawPosition();
|
||||
});
|
||||
selected.pathEnds?.forEach(pathEnd => pointMoveFn(pathEnd.data.position));
|
||||
selected.pathEndsPaths?.forEach(path => path[PathSmbl].draw());
|
||||
}
|
||||
|
||||
/** @param {PointerEvent} evt */
|
||||
function up(evt) {
|
||||
if (!isMove) {
|
||||
// click on canvas
|
||||
if (!isDownOnSelectedShape) { dispose(); return; }
|
||||
|
||||
// click on selected shape - show settings panel
|
||||
settingsPnl = modalCreate(evt.clientX - 10, evt.clientY - 10, new GroupSettings(cmd => {
|
||||
switch (cmd) {
|
||||
case 'del':
|
||||
arrPop(selected.shapes, shapeEl => shapeEl[ShapeSmbl].del());
|
||||
arrPop(selected.pathEndsPaths, pathEl => pathEl[PathSmbl].del());
|
||||
dispose();
|
||||
break;
|
||||
case 'copy': {
|
||||
copyAndPast(canvas, elemsToCopyGet(selected)); // will call dispose
|
||||
break;
|
||||
}
|
||||
}
|
||||
}));
|
||||
} else {
|
||||
// move end
|
||||
drawSelection(point => placeToCell(point, canvas[CanvasSmbl].data.cell));
|
||||
}
|
||||
|
||||
dispose(true);
|
||||
}
|
||||
|
||||
/** @param {PointerEventFixMovement} evt */
|
||||
function move(evt) {
|
||||
// move canvas
|
||||
if (!isDownOnSelectedShape) { dispose(true); return; }
|
||||
|
||||
// move selected shapes
|
||||
isMove = true;
|
||||
drawSelection(point => movementApplay(point, canvas[CanvasSmbl].data.scale, evt));
|
||||
}
|
||||
|
||||
/** @param {boolean=} saveOnDown */
|
||||
function dispose(saveOnDown) {
|
||||
listenDel(svg, 'pointerup', up);
|
||||
listenDel(svg, 'pointermove', move);
|
||||
isMove = false;
|
||||
isDownOnSelectedShape = false;
|
||||
|
||||
if (!saveOnDown) {
|
||||
canvasSelectionClearSet(canvas, null);
|
||||
if (listenCopyDispose) { listenCopyDispose(); listenCopyDispose = null; }
|
||||
|
||||
listenDel(svg, 'pointerdown', down, true);
|
||||
pnlDel();
|
||||
arrPop(selected.shapes, shapeEl => classDel(shapeEl, highlightClass));
|
||||
arrPop(selected.pathEndsPaths, pathEl => pathUnhighlight(pathEl));
|
||||
selected.pathEnds = null;
|
||||
selected.shapesPaths = null;
|
||||
}
|
||||
}
|
||||
|
||||
svg.addEventListener('pointerdown', down, { passive: true, capture: true });
|
||||
|
||||
canvasSelectionClearSet(canvas, dispose);
|
||||
let listenCopyDispose = listenCopy(() => elemsToCopyGet(selected));
|
||||
}
|
||||
|
||||
/** @param {Selected} selected */
|
||||
function elemsToCopyGet(selected) {
|
||||
/** @type {Set<PathElement>} */
|
||||
const fullSelectedPaths = new Set();
|
||||
|
||||
/** @param {PathEnd} pathEnd */
|
||||
const pathEndSelected = pathEnd =>
|
||||
selected.shapes.includes(pathEnd.shape?.shapeEl) || selected.pathEnds.includes(pathEnd);
|
||||
|
||||
/** @param {PathElement} pathEl */
|
||||
function fullSelectedPathAdd(pathEl) {
|
||||
if (pathEndSelected(pathEl[PathSmbl].data.s) && pathEndSelected(pathEl[PathSmbl].data.e)) {
|
||||
fullSelectedPaths.add(pathEl);
|
||||
}
|
||||
}
|
||||
|
||||
selected.shapesPaths?.forEach(fullSelectedPathAdd);
|
||||
selected.pathEndsPaths?.forEach(fullSelectedPathAdd);
|
||||
|
||||
return [...selected.shapes, ...fullSelectedPaths];
|
||||
}
|
||||
|
||||
/** @param {PathElement} pathEl`` */
|
||||
function pathUnhighlight(pathEl) {
|
||||
classDel(pathEl, highlightSClass);
|
||||
classDel(pathEl, highlightEClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Point} rectPosition
|
||||
* @param {number} rectWidth, @param {number} rectHeight
|
||||
* @param {number} x, @param {number} y
|
||||
*/
|
||||
const pointInRect = (rectPosition, rectWidth, rectHeight, x, y) =>
|
||||
rectPosition.x <= x && x <= rectPosition.x + rectWidth &&
|
||||
rectPosition.y <= y && y <= rectPosition.y + rectHeight;
|
||||
|
||||
/**
|
||||
* @typedef { {
|
||||
* shapes:ShapeElement[]
|
||||
* shapesPaths:PathElement[]
|
||||
* pathEnds: PathEnd[]
|
||||
* pathEndsPaths: PathElement[]
|
||||
* } } Selected
|
||||
*/
|
||||
/** @typedef { {x:number, y:number} } Point */
|
||||
/** @typedef { import('../infrastructure/canvas-smbl.js').CanvasElement } CanvasElement */
|
||||
/** @typedef { import('../shapes/shape-smbl').ShapeElement } ShapeElement */
|
||||
/** @typedef { import('../shapes/shape-evt-proc').Shape } Shape */
|
||||
/** @typedef { import('../shapes/path').Path } Path */
|
||||
/** @typedef { import('../shapes/path').PathEnd } PathEnd */
|
||||
/** @typedef { import('../shapes/path-smbl').PathElement } PathElement */
|
||||
/** @typedef { SVGGraphicsElement & { [ShapeSmbl]?: Shape, [PathSmbl]?:Path }} ShapeOrPathElement */
|
||||
/** @typedef { import('../infrastructure/move-evt-mobile-fix.js').PointerEventFixMovement} PointerEventFixMovement */
|
||||
/** @typedef { import('./dgrm-serialization.js').DiagramSerialized } DiagramSerialized */
|
||||
32
main_plugin/dgrm/diagram/group-settings.js
Executable file
32
main_plugin/dgrm/diagram/group-settings.js
Executable file
@@ -0,0 +1,32 @@
|
||||
import { copySvg, delSvg } from '../infrastructure/assets.js';
|
||||
import { clickForAll, evtTargetAttr } from '../infrastructure/util.js';
|
||||
|
||||
export class GroupSettings extends HTMLElement {
|
||||
/** @param {(cms:string)=>void} cmdHandler */
|
||||
constructor(cmdHandler) {
|
||||
super();
|
||||
/** @private */
|
||||
this._cmdHandler = cmdHandler;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
const shadow = this.attachShadow({ mode: 'closed' });
|
||||
shadow.innerHTML = `
|
||||
<style>
|
||||
.ln { display: flex; }
|
||||
.ln > * {
|
||||
height: 24px;
|
||||
padding: 10px;
|
||||
}
|
||||
[data-cmd] { cursor: pointer; }
|
||||
</style>
|
||||
<div class="ln">
|
||||
${copySvg}
|
||||
${delSvg}
|
||||
</div>`;
|
||||
|
||||
clickForAll(shadow, '[data-cmd]',
|
||||
evt => this._cmdHandler(evtTargetAttr(evt, 'data-cmd')));
|
||||
}
|
||||
}
|
||||
customElements.define('ap-grp-settings', GroupSettings);
|
||||
Reference in New Issue
Block a user