403 lines
12 KiB
JavaScript
Executable File
403 lines
12 KiB
JavaScript
Executable File
import { child, classAdd, classDel, classHas, listen, listenDel, svgEl } from '../infrastructure/util.js';
|
|
import { moveEvtProc, movementApplay } from '../infrastructure/move-evt-proc.js';
|
|
import { placeToCell, pointInCanvas } from '../infrastructure/move-scale-applay.js';
|
|
import { priorityElemFromPoint } from '../infrastructure/evt-route-applay.js';
|
|
import { ShapeSmbl } from './shape-smbl.js';
|
|
import { PathSettings } from './path-settings.js';
|
|
import { PathSmbl } from './path-smbl.js';
|
|
import { CanvasSmbl } from '../infrastructure/canvas-smbl.js';
|
|
import { modalCreate } from './modal-create.js';
|
|
import { canvasSelectionClearSet } from '../diagram/canvas-clear.js';
|
|
import { listenCopy } from '../diagram/group-select-applay.js';
|
|
|
|
/**
|
|
* @param {CanvasElement} canvas
|
|
* @param {PathData} pathData
|
|
*/
|
|
export function path(canvas, pathData) {
|
|
|
|
/** @type {PathElement} */
|
|
const svgGrp = svgEl('g', `
|
|
<path data-key="outer" d="M0 0" stroke="transparent" stroke-width="20" fill="none" />
|
|
<path data-key="path" class="path" d="M0 0" stroke="#495057" stroke-width="1.8" fill="none" style="pointer-events: none;" />
|
|
<path data-key="selected" d="M0 0" stroke="transparent" stroke-width="10" fill="none" style="pointer-events: none;" />
|
|
<g data-key="start">
|
|
<circle data-evt-index="1" class="path-end" r="10" stroke-width="0" fill="transparent" />
|
|
<path class="path" d="M-7 7 l 7 -7 l -7 -7" stroke="#495057" stroke-width="1.8" fill="none" style="pointer-events: none;"></path>
|
|
</g>
|
|
<g data-key="end">
|
|
<circle data-evt-index="1" class="path-end" r="10" stroke-width="0" fill="transparent" />
|
|
<path class="path" d="M-7 7 l 7 -7 l -7 -7" stroke="#495057" stroke-width="1.8" fill="none" style="pointer-events: none;"></path>
|
|
</g>`);
|
|
classAdd(svgGrp, 'shpath');
|
|
|
|
pathData.s.el = child(svgGrp, 'start');
|
|
pathData.e.el = child(svgGrp, 'end');
|
|
pathData.styles = pathData.styles ?? ['arw-e'];
|
|
const paths = childs(svgGrp, 'path', 'outer', 'selected');
|
|
|
|
function draw() {
|
|
const endDir = dirByAngle(pathData.s.data.position, pathData.e.data.position);
|
|
pathData.e.data.dir = endDir;
|
|
pathData.s.data.dir = dirReverse(endDir);
|
|
|
|
const dAttr = pathCalc(pathData);
|
|
paths.forEach(pp => pp.setAttribute('d', dAttr));
|
|
|
|
endDraw(pathData.s);
|
|
endDraw(pathData.e);
|
|
}
|
|
|
|
|
|
/** @param {PathEnd} pathEnd */
|
|
function pathDelFromShape(pathEnd) { shapeObj(pathEnd.shape)?.pathDel(svgGrp); }
|
|
|
|
/** @param {PathEnd} pathEnd */
|
|
function pathAddToShape(pathEnd) {
|
|
if (pathEnd.shape) {
|
|
pathEnd.data = shapeObj(pathEnd.shape).pathAdd(pathEnd.shape.connectorKey, svgGrp);
|
|
}
|
|
};
|
|
|
|
/** @type { {position:(bottomX:number, bottomY:number)=>void, del:()=>void} } */
|
|
let settingsPnl;
|
|
function del() {
|
|
unSelect();
|
|
reset();
|
|
pathDelFromShape(pathData.s);
|
|
pathDelFromShape(pathData.e);
|
|
svgGrp.remove();
|
|
}
|
|
|
|
/**
|
|
* @type {0|1|2}
|
|
* 0 - init, 1 - selected, 2 - edit
|
|
*/
|
|
let state = 0;
|
|
|
|
/** @type {()=>void} */
|
|
let listenCopyDispose;
|
|
|
|
/** @param {PointerEvent} evt */
|
|
function select(evt) {
|
|
// in edit mode
|
|
if (state === 2) { return; }
|
|
|
|
// to edit mode
|
|
if (state === 1) {
|
|
state = 2;
|
|
settingsPnl = modalCreate(evt.clientX - 10, evt.clientY - 10, new PathSettings(canvas, svgGrp));
|
|
return;
|
|
}
|
|
|
|
// to select mode
|
|
state = 1;
|
|
classAdd(svgGrp, 'select');
|
|
endSetEvtIndex(pathData.s, 2);
|
|
endSetEvtIndex(pathData.e, 2);
|
|
|
|
canvasSelectionClearSet(canvas, unSelect);
|
|
listenCopyDispose = listenCopy(() => [svgGrp]);
|
|
};
|
|
|
|
/** @type { {():void} } */
|
|
let hoverEmulateDispose;
|
|
function unSelect() {
|
|
state = 0;
|
|
classDel(svgGrp, 'select');
|
|
endSetEvtIndex(pathData.s, 1);
|
|
endSetEvtIndex(pathData.e, 1);
|
|
|
|
settingsPnl?.del(); settingsPnl = null;
|
|
|
|
if (hoverEmulateDispose) {
|
|
hoverEmulateDispose();
|
|
hoverEmulateDispose = null;
|
|
svgGrp.style.pointerEvents = 'unset';
|
|
}
|
|
|
|
canvasSelectionClearSet(canvas, null);
|
|
if (listenCopyDispose) { listenCopyDispose(); listenCopyDispose = null; }
|
|
};
|
|
|
|
/** @type {'s'|'e'} */
|
|
let movedEnd;
|
|
|
|
const reset = moveEvtProc(
|
|
canvas.ownerSVGElement,
|
|
svgGrp,
|
|
canvas[CanvasSmbl].data,
|
|
// data.end.position,
|
|
{
|
|
get x() { return pathData[movedEnd]?.data.position.x; },
|
|
set x(val) { if (movedEnd) { pathData[movedEnd].data.position.x = val; } },
|
|
|
|
get y() { return pathData[movedEnd]?.data.position.y; },
|
|
set y(val) { if (movedEnd) { pathData[movedEnd].data.position.y = val; } }
|
|
},
|
|
// onMoveStart
|
|
/** @param {PointerEvent & { target: Element} } evt */ evt => {
|
|
unSelect();
|
|
|
|
movedEnd = pathData.e.el.contains(evt.target) ? 'e' : pathData.s.el.contains(evt.target) ? 's' : null;
|
|
|
|
//
|
|
// move whole path
|
|
if (!movedEnd) {
|
|
return;
|
|
}
|
|
|
|
//
|
|
// move path end
|
|
|
|
// disconnect from shape
|
|
if (pathData[movedEnd].shape) {
|
|
if (pathData[movedEnd].shape.shapeEl !== pathData[movedEnd === 's' ? 'e' : 's'].shape?.shapeEl) {
|
|
pathDelFromShape(pathData[movedEnd]);
|
|
}
|
|
pathData[movedEnd].shape = null;
|
|
pathData[movedEnd].data = {
|
|
dir: pathData[movedEnd].data.dir,
|
|
position: pointInCanvas(canvas[CanvasSmbl].data, evt.clientX, evt.clientY)
|
|
};
|
|
}
|
|
|
|
// hover emulation - start
|
|
svgGrp.style.pointerEvents = 'none';
|
|
hoverEmulateDispose = hoverEmulate(svgGrp.parentElement);
|
|
},
|
|
// onMove
|
|
/** @param {PointerEventFixMovement} evt */
|
|
evt => {
|
|
if (!movedEnd) {
|
|
moveWholePath(canvas[CanvasSmbl].data, pathData, draw, evt);
|
|
} else {
|
|
const diagram = document.getElementById('diagram');
|
|
const rect = diagram.getBoundingClientRect();
|
|
pathData[movedEnd].data.position = {
|
|
x: evt.clientX - rect.left,
|
|
y: evt.clientY - rect.top
|
|
};
|
|
draw();
|
|
}
|
|
},
|
|
// onMoveEnd
|
|
evt => {
|
|
if (!movedEnd) {
|
|
moveWholePathFinish(canvas[CanvasSmbl].data, pathData, draw);
|
|
} else {
|
|
// connect to shape
|
|
const elemFromPoint = priorityElemFromPoint(evt);
|
|
const connectorKey = elemFromPoint?.getAttribute('data-connect');
|
|
if (connectorKey) {
|
|
// @ts-ignore
|
|
pathData[movedEnd].shape = { shapeEl: elemFromPoint.parentElement, connectorKey };
|
|
pathAddToShape(pathData[movedEnd]);
|
|
} else {
|
|
placeToCell(pathData[movedEnd].data.position, canvas[CanvasSmbl].data.cell);
|
|
}
|
|
draw();
|
|
}
|
|
|
|
// hover emulation - end
|
|
unSelect();
|
|
},
|
|
// onClick
|
|
select,
|
|
// onOutdown
|
|
unSelect
|
|
);
|
|
|
|
svgGrp[PathSmbl] = {
|
|
draw,
|
|
/** @param {PointerEventInit} evt */
|
|
pointerCapture: evt => pathData.e.el.dispatchEvent(new PointerEvent('pointerdown', evt)),
|
|
del,
|
|
data: pathData
|
|
};
|
|
|
|
if (pathData.styles) { classAdd(svgGrp, ...pathData.styles); }
|
|
pathAddToShape(pathData.s);
|
|
pathAddToShape(pathData.e);
|
|
draw();
|
|
|
|
return svgGrp;
|
|
}
|
|
|
|
/**
|
|
* @param {{scale:number}} canvasData
|
|
* @param {PathData} pathData
|
|
* @param {{():void}} draw
|
|
* @param {PointerEventFixMovement} evt
|
|
*/
|
|
function moveWholePath(canvasData, pathData, draw, evt) {
|
|
/** @param {Point} point */
|
|
const move = point => movementApplay(point, canvasData.scale, evt);
|
|
moveShapeOrEnd(pathData.s, move);
|
|
moveShapeOrEnd(pathData.e, move);
|
|
|
|
// if any shape connected - shape will draw connected path
|
|
if (!pathData.s.shape && !pathData.e.shape) { draw(); }
|
|
}
|
|
|
|
/**
|
|
* @param {{cell:number}} canvasData
|
|
* @param {PathData} pathData
|
|
* @param {{():void}} draw
|
|
*/
|
|
function moveWholePathFinish(canvasData, pathData, draw) {
|
|
/** @param {Point} point */
|
|
const toCell = point => placeToCell(point, canvasData.cell);
|
|
moveShapeOrEnd(pathData.s, toCell);
|
|
moveShapeOrEnd(pathData.e, toCell);
|
|
|
|
if (!pathData.s.shape || !pathData.e.shape) { draw(); }
|
|
}
|
|
|
|
/**
|
|
* applay moveFn to connected shape or to path end point
|
|
* @param {PathEnd} pathEnd, @param {{(point:Point):void}} moveFn */
|
|
function moveShapeOrEnd(pathEnd, moveFn) {
|
|
if (pathEnd.shape) {
|
|
moveFn(shapeObj(pathEnd.shape).data.position);
|
|
shapeObj(pathEnd.shape).drawPosition();
|
|
} else {
|
|
moveFn(pathEnd.data.position);
|
|
}
|
|
}
|
|
|
|
/** @param {PathConnectedShape} pathConnectedShape */
|
|
const shapeObj = pathConnectedShape => pathConnectedShape?.shapeEl[ShapeSmbl];
|
|
|
|
/** @param {PathEnd} pathEnd */
|
|
function endDraw(pathEnd) {
|
|
pathEnd.el.style.transform = `translate(${pathEnd.data.position.x}px, ${pathEnd.data.position.y}px) rotate(${arrowAngle(pathEnd.data.dir)}deg)`;
|
|
}
|
|
|
|
/** @param {PathEnd} pathEnd, @param {number} index */
|
|
function endSetEvtIndex(pathEnd, index) { pathEnd.el.firstElementChild.setAttribute('data-evt-index', index.toString()); }
|
|
|
|
/** @param {Dir} dir */
|
|
const arrowAngle = dir => dir === 'right'
|
|
? 180
|
|
: dir === 'left'
|
|
? 0
|
|
: dir === 'bottom'
|
|
? 270
|
|
: 90;
|
|
|
|
/** @param {Dir} dir, @return {Dir} */
|
|
export const dirReverse = dir => dir === 'left'
|
|
? 'right'
|
|
: dir === 'right'
|
|
? 'left'
|
|
: dir === 'top' ? 'bottom' : 'top';
|
|
|
|
/** @param {Point} s, @param {Point} e, @return {Dir} */
|
|
function dirByAngle(s, e) {
|
|
const rad = Math.atan2(e.y - s.y, e.x - s.x);
|
|
return numInRangeIncludeEnds(rad, -0.8, 0.8)
|
|
? 'left'
|
|
: numInRangeIncludeEnds(rad, 0.8, 2.4)
|
|
? 'top'
|
|
: numInRangeIncludeEnds(rad, 2.4, 3.2) || numInRangeIncludeEnds(rad, -3.2, -2.4) ? 'right' : 'bottom';
|
|
}
|
|
|
|
/** @param {PathData} data */
|
|
function pathCalc(data) {
|
|
let coef = Math.hypot(
|
|
data.s.data.position.x - data.e.data.position.x,
|
|
data.s.data.position.y - data.e.data.position.y) * 0.5;
|
|
coef = coef > 70
|
|
? 70
|
|
: coef < 15 ? 15 : coef;
|
|
|
|
/** @param {PathEndData} pathEnd */
|
|
function cx(pathEnd) {
|
|
return (pathEnd.dir === 'right' || pathEnd.dir === 'left')
|
|
? pathEnd.dir === 'right' ? pathEnd.position.x + coef : pathEnd.position.x - coef
|
|
: pathEnd.position.x;
|
|
}
|
|
|
|
/** @param {PathEndData} pathEnd */
|
|
function cy(pathEnd) {
|
|
return (pathEnd.dir === 'right' || pathEnd.dir === 'left')
|
|
? pathEnd.position.y
|
|
: pathEnd.dir === 'bottom' ? pathEnd.position.y + coef : pathEnd.position.y - coef;
|
|
}
|
|
|
|
return `M ${data.s.data.position.x} ${data.s.data.position.y} C ${cx(data.s.data)} ${cy(data.s.data)}, ` +
|
|
`${cx(data.e.data)} ${cy(data.e.data)}, ${data.e.data.position.x} ${data.e.data.position.y}`;
|
|
}
|
|
|
|
/** @param {Element} element */
|
|
function hoverEmulate(element) {
|
|
/** @type {Element} */
|
|
let elemFromPoint = null;
|
|
|
|
/** @param {PointerEvent} evt */
|
|
function move(evt) {
|
|
const elemFromPointNew = priorityElemFromPoint(evt);
|
|
if (elemFromPoint !== elemFromPointNew) {
|
|
if (classHas(elemFromPointNew, 'hovertrack')) {
|
|
classAdd(elemFromPointNew, 'hover');
|
|
}
|
|
let parentHover = false;
|
|
if (classHas(elemFromPointNew?.parentElement, 'hovertrack')) {
|
|
classAdd(elemFromPointNew.parentElement, 'hover');
|
|
parentHover = true;
|
|
}
|
|
|
|
classDel(elemFromPoint, 'hover');
|
|
if (elemFromPoint?.parentElement !== elemFromPointNew?.parentElement || !parentHover) {
|
|
classDel(elemFromPoint?.parentElement, 'hover');
|
|
}
|
|
|
|
elemFromPoint = elemFromPointNew;
|
|
}
|
|
}
|
|
|
|
listen(element, 'pointermove', move);
|
|
// dispose fn
|
|
return function() {
|
|
listenDel(element, 'pointermove', move);
|
|
classDel(elemFromPoint, 'hover');
|
|
classDel(elemFromPoint?.parentElement, 'hover');
|
|
elemFromPoint = null;
|
|
};
|
|
}
|
|
|
|
/** @param {Element} el, @param {...string} keys */
|
|
const childs = (el, ...keys) => keys.map(kk => child(el, kk));
|
|
|
|
/** @param {number} num, @param {number} a, @param {number} b */
|
|
const numInRangeIncludeEnds = (num, a, b) => a <= num && num <= b;
|
|
|
|
/** @typedef { {x:number, y:number} } Point */
|
|
/** @typedef { 'left' | 'right' | 'top' | 'bottom' } Dir */
|
|
/** @typedef { {shapeEl: ShapeElement, connectorKey: string} } PathConnectedShape */
|
|
/** @typedef { {position: Point, dir: Dir }} PathEndData */
|
|
/** @typedef { {shape?:PathConnectedShape, data?:PathEndData, el?:SVGElement} } PathEnd */
|
|
/**
|
|
@typedef {{
|
|
s: PathEnd,
|
|
e: PathEnd,
|
|
styles?: string[],
|
|
}} PathData
|
|
*/
|
|
/** @typedef { {shape?:PathConnectedShape, data?:PathEndData, oppositeShape?:PathConnectedShape, type:number} } MovedEnd */
|
|
/**
|
|
@typedef {{
|
|
draw(): void
|
|
pointerCapture: (evt:PointerEventInit)=>void
|
|
del(): void
|
|
data: PathData
|
|
}} Path
|
|
*/
|
|
|
|
/** @typedef { import('./path-smbl.js').PathElement } PathElement */
|
|
/** @typedef { import('../infrastructure/canvas-smbl.js').CanvasElement } CanvasElement */
|
|
/** @typedef { import('./shape-smbl').ShapeElement } ShapeElement */
|
|
/** @typedef { import('./shape-evt-proc').Shape } Shape */
|
|
/** @typedef { import('../infrastructure/move-evt-mobile-fix.js').PointerEventFixMovement } PointerEventFixMovement */
|