Добавляем все файлы
This commit is contained in:
69
main_plugin/dgrm/shapes/circle.js
Executable file
69
main_plugin/dgrm/shapes/circle.js
Executable file
@@ -0,0 +1,69 @@
|
||||
import { ceil, child, positionSet, svgTxtFarthestPoint } from '../infrastructure/util.js';
|
||||
import { shapeCreate } from './shape-evt-proc.js';
|
||||
|
||||
/**
|
||||
* @param {CanvasElement} canvas
|
||||
* @param {CircleData} circleData
|
||||
*/
|
||||
export function circle(canvas, circleData) {
|
||||
const templ = `
|
||||
<circle data-key="outer" data-evt-no data-evt-index="2" r="72" fill="transparent" stroke-width="0" />
|
||||
<circle data-key="main" r="48" fill="#ff6600" stroke="#fff" stroke-width="1" />
|
||||
<text data-key="text" x="0" y="0" text-anchor="middle" style="pointer-events: none;" fill="#fff"> </text>`;
|
||||
|
||||
const shape = shapeCreate(canvas, circleData, templ,
|
||||
{
|
||||
right: { dir: 'right', position: { x: 48, y: 0 } },
|
||||
left: { dir: 'left', position: { x: -48, y: 0 } },
|
||||
bottom: { dir: 'bottom', position: { x: 0, y: 48 } },
|
||||
top: { dir: 'top', position: { x: 0, y: -48 } }
|
||||
},
|
||||
// onTextChange
|
||||
txtEl => {
|
||||
const newRadius = textElRadius(txtEl, 48, 24);
|
||||
if (newRadius !== circleData.r) {
|
||||
circleData.r = newRadius;
|
||||
resize();
|
||||
}
|
||||
});
|
||||
|
||||
function resize() {
|
||||
shape.cons.right.position.x = circleData.r;
|
||||
shape.cons.left.position.x = -circleData.r;
|
||||
shape.cons.bottom.position.y = circleData.r;
|
||||
shape.cons.top.position.y = -circleData.r;
|
||||
|
||||
for (const connectorKey in shape.cons) {
|
||||
positionSet(child(shape.el, connectorKey), shape.cons[connectorKey].position);
|
||||
}
|
||||
|
||||
radiusSet(shape.el, 'outer', circleData.r + 24);
|
||||
radiusSet(shape.el, 'main', circleData.r);
|
||||
shape.draw();
|
||||
}
|
||||
|
||||
if (!!circleData.r && circleData.r !== 48) { resize(); } else { shape.draw(); }
|
||||
|
||||
return shape.el;
|
||||
}
|
||||
|
||||
/** @param {Element} svgGrp, @param {string} key, @param {number} r */
|
||||
function radiusSet(svgGrp, key, r) { /** @type {SVGCircleElement} */(child(svgGrp, key)).r.baseVal.value = r; }
|
||||
|
||||
/**
|
||||
* calc radius that cover all <tspan> in SVGTextElement
|
||||
* origin is in the center of the circle
|
||||
* @param {SVGTextElement} textEl
|
||||
* @param {*} minR
|
||||
* @param {*} step
|
||||
*/
|
||||
function textElRadius(textEl, minR, step) {
|
||||
const farthestPoint = svgTxtFarthestPoint(textEl);
|
||||
return ceil(minR, step, Math.sqrt(farthestPoint.x ** 2 + farthestPoint.y ** 2));
|
||||
}
|
||||
|
||||
/** @typedef { {x:number, y:number} } Point */
|
||||
/** @typedef { import('../infrastructure/canvas-smbl.js').CanvasElement } CanvasElement */
|
||||
/** @typedef { import('./shape-evt-proc').CanvasData } CanvasData */
|
||||
/** @typedef { import('./shape-evt-proc').ConnectorsData } ConnectorsData */
|
||||
/** @typedef { {type:number, position: Point, title?: string, styles?: string[], r?:number} } CircleData */
|
||||
27
main_plugin/dgrm/shapes/modal-create.js
Executable file
27
main_plugin/dgrm/shapes/modal-create.js
Executable file
@@ -0,0 +1,27 @@
|
||||
/** @type {HTMLDivElement} */
|
||||
let editModalDiv;
|
||||
|
||||
/** @param {number} bottomX, @param {number} bottomY, @param {HTMLElement} elem */
|
||||
export function modalCreate(bottomX, bottomY, elem) {
|
||||
editModalDiv = document.createElement('div');
|
||||
editModalDiv.style.cssText = 'position: fixed; box-shadow: 0px 0px 58px 2px rgb(34 60 80 / 20%); border-radius: 16px; background-color: rgba(255,255,255, .9);';
|
||||
editModalDiv.append(elem);
|
||||
document.body.append(editModalDiv);
|
||||
|
||||
function position(btmX, btmY) {
|
||||
editModalDiv.style.left = `${btmX}px`;
|
||||
editModalDiv.style.top = `${btmY - 35}px`;
|
||||
}
|
||||
|
||||
position(bottomX, bottomY);
|
||||
|
||||
return {
|
||||
position,
|
||||
del: () => { editModalDiv.remove(); editModalDiv = null; }
|
||||
};
|
||||
}
|
||||
|
||||
/** @param {number} dif */
|
||||
export function modalChangeTop(dif) {
|
||||
editModalDiv.style.top = `${editModalDiv.getBoundingClientRect().top - 90}px`;
|
||||
}
|
||||
75
main_plugin/dgrm/shapes/path-settings.js
Executable file
75
main_plugin/dgrm/shapes/path-settings.js
Executable file
@@ -0,0 +1,75 @@
|
||||
import { copyAndPast } from '../diagram/group-select-applay.js';
|
||||
import { classAdd, classDel, clickForAll, listen, classSingleAdd, evtTargetAttr } from '../infrastructure/util.js';
|
||||
import { PathSmbl } from './path-smbl.js';
|
||||
|
||||
export class PathSettings extends HTMLElement {
|
||||
/**
|
||||
* @param {CanvasElement} canvas
|
||||
* @param {PathElement} pathElement
|
||||
*/
|
||||
constructor(canvas, pathElement) {
|
||||
super();
|
||||
/** @private */
|
||||
this._pathElement = pathElement;
|
||||
|
||||
/** @private */
|
||||
this._canvas = canvas;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
const pathStyles = this._pathElement[PathSmbl].data.styles;
|
||||
const actStyle = style => this._pathElement[PathSmbl].data.styles?.includes(style) ? 'class="actv"' : '';
|
||||
|
||||
const shadow = this.attachShadow({ mode: 'closed' });
|
||||
shadow.innerHTML = `
|
||||
<style>
|
||||
.ln { display: flex; }
|
||||
.ln > * {
|
||||
height: 24px;
|
||||
padding: 10px;
|
||||
fill-opacity: 0.3;
|
||||
stroke-opacity: 0.3;
|
||||
}
|
||||
[data-cmd] { cursor: pointer; }
|
||||
.actv {
|
||||
fill-opacity: 1;
|
||||
stroke-opacity: 1;
|
||||
}
|
||||
</style>
|
||||
<ap-shape-edit id="edit" edit-btn="true">
|
||||
<div class="ln">
|
||||
<svg data-cmd data-cmd-arg="arw-s" ${actStyle('arw-s')} viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M7.828 11H20v2H7.828l5.364 5.364-1.414 1.414L4 12l7.778-7.778 1.414 1.414z" fill="rgb(52,71,103)"/></svg>
|
||||
<svg data-cmd data-cmd-arg="arw-e" ${actStyle('arw-e')} viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M16.172 11l-5.364-5.364 1.414-1.414L20 12l-7.778 7.778-1.414-1.414L16.172 13H4v-2z" fill="rgb(52,71,103)"/></svg>
|
||||
<svg data-cmd data-cmd-arg="dash" ${actStyle('dash')} viewBox="0 0 24 24" width="24" height="24"><path d="M 2,11 L 20,11" stroke="rgb(52,71,103)" style="stroke-dasharray: 4,3; stroke-width: 3;"></path></svg>
|
||||
</div>
|
||||
</ap-shape-edit>`;
|
||||
|
||||
// colors, del
|
||||
listen(shadow.getElementById('edit'), 'cmd', /** @param {CustomEvent<{cmd:string, arg:string}>} evt */ evt => {
|
||||
switch (evt.detail.cmd) {
|
||||
case 'style': classSingleAdd(this._pathElement, this._pathElement[PathSmbl].data, 'cl-', evt.detail.arg); break;
|
||||
case 'del': this._pathElement[PathSmbl].del(); break;
|
||||
case 'copy': copyAndPast(this._canvas, [this._pathElement]); break;
|
||||
}
|
||||
});
|
||||
|
||||
// arrows, dotted
|
||||
clickForAll(shadow, '[data-cmd]', evt => {
|
||||
const argStyle = evtTargetAttr(evt, 'data-cmd-arg');
|
||||
const currentArr = pathStyles.indexOf(argStyle);
|
||||
if (currentArr > -1) {
|
||||
classDel(this._pathElement, argStyle);
|
||||
pathStyles.splice(currentArr, 1);
|
||||
classDel(evt.currentTarget, 'actv');
|
||||
} else {
|
||||
classAdd(this._pathElement, argStyle);
|
||||
pathStyles.push(argStyle);
|
||||
classAdd(evt.currentTarget, 'actv');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
customElements.define('ap-path-settings', PathSettings);
|
||||
|
||||
/** @typedef { import('./path-smbl').PathElement } PathElement */
|
||||
/** @typedef { import('../infrastructure/canvas-smbl.js').CanvasElement } CanvasElement */
|
||||
2
main_plugin/dgrm/shapes/path-smbl.js
Executable file
2
main_plugin/dgrm/shapes/path-smbl.js
Executable file
@@ -0,0 +1,2 @@
|
||||
export const PathSmbl = Symbol('path');
|
||||
/** @typedef {SVGGraphicsElement & { [PathSmbl]?: import("./path").Path }} PathElement */
|
||||
402
main_plugin/dgrm/shapes/path.js
Executable file
402
main_plugin/dgrm/shapes/path.js
Executable file
@@ -0,0 +1,402 @@
|
||||
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 */
|
||||
86
main_plugin/dgrm/shapes/rect-txt-settings.js
Executable file
86
main_plugin/dgrm/shapes/rect-txt-settings.js
Executable file
@@ -0,0 +1,86 @@
|
||||
import { copyAndPast } from '../diagram/group-select-applay.js';
|
||||
import { classAdd, classDel, clickForAll, listen, classSingleAdd, evtTargetAttr } from '../infrastructure/util.js';
|
||||
import { modalCreate } from './modal-create.js';
|
||||
import { ShapeSmbl } from './shape-smbl.js';
|
||||
|
||||
/**
|
||||
* @param {import('../infrastructure/canvas-smbl.js').CanvasElement} canvas
|
||||
* @param {import('./shape-smbl').ShapeElement} shapeElement
|
||||
* @param {number} bottomX positon of the bottom left corner of the panel
|
||||
* @param {number} bottomY positon of the bottom left corner of the panel
|
||||
*/
|
||||
export const rectTxtSettingsPnlCreate = (canvas, shapeElement, bottomX, bottomY) =>
|
||||
modalCreate(bottomX, bottomY, new RectTxtSettings(canvas, shapeElement));
|
||||
|
||||
class RectTxtSettings extends HTMLElement {
|
||||
/**
|
||||
* @param {import('../infrastructure/canvas-smbl.js').CanvasElement} canvas
|
||||
* @param {import('./shape-smbl').ShapeElement} rectElement
|
||||
*/
|
||||
constructor(canvas, rectElement) {
|
||||
super();
|
||||
/** @private */
|
||||
this._rectElement = rectElement;
|
||||
|
||||
/** @private */
|
||||
this._canvas = canvas;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
const shadow = this.attachShadow({ mode: 'closed' });
|
||||
shadow.innerHTML = `
|
||||
<style>
|
||||
.ln { display: flex; }
|
||||
.ln > * {
|
||||
height: 24px;
|
||||
padding: 10px;
|
||||
fill-opacity: 0.3;
|
||||
stroke-opacity: 0.3;
|
||||
}
|
||||
[data-cmd] { cursor: pointer; }
|
||||
|
||||
.ta-1 [data-cmd-arg="1"],
|
||||
.ta-2 [data-cmd-arg="2"],
|
||||
.ta-3 [data-cmd-arg="3"]
|
||||
{ fill-opacity: 1; stroke-opacity: 1; }
|
||||
</style>
|
||||
<ap-shape-edit id="F" edit-btn="true">
|
||||
<div class="ln">
|
||||
<svg data-cmd data-cmd-arg="1" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M3 4h18v2H3V4zm0 15h14v2H3v-2zm0-5h18v2H3v-2zm0-5h14v2H3V9z" fill="rgb(52,71,103)"/></svg>
|
||||
<svg data-cmd data-cmd-arg="2" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M3 4h18v2H3V4zm2 15h14v2H5v-2zm-2-5h18v2H3v-2zm2-5h14v2H5V9z" fill="rgb(52,71,103)"/></svg>
|
||||
<svg data-cmd data-cmd-arg="3" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M3 4h18v2H3V4zm4 15h14v2H7v-2zm-4-5h18v2H3v-2zm4-5h14v2H7V9z" fill="rgb(52,71,103)"/></svg>
|
||||
</div>
|
||||
</ap-shape-edit>`;
|
||||
|
||||
const rectData = /** @type {import('./rect.js').RectData} */(this._rectElement[ShapeSmbl].data);
|
||||
|
||||
const editEl = shadow.getElementById('edit');
|
||||
classAdd(editEl, `ta-${rectData.a}`);
|
||||
|
||||
// colors, del
|
||||
listen(editEl, 'cmd', /** @param {CustomEvent<{cmd:string, arg:string}>} evt */ evt => {
|
||||
switch (evt.detail.cmd) {
|
||||
case 'style': classSingleAdd(this._rectElement, rectData, 'cl-', evt.detail.arg); break;
|
||||
case 'del': this._rectElement[ShapeSmbl].del(); break;
|
||||
case 'copy': copyAndPast(this._canvas, [this._rectElement]); break;
|
||||
}
|
||||
});
|
||||
|
||||
// text align
|
||||
clickForAll(shadow, '[data-cmd]', evt => {
|
||||
const alignNew = /** @type {1|2|3} */(Number.parseInt(evtTargetAttr(evt, 'data-cmd-arg')));
|
||||
if (alignNew === rectData.a) { return; }
|
||||
|
||||
const alignOld = rectData.a;
|
||||
|
||||
// applay text align to shape
|
||||
rectData.a = alignNew;
|
||||
this._rectElement[ShapeSmbl].draw();
|
||||
|
||||
// highlight text align btn in settings panel
|
||||
classDel(editEl, `ta-${alignOld}`);
|
||||
classAdd(editEl, `ta-${rectData.a}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
customElements.define('ap-rect-txt-settings', RectTxtSettings);
|
||||
142
main_plugin/dgrm/shapes/rect.js
Executable file
142
main_plugin/dgrm/shapes/rect.js
Executable file
@@ -0,0 +1,142 @@
|
||||
import { ceil, child, classAdd, classDel, positionSet } from '../infrastructure/util.js';
|
||||
import { rectTxtSettingsPnlCreate } from './rect-txt-settings.js';
|
||||
import { shapeCreate } from './shape-evt-proc.js';
|
||||
import { settingsPnlCreate } from './shape-settings.js';
|
||||
import { ShapeSmbl } from './shape-smbl.js';
|
||||
|
||||
/**
|
||||
* @param {CanvasElement} canvas
|
||||
* @param {RectData} rectData
|
||||
*/
|
||||
export function rect(canvas, rectData) {
|
||||
rectData.w = rectData.w ?? 96;
|
||||
rectData.h = rectData.h ?? 48;
|
||||
rectData.a = rectData.a ?? (rectData.t ? 1 : 2);
|
||||
|
||||
const templ = `
|
||||
<rect data-key="outer" data-evt-no data-evt-index="2" width="144" height="96" x="-72" y="-48" fill="transparent" stroke="transparent" stroke-width="0" />
|
||||
<rect data-key="main" width="96" height="48" x="-48" y="-24" rx="15" ry="15" fill="#1aaee5" stroke="#fff" stroke-width="1" />
|
||||
<text data-key="text" y="0" x="${rectTxtXByAlign(rectData)}" style="pointer-events: none;" fill="#fff"> </text>`;
|
||||
|
||||
const shape = shapeCreate(canvas, rectData, templ,
|
||||
{
|
||||
right: { dir: 'right', position: { x: 48, y: 0 } },
|
||||
left: { dir: 'left', position: { x: -48, y: 0 } },
|
||||
bottom: { dir: 'bottom', position: { x: 0, y: 24 } },
|
||||
top: { dir: 'top', position: { x: 0, y: -24 } }
|
||||
},
|
||||
// onTextChange
|
||||
txtEl => {
|
||||
const textBox = txtEl.getBBox();
|
||||
const newWidth = ceil(96, 48, textBox.width + (rectData.t ? 6 : 0)); // 6 px right padding for text shape
|
||||
const newHeight = ceil(48, 48, textBox.height);
|
||||
|
||||
if (rectData.w !== newWidth || rectData.h !== newHeight) {
|
||||
rectData.w = newWidth;
|
||||
rectData.h = newHeight;
|
||||
resize();
|
||||
}
|
||||
},
|
||||
// settingsPnlCreateFn
|
||||
rectData.t ? rectTxtSettingsPnlCreate : settingsPnlCreate);
|
||||
|
||||
classAdd(shape.el, rectData.t ? 'shtxt' : 'shrect');
|
||||
|
||||
let currentW = rectData.w;
|
||||
let currentTxtAlign = rectData.a;
|
||||
/** @param {boolean?=} fixTxtAlign */
|
||||
function resize(fixTxtAlign) {
|
||||
const mainX = rectData.w / -2;
|
||||
const mainY = rectData.h / -2;
|
||||
const middleX = 0;
|
||||
|
||||
shape.cons.right.position.x = -mainX;
|
||||
shape.cons.left.position.x = mainX;
|
||||
shape.cons.bottom.position.y = -mainY;
|
||||
shape.cons.bottom.position.x = middleX;
|
||||
shape.cons.top.position.y = mainY;
|
||||
shape.cons.top.position.x = middleX;
|
||||
for (const connectorKey in shape.cons) {
|
||||
positionSet(child(shape.el, connectorKey), shape.cons[connectorKey].position);
|
||||
}
|
||||
|
||||
rectSet(shape.el, 'main', rectData.w, rectData.h, mainX, mainY);
|
||||
rectSet(shape.el, 'outer', rectData.w + 48, rectData.h + 48, mainX - 24, mainY - 24);
|
||||
|
||||
// if text align or width changed
|
||||
// fix text align
|
||||
if (fixTxtAlign || currentTxtAlign !== rectData.a || currentW !== rectData.w) {
|
||||
let txtX;
|
||||
let posXDelta;
|
||||
switch (rectData.a) {
|
||||
// text align left
|
||||
case 1:
|
||||
txtX = mainX + 8;
|
||||
posXDelta = (rectData.w - currentW) / 2;
|
||||
break;
|
||||
case 2:
|
||||
txtX = 0;
|
||||
posXDelta = 0;
|
||||
break;
|
||||
// text align right
|
||||
case 3:
|
||||
txtX = -mainX - 8;
|
||||
posXDelta = (rectData.w - currentW) / -2;
|
||||
break;
|
||||
}
|
||||
|
||||
const txtEl = child(shape.el, 'text');
|
||||
txtEl.x.baseVal[0].value = txtX;
|
||||
txtEl.querySelectorAll('tspan').forEach(ss => { ss.x.baseVal[0].value = txtX; });
|
||||
|
||||
rectData.position.x += posXDelta;
|
||||
|
||||
classDel(shape.el, `ta-${currentTxtAlign}`);
|
||||
classAdd(shape.el, `ta-${rectData.a}`);
|
||||
|
||||
currentTxtAlign = rectData.a;
|
||||
currentW = rectData.w;
|
||||
}
|
||||
|
||||
shape.draw();
|
||||
}
|
||||
|
||||
classAdd(shape.el, `ta-${rectData.a}`);
|
||||
if (rectData.w !== 96 || rectData.h !== 48) { resize(true); } else { shape.draw(); }
|
||||
|
||||
shape.el[ShapeSmbl].draw = resize;
|
||||
|
||||
return shape.el;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element} svgGrp, @param {string} key,
|
||||
* @param {number} w, @param {number} h
|
||||
* @param {number} x, @param {number} y
|
||||
*/
|
||||
function rectSet(svgGrp, key, w, h, x, y) {
|
||||
/** @type {SVGRectElement} */ const rect = child(svgGrp, key);
|
||||
rect.width.baseVal.value = w;
|
||||
rect.height.baseVal.value = h;
|
||||
rect.x.baseVal.value = x;
|
||||
rect.y.baseVal.value = y;
|
||||
}
|
||||
|
||||
/** @param {RectData} rectData */
|
||||
const rectTxtXByAlign = rectData => rectData.a === 1
|
||||
? -40 // text align keft
|
||||
: rectData.a === 2
|
||||
? 0 // text align middle
|
||||
: 40; // text align right
|
||||
|
||||
/** @typedef { {x:number, y:number} } Point */
|
||||
/** @typedef { import('../infrastructure/canvas-smbl.js').CanvasElement } CanvasElement */
|
||||
/** @typedef { import('./shape-evt-proc').CanvasData } CanvasData */
|
||||
/** @typedef { import('./shape-evt-proc').ConnectorsData } ConnectorsData */
|
||||
/**
|
||||
@typedef {{
|
||||
type:number, position: Point, title?: string, styles?: string[],
|
||||
w?:number, h?:number
|
||||
t?:boolean,
|
||||
a?: 1|2|3
|
||||
}} RectData */
|
||||
101
main_plugin/dgrm/shapes/rhomb.js
Executable file
101
main_plugin/dgrm/shapes/rhomb.js
Executable file
@@ -0,0 +1,101 @@
|
||||
import { ceil, child, classAdd, positionSet, svgTxtFarthestPoint } from '../infrastructure/util.js';
|
||||
import { shapeCreate } from './shape-evt-proc.js';
|
||||
|
||||
/**
|
||||
* @param {CanvasElement} canvas
|
||||
* @param {RhombData} rhombData
|
||||
*/
|
||||
export function rhomb(canvas, rhombData) {
|
||||
const templ = `
|
||||
<path data-key="outer" data-evt-no data-evt-index="2" d="M-72 0 L0 -72 L72 0 L0 72 Z" stroke-width="0" fill="transparent" />
|
||||
<path data-key="border" d="M-39 0 L0 -39 L39 0 L0 39 Z" stroke-width="20" stroke="#fff" fill="transparent" stroke-linejoin="round" />
|
||||
<path data-key="main" d="M-39 0 L0 -39 L39 0 L0 39 Z" stroke-width="18" stroke-linejoin="round" stroke="#1D809F" fill="#1D809F" />
|
||||
<text data-key="text" x="0" y="0" text-anchor="middle" style="pointer-events: none;" fill="#fff"> </text>`;
|
||||
|
||||
const shape = shapeCreate(canvas, rhombData, templ,
|
||||
{
|
||||
right: { dir: 'right', position: { x: 48, y: 0 } },
|
||||
left: { dir: 'left', position: { x: -48, y: 0 } },
|
||||
bottom: { dir: 'bottom', position: { x: 0, y: 48 } },
|
||||
top: { dir: 'top', position: { x: 0, y: -48 } }
|
||||
},
|
||||
// onTextChange
|
||||
txtEl => {
|
||||
const newWidth = ceil(96, 48, textElRhombWidth(txtEl) - 20); // -20 experemental val
|
||||
if (newWidth !== rhombData.w) {
|
||||
rhombData.w = newWidth;
|
||||
resize();
|
||||
}
|
||||
});
|
||||
classAdd(shape.el, 'shrhomb');
|
||||
|
||||
function resize() {
|
||||
const connectors = rhombCalc(rhombData.w, 0);
|
||||
shape.cons.right.position.x = connectors.r.x;
|
||||
shape.cons.left.position.x = connectors.l.x;
|
||||
shape.cons.bottom.position.y = connectors.b.y;
|
||||
shape.cons.top.position.y = connectors.t.y;
|
||||
for (const connectorKey in shape.cons) {
|
||||
positionSet(child(shape.el, connectorKey), shape.cons[connectorKey].position);
|
||||
}
|
||||
|
||||
const mainRhomb = rhombCalc(rhombData.w, 9);
|
||||
rhombSet(shape.el, 'main', mainRhomb);
|
||||
rhombSet(shape.el, 'border', mainRhomb);
|
||||
rhombSet(shape.el, 'outer', rhombCalc(rhombData.w, -24));
|
||||
|
||||
shape.draw();
|
||||
}
|
||||
|
||||
if (!!rhombData.w && rhombData.w !== 96) { resize(); } else { shape.draw(); }
|
||||
|
||||
return shape.el;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element} svgGrp, @param {string} key,
|
||||
* @param {RhombPoints} rhomb
|
||||
*/
|
||||
function rhombSet(svgGrp, key, rhomb) {
|
||||
/** @type {SVGPathElement} */(child(svgGrp, key)).setAttribute('d', `M${rhomb.l.x} ${rhomb.l.y} L${rhomb.t.x} ${rhomb.t.y} L${rhomb.r.x} ${rhomb.r.y} L${rhomb.b.x} ${rhomb.b.y} Z`);
|
||||
}
|
||||
|
||||
/**
|
||||
* calc square rhomb points by width
|
||||
* origin is in the center of the rhomb
|
||||
* @param {number} width, @param {number} margin
|
||||
* @returns {RhombPoints}
|
||||
*/
|
||||
function rhombCalc(width, margin) {
|
||||
const half = width / 2;
|
||||
const mrgnMinHalf = margin - half;
|
||||
const halfMinMrgn = half - margin;
|
||||
return {
|
||||
l: { x: mrgnMinHalf, y: 0 },
|
||||
t: { x: 0, y: mrgnMinHalf },
|
||||
r: { x: halfMinMrgn, y: 0 },
|
||||
b: { x: 0, y: halfMinMrgn }
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* calc width of the square rhomb that cover all tspan in {textEl}
|
||||
* origin is in the center of the rhomb
|
||||
* @param {SVGTextElement} textEl
|
||||
*/
|
||||
function textElRhombWidth(textEl) {
|
||||
const farthestPoint = svgTxtFarthestPoint(textEl);
|
||||
return 2 * (Math.abs(farthestPoint.x) + Math.abs(farthestPoint.y));
|
||||
}
|
||||
|
||||
/** @typedef { {x:number, y:number} } Point */
|
||||
/** @typedef { import('../infrastructure/canvas-smbl.js').CanvasElement } CanvasElement */
|
||||
/** @typedef { import('./shape-evt-proc').CanvasData } CanvasData */
|
||||
/** @typedef { import('./shape-evt-proc').ConnectorsData } ConnectorsData */
|
||||
/**
|
||||
@typedef {{
|
||||
type:number, position: Point, title?: string, styles?: string[]
|
||||
w?:number
|
||||
}} RhombData
|
||||
*/
|
||||
/** @typedef { { l:Point, t:Point, r:Point, b:Point } } RhombPoints */
|
||||
306
main_plugin/dgrm/shapes/shape-evt-proc.js
Executable file
306
main_plugin/dgrm/shapes/shape-evt-proc.js
Executable file
@@ -0,0 +1,306 @@
|
||||
import { child, classAdd, classDel, deepCopy, svgEl } from '../infrastructure/util.js';
|
||||
import { moveEvtProc } from '../infrastructure/move-evt-proc.js';
|
||||
import { path, dirReverse } from './path.js';
|
||||
import { textareaCreate } from '../infrastructure/svg-text-area.js';
|
||||
import { settingsPnlCreate } from './shape-settings.js';
|
||||
import { placeToCell, pointInCanvas } from '../infrastructure/move-scale-applay.js';
|
||||
import { ShapeSmbl } from './shape-smbl.js';
|
||||
import { svgTextDraw } from '../infrastructure/svg-text-draw.js';
|
||||
import { PathSmbl } from './path-smbl.js';
|
||||
import { CanvasSmbl } from '../infrastructure/canvas-smbl.js';
|
||||
import { canvasSelectionClearSet } from '../diagram/canvas-clear.js';
|
||||
import { listenCopy } from '../diagram/group-select-applay.js';
|
||||
|
||||
/**
|
||||
* provides:
|
||||
* - shape move
|
||||
* - connectors
|
||||
*
|
||||
* - text editor
|
||||
* - standard edit panel
|
||||
* - onTextChange callback
|
||||
* @param {CanvasElement} canvas
|
||||
* @param {string} shapeHtml must have '<text data-key="text">'
|
||||
* @param {ShapeData & { title?: string, styles?: string[]}} shapeData
|
||||
* @param {ConnectorsData} cons
|
||||
* @param {SettingsPnlCreateFn=} settingsPnlCreateFn
|
||||
* @param {{(txtEl:SVGTextElement):void}} onTextChange
|
||||
*/
|
||||
export function shapeCreate(canvas, shapeData, shapeHtml, cons, onTextChange, settingsPnlCreateFn) {
|
||||
/** @type {ShapeElement} */
|
||||
const el = svgEl('g', `${shapeHtml}
|
||||
${Object.entries(cons)
|
||||
.map(cc => `<circle data-key="${cc[0]}" data-connect="${cc[1].dir}" class="hovertrack" data-evt-index="2" r="10" cx="0" cy="0" style="transform: translate(${cc[1].position.x}px, ${cc[1].position.y}px);" />`)
|
||||
.join()}`);
|
||||
|
||||
const textSettings = {
|
||||
/** @type {SVGTextElement} */
|
||||
el: child(el, 'text'),
|
||||
/** vericale middle, em */
|
||||
vMid: 0
|
||||
};
|
||||
|
||||
svgTextDraw(textSettings.el, textSettings.vMid, shapeData.title);
|
||||
|
||||
const shapeProc = shapeEditEvtProc(canvas, el, shapeData, cons, textSettings,
|
||||
settingsPnlCreateFn,
|
||||
// onTextChange
|
||||
() => onTextChange(textSettings.el));
|
||||
|
||||
return {
|
||||
el,
|
||||
cons,
|
||||
draw: shapeProc.draw
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* provides:
|
||||
* - shape move
|
||||
* - connectors
|
||||
* - copy fn
|
||||
*
|
||||
* - text editor
|
||||
* - standard edit panel
|
||||
* - onTextChange callback
|
||||
* @param {CanvasElement} canvas
|
||||
* @param {ShapeElement} svgGrp
|
||||
* @param {ShapeData & { title?: string, styles?: string[]}} shapeData
|
||||
* @param {ConnectorsData} connectorsInnerPosition
|
||||
* @param { {el:SVGTextElement, vMid: number} } textSettings vMid in em
|
||||
* @param {{():void}} onTextChange
|
||||
* @param {SettingsPnlCreateFn} settingsPnlCreateFn
|
||||
*/
|
||||
function shapeEditEvtProc(canvas, svgGrp, shapeData, connectorsInnerPosition, textSettings, settingsPnlCreateFn, onTextChange) {
|
||||
/** @type {{dispose():void, draw():void}} */
|
||||
let textEditor;
|
||||
|
||||
/** @type { {position:(bottomX:number, bottomY:number)=>void, del:()=>void} } */
|
||||
let settingsPnl;
|
||||
|
||||
function unSelect() {
|
||||
textEditor?.dispose(); textEditor = null;
|
||||
settingsPnl?.del(); settingsPnl = null;
|
||||
}
|
||||
|
||||
/** @param {string} txt */
|
||||
function onTxtChange(txt) {
|
||||
shapeData.title = txt;
|
||||
onTextChange();
|
||||
}
|
||||
|
||||
const settingPnlCreate = settingsPnlCreateFn ?? settingsPnlCreate;
|
||||
const shapeProc = shapeEvtProc(canvas, svgGrp, shapeData, connectorsInnerPosition,
|
||||
// onEdit
|
||||
() => {
|
||||
textEditor = textareaCreate(textSettings.el, textSettings.vMid, shapeData.title, onTxtChange, onTxtChange);
|
||||
|
||||
const position = svgGrp.getBoundingClientRect();
|
||||
settingsPnl = settingPnlCreate(canvas, svgGrp, position.left + 10, position.top + 10);
|
||||
},
|
||||
// onUnselect
|
||||
unSelect
|
||||
);
|
||||
|
||||
if (shapeData.styles) { classAdd(svgGrp, ...shapeData.styles); }
|
||||
|
||||
svgGrp[ShapeSmbl].del = function() {
|
||||
shapeProc.del();
|
||||
svgGrp.remove();
|
||||
};
|
||||
|
||||
return {
|
||||
draw: () => {
|
||||
shapeProc.drawPosition();
|
||||
|
||||
if (settingsPnl) {
|
||||
const position = svgGrp.getBoundingClientRect();
|
||||
settingsPnl.position(position.left + 10, position.top + 10);
|
||||
}
|
||||
|
||||
if (textEditor) { textEditor.draw(); }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* provides:
|
||||
* - shape move
|
||||
* - connectors
|
||||
* - copy fn
|
||||
* - onEdit, onEditStop callbacks
|
||||
* @param {CanvasElement} canvas
|
||||
* @param {ShapeElement} svgGrp
|
||||
* @param {ShapeData} shapeData
|
||||
* @param {ConnectorsData} connectorsInnerPosition
|
||||
* @param {{():void}} onEdit
|
||||
* @param {{():void}} onUnselect
|
||||
*/
|
||||
function shapeEvtProc(canvas, svgGrp, shapeData, connectorsInnerPosition, onEdit, onUnselect) {
|
||||
classAdd(svgGrp, 'hovertrack');
|
||||
|
||||
/** @type {ConnectorsData} */
|
||||
const connectorsData = deepCopy(connectorsInnerPosition);
|
||||
|
||||
/** @type { Set<PathElement> } */
|
||||
const paths = new Set();
|
||||
|
||||
function drawPosition() {
|
||||
svgGrp.style.transform = `translate(${shapeData.position.x}px, ${shapeData.position.y}px)`;
|
||||
|
||||
// paths
|
||||
for (const connectorKey in connectorsInnerPosition) {
|
||||
connectorsData[connectorKey].position = {
|
||||
x: connectorsInnerPosition[connectorKey].position.x + shapeData.position.x,
|
||||
y: connectorsInnerPosition[connectorKey].position.y + shapeData.position.y
|
||||
};
|
||||
}
|
||||
|
||||
for (const path of paths) {
|
||||
path[PathSmbl].draw();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @type {0|1|2}
|
||||
* 0 - init, 1 - selected, 2 - edit
|
||||
*/
|
||||
let state = 0;
|
||||
|
||||
/** @type {()=>void} */
|
||||
let listenCopyDispose;
|
||||
|
||||
function unSelect() {
|
||||
onUnselect();
|
||||
|
||||
state = 0;
|
||||
classDel(svgGrp, 'select');
|
||||
classDel(svgGrp, 'highlight');
|
||||
|
||||
canvasSelectionClearSet(canvas, null);
|
||||
if (listenCopyDispose) { listenCopyDispose(); listenCopyDispose = null; }
|
||||
}
|
||||
|
||||
const moveProcReset = moveEvtProc(
|
||||
canvas.ownerSVGElement,
|
||||
svgGrp,
|
||||
canvas[CanvasSmbl].data,
|
||||
shapeData.position,
|
||||
// onMoveStart
|
||||
/** @param {PointerEvent & { target: Element} } evt */
|
||||
evt => {
|
||||
unSelect();
|
||||
|
||||
const connectorKey = evt.target.getAttribute('data-connect');
|
||||
if (connectorKey) {
|
||||
moveProcReset();
|
||||
|
||||
const diagramEl = document.getElementById('diagram');
|
||||
const rect = diagramEl.getBoundingClientRect();
|
||||
const x = evt.clientX - rect.left;
|
||||
const y = evt.clientY - rect.top;
|
||||
|
||||
const pathEl = path(canvas, {
|
||||
s: { shape: { shapeEl: svgGrp, connectorKey } },
|
||||
e: {
|
||||
data: {
|
||||
dir: dirReverse(connectorsData[connectorKey].dir),
|
||||
position: pointInCanvas(canvas[CanvasSmbl].data, x, y)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
svgGrp.parentNode.append(pathEl);
|
||||
pathEl[PathSmbl].pointerCapture(evt);
|
||||
paths.add(pathEl);
|
||||
}
|
||||
},
|
||||
// onMove
|
||||
drawPosition,
|
||||
// onMoveEnd
|
||||
_ => {
|
||||
placeToCell(shapeData.position, canvas[CanvasSmbl].data.cell);
|
||||
drawPosition();
|
||||
},
|
||||
// onClick
|
||||
_ => {
|
||||
// in edit mode
|
||||
if (state === 2) { return; }
|
||||
|
||||
// to edit mode
|
||||
if (state === 1) {
|
||||
state = 2;
|
||||
classDel(svgGrp, 'select');
|
||||
classAdd(svgGrp, 'highlight');
|
||||
// edit mode
|
||||
onEdit();
|
||||
return;
|
||||
}
|
||||
|
||||
// to select mode
|
||||
state = 1;
|
||||
classAdd(svgGrp, 'select');
|
||||
|
||||
canvasSelectionClearSet(canvas, unSelect);
|
||||
listenCopyDispose = listenCopy(() => [svgGrp]);
|
||||
},
|
||||
// onOutdown
|
||||
unSelect);
|
||||
|
||||
svgGrp[ShapeSmbl] = {
|
||||
/**
|
||||
* @param {string} connectorKey
|
||||
* @param {PathElement} pathEl
|
||||
*/
|
||||
pathAdd: function(connectorKey, pathEl) {
|
||||
paths.add(pathEl);
|
||||
return connectorsData[connectorKey];
|
||||
},
|
||||
|
||||
/** @param {PathElement} pathEl */
|
||||
pathDel: function(pathEl) {
|
||||
paths.delete(pathEl);
|
||||
},
|
||||
|
||||
drawPosition,
|
||||
|
||||
data: shapeData
|
||||
};
|
||||
|
||||
return {
|
||||
drawPosition,
|
||||
del: () => {
|
||||
unSelect();
|
||||
moveProcReset();
|
||||
for (const path of paths) {
|
||||
path[PathSmbl].del();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** @typedef { {x:number, y:number} } Point */
|
||||
/** @typedef { {position:Point, scale:number, cell:number} } CanvasData */
|
||||
|
||||
/** @typedef { 'left' | 'right' | 'top' | 'bottom' } PathDir */
|
||||
/** @typedef { {position: Point, dir: PathDir} } PathEnd */
|
||||
/** @typedef { Object.<string, PathEnd> } ConnectorsData */
|
||||
|
||||
/** @typedef { {type: number, position: Point, styles?:string[]} } ShapeData */
|
||||
/**
|
||||
@typedef {{
|
||||
pathAdd(connectorKey:string, pathEl:PathElement): PathEnd
|
||||
pathDel(pathEl:PathElement): void
|
||||
drawPosition: ()=>void
|
||||
data: ShapeData
|
||||
del?: ()=>void
|
||||
draw?: ()=>void
|
||||
}} Shape
|
||||
*/
|
||||
|
||||
/** @typedef { {(canvas:CanvasElement, shapeElement:ShapeElement, bottomX:number, bottomY:number):{position(btmX:number, btmY:number):void, del():void} } } SettingsPnlCreateFn */
|
||||
|
||||
/** @typedef { import('../infrastructure/canvas-smbl.js').CanvasElement } CanvasElement */
|
||||
/** @typedef {import('./shape-smbl').ShapeElement} ShapeElement */
|
||||
/** @typedef {import('./path').Path} Path */
|
||||
/** @typedef {import('./path-smbl').PathElement} PathElement */
|
||||
121
main_plugin/dgrm/shapes/shape-settings.js
Executable file
121
main_plugin/dgrm/shapes/shape-settings.js
Executable file
@@ -0,0 +1,121 @@
|
||||
import { copyAndPast } from '../diagram/group-select-applay.js';
|
||||
import { copySvg, delSvg } from '../infrastructure/assets.js';
|
||||
import { clickForAll, listen, classSingleAdd, evtTargetAttr } from '../infrastructure/util.js';
|
||||
import { modalChangeTop, modalCreate } from './modal-create.js';
|
||||
import { ShapeSmbl } from './shape-smbl.js';
|
||||
|
||||
/**
|
||||
* @param {import('../infrastructure/canvas-smbl').CanvasElement} canvas
|
||||
* @param {import('./shape-smbl').ShapeElement} shapeElement
|
||||
* @param {number} bottomX positon of the bottom left corner of the panel
|
||||
* @param {number} bottomY positon of the bottom left corner of the panel
|
||||
*/
|
||||
export function settingsPnlCreate(canvas, shapeElement, bottomX, bottomY) {
|
||||
const shapeSettings = new ShapeEdit();
|
||||
listen(shapeSettings, 'cmd', /** @param {CustomEvent<{cmd:string, arg:string}>} evt */ evt => {
|
||||
switch (evt.detail.cmd) {
|
||||
case 'style': classSingleAdd(shapeElement, shapeElement[ShapeSmbl].data, 'cl-', evt.detail.arg); break;
|
||||
case 'del': shapeElement[ShapeSmbl].del(); break;
|
||||
case 'copy': copyAndPast(canvas, [shapeElement]); break;
|
||||
}
|
||||
});
|
||||
return modalCreate(bottomX, bottomY, shapeSettings);
|
||||
}
|
||||
|
||||
class ShapeEdit extends HTMLElement {
|
||||
connectedCallback() {
|
||||
const shadow = this.attachShadow({ mode: 'closed' });
|
||||
shadow.innerHTML =
|
||||
`<style>
|
||||
.ln { display: flex; }
|
||||
.ln > * {
|
||||
height: 24px;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
#prop { padding-bottom: 10px; }
|
||||
|
||||
.crcl { width: 25px; height: 25px; border-radius: 50%; }
|
||||
</style>
|
||||
<div id="pnl">
|
||||
<div id="clr" style="display: none;">
|
||||
<div class="ln">
|
||||
<div data-cmd="style" data-cmd-arg="cl-red">
|
||||
<div class="crcl" style="background: #E74C3C"></div>
|
||||
</div>
|
||||
<div data-cmd="style" data-cmd-arg="cl-orange">
|
||||
<div class="crcl" style="background: #ff6600"></div>
|
||||
</div>
|
||||
<div data-cmd="style" data-cmd-arg="cl-green">
|
||||
<div class="crcl" style="background: #19bc9b"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ln">
|
||||
<div data-cmd="style" data-cmd-arg="cl-blue">
|
||||
<div class="crcl" style="background: #1aaee5"></div>
|
||||
</div>
|
||||
<div data-cmd="style" data-cmd-arg="cl-dblue">
|
||||
<div class="crcl" style="background: #1D809F"></div>
|
||||
</div>
|
||||
<div data-cmd="style" data-cmd-arg="cl-dgray">
|
||||
<div class="crcl" style="background: #495057"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="prop" style="display: none;"><slot id="slot"></slot></div>
|
||||
</div>
|
||||
<div class="ln">
|
||||
<svg data-toggle="clr" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M19.228 18.732l1.768-1.768 1.767 1.768a2.5 2.5 0 1 1-3.535 0zM8.878 1.08l11.314 11.313a1 1 0 0 1 0 1.415l-8.485 8.485a1 1 0 0 1-1.414 0l-8.485-8.485a1 1 0 0 1 0-1.415l7.778-7.778-2.122-2.121L8.88 1.08zM11 6.03L3.929 13.1 11 20.173l7.071-7.071L11 6.029z" fill="rgb(52,71,103)"/></svg>
|
||||
<svg data-toggle="prop" ${this.getAttribute('edit-btn') ? '' : 'style="display: none;"'} viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12.9 6.858l4.242 4.243L7.242 21H3v-4.243l9.9-9.9zm1.414-1.414l2.121-2.122a1 1 0 0 1 1.414 0l2.829 2.829a1 1 0 0 1 0 1.414l-2.122 2.121-4.242-4.242z" fill="rgb(52,71,103)"/></svg>
|
||||
${copySvg}
|
||||
${delSvg}
|
||||
</div>`;
|
||||
|
||||
//
|
||||
// tabs
|
||||
|
||||
{
|
||||
const pnl = shadow.getElementById('pnl');
|
||||
|
||||
/** @param {1|-1} coef */
|
||||
function modalSetTop(coef) {
|
||||
modalChangeTop(window.scrollY + coef * pnl.getBoundingClientRect().height); // window.scrollY fix IPhone keyboard
|
||||
}
|
||||
|
||||
/** @type {HTMLElement} */
|
||||
let currentTab;
|
||||
|
||||
clickForAll(shadow, '[data-toggle]', evt => {
|
||||
if (currentTab) {
|
||||
modalSetTop(1);
|
||||
display(currentTab, false);
|
||||
}
|
||||
|
||||
const tab = shadow.getElementById(evtTargetAttr(evt, 'data-toggle'));
|
||||
if (currentTab !== tab) {
|
||||
display(tab, true);
|
||||
modalSetTop(-1);
|
||||
currentTab = tab;
|
||||
} else {
|
||||
currentTab = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// commands
|
||||
|
||||
clickForAll(shadow, '[data-cmd]', evt => {
|
||||
this.dispatchEvent(new CustomEvent('cmd', {
|
||||
detail: {
|
||||
cmd: evtTargetAttr(evt, 'data-cmd'),
|
||||
arg: evtTargetAttr(evt, 'data-cmd-arg')
|
||||
}
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
customElements.define('ap-shape-edit', ShapeEdit);
|
||||
|
||||
/** @param {ElementCSSInlineStyle} el, @param {boolean} isDisp */
|
||||
function display(el, isDisp) { el.style.display = isDisp ? 'unset' : 'none'; }
|
||||
3
main_plugin/dgrm/shapes/shape-smbl.js
Executable file
3
main_plugin/dgrm/shapes/shape-smbl.js
Executable file
@@ -0,0 +1,3 @@
|
||||
export const ShapeSmbl = Symbol('shape');
|
||||
|
||||
/** @typedef {SVGGraphicsElement & { [ShapeSmbl]?: import('./shape-evt-proc').Shape }} ShapeElement */
|
||||
27
main_plugin/dgrm/shapes/shape-type-map.js
Executable file
27
main_plugin/dgrm/shapes/shape-type-map.js
Executable file
@@ -0,0 +1,27 @@
|
||||
import { circle } from './circle.js';
|
||||
import { path } from './path.js';
|
||||
import { rect } from './rect.js';
|
||||
import { rhomb } from './rhomb.js';
|
||||
|
||||
/**
|
||||
* @param {CanvasElement} canvas
|
||||
* @returns {Record<number, ShapeType>}
|
||||
*/
|
||||
export function shapeTypeMap(canvas) {
|
||||
return {
|
||||
0: { create: shapeData => path(canvas, shapeData) },
|
||||
1: { create: shapeData => circle(canvas, shapeData) },
|
||||
2: { create: shapeData => rect(canvas, shapeData) },
|
||||
3: { create: shapeData => { /** @type {RectData} */(shapeData).t = true; return rect(canvas, shapeData); } },
|
||||
4: { create: shapeData => rhomb(canvas, shapeData) }
|
||||
};
|
||||
}
|
||||
|
||||
/** @typedef { {x:number, y:number} } Point */
|
||||
/** @typedef { import('./rect.js').RectData } RectData */
|
||||
/** @typedef { import('../infrastructure/canvas-smbl.js').CanvasElement } CanvasElement */
|
||||
/**
|
||||
@typedef {{
|
||||
create: (shapeData)=>SVGGraphicsElement
|
||||
}} ShapeType
|
||||
*/
|
||||
Reference in New Issue
Block a user