Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rectangular Select #36

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions app/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const RESET_ZOOM = 'RESET_ZOOM';
export const MOVE_NODE = 'MOVE_NODE';
export const MOVE_EDGE = 'MOVE_EDGE';
export const MOVE_CAPTION = 'MOVE_CAPTION';
export const UPDATE_END_POINTS = 'UPDATE_END_POINTS';
export const SWAP_NODE_HIGHLIGHT = 'SWAP_NODE_HIGHLIGHT';
export const SWAP_EDGE_HIGHLIGHT = 'SWAP_EDGE_HIGHLIGHT';
export const SWAP_CAPTION_HIGHLIGHT = 'SWAP_CAPTION_HIGHLIGHT';
Expand Down Expand Up @@ -88,6 +89,10 @@ export function moveCaption(captionId, x, y) {
return { type: MOVE_CAPTION, captionId, x, y };
}

export function updateEndPoints(graph, edgeId, xa, ya, xb, yb) {
return { type: UPDATE_END_POINTS, graph, edgeId, xa, ya, xb, yb };
}

export function swapNodeHighlight(nodeId) {
return { type: SWAP_NODE_HIGHLIGHT, nodeId };
}
Expand Down
16 changes: 11 additions & 5 deletions app/components/Edge.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ export default class Edge extends BaseComponent {
super(props);
this.bindAll('_handleDragStart', '_handleDrag', '_handleDragStop', '_handleClick', '_handleTextClick');
// need control point immediately for dragging
let { cx, cy } = this._calculateGeometry(props.edge.display);
this.state = merge({}, props.edge.display, { cx, cy });
let { cx, cy, x1, y1, x2, y2, s1, s2, xa, ya, xb, yb } = this._calculateGeometry(props.edge.display);
this.state = merge({}, props.edge.display, { cx, cy, x1, y1, x2, y2, s1, s2, xa, ya, xb, yb });
}

render() {

let e = this.props.edge;
let sp = this._getSvgParams(e);

let width = 1 + (e.display.scale - 1) * 5;
let selected = this.props.selected;
let highlighted = e.display.status == "highlighted";
Expand Down Expand Up @@ -80,6 +82,11 @@ export default class Edge extends BaseComponent {
this.setState(newState);
}

componentDidMount(props) {
this.props.updateEndPoints(this.props.graphId, this.props.edge.id, this.state.xa, this.state.ya, this.state.xb, this.state.yb);
this.props.moveEdge(this.props.edge.id, this.state.cx, this.state.cy, this.state.x1, this.state.y1, this.state.x2, this.state.y2, this.state.s1, this.state.s2, this.state.xa, this.state.ya, this.state.xb, this.state.yb);
}

shouldComponentUpdate(nextProps, nextState) {
return nextProps.selected !== this.props.selected ||
JSON.stringify(nextState) !== JSON.stringify(this.state);
Expand Down Expand Up @@ -131,9 +138,8 @@ export default class Edge extends BaseComponent {

_getSvgParams(edge) {
let e = edge;
let { label, scale, arrow, dash, status } = this.state;
let { x, y, cx, cy, xa, ya, xb, yb, is_reverse } = this._calculateGeometry(this.state);

let { label, scale, arrow, dash, status } = this.state;
const pathId = `path-${e.id}`;
const fontSize = 10 * Math.sqrt(scale);

Expand All @@ -151,11 +157,11 @@ export default class Edge extends BaseComponent {
textColor: eds.textColor[status],
bgColor: eds.bgColor[status],
bgOpacity: eds.bgOpacity[status]

};
}

_calculateGeometry(state) {
// let edge = this.props.edge;
let { cx, cy, x1, y1, x2, y2, s1, s2 } = state;
let r1 = s1 * nds.circleRadius;
let r2 = s2 * nds.circleRadius;
Expand Down
20 changes: 15 additions & 5 deletions app/components/Graph.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import BaseComponent from './BaseComponent';
import Node from './Node';
import Edge from './Edge';
import Caption from './Caption';
import Lasso from './Lasso';
import GraphModel from '../models/Graph';
import { DraggableCore } from 'react-draggable';
import values from 'lodash/object/values';
Expand All @@ -24,20 +25,28 @@ export default class Graph extends BaseComponent {

render() {
let { x, y, prevGraph, viewBox, height } = this.state;

return (
<svg id="svg" version="1.1" xmlns="http://www.w3.org/2000/svg" className="Graph" width="100%" height={height} viewBox={viewBox} preserveAspectRatio="xMidYMid">
<DraggableCore
handle="#zoom-handle"
moveOnStartChange={false}
onStart={this._handleDragStart}
onDrag={this._handleDrag}
onStop={this._handleDragStop}>
onStart={!this.props.isLasso ? this._handleDragStart : null}
onDrag={!this.props.isLasso ? this._handleDrag : null}
onStop={!this.props.isLasso ? this._handleDragStop : null}
>
<g id="zoom" transform={`translate(${x}, ${y})`}>
<rect id="zoom-handle" x="-5000" y="-5000" width="10000" height="10000" fill="#fff" />
{ this._renderEdges() }
{ this._renderNodes() }
{ this._renderCaptions() }
{this.props.isLasso &&
<Lasso graph={this}
selectNode={this.props.clickNode}
selectEdge={this.props.clickEdge}
selectCaption={this.props.clickCaption}
simulateShiftKeyDown={this.props.simulateShiftKeyDown}
simulateShiftKeyUp={this.props.simulateShiftKeyUp}
toggleLasso={this.props.toggleLasso} />}
</g>
</DraggableCore>
<defs dangerouslySetInnerHTML={ { __html: this._renderMarkers() } }/>
Expand Down Expand Up @@ -68,7 +77,8 @@ export default class Graph extends BaseComponent {
zoom={this.props.zoom}
selected={this.props.selection && includes(this.props.selection.edgeIds, e.id)}
clickEdge={this.props.clickEdge}
moveEdge={this.props.moveEdge}
moveEdge={this.props.moveEdge}
updateEndPoints={this.props.updateEndPoints}
isLocked={this.props.isLocked} />);
}

Expand Down
228 changes: 228 additions & 0 deletions app/components/Lasso.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
import React, { Component, PropTypes } from 'react';
import ReactDOM from 'react-dom';
import each from 'lodash/collection/each';
import { DraggableCore } from 'react-draggable';
import BaseComponent from './BaseComponent';
import Graph from '../models/Graph';
import nds from '../NodeDisplaySettings';
import ds from '../CaptionDisplaySettings';
import svgIntersections from 'svg-intersections';
import Point2D from 'kld-affine/lib/Point2D'
import bezier from 'svg-intersections/lib/functions/bezier';
import isNaN from 'lodash/lang/isNaN';


export default class Lasso extends BaseComponent {
constructor(props) {
super(props);
this.bindAll('_handleDragStart', '_handleDrag', '_handleDragStop');
this.state = {
width: 0,
height: 0,
x: 0,
y: 0,
viewBoxWidth: +this.props.graph.state.viewBox.split(" ")[2],
viewBoxHeight: +this.props.graph.state.viewBox.split(" ")[3],
thisOffsetLeft: null,
thisOffsetRight: null,
thisOffsetTop: null,
thisOffsetBottom: null

}
}

componentWillUpdate(props){

this.setState({thisOffsetLeft: this.refs.lassoBg.getBoundingClientRect()["left"]});
this.setState({thisOffsetRight: this.refs.lassoBg.getBoundingClientRect()["right"]});
this.setState({thisOffsetTop: this.refs.lassoBg.getBoundingClientRect()["top"]});
this.setState({thisOffsetBottom: this.refs.lassoBg.getBoundingClientRect()["bottom"]});
}



shouldComponentUpdate(nextProps, nextState){
return ((this.refs.lassoBg.getBoundingClientRect()["left"] != this.state.thisOffsetLeft) ||
(this.refs.lassoBg.getBoundingClientRect()["right"] != this.state.thisOffsetRight) ||
(this.refs.lassoBg.getBoundingClientRect()["top"] != this.state.thisOffsetTop) ||
(this.refs.lassoBg.getBoundingClientRect()["bottom"] != this.state.thisOffsetBottom) ||
(this.state.thisOffsetLeft == null) ||
(this.state.height != nextState.height) || (this.state.height != nextState.height) ||
(this.state.x != nextState.x) || (this.state.y != nextState.y))
&& (!_.isNaN(this.state.x)) && (!_.isNaN(this.state.width)) && (!_.isNaN(this.state.y)) && (!_.isNaN(this.state.height));
}

render() {

return (
<g>
<DraggableCore
onStart={this._handleDragStart}
onDrag={this._handleDrag}
onStop={this._handleDragStop}>
<rect
ref="lassoBg"
x={-this.state.viewBoxWidth/2}
y={-this.state.viewBoxHeight/2}
width={this.state.viewBoxWidth}
height={this.state.viewBoxHeight}
opacity = "0"/>
</DraggableCore>
<rect
x={this.state.x}
y={this.state.y}
width={this.state.width}
height={this.state.height}
fill="none"
stroke="#aaa"
strokeWidth="2px"
strokeDasharray="7,7"
/>
</g>
)
}

componentDidMount(e, ui){
/*will need to update offsets potentially at other points*/
this.setState({thisOffsetLeft: this.refs.lassoBg.getBoundingClientRect()["left"]});
this.setState({thisOffsetRight: this.refs.lassoBg.getBoundingClientRect()["right"]});
this.setState({thisOffsetTop: this.refs.lassoBg.getBoundingClientRect()["top"]});
this.setState({thisOffsetBottom: this.refs.lassoBg.getBoundingClientRect()["bottom"]});

}

_handleDragStart(e, ui) {
this.props.simulateShiftKeyDown();

var newX = (ui.position.clientX - e.target.getBoundingClientRect()["left"])/
(e.target.getBoundingClientRect()["right"] - e.target.getBoundingClientRect()["left"]) *
(this.state.viewBoxWidth) - this.state.viewBoxWidth/2;

var newY = (ui.position.clientY - e.target.getBoundingClientRect()["top"])/
(e.target.getBoundingClientRect()["bottom"] - e.target.getBoundingClientRect()["top"]) *
(this.state.viewBoxHeight) - this.state.viewBoxHeight/2;

this.setState({x: newX});
this.setState({y: newY});
this._startDrag = ui.position;
this._startPosition = {
x: this.state.x,
y: this.state.y
};

}

_handleDragStop(e, ui) {
if (this._dragging) {
var graphThis = this;
var bezierIntersections = bezier;

each(this.props.graph.props.graph.nodes, function(n){
var thisRadius = nds.circleRadius * n.display.scale;
if ((n.display.x + thisRadius) > graphThis.state.x && (n.display.x - thisRadius) < (graphThis.state.x + graphThis.state.width)
&& (n.display.y + thisRadius) > graphThis.state.y && (n.display.y - thisRadius) < (graphThis.state.y + graphThis.state.height)){
graphThis.props.selectNode(n.id);
}
})


each(this.props.graph.props.graph.captions, function(c){
//at this point, let's approximate the width/height of text rather than getting DOM element's size
if ((c.display.x + c.display.text.length * c.display.scale * 15 * 0.6) > graphThis.state.x && c.display.x < (graphThis.state.x + graphThis.state.width)
&& (c.display.y + ds.lineHeight * c.display.scale/2) > graphThis.state.y && (c.display.y - ds.lineHeight * c.display.scale/1.5) < (graphThis.state.y + graphThis.state.height)){
graphThis.props.selectCaption(c.id);
}
})

each(this.props.graph.props.graph.edges, function(e){

if (e.display.x1 >= graphThis.state.x &&
e.display.x1 <= (graphThis.state.x + graphThis.state.width) &&
e.display.cx >= graphThis.state.x &&
e.display.cx <= (graphThis.state.x + graphThis.state.width) &&
e.display.x2 >= graphThis.state.x &&
e.display.x2 <= (graphThis.state.x + graphThis.state.width) &&
e.display.y1 >= graphThis.state.y &&
e.display.y1 <= (graphThis.state.y + graphThis.state.height) &&
e.display.cy >= graphThis.state.y &&
e.display.cy <= (graphThis.state.y + graphThis.state.height) &&
e.display.y2 >= graphThis.state.y &&
e.display.y2 <= (graphThis.state.y + graphThis.state.height)){
graphThis.props.selectEdge(e.id);
} else {
if (e.display.cx != null){

var intersectionTop = bezierIntersections.intersectBezier2Line(
new Point2D(e.display.xa, e.display.ya),
new Point2D(((e.display.x1 + e.display.x2)/2 + e.display.cx), ((e.display.y1 + e.display.y2)/2 + e.display.cy)),
new Point2D(e.display.xb, e.display.yb),
new Point2D(graphThis.state.x, graphThis.state.y),
new Point2D((graphThis.state.x + graphThis.state.width), graphThis.state.y));

var intersectionRight = bezierIntersections.intersectBezier2Line(
new Point2D(e.display.xa, e.display.ya),
new Point2D(((e.display.x1 + e.display.x2)/2 + e.display.cx), ((e.display.y1 + e.display.y2)/2 + e.display.cy)),
new Point2D(e.display.xb, e.display.yb),
new Point2D((graphThis.state.x + graphThis.state.width), graphThis.state.y),
new Point2D((graphThis.state.x + graphThis.state.width), (graphThis.state.y + graphThis.state.height)));

var intersectionBottom = bezierIntersections.intersectBezier2Line(
new Point2D(e.display.xa, e.display.ya),
new Point2D(((e.display.x1 + e.display.x2)/2 + e.display.cx), ((e.display.y1 + e.display.y2)/2 + e.display.cy)),
new Point2D(e.display.xb, e.display.yb),
new Point2D(graphThis.state.x, (graphThis.state.y + graphThis.state.height)),
new Point2D((graphThis.state.x + graphThis.state.width), (graphThis.state.y + graphThis.state.height)));

var intersectionLeft = bezierIntersections.intersectBezier2Line(
new Point2D(e.display.xa, e.display.ya),
new Point2D(((e.display.x1 + e.display.x2)/2 + e.display.cx), ((e.display.y1 + e.display.y2)/2 + e.display.cy)),
new Point2D(e.display.xb, e.display.yb),
new Point2D(graphThis.state.x, (graphThis.state.y + graphThis.state.height)),
new Point2D(graphThis.state.x, graphThis.state.y));

if (intersectionTop.points.length > 0 || intersectionBottom.points.length > 0 || intersectionLeft.points.length > 0 || intersectionRight.points.length > 0){
graphThis.props.selectEdge(e.id);
}
}
}
});


this.setState({ x : -500 });
this.setState({ y : -500 });
this.setState({ width : 0 });
this.setState({ height : 0 });
}


}

_handleDrag(e, ui) {
if (this.props.isLocked) return;


this._dragging = true; // so that _handleClick knows it's not just a click

let deltaX = (ui.position.clientX - this._startDrag.clientX) / this.props.graph.state.actualZoom;
let deltaY = (ui.position.clientY - this._startDrag.clientY) / this.props.graph.state.actualZoom;

let width = Math.abs(deltaX);
let height = Math.abs(deltaY);

this.setState({width, height});

if (deltaX < 0){
var x = (ui.position.clientX - this.state.thisOffsetLeft)/
(this.state.thisOffsetRight - this.state.thisOffsetLeft) *
(this.state.viewBoxWidth) - this.state.viewBoxWidth/2;
this.setState({ x });
}
if (deltaY < 0){
var y = (ui.position.clientY - this.state.thisOffsetTop)/
(this.state.thisOffsetBottom - this.state.thisOffsetTop) *
(this.state.viewBoxHeight) - this.state.viewBoxHeight/2;
this.setState({ y });
}
}

}
31 changes: 31 additions & 0 deletions app/components/LassoButton.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React, { Component, PropTypes } from 'react';

export default class LassoButton extends Component {

render() {
var theClass = "";
if (this.props.lassoActive){
theClass = this.props.showEditTools ? "editContentMode" : "editAnnotationsMode";
}
return (
<button
id="oligrapherLassoButton"
className={"btn btn-sm btn-default " + theClass}
title={this.props.showEditTools ? "disable lasso tool" : "enable lasso tool"}
onClick={this.props.toggle}>
<span>
<svg xmlns="http://www.w3.org/2000/svg" width="14px" height="14px" >
<rect x="0"
y="0"
width="14"
height="14"
fill="none"
stroke="#333"
strokeWidth="3"
strokeDasharray="4,3" />
</svg>
</span>
</button>
);
}
}
Loading