@antv/x6
Version:
JavaScript diagramming library that uses SVG and HTML for rendering.
385 lines • 14.7 kB
JavaScript
import { ObjectExt, Dom, Vector } from '../../util';
import { Point } from '../../geometry';
import { View } from '../../view';
// need: <meta http-equiv="x-ua-compatible" content="IE=Edge" />
export class PathDrawer extends View {
constructor(options) {
super();
this.MOVEMENT_DETECTION_THRESHOLD = 150;
this.options = ObjectExt.merge({}, PathDrawer.defaultOptions, options);
this.action = 'awaiting-input';
this.render();
this.startListening();
}
get vel() {
return Vector.create(this.container);
}
render() {
const options = this.options;
this.container = Dom.createSvgElement('g');
Dom.addClass(this.container, this.prefixClassName('path-drawer'));
this.pathTemplate = Dom.createSvgElement('path');
Dom.attr(this.pathTemplate, options.pathAttributes);
this.startPointElement = Vector.create(options.startPointMarkup).addClass('start-point').node;
this.controlElement = Dom.createSvgElement('path');
Dom.addClass(this.controlElement, 'control-path');
Vector.create('rect', {
x: 0,
y: 0,
width: '100%',
height: '100%',
fill: 'transparent',
stroke: 'none',
}).appendTo(this.container);
this.options.target.appendChild(this.container);
return this;
}
onRemove() {
this.remove(this.pathElement);
this.clear();
this.stopListening();
}
startListening() {
this.delegateEvents({
mousedown: 'onMouseDown',
touchstart: 'onMouseDown',
dblclick: 'onDoubleClick',
contextmenu: 'onContextMenu',
'mousedown .start-point': 'onStartPointMouseDown',
'touchstart .start-point': 'onStartPointMouseDown',
});
}
stopListening() {
this.undelegateEvents();
}
clear() {
const path = this.pathElement;
if (path && path.pathSegList.numberOfItems <= 1) {
this.remove(path);
}
this.startPointElement.remove();
this.controlElement.remove();
this.undelegateDocumentEvents();
this.action = 'awaiting-input';
this.emit('clear');
}
createPath(x, y) {
this.pathElement = this.pathTemplate.cloneNode(true);
this.addMoveSegment(x, y);
Dom.translate(this.startPointElement, x, y, {
absolute: true,
});
this.vel.before(this.pathElement);
this.vel.append(this.startPointElement);
this.emit('path:create', { path: this.pathElement });
}
closePath() {
const path = this.pathElement;
const first = this.getPathSeg(path, 0);
const last = this.getPathSeg(path, -1);
if (last.pathSegType === SVGPathSeg.PATHSEG_LINETO_ABS) {
path.pathSegList.replaceItem(path.createSVGPathSegClosePath(), path.pathSegList.numberOfItems - 1);
}
else {
last.x = first.x;
last.y = first.y;
path.pathSegList.appendItem(path.createSVGPathSegClosePath());
}
this.finishPath('path:close');
}
finishPath(name) {
const path = this.pathElement;
if (path && 0 < this.numberOfVisibleSegments()) {
this.emit('path:finish', { path });
this.trigger(name, { path });
}
else {
this.emit('path:abort', { path });
}
this.clear();
}
numberOfVisibleSegments() {
const path = this.pathElement;
let remaining = path.pathSegList.numberOfItems;
remaining = remaining - 1;
const last = this.getPathSeg(path, -1);
if (last.pathSegType === SVGPathSeg.PATHSEG_CLOSEPATH) {
remaining = remaining - 1;
}
return remaining;
}
addMoveSegment(x, y) {
const path = this.pathElement;
const seg = path.createSVGPathSegMovetoAbs(x, y);
path.pathSegList.appendItem(seg);
this.emit('path:segment:add', { path });
this.emit('path:move-segment:add', { path });
}
addLineSegment(x, y) {
const path = this.pathElement;
const seg = path.createSVGPathSegLinetoAbs(x, y);
path.pathSegList.appendItem(seg);
this.emit('path:segment:add', { path });
this.emit('path:line-segment:add', { path });
}
addCurveSegment(x, y, x1, y1, x2, y2) {
const path = this.pathElement;
const seg = path.createSVGPathSegCurvetoCubicAbs(x, y, x1, y1, x2 || x, y2 || y);
path.pathSegList.appendItem(seg);
this.emit('path:segment:add', { path });
this.emit('path:curve-segment:add', { path });
}
adjustLastSegment(x, y, x1, y1, x2, y2) {
const path = this.pathElement;
const snapRadius = this.options.snapRadius;
if (snapRadius && x != null && y != null) {
const snaped = this.snapLastSegmentCoordinates(x, y, snapRadius);
x = snaped.x; // tslint:disable-line
y = snaped.y; // tslint:disable-line
}
const seg = this.getPathSeg(path, -1);
if (x != null) {
seg.x = x;
}
if (y != null) {
seg.y = y;
}
if (x1 != null) {
seg.x1 = x1;
}
if (y1 != null) {
seg.y1 = y1;
}
if (x2 != null) {
seg.x2 = x2;
}
if (y2 != null) {
seg.y2 = y2;
}
this.emit('path:edit', { path });
this.emit('path:last-segment:adjust', { path });
}
snapLastSegmentCoordinates(x, y, snapRadius) {
const path = this.pathElement;
let xSnaped = false;
let ySnaped = false;
let targetX = x;
let targetY = y;
for (let i = path.pathSegList.numberOfItems - 2; 0 <= i && (!xSnaped || !ySnaped); i -= 1) {
const seg = this.getPathSeg(path, i);
if (!xSnaped && Math.abs(seg.x - x) < snapRadius) {
targetX = seg.x;
xSnaped = true;
}
if (!ySnaped && Math.abs(seg.y - y) < snapRadius) {
targetY = seg.y;
ySnaped = true;
}
}
return new Point(targetX, targetY);
}
removeLastSegment() {
const path = this.pathElement;
path.pathSegList.removeItem(path.pathSegList.numberOfItems - 1);
this.emit('path:edit', { path });
this.emit('path:last-segment:remove', { path });
}
findControlPoint(x, y) {
const path = this.pathElement;
const seg = this.getPathSeg(path, -1);
return new Point(x, y).reflection(seg);
}
replaceLastSegmentWithCurve() {
const path = this.pathElement;
const last = this.getPathSeg(path, -1);
const prev = this.getPathSeg(path, -2);
const seg = path.createSVGPathSegCurvetoCubicAbs(last.x, last.y, prev.x, prev.y, last.x, last.y);
path.pathSegList.replaceItem(seg, path.pathSegList.numberOfItems - 1);
this.emit('path:edit', { path });
this.emit('path:last-segment:replace-with-curve', { path });
}
adjustControlPath(x1, y1, x2, y2) {
const controlPathElement = this.controlElement;
controlPathElement.pathSegList.initialize(controlPathElement.createSVGPathSegMovetoAbs(x1, y1));
controlPathElement.pathSegList.appendItem(controlPathElement.createSVGPathSegLinetoAbs(x2, y2));
this.vel.append(controlPathElement);
const path = this.pathElement;
this.emit('path:interact', { path });
this.emit('path:control:adjust', { path });
}
removeControlPath() {
const path = this.pathElement;
const svgControl = this.controlElement;
svgControl.pathSegList.clear();
this.vel.append(svgControl);
this.emit('path:interact', { path });
this.emit('path:control:remove', { path });
}
getPathSeg(path, index) {
const i = index < 0 ? path.pathSegList.numberOfItems + index : index;
return path.pathSegList.getItem(i);
}
onMouseDown(evt) {
const e = this.normalizeEvent(evt);
e.stopPropagation();
if (this.isLeftMouseDown(e) &&
this.isSamePositionEvent(e) &&
this.container.parentNode) {
const local = this.vel.toLocalPoint(e.clientX, e.clientY);
switch (this.action) {
case 'awaiting-input':
this.createPath(local.x, local.y);
this.action = 'path-created';
this.delegateDocumentEvents(PathDrawer.documentEvents);
break;
case 'adjusting-line-end':
this.action = 'awaiting-line-end';
break;
case 'adjusting-curve-end':
this.action = 'awaiting-curve-control-2';
}
this.timeStamp = e.timeStamp;
}
}
onMouseMove(evt) {
const e = this.normalizeEvent(evt);
e.stopPropagation();
if ('awaiting-input' !== this.action) {
const local = this.vel.toLocalPoint(e.clientX, e.clientY);
const timeStamp = this.timeStamp;
if (timeStamp) {
if (e.timeStamp - timeStamp < this.MOVEMENT_DETECTION_THRESHOLD) {
switch (this.action) {
case 'path-created': {
const translate = Dom.translate(this.startPointElement);
this.adjustControlPath(translate.tx, translate.ty, local.x, local.y);
break;
}
case 'awaiting-line-end':
case 'adjusting-curve-control-1': {
this.adjustLastSegment(local.x, local.y);
break;
}
case 'awaiting-curve-control-2': {
this.adjustLastSegment(local.x, local.y, null, null, local.x, local.y);
}
}
}
else {
switch (this.action) {
case 'path-created':
this.action = 'adjusting-curve-control-1';
break;
case 'awaiting-line-end':
this.replaceLastSegmentWithCurve();
this.action = 'adjusting-curve-control-2';
break;
case 'awaiting-curve-control-2':
this.action = 'adjusting-curve-control-2';
break;
case 'adjusting-curve-control-1': {
const translate = Dom.translate(this.startPointElement);
this.adjustControlPath(translate.tx, translate.ty, local.x, local.y);
break;
}
case 'adjusting-curve-control-2': {
const controlPoint = this.findControlPoint(local.x, local.y);
this.adjustLastSegment(null, null, null, null, controlPoint.x, controlPoint.y);
this.adjustControlPath(controlPoint.x, controlPoint.y, local.x, local.y);
}
}
}
}
else {
switch (this.action) {
case 'adjusting-line-end':
this.adjustLastSegment(local.x, local.y);
break;
case 'adjusting-curve-end':
this.adjustLastSegment(local.x, local.y, null, null, local.x, local.y);
}
}
}
}
onPointerUp(evt) {
this.timeStamp = null;
const e = this.normalizeEvent(evt);
e.stopPropagation();
if (this.isLeftMouseDown(e) && this.isSamePositionEvent(e)) {
const local = this.vel.toLocalPoint(e.clientX, e.clientY);
switch (this.action) {
case 'path-created':
case 'awaiting-line-end':
this.addLineSegment(local.x, local.y);
this.action = 'adjusting-line-end';
break;
case 'awaiting-curve-control-2':
this.removeControlPath();
this.addLineSegment(local.x, local.y);
this.action = 'adjusting-line-end';
break;
case 'adjusting-curve-control-1':
case 'adjusting-curve-control-2':
this.addCurveSegment(local.x, local.y, local.x, local.y);
this.action = 'adjusting-curve-end';
}
}
}
onStartPointMouseDown(evt) {
const e = this.normalizeEvent(evt);
e.stopPropagation();
if (this.isLeftMouseDown(e) && this.isSamePositionEvent(e)) {
this.closePath();
}
}
onDoubleClick(evt) {
const e = this.normalizeEvent(evt);
e.preventDefault();
e.stopPropagation();
if (this.isLeftMouseDown(e)) {
if (this.pathElement && 0 < this.numberOfVisibleSegments()) {
this.removeLastSegment();
this.finishPath('path:stop');
}
}
}
onContextMenu(evt) {
const e = this.normalizeEvent(evt);
e.preventDefault();
e.stopPropagation();
if (this.isSamePositionEvent(e)) {
if (this.pathElement && 0 < this.numberOfVisibleSegments()) {
this.removeLastSegment();
this.finishPath('path:stop');
}
}
}
isLeftMouseDown(e) {
return (e.which || 0) <= 1;
}
isSamePositionEvent(e) {
const originalEvent = e.originalEvent;
return originalEvent == null || originalEvent.detail <= 1;
}
}
(function (PathDrawer) {
PathDrawer.defaultOptions = {
pathAttributes: {
class: null,
fill: '#ffffff',
stroke: '#000000',
'stroke-width': 1,
'pointer-events': 'none',
},
startPointMarkup: '<circle r="5"/>',
snapRadius: 0,
};
PathDrawer.documentEvents = {
mousemove: 'onMouseMove',
touchmove: 'onMouseMove',
mouseup: 'onMouseUp',
touchend: 'onMouseUp',
touchcancel: 'onMouseUp',
};
})(PathDrawer || (PathDrawer = {}));
//# sourceMappingURL=drawer.js.map