<commit_before>/*!
Cloud4all Preferences Management Tools

Copyright 2013 CERTH/HIT

Licensed under the New BSD license. You may not use this file except in
compliance with this License.

You may obtain a copy of the License at
https://github.com/GPII/prefsEditors/LICENSE.txt
*/

/*global fluid, jQuery, gpii*/
/*jslint white: true, onevar: true, funcinvoke: true, forvar: true, undef: true, newcap: true, nomen: true, regexp: true, plusplus: true, bitwise: true, maxerr: 50, indent: 4 */

(function ($, fluid) {

    fluid.defaults("gpii.uiOptions.panels.increaseSize", {
        gradeNames: ["fluid.uiOptions.panels", "gpii.uiOptions.panels.textSize", "gpii.uiOptions.panels.cursorSize", "gpii.uiOptions.panels.magnifierFollows", "gpii.uiOptions.panels.magnifier", "gpii.uiOptions.panels.magnifierPosition", "autoInit"],
        events: {
            onToggleIncreaseSizeAdjusters: null,
            onShowExtraAdjuster: null,
            onHideExtraAdjuster: null
        },
        listeners: {
            "afterRender.hideIncreaseSizeAdjusters": {
                "this": "{that}.dom.increaseSizeAdjusters",
                "method": "hide",
                "args": [0]
            },
            "afterRender.setTextIncreaseSizeHeader": {
                "this": "{that}.dom.increaseSizeHeader",
                "method": "text",
                "args": ["{that}.options.strings.increaseSizeHeader"]
            },
            "afterRender.setATTRaddToMyPreferencesStar": {
                "this": "{that}.dom.addToMyPreferencesStar",
                "method": "attr",
                "args": [{
                    "tooltip-checked": "{that}.options.strings.tooltipChecked",
                    "tooltip-unchecked": "{that}.options.strings.tooltipUnchecked"
                }]
            },
            "afterRender.bindEventPreferenceSwitchIncreaseSize": {
                "this": "{that}.dom.preferenceSwitchIncreaseSize",
                "method": "change",
                "args": ["{that}.events.onToggleIncreaseSizeAdjusters.fire"]
            },
            "onToggleIncreaseSizeAdjusters.showHide": {
                "this": "{that}.dom.increaseSizeAdjusters",
                "method": "slideToggle"
            },
            "afterRender.console": {
                "this": "console",
                "method": "log",
                "args": ["{that}.options.strings.increaseSizeHeader"]
            },
            "afterRender.bindEventPreferenceSwitchIncreaseSizeExtra": {
                "this": "{that}.dom.preferenceSwitchIncreaseSizeExtra",
                "method": "change",
                "args": ["{that}.updateModelValue"]
            },
            "afterRender.setTextMoreLess": {
                "this": "{that}.dom.moreLess",
                "method": "text",
                "args": ["{that}.options.strings.more"]
            },
            "onShowExtraAdjuster.show": {
                "this": "{that}.dom.increaseSizeExtraAdjusters",
                "method": "slideToggle"
            },
            "onHideExtraAdjuster.hide": {
                "this": "{that}.dom.increaseSizeExtraAdjusters",
                "method": "slideToggle"
            },
            "onShowExtraAdjuster.setLessText": {
                "this": "{that}.dom.moreLess",
                "method": "text",
                "args": ["{that}.options.strings.less"]
            },
            "onHideExtraAdjuster.setMoreText": {
                "this": "{that}.dom.moreLess",
                "method": "text",
                "args": ["{that}.options.strings.more"]
            },
            "afterRender.hideIncreaseSizeExtraAdjusters": {
                "this": "{that}.dom.increaseSizeExtraAdjusters",
                "method": "hide",
                "args": [0]
            },
            "afterRender.init": {
                listener: "{that}.applier.modelChanged.addListener",
                args: ["increaseSizeExtraAdjustersEnabled", "{that}.toggleIncreaseSizeExtraAdjusters"]
            }
        },
        selectors: {
            increaseSizeHeader: ".headerTitle",
            increaseSizeAdjusters: ".flc-uiOptions-category",
            preferenceSwitchIncreaseSize: "#preferenceSwitchIncreaseSize",
            addToMyPreferencesStar: ".addToMyPreferencesLabel",
            increaseSizeExtraAdjusters: ".flc-uiOptions-category-hidden",
            preferenceSwitchIncreaseSizeExtra: "#preferenceSwitchIncreaseSizeExtra",
            moreLess: ".moreLess"
        },
        selectorsToIgnore: ["increaseSizeHeader", "increaseSizeAdjusters", "preferenceSwitchIncreaseSize", "addToMyPreferencesStar", "increaseSizeExtraAdjusters", "preferenceSwitchIncreaseSizeExtra", "moreLess"],
        strings: {
            increaseSizeHeader: {
                expander: {
                    func: "gpii.uiOptions.pmt.lookupMsg",
                    args: ["{uiOptionsLoader}.msgBundle", "increaseSizeHeader"]
                }
            },
            tooltipChecked: {
                expander: {
                    func: "gpii.uiOptions.pmt.lookupMsg",
                    args: ["{uiOptionsLoader}.msgBundle", "tooltipChecked"]
                }
            },
            tooltipUnchecked: {
                expander: {
                    func: "gpii.uiOptions.pmt.lookupMsg",
                    args: ["{uiOptionsLoader}.msgBundle", "tooltipUnchecked"]
                }
            },
            more: {
                expander: {
                    func: "gpii.uiOptions.pmt.lookupMsg",
                    args: ["{uiOptionsLoader}.msgBundle", "more"]
                }
            },
            less: {
                expander: {
                    func: "gpii.uiOptions.pmt.lookupMsg",
                    args: ["{uiOptionsLoader}.msgBundle", "less"]
                }
            }
        },
        model: {
            increaseSizeExtraAdjustersEnabled: false
        },
        protoTree: {
            preferenceSwitchIncreaseSizeExtra: "${increaseSizeExtraAdjustersEnabled}",
            cursorSizeLabel: {messagekey: "cursorSizeLabel"},
            multiplier: {messagekey: "multiplier"},
            cursorSize: {
                decorators: {
                    type: "fluid",
                    func: "fluid.uiOptions.textfieldSlider",
                    options: {
                        range: "{that}.options.controlValues.cursorSize",
                        path: "value"
                    }
                }
            },
            magnifierMinus: "-",
            magnifierLabel: {messagekey: "magnifierLabel"},
            magnifierPlus: "+",

            magnifierValueText: "${magnification}",
            magnifierFollowsHeading: {messagekey: "magnifierFollowsLabel"},
            expander: [
                {
                    type: "fluid.renderer.selection.inputs",
                    rowID: "magnifierFollowsRow",
                    labelID: "magnifierFollowsLabel",
                    inputID: "magnifierFollowsInput",
                    selectID: "magnifierFollows-radio",
                    tree: {
                        optionnames: "${{that}.options.strings.magnifierFollows}",
                        optionlist: "${{that}.options.controlValues.magnifierFollows}",
                        selection: "${magnifierFollows}"
                    }
                }/*,
                {
                    type: "fluid.renderer.selection.inputs",
                    rowID: "magnifierPositionRow",
                    labelID: "magnifierPositionLabel",
                    inputID: "magnifierPositionInput",
                    selectID: "magnifierPosition-radio",
                    tree: {
                        optionnames: "${{that}.options.strings.magnifierPosition}",
                        optionlist: "${{that}.options.controlValues.magnifierPosition}",
                        selection: "${magnifierPosition}"
                    }
                }*/
            ],
            magnifierPositionHeading: {messagekey: "magnifierPositionLabel"},
            textSizeMinus: "-",
            textSizeLabel: {messagekey: "textSizeLabel"},
            textSizePlus: "+",

            textSizeValueText: "${fontSize}"
        },
        invokers: {
            toggleIncreaseSizeExtraAdjusters: {
                "funcName": "gpii.uiOptions.panels.increaseSize.toggleIncreaseSizeExtraAdjusters",
                "args": [
                    "{that}.model.increaseSizeExtraAdjustersEnabled",
                    "{that}.events.onShowExtraAdjuster.fire",
                    "{that}.events.onHideExtraAdjuster.fire"
                ]
            },
            updateModelValue: {
                "funcName": "gpii.uiOptions.panels.increaseSize.updateIncreaseSizeExtraAdjustersModelValue",
                "args": [
                    "{that}",
                    "{that}.dom.preferenceSwitchIncreaseSizeExtra"
                ]
            }
        }
    });

    gpii.uiOptions.panels.increaseSize.toggleIncreaseSizeExtraAdjusters = function (increaseSizeExtraAdjustersEnabled, showEvent, hideEvent) {
        if (increaseSizeExtraAdjustersEnabled) {
            hideEvent();
        } else {
            showEvent();
        }
    };

    gpii.uiOptions.panels.increaseSize.updateIncreaseSizeExtraAdjustersModelValue = function (that, elm) {
        that.applier.requestChange("increaseSizeExtraAdjustersEnabled", !elm.is(":checked"));
    };

})(jQuery, fluid);
<commit_msg>Increase size extra adjusters' toggling is model bound.<commit_after>/*!
Cloud4all Preferences Management Tools

Copyright 2013 CERTH/HIT

Licensed under the New BSD license. You may not use this file except in
compliance with this License.

You may obtain a copy of the License at
https://github.com/GPII/prefsEditors/LICENSE.txt
*/

/*global fluid, jQuery, gpii*/
/*jslint white: true, onevar: true, funcinvoke: true, forvar: true, undef: true, newcap: true, nomen: true, regexp: true, plusplus: true, bitwise: true, maxerr: 50, indent: 4 */

(function ($, fluid) {

    fluid.defaults("gpii.uiOptions.panels.increaseSize", {
        gradeNames: ["fluid.uiOptions.panels", "gpii.uiOptions.panels.textSize", "gpii.uiOptions.panels.cursorSize", "gpii.uiOptions.panels.magnifierFollows", "gpii.uiOptions.panels.magnifier", "gpii.uiOptions.panels.magnifierPosition", "autoInit"],
        events: {
            onToggleIncreaseSizeAdjusters: null,
            onShowExtraAdjuster: null,
            onHideExtraAdjuster: null
        },
        listeners: {
            "afterRender.hideIncreaseSizeAdjusters": {
                "this": "{that}.dom.increaseSizeAdjusters",
                "method": "hide",
                "args": [0]
            },
            "afterRender.setTextIncreaseSizeHeader": {
                "this": "{that}.dom.increaseSizeHeader",
                "method": "text",
                "args": ["{that}.options.strings.increaseSizeHeader"]
            },
            "afterRender.setATTRaddToMyPreferencesStar": {
                "this": "{that}.dom.addToMyPreferencesStar",
                "method": "attr",
                "args": [{
                    "tooltip-checked": "{that}.options.strings.tooltipChecked",
                    "tooltip-unchecked": "{that}.options.strings.tooltipUnchecked"
                }]
            },
            "afterRender.bindEventPreferenceSwitchIncreaseSize": {
                "this": "{that}.dom.preferenceSwitchIncreaseSize",
                "method": "change",
                "args": ["{that}.events.onToggleIncreaseSizeAdjusters.fire"]
            },
            "onToggleIncreaseSizeAdjusters.showHide": {
                "this": "{that}.dom.increaseSizeAdjusters",
                "method": "slideToggle"
            },
            "afterRender.console": {
                "this": "console",
                "method": "log",
                "args": ["{that}.options.strings.increaseSizeHeader"]
            },
            "afterRender.bindEventPreferenceSwitchIncreaseSizeExtra": {
                "this": "{that}.dom.preferenceSwitchIncreaseSizeExtra",
                "method": "change",
                "args": ["{that}.toggleIncreaseSizeExtraAdjusters"]
            },
            "afterRender.setTextMoreLess": {
                "this": "{that}.dom.moreLess",
                "method": "text",
                "args": ["{that}.options.strings.more"]
            },
            "onShowExtraAdjuster.show": {
                "this": "{that}.dom.increaseSizeExtraAdjusters",
                "method": "slideToggle"
            },
            "onHideExtraAdjuster.hide": {
                "this": "{that}.dom.increaseSizeExtraAdjusters",
                "method": "slideToggle"
            },
            "onShowExtraAdjuster.setLessText": {
                "this": "{that}.dom.moreLess",
                "method": "text",
                "args": ["{that}.options.strings.less"]
            },
            "onHideExtraAdjuster.setMoreText": {
                "this": "{that}.dom.moreLess",
                "method": "text",
                "args": ["{that}.options.strings.more"]
            },
            "afterRender.toggleIncreaseSizeExtraAdjusters": {
                listener: "{that}.toggleIncreaseSizeExtraAdjusters"
            }
        },
        selectors: {
            increaseSizeHeader: ".headerTitle",
            increaseSizeAdjusters: ".flc-uiOptions-category",
            preferenceSwitchIncreaseSize: "#preferenceSwitchIncreaseSize",
            addToMyPreferencesStar: ".addToMyPreferencesLabel",
            increaseSizeExtraAdjusters: ".flc-uiOptions-category-hidden",
            preferenceSwitchIncreaseSizeExtra: "#preferenceSwitchIncreaseSizeExtra",
            moreLess: ".moreLess"
        },
        selectorsToIgnore: ["increaseSizeHeader", "increaseSizeAdjusters", "preferenceSwitchIncreaseSize", "addToMyPreferencesStar", "increaseSizeExtraAdjusters", "moreLess"],
        strings: {
            increaseSizeHeader: {
                expander: {
                    func: "gpii.uiOptions.pmt.lookupMsg",
                    args: ["{uiOptionsLoader}.msgBundle", "increaseSizeHeader"]
                }
            },
            tooltipChecked: {
                expander: {
                    func: "gpii.uiOptions.pmt.lookupMsg",
                    args: ["{uiOptionsLoader}.msgBundle", "tooltipChecked"]
                }
            },
            tooltipUnchecked: {
                expander: {
                    func: "gpii.uiOptions.pmt.lookupMsg",
                    args: ["{uiOptionsLoader}.msgBundle", "tooltipUnchecked"]
                }
            },
            more: {
                expander: {
                    func: "gpii.uiOptions.pmt.lookupMsg",
                    args: ["{uiOptionsLoader}.msgBundle", "more"]
                }
            },
            less: {
                expander: {
                    func: "gpii.uiOptions.pmt.lookupMsg",
                    args: ["{uiOptionsLoader}.msgBundle", "less"]
                }
            }
        },
        model: {
            increaseSizeExtraAdjustersEnabledSwitch: false
        },
        protoTree: {
            preferenceSwitchIncreaseSizeExtra: "${increaseSizeExtraAdjustersEnabledSwitch}",
            cursorSizeLabel: {messagekey: "cursorSizeLabel"},
            multiplier: {messagekey: "multiplier"},
            cursorSize: {
                decorators: {
                    type: "fluid",
                    func: "fluid.uiOptions.textfieldSlider",
                    options: {
                        range: "{that}.options.controlValues.cursorSize",
                        path: "value"
                    }
                }
            },
            magnifierMinus: "-",
            magnifierLabel: {messagekey: "magnifierLabel"},
            magnifierPlus: "+",

            magnifierValueText: "${magnification}",
            magnifierFollowsHeading: {messagekey: "magnifierFollowsLabel"},
            expander: [
                {
                    type: "fluid.renderer.selection.inputs",
                    rowID: "magnifierFollowsRow",
                    labelID: "magnifierFollowsLabel",
                    inputID: "magnifierFollowsInput",
                    selectID: "magnifierFollows-radio",
                    tree: {
                        optionnames: "${{that}.options.strings.magnifierFollows}",
                        optionlist: "${{that}.options.controlValues.magnifierFollows}",
                        selection: "${magnifierFollows}"
                    }
                }/*,
                {
                    type: "fluid.renderer.selection.inputs",
                    rowID: "magnifierPositionRow",
                    labelID: "magnifierPositionLabel",
                    inputID: "magnifierPositionInput",
                    selectID: "magnifierPosition-radio",
                    tree: {
                        optionnames: "${{that}.options.strings.magnifierPosition}",
                        optionlist: "${{that}.options.controlValues.magnifierPosition}",
                        selection: "${magnifierPosition}"
                    }
                }*/
            ],
            magnifierPositionHeading: {messagekey: "magnifierPositionLabel"},
            textSizeMinus: "-",
            textSizeLabel: {messagekey: "textSizeLabel"},
            textSizePlus: "+",

            textSizeValueText: "${fontSize}"
        },
        invokers: {
            toggleIncreaseSizeExtraAdjusters: {
                "funcName": "gpii.uiOptions.panels.increaseSize.toggleIncreaseSizeExtraAdjusters",
                "args": [
                    "{that}.model.increaseSizeExtraAdjustersEnabledSwitch",
                    "{that}.events.onShowExtraAdjuster.fire",
                    "{that}.events.onHideExtraAdjuster.fire"
                ]
            }
        }
    });

    gpii.uiOptions.panels.increaseSize.toggleIncreaseSizeExtraAdjusters = function (increaseSizeExtraAdjustersEnabledSwitch, showEvent, hideEvent) {
        if (increaseSizeExtraAdjustersEnabledSwitch) {
            showEvent();
        } else {
            hideEvent();
        }
    };

})(jQuery, fluid);
<|endoftext|>


<commit_before>import React, { Component } from 'react'
import { connect } from 'react-redux'
import {
    Accordion,
    Card,
    Col,
    Form,
    Row,
} from 'react-bootstrap'
import Toolpath from 'gcode-toolpath';
import Switch from 'react-switch'
import {
  setFileName,
  setFileComments,
  setFileVertices,
  setFileZoom,
  toggleFileAspectRatio
} from './fileSlice'
import './PatternInput.scss'
import ReactGA from 'react-ga'

const mapStateToProps = (state, ownProps) => {
  return {
    name: state.file.name,
    comments: state.file.comments,
    vertices: state.file.vertices,
    zoom: state.file.zoom,
    aspectRatio: state.file.aspectRatio,
  }
}

const mapDispatchToProps = (dispatch, ownProps) => {
  var convertToXY = (thetaRhos) => {
    var vertices = []
    var previous = undefined
    var max_angle = Math.PI / 64.0
    for (let ii = 0; ii < thetaRhos.length; ii++) {
      var next = thetaRhos[ii]
      if (previous) {
        if (Math.abs(next[0] - previous[0]) < max_angle) {
          // These sin, cos elements are inverted. I'm not sure why
          vertices.push({
                        x: previous[1] * Math.sin(previous[0]),
                        y: previous[1] * Math.cos(previous[0])
          })
        } else {
          // We need to do some interpolating.
          let deltaAngle = next[0] - previous[0]
          let rhoStep = max_angle / Math.abs(deltaAngle) * (next[1] - previous[1])
          var rho = previous[1]
          if (deltaAngle > 0.0) {
            var emergency_break = 0
            for (let angle = previous[0]; angle < next[0]; angle += max_angle, rho += rhoStep) {
              vertices.push({
                            x: rho * Math.sin(angle),
                            y: rho * Math.cos(angle)
              })
              if (emergency_break++ > 100000) {
                break
              }
            }
          } else {
            for (let angle = previous[0]; angle > next[0]; angle -= max_angle, rho += rhoStep) {
              vertices.push({
                            x: rho * Math.sin(angle),
                            y: rho * Math.cos(angle)
              })
              if (emergency_break++ > 100000) {
                break
              }
            }
          }
        }
      }
      previous = next
    }
    return vertices
  }

  var parseThrFile = (file) => {
    var rv = {}
    rv.comments = []
    rv.vertices = []

    var reader = new FileReader()

    reader.onload = (event) => {
      const startTime = performance.now()
      var text = reader.result
      var lines = text.split('\n')
      var has_vertex = false
      for (let ii = 0; ii < lines.length; ii++) {
        var line = lines[ii].trim()
        if (line.length === 0) {
          // blank lines
          continue
        }
        if (line.indexOf("#") === 0 && !has_vertex) {
          rv.comments.push(lines[ii])
        }

        if (line.indexOf("#") !== 0) {
          has_vertex = true
          // This is a point, let's try to read it.
          var pointStrings = line.split(/\s+/)
          if (pointStrings.length !== 2) {
            // AAAH
            console.log(pointStrings)
            continue
          }
          rv.vertices.push([parseFloat(pointStrings[0]), parseFloat(pointStrings[1])])
        }
      }

      dispatch(setFileComments(rv.comments))
      dispatch(setFileVertices(convertToXY(rv.vertices)))
      const endTime = performance.now()
      ReactGA.timing({
        category: 'PatternInput',
        variable: 'readThetaRho',
        value: endTime - startTime, // in milliseconds
      });
    }

    reader.readAsText(file)
  }

  var normalizeCoords = (vertices) => {
    let minX = 1e9
    let minY = 1e9
    let maxX = -1e9
    let maxY = -1e9
    vertices.forEach( (vertex) => {
      minX = Math.min(vertex.x, minX)
      minY = Math.min(vertex.y, minY)
      maxX = Math.max(vertex.x, maxX)
      maxY = Math.max(vertex.y, maxY)
    })

    const offsetX = (maxX + minX)/2.0
    const offsetY = (maxY + minY)/2.0

    const scaleX = 1.0/offsetX
    const scaleY = 1.0/offsetY

    return vertices.map( (vertex) => {
      return {
        x: scaleX * (vertex.x - offsetX),
        y: scaleY * (vertex.y - offsetY)
      }
    })
  }

  var parseGcodeFile = (file) => {
    let rv = {}
    rv.comments = []
    rv.vertices = []

    let reader = new FileReader()

    var isComment = (line) => {
      return (line.indexOf(";") === 0) || (line.indexOf('(') === 0)
    }

    reader.onload = (event) => {
      const startTime = performance.now()
      var text = reader.result
      var lines = text.split('\n')
      for (let ii = 0; ii < lines.length; ii++) {
        var line = lines[ii].trim()
        if (line.length === 0) {
          // blank lines
          continue
        }
        if (isComment(line)) {
          rv.comments.push(lines[ii])
        } else {
          break
        }
      }

      let first_point = true
      var addVertex = (x, y) => {
        if (!first_point) {
          rv.vertices.push({x: x,y: y})
        }
        first_point = false
      }
      const toolpath = new Toolpath({
        // @param {object} modal The modal object.
        // @param {object} v1 A 3D vector of the start point.
        // @param {object} v2 A 3D vector of the end point.
        addLine: (modal, v1, v2) => {
          addVertex(v2.x, v2.y)
        },
        // @param {object} modal The modal object.
        // @param {object} v1 A 3D vector of the start point.
        // @param {object} v2 A 3D vector of the end point.
        // @param {object} v0 A 3D vector of the fixed point.
        addArcCurve: (modal, v1, v2, v0) => {
        }
      });

      toolpath
        .loadFromString(text, (err, results) => {
          dispatch(setFileComments(rv.comments))
          dispatch(setFileVertices(normalizeCoords(rv.vertices)))
          const endTime = performance.now()
          ReactGA.timing({
            category: 'PatternInput',
            variable: 'readGCode',
            value: endTime - startTime, // in milliseconds
          });
        })

    }

    reader.readAsText(file)
  }

  return {
    setVertices: (event: any) => {
      var file = event.target.files[0]
      dispatch(setFileName(file.name))
      if (file.name.toLowerCase().endsWith('.thr')) {
        parseThrFile(file)
      } else if (file.name.toLowerCase().endsWith('.gcode') || file.name.toLowerCase().endsWith('.nc')) {
        parseGcodeFile(file)
      }

    },
    setZoom: (event) => {
      dispatch(setFileZoom(parseFloat(event.target.value)))
    },
    toggleAspectRatio: (event) => {
      dispatch(toggleFileAspectRatio())
    },
  }
}

class PatternInput extends Component {
  render() {
    var commentsRender = this.props.comments.map((comment, index) => {
      return {comment}

    })

    return (

        <Card className="p-3 pb-4">
          <Accordion className="mb-4">
            <Card>
              <Card.Header as={Form.Label} htmlFor="fileUpload" style={{ cursor: "pointer" }}>

Import

                Imports a Sisyphus-style theta rho (.thr) file into Sandify
                <Form.Control
                    id="fileUpload"
                    type="file"
                    accept=".thr,.gcode,.nc"
                    onChange={this.props.setVertices}
                    style={{ display: "none" }} />
              </Card.Header>
            </Card>
          </Accordion>

          <Row className="align-items-center">
            <Col sm={5}>
              <Form.Label htmlFor="keepAspectRatio">
                Keep original aspect ratio
              </Form.Label>
            </Col>
            <Col sm={7}>
              <Switch checked={this.props.aspectRatio} onChange={this.props.toggleAspectRatio} />
            </Col>
          </Row>

          <Row className="align-items-center pt-1">
            <Col sm={5}>
              <Form.Label htmlFor="thr-zoom">
                Zoom
              </Form.Label>
            </Col>
            <Col sm={7}>
              <Form.Control type="number" id="thr-zoom" value={this.props.zoom} onChange={this.props.setZoom} />
            </Col>
          </Row>

          { this.props.name &&

            Name: {this.props.name}

            Comments:

              { commentsRender }

            Number of points: {this.props.vertices.length }

}
        </Card>

Where to get .thr files

Markyland on Reddit

Dithermaster's github

JSisyphus by Slightly Loony

Sisyphus on Reddit

Sisyphus Community

The Juggler

Note about copyrights

Be careful and respectful. Understand that the original author put their labor, intensity, and ideas into this art. The creators have a right to own it (and they have a copyright, even if it doesn't say so).

If you don't have permisson (a license) to use their art, then you shouldn't be. If you do have permission to use their art, then you should be thankful, and I'm sure they would appreciate you sending them a note of thanks. A picture of your table creating their shared art would probably make them smile.

Someone posting the .thr file to a forum or subreddit probably wants it to be shared, and drawing it on your home table is probably OK. Just be careful if you want to use them for something significant without explicit permission.

I am not a lawyer.

    )
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(PatternInput)
<commit_msg>Super ugly, but arcs are working. Needs cleanup.<commit_after>import React, { Component } from 'react'
import { connect } from 'react-redux'
import {
    Accordion,
    Card,
    Col,
    Form,
    Row,
} from 'react-bootstrap'
import Toolpath from 'gcode-toolpath';
import Switch from 'react-switch'
import {
  setFileName,
  setFileComments,
  setFileVertices,
  setFileZoom,
  toggleFileAspectRatio
} from './fileSlice'
import './PatternInput.scss'
import ReactGA from 'react-ga'

const mapStateToProps = (state, ownProps) => {
  return {
    name: state.file.name,
    comments: state.file.comments,
    vertices: state.file.vertices,
    zoom: state.file.zoom,
    aspectRatio: state.file.aspectRatio,
  }
}

const mapDispatchToProps = (dispatch, ownProps) => {
  var convertToXY = (thetaRhos) => {
    var vertices = []
    var previous = undefined
    var max_angle = Math.PI / 64.0
    for (let ii = 0; ii < thetaRhos.length; ii++) {
      var next = thetaRhos[ii]
      if (previous) {
        if (Math.abs(next[0] - previous[0]) < max_angle) {
          // These sin, cos elements are inverted. I'm not sure why
          vertices.push({
                        x: previous[1] * Math.sin(previous[0]),
                        y: previous[1] * Math.cos(previous[0])
          })
        } else {
          // We need to do some interpolating.
          let deltaAngle = next[0] - previous[0]
          let rhoStep = max_angle / Math.abs(deltaAngle) * (next[1] - previous[1])
          var rho = previous[1]
          if (deltaAngle > 0.0) {
            var emergency_break = 0
            for (let angle = previous[0]; angle < next[0]; angle += max_angle, rho += rhoStep) {
              vertices.push({
                            x: rho * Math.sin(angle),
                            y: rho * Math.cos(angle)
              })
              if (emergency_break++ > 100000) {
                break
              }
            }
          } else {
            for (let angle = previous[0]; angle > next[0]; angle -= max_angle, rho += rhoStep) {
              vertices.push({
                            x: rho * Math.sin(angle),
                            y: rho * Math.cos(angle)
              })
              if (emergency_break++ > 100000) {
                break
              }
            }
          }
        }
      }
      previous = next
    }
    return vertices
  }

  var parseThrFile = (file) => {
    var rv = {}
    rv.comments = []
    rv.vertices = []

    var reader = new FileReader()

    reader.onload = (event) => {
      const startTime = performance.now()
      var text = reader.result
      var lines = text.split('\n')
      var has_vertex = false
      for (let ii = 0; ii < lines.length; ii++) {
        var line = lines[ii].trim()
        if (line.length === 0) {
          // blank lines
          continue
        }
        if (line.indexOf("#") === 0 && !has_vertex) {
          rv.comments.push(lines[ii])
        }

        if (line.indexOf("#") !== 0) {
          has_vertex = true
          // This is a point, let's try to read it.
          var pointStrings = line.split(/\s+/)
          if (pointStrings.length !== 2) {
            // AAAH
            console.log(pointStrings)
            continue
          }
          rv.vertices.push([parseFloat(pointStrings[0]), parseFloat(pointStrings[1])])
        }
      }

      dispatch(setFileComments(rv.comments))
      dispatch(setFileVertices(convertToXY(rv.vertices)))
      const endTime = performance.now()
      ReactGA.timing({
        category: 'PatternInput',
        variable: 'readThetaRho',
        value: endTime - startTime, // in milliseconds
      });
    }

    reader.readAsText(file)
  }

  var normalizeCoords = (vertices) => {
    let minX = 1e9
    let minY = 1e9
    let maxX = -1e9
    let maxY = -1e9
    vertices.forEach( (vertex) => {
      minX = Math.min(vertex.x, minX)
      minY = Math.min(vertex.y, minY)
      maxX = Math.max(vertex.x, maxX)
      maxY = Math.max(vertex.y, maxY)
    })
    console.log(minX)
    console.log(minY)
    console.log(maxX)
    console.log(maxY)
    const offsetX = (maxX + minX)/2.0
    const offsetY = (maxY + minY)/2.0

    const scaleX = 0.97/(maxX - offsetX)
    const scaleY = 0.97/(maxY - offsetY)

    return vertices.map( (vertex) => {
      return {
        x: scaleX * (vertex.x - offsetX),
        y: scaleY * (vertex.y - offsetY)
      }
    })
  }

  var parseGcodeFile = (file) => {
    let rv = {}
    rv.comments = []
    rv.vertices = []

    let reader = new FileReader()

    var isComment = (line) => {
      return (line.indexOf(";") === 0) || (line.indexOf('(') === 0)
    }

    reader.onload = (event) => {
      const startTime = performance.now()
      var text = reader.result
      var lines = text.split('\n')
      for (let ii = 0; ii < lines.length; ii++) {
        var line = lines[ii].trim()
        if (line.length === 0) {
          // blank lines
          continue
        }
        if (isComment(line)) {
          rv.comments.push(lines[ii])
        } else {
          break
        }
      }

      var addVertex = (x, y) => {
        rv.vertices.push({x: x,y: y})
      }
      const toolpath = new Toolpath({
        // @param {object} modal The modal object.
        // @param {object} v1 A 3D vector of the start point.
        // @param {object} v2 A 3D vector of the end point.
        addLine: (modal, v1, v2) => {
          if (v1.x !== v2.x || v1.y !== v2.y) {
            addVertex(v2.x, v2.y)
          }
        },
        // @param {object} modal The modal object.
        // @param {object} v1 A 3D vector of the start point.
        // @param {object} v2 A 3D vector of the end point.
        // @param {object} v0 A 3D vector of the fixed point.
        addArcCurve: (modal, v1, v2, v0) => {
          if (v1.x !== v2.x || v1.y !== v2.y) {
            console.log(modal)
            console.log(v0)
            console.log(v1)
            console.log(v2)
            let startTheta = Math.atan2(v1.y-v0.y, v1.x-v0.x)
            let endTheta = Math.atan2(v2.y-v0.y, v2.x-v0.x)
            let deltaTheta = endTheta - startTheta
            const radius = Math.sqrt(Math.pow(v2.x-v0.x, 2.0) + Math.pow(v2.y-v0.y, 2.0))
            let direction = 1.0
            if (modal.motion === 'G2') {
              // Clockwise
              if (deltaTheta > 0.0) {
                endTheta -= 2.0*Math.PI
                deltaTheta -= 2.0*Math.PI
              }
              direction = -1.0
            } else if (modal.motion === 'G3') {
              // Anti-clockwise
              if (deltaTheta < 0.0) {
                endTheta += 2.0*Math.PI
                deltaTheta += 2.0*Math.PI
              }
            }
            console.log(startTheta)
            console.log(endTheta)
            console.log(deltaTheta)
            console.log(radius)
            // What angle do we need to have a resolution of approx. 0.1mm?
            const arcLength = Math.abs(deltaTheta) * radius
            const thetaStep = deltaTheta * 0.1/arcLength
            console.log(thetaStep)
            for (let theta = startTheta; direction * theta < direction * endTheta; theta += thetaStep) {
              addVertex(v0.x + radius * Math.cos(theta), v0.y + radius * Math.sin(theta))
            }
          }
        }
      });

      toolpath
        .loadFromString(text, (err, results) => {
          console.log(rv.vertices)
          dispatch(setFileComments(rv.comments))
          dispatch(setFileVertices(normalizeCoords(rv.vertices)))
          const endTime = performance.now()
          ReactGA.timing({
            category: 'PatternInput',
            variable: 'readGCode',
            value: endTime - startTime, // in milliseconds
          });
        })

    }

    reader.readAsText(file)
  }

  return {
    setVertices: (event: any) => {
      var file = event.target.files[0]
      dispatch(setFileName(file.name))
      if (file.name.toLowerCase().endsWith('.thr')) {
        parseThrFile(file)
      } else if (file.name.toLowerCase().endsWith('.gcode') || file.name.toLowerCase().endsWith('.nc')) {
        parseGcodeFile(file)
      }

    },
    setZoom: (event) => {
      dispatch(setFileZoom(parseFloat(event.target.value)))
    },
    toggleAspectRatio: (event) => {
      dispatch(toggleFileAspectRatio())
    },
  }
}

class PatternInput extends Component {
  render() {
    var commentsRender = this.props.comments.map((comment, index) => {
      return {comment}

    })

    return (

        <Card className="p-3 pb-4">
          <Accordion className="mb-4">
            <Card>
              <Card.Header as={Form.Label} htmlFor="fileUpload" style={{ cursor: "pointer" }}>

Import

                Imports a Sisyphus-style theta rho (.thr) file into Sandify
                <Form.Control
                    id="fileUpload"
                    type="file"
                    accept=".thr,.gcode,.nc"
                    onChange={this.props.setVertices}
                    style={{ display: "none" }} />
              </Card.Header>
            </Card>
          </Accordion>

          <Row className="align-items-center">
            <Col sm={5}>
              <Form.Label htmlFor="keepAspectRatio">
                Keep original aspect ratio
              </Form.Label>
            </Col>
            <Col sm={7}>
              <Switch checked={this.props.aspectRatio} onChange={this.props.toggleAspectRatio} />
            </Col>
          </Row>

          <Row className="align-items-center pt-1">
            <Col sm={5}>
              <Form.Label htmlFor="thr-zoom">
                Zoom
              </Form.Label>
            </Col>
            <Col sm={7}>
              <Form.Control type="number" id="thr-zoom" value={this.props.zoom} onChange={this.props.setZoom} />
            </Col>
          </Row>

          { this.props.name &&

            Name: {this.props.name}

            Comments:

              { commentsRender }

            Number of points: {this.props.vertices.length }

}
        </Card>

Where to get .thr files

Markyland on Reddit

Dithermaster's github

JSisyphus by Slightly Loony

Sisyphus on Reddit

Sisyphus Community

The Juggler

Note about copyrights

Be careful and respectful. Understand that the original author put their labor, intensity, and ideas into this art. The creators have a right to own it (and they have a copyright, even if it doesn't say so).

If you don't have permisson (a license) to use their art, then you shouldn't be. If you do have permission to use their art, then you should be thankful, and I'm sure they would appreciate you sending them a note of thanks. A picture of your table creating their shared art would probably make them smile.

Someone posting the .thr file to a forum or subreddit probably wants it to be shared, and drawing it on your home table is probably OK. Just be careful if you want to use them for something significant without explicit permission.

I am not a lawyer.

    )
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(PatternInput)
<|endoftext|>


<commit_before>// @flow

import { withStyles } from '@material-ui/styles';
import clsx from 'clsx';
import React, { useCallback } from 'react';

import { Icon, IconArrowDown } from '../../../icons';
import { withPixelLineHeight } from '../../../styles/functions.web';

type Props = {

    /**
     * Text of the button.
     */
    children: React$Node,

    /**
     * An object containing the CSS classes.
     */
    classes: Object,

    /**
     * Text css class of the button.
     */
    className?: string,

    /**
     * If the button is disabled or not.
     */
    disabled?: boolean,

    /**
     * If the button has options.
     */
    hasOptions?: boolean,

    /**
     * Icon to display in the options section.
     */
    OptionsIcon?: React$Node,

    /**
     * TestId of the button. Can be used to locate element when testing UI.
     */
    testId?: string,

    /**
     * The type of th button: primary, secondary, text.
     */
    type: string,

    /**
     * OnClick button handler.
     */
    onClick: Function,

    /**
     * Click handler for options.
     */
    onOptionsClick?: Function,

    /**
     * To navigate with the keyboard.
     */
    tabIndex?: number,

    /**
     * To give a role to the icon.
     */
    role?: string,

    /**
     * To give a aria-pressed to the icon.
     */
    ariaPressed?: boolean,

    /**
     * The Label of the current element.
     */
    ariaLabel?: string,

    /**
     * The Label of the child element.
     */
    ariaDropDownLabel?: string
};

/**
 * Creates the styles for the component.
 *
 * @param {Object} theme - The current UI theme.
 *
 * @returns {Object}
 */
const styles = theme => {
    return {
        actionButton: {
            ...withPixelLineHeight(theme.typography.bodyLongBold),
            borderRadius: theme.shape.borderRadius,
            boxSizing: 'border-box',
            color: theme.palette.text01,
            cursor: 'pointer',
            display: 'inline-block',
            marginBottom: '16px',
            padding: '7px 16px',
            position: 'relative',
            textAlign: 'center',
            width: '100%',

            '&.primary': {
                background: theme.palette.action01,
                border: '1px solid #0376DA',

                '&:hover': {
                    backgroundColor: theme.palette.action01Hover
                }
            },

            '&.secondary': {
                background: theme.palette.action02,
                border: '1px solid transparent'
            },

            '&.text': {
                width: 'auto',
                fontSize: '13px',
                margin: '0',
                padding: '0'
            },

            '&.disabled': {
                background: theme.palette.disabled01,
                border: '1px solid #5E6D7A',
                color: '#AFB6BC',
                cursor: 'initial',

                '.icon': {
                    '& > svg': {
                        fill: '#AFB6BC'
                    }
                }
            },

            [theme.breakpoints.down('400')]: {
                fontSize: 16,
                marginBottom: 8,
                padding: '11px 16px'
            }
        },
        options: {
            borderRadius: theme.shape.borderRadius / 2,
            alignItems: 'center',
            display: 'flex',
            height: '100%',
            justifyContent: 'center',
            position: 'absolute',
            right: 0,
            top: 0,
            width: 36,

            '&:hover': {
                backgroundColor: '#0262B6'
            },

            '& svg': {
                pointerEvents: 'none'
            }
        }
    };
};

/**
 * Button used for pre meeting actions.
 *
 * @returns {ReactElement}
 */
function ActionButton({
    children,
    classes,
    className = '',
    disabled,
    hasOptions,
    OptionsIcon = IconArrowDown,
    testId,
    type = 'primary',
    onClick,
    onOptionsClick,
    tabIndex,
    role,
    ariaPressed,
    ariaLabel,
    ariaDropDownLabel
}: Props) {

    const onKeyPressHandler = useCallback(e => {
        if (onClick && !disabled && (e.key === ' ' || e.key === 'Enter')) {
            e.preventDefault();
            onClick(e);
        }
    }, [ onClick, disabled ]);

    const onOptionsKeyPressHandler = useCallback(e => {
        if (onOptionsClick && !disabled && (e.key === ' ' || e.key === 'Enter')) {
            e.preventDefault();
            e.stopPropagation();
            onOptionsClick(e);
        }
    }, [ onOptionsClick, disabled ]);

    const containerClasses = clsx(
        classes.actionButton,
        className && className,
        type,
        disabled && 'disabled'
    );

    return (
        <div
            aria-disabled = { disabled }
            aria-label = { ariaLabel }
            className = { containerClasses }
            data-testid = { testId ? testId : undefined }
            onClick = { disabled ? undefined : onClick }
            onKeyPress = { onKeyPressHandler }
            role = 'button'
            tabIndex = { 0 } >
            {children}
            { hasOptions
                  && <div
                      aria-disabled = { disabled }
                      aria-haspopup = 'true'
                      aria-label = { ariaDropDownLabel }
                      aria-pressed = { ariaPressed }
                      className = { classes.options }
                      data-testid = 'prejoin.joinOptions'
                      onClick = { disabled ? undefined : onOptionsClick }
                      onKeyPress = { onOptionsKeyPressHandler }
                      role = { role }
                      tabIndex = { tabIndex }>
                      <Icon
                          className = 'icon'
                          size = { 14 }
                          src = { OptionsIcon } />
                  </div>
            }
        </div>
    );
}

export default withStyles(styles)(ActionButton);
<commit_msg>fix(lobby) Fix buttons colors (#11867)<commit_after>// @flow

import { withStyles } from '@material-ui/styles';
import clsx from 'clsx';
import React, { useCallback } from 'react';

import { Icon, IconArrowDown } from '../../../icons';
import { withPixelLineHeight } from '../../../styles/functions.web';

type Props = {

    /**
     * Text of the button.
     */
    children: React$Node,

    /**
     * An object containing the CSS classes.
     */
    classes: Object,

    /**
     * Text css class of the button.
     */
    className?: string,

    /**
     * If the button is disabled or not.
     */
    disabled?: boolean,

    /**
     * If the button has options.
     */
    hasOptions?: boolean,

    /**
     * Icon to display in the options section.
     */
    OptionsIcon?: React$Node,

    /**
     * TestId of the button. Can be used to locate element when testing UI.
     */
    testId?: string,

    /**
     * The type of th button: primary, secondary, text.
     */
    type: string,

    /**
     * OnClick button handler.
     */
    onClick: Function,

    /**
     * Click handler for options.
     */
    onOptionsClick?: Function,

    /**
     * To navigate with the keyboard.
     */
    tabIndex?: number,

    /**
     * To give a role to the icon.
     */
    role?: string,

    /**
     * To give a aria-pressed to the icon.
     */
    ariaPressed?: boolean,

    /**
     * The Label of the current element.
     */
    ariaLabel?: string,

    /**
     * The Label of the child element.
     */
    ariaDropDownLabel?: string
};

/**
 * Creates the styles for the component.
 *
 * @param {Object} theme - The current UI theme.
 *
 * @returns {Object}
 */
const styles = theme => {
    return {
        actionButton: {
            ...withPixelLineHeight(theme.typography.bodyLongBold),
            borderRadius: theme.shape.borderRadius,
            boxSizing: 'border-box',
            color: theme.palette.text01,
            cursor: 'pointer',
            display: 'inline-block',
            marginBottom: '16px',
            padding: '7px 16px',
            position: 'relative',
            textAlign: 'center',
            width: '100%',
            border: 0,

            '&.primary': {
                background: theme.palette.action01,
                color: theme.palette.text01,

                '&:hover': {
                    backgroundColor: theme.palette.action01Hover
                }
            },

            '&.secondary': {
                background: theme.palette.action02,
                color: theme.palette.text04,

                '&:hover': {
                    backgroundColor: theme.palette.action02Hover
                }
            },

            '&.text': {
                width: 'auto',
                fontSize: '13px',
                margin: '0',
                padding: '0'
            },

            '&.disabled': {
                background: theme.palette.disabled01,
                border: '1px solid #5E6D7A',
                color: '#AFB6BC',
                cursor: 'initial',

                '.icon': {
                    '& > svg': {
                        fill: '#AFB6BC'
                    }
                }
            },

            [theme.breakpoints.down('400')]: {
                fontSize: 16,
                marginBottom: 8,
                padding: '11px 16px'
            }
        },
        options: {
            borderRadius: theme.shape.borderRadius / 2,
            alignItems: 'center',
            display: 'flex',
            height: '100%',
            justifyContent: 'center',
            position: 'absolute',
            right: 0,
            top: 0,
            width: 36,

            '&:hover': {
                backgroundColor: '#0262B6'
            },

            '& svg': {
                pointerEvents: 'none'
            }
        }
    };
};

/**
 * Button used for pre meeting actions.
 *
 * @returns {ReactElement}
 */
function ActionButton({
    children,
    classes,
    className = '',
    disabled,
    hasOptions,
    OptionsIcon = IconArrowDown,
    testId,
    type = 'primary',
    onClick,
    onOptionsClick,
    tabIndex,
    role,
    ariaPressed,
    ariaLabel,
    ariaDropDownLabel
}: Props) {

    const onKeyPressHandler = useCallback(e => {
        if (onClick && !disabled && (e.key === ' ' || e.key === 'Enter')) {
            e.preventDefault();
            onClick(e);
        }
    }, [ onClick, disabled ]);

    const onOptionsKeyPressHandler = useCallback(e => {
        if (onOptionsClick && !disabled && (e.key === ' ' || e.key === 'Enter')) {
            e.preventDefault();
            e.stopPropagation();
            onOptionsClick(e);
        }
    }, [ onOptionsClick, disabled ]);

    const containerClasses = clsx(
        classes.actionButton,
        className && className,
        type,
        disabled && 'disabled'
    );

    return (
        <div
            aria-disabled = { disabled }
            aria-label = { ariaLabel }
            className = { containerClasses }
            data-testid = { testId ? testId : undefined }
            onClick = { disabled ? undefined : onClick }
            onKeyPress = { onKeyPressHandler }
            role = 'button'
            tabIndex = { 0 } >
            {children}
            { hasOptions
                  && <div
                      aria-disabled = { disabled }
                      aria-haspopup = 'true'
                      aria-label = { ariaDropDownLabel }
                      aria-pressed = { ariaPressed }
                      className = { classes.options }
                      data-testid = 'prejoin.joinOptions'
                      onClick = { disabled ? undefined : onOptionsClick }
                      onKeyPress = { onOptionsKeyPressHandler }
                      role = { role }
                      tabIndex = { tabIndex }>
                      <Icon
                          className = 'icon'
                          size = { 14 }
                          src = { OptionsIcon } />
                  </div>
            }
        </div>
    );
}

export default withStyles(styles)(ActionButton);
<|endoftext|>


<commit_before>'use strict';

var math = require( '../../../math' );
var is = require( '../../../is' );
var util = require( '../../../util' );

var BRp = {};

BRp.arrowShapeWidth = 0.3;

BRp.registerArrowShapes = function(){
  var arrowShapes = this.arrowShapes = {};
  var renderer = this;

  // Contract for arrow shapes:
  // 0, 0 is arrow tip
  // (0, 1) is direction towards node
  // (1, 0) is right
  //
  // functional api:
  // collide: check x, y in shape
  // roughCollide: called before collide, no false negatives
  // draw: draw
  // spacing: dist(arrowTip, nodeBoundary)
  // gap: dist(edgeTip, nodeBoundary), edgeTip may != arrowTip

  var bbCollide = function( x, y, size, angle, translation, padding ){
    var x1 = translation.x - size / 2 - padding;
    var x2 = translation.x + size / 2 + padding;
    var y1 = translation.y - size / 2 - padding;
    var y2 = translation.y + size / 2 + padding;

    var inside = (x1 <= x && x <= x2) && (y1 <= y && y <= y2);

    return inside;
  };

  var transform = function( x, y, size, angle, translation ){
    var xRotated = x * Math.cos( angle ) - y * Math.sin( angle );
    var yRotated = x * Math.sin( angle ) + y * Math.cos( angle );

    var xScaled = xRotated * size;
    var yScaled = yRotated * size;

    var xTranslated = xScaled + translation.x;
    var yTranslated = yScaled + translation.y;

    return {
      x: xTranslated,
      y: yTranslated
    };
  };

  var transformPoints = function( pts, size, angle, translation ){
    var retPts = [];

    for( var i = 0; i < pts.length; i += 2 ){
      var x = pts[ i ];
      var y = pts[ i + 1];

      retPts.push( transform( x, y, size, angle, translation ) );
    }

    return retPts;
  };

  var pointsToArr = function( pts ){
    var ret = [];

    for( var i = 0; i < pts.length; i++ ){
      var p = pts[ i ];

      ret.push( p.x, p.y );
    }

    return ret;
  };

  var defineArrowShape = function( name, defn ){
    if( is.string( defn ) ){
      defn = arrowShapes[ defn ];
    }

    arrowShapes[ name ] = util.extend( {
      name: name,

      points: [
        -0.15, -0.3,
        0.15, -0.3,
        0.15, 0.3,
        -0.15, 0.3
      ],

      collide: function( x, y, size, angle, translation, padding ){
        var points = pointsToArr( transformPoints( this.points, size + 2 * padding, angle, translation ) );
        var inside = math.pointInsidePolygonPoints( x, y, points );

        return inside;
      },

      roughCollide: bbCollide,

      draw: function( context, size, angle, translation ){
        var points = transformPoints( this.points, size, angle, translation );

        renderer.arrowShapeImpl( 'polygon' )( context, points );
      },

      spacing: function( edge ){
        return 0;
      },

      gap: function( edge ){
        return edge.pstyle( 'width' ).pfValue * 2;
      }
    }, defn );
  };

  defineArrowShape( 'none', {
    collide: util.falsify,

    roughCollide: util.falsify,

    draw: util.noop,

    spacing: util.zeroify,

    gap: util.zeroify
  } );

  defineArrowShape( 'triangle', {
    points: [
      -0.15, -0.3,
      0, 0,
      0.15, -0.3
    ]
  } );

  defineArrowShape( 'arrow', 'triangle' );

  defineArrowShape( 'triangle-backcurve', {
    points: arrowShapes[ 'triangle' ].points,

    controlPoint: [ 0, -0.15 ],

    roughCollide: bbCollide,

    draw: function( context, size, angle, translation ){
      var ptsTrans = transformPoints( this.points, size, angle, translation );
      var ctrlPt = this.controlPoint;
      var ctrlPtTrans = transform( ctrlPt[0], ctrlPt[1], size, angle, translation );

      renderer.arrowShapeImpl( this.name )( context, ptsTrans, ctrlPtTrans );
    },

    gap: function( edge ){
      return edge.pstyle( 'width' ).pfValue;
    }
  } );

  defineArrowShape( 'triangle-tee', {
    points: [
      -0.15, -0.3,
      0, 0,
      0.15, -0.3,
      -0.15, -0.3
    ],

    pointsTee: [
      -0.15, -0.4,
      -0.15, -0.5,
      0.15, -0.5,
      0.15, -0.4
    ],

    collide: function( x, y, size, angle, translation, padding ){
      var triPts = pointsToArr( transformPoints( this.points, size + 2 * padding, angle, translation ) );
      var teePts = pointsToArr( transformPoints( this.pointsTee, size + 2 * padding, angle, translation ) );

      var inside = math.pointInsidePolygonPoints( x, y, triPts ) || math.pointInsidePolygonPoints( x, y, teePts );

      return inside;
    },

    draw: function( context, size, angle, translation ){
      var triPts = transformPoints( this.points, size, angle, translation );
      var teePts = transformPoints( this.pointsTee, size, angle, translation );

      renderer.arrowShapeImpl( this.name )( context, triPts, teePts );
    }
  } );

  defineArrowShape( 'vee', {
    points: [
      -0.15, -0.3,
      0, 0,
      0.15, -0.3,
      0, -0.15
    ],

    gap: function( edge ){
      return edge.pstyle( 'width' ).pfValue;
    }
  } );

  defineArrowShape( 'circle', {
    radius: 0.15,

    collide: function( x, y, size, angle, translation, padding ){
      var t = translation;
      var inside = ( Math.pow( t.x - x, 2 ) + Math.pow( t.y - y, 2 ) <= Math.pow( (size + 2 * padding) * this.radius, 2 ) );

      return inside;
    },

    draw: function( context, size, angle, translation ){
      renderer.arrowShapeImpl( this.name )( context, translation.x, translation.y, this.radius * size );
    },

    spacing: function( edge ){
      return renderer.getArrowWidth( edge.pstyle( 'width' ).pfValue )
        * this.radius;
    }
  } );

  defineArrowShape( 'inhibitor', {
    points: [
      -0.15, 0,
      -0.15, -0.1,
      0.15, -0.1,
      0.15, 0
    ],

    spacing: function( edge ){
      return 1;
    },

    gap: function( edge ){
      return 1;
    }
  } );

  defineArrowShape( 'tee', 'inhibitor' );

  defineArrowShape( 'square', {
    points: [
      -0.15, 0.00,
      0.15, 0.00,
      0.15, -0.3,
      -0.15, -0.3
    ]
  } );

  defineArrowShape( 'diamond', {
    points: [
      -0.15, -0.15,
      0, -0.3,
      0.15, -0.15,
      0, 0
    ],

    gap: function( edge ){
      return edge.pstyle( 'width' ).pfValue;
    }
  } );

};

module.exports = BRp;
<commit_msg>Remove `inhibitor` edge arrow name.  It was included when Cy.js was a prototype, and `tee` has always been the real name.<commit_after>'use strict';

var math = require( '../../../math' );
var is = require( '../../../is' );
var util = require( '../../../util' );

var BRp = {};

BRp.arrowShapeWidth = 0.3;

BRp.registerArrowShapes = function(){
  var arrowShapes = this.arrowShapes = {};
  var renderer = this;

  // Contract for arrow shapes:
  // 0, 0 is arrow tip
  // (0, 1) is direction towards node
  // (1, 0) is right
  //
  // functional api:
  // collide: check x, y in shape
  // roughCollide: called before collide, no false negatives
  // draw: draw
  // spacing: dist(arrowTip, nodeBoundary)
  // gap: dist(edgeTip, nodeBoundary), edgeTip may != arrowTip

  var bbCollide = function( x, y, size, angle, translation, padding ){
    var x1 = translation.x - size / 2 - padding;
    var x2 = translation.x + size / 2 + padding;
    var y1 = translation.y - size / 2 - padding;
    var y2 = translation.y + size / 2 + padding;

    var inside = (x1 <= x && x <= x2) && (y1 <= y && y <= y2);

    return inside;
  };

  var transform = function( x, y, size, angle, translation ){
    var xRotated = x * Math.cos( angle ) - y * Math.sin( angle );
    var yRotated = x * Math.sin( angle ) + y * Math.cos( angle );

    var xScaled = xRotated * size;
    var yScaled = yRotated * size;

    var xTranslated = xScaled + translation.x;
    var yTranslated = yScaled + translation.y;

    return {
      x: xTranslated,
      y: yTranslated
    };
  };

  var transformPoints = function( pts, size, angle, translation ){
    var retPts = [];

    for( var i = 0; i < pts.length; i += 2 ){
      var x = pts[ i ];
      var y = pts[ i + 1];

      retPts.push( transform( x, y, size, angle, translation ) );
    }

    return retPts;
  };

  var pointsToArr = function( pts ){
    var ret = [];

    for( var i = 0; i < pts.length; i++ ){
      var p = pts[ i ];

      ret.push( p.x, p.y );
    }

    return ret;
  };

  var defineArrowShape = function( name, defn ){
    if( is.string( defn ) ){
      defn = arrowShapes[ defn ];
    }

    arrowShapes[ name ] = util.extend( {
      name: name,

      points: [
        -0.15, -0.3,
        0.15, -0.3,
        0.15, 0.3,
        -0.15, 0.3
      ],

      collide: function( x, y, size, angle, translation, padding ){
        var points = pointsToArr( transformPoints( this.points, size + 2 * padding, angle, translation ) );
        var inside = math.pointInsidePolygonPoints( x, y, points );

        return inside;
      },

      roughCollide: bbCollide,

      draw: function( context, size, angle, translation ){
        var points = transformPoints( this.points, size, angle, translation );

        renderer.arrowShapeImpl( 'polygon' )( context, points );
      },

      spacing: function( edge ){
        return 0;
      },

      gap: function( edge ){
        return edge.pstyle( 'width' ).pfValue * 2;
      }
    }, defn );
  };

  defineArrowShape( 'none', {
    collide: util.falsify,

    roughCollide: util.falsify,

    draw: util.noop,

    spacing: util.zeroify,

    gap: util.zeroify
  } );

  defineArrowShape( 'triangle', {
    points: [
      -0.15, -0.3,
      0, 0,
      0.15, -0.3
    ]
  } );

  defineArrowShape( 'arrow', 'triangle' );

  defineArrowShape( 'triangle-backcurve', {
    points: arrowShapes[ 'triangle' ].points,

    controlPoint: [ 0, -0.15 ],

    roughCollide: bbCollide,

    draw: function( context, size, angle, translation ){
      var ptsTrans = transformPoints( this.points, size, angle, translation );
      var ctrlPt = this.controlPoint;
      var ctrlPtTrans = transform( ctrlPt[0], ctrlPt[1], size, angle, translation );

      renderer.arrowShapeImpl( this.name )( context, ptsTrans, ctrlPtTrans );
    },

    gap: function( edge ){
      return edge.pstyle( 'width' ).pfValue;
    }
  } );

  defineArrowShape( 'triangle-tee', {
    points: [
      -0.15, -0.3,
      0, 0,
      0.15, -0.3,
      -0.15, -0.3
    ],

    pointsTee: [
      -0.15, -0.4,
      -0.15, -0.5,
      0.15, -0.5,
      0.15, -0.4
    ],

    collide: function( x, y, size, angle, translation, padding ){
      var triPts = pointsToArr( transformPoints( this.points, size + 2 * padding, angle, translation ) );
      var teePts = pointsToArr( transformPoints( this.pointsTee, size + 2 * padding, angle, translation ) );

      var inside = math.pointInsidePolygonPoints( x, y, triPts ) || math.pointInsidePolygonPoints( x, y, teePts );

      return inside;
    },

    draw: function( context, size, angle, translation ){
      var triPts = transformPoints( this.points, size, angle, translation );
      var teePts = transformPoints( this.pointsTee, size, angle, translation );

      renderer.arrowShapeImpl( this.name )( context, triPts, teePts );
    }
  } );

  defineArrowShape( 'vee', {
    points: [
      -0.15, -0.3,
      0, 0,
      0.15, -0.3,
      0, -0.15
    ],

    gap: function( edge ){
      return edge.pstyle( 'width' ).pfValue;
    }
  } );

  defineArrowShape( 'circle', {
    radius: 0.15,

    collide: function( x, y, size, angle, translation, padding ){
      var t = translation;
      var inside = ( Math.pow( t.x - x, 2 ) + Math.pow( t.y - y, 2 ) <= Math.pow( (size + 2 * padding) * this.radius, 2 ) );

      return inside;
    },

    draw: function( context, size, angle, translation ){
      renderer.arrowShapeImpl( this.name )( context, translation.x, translation.y, this.radius * size );
    },

    spacing: function( edge ){
      return renderer.getArrowWidth( edge.pstyle( 'width' ).pfValue )
        * this.radius;
    }
  } );

  defineArrowShape( 'tee', {
    points: [
      -0.15, 0,
      -0.15, -0.1,
      0.15, -0.1,
      0.15, 0
    ],

    spacing: function( edge ){
      return 1;
    },

    gap: function( edge ){
      return 1;
    }
  } );

  defineArrowShape( 'square', {
    points: [
      -0.15, 0.00,
      0.15, 0.00,
      0.15, -0.3,
      -0.15, -0.3
    ]
  } );

  defineArrowShape( 'diamond', {
    points: [
      -0.15, -0.15,
      0, -0.3,
      0.15, -0.15,
      0, 0
    ],

    gap: function( edge ){
      return edge.pstyle( 'width' ).pfValue;
    }
  } );

};

module.exports = BRp;
<|endoftext|>


<commit_before>
exports.Reader = Reader
exports.Writer = Writer

var fs = require("graceful-fs")
  , Stream = require("stream").Stream
  , inherits = require("inherits")
  , rimraf = require("rimraf")
  , mkdir = require("mkdirp")

function Reader (opts) {
  if (typeof opts === "string") opts = { path: opts }

  var me = this
  if (!(me instanceof Reader)) return new Reader(opts)
  Stream.call(me)
  me.readable = true
  me.writable = false

  if (!opts.path) {
    return me.emit("error", new Error("path option is required"))
  }

  var stat = opts.follow ? "stat" : "lstat"
  me.filter = typeof opts.filter === "function" ? opts.filter : null
  me.depth = opts.depth || 0
  me.parent = opts.parent || null
  me.path = opts.path

  fs[stat](me.path, function (er, props) {
    if (er) return me.emit("error", er)
    me.props = props
    me.type = getType(props)
    me.emit("stat", props)

    if (me.filter) {
      var ret = me.filter(props)
      if (!ret) return me.emit("end")
    }

    // if it's a directory, then we'll be emitting "file" events.
    if (props.isDirectory()) {
      dirWalk(me)
      return
    } else if (props.isSymbolicLink()) {
      readlink(me)
      return
    }

    // TODO: special handling for character devices, block devices,
    // FIFO's, sockets.  They stat as size=0, but emit all sorts of data.

    // if it's got a zero-size, then just emit "end" right now
    if (props.size === 0) {
      me.emit("end")
      return
    }

    // otherwise, set up a readable stream for it.
    // TODO: Check that the bytesEmitted at the end matches
    // the expected size.
    var s = me._stream = fs.createReadStream(me.path, opts)
    proxyEvents(["open", "error", "data", "end", "close"], s, me)
  })
}

inherits(Reader, Stream)

function readlink (me) {
  fs.readlink(me.path, function (er, lp) {
    if (er) return me.emit("error", er)
    me.emit("linkpath", lp)
    me.emit("end")
  })
}

function dirWalk (me) {
  fs.readdir(me.path, function (er, entries) {
    if (er) return me.emit("error", er)
    me.emit("entries", entries)

    // now, go through all the entries and create Readers for them
    // Use the same filter, if one was provided, so that we don't
    // dive into directories that are excluded.
    var len = entries.length
      , f = 0

    ;(function walk () {
      if (f === len) return me.emit("end")
      var fst = Reader({ path: path.resolve(me.path, entries[f])
                       , filter: me.filter
                       , depth: me.depth + 1
                       , parent: me })

      fst.on("error", function (e) {
        me.emit("error", e)
      })

      fst.on("props", function (p) {
        me.emit("entry", fst)
      })

      fst.on("end", function () {
        f ++
        walk()
      })
    })()
  })
}

function proxyEvents (evs, from, to) {
  evs.forEach(function (ev) {
    from.on(ev, function () {
      var l = arguments.length
        , args = new Array(l + 1)
      args[0] = ev
      for (var i = 0, l = arguments.length; i < l; i ++) {
        args[i + 1] = arguments[i]
      }
      to.emit.apply(to, args)
    })
  })
}

Reader.prototype.pipe = function (dest, opts) {
  if (this.type === "Directory" && typeof dest.add === "function") {
    // piping to a multi-compatible, and we've got directory entries.
    this.on("entry", function (entry) {
      dest.add(entry)
    })
  }

  Stream.prototype.pipe.apply(this, arguments)
}

Reader.prototype.pause = function () {
  if (this._stream) this._stream.pause()
}

Reader.prototype.resume = function () {
  if (this._stream) this._stream.resume()
}

function Writer (props) {
  var me = this
  if (!(me instanceof Writer)) return new Writer(props)
  Stream.call(me)

  if (typeof props === "string") {
    props = { path: props }
  }

  if (!props.path) {
    me.emit("error", new Error("Must provide a path"))
    return
  }

  me.props = props
  me.depth = props.depth || 0
  me.clobber = false === props.clobber ? props.clobber : true
  me.parent = props.parent || null
  me.path = props.path
  me.linkpath = props.linkpath || null

  me.readable = false
  me.writable = true

  // buffer until stat
  me._buffer = []
  me._ready = false

  var stat = props.follow ? "stat" : "lstat"

  fs[stat](props.path, function (er, current) {
    // if it's not there, great.  We'll just create it.
    // if it is there, then we'll need to change whatever differs
    // if we can't modify what needs modifying, then error on it.

    // try to figure out what kind of type it is.
    // We can actually only create directories, files, and symlinks,
    // so if it's a block/character device or fifo, then abort.
    // However, if it's a fifo or device or socket, and we're not
    // *changing* it, then that's fine, since we might just be writing
    // data into it.
    //
    // If it wants to be a directory or file or symlink, and is currently
    // anything else, then clobber it.
    var changeType = true
      , currentType = current ? getType(current) : null
      , wantedType = me.type = getType(props) || currentType || "File"
      , creatable = wantedType === "Directory" ||
                    wantedType === "File" ||
                    wantedType === "SymbolicLink"
      , recreate = wantedType !== currentType

    Object.keys(current || {}).forEach(function (k) {
      if (!props.hasOwnProperty(k)) props[k] = current[k]
    })

    me._old = current

    if (!recreate) {
      setProps(me, current)
      return
    }

    if (wantedType === "SymbolicLink" && !me.linkpath) {
      return me.emit("error", new Error(
        "Cannot create symlink without linkpath"))
    }

    if (!creatable) {
      return me.emit("error", new Error(
        "Cannot create filetype: " + wantedType))
    }

    clobber(me)
  })
}

inherits(Writer, Stream)

function clobber (me) {
  rimraf(me.path, function (er) {
    if (er) return me.emit("error", er)
    create(me)
  })
}

function create (me) {
  mkdir(path.dirname(me.path), me.mode & 0777, function (er) {
    if (er) return me.emit("error", me)

    switch (me.type) {
      case "Directory":
        mkdir(me.path, me.mode & 0777, next)
        break

      case "SymbolicLink":
        fs.symlink(me.path, me.linkpath, next)
        break

      case "File":
        me._stream = fs.createWriteStream(me.path, me.props)
        me._stream.on("open", function (fd) {
          next()
        })
        break

      default:
        return me.emit("error", new Error("Cannot create type: "+me.type))
    }
  })

  function next (er) {
    if (er) return me.emit("error", er)
    fs[me.follow ? "stat" : "lstat"](me.path, function (er, stat) {
      if (er) return me.emit("error", er)
      me._old = stat
      setProps(me, stat)
    })
  }
}

function setProps (me, current) {
  // set up all the things.
  var todo = 0
  var errState = null

  // mode
  var wantMode = me.props.mode
  if (typeof wantMode === "number") {
    wantMode = wantMode & 0777
    var curMode = current.mode & 0777
    if (wantMode !== curMode) {
      todo ++
      if (me._stream && me._stream.fd && fs.fchmod) {
        fs.fchmod(me._stream.fd, wantMode, next)
      } else {
        fs.chmod(me.path, wantMode, next)
      }
    }
  }

  // uid, gid
  if (typeof me.props.uid === "number" || typeof me.props.gid === "number") {
    if (typeof me.props.uid !== "number") me.props.uid = current.uid
    if (typeof me.props.gid !== "number") me.props.gid = current.gid
    if (me.props.uid !== current.uid || me.props.gid !== current.gid) {
      todo ++
      if (me._stream && me._stream.fd && fs.fchown) {
        fs.fchown(me._stream.fd, me.props.uid, me.props.gid, next)
      } else {
        fs.chown(me.path, me.props.uid, me.props.gid, next)
      }
    }
  }

  // atime, mtime.
  if (fs.utimes) {
    var curA = current.atime
      , curM = current.mtime
      , meA = me.props.atime
      , meM = me.props.mtime

    if (meA === undefined) meA = curA
    if (meM === undefined) meM = curM

    if (!isDate(meA)) meA = new Date(meA)
    if (!isDate(meM)) meA = new Date(meM)

    if (meA.getTime() !== curA.getTime() ||
        meM.getTime() !== curM.getTime()) {
      todo ++
      if (me._stream && me._stream.fd && fs.futimes) {
        fs.futimes(me._stream.fd, meA, meM, next)
      } else {
        fs.utimes(me.path, meA, meM, next)
      }
    }
  }

  // finally, handle the case if there was nothing to do.
  if (todo === 0) next()

  function next (er) {
    if (errState) return
    if (er) return me.emit("error", errState = er)
    if (--todo > 0) return

    // all the props have been set, now see if we got any pending writes
    // in the meantime.
    me._ready = true
    var buffer = me._buffer
    if (buffer.length) {
      me.buffer = []
      buffer.forEach(function (c) {
        me[c[0]](c[1])
      })
    }
  }
}

function isDate(d) {
  return typeof d === 'object' && objectToString(d) === '[object Date]';
}

Writer.prototype.pipe = function () {
  this.emit("error", new Error("Can't pipe from writable stream"))
}

Writer.prototype.write = function (c) {
  var me = this

  if (me._ended) {
    me.emit("error", new Error("write after end"))
    return false
  }

  if (!me._ready || me._buffer.length) {
    me._buffer.push(["write", c])
    me._needsDrain = true
    return false
  }

  if (me.type !== "File") {
    // just ignore writes to non-File types.
    // some tar entries contain metadata for directories
    return true
  }

  return me._stream.write(c)
}

// create a subfolder underneath this one
Writer.prototype.add = function (entry) {
  var me = this

  if (me._ended) {
    me.emit("error", new Error("add after end"))
    return false
  }

  if (!me._ready || me._buffer.length) {
    me._buffer.push(["add", entry])
    me._needsDrain = true
    return false
  }

  if (me._ended) {
    me.emit("error", new Error("write after end"))
    return false
  }

  if (me.type !== "Directory") {
    // can't add sub-entries to a non-dir type
    me.emit("error", new Error("can't add to non-Directory type"))
    return
  }

  entry.path = path.join(me.path, entry.path)
  entry.parent = me
  entry.depth = me.depth + 1

  entry = new Writer(entry)
  me.emit("entry", entry)
}

Writer.prototype.end = function (c) {
  var me = this
  if (c) me.write(c)
  if (me._stream) me._stream.end()
  me._ended = true
}

function getType (st) {
  var types =
      [ "Directory"
      , "File"
      , "SymbolicLink"
      , "BlockDevice"
      , "CharacterDevice"
      , "FIFO"
      , "Socket" ]
    , type

  for (var i = 0, l = types.length; i < l; i ++) {
    type = types[i]
    var is = st[type] || st["is" + type]
    if (typeof is === "function") is = is()
    if (is) {
      st[type] = true
      st.type = type
      return type
    }
  }

  return null
}
<commit_msg>Add basename and dirname, and bubble up entries<commit_after>
exports.Reader = Reader
exports.Writer = Writer

var fs = require("graceful-fs")
  , Stream = require("stream").Stream
  , inherits = require("inherits")
  , rimraf = require("rimraf")
  , mkdir = require("mkdirp")
  , path = require("path")

function Reader (opts) {
  if (typeof opts === "string") opts = { path: opts }

  var me = this
  if (!(me instanceof Reader)) return new Reader(opts)
  Stream.call(me)
  me.readable = true
  me.writable = false

  if (!opts.path) {
    return me.emit("error", new Error("path option is required"))
  }

  var stat = opts.follow ? "stat" : "lstat"
  me.filter = typeof opts.filter === "function" ? opts.filter : null
  me.depth = opts.depth || 0
  me.parent = opts.parent || null
  me.path = opts.path
  me.basename = path.basename(opts.path)
  me.dirname = path.dirname(opts.path)

  fs[stat](me.path, function (er, props) {
    if (er) return me.emit("error", er)
    me.props = props
    me.type = getType(props)

    // if the filter doesn't pass, then just skip over this one.
    // still have to emit end so that dir-walking can move on.
    if (me.filter) {
      var ret = me.filter()
      if (!ret) return me.emit("end")
    }

    me.emit("stat", props)

    // if it's a directory, then we'll be emitting "file" events.
    if (props.isDirectory()) {
      dirWalk(me)
      return
    } else if (props.isSymbolicLink()) {
      readlink(me)
      return
    }

    // TODO: special handling for character devices, block devices,
    // FIFO's, sockets.  They stat as size=0, but emit all sorts of data.

    // if it's got a zero-size, then just emit "end" right now
    if (props.size === 0) {
      me.emit("end")
      return
    }

    // otherwise, set up a readable stream for it.
    // TODO: Check that the bytesEmitted at the end matches
    // the expected size.
    var s = me._stream = fs.createReadStream(me.path, opts)
    proxyEvents(["open", "error", "data", "end", "close"], s, me)
  })
}

inherits(Reader, Stream)

function readlink (me) {
  fs.readlink(me.path, function (er, lp) {
    if (er) return me.emit("error", er)
    me.emit("linkpath", lp)
    me.emit("end")
  })
}

function dirWalk (me) {
  fs.readdir(me.path, function (er, entries) {
    if (er) return me.emit("error", er)
    me.emit("entries", entries)

    // now, go through all the entries and create Readers for them
    // Use the same filter, if one was provided, so that we don't
    // dive into directories that are excluded.
    var len = entries.length
      , f = 0

    ;(function walk () {
      if (f === len) return me.emit("end")
      var fst = Reader({ path: path.resolve(me.path, entries[f])
                       , filter: me.filter
                       , depth: me.depth + 1
                       , parent: me })

      fst.on("error", function (e) {
        me.emit("error", e)
      })

      fst.on("stat", function (p) {
        me.emit("entry", fst)
      })

      // bubble up
      fst.on("entry", function (entry) {
        me.emit("entry", entry)
      })

      fst.on("end", function () {
        f ++
        walk()
      })
    })()
  })
}

function proxyEvents (evs, from, to) {
  evs.forEach(function (ev) {
    from.on(ev, function () {
      var l = arguments.length
        , args = new Array(l + 1)
      args[0] = ev
      for (var i = 0, l = arguments.length; i < l; i ++) {
        args[i + 1] = arguments[i]
      }
      to.emit.apply(to, args)
    })
  })
}

Reader.prototype.pipe = function (dest, opts) {
  if (this.type === "Directory" && typeof dest.add === "function") {
    // piping to a multi-compatible, and we've got directory entries.
    this.on("entry", function (entry) {
      dest.add(entry)
    })
  }

  Stream.prototype.pipe.apply(this, arguments)
}

Reader.prototype.pause = function () {
  if (this._stream) this._stream.pause()
}

Reader.prototype.resume = function () {
  if (this._stream) this._stream.resume()
}

function Writer (props) {
  var me = this
  if (!(me instanceof Writer)) return new Writer(props)
  Stream.call(me)

  if (typeof props === "string") {
    props = { path: props }
  }

  if (!props.path) {
    me.emit("error", new Error("Must provide a path"))
    return
  }

  me.props = props
  me.depth = props.depth || 0
  me.clobber = false === props.clobber ? props.clobber : true
  me.parent = props.parent || null
  me.path = props.path
  me.basename = path.basename(opts.path)
  me.dirname = path.dirname(opts.path)
  me.linkpath = props.linkpath || null

  me.readable = false
  me.writable = true

  // buffer until stat
  me._buffer = []
  me._ready = false

  var stat = props.follow ? "stat" : "lstat"

  fs[stat](props.path, function (er, current) {
    // if it's not there, great.  We'll just create it.
    // if it is there, then we'll need to change whatever differs
    // if we can't modify what needs modifying, then error on it.

    // try to figure out what kind of type it is.
    // We can actually only create directories, files, and symlinks,
    // so if it's a block/character device or fifo, then abort.
    // However, if it's a fifo or device or socket, and we're not
    // *changing* it, then that's fine, since we might just be writing
    // data into it.
    //
    // If it wants to be a directory or file or symlink, and is currently
    // anything else, then clobber it.
    var changeType = true
      , currentType = current ? getType(current) : null
      , wantedType = me.type = getType(props) || currentType || "File"
      , creatable = wantedType === "Directory" ||
                    wantedType === "File" ||
                    wantedType === "SymbolicLink"
      , recreate = wantedType !== currentType

    Object.keys(current || {}).forEach(function (k) {
      if (!props.hasOwnProperty(k)) props[k] = current[k]
    })

    me._old = current

    if (!recreate) {
      setProps(me, current)
      return
    }

    if (wantedType === "SymbolicLink" && !me.linkpath) {
      return me.emit("error", new Error(
        "Cannot create symlink without linkpath"))
    }

    if (!creatable) {
      return me.emit("error", new Error(
        "Cannot create filetype: " + wantedType))
    }

    clobber(me)
  })
}

inherits(Writer, Stream)

function clobber (me) {
  rimraf(me.path, function (er) {
    if (er) return me.emit("error", er)
    create(me)
  })
}

function create (me) {
  mkdir(path.dirname(me.path), me.mode & 0777, function (er) {
    if (er) return me.emit("error", me)

    switch (me.type) {
      case "Directory":
        mkdir(me.path, me.mode & 0777, next)
        break

      case "SymbolicLink":
        fs.symlink(me.path, me.linkpath, next)
        break

      case "File":
        me._stream = fs.createWriteStream(me.path, me.props)
        me._stream.on("open", function (fd) {
          next()
        })
        break

      default:
        return me.emit("error", new Error("Cannot create type: "+me.type))
    }
  })

  function next (er) {
    if (er) return me.emit("error", er)
    fs[me.follow ? "stat" : "lstat"](me.path, function (er, stat) {
      if (er) return me.emit("error", er)
      me._old = stat
      setProps(me, stat)
    })
  }
}

function setProps (me, current) {
  // set up all the things.
  var todo = 0
  var errState = null

  // mode
  var wantMode = me.props.mode
  if (typeof wantMode === "number") {
    wantMode = wantMode & 0777
    var curMode = current.mode & 0777
    if (wantMode !== curMode) {
      todo ++
      if (me._stream && me._stream.fd && fs.fchmod) {
        fs.fchmod(me._stream.fd, wantMode, next)
      } else {
        fs.chmod(me.path, wantMode, next)
      }
    }
  }

  // uid, gid
  if (typeof me.props.uid === "number" || typeof me.props.gid === "number") {
    if (typeof me.props.uid !== "number") me.props.uid = current.uid
    if (typeof me.props.gid !== "number") me.props.gid = current.gid
    if (me.props.uid !== current.uid || me.props.gid !== current.gid) {
      todo ++
      if (me._stream && me._stream.fd && fs.fchown) {
        fs.fchown(me._stream.fd, me.props.uid, me.props.gid, next)
      } else {
        fs.chown(me.path, me.props.uid, me.props.gid, next)
      }
    }
  }

  // atime, mtime.
  if (fs.utimes) {
    var curA = current.atime
      , curM = current.mtime
      , meA = me.props.atime
      , meM = me.props.mtime

    if (meA === undefined) meA = curA
    if (meM === undefined) meM = curM

    if (!isDate(meA)) meA = new Date(meA)
    if (!isDate(meM)) meA = new Date(meM)

    if (meA.getTime() !== curA.getTime() ||
        meM.getTime() !== curM.getTime()) {
      todo ++
      if (me._stream && me._stream.fd && fs.futimes) {
        fs.futimes(me._stream.fd, meA, meM, next)
      } else {
        fs.utimes(me.path, meA, meM, next)
      }
    }
  }

  // finally, handle the case if there was nothing to do.
  if (todo === 0) next()

  function next (er) {
    if (errState) return
    if (er) return me.emit("error", errState = er)
    if (--todo > 0) return

    // all the props have been set, now see if we got any pending writes
    // in the meantime.
    me._ready = true
    var buffer = me._buffer
    if (buffer.length) {
      me.buffer = []
      buffer.forEach(function (c) {
        me[c[0]](c[1])
      })
    }
  }
}

function isDate(d) {
  return typeof d === 'object' && objectToString(d) === '[object Date]';
}

Writer.prototype.pipe = function () {
  this.emit("error", new Error("Can't pipe from writable stream"))
}

Writer.prototype.write = function (c) {
  var me = this

  if (me._ended) {
    me.emit("error", new Error("write after end"))
    return false
  }

  if (!me._ready || me._buffer.length) {
    me._buffer.push(["write", c])
    me._needsDrain = true
    return false
  }

  if (me.type !== "File") {
    // just ignore writes to non-File types.
    // some tar entries contain metadata for directories
    return true
  }

  return me._stream.write(c)
}

// create a subfolder underneath this one
Writer.prototype.add = function (entry) {
  var me = this

  if (me._ended) {
    me.emit("error", new Error("add after end"))
    return false
  }

  if (!me._ready || me._buffer.length) {
    me._buffer.push(["add", entry])
    me._needsDrain = true
    return false
  }

  if (me._ended) {
    me.emit("error", new Error("write after end"))
    return false
  }

  if (me.type !== "Directory") {
    // can't add sub-entries to a non-dir type
    me.emit("error", new Error("can't add to non-Directory type"))
    return
  }

  entry.path = path.join(me.path, entry.path)
  entry.parent = me
  entry.depth = me.depth + 1

  entry = new Writer(entry)
  me.emit("entry", entry)
}

Writer.prototype.end = function (c) {
  var me = this
  if (c) me.write(c)
  if (me._stream) me._stream.end()
  me._ended = true
}

function getType (st) {
  var types =
      [ "Directory"
      , "File"
      , "SymbolicLink"
      , "BlockDevice"
      , "CharacterDevice"
      , "FIFO"
      , "Socket" ]
    , type

  for (var i = 0, l = types.length; i < l; i ++) {
    type = types[i]
    var is = st[type] || st["is" + type]
    if (typeof is === "function") is = is.call(st)
    if (is) {
      st[type] = true
      st.type = type
      return type
    }
  }

  return null
}
<|endoftext|>