Добавляем все файлы

This commit is contained in:
2025-11-06 19:41:55 +02:00
parent 235d6a3a18
commit 2e5aaec307
218 changed files with 79015 additions and 0 deletions

View 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">&nbsp;</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 */

View 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`;
}

View 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 */

View 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
View 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 */

View 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
View 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">&nbsp;</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
View 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">&nbsp;</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 */

View 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 */

View 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'; }

View File

@@ -0,0 +1,3 @@
export const ShapeSmbl = Symbol('shape');
/** @typedef {SVGGraphicsElement & { [ShapeSmbl]?: import('./shape-evt-proc').Shape }} ShapeElement */

View 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
*/