We tackle segment intersection. For every pair of consecutive segments the user draws, the intersection is computed on the server and displayed. The computation is asynchronous, and the client remains responsive until the result arrives.

seg int
Figure 1. The intersection point of pairs of segments

1. Client

D3.js makes it possible to write brief, expressive code. It replaces jQuery. Also, visualization is a cornerstone, not an afterthought.

1.1. Geometry

Any system consisting partly of drawing requires handling coordinates. It is handy to formalize the notion of a point, which increases the readability of our code, eliminates a source of errors from handling independent coordinates, and encourages us to write geometric functionality just once, ultimately reducing the size of the system.

function Point(x, y) {
    this.x = x;
    this.y = y;
}

We do not aim here to design a full-fledged library, just the minimal functionality that keeps our code tidy. For example we have no need for transformations, and so we write no functions or classes for them.

For the segment class we make a distinction between the first and the second points entered by the user. We say that the segment is directed from its source to its target.

function Segment(src, tgt) {
    this.src = src;
    this.tgt = tgt;
}

An array stores the segments entered by the user, and a second array stores the computed intersection points. We seed the two arrays, to avoid wondering in this prototype starting from an empty image.

var segments = [
    new Segment(new Point( 20,  20), new Point(120, 120)),
    new Segment(new Point(120,  20), new Point( 20, 120))
];
var intersectionPoints = [
    new Point(70, 70)
];

1.2. D3.js

D3.js will handle rendering. After selecting the element that has an id sketchpad (a div), we insert an SVG node.

function prelude() {
    var sketchpad = d3.select("#sketchpad");
    svg = sketchpad.append("svg")
                   .attr("viewBox", "0 0 300 300");
    return svg;
}

The refresh function is called once at the beginning, and every time the user releases the mouse button to draw the second of a pair of segments. We selectAll nodes of type line under the SVG node. The chained functions from data to attrs handle the coupling between our data structure (the array segments and its configuration) and D3.js. data takes the two arrays (the array of SVG lines and the array of segments) and splits the array of segments to two arrays, the first matching in size the existing number of nodes, and the second containing the excess. Internally D3.js terms these arrays _enter and _exit. These correspond to the two functions enter() and exit() that we can execute on their corresponding array.

In our case, and before the SVG diagram has been initialized, the outcome of the selection will be an empty set. Hence the array _enter prepared by D3.js will match the array segments passed as a parameter. The array _exit will be emtpy. The next function, enter(), iterates over each elements in _enter and appends an SVG line for each element. SVG expects the attributes to be named x1, y1, x2, and y2, and so those are the ones that we provide. Each is initialized with an anonymous function that retrieves the corresponding number in our structure (d.src.x, d.src.y, …​).

The same operations are repeated. Only this time we insert small circles (representing the intersection points) and match those to the array points to draw the intersection points.

We skipped one detail: whenever possible, we’d like to minimize creations/destructions on the DOM, to maximize responsiveness in the interactive interface. We insert a segment with the id dragSegment in the SVG diagram and only modify its opacity when it begins or ends interaction. One detail in the selection remains. We did not write svg.selectAll("line"). We wrote instead svg.selectAll("line:not(#dragSegment)"). The former would have selected all line nodes including dragSegment. The latter selects all lines, but leaves out the one with the id dragSegment.

function refresh() {
    svg.selectAll("line:not(#dragSegment)")
       .data(segments)
       .enter()
       .append("line")
       .attrs({x1: function(d) { return d.src.x; },
               y1: function(d) { return d.src.y; },
               x2: function(d) { return d.tgt.x; },
               y2: function(d) { return d.tgt.y; }});

    svg.selectAll("circle")
       .data(intersectionPoints)
       .enter()
       .append("circle")
       .attrs({cx: function(d) { return d.x; },
               cy: function(d) { return d.y; },
               r: function(d) { return 3; }
       })
}

We maintain a boolean flag to signal whether the user is dragging a segment, and also the initial point of the drag action, pointBegin.

var dragging = false;
var pointBegin = new Point(0, 0);

Next we define three functions to handle mousedown, mousemove, and mouseup events. As mentioned, the dragSegment object remains part of the SVG drawing. Only its opacity is updated during mouse press and release events.

var mousedown = function() {
    dragging = true;
    pointBegin.x = d3.mouse(this)[0];
    pointBegin.y = d3.mouse(this)[1];
    fifiSeg.attrs({x1: pointBegin.x,
                   y1: pointBegin.y,
                   x2: pointBegin.x,
                   y2: pointBegin.y})
           .style("opacity", 1.0);
};

We leave some room for an optimization: We listen to mouse move events even when no drag operation is currently underway.

var mousemove = function() {
    if(dragging) {
        fifiSeg.attrs({x1: pointBegin.x,
                       y1: pointBegin.y,
                       x2: d3.mouse(this)[0],
                       y2: d3.mouse(this)[1]})
               .style("opacity", 1.0);
    }
};

D3.request takes the URL of the JSON request. After specifying in the header that the request is a JSON message, we set the error and load functions. The response string is parsed using JSON.parse(), and if the two segments intersect, the point where they meet is inserted in the array intersectionPoints. The post() function prepares the message to send using JSON.stringify().

Neither the error nor the load functions are invoked directly. Both are callbacks, meaning that they are registered to respond to an error and to a load event. The computation proceeds uninterrupted after the message is posted. If the server is down, we only give a subtle hint to the user (by displaying a message in the console).

In particular, Now that calling refresh() has rendered the dragged (either first or second) segment because a new segment has been pushed to the array segments, we can immediately hide the fifi segment, or the segment that provides the visual cue for rubber-banding, by modifying its opacity.

var mouseup = function() {
    segments.push(new Segment(new Point(pointBegin.x, pointBegin.y),
                              new Point(d3.mouse(this)[0], d3.mouse(this)[1])));
    var sl = segments.length;
    if(sl % 2 == 0) {
        d3.request("calculate_intersection.php")
          .header("Content-Type", "application/json")
          .on("error", function(error) { console.log(error); })
          .on("load", function(xhr) {
              msg = JSON.parse(xhr.response);
              if(msg.intersection) {
                  intersectionPoints.push(new Point(msg.x, msg.y));
              }
              refresh();
          })
          .post(JSON.stringify({'seg1': segments[sl-2],
                                'seg2': segments[sl-1]}));
    }
    fifiSeg.style('opacity', 0.0);
    dragging = false;
    refresh();
};

The remainder of the code sets up the mouse event callbacks and inserts an initially invisible fifiSeg in the SVG drawing.

function setup_callbacks(svg) {
    svg.on('mousedown', mousedown)
       .on('mousemove', mousemove)
       .on('mouseup', mouseup);
}

var svg = prelude();
var fifiSeg = svg.append("line")
                 .attr('id', 'dragSegment')
                 .attrs({x1: 0, y1: 0,
                         x2: 0, y2: 0})
                 .style('stroke', 'yellow')
                 .style("opacity", 0.0);

setup_callbacks(svg);
refresh();

2. Server

The Javascript code described above operates correctly regardless of whether the server is running. The only visual cue for the user is that the appearance of the intersection points.

2.1. PHP

The only routes that the client can access directly is the root '/'. At that address we respond with the file index.html. The HTML file consists of just a div placeholder where we will insert an SVG node.

<!DOCTYPE html>
<head>
    <meta charset="UTF-8">
    <link rel="stylesheet" type="text/css" href="main.css">
</head>
<body>
    <h1>Sketchpad</h1>
    <div id="sketchpad">
    </div>
    <script src="https://d3js.org/d3.v4.min.js"></script>
    <script src="https://d3js.org/d3-selection-multi.v1.min.js"></script>
    <script src="main.js"></script>
</body>

The API also consists of only one function at the URL /calculate_intersection. Both the request ($data = json_decode(..)) and the response (json_encode(..)) are JSON strings. We construct two segments, compute their intersection, if any, and respond with the point of intersection.

    $data = json_decode(file_get_contents('php://input'));

    include 'geom.php';

    $seg1 = new Segment($data->seg1);
    $seg2 = new Segment($data->seg2);
    $result = intersect($seg1, $seg2);

    header('Content-Type: application/json');
    echo json_encode($result);

2.2. Geometry

Here also we do not aim to build a comprehensive geometry library, but only those functions needed, starting with the humble determinant, the cornerstone of geometric calculations.

    function determinant($a, $b, $c, $d)
    {
        return $a * $d - $b * $c;
    }

A Point object can take either one or two arguments. If one, we expect an instance of stdClass, as constructed by json_decode(..), and if two, we expect the two coordinates.

    class Point {
        public $x, $y;
        public function __construct($x, $y) {
            $this->x = $x;
            $this->y = $y;
        }
        static public function stdCtr(stdClass $sc) {
            $newPoint = new self($sc->x, $sc->y);
            return $newPoint;
        }
    }

A Segment is also initialized by an instance of stdClass. A Line is initialized by a segment.

    class Segment {
        public $src, $tgt;
        public function __construct(stdClass $sc) {
            $this->src = Point::stdCtr($sc->src);
            $this->tgt = Point::stdCtr($sc->tgt);
        }
    }

    class Line {
        public $a, $b, $c;
        public function __construct($seg) {
            $this->a = + determinant($seg->src->y,          1.0,
                                     $seg->tgt->y,          1.0);
            $this->b = - determinant($seg->src->x,          1.0,
                                     $seg->tgt->x,          1.0);
            $this->c = + determinant($seg->src->x, $seg->src->y,
                                     $seg->tgt->x, $seg->tgt->y);
        }
    }

To compute the intersection, we first determine whether the endpoints of each segment lie on opposite sides of the other.

    function oriented_side($line, $point) {
        return $line->a * $point->x + $line->b * $point->y + $line->c;
    }

    function straddle($line, $segment) {
        $src_side = oriented_side($line, $segment->src);
        $tgt_side = oriented_side($line, $segment->tgt);
        $s1 = ($src_side >= 0.0) && ($tgt_side <  0.0);
        $s2 = ($src_side <  0.0) && ($tgt_side >= 0.0);
        return $s1 || $s2;
    }

If both do, we find the intersection point.

    function intersect($seg1, $seg2) {
        $l1 = new Line($seg1);
        $l2 = new Line($seg2);
        if (straddle($l1, $seg2) && straddle($l2, $seg1)) {
            $denom = determinant($l1->a, $l1->b, $l2->a, $l2->b);
            if ($denom != 0.0) {
                $detx = + determinant($l1->b, $l1->c, $l2->b, $l2->c);
                $dety = - determinant($l1->a, $l1->c, $l2->a, $l2->c);
                return array("intersection" => true,
                             "x" => $detx/$denom,
                             "y" => $dety/$denom);
            }
        }
        return array("intersection" => false);
    }

The source code is here.