n.onDrag(1,0));t=!0;break;case Constants.KEY_UP:this.currentHoverControllers.map(n=>n.onDrag(0,-1));t=!0;break;case Constants.KEY_LEFT:this.currentHoverControllers.map(n=>n.onDrag(-1,0));t=!0;break;case Constants.KEY_DOWN:this.currentHoverControllers.map(n=>n.onDrag(0,1));t=!0;break;case Constants.KEY_DELETE:this.currentHoverControllers.map(n=>n.onMouseLeave());diagramModel.removeShape(this.hoverShapeId);this.destroyShapeById(this.hoverShapeId);i=Helpers.getElement(this.hoverShapeId);i.parentNode.removeChild(i);this.currentHoverControllers=[];this.hoverShapeId=null;t=!0}return r&&t}onMouseDown(n){if(n.button==LEFT_MOUSE_BUTTON){n.preventDefault();var t=n.currentTarget.getAttribute("id");this.selectedShapeId=t;this.activeControllers=this.controllers[t];this.selectedControllers=this.controllers[t];this.mouseDown=!0;this.startDownX=n.clientX;this.startDownY=n.clientY;this.x=n.clientX;this.y=n.clientY;this.activeControllers.map(n=>n.onMouseDown())}}onMouseMove(n){n.preventDefault();this.mouseDown&&this.activeControllers!=null&&(this.dx=n.clientX-this.x,this.dy=n.clientY-this.y,this.x=n.clientX,this.y=n.clientY,this.activeControllers.map(n=>n.onDrag(this.dx,this.dy)))}onMouseUp(n){if(n.preventDefault(),n.button==LEFT_MOUSE_BUTTON&&this.activeControllers!=null){this.selectedShapeId=null;this.x=n.clientX;this.y=n.clientY;var t=this.isClick;this.activeControllers.map(n=>n.onMouseUp(t));this.clearSelectedObject();this.draggingToolboxShape&&this.finishDragAndDrop(this.shapeBeingDraggedAndDropped,n.currentTarget)}}onMouseEnter(n){n.preventDefault();var t=n.currentTarget.getAttribute("id");this.hoverShapeId=t;this.mouseDown||this.leavingId!=-1&&(console.log("Leaving "+this.leavingId),this.controllers[t][0].isAnchorController?console.log("Leaving shape to enter anchor."):(this.currentHoverControllers.map(n=>n.onMouseLeave()),console.log("Entering "+t+" => "+this.controllers[t].map(n=>n.constructor.name).join(", ")),this.currentHoverControllers=this.controllers[t],this.currentHoverControllers.map(n=>n.onMouseEnter())))}onMouseLeave(n){n.preventDefault();this.leavingId=n.currentTarget.getAttribute("id");this.hoverShapeId=null}getControllers(n){var t=n.currentTarget.getAttribute("id");return this.controllers[t]}getControllersById(n){return this.controllers[n]}getControllersByElement(n){var t=n.getAttribute("id");return this.getControllersById(t)}clearSelectedObject(){this.mouseDown=!1;this.activeControllers=null}finishDragAndDrop(n,t){Helpers.getElement(Constants.SVG_TOOLBOX_ID).removeChild(n);var i=n.getAttribute("id");this.controllers[i].map(n=>n.model.translate(-surfaceModel.tx+toolboxGroupController.model.tx,-surfaceModel.ty+toolboxGroupController.model.ty));Helpers.getElement(Constants.SVG_OBJECTS_ID).appendChild(n);i==t.getAttribute("id")&&(this.currentHoverControllers=this.controllers[i],this.currentHoverControllers.map(n=>n.onMouseEnter()));this.draggingToolboxShape=!1}}class ShapeController extends Controller{constructor(n,t,i){super(n,t,i)}getAnchors(){return[]}getConnectionPoints(){return[]}getCorners(){return[this.getULCorner(),this.getLRCorner()]}onDrag(n,t){super.onDrag(n,t)}get canConnectToShapes(){return!1}connect(){throw"Shape appears to be capable of connecting to other shapes but doesn't implement connect(idx, p).";}onMouseEnter(){!this.mouseController.mouseDown&&this.shouldShowAnchors&&anchorGroupController.showAnchors(this)}onMouseLeave(){this.shouldShowAnchors&&anchorGroupController.removeAnchors()}moveAnchor(n,t,i){n.translate(t,i)}adjustAnchorX(n,t){n.translate(t,0)}adjustAnchorY(n,t){n.translate(0,t)}}class ToolboxShapeController extends Controller{constructor(n,t,i){super(n,t,i)}get isToolboxShapeController(){return!0}onMouseUp(n){if(n){console.log("toolbox shape click");var t=this.createElementAt(270,130);diagramModel.addModel(t.model,t.view.id);t.model.translate(-surfaceModel.tx,-surfaceModel.ty);this.addToObjectsGroup(t);this.attachToMouseController(t)}}onDrag(){var n,t;this.mouseController.isClick||(console.log("toolbox shape onDrag"),n=this.createElementAt(this.mouseController.x-toolboxGroupController.model.tx,this.mouseController.y-toolboxGroupController.model.ty),diagramModel.addModel(n.model,n.view.id),this.addToToolboxGroup(n),t=this.attachToMouseController(n),this.mouseController.activeControllers=t,this.mouseController.draggingToolboxShape=!0,this.mouseController.shapeBeingDraggedAndDropped=n.el)}addToObjectsGroup(n){Helpers.getElement(Constants.SVG_OBJECTS_ID).appendChild(n.el)}addToToolboxGroup(n){Helpers.getElement(Constants.SVG_TOOLBOX_ID).appendChild(n.el)}attachToMouseController(n){return this.mouseController.attach(n.view,n.controller),this.mouseController.attach(n.view,anchorGroupController),[n.controller,anchorGroupController]}}class SurfaceController extends Controller{constructor(n,t,i){super(n,t,i)}get isSurfaceController(){return!0}get hasConnectionPoints(){return!1}onDrag(n,t){this.model.updateTranslation(n,t);var n=this.model.tx%this.model.gridCellW,t=this.model.ty%this.model.gridCellH;this.model.setTranslate(n,t)}onMouseLeave(){this.mouseController.clearSelectedObject()}}class ObjectsController extends Controller{constructor(n,t,i){super(n,t,i)}get isSurfaceController(){return!0}get hasConnectionPoints(){return!1}wireUpEvents(){}}class ToolboxGroupController extends Controller{constructor(n,t,i){super(n,t,i)}wireUpEvents(){}}class ToolboxSurfaceController extends Controller{constructor(n,t,i){super(n,t,i)}get isSurfaceController(){return!0}get hasConnectionPoints(){return!1}onDrag(n,t){toolboxGroupController.onDrag(n,t)}}class RectangleController extends ShapeController{constructor(n,t,i){super(n,t,i)}getAnchors(){var n=this.getCorners(),t=new Point((n[0].x+n[1].x)/2,n[0].y),i=new Point((n[0].x+n[1].x)/2,n[1].y),r=new Point(n[0].x,(n[0].y+n[1].y)/2),u=new Point(n[1].x,(n[0].y+n[1].y)/2);return[{anchor:t,onDrag:this.topMove.bind(this)},{anchor:i,onDrag:this.bottomMove.bind(this)},{anchor:r,onDrag:this.leftMove.bind(this)},{anchor:u,onDrag:this.rightMove.bind(this)}]}getConnectionPoints(){var n=this.getCorners(),t=new Point((n[0].x+n[1].x)/2,n[0].y),i=new Point((n[0].x+n[1].x)/2,n[1].y),r=new Point(n[0].x,(n[0].y+n[1].y)/2),u=new Point(n[1].x,(n[0].y+n[1].y)/2);return[{connectionPoint:t},{connectionPoint:i},{connectionPoint:r},{connectionPoint:u}]}getULCorner(){var n=new Point(this.model.x,this.model.y);return this.getAbsoluteLocation(n)}getLRCorner(){var n=new Point(this.model.x+this.model.width,this.model.y+this.model.height);return this.getAbsoluteLocation(n)}topMove(n,t,i,r){var u=this.model.y+r,f=this.model.height-r;this.model.y=u;this.model.height=f;this.moveAnchor(n[0],0,r);this.adjustAnchorY(n[2],r/2);this.adjustAnchorY(n[3],r/2);this.adjustConnectorsAttachedToConnectionPoint(0,r,0);this.adjustConnectorsAttachedToConnectionPoint(0,r/2,2);this.adjustConnectorsAttachedToConnectionPoint(0,r/2,3)}bottomMove(n,t,i,r){var u=this.model.height+r;this.model.height=u;this.moveAnchor(n[1],0,r);this.adjustAnchorY(n[2],r/2);this.adjustAnchorY(n[3],r/2);this.adjustConnectorsAttachedToConnectionPoint(0,r,1);this.adjustConnectorsAttachedToConnectionPoint(0,r/2,2);this.adjustConnectorsAttachedToConnectionPoint(0,r/2,3)}leftMove(n,t,i){var r=this.model.x+i,u=this.model.width-i;this.model.x=r;this.model.width=u;this.moveAnchor(n[2],i,0);this.adjustAnchorX(n[0],i/2);this.adjustAnchorX(n[1],i/2);this.adjustConnectorsAttachedToConnectionPoint(i,0,2);this.adjustConnectorsAttachedToConnectionPoint(i/2,0,0);this.adjustConnectorsAttachedToConnectionPoint(i/2,0,1)}rightMove(n,t,i){var r=this.model.width+i;this.model.width=r;this.moveAnchor(n[3],i,0);this.adjustAnchorX(n[0],i/2);this.adjustAnchorX(n[1],i/2);this.adjustConnectorsAttachedToConnectionPoint(i,0,3);this.adjustConnectorsAttachedToConnectionPoint(i/2,0,0);this.adjustConnectorsAttachedToConnectionPoint(i/2,0,1)}}class CircleController extends ShapeController{constructor(n,t,i){super(n,t,i)}getAnchors(){var n=this.getCorners(),t=new Point((n[0].x+n[1].x)/2,n[0].y),i=new Point((n[0].x+n[1].x)/2,n[1].y),r=new Point(n[0].x,(n[0].y+n[1].y)/2),u=new Point(n[1].x,(n[0].y+n[1].y)/2);return[{anchor:t,onDrag:this.topMove.bind(this)},{anchor:i,onDrag:this.bottomMove.bind(this)},{anchor:r,onDrag:this.leftMove.bind(this)},{anchor:u,onDrag:this.rightMove.bind(this)}]}getConnectionPoints(){var n=this.getCorners(),t=new Point((n[0].x+n[1].x)/2,n[0].y),i=new Point((n[0].x+n[1].x)/2,n[1].y),r=new Point(n[0].x,(n[0].y+n[1].y)/2),u=new Point(n[1].x,(n[0].y+n[1].y)/2);return[{connectionPoint:t},{connectionPoint:i},{connectionPoint:r},{connectionPoint:u}]}getULCorner(){var n=new Point(this.model.cx-this.model.r,this.model.cy-this.model.r);return this.getAbsoluteLocation(n)}getLRCorner(){var n=new Point(this.model.cx+this.model.r,this.model.cy+this.model.r);return this.getAbsoluteLocation(n)}topMove(n,t,i,r){this.changeRadius(-r);this.moveAnchor(n[0],0,r);this.moveAnchor(n[1],0,-r);this.moveAnchor(n[2],r,0);this.moveAnchor(n[3],-r,0);this.adjustConnectorsAttachedToConnectionPoint(0,r,0);this.adjustConnectorsAttachedToConnectionPoint(0,-r,1);this.adjustConnectorsAttachedToConnectionPoint(r,0,2);this.adjustConnectorsAttachedToConnectionPoint(-r,0,3)}bottomMove(n,t,i,r){this.changeRadius(r);this.moveAnchor(n[0],0,-r);this.moveAnchor(n[1],0,r);this.moveAnchor(n[2],-r,0);this.moveAnchor(n[3],r,0);this.adjustConnectorsAttachedToConnectionPoint(0,-r,0);this.adjustConnectorsAttachedToConnectionPoint(0,r,1);this.adjustConnectorsAttachedToConnectionPoint(-r,0,2);this.adjustConnectorsAttachedToConnectionPoint(r,0,3)}leftMove(n,t,i){this.changeRadius(-i);this.moveAnchor(n[0],0,i);this.moveAnchor(n[1],0,-i);this.moveAnchor(n[2],i,0);this.moveAnchor(n[3],-i,0);this.adjustConnectorsAttachedToConnectionPoint(0,i,0);this.adjustConnectorsAttachedToConnectionPoint(0,-i,1);this.adjustConnectorsAttachedToConnectionPoint(i,0,2);this.adjustConnectorsAttachedToConnectionPoint(-i,0,3)}rightMove(n,t,i){this.changeRadius(i);this.moveAnchor(n[0],0,-i);this.moveAnchor(n[1],0,i);this.moveAnchor(n[2],-i,0);this.moveAnchor(n[3],i,0);this.adjustConnectorsAttachedToConnectionPoint(0,-i,0);this.adjustConnectorsAttachedToConnectionPoint(0,i,1);this.adjustConnectorsAttachedToConnectionPoint(-i,0,2);this.adjustConnectorsAttachedToConnectionPoint(i,0,3)}changeRadius(n){this.model.r=this.model.r+n}}class DiamondController extends ShapeController{constructor(n,t,i){super(n,t,i)}getAnchors(){var n=this.getCorners(),t=new Point((n[0].x+n[1].x)/2,n[0].y),i=new Point((n[0].x+n[1].x)/2,n[1].y),r=new Point(n[0].x,(n[0].y+n[1].y)/2),u=new Point(n[1].x,(n[0].y+n[1].y)/2);return[{anchor:t,onDrag:this.topMove.bind(this)},{anchor:i,onDrag:this.bottomMove.bind(this)},{anchor:r,onDrag:this.leftMove.bind(this)},{anchor:u,onDrag:this.rightMove.bind(this)}]}getConnectionPoints(){var n=this.getCorners(),t=new Point((n[0].x+n[1].x)/2,n[0].y),i=new Point((n[0].x+n[1].x)/2,n[1].y),r=new Point(n[0].x,(n[0].y+n[1].y)/2),u=new Point(n[1].x,(n[0].y+n[1].y)/2);return[{connectionPoint:t},{connectionPoint:i},{connectionPoint:r},{connectionPoint:u}]}getULCorner(){var n=this.view.svgElement.getBoundingClientRect(),t=new Point(n.left,n.top);return Helpers.translateToSvgCoordinate(t)}getLRCorner(){var n=this.view.svgElement.getBoundingClientRect(),t=new Point(n.right,n.bottom);return Helpers.translateToSvgCoordinate(t)}topMove(n,t,i,r){var u=this.getULCorner(),f=this.getLRCorner();this.changeHeight(u,f,-r);this.moveAnchor(n[0],0,r);this.moveAnchor(n[1],0,-r);this.adjustConnectorsAttachedToConnectionPoint(0,r,0);this.adjustConnectorsAttachedToConnectionPoint(0,-r,1)}bottomMove(n,t,i,r){var u=this.getULCorner(),f=this.getLRCorner();this.changeHeight(u,f,r);this.moveAnchor(n[0],0,-r);this.moveAnchor(n[1],0,r);this.adjustConnectorsAttachedToConnectionPoint(0,-r,0);this.adjustConnectorsAttachedToConnectionPoint(0,r,1)}leftMove(n,t,i){var r=this.getULCorner(),u=this.getLRCorner();this.changeWidth(r,u,-i);this.moveAnchor(n[2],i,0);this.moveAnchor(n[3],-i,0);this.adjustConnectorsAttachedToConnectionPoint(i,0,2);this.adjustConnectorsAttachedToConnectionPoint(-i,0,3)}rightMove(n,t,i){var r=this.getULCorner(),u=this.getLRCorner();this.changeWidth(r,u,i);this.moveAnchor(n[2],-i,0);this.moveAnchor(n[3],i,0);this.adjustConnectorsAttachedToConnectionPoint(-i,0,2);this.adjustConnectorsAttachedToConnectionPoint(i,0,3)}changeWidth(n,t,i){n.x-=i;t.x+=i;this.updatePath(n,t)}changeHeight(n,t,i){n.y-=i;t.y+=i;this.updatePath(n,t)}updatePath(n,t){var n=this.getRelativeLocation(n),t=this.getRelativeLocation(t),r=(n.x+t.x)/2,u=(n.y+t.y)/2,i="M "+r+" "+n.y;i=i+" L "+n.x+" "+u;i=i+" L "+r+" "+t.y;i=i+" L "+t.x+" "+u;i=i+" Z";this.model.d=i}}class LineController extends ShapeController{constructor(n,t,i){super(n,t,i)}get canConnectToShapes(){return!0}onDrag(n,t){super.onDrag(n,t);diagramModel.disconnect(this.view.id,0);diagramModel.disconnect(this.view.id,1)}connect(n,t){switch(n){case 0:this.model.x1=t.x;this.model.y1=t.y;break;case 1:this.model.x2=t.x;this.model.y2=t.y}}translateEndpoint(n,t,i){var r;switch(n){case 0:r=new Point(this.model.x1,this.model.y1);r=r.translate(t,i);this.model.x1=r.x;this.model.y1=r.y;break;case 1:r=new Point(this.model.x2,this.model.y2);r=r.translate(t,i);this.model.x2=r.x;this.model.y2=r.y}}getAnchors(){var n=this.getCorners();return[{anchor:n[0],onDrag:this.moveULCorner.bind(this)},{anchor:n[1],onDrag:this.moveLRCorner.bind(this)}]}getULCorner(){var n=new Point(this.model.x1,this.model.y1);return this.getAbsoluteLocation(n)}getLRCorner(){var n=new Point(this.model.x2,this.model.y2);return this.getAbsoluteLocation(n)}moveULCorner(n,t,i,r){this.model.x1=this.model.x1+i;this.model.y1=this.model.y1+r;this.moveAnchor(t,i,r);diagramModel.disconnect(this.view.id,0)}moveLRCorner(n,t,i,r){this.model.x2=this.model.x2+i;this.model.y2=this.model.y2+r;this.moveAnchor(t,i,r);diagramModel.disconnect(this.view.id,1)}}class TextController extends ShapeController{constructor(n,t,i){super(n,t,i)}get shouldShowAnchors(){return!1}getConnectionPoints(){var n=this.getCorners(),t=new Point((n[0].x+n[1].x)/2,n[0].y),i=new Point((n[0].x+n[1].x)/2,n[1].y),r=new Point(n[0].x,(n[0].y+n[1].y)/2),u=new Point(n[1].x,(n[0].y+n[1].y)/2);return[{connectionPoint:t},{connectionPoint:i},{connectionPoint:r},{connectionPoint:u}]}onMouseDown(n){super.onMouseDown(n);var t=this.model.text;document.getElementById("text").value=t}getULCorner(){var n=this.view.svgElement.getBoundingClientRect(),t=new Point(n.left,n.top);return Helpers.translateToSvgCoordinate(t)}getLRCorner(){var n=this.view.svgElement.getBoundingClientRect(),t=new Point(n.right,n.bottom);return Helpers.translateToSvgCoordinate(t)}}class ToolboxRectangleController extends ToolboxShapeController{constructor(n,t,i){super(n,t,i)}createElementAt(n,t){var r=Helpers.createElement("g",{}),e=Helpers.createElement("rect",{x:n-30,y:t-30,width:60,height:60,fill:"#FFFFFF",stroke:"black","stroke-width":1}),i,u,f;return r.appendChild(e),i=new RectangleModel,i._x=n-30,i._y=t-30,i._width=60,i._height=60,u=new ShapeView(r,i),f=new RectangleController(this.mouseController,u,i),{el:r,model:i,view:u,controller:f}}}class ToolboxCircleController extends ToolboxShapeController{constructor(n,t,i){super(n,t,i)}createElementAt(n,t){var u=Helpers.createElement("circle",{cx:n,cy:t,r:30,fill:"#FFFFFF",stroke:"black","stroke-width":1}),i=new CircleModel,r,f;return i._cx=n,i._cy=t,i._r=30,r=new ShapeView(u,i),f=new CircleController(this.mouseController,r,i),{el:u,model:i,view:r,controller:f}}}class ToolboxDiamondController extends ToolboxShapeController{constructor(n,t,i){super(n,t,i)}createElementAt(n,t){var o=[{cmd:"M",x:n-15,y:t-30},{cmd:"L",x:n-45,y:t},{cmd:"L",x:n-15,y:t+30},{cmd:"L",x:n+15,y:t}],r=o.reduce((n,t)=>n=n+t.cmd+" "+t.x+" "+t.y,""),u,i,f,e;return r=r+" Z",u=Helpers.createElement("path",{d:r,stroke:"black","stroke-width":1,fill:"#FFFFFF"}),i=new DiamondModel,i._d=r,f=new ShapeView(u,i),e=new DiamondController(this.mouseController,f,i),{el:u,model:i,view:f,controller:e}}}class ToolboxLineController extends ToolboxShapeController{constructor(n,t,i){super(n,t,i)}createElementAt(n,t){var u=n-30,f=t-30,e=n+30,o=t+30,r=Helpers.createElement("g",{}),i,s,h;return r.appendChild(Helpers.createElement("line",{x1:u,y1:f,x2:e,y2:o,"stroke-width":20,stroke:"black","stroke-opacity":"0","fill-opacity":"0"})),r.appendChild(Helpers.createElement("line",{x1:u,y1:f,x2:e,y2:o,fill:"#FFFFFF",stroke:"black","stroke-width":1})),i=new LineModel,i._x1=u,i._y1=f,i._x2=e,i._y2=o,s=new LineView(r,i),h=new LineController(this.mouseController,s,i),{el:r,model:i,view:s,controller:h}}}class ToolboxTextController extends ToolboxShapeController{constructor(n,t,i){super(n,t,i)}createElementAt(n,t){var r=Helpers.createElement("text",{x:n,y:t,"font-size":12,"font-family":"Verdana"}),i,u,f;return r.innerHTML=Constants.DEFAULT_TEXT,i=new TextModel,i._x=n,i._y=t,i._text=Constants.DEFAULT_TEXT,u=new TextView(r,i),f=new TextController(this.mouseController,u,i),{el:r,model:i,view:u,controller:f}}}
--------------------------------------------------------------------------------
/flowSharpWeb.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
42 |
43 |
44 |
45 |
46 | FlowSharpWeb
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 | Text:
118 |
119 |
120 |
121 |
184 |
185 |
191 |
192 |
193 |
408 |
455 |
--------------------------------------------------------------------------------
/helpers.js:
--------------------------------------------------------------------------------
1 | class Helpers {
2 | // From SO: https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
3 | static uuidv4() {
4 | return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g,
5 | c => (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16))
6 | }
7 |
8 | // https://stackoverflow.com/questions/17824145/parse-svg-transform-attribute-with-javascript
9 | static parseTransform(transform) {
10 | var transforms = {};
11 | for (var i in a = transform.match(/(\w+\((\-?\d+\.?\d*e?\-?\d*,?)+\))+/g)) {
12 | var c = a[i].match(/[\w\.\-]+/g);
13 | transforms[c.shift()] = c;
14 | }
15 |
16 | return transforms;
17 | }
18 |
19 | static getElement(id) {
20 | var svg = document.getElementById(Constants.SVG_ELEMENT_ID);
21 | var el = svg.getElementById(id);
22 |
23 | return el;
24 | }
25 |
26 | static getElements(className) {
27 | var svg = document.getElementById(Constants.SVG_ELEMENT_ID);
28 | var els = svg.getElementsByClassName(className);
29 |
30 | return els;
31 | }
32 |
33 | // Create the specified element with the attributes provided in a key-value dictionary.
34 | static createElement(elementName, attributes, createClass = true) {
35 | var el = document.createElementNS(Constants.SVG_NS, elementName);
36 |
37 | // Create a unique ID for the element so we can acquire the correct shape controller
38 | // when the user drags the shape.
39 | el.setAttributeNS(null, "id", Helpers.uuidv4());
40 |
41 | if (createClass) {
42 | // Create a class common to all shapes so that, on file load, we can get them all and re-attach them
43 | // to the mouse controller.
44 | el.setAttributeNS(null, "class", Constants.SHAPE_CLASS_NAME);
45 | }
46 |
47 | // Add the attributes to the element.
48 | Object.entries(attributes).map(([key, val]) => {
49 | if (key == "href") {
50 | el.setAttributeNS("http://www.w3.org/1999/xlink", key, val);
51 |
52 | } else {
53 | el.setAttributeNS(null, key, val);
54 | }
55 | });
56 |
57 | //Object.entries(attributes).map(([key, val]) => {
58 | // console.log("ATTR: " + key);
59 | // el.setAttributeNS(null, key, val);
60 | //});
61 |
62 | return el;
63 | }
64 |
65 | // https://stackoverflow.com/questions/22183727/how-do-you-convert-screen-coordinates-to-document-space-in-a-scaled-svg
66 | static translateToSvgCoordinate(p) {
67 | var svg = document.getElementById(Constants.SVG_ELEMENT_ID);
68 | var pt = svg.createSVGPoint();
69 | var offset = pt.matrixTransform(svg.getScreenCTM().inverse());
70 | p = p.translate(offset.x, offset.y);
71 |
72 | return p;
73 | }
74 |
75 | static translateToScreenCoordinate(p) {
76 | var svg = document.getElementById(Constants.SVG_ELEMENT_ID);
77 | var pt = svg.createSVGPoint();
78 | var offset = pt.matrixTransform(svg.getScreenCTM());
79 | p = p.translate(-offset.x, -offset.y);
80 |
81 | return p;
82 | }
83 |
84 | static getNearbyShapes(p) {
85 | // https://stackoverflow.com/questions/2174640/hit-testing-svg-shapes
86 | // var el = document.elementFromPoint(evt.clientX, evt.clientY);
87 | // console.log(el);
88 |
89 | var svg = document.getElementById("svg");
90 | var hitRect = svg.createSVGRect();
91 | hitRect.x = p.x - Constants.NEARBY_DELTA / 2;
92 | hitRect.y = p.y - Constants.NEARBY_DELTA / 2;
93 | hitRect.height = Constants.NEARBY_DELTA;
94 | hitRect.width = Constants.NEARBY_DELTA;
95 | var nodeList = svg.getIntersectionList(hitRect, null);
96 |
97 | var nearShapes = [];
98 |
99 | for (var i = 0; i < nodeList.length; i++) {
100 | // get only nodes that are shapes.
101 | if (nodeList[i].getAttribute("class") == Constants.SHAPE_CLASS_NAME) {
102 | nearShapes.push(nodeList[i]);
103 | }
104 | }
105 |
106 | return nearShapes;
107 | }
108 |
109 | // https://stackoverflow.com/questions/3955229/remove-all-child-elements-of-a-dom-node-in-javascript
110 | static removeChildren(node) {
111 | while (node.firstChild) {
112 | node.removeChild(node.firstChild);
113 | }
114 | }
115 |
116 | static isNear(p1, p2, delta) {
117 | return Math.abs(p1.x - p2.x) <= delta && Math.abs(p1.y - p2.y) <= delta;
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/models/AnchorModel.js:
--------------------------------------------------------------------------------
1 | class AnchorModel extends RectangleModel {
2 | constructor() {
3 | super(null);
4 | }
5 |
6 | // Anchors are not user-selectable shapes.
7 | get isShape() { return false; }
8 | }
--------------------------------------------------------------------------------
/models/circleModel.js:
--------------------------------------------------------------------------------
1 | class CircleModel extends ShapeModel {
2 | constructor() {
3 | super(Constants.SHAPE_CIRCLE);
4 | this._cx = 0;
5 | this._cy = 0;
6 | this._r = 0;
7 | }
8 |
9 | get isShape() { return true; }
10 |
11 | serialize() {
12 | var model = super.serialize();
13 | model.cx = this._cx;
14 | model.cy = this._cy;
15 | model.r = this._r;
16 |
17 | return { Circle: model };
18 | }
19 |
20 | deserialize(model, el) {
21 | super.deserialize(model, el);
22 | this.cx = model.cx;
23 | this.cy = model.cy;
24 | this.r = model.r;
25 | }
26 |
27 | getProperties() {
28 | return [
29 | { propertyName: 'cx', label: 'CX', column: 0, row: 0, getter: () => this.cx + this.tx },
30 | { propertyName: 'cy', label: 'CY', column: 1, row: 0, getter: () => this.cy + this.ty },
31 | { propertyName: 'r', label: 'Radius', column: 0, row: 1, getter: () => this.r },
32 | { propertyName: 'tx', alias: 'cx', getter: () => this.cx + this.tx },
33 | { propertyName: 'ty', alias: 'cy', getter: () => this.cy + this.ty },
34 | ];
35 | }
36 |
37 | get cx() { return this._cx; }
38 | get cy() { return this._cy; }
39 | get r() { return this._r; }
40 |
41 | set cx(value) {
42 | this._cx = value;
43 | this.propertyChanged("cx", value);
44 | }
45 |
46 | set cy(value) {
47 | this._cy = value;
48 | this.propertyChanged("cy", value);
49 | }
50 |
51 | set r(value) {
52 | this._r = value;
53 | this.propertyChanged("r", value);
54 | }
55 | }
--------------------------------------------------------------------------------
/models/diagramModel.js:
--------------------------------------------------------------------------------
1 | class DiagramModel {
2 | constructor(mouseController) {
3 | this.mouseController = mouseController;
4 | this.models = [];
5 | this.mvc = {
6 | Rectangle: { model: RectangleModel, view: ShapeView, controller: RectangleController, creator : () => this.createElement("rect") },
7 | Circle: { model: CircleModel, view: ShapeView, controller: CircleController, creator: () => this.createElement("circle") },
8 | Diamond: { model: DiamondModel, view: ShapeView, controller: DiamondController, creator: () => this.createElement("path") },
9 | Line: { model: LineModel, view: LineView, controller: LineController, creator: () => this.createLineElement() },
10 | LineWithStart: { model: LineModelWithStart, view: LineView, controller: LineController, creator: () => this.createLineWithStartElement() },
11 | LineWithStartEnd: { model: LineModelWithStartEnd, view: LineView, controller: LineController, creator: () => this.createLineWithStartEndElement() },
12 | Text: { model: TextModel, view: TextView, controller: TextController, creator: () => this.createTextElement() },
13 | Image: { model: ImageModel, view: ShapeView, controller: ImageController, creator: () => this.createImageElement() },
14 | };
15 |
16 | // For the moment we'll use array indices into the shape's connection points.
17 | // This is problematic when the feature is added so that the user can add/remove connection points.
18 | // In that case, each shape should create its default connection points with associated id's.
19 | // As for the line, it should be OK to always use the endpoint index.
20 | // Connection structure:
21 | // shapeId, lineId, shapeConnectionPointIndex, lineEndpointIndex
22 | this.connections = [];
23 | }
24 |
25 | clear() {
26 | this.models = [];
27 | this.connections = [];
28 | }
29 |
30 | addModel(model, id) {
31 | this.models.push({ model: model, id: id });
32 | }
33 |
34 | connect(shapeId, lineId, shapeCPIdx, lineAnchorIdx) {
35 | this.connections.push({ shapeId: shapeId, lineId: lineId, shapeCPIdx: shapeCPIdx, lineAnchorIdx: lineAnchorIdx });
36 | }
37 |
38 | // Disconnect any connections associated with the line and anchor index.
39 | disconnect(lineId, lineAnchorIdx) {
40 | this.connections = this.connections.filter(c => !(c.lineId == lineId && c.lineAnchorIdx == lineAnchorIdx));
41 | }
42 |
43 | // remove connections of this shape as both the "connected to" shape (shapeId) and the "connecting" shape (lineId).
44 | removeShape(shapeId) {
45 | this.connections = this.connections.filter(c => !(c.shapeId == shapeId || c.lineId == shapeId));
46 | this.models = this.models.filter(m => m.id != shapeId);
47 | }
48 |
49 | createElement(elName) {
50 | var group = Helpers.createElement("g", {});
51 | var el = Helpers.createElement(elName, { fill: "#FFFFFF", stroke: "black", "stroke-width": 1 });
52 | group.appendChild(el);
53 |
54 | return group;
55 | }
56 |
57 | createTextElement() {
58 | var group = Helpers.createElement("g", {});
59 | var el = Helpers.createElement('text', { "font-size": 12, "font-family": "Verdana" });
60 | el.innerHTML = Constants.DEFAULT_TEXT;
61 | group.appendChild(el);
62 |
63 | return group;
64 | }
65 |
66 | createImageElement() {
67 | var group = Helpers.createElement("g", {});
68 | var el = Helpers.createElement('image', {});
69 | group.appendChild(el);
70 |
71 | return group;
72 | }
73 |
74 | createLineElement(elName) {
75 | var group = Helpers.createElement("g", {});
76 | var el = Helpers.createElement('g', {});
77 | el.appendChild(Helpers.createElement('line', {"stroke-width": 20, stroke: "black", "stroke-opacity": "0", "fill-opacity": "0" }));
78 | el.appendChild(Helpers.createElement('line', {fill: "#FFFFFF", stroke: "black", "stroke-width": 1 }));
79 | group.appendChild(el);
80 |
81 | return group;
82 | }
83 |
84 | createLineWithStartElement(elName) {
85 | var group = Helpers.createElement("g", {});
86 | var el = Helpers.createElement('g', {});
87 | el.appendChild(Helpers.createElement('line', { "stroke-width": 20, stroke: "black", "stroke-opacity": "0", "fill-opacity": "0" }));
88 | el.appendChild(Helpers.createElement('line', { fill: "#FFFFFF", stroke: "black", "stroke-width": 1, "marker-start": "url(#trianglestart)" }));
89 | group.appendChild(el);
90 |
91 | return group;
92 | }
93 |
94 | createLineWithStartEndElement(elName) {
95 | var group = Helpers.createElement("g", {});
96 | var el = Helpers.createElement('g', {});
97 | el.appendChild(Helpers.createElement('line', { "stroke-width": 20, stroke: "black", "stroke-opacity": "0", "fill-opacity": "0" }));
98 | el.appendChild(Helpers.createElement('line', { fill: "#FFFFFF", stroke: "black", "stroke-width": 1, "marker-start": "url(#trianglestart)", "marker-end": "url(#triangleend)" }));
99 | group.appendChild(el);
100 |
101 | return group;
102 | }
103 |
104 | // Returns JSON of serialized models.
105 | serialize() {
106 | var uberModel = [];
107 | var model = surfaceModel.serialize();
108 | model[Object.keys(model)[0]].id = Constants.SVG_SURFACE_ID;
109 | uberModel.push(model);
110 |
111 | this.models.map(m => {
112 | var model = m.model.serialize();
113 | model[Object.keys(model)[0]].id = m.id;
114 | uberModel.push(model);
115 | });
116 |
117 | return JSON.stringify({ model: uberModel, connections: this.connections });
118 | }
119 |
120 | // Creates an MVC for each model of the provided JSON.
121 | deserialize(jsonString) {
122 | var modelData = JSON.parse(jsonString);
123 | var models = modelData.model;
124 | this.connections = modelData.connections;
125 | var objectModels = [];
126 | surfaceModel.setTranslation(0, 0);
127 | objectsModel.setTranslation(0, 0);
128 |
129 | models.map(model => {
130 | var key = Object.keys(model)[0];
131 | var val = model[key];
132 |
133 | if (key == "Surface") {
134 | // Special handler for surface, we keep the existing MVC objects.
135 | // We set both the surface and objects translation, but the surface translation
136 | // is mod'd by the gridCellW/H.
137 | surfaceModel.deserialize(val);
138 | objectsModel.setTranslation(surfaceModel.tx, surfaceModel.ty);
139 | } else {
140 | var model = new this.mvc[key].model();
141 | objectModels.push(model);
142 | var el = this.mvc[key].creator();
143 | // Create the view first so it hooks into the model's property change event.
144 | var view = new this.mvc[key].view(el, model);
145 | model.deserialize(val, el);
146 | view.id = val.id;
147 | var controller = new this.mvc[key].controller(mouseController, view, model);
148 |
149 | // Update our diagram's model collection.
150 | this.models.push({ model: model, id: val.id });
151 |
152 | Helpers.getElement(Constants.SVG_OBJECTS_ID).appendChild(el);
153 | this.mouseController.attach(view, controller);
154 |
155 | // Most shapes also need an anchor controller. An exception is the Text shape, at least for now.
156 | if (controller.shouldShowAnchors) {
157 | this.mouseController.attach(view, anchorGroupController);
158 | }
159 | }
160 | });
161 | }
162 | }
163 |
--------------------------------------------------------------------------------
/models/diamondModel.js:
--------------------------------------------------------------------------------
1 | class DiamondModel extends PathModel {
2 | constructor() {
3 | super(Constants.SHAPE_DIAMOND);
4 | }
5 |
6 | get isShape() { return true; }
7 |
8 | serialize() {
9 | var model = super.serialize();
10 |
11 | return { Diamond: model };
12 | }
13 | }
--------------------------------------------------------------------------------
/models/imageModel.js:
--------------------------------------------------------------------------------
1 | class ImageModel extends ShapeModel {
2 | constructor(shapeName = Constants.SHAPE_IMAGE) {
3 | super(shapeName);
4 | this._x = 0;
5 | this._y = 0;
6 | this._width = 0;
7 | this._height = 0;
8 | this._href = "";
9 | }
10 |
11 | get isShape() { return true; }
12 |
13 | serialize() {
14 | var model = super.serialize();
15 | model.x = this._x;
16 | model.y = this._y;
17 | model.width = this._width;
18 | model.height = this._height;
19 | model.href = this._href;
20 |
21 | return { Image: model };
22 | }
23 |
24 | deserialize(model, el) {
25 | super.deserialize(model, el);
26 | this.x = model.x;
27 | this.y = model.y;
28 | this.width = model.width;
29 | this.height = model.height;
30 | this.href = model.href;
31 | }
32 |
33 | getProperties() {
34 | return [
35 | { propertyName: 'x', label: 'X', column: 0, row: 0, getter: () => this.x + this.tx },
36 | { propertyName: 'y', label: 'Y', column: 1, row: 0, getter: () => this.y + this.ty },
37 | { propertyName: 'width', label: 'Width', column: 0, row: 1, getter: () => this.width },
38 | { propertyName: 'height', label: 'Height', column: 1, row: 1, getter: () => this.height },
39 | { propertyName: 'href', label: 'HREF', column: 0, row: 2, getter: () => this.href },
40 | { propertyName: 'tx', alias: 'x', getter: () => this.x + this.tx },
41 | { propertyName: 'ty', alias: 'y', getter: () => this.y + this.ty },
42 | ];
43 | }
44 |
45 | get x() { return this._x; }
46 | get y() { return this._y; }
47 | get width() { return this._width; }
48 | get height() { return this._height; }
49 | get href() { return this._href;}
50 |
51 | set x(value) {
52 | this._x = value;
53 | this.propertyChanged("x", value);
54 | }
55 |
56 | set y(value) {
57 | this._y = value;
58 | this.propertyChanged("y", value);
59 | }
60 |
61 | set width(value) {
62 | this._width = value;
63 | this.propertyChanged("width", value);
64 | }
65 |
66 | set height(value) {
67 | this._height = value;
68 | this.propertyChanged("height", value);
69 | }
70 |
71 | set href(value) {
72 | this._href = value;
73 | this.propertyChanged("href", value);
74 | }
75 | }
--------------------------------------------------------------------------------
/models/lineModel.js:
--------------------------------------------------------------------------------
1 | class LineModel extends ShapeModel {
2 | constructor() {
3 | super(Constants.SHAPE_LINE);
4 | this._x1 = 0;
5 | this._y1 = 0;
6 | this._x2 = 0;
7 | this._y2 = 0;
8 | }
9 |
10 | get isShape() { return true; }
11 |
12 | serialize() {
13 | var model = super.serialize();
14 | model.x1 = this._x1;
15 | model.y1 = this._y1;
16 | model.x2 = this._x2;
17 | model.y2 = this._y2;
18 |
19 | return { Line: model };
20 | }
21 |
22 | deserialize(model, el) {
23 | super.deserialize(model, el);
24 | this.x1 = model.x1;
25 | this.y1 = model.y1;
26 | this.x2 = model.x2;
27 | this.y2 = model.y2;
28 | }
29 |
30 | getProperties() {
31 | return [
32 | { propertyName: 'x1', label: 'X1', column: 0, row: 0, getter: () => this.x1 + this.tx },
33 | { propertyName: 'y1', label: 'Y1', column: 1, row: 0, getter: () => this.y1 + this.ty },
34 | { propertyName: 'x2', label: 'X2', column: 0, row: 1, getter: () => this.x2 + this.tx },
35 | { propertyName: 'y2', label: 'Y2', column: 1, row: 1, getter: () => this.y2 + this.ty },
36 | { propertyName: 'tx', alias: 'x1', getter: () => this.x1 + this.tx },
37 | { propertyName: 'ty', alias: 'y1', getter: () => this.y1 + this.ty },
38 | { propertyName: 'tx', alias: 'x2', getter: () => this.x2 + this.tx },
39 | { propertyName: 'ty', alias: 'y2', getter: () => this.y2 + this.ty },
40 | ];
41 | }
42 |
43 | get x1() { return this._x1; }
44 | get y1() { return this._y1; }
45 | get x2() { return this._x2; }
46 | get y2() { return this._y2; }
47 |
48 | set x1(value) {
49 | this._x1 = value;
50 | this.propertyChanged("x1", value);
51 | }
52 |
53 | set y1(value) {
54 | this._y1 = value;
55 | this.propertyChanged("y1", value);
56 | }
57 |
58 | set x2(value) {
59 | this._x2 = value;
60 | this.propertyChanged("x2", value);
61 | }
62 |
63 | set y2(value) {
64 | this._y2 = value;
65 | this.propertyChanged("y2", value);
66 | }
67 | }
68 |
69 | // Overrides so we can specify the key for the model.
70 |
71 | class LineModelWithStart extends LineModel {
72 | serialize() {
73 | var model = this.baseSerialize();
74 | model.x1 = this._x1;
75 | model.y1 = this._y1;
76 | model.x2 = this._x2;
77 | model.y2 = this._y2;
78 |
79 | return { LineWithStart: model };
80 | }
81 | }
82 |
83 | class LineModelWithStartEnd extends LineModel {
84 | serialize() {
85 | var model = this.baseSerialize();
86 | model.x1 = this._x1;
87 | model.y1 = this._y1;
88 | model.x2 = this._x2;
89 | model.y2 = this._y2;
90 |
91 | return { LineWithStartEnd: model };
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/models/model.js:
--------------------------------------------------------------------------------
1 | // See static initializer at bottom of class definition!
2 | // Model.idCount = 0;
3 |
4 | class Model {
5 | constructor(shapeName) {
6 | this.eventPropertyChanged = new Event();
7 |
8 | // Certain shapes (like anchors, surface, etc.) are temporary so we don't want to increment the model ID.
9 | if (shapeName != null) {
10 | this._shapeName = shapeName;
11 | this._shapeId = shapeName + '.' + Model.idCount;
12 | Model.idCount += 1;
13 | }
14 |
15 | this._tx = 0;
16 | this._ty = 0;
17 | }
18 |
19 | // By default, we assume the model is not actually a shape. Only circle, diamond, line, rectangle, text, and other "shapes" are shapes.
20 | get isShape() { return false;}
21 |
22 | get tx() { return this._tx; }
23 | get ty() { return this._ty; }
24 | get shapeName() { return this._shapeName; }
25 | get shapeId() { return this._shapeId; }
26 |
27 | propertyChanged(propertyName, value) {
28 | // console.log(propertyName + " = " + value);
29 | this.eventPropertyChanged.fire(this, {propertyName : propertyName, value : value})
30 | }
31 |
32 | getProperties() {
33 | return [];
34 | }
35 |
36 | serialize() {
37 | return { tx: this._tx, ty: this._ty, shapeName: this._shapeName, shapeId: this._shapeId };
38 | }
39 |
40 | // Used to skip the ShapeModel's serializer in derived Line classes with start/end arrows.
41 | // Sort of annoying to have to do this.
42 | baseSerialize() {
43 | return { tx: this._tx, ty: this._ty, shapeName: this._shapeName, shapeId: this._shapeId };
44 | }
45 |
46 | deserialize(model, el) {
47 | this._tx = model.tx;
48 | this._ty = model.ty;
49 | this._shapeName = model.shapeName;
50 | this.setTranslate(this._tx, this._ty);
51 | }
52 |
53 | translate(x, y) {
54 | this._tx += x;
55 | this._ty += y;
56 | this.propertyChanged("tx", this._tx);
57 | this.propertyChanged("ty", this._ty);
58 | this.setTranslate(this._tx, this._ty);
59 | }
60 |
61 | // Update our internal translation and set the translation immediately.
62 | setTranslation(x, y) {
63 | this._tx = x;
64 | this._ty = y;
65 | this.propertyChanged("tx", this._tx);
66 | this.propertyChanged("ty", this._ty);
67 | this.setTranslate(x, y);
68 | }
69 |
70 | // Deferred translation -- this only updates _tx and _ty
71 | // Used when we want to internally maintain the true _tx and _ty
72 | // but set the translation to a modulus, as in when translating
73 | // the grid.
74 | updateTranslation(dx, dy) {
75 | this._tx += dx;
76 | this._ty += dy;
77 | this.propertyChanged("tx", this._tx);
78 | this.propertyChanged("ty", this._ty);
79 | }
80 |
81 | // Sets the "translate" portion of the "transform" property.
82 | // All models have a translation. Notice we do not use _tx, _ty here
83 | // nor do we set _tx, _ty to (x, y) because (x, y) might be mod'ed by
84 | // the grid (w, h). We want to use exactly the parameters passed in
85 | // without modifying our model.
86 | // See SurfaceController.onDrag and note how the translation is updated
87 | // but setTranslate is called with the mod'ed (x, y) coordinates.
88 | setTranslate(x, y) {
89 | this.translation = "translate(" + x + "," + y + ")";
90 | this.transform = this.translation;
91 | }
92 |
93 | // TODO: Later to be extended to build the transform so that it includes rotation and other things we can do.
94 | set transform(value) {
95 | this._transform = value;
96 | this.propertyChanged("transform", value);
97 | }
98 |
99 | set tx(value) {
100 | this._tx = value;
101 | this.propertyChanged("tx", value);
102 | this.translation = "translate(" + this._tx + "," + this._ty + ")";
103 | this.transform = this.translation;
104 | }
105 |
106 | set ty(value) {
107 | this._ty = value;
108 | this.propertyChanged("ty", value);
109 | this.translation = "translate(" + this._tx + "," + this._ty + ")";
110 | this.transform = this.translation;
111 | }
112 | }
113 |
114 | Model.idCount = 0;
115 |
--------------------------------------------------------------------------------
/models/objectsModel.js:
--------------------------------------------------------------------------------
1 | class ObjectsModel extends Model {
2 | constructor() {
3 | super();
4 | }
5 |
6 | get actualElement() {
7 | return this.svgElement;
8 | }
9 | }
--------------------------------------------------------------------------------
/models/pathModel.js:
--------------------------------------------------------------------------------
1 | class PathModel extends ShapeModel {
2 | constructor(shapeName) {
3 | super(shapeName);
4 | this._d = null;
5 | }
6 |
7 | serialize() {
8 | var model = super.serialize();
9 | model.d = this._d;
10 |
11 | return model;
12 | }
13 |
14 | deserialize(model, el) {
15 | super.deserialize(model, el);
16 | this.d = model.d;
17 | }
18 |
19 | get d() { return this._d; }
20 |
21 | set d(value) {
22 | this._d = value;
23 | this.propertyChanged("d", value);
24 | }
25 | }
26 |
27 |
--------------------------------------------------------------------------------
/models/rectangleModel.js:
--------------------------------------------------------------------------------
1 | class RectangleModel extends ShapeModel {
2 | constructor(shapeName = Constants.SHAPE_RECTANGLE) {
3 | super(shapeName);
4 | this._x = 0;
5 | this._y = 0;
6 | this._width = 0;
7 | this._height = 0;
8 | }
9 |
10 | get isShape() { return true; }
11 |
12 | serialize() {
13 | var model = super.serialize();
14 | model.x = this._x;
15 | model.y = this._y;
16 | model.width = this._width;
17 | model.height = this._height;
18 |
19 | return { Rectangle: model };
20 | }
21 |
22 | deserialize(model, el) {
23 | super.deserialize(model, el);
24 | this.x = model.x;
25 | this.y = model.y;
26 | this.width = model.width;
27 | this.height = model.height;
28 | }
29 |
30 | getProperties() {
31 | return [
32 | { propertyName: 'x', label: 'X', column: 0, row: 0, getter: () => this.x + this.tx},
33 | { propertyName: 'y', label: 'Y', column: 1, row: 0, getter: () => this.y + this.ty},
34 | { propertyName: 'width', label: 'Width', column: 0, row: 1, getter: () => this.width },
35 | { propertyName: 'height', label: 'Height', column: 1, row: 1, getter: () => this.height },
36 | { propertyName: 'tx', alias: 'x', getter: () => this.x + this.tx },
37 | { propertyName: 'ty', alias: 'y', getter: () => this.y + this.ty },
38 | ];
39 | }
40 |
41 | get x() { return this._x; }
42 | get y() { return this._y; }
43 | get width() { return this._width; }
44 | get height() { return this._height; }
45 |
46 | set x(value) {
47 | this._x = value;
48 | this.propertyChanged("x", value);
49 | }
50 |
51 | set y(value) {
52 | this._y = value;
53 | this.propertyChanged("y", value);
54 | }
55 |
56 | set width(value) {
57 | this._width = value;
58 | this.propertyChanged("width", value);
59 | }
60 |
61 | set height(value) {
62 | this._height = value;
63 | this.propertyChanged("height", value);
64 | }
65 | }
--------------------------------------------------------------------------------
/models/shapeModel.js:
--------------------------------------------------------------------------------
1 | class ShapeModel extends Model {
2 | constructor(shapeName) {
3 | super(shapeName);
4 | }
5 | }
--------------------------------------------------------------------------------
/models/surfaceModel.js:
--------------------------------------------------------------------------------
1 | class SurfaceModel extends Model {
2 | constructor() {
3 | super(Constants.SHAPE_SURFACE);
4 | this.gridCellW = 80;
5 | this.gridCellH = 80;
6 | this.cellW = 8;
7 | this.cellH = 8;
8 | }
9 |
10 | get actualElement() {
11 | return this.svgElement;
12 | }
13 |
14 | serialize() {
15 | var model = super.serialize();
16 | model.gridCellW = this.gridCellW;
17 | model.gridCellH = this.gridCellH;
18 | model.cellW = this.cellW;
19 | model.cellH = this.cellH;
20 |
21 | return { Surface: model };
22 | }
23 |
24 | deserialize(model, el) {
25 | // DO NOT CALL BASE METHOD. Surface translations are mod'd by the gridCellW/H
26 | this.gridCellW = model.gridCellW;
27 | this.gridCellH = model.gridCellH;
28 | this.cellW = model.cellW;
29 | this.cellH = model.cellH;
30 | this.resizeGrid(this.gridCellW, this.gridCellH, this.cellW, this.cellH);
31 |
32 | //
33 | this._tx = model.tx;
34 | this._ty = model.ty;
35 |
36 | var dx = this.tx % this.gridCellW;
37 | var dy = this.ty % this.gridCellH;
38 |
39 | this.setTranslate(dx, dy);
40 | }
41 |
42 | // Programmatically change the grid spacing for the larger grid cells and smaller grid cells.
43 | // None of this is relevant to the SurfaceView so we just set the attributes directly.
44 | resizeGrid(lw, lh, sw, sh) {
45 | this.gridCellW = lw;
46 | this.gridCellH = lh;
47 | this.cellW = sw;
48 | this.cellH = sh;
49 | var elLargeGridRect = document.getElementById("largeGridRect");
50 | var elLargeGridPath = document.getElementById("largeGridPath");
51 | var elLargeGrid = document.getElementById("largeGrid");
52 |
53 | var elSmallGridPath = document.getElementById("smallGridPath");
54 | var elSmallGrid = document.getElementById("smallGrid");
55 |
56 | var elSvg = document.getElementById("svg");
57 | var elSurface = document.getElementById("surface");
58 | var elGrid = document.getElementById("grid");
59 |
60 | elLargeGridRect.setAttribute("width", lw);
61 | elLargeGridRect.setAttribute("height", lh);
62 |
63 | elLargeGridPath.setAttribute("d", "M " + lw + " 0 H 0 V " + lh);
64 | elLargeGrid.setAttribute("width", lw);
65 | elLargeGrid.setAttribute("height", lh);
66 |
67 | elSmallGridPath.setAttribute("d", "M " + sw + " 0 H 0 V " + sh);
68 | elSmallGrid.setAttribute("width", sw);
69 | elSmallGrid.setAttribute("height", sh);
70 |
71 | elGrid.setAttribute("x", -lw);
72 | elGrid.setAttribute("y", -lh);
73 |
74 | var svgW = +elSvg.getAttribute("width");
75 | var svgH = +elSvg.getAttribute("height");
76 |
77 | elSurface.setAttribute("width", svgW + lw * 2);
78 | elSurface.setAttribute("height", svgH + lh * 2);
79 |
80 | elSurface.setAttribute("x", -lw);
81 | elSurface.setAttribute("y", -lh);
82 |
83 | elSurface.setAttribute("width", svgW + lw * 2);
84 | elSurface.setAttribute("height", svgH + lh * 2);
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/models/textModel.js:
--------------------------------------------------------------------------------
1 | class TextModel extends ShapeModel {
2 | constructor() {
3 | super(Constants.SHAPE_TEXT);
4 | this._x = 0;
5 | this._y = 0;
6 | this._text = "";
7 | }
8 |
9 | get isShape() { return true; }
10 |
11 | serialize() {
12 | var model = super.serialize();
13 | model.x = this._x;
14 | model.y = this._y;
15 | model.text = this._text;
16 |
17 | return { Text: model };
18 | }
19 |
20 | deserialize(model, el) {
21 | super.deserialize(model, el);
22 | this.x = model.x;
23 | this.y = model.y;
24 | this.text = model.text;
25 | }
26 |
27 | getProperties() {
28 | return [
29 | { propertyName: 'x', label: 'X', column: 0, row: 0, getter: () => this.x + this.tx },
30 | { propertyName: 'y', label: 'Y', column: 1, row: 0, getter: () => this.y + this.ty },
31 | { propertyName: 'text', label: 'Text', column: 0, row: 1, getter: () => this.text },
32 | { propertyName: 'tx', alias: 'x', getter: () => this.x + this.tx },
33 | { propertyName: 'ty', alias: 'y', getter: () => this.y + this.ty },
34 | ];
35 | }
36 |
37 | get x() { return this._x; }
38 | get y() { return this._y; }
39 | get text() { return this._text; }
40 |
41 | set x(value) {
42 | this._x = value;
43 | this.propertyChanged("x", value);
44 | }
45 |
46 | set y(value) {
47 | this._y = value;
48 | this.propertyChanged("y", value);
49 | }
50 |
51 | set text(value) {
52 | this._text = value;
53 | this.propertyChanged("text", value);
54 | }
55 | }
--------------------------------------------------------------------------------
/point.js:
--------------------------------------------------------------------------------
1 | class Point {
2 | constructor(x, y) {
3 | this.x = x;
4 | this.y = y;
5 | }
6 |
7 | translate(x, y) {
8 | var p = new Point(this.x + x, this.y + y);
9 |
10 | return p;
11 | }
12 | }
--------------------------------------------------------------------------------
/prototypes.js:
--------------------------------------------------------------------------------
1 | // "Extension methods"
2 |
3 | Array.prototype.any = function (predicate) {
4 | for (var i = 0; i < this.length; i++) {
5 | if (predicate) {
6 | var any = predicate(this[i]);
7 | if (any) {
8 | return true;
9 | }
10 | }
11 | }
12 |
13 | return false;
14 | }
15 |
16 | /*
17 |
18 | enumProto.any = function (predicate) {
19 | var any = false;
20 | this.forEach(function (elem, index) {
21 | if (predicate) {
22 | any = predicate(elem, index);
23 | return !any;
24 | }
25 | any = true;
26 | return false;
27 | });
28 | return any;
29 | };
30 |
31 | */
--------------------------------------------------------------------------------
/svggrid8.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | FlowSharpWeb
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | Text:
70 |
71 |
72 |
73 |
135 |
136 |
137 |
138 |
334 |
381 |
--------------------------------------------------------------------------------
/views/anchorView.js:
--------------------------------------------------------------------------------
1 | class AnchorView extends View {
2 | constructor(svgElement, model) {
3 | super(svgElement, model);
4 | }
5 |
6 | // For anchors, we always move the group, not the child elements.
7 | onPropertyChange(sender, args) {
8 | this.svgElement.setAttribute(args.propertyName, args.value);
9 | }
10 | }
--------------------------------------------------------------------------------
/views/lineView.js:
--------------------------------------------------------------------------------
1 | class LineView extends ShapeView {
2 | constructor(svgElement, model) {
3 | super(svgElement, model);
4 | }
5 |
6 | onPropertyChange(sender, args) {
7 | // A line consists of a transparent portion [0] with a larger stroke width than the visible line [1]
8 | // firstElementChild drills into the outer group.
9 | this.svgElement.firstElementChild.children[0].setAttribute(args.propertyName, args.value);
10 | this.svgElement.firstElementChild.children[1].setAttribute(args.propertyName, args.value);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/views/objectsView.js:
--------------------------------------------------------------------------------
1 | class ObjectsView extends View {
2 | constructor(svgObjects, shapesModel) {
3 | super(svgObjects, shapesModel);
4 | }
5 |
6 | // For objects, we always move the group, not the child elements.
7 | onPropertyChange(sender, args) {
8 | this.svgElement.setAttribute(args.propertyName, args.value);
9 | }
10 | }
--------------------------------------------------------------------------------
/views/propertyGridView.js:
--------------------------------------------------------------------------------
1 | class PropertyGridView {
2 | constructor(mouseController) {
3 | this.mouseController = mouseController;
4 | this.currentModel = undefined;
5 | this.pnpcMap = {}; // Property-Name : Property-Control map
6 | this.aliases = []; // Array of key-value pairs, because some shapes, like lines, have multiple getters for a single property, like "tx"
7 | mouseController.eventShapeSelected.attach(this.onShapeSelected.bind(this));
8 | }
9 |
10 | // Show the shape ID on the property grid.
11 | onShapeSelected(sender, args) {
12 | document.getElementById(Constants.SHAPE_ID).innerHTML = args.shapeId;
13 | this.unregisterExistingPropertyChangedEvent(args.model);
14 | this.registerPropertyChangedEvent(args.model);
15 | this.renderProperties(args.model);
16 | }
17 |
18 | propertyChanged(sender, args) {
19 | let model = sender;
20 | let propertyName = args.propertyName;
21 | let value = args.value;
22 |
23 | if (model.shapeName !== undefined && value != null) {
24 | // console.log(model.shapeName + " : " + propertyName + ' = ' + value);
25 |
26 | // TODO: x, y, width, height and translate all affect the x, y, w, h PG controls.
27 | // Also, we map to a function that deals with the setting of the value, particularly
28 | // to perform computations and custom UI control value settings, like colors, comboboxes with line ends, width num up/down, etc.
29 |
30 | let gridControlId = this.pnpcMap[propertyName];
31 |
32 | // If we don't have a grid control, then we probably have a property that is an alias to an existing property.
33 | // An example is where the translation (tx,ty) is actually an alias for (cx,cy) - circle or (x,y) - rectangle or (x1/x2, y1/y2) - line.
34 | if (gridControlId === undefined) {
35 | // We allow for multiple aliasing, so for a line, updates tx adjusts x1 and x2 together.
36 | this.aliases.filter(a => a.pname == propertyName).forEach(a => {
37 | let alias = a.palias;
38 | gridControlId = this.pnpcMap[alias];
39 |
40 | if (gridControlId !== undefined) {
41 | let getter = model.getProperties().filter(p => p.propertyName == a.pname && p.alias == alias)[0].getter;
42 | let computedValue = getter();
43 | // console.log("pname = " + a.pname + ", alias = " + a.palias + ", ID = " + gridControlId + ", Value = " + computedValue);
44 | document.getElementById(gridControlId).value = computedValue;
45 | }
46 | });
47 | } else {
48 | // We don't necessarily want to use the value of the property that got changed, particular with regards to (x, y) and (tx, ty).
49 | // Instead, we always want to use the value returned by the getter method.
50 | let computedValue = model.getProperties().filter(p => p.propertyName === propertyName)[0].getter();
51 | document.getElementById(gridControlId).value = computedValue;
52 | }
53 | }
54 | }
55 |
56 | unregisterExistingPropertyChangedEvent(model) {
57 | if (this.currentModel !== undefined && this.currentModel != model) {
58 | this.currentModel.eventPropertyChanged.detachKeyed(Constants.PROPERTY_GRID_LISTENER_KEY, this.propertyChanged.bind(this));
59 | }
60 | }
61 |
62 | registerPropertyChangedEvent(model) {
63 | this.currentModel = model;
64 | this.currentModel.eventPropertyChanged.attachKeyed(Constants.PROPERTY_GRID_LISTENER_KEY, this.propertyChanged.bind(this));
65 | }
66 |
67 | renderProperties(model) {
68 | this.pnpcMap = {};
69 | let twoColumnPropertyGridTemplate =
70 | '' +
71 | ' | ' +
72 | ' | ' +
73 | ' | ' +
74 | ' | ' +
75 | '
';
76 |
77 | let rowNum = -1;
78 | let pg = document.getElementById(Constants.PROPERTY_GRID_ID);
79 | pg.innerHTML = '';
80 |
81 | model.getProperties().filter(p => p.alias === undefined).forEach(p => {
82 | let col = p.column;
83 | let row = p.row;
84 | let rowHtml = '';
85 | // let newRow = row > rowNum;
86 |
87 | // Allow for empty rows as visual spacing
88 | while (row > rowNum) {
89 | rowNum += 1;
90 | rowHtml = twoColumnPropertyGridTemplate;
91 | rowHtml = rowHtml.replace('row0', 'row' + rowNum);
92 | rowHtml = rowHtml.replace('rc00', 'rc' + rowNum + '-0');
93 | rowHtml = rowHtml.replace('prop00', 'prop' + rowNum + '-0');
94 | rowHtml = rowHtml.replace('rc01', 'rc' + rowNum + '-1');
95 | rowHtml = rowHtml.replace('prop01', 'prop' + rowNum + '-1');
96 | pg.innerHTML += rowHtml;
97 | }
98 |
99 | let labelid = 'rc' + rowNum + '-' + col;
100 | let propid = 'prop' + row + '-' + col;
101 | this.pnpcMap[p.propertyName] = propid;
102 | document.getElementById(labelid).innerHTML = p.label + ':';
103 | });
104 |
105 | // Initialize all property grid input box values.
106 | model.getProperties().filter(p => p.alias === undefined).forEach(p => {
107 | let gridControlId = this.pnpcMap[p.propertyName];
108 |
109 | if (gridControlId !== undefined) {
110 | document.getElementById(gridControlId).value = p.getter();
111 | }
112 | });
113 |
114 | // Setup aliases
115 | this.aliases = [];
116 | model.getProperties().filter(p => p.alias !== undefined).forEach(p => this.aliases.push({ pname: p.propertyName, palias : p.alias }));
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/views/shapeView.js:
--------------------------------------------------------------------------------
1 | class ShapeView extends View {
2 | constructor(svgElement, model) {
3 | super(svgElement, model);
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/views/surfaceView.js:
--------------------------------------------------------------------------------
1 | class SurfaceView extends View {
2 | constructor(svgSurface, surfaceModel) {
3 | super(svgSurface, surfaceModel);
4 | }
5 |
6 | // For surface, we always move the group, not the child elements.
7 | onPropertyChange(sender, args) {
8 | this.svgElement.setAttribute(args.propertyName, args.value);
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/views/textView.js:
--------------------------------------------------------------------------------
1 | class TextView extends View{
2 | constructor(svgElement, model) {
3 | super(svgElement, model);
4 | }
5 |
6 | // Custom handling for property "text"
7 | onPropertyChange(sender, args) {
8 | if (args.propertyName == "text") {
9 | this.actualElement.innerHTML = args.value;
10 | } else {
11 | super.onPropertyChange(sender, args);
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/views/toolboxSurfaceView.js:
--------------------------------------------------------------------------------
1 | class ToolboxSurfaceView extends View {
2 | constructor(svgSurface, surfaceModel) {
3 | super(svgSurface, surfaceModel);
4 | }
5 |
6 | // For surface, we always move the group, not the child elements.
7 | onPropertyChange(sender, args) {
8 | this.svgElement.setAttribute(args.propertyName, args.value);
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/views/toolboxView.js:
--------------------------------------------------------------------------------
1 | class ToolboxView extends View {
2 | constructor(svgSurface, surfaceModel) {
3 | super(svgSurface, surfaceModel);
4 | }
5 |
6 | // For surface, we always move the group, not the child elements.
7 | onPropertyChange(property, value) {
8 | this.svgElement.setAttribute(property, value);
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/views/view.js:
--------------------------------------------------------------------------------
1 | class View {
2 | constructor(svgElement, model) {
3 | this.svgElement = svgElement;
4 | model.eventPropertyChanged.attach(this.onPropertyChange.bind(this));
5 | }
6 |
7 | get id() {
8 | return this.svgElement.getAttribute("id");
9 | }
10 |
11 | set id(val) {
12 | this.svgElement.setAttribute("id", val);
13 | }
14 |
15 | // Returns the ID of the first child, the "real" shape, of the group surrounding the shape.
16 | get actualId() {
17 | return this.actualElement.getAttribute("id");
18 | }
19 |
20 | // Anchors don't have a wrapping group so there are no child elements.
21 | get actualElement() {
22 | return this.svgElement.firstElementChild == null ? this.svgElement : this.svgElement.firstElementChild;
23 | }
24 |
25 | onPropertyChange(sender, args) {
26 | // Every shape is grouped, so we want to update the property of the first child in the group.
27 | // This behavior is overridden by specific views -- surface and objects, for example.
28 | // firstElementChild ignores text and comment nodes.
29 | // this.svgElement.firstElementChild.setAttribute(property, value);
30 |
31 | this.actualElement.setAttribute(args.propertyName, args.value);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------