Skip to content
This repository has been archived by the owner on Sep 26, 2019. It is now read-only.

Creating Custom Marking Tools

Sascha T. Ishikawa edited this page Jul 9, 2015 · 1 revision

At the moment, all marking tools are invoked by the drawing task (tasks/drawing.cjsx). This is not likely to change, although the ‘drawing’ task might be renamed to ‘marking.’

But First, The Point Tool...

The simplest marking tool available is the point tool. As such, it is worth describing how it was implemented since more complicated tools, such as the rectangle and the textRow tools, are based on it.

The bulk of the tool is contained in point/index.cjsx. This is the top-level component that handles initialization, mouse events, and rendering. A general outline of the code is described in the code below:

REQUIRED DEPENDENCIES: This is essentially a list of ‘require’ statements that imports other components or libraries that are used by the tool. The Draggable library is itself a component that conveniently wraps around other components or SVG tags, in the case of the point tool. It creates and manages its own event listeners and fires custom event handlers to respond to click events that can be passed to it as props. Additional components used by the tool, such as the DeleteButton, must also be included. Obviously, the React library needs to be there too.

DISPLAY PARAMETERS: This is where attributes of the SVG elements that make up the tools are defined. Examples include circle radius, stroke width, fill color, etc. They are capitalized by convention.

REACT COMPONENT (CLASS) DECLARATION: This is the React component that defines the tool. STATIC INITIALIZATION METHODS: These methods are important because they set the initial values of the mark, which is a representation of the tool and its attributes. Think of ‘mark’ as the ‘model’ (in MVC terms) for the tool. HELPER METHODS: Any additional methods that make the code more legible and reduce redundant code. RENDER METHOD: The most important (and only mandatory method) in any react component. Re-draws the tool whenever the state changes. EVENT HANDLERS: Methods that respond to mouse events. (Sub) components within the tool should be wrapped with a Draggable component and passed the necessary event handlers as props. For example,

<Draggable onDrag={@handleDrag}>
  <circle r={radius} />
</Draggable>

The outline has been applied to the point/index.cjsx code below:

# REQUIRED DEPENDENCIES

React           = require 'react'
Draggable       = require 'lib/draggable'
DeleteButton    = require './delete-button'


# DISPLAY PARAMETERS

RADIUS                = 10
SELECTED_RADIUS       = 20
CROSSHAIR_SPACE       = 0.2
CROSSHAIR_WIDTH       = 1
DELETE_BUTTON_ANGLE   = 45
STROKE_WIDTH          = 1.5
SELECTED_STROKE_WIDTH = 2.5

# REACT COMPONENT (CLASS) DECLARATION

module.exports = React.createClass
  displayName: 'PointTool'

  # STATIC INITIALIZATION METHODS

  statics:
    defaultValues: ({x, y}) ->
      {x, y}

    initMove: ({x, y}) ->
      {x, y}

  # HELPER METHODS

  getDeleteButtonPosition: ->
    theta = (DELETE_BUTTON_ANGLE) * (Math.PI / 180)
    x: (SELECTED_RADIUS / @props.xScale) * Math.cos theta
    y: -1 * (SELECTED_RADIUS / @props.yScale) * Math.sin theta

  # RENDER METHOD

  render: ->
    averageScale = (@props.xScale + @props.yScale) / 2
    crosshairSpace = CROSSHAIR_SPACE / averageScale
    crosshairWidth = CROSSHAIR_WIDTH / averageScale
    selectedRadius = SELECTED_RADIUS / averageScale
    radius = if @props.selected
      SELECTED_RADIUS / averageScale
    else
      RADIUS / averageScale

    scale = (@props.xScale + @props.yScale) / 2

    <g 
      tool={this} 
      transform="translate(#{@props.mark.x}, #{@props.mark.y})" 
      onMouseDown={@handleMouseDown}
    >
      <g
        className="drawing-tool-main"
        fill='transparent'
        stroke='#f60'
        strokeWidth={SELECTED_STROKE_WIDTH/scale}
        onMouseDown={@props.onSelect unless @props.disabled}
      >
        <Draggable onDrag={@handleDrag}>
          <circle r={radius} />
        </Draggable>

        { if @props.selected
          <DeleteButton tool={this} getDeleteButtonPosition={@getDeleteButtonPosition} />
        }

      </g>
    </g>

  # EVENT HANDLERS

  handleDrag: (e, d) ->
    @props.mark.x += d.x / @props.xScale
    @props.mark.y += d.y / @props.yScale
    @props.onChange e
    
  handleMouseDown: ->
    @props.onSelect @props.mark # unless @props.disabled

The ‘textRow’ Tool

The textRow tool is used to mark regions that only require ranges of y-values. This is similar to a rectangle tool, except the width of the rectangle is fixed at 100% of the width of the subject viewer. This tool enables users to mark entire rows of text in the document.

Creating the ‘textRow’ Tool

We begin by duplicating the point tool and renaming it, by setting displayName: ‘TextRowTool’. Next, we extend the static initialization methods defaultValues() and initMove() to include the yUpper and yLower values. These are the values we really care about since they store the upper and lower coordinates of the rectangle. The x and y values still remain, but they are used to store the location of the horizontal line that bisects the rectangle. In other words, the y-position (since the x value is irrelevant). The default dimensions are set using the DEFAULT_HEIGHT parameter. For practical reasons, namely to handle resizing events, y is used to represent the midpoint between yUpper and yLower (offset by - DEFAULT_HEIGHT/2). As such, it is not relevant to the final transcriptions.

SVG elements were modified to suit our needs (lines removed, etc.). An additional DragHandle component, based on DeleteButton, was introduced to serve as the resize handles of the mark. Since there are two handle buttons, one at the top, and one at the bottom of the mark, event handlers were created for both, handleUpperResize() and handleLowerResize(), respectively. These are located in the top-level index file and passed as props to the Draggable component. Helper methods, getUpperHandlePosition() and getLowerHandlePosition() were introduced to re-calculate where the handle buttons should be located after resize events. Lastly, a bunch of ugly math was added, as required, to calculate all the correct locations.